multitenant not loading parameters - asp.net-core

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

Related

How to make sure user authenticates in Blazor when navigating to a page, but redirecting to that page after sign in?

I have successfully set up Azure ADB2C authentication with my Server-Blazor app. Users can successfully login, but after that login, they are currently always redirected to the same (pre-configured) page/url.
What I actually want now, is the following logic:
A user navigates to a page (/registrationwizard/02, for example).
When the user is already signed in, the logic in that page just shows whatever is needed (I can do that with AuthorizeView)
But when the user is not yet signed in, the user should be redirected to the login logic, I currently do that with a <a href='MicrosoftIdentity/Account/SignIn'>sign in</a> link.
And after that sign in logic, the user should be returned back to the requested original page (/registrationwizard/02), and not the preconfigured page in the startup logic. (signin-oidc)
I did some searches, but cannot seem to find this (while I believe this should be rather straight forward?)
This is the (I believe) relevant part of my startup logic:
(have been trying to fiddle with the ReturnUrlParameter parameter as well)
builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(
options =>
{
builder.Configuration.Bind("AzureAdB2C", options);
options.ResponseType = OpenIdConnectResponseType.CodeIdToken;
options.Scope.Add("https://xxx.onmicrosoft.com/api/data");
options.UseTokenLifetime = true;
options.SaveTokens = true;
// Played around with this parameter too
options.ReturnUrlParameter = "returnUrl";
}
,
options =>
{
options.Cookie.SameSite = SameSiteMode.None;
options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
options.Cookie.IsEssential = true;
}
);
builder.Services.AddControllersWithViews()
.AddMicrosoftIdentityUI();
Thanks
Revised Intermediate Answer
You are correct on ReturnUrlParameter. You also need to add the AccessDeniedPath option.
I've added them to AppSettings:
"ReturnUrlParameter": "custompath",
"AccessDeniedPath": "/"
You should then be able to manually do this:
https://localhost:5001/MicrosoftIdentity/account/signin?custompath=%2fcounter
However, it doesn't appear to work. I have a project that I'm updating next month moving from custom to AzureADB2C authentication and it will have the same problem!
I've raised an issue with the Aspnetcore team on Github. It's probably a documentation issue.
Issue: https://github.com/dotnet/aspnetcore/issues/45024

ASP.NET Core 3.1 web application use authorization for multiple areas using different authentication types

I have an ASP.NET Core 3.1 application which follows domain driven architecture and it has 2 areas, one for admin and other one for customers (application users).
I want to enable authentication and authorization for each area separately. For example use Identity 4 for the customer area and cookie base authentication for admin area. But it should be done using a single database and role base authentication should not used to separate areas.
What is the best approach to follow. For example "Multiple authentication scheme", Or any other method.
When it comes to login for admin and customer you can implement it by using acr_values (see definition in spec). Identity server can decide how to authenticate based on acr_values, for example if you provided admin_login as acr_values, then based on that Identity Server will authenticate user (use different identity provider or different database/table).
Your application needs to know whether user wants to login as customer or admin before you redirect to identity server authorize endpoint. In order to know that you will have to implement different authentication schemes in your application (one for admin and one for customer). Once you know user login type, you can add correct acr_values. Below code is not tested but it should give you an idea on how to implement it.
services.AddAuthentication(options =>
{
options.DefaultScheme = "CustomerCookie";
options.DefaultChallengeScheme = "oidc";
})
.AddCookie("CustomerCookie", options =>
{
options.Cookie.Name = "CustomerCookie";
options.ForwardChallenge = "oidc";
})
.AddCookie("AdminCookie", options =>
{
options.Cookie.Name = "AdminCookie";
options.ForwardChallenge = "admin-oidc";
})
.AddOpenIdConnect("oidc", options =>
{
// Configure all other options needed.
options.SignInScheme = "CustomerCookie";
options.CallbackPath = "/signin-oidc-customer";
options.Events.OnRedirectToIdentityProvider = (context) =>
{
context.ProtocolMessage.SetParameter("acr_values", "customer_login");
return Task.FromResult(0);
};
})
.AddOpenIdConnect("admin-oidc", options =>
{
// Configure all other options needed.
options.SignInScheme = "AdminCookie";
options.CallbackPath = "/signin-oidc-admin";
options.Events.OnRedirectToIdentityProvider = (context) =>
{
context.ProtocolMessage.SetParameter("acr_values", "admin_login");
return Task.FromResult(0);
};
});
On identity server side you have full control on what to do based on acr_values, you can use external provider for admin.
You could use IIdentityServerInteractionService.GetAuthorizationContextAsync to retrieve acr_values and you could implement IProfileService so that once authenticated, you can decide what claims to include based on user type (admin or customer).
That would be the basic idea, hopefully it is useful.

