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

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.

Related

Set up authentication in .net-core using ws-federation

I'm trying to set up authentication against ADFS using ws-federation
services.AddAuthentication(sharedOptions =>
{
sharedOptions.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
sharedOptions.DefaultChallengeScheme = WsFederationDefaults.AuthenticationScheme;
})
.AddWsFederation(options =>
{
options.Wtrealm = "https://localhost:44351/";
options.MetadataAddress = "https://xxx/federationmetadata/2007-06/federationmetadata.xml";
})
.AddCookie();
Six claims are returned, but I know that there are other claims which are not returned. For example the claim nameidentifier (http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier).
How can I make ADFS return more claims?
As far as I know, you should set the ADFS server to return the more claims instead of doing it at the client side.
You should have the enough permission to manage your server and follow this article to modify the ADFS server to return more claims or custom claims.
More details, you could refer to this article.

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.

Add claims to access token depending on clients

I have the situation when one web app needs access token with one claim and another web app needs another claim.
For example, client_1 should have access token with claim is_admin and client_2 should have claim stores.
What I want to know - is it normal to add such information in such a manner in access token, or is there a better alternative?
And if to add this info in token - can you suggest how to distinguish which claim to add in IProfileService implementation depending on a client? Is it good to use ClientProperties for this purpose?
UPDATE: I use Azure AD as external identity provider and users don't have those claims - I need to retrieve it from other sources
In your configuration add an IdentityResource that represents the scope of the client app, like client_1_scope, including the IdentityClaim is_admin.
Do the same for client_2_scope including IdentityClaim stores.
Also allow the client to request the defined scope (add a record in ClientScopes).
In client_1 request the scope client_1_scope like this:
options.Scope.Add("client_1_scope");
And in client_2 like this:
options.Scope.Add("client_2_scope");
When the user has a claim is_admin or stores, then the claim will be included as part of the requested scope only.
Add this line to make sure the claims are added:
options.GetClaimsFromUserInfoEndpoint = true;
In the client_1 app the configuration could look something like this:
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("client_1_scope");
options.Authority = "";
options.ClientId = "";
options.ClientSecret = "";
options.ResponseType = "code id_token";
});
The information will be saved in the cookie. If you want claims to be part of an access token that is send to an Api then you should use the Api~ tables to configure the claims.
If those other sources already exist then you can implement IProfileService where you can add the dynamic claims, based on the scope as described above, so you still need to request the scope.
Something like:
using IdentityServer4.Models;
using IdentityServer4.Services;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
public class MyProfileService : IProfileService
{
public async Task GetProfileDataAsync(ProfileDataRequestContext context)
{
// Include configured claims.
context.AddRequestedClaims(context.Subject.Claims);
// The service gets called multipe times. In this case we need
// the UserInfo endpoint because that's where the scope is defined.
if (context.Caller == "UserInfoEndpoint")
{
// Get the value from somewhere and transform to a list of claims.
// You can filter by requested scopes
List<Claim> userClaims = GetUserClaims(context.RequestedResources.IdentityResources);
if (userClaims.Any())
context.IssuedClaims.AddRange(userClaims);
}
}
public async Task IsActiveAsync(IsActiveContext context)
{
context.IsActive = true;
}
}
If you want to add claims to an access token (used for api's) then context.Caller is ClaimsProviderAccessToken and you should look at context.RequestedResources.ApiResources.
Register the service in startup:
.AddProfileService<MyProfileService>()
Please note, this is just an example. I didn't test the code.
Having a seperate source, you can also look at PolicyServer. In that case you can keep IdentityServer for authentication and the PolicyServer for 'opt-in' authorization.

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.