Add claims to access token depending on clients - asp.net-core

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.

Related

How to set up OpenIddict to rely on AzureAd without using Microsoft.AspNetCore.Identity.UI

Our roles model is different so we can't use the stock Microsoft identity database model and all UX that goes with it, more's the pity.
All I want to do is
use OpenIdDict
have AzureAd do authentication
put my own claims into the claims principal so they go into the identity token when OpenIdDict creates it
I'm not interested in IdentityServer for assorted reasons.
I worked through a tutorial and had no trouble building all this using cookie based authn handled in an AccountController but I cannot figure out how to switch over to Azure and could really use some help.
Startup looks like this
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews();
// services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
// .AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, options =>
// {
// options.LoginPath = "/account/login";
// });
services.AddAuthentication()
.AddMicrosoftIdentityWebApi(Configuration.GetSection("AzureAd"));
// from package `Microsoft.Identity.Web`
services.AddDbContext<DbContext>(options =>
{
// Configure the context to use an in-memory store.
options.UseInMemoryDatabase(nameof(DbContext));
// Register the entity sets needed by OpenIddict.
options.UseOpenIddict();
});
services.AddHostedService<TestData>();
var openiddictBuilder = services.AddOpenIddict();
// Register the OpenIddict core components.
openiddictBuilder.AddCore(options =>
{
// Configure OpenIddict to use the EF Core stores/models.
options.UseEntityFrameworkCore()
.UseDbContext<DbContext>();
});
// Register the OpenIddict server components.
openiddictBuilder.AddServer(options =>
{
options
.AllowAuthorizationCodeFlow().RequireProofKeyForCodeExchange()
.AllowClientCredentialsFlow()
.AllowRefreshTokenFlow()
.SetAuthorizationEndpointUris("/connect/authorize")
.SetTokenEndpointUris("/connect/token")
// Encryption and signing of tokens
.AddEphemeralEncryptionKey()
.AddEphemeralSigningKey()
.DisableAccessTokenEncryption()
// Register scopes (permissions)
.RegisterScopes("api")
// Register the ASP.NET Core host and configure the ASP.NET Core-specific options.
.UseAspNetCore()
.EnableTokenEndpointPassthrough()
.EnableAuthorizationEndpointPassthrough()
;
});
}
There's an AuthorizeController with an Authorize method that looks like this
[HttpGet("~/connect/authorize")]
[HttpPost("~/connect/authorize")]
[IgnoreAntiforgeryToken]
public async Task<IActionResult> Authorize()
{
var request = HttpContext.GetOpenIddictServerRequest() ??
throw new InvalidOperationException("The OpenID Connect request cannot be retrieved.");
// Retrieve the user principal stored in the authentication cookie.
// var result = await HttpContext.AuthenticateAsync(CookieAuthenticationDefaults.AuthenticationScheme);
var result = await HttpContext.AuthenticateAsync(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
// If the user principal can't be extracted, redirect the user to the login page.
if (!result.Succeeded)
{
var authprops = new AuthenticationProperties
{
RedirectUri = Request.PathBase + Request.Path + QueryString.Create(
Request.HasFormContentType ? Request.Form.ToList() : Request.Query.ToList())
};
return Challenge(
authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
properties: authprops);
}
// Create a new claims principal
var claims = new List<Claim>
{
// 'subject' claim which is required
new Claim(OpenIddictConstants.Claims.Subject, result.Principal.Identity.Name),
new Claim(OpenIddictConstants.Claims.Role,"admin").SetDestinations(
OpenIddictConstants.Destinations.IdentityToken),
new Claim(OpenIddictConstants.Claims.Role,"gerbil wrangler").SetDestinations(
OpenIddictConstants.Destinations.IdentityToken)
};
var claimsIdentity = new ClaimsIdentity(claims, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
var claimsPrincipal = new ClaimsPrincipal(claimsIdentity);
// Set requested scopes (this is not done automatically)
claimsPrincipal.SetScopes(request.GetScopes());
// Signing in with the OpenIdDict authentiction scheme causes OpenIdDict
// to issue a code which can be exchanged for an access token
return SignIn(claimsPrincipal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
}
As I understand the theory of operation, OpenIddict proxies authentication and then issues a new token. That implies that the AzureAd redirect_uri ought to be set to an endpoint provided by OpenIddict, probably something like signin-openiddict and assuming that I'm right about all that, the client talking to OpenIddict will in turn provide a completely distinct and unrelated redirect_uri. But I haven't found any documentation covering this yet, so feel free to educate me.
In OpenIddict (and IdentityServer) the login and token generation are separated.
Those 2 parts are generally:
The user logs in using arbitrary methods and the authentication cookie is created.
The token endpoint reads the authentication cookie and creates tokens.
To use Azure Ad Authentication, you need to alter the first part to generate an authentication cookie using Azure Ad. To achieve this you'll need to implement the ExternalLogin and ExternalLoginCallback as seen in this example: https://github.com/openiddict/openiddict-core/blob/cda55862bcba67bf3de4ba08cf512ee9e2269cf5/samples/Mvc.Server/Controllers/AccountController.cs#L141
Instead of using the SignInManager, you need to create the authentication cookie yourself in the ExternalLoginCallback method. This can be done by using the HttpContext.SignInAsync method.
The second part (token generation) is left unchanged and should work without modification.

Validate that scope is present

I have a ASP.NET Core MVC Project. Authentication is performed using an oidc identity provider.
The client requests a scope "myscope". This scope is added to the access token.
.AddOpenIdConnect(options => {
...
options.ResponseType = OpenIdConnectResponseType.Code;
options.SaveTokens = true;
options.GetClaimsFromUserInfoEndpoint = true;
// options.ClaimActions.MapJsonKey("scope", "scope");
options.Scope.Add("openid");
options.Scope.Add("profile");
options.Scope.Add("myscope");
options.Events.OnTicketReceived = (x) => {
var props = x.Properties;
// here i can inspect the access token and see the scope is present
return Task.CompletedTask;
};
});
As well i can retrieve the access token from the HttpContext to eventually pass it to an api. But at the moment i want to access my database directly.
Thus i thought i want to validate that the scope "myscope" is present. I want to achieve that using a policy.
services.AddMvc(options => {
var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.RequireClaim(JwtClaimTypes.Scope, "myscope")
.Build();
options.Filters.Add(new AuthorizeFilter(policy));
})
The authorization unfortunately fails. The reason is, that the claims from the access token are not mapped into the User Claims Principal. MapJsonKey does not help here as well.
How can i check if a scope (of the access token) is present using authorization policies?
I am as well thinking about, if i am trying a pointless approach. I am using Identity Server 4 as my identity provider. The scope is specified as an api resource. Maybe you could argue that the scope should be an identity resource, thus being present in the id token and therefore mapped to the ClaimsPrincipal.
by default you need to be explicit and tell which claims you want from the tokens and user-info endpoint to end up in the user object (Claims principal), by adding this to the AddOpenIDConnect options
options.ClaimActions.MapUniqueJsonKey("website", "website");
options.ClaimActions.MapUniqueJsonKey("myscope", "myscope");
options.ClaimActions.MapUniqueJsonKey("birthdate", "birthdate");
You should also add a IdentityResource definition, because it controls what goes into the ID-Token , like
_identityResources = new List<IdentityResource>()
{
new IdentityResources.OpenId(),
new IdentityResources.Email(),
new IdentityResources.Profile(),
new IdentityResources.Address(),
"myscope"
};

OpenIdConnect with .NET Core 2.2 MVC towards IdentityServer3 using ScopePolicy. How do you get scopes into user principal?

It seems to set up OpenIdConnect authentication from .NET Core 2.2 to IdentityServer3 I have to setup through generic AddOpenIdConnect() call, and in order for scope policy to work, I have overridden OnTokenValidated, where I parse the access token received, and add the scopes in it to the ClaimsPrincipal object.
I have found no other way of getting scope policy to work. This seems a bit hackish though. Is there a better or simpler way, so I don't need to override events, or at least not parse the access token? It is parsed in the framework anyhow, so I would suspect there were other functionality available to get scopes into the claims principal.
Moving our code from .NET 4.5.2 to .NET Core 2.2, I need to set up authentication towards our IdentityServer3 server in a very different way.
I was hoping new functionality in later framework allowed for simple setup of authentication towards IdentityServer3, but I've found no fitting example.
I saw someone saying that IdentityServer4.AccessTokenValidation NuGet package could work towards IdentityServer3, but only example I've found has been with simple JWT authentication not allowing implicit user login flow.
Consequently, I've ended up using standard ASP.NET Core libraries to set up openidconnect, and then I need to tweak the code to make it work.
Not sure if the code below handles all it needs to, but at least I've gotten where I can log in and use the new web site, and write cypress tests. Any suggestions on how to do this better or simpler would be appreciated.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseAuthentication();
app.UseMvc();
}
public void ConfigureServices(IServiceCollection services)
{
// Without this, I get "Correlation failed." error from Microsoft.AspNetCore.Authentication.RemoteAuthenticationHandler`1.HandleRequestAsync()
services.Configure<CookiePolicyOptions>(options =>
{
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.None;
});
services.AddAuthentication(o => {
o.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
o.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
}).AddCookie().AddOpenIdConnect(o =>
{
o.Authority = "https://myidentityserver3.myfirm.com";
o.ClientId = "myidentityserver3clientname";
o.SignedOutRedirectUri = "https://localhost:50011/signout";
o.ResponseType = "id_token token";
o.SaveTokens = true;
o.Scope.Add("openid");
o.Scope.Add("roles");
o.Scope.Add("profile");
o.Scope.Add("customrequiredscopeforapi");
o.GetClaimsFromUserInfoEndpoint = false;
{
var old = o.Events.OnTokenValidated;
o.Events.OnTokenValidated = async ctx =>
{
if (old != null) await old(ctx);
var token = MyCustomAuthUtils.ParseBearerToken(ctx.ProtocolMessage.AccessToken);
foreach (var scope in token.Scopes)
{
ctx.Principal.AddIdentity(new ClaimsIdentity(new[] { new Claim("Scope", scope) }));
}
// Our controllers need access token to call other web api's, so putting it here.
// Not sure if that is a good way to do it.
ctx.Principal.AddIdentity(new ClaimsIdentity(new[] { new Claim("access_token", ctx.ProtocolMessage.AccessToken) }));
};
}
});
var mvcBuilder = services.AddMvc(o =>
{
o.Filters.Add(new AuthorizeFilter(ScopePolicy.Create("customrequiredscopeforapi")));
});
services.AddAuthorization();
}
The first thing is you don't need to manally decode the access token , just use ctx.SecurityToken.Claims in OnTokenValidated event to get all claims included in the token .
I'm not sure why you need to use scope to identify the permission . The scope parameter in the OIDC-conformant pipeline determines:
The permissions that an authorized application should have for a given resource server
Which standard profile claims should be included in the ID Token (if the user consents to provide this information to the application)
You can use role to identify whether current login user could access the protected resource . And the OpenID Connect middleware will help mapping the role claim to claim principle .

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.

Identity Server 4 Client Credentials for custom endpoint on token Server

I implemented a token server using Identity Server 4.
I added a custom API endpoint to the token server and struggle with the authentication. The custom endpoint is inherited from ControllerBase and has 3 methods (GET, POST, DELETE).
I intend to call the custom endpoint from within another API using a dedicated client with credentials (server to server) implemented as HttpClient in .NET Core. There is no user involved into this.
For getting the access token I use the IdentityModel DiscoveryClient and TokenEndpoint.
So in sum I did the following so far:
setup "regular" identity server and validate it works -> it works
implement custom endpoint and test it without authorizatio -> it works
add another api resource ("api.auth") with a custom scope "api.auth.endpoint1"
setup a client with client credentials allowing access to scope "api.auth.endpoint1".
implement the HttpClient and test setup -> I get an access token via the Identity Model Token Endpoint.
Now, when I call the endpoint using the HttpClient with the access token I received I get response code 200 (OK) but the content is the login page of the identity server.
The documentation of Identity Server 4 state the use of
services.AddAuthentication()
.AddIdentityServerAuthentication("token", isAuth =>
{
isAuth.Authority = "base_address_of_identityserver";
isAuth.ApiName = "name_of_api";
});
as well as the use of
[Authorize(AuthenticationSchemes = "token")]
Unfortunatly the compiler state that .AddIdentityServerAuthentication can't be found. Do I miss a special nuget?
The nugets I use on the token server so far are:
IdentityServer4 (v2.2.0)
IdentityServer4.AspNetIdentity (v2.1.0)
IdentityServer4.EntityFramework (v2.1.1)
Figured out that part. The missing nuget for AddIdentityServerAuthentication is:
IdentityServer4.AccessTokenValidation
Struggling with the authorization based on the custom scope.
Does anyone know how the security has to be configured?
Configure a client with ClientGrantTypes = client_credentials and your api like this:
services
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddIdentityServerAuthentication(options =>
{
options.Authority = "http://localhost:5000";
options.ApiName = "api.auth";
});
Where ApiName is the name of the resource. Please note that resource != scope. In most samples the resource name is equal to the scope name. But not in your case, where resource name is api.auth and scope name is api.auth.endpoint1.
Configure the client to request the scope.
var tokenClient = new TokenClient(disco.TokenEndpoint, clientId, secret);
var tokenResponse = await tokenClient.RequestClientCredentialsAsync("api.auth.endpoint1");
IdentityServer will lookup the Resource name and add that to the token as audience (aud) while the scope is added as claim with type scope.
This should be enough to make it work. Also check the sample project.
Custom authentication scheme and scope based policies for different access rights bundled together looks like that:
// Startup.ConfigureServices
services
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddIdentityServerAuthentication("CustomAuthEndpointsAuthenticationScheme", options =>
{
options.Authority = "http://localhost:5000";
options.ApiName = "api.auth"; //IdentityServer4.Models.ApiResource.Name aka Audience
});
services.AddAuthorization(options =>
{
options.AddPolicy("Endpoint1Policy", policy => {
policy.AddAuthenticationSchemes(new string[] { "CustomAuthEndpointsAuthenticationScheme" });
policy.RequireScope("api.auth.endpoint1"); } ); //IdentityServer4.Models.Scope.Name
options.AddPolicy("Endpoint2Policy", policy => {
policy.AddAuthenticationSchemes(new string[] { "CustomAuthEndpointsAuthenticationScheme" });
policy.RequireScope("api.auth.endpoint2"); } ); //IdentityServer4.Models.Scope.Name
} );
// securing the custom endpoint controllers with different access rights
[Authorize(AuthenticationSchemes = "CustomAuthEndpointsAuthenticationScheme", Policy = "Endpoint1Policy")]
It seems not to interfere with the IdentityServer4 default endpoints nor with the ASP.NET Core Identity part.