Hide newly released application temporarly for users in PROD

We use SSO for autentication of our users. Now we have released a new application only for pilot-testers to our production environment which uses SSO as well. The problem is if other users know the URL could log on to the new application, if they are already logged on to one of our applications.
How do we solve this that only pilot-testers can log on into the application?
What you should do is short-circuit the pipeline when an invalid or unknown user wants to access the application. You can accomplish this with middleware or by adding a filter to the authorization component.
The easiest way may be to use Claim-based authorization for that. You'll only need to add a policy that looks for the presence of a claim.
The startup of the client could look something like this:
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc(options =>
{
// this sets up a default authorization policy for the application
// in this case, authenticated users are required
// (besides controllers/actions that have [AllowAnonymous])
var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.RequireClaim("http://mynewapp.com/pilot-tester")
.Build();
options.Filters.Add(new AuthorizeFilter(policy));
});
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
services.AddAuthentication(options =>
{
options.DefaultScheme = "Cookies";
options.DefaultChallengeScheme = "oidc";
})
.AddCookie("Cookies")
.AddOpenIdConnect("oidc", options =>
{
options.SignInScheme = "Cookies";
options.GetClaimsFromUserInfoEndpoint = true;
options.ClaimActions.MapAll();
options.Scope.Add("mynewapp");
options.Authority = "http://localhost:5000";
options.RequireHttpsMetadata = false;
options.ClientId = "mvc";
options.ClientSecret = "secret";
options.ResponseType = "code id_token";
});
}
This will only grant access to pilot-testers. Please note that all code where the AllowAnonymous attribute is used, still will be available for everybody!
If you want to prevent access to these methods then you'll need to check the user with code, e.g.:
if (User.Identity.IsAuthenticated &&
!User.HasClaim(c => c.Type == "http://mynewapp.com/pilot-tester"))
return Redirect("...");
How to configure IdentityServer:
When your app only is a website without other api's, then you'll need to add the claim to the Identity.
In the database make sure the following records are added (the values are examples):
AspNetUserClaims - add a claim for each user that is a pilot-tester. The type should be something you can use for the filter, like http://mynewapp.com/pilot-tester and value true.
IdentityResources - mynewapp. Corresponds with the requested scope.
IdentityClaims - http://mynewapp.com/pilot-tester (linked to IdentityResource mynewapp).
How this works:
The user is a resource with claims. In order to keep tokens small the claims are filtered by the claims that are part of the requested scopes: openid, profile and mynewapp.
All claims that match by type are included to the User.Identity.Claims collection, that is being used when testing the policy.
If you are using an API then you should protect that resource as well. Add a record to ApiResources Api1. The client application should request the scope:
options.Scope.Add("api1");
Please note that in this case ApiResource and ApiScope have the same name. But the relation between ApiResource and ApiScope is 1:n.
Add a record to the ApiClaims table (or ApiScope to narrow it):
ApiClaims - http://mynewapp.com/pilot-tester (linked to ApiResource Api1).
The user resource remains the same, but now IdentityServer will add the claim to the access token as well. Register the policy in the api in the same way as above.
Being temporary you may want to make the filters conditional, giving you the option to enable / disable the filter.
But you may not have to code at all. Being behind a proxy means that you can look at the filter options there first. You may want to filter on ip adress. This means that you can grant access to everybody from certain ip addresses, without having to change the application.

IdentityServer4 authenticate each client separately

