IdentityServer4 handle multiple facebook AppIds for tenancy - asp.net-core

We are adding tenancy to our IdentityServer4 (https://github.com/IdentityServer/IdentityServer4) implementation. So far everything is good. AspIdentity appends a prefix to the username based on the hostname on the request ie. sso.domain1.com and sso.domain2.com create tenancy in the identity database. The external oauth for google works fine as well as google's API console allows multiple websites to access the same AppId. Facebook, on the other hand, only allows one domain per AppId. The external providers are registered during the application startup so this presents a problem as we need to determine the correct Facebook AppId to use per request based on the hostname.
Any suggestions on the appropriate way to handle this scenario? I tried having all Facebook AppIds registered at startup and letting the login page UI determine which Facebook button to make visible. IdentityServer threw an exception for this as it doesn't allow multiple providers with the same scheme name.
Is there somewhere in the pipeline we could overload to pass in the Request host and change the External Provider AppId per request?
Update 1:
Based on McGuireV10 answer I was able to get closer to the goal. The issue now is that in the event I can set the ClientId and ClientSecret option properties, but that doesn't change the Uri that was generated for the RequestUri property. Should I be doing it a different way or do I need to rebuild the context so it regenerates the RedirectUri? I've been trying to go through Microsoft's Security source code, but haven't been able to find this yet. Ideas?
services.AddAuthentication().AddFacebook(externalAuthentication.Name, options => {
options.SignInScheme = externalAuthentication.SignInScheme;
options.ClientId = externalAuthentication.DefaultClientId;
options.ClientSecret = externalAuthentication.DefaultClientSecret;
options.RemoteAuthenticationTimeout = TimeSpan.FromMinutes(5);
options.Events = new Microsoft.AspNetCore.Authentication.OAuth.OAuthEvents {
OnRedirectToAuthorizationEndpoint = context => {
var tenancySetting = GetExternalAuthProviderForRequest(context.Request, externalAuthentication);
if (tenancySetting != null) {
context.Options.ClientId = tenancySetting.ClientId;
context.Options.ClientSecret = tenancySetting.ClientSecret;
}
context.RedirectUri = BuildChallengeUrl(context);
context.HttpContext.Response.Redirect(context.RedirectUri);
return Task.FromResult(0);
}
};
});
Update 2:
It is working now. I'm sure there must be a better way to do it, but I took the easy way out for now. I grabbed Microsoft's source code (https://github.com/aspnet/Security) and after looking through that I'm pretty sure that the HandleChallengeAsync method (Microsoft.AspNetCore.Authentication.OAuth.OAuthHandler) is being called in the pipeline prior to entering the RedirectToAuthorizationEndpoint event. HandleChallegeAsync takes care of building the RedirectUri property on the context. There doesn't seem to be an existing method to rebuild the RedirectUri in Microsoft's code so I copied out their code for BuildChallegeUrl and used that to rebuild the RedirectUri. I updated the sample code to reflect this change.

Try adding an OpenIdConnectEvents handler for the OnRedirectToIdentityProvider event and replace the ClientId and ClientSecret properties there, but I don't know if that will confuse Identity Server in some way. I don't have a similar use-case so I haven't tried this specific thing myself, but I intercept other events and change the protocol properties without any problems. I also don't know if you'd still need to set the id and secret on the options property itself, but that's easy enough to test. It would look something like this:
services.AddAuthentication()
.AddFacebook("Facebook", options =>
{
options.Events = new OpenIdConnectEvents
{
OnRedirectToIdentityProvider = context =>
{
context.ProtocolMessage.ClientId = "abc";
context.ProtocolMessage.ClientSecret = "xyz";
return Task.FromResult<object>(null);
}
};
options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme;
// you probably don't need these:
options.ClientId = oauth2config["FacebookId"];
options.ClientSecret = oauth2config["FacebookSecret"];
});
Obviously you'd need something more complex and implementation-specific to actually figure out the client etc.

Related

Adding a field to claim for Azure AD in ASP.NET Core 6

My MVC web application previously used cookie based authentication handled by identity framework and usernames and passwords stored in my db. However, I have since moved to Azure AD for authentication and it is working great however I use a field in my database for each user called InternalUserNumber and this is used in many stored procedures and table valued functions called from my web application's dapper queries. So on authenticating to Azure AD the user is redirected to the homepage which on load I was planning to run a sql query using the preferred_username (email address) against our database to get the UserID and I was going to store it as a session variable which I can use later in my code. However, this got me thinking I use to store the UserID as a claim in the cookie as follows:
new Claim(ClaimTypes.Name, user.InternalUserNumber.ToString())
Is this still doable using the Azure AD cookie which is generated after logging in? I do not have access to the admin panel in Azure AD so this has to be all code based. Or is my only option storing this in the session as a variable and if so whats the best route for that? I would note i do plan to implement SignalR which I read you cannot use session if you are going to use SignalR so theres that potential issue for that route.
Thanks
Edit :
I have actually added some code which uses an event passed in program.cs. I was trying to implement a service to do the following code but I failed to be able to figure it out but instead put directly inside the event code as shown below. My issue is I cannot access the preferred_username in HttpContext object as it returns null? I am able to use HttpContext in my controllers so why am I not able to use it here in Program.CS during this event being triggered?
builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(options =>
{
builder.Configuration.Bind("AzureAd", options);
options.Events = new OpenIdConnectEvents
{
OnTokenValidated = context =>
{
//var service = context.HttpContext.RequestServices.GetRequiredService<ISomeService>();
//var result = await service.DoSomethingAsync();
string query = "select dbo.A2F_0013_ReturnUserIDForEmail(#Email) as UserID";
string connectionString = builder.Configuration.GetValue<string>("ConnectionStrings:DBContext");
string signInEmailAddress = context.HttpContext.User.FindFirstValue("preferred_username");
using (var connection = new SqlConnection(connectionString))
{
var queryResult = connection.QueryFirst(query, new { Email = signInEmailAddress });
// add claims
var claims = new List<Claim>
{
new Claim("UserID", queryResult.UserID.ToString())
};
var appIdentity = new ClaimsIdentity(claims);
context.Principal.AddIdentity(appIdentity);
}
return Task.CompletedTask;
},
};
})
.EnableTokenAcquisitionToCallDownstreamApi(initialScopes)
.AddMicrosoftGraph(builder.Configuration.GetSection("DownstreamApi"))
.AddInMemoryTokenCaches();

ASP.NET Core : Return Json response on Unauthorized in a filter at the controller/action level

I am not using Identity.
I have this ASP.NET Core configuration enabling two authentication schemes, cookies and basic auth:
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(options =>
{
options.Cookie.Name = "_auth";
options.Cookie.HttpOnly = true;
options.LoginPath = new PathString("/Account/Login");
options.LogoutPath = new PathString("/Account/LogOff");
options.AccessDeniedPath = new PathString("/Account/Login");
options.ExpireTimeSpan = TimeSpan.FromHours(4);
options.SlidingExpiration = true;
})
.AddScheme<AuthenticationSchemeOptions, BasicAuthenticationHandler>("BasicAuthentication", null);
BasicAuthenticationHandler is a custom class inheriting from AuthenticationHandler and overriding HandleAuthenticateAsync to check the request headers for basic authentication challenge, and returns either AuthenticateResult.Fail() or AuthenticateResult.Success() with a ticket and the user claims.
It works fine as is:
Controllers/Actions with the [Authorize] tag will check the cookies and redirect to the login page is not present.
Controllers/Actions with the [Authorize(AuthenticationSchemes = "BasicAuthentication")] tag will check the header and reply a 401 Unauthorized HTTP code if not present.
Controllers/Actions with the [Authorize(AuthenticationSchemes = "BasicAuthentication,Cookies")] tag will allow both methods to access the page, but somehow use the Cookies redirection mechanism when failing both checks.
My goal is to have most of my project to use Cookies (hence why it is set as default), but have some API type of controllers to accept both methods. It should also be possible to tag the Controllers/Actions to return a specific Json body when desired (as opposed to the login redirect or base 401 response), but only for certain controllers.
I've spent the last 2 days reading different similar questions and answers here on StackOverflow, nothing seems to accommodate my need.
Here's a few methods I found:
The options under AddCookie allow you to set certain events, like OnRedirectToAccessDenied and change the response from there. This does not work because it applies to the whole project.
Under my BasicAuthenticationHandler class, the AuthenticationHandler class allow to override HandleChallengeAsync to change the response from there instead of replying 401. Unfortunately, again it applies globally to everywhere you use the scheme, not on a controller/action level. Not sure if it's applied when mixing multiple schemes either.
Many answers point to adding a Middleware to the solution, again, it impacts the whole project.
Many answers point to Policies, but it seems to be to control whether or not an user have access to the resource based on claims, not controlling the response when he do not.
Many answers suggest creating a class inheriting from AuthorizeAttribute, IAuthorizationFilter. Again, this allow to override the OnAuthorization method to decide if the user have the right or not to access the resource, but not to control the response AFTER the normal authentication scheme failed.
I'm thinking either there's a filter type I'm missing, or maybe I need to create a third authentication type that will mix the previous two and control the response from there. Finding a way to add a custom error message in the options would also be nice.
I managed to do it via a IAuthorizationMiddlewareResultHandler. Not my favorite approach because there can be only one per project and it intercepts all calls, but by checking if a specific (empty) attribute is set, I can control the flow:
public class JsonAuthorizationAttribute : Attribute
{
public string Message { get; set; }
}
public class MyAuthorizationMiddlewareResultHandler : IAuthorizationMiddlewareResultHandler
{
private readonly AuthorizationMiddlewareResultHandler DefaultHandler = new AuthorizationMiddlewareResultHandler();
public async Task HandleAsync(RequestDelegate requestDelegate, HttpContext httpContext, AuthorizationPolicy authorizationPolicy, PolicyAuthorizationResult policyAuthorizationResult)
{
// if the authorization was forbidden and the resource had specific attribute, respond as json
if (policyAuthorizationResult.Forbidden)
{
var endpoint = httpContext.GetEndpoint();
var jsonHeader = endpoint?.Metadata?.GetMetadata<JsonAuthorizationAttribute>();
if (jsonHeader != null)
{
var message = "Invalid User Credentials";
if (!string.IsNullOrEmpty(jsonHeader.Message))
message = jsonHeader.Message;
httpContext.Response.StatusCode = 401;
httpContext.Response.ContentType = "application/json";
var jsonResponse = JsonSerializer.Serialize(new
{
error = message
});
await httpContext.Response.WriteAsync(jsonResponse);
return;
}
}
// Fallback to the default implementation.
await DefaultHandler.HandleAsync(requestDelegate, httpContext, authorizationPolicy, policyAuthorizationResult);
}
}
I was typing this on comment... but it's doesn't fit... so here is something we probably need to make clear before choosing a solution:
Authorization process happen at the upper middleware above controller
Yes, AuthorizationMiddleware was registered when we use app.UseAuthorization();, that quite far above controller layer, so it was returned long before the request can reach controller, so, any type of filter cannot be applied here.
Not specify an authentication scheme or policy would easily lead to un-stable behavior.
Imagine, Authentication process return an instance of User that stick with the request, but what would happen if the permission on cookie and basicAuth was difference, like cookie have myclaim, while basicAuth doens't ? Related process on both type of scheme was difference (like challenge on cookie would lead to /Account/Login and basicAuth to /Login ?). And various logic case that we could implement on each page.
I Know, this is not possible, but it would become a mess, not for the author of these code, but for those maintainers to come.
Json response for some specific process on client ?
This might sound detailed at first glance, but it would rather become burden soon, if some more authentication requirement raise after that (like Jwt). Covering each of these case on client would make user experience quite awkward (like, half-authentication and authorization).
And if It's un-avoidable in the project. Might I suggest create a default authentication scheme with ForwardDefaultSelector that would elected which authentication scheme to use for each request. And maintain a stable routing HashSet that would use to detect on which endpoint to set Json Response as wished on some upper level than AuthorizationMiddleware, by using middleware, ofcourse. Then, we narrow down to 2 centralize places to checkout the authorization.
Chaos came when we tried to make one thing to do somethings. At least in this case, I think we would breath easier when coming to debug phase.

multitenant not loading parameters

this is my code and it is not working:
services.AddMultiTenant<SampleTenantInfo>()
.WithConfigurationStore()
.WithRouteStrategy()
.WithPerTenantAuthentication()
.WithPerTenantOptions<OpenIdConnectOptions>((options, tenantInfo) => {
//options.ResponseType = tenantInfo.ResponseType;
options.Authority = tenantInfo.OpenIdConnectAuthority;
options.ClientId = tenantInfo.OpenIdConnectClientId;
options.ClientSecret = tenantInfo.OpenIdConnectClientSecret;
options.ResponseType = "code";
});
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie()
.AddOpenIdConnect(options =>
{
options.Prompt = "login consent"; // For sample purposes.
});
what I'm trying to solve: this is meant to be an application that allows multiple logins but one per user, so I have several tenants, and the user is meant to select one of them and then the idea is that there is an openidconnect that is set per tenant, so the user gets redirected to login to the selected tenant.
What I have done:
in the beginning, my code didnot include the withpertenantoptions, and I could only login in one of the tenants, but not in the other one, debugging it I saw that ResponseType was not set, so I added withpertenantoptions, to set up just the response type but the rest of the parameters were not loaded, then I added the rest of the parameters depending on the tenant but they are still coming null. They are kept in a appsettings.file.
I am using Finbuckle to deal with the multitenant
The problem was simple, I was setting up oidc options twice, so I was taking the bad ones, solution is to remove {
options.Prompt = "login consent"; // For sample purposes.
}
and then set up everything in the other options

Implementing roles with custom storage provider for Identity

Recently, I was able to successfully implement authentication (SSO with ADFS using WS-Federation) for an app. Now, I am trying to understand and get authorization working, so this question may be unclear.
I'm using this topic to implement roles with custom storage provider for Identity without entity framework.
I've got custom User and Role models set up, along with the custom UserStore and RoleStore that implement the appropriate interfaces. There's also tables for roles ready to be used.
I run into issues when trying to access either an [Authorized] or [Authorized(Roles = "RoleName")]. As expected, the actions require me to authenticate with ADFS, but when I submit correct credentials the login loops a few times and displays the ADFS error page. This problem with ADFS is not present without the role implementation. UserStore and RoleStore does not implement code yet, but the app never tries uses any of their methods.
I tried implementing different options in Startup.cs, some of which I have commented out, and reordering services. Inserting dummy code into the RoleStore didn't help either. Basically, I just want to be able to add role checks from custom storage using Identity. I can get the username of the user at any time after they log in to find their role.
Startup.cs ConfigureServices method is where it's most unclear for me, and probably the most likely place where something is set up incorrectly.
Startup.cs ConfigureServices():
public void ConfigureServices(IServiceCollection services)
{
// Add identity types
services.AddIdentity<User, Role>()
//.AddUserManager<UserManager<User>>() // some other settings I've tried ...
//.AddRoleManager<RoleManager<Role>>()
//.AddUserStore<UserStore>()
//.AddRoleStore<RoleStore>()
//.AddRoles<Role>()
.AddDefaultTokenProviders();
// Identity Services
services.AddTransient<IUserStore<User>, UserStore>();
services.AddTransient<IRoleStore<Role>, RoleStore>();
//for SQL connection, I'll be using a different one (not the one from the link to topic)
//dependency injection
services.AddScoped<ISomeService, SomeService>();
services.AddAuthentication(sharedOptions =>
{
// authentication options...
})
.AddWsFederation(options =>
{
// wsfed options...
})
.AddCookie(options =>
{
options.Cookie.Name = "NameOfCookie";
//options.LoginPath = "/Access/Login"; //app function the same without this
options.LogoutPath = "/Access/Logout";
options.AccessDeniedPath = "/Access/AccessDenied";
options.ExpireTimeSpan = TimeSpan.FromMinutes(120);
options.SlidingExpiration = true;
});
services.AddControllersWithViews();
}
Another way of doing this is to add a custom attribute store to ADFS.
Then the roles etc. that you require from the custom attribute store can be configured as claims.

Getting access token within Claims Transformer in ASP.NET Core

I'm developing a set of applications including an Identity Server using IdentityServer4, a .NET Core MVC app, a .NET Core WebAPI.
As part of a asp.net core mvc application I am using AddOpenIdConnect to do authentication and doing options.SaveTokens = true.
However, as part of Claims Transformation, in TransformAsync I would like to be able to have access to the access token provided by the identityserver. This is to be able to call a permissions endpoint on the api to populate the principal with claims that I can use to do authorization on my controllers etc.
If I call HttpContext.GetTokenAsync("access_token") I get a stackoverflowexception due to the infinite loop created by authenticate being called, which then calls TransformAsync again.
Is this a sound approach in the first place? Typically, TransformAsync is where I would populate application permissions. Is there any way of accessing the token without triggering the authenticate again?
Would appreciate any help as we're a bit stumped! Thanks
Edit: I've seen suggestions around doing transformations in the OnTicketReceived
event. It looks like I'd have access to the token through the properties in there. Is this a better place to do it?
I came across the same problem. My solution was,
Override JwtBearerEvents.TokenValidated event called by IdentityServer4.AccessTokenValidation middleware.
private Task OnTokenValidated(TokenValidatedContext tokenValidatedContext)
{
tokenValidatedContext.HttpContext.Items["access_token"] = (tokenValidatedContext.SecurityToken as JwtSecurityToken).RawData;
return Task.CompletedTask;
}
This will utilize HttpContext.Items collection which is request scoped. Now you can retreive this access token in TransformAsync method, like below.
var access_token = _httpContextAccessor.HttpContext.Items["access_token"] as string;
Please note that you need to inject IHttpContextAccessor to access HttpContext in ClaimsTransformer.
It has been many years since this question was posted, but if you are still looking for a solution to the issue, you can get the access token in the OnTokenValidated event.
OnTokenValidated = tokenValidatedContext =>
{
var handler = new JwtSecurityTokenHandler();
// get access token
var jsonToken = handler.ReadJwtToken(tokenValidatedContext.TokenEndpointResponse.AccessToken);
var claims = new List<Claim>();
claims.Add(new Claim("customClaimType", "customClaimValue"));
var appIdentity = new ClaimsIdentity(claims);
tokenValidatedContext.Principal.AddIdentity(appIdentity);
return Task.CompletedTask;
}
Reference : Adding Custom Claims During Authentication
I think you can inject the IAuthenticationHandlerProvider service and use following:
Get the authentication handler by scheme name.
get the AuthenticateResult by invoking AuthenticateAsync
get the token from the authentication properties
var token = string.Empty;
var handler = await Handlers.GetHandlerAsync(context, scheme); // i.e. "OIDC"
var result = await handler.AuthenticateAsync();
if(result?.Succeeded == true) {
token = result?.Properties?.GetTokenValue(tokenName);
}
haven't tested it but i think it should work