I use two different clients. The IdentityServer4 provides API protections and log in form. Can I configure clients to avoid single sign on. I mean that even if I logged in the first client I need to log in the second client too.
My ID4 configuration:
internal static IEnumerable<Client> GetClients(IEnumerable<RegisteredClient> clients)
{
return clients.Select(x =>
{
var scopes = x.AllowedScopes.ToList();
scopes.Add(IdentityServerConstants.StandardScopes.OpenId);
scopes.Add(IdentityServerConstants.StandardScopes.Profile);
scopes.Add(IdentityServerConstants.StandardScopes.OfflineAccess);
var client = new Client
{
ClientId = x.Id,
ClientName = x.Name,
AllowedGrantTypes = GrantTypes.Hybrid,
RequireConsent = false,
RefreshTokenExpiration = TokenExpiration.Sliding,
RefreshTokenUsage = TokenUsage.ReUse,
ClientSecrets = {new Secret(x.Secret.Sha256())},
RedirectUris = new[] {$"{x.Url}/signin-oidc"},
PostLogoutRedirectUris = new[] {$"{x.Url}/signout-callback-oidc"},
UpdateAccessTokenClaimsOnRefresh = true,
AllowAccessTokensViaBrowser = true,
AllowedScopes = scopes,
AllowedCorsOrigins = {x.Url},
AllowOfflineAccess = true
};
return client;
});
}
All client have the same register code (Maybe it is a problem):
const string oidcScheme = "oidc";
const string coockieScheme = CookieAuthenticationDefaults.AuthenticationScheme;
services.AddAuthentication(options =>
{
options.DefaultScheme = coockieScheme;
options.DefaultChallengeScheme = oidcScheme;
})
.AddCookie(coockieScheme)
.AddOpenIdConnect(oidcScheme, options =>
{
options.SignInScheme = coockieScheme;
options.Authority = identitySettings.Authority;
options.RequireHttpsMetadata = false;
options.ClientId = identitySettings.Id;
options.ClientSecret = identitySettings.Secret;
options.ResponseType = "code id_token";
options.Scope.Add("offline_access");
foreach (var scope in identitySettings.Scopes)
{
options.Scope.Add(scope);
}
options.GetClaimsFromUserInfoEndpoint = true;
options.SaveTokens = true;
});
any help will be useful.
As long as you are in the same browser session, and your apps are having the same authority (are using the same Identity Server) this will not work.
I'll explain you why - once you log in from the first client, Identity Server creates a cookie (with all the relevant data needed in it).
Now comes the second client - the authority (the Identity Server) is the same that has issued the cookie. So Identity Server recognizes your session, sees that you are already authenticated and redirects you to the second client, without asking for credentials.
After all, this is the idea of Identity Server:
IdentityServer4 is an OpenID Connect and OAuth 2.0 framework for ASP.NET Core 2.
It enables the following features in your applications:
Authentication as a Service
Centralized login logic and workflow for all of your applications (web, native, mobile, services). IdentityServer is an officially certified implementation of OpenID Connect.
Single Sign-on / Sign-out
Single sign-on (and out) over multiple application types.
and more....
This is from the official documentation.
You have to either go for different authorities (Identity Server instances) for each client, or re-think is Identity Server the right solution for you in this case.
NOT RECOMMENDED
I'm not recommending this, because it kind of overrides the SSO idea of Identity Server, however if you still want to do it then - you can achieve what you want if you override the IProfileService. There is a method public Task IsActiveAsync(IsActiveContext context) and this context has a property IsActive which determines if the current principal is active in the current client.
You can try and implement some custom logic here, and based on the user ID (context.Subject.GetSubjectId()) and the client id (context.Client.ClientId) to determine if the user is already logged in this client or not.
EDIT
After your comment - this is something that doesn't come OOTB from Identity Server (if I can say it like this), but luckily you have an option.
Policy based authorization per client. Like this, your user can authenticate against Identity Server (and all of its clients), but only the specific clients will authorize him. You can treat this policies as a custom authorize attribute (more or less).
Like this, a user will receive unauthorized in clients, where he.. is not authorized. Hope that this clears the thing and helps :)
You can set prompt=login from all your clients.
prompt
none - no UI will be shown during the request. If this is not possible (e.g. because the user has to sign in or consent) an error is returned
login - the login UI will be shown, even if the user is already signed-in and has a valid session
https://identityserver4.readthedocs.io/en/latest/endpoints/authorize.html
This will force the second client to login again regardless of the previous client's login status.

IdentityServer4 handle multiple facebook AppIds for tenancy

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.