Validate that scope is present - asp.net-core

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"
};

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.

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.

AspNetCore use cookie and bearer together, and make Authorize attribute to use both by default

I have application where its using Cookie and Bearer token. Since i dont want update each Authorize by providing a schemas I did rewrite default schemas:
services
.AddAuthorization(options =>
{
options.DefaultPolicy = new AuthorizationPolicyBuilder()
.AddAuthenticationSchemes(OAuthValidationDefaults.AuthenticationScheme,
CookieAuthenticationDefaults.AuthenticationScheme,
"Identity.Application")
.RequireAuthenticatedUser()
.Build();
});
services.AddAuthentication()
.AddExternalAuthProviders(Configuration)
.AddFlymarkOpenIdConnectServer()
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, options =>
{
options.LoginPath = "/Identity/Account/LogIn";
options.SlidingExpiration = true;
options.Events.OnRedirectToLogin = OnRedirectToLogin;
})
.AddOAuthValidation(OAuthValidationDefaults.AuthenticationScheme,
o=>o.Events.OnCreateTicket = OnCreateTicket);
services.ConfigureApplicationCookie(config =>
{
config.Events = new CookieAuthenticationEvents
{
OnRedirectToLogin = OnRedirectToLogin
};
});
I thought that if I add CookieAuthenticationDefaults.AuthenticationScheme then it will work but cookies are not working untill I add Identity.Application
So my question is why its not working or where is constant to use instead of hardcoded string
ASP.NET Core Identity uses its own instances of the cookie authentication handler, magically registered for you when you call services.AddIdentity().
As you figured out, these instances don't use the default scheme name but a name that starts with the Identity. prefix.
In your code, you're basically registering a new cookie handler instance with the default scheme name. But since nothing in your code uses it to issue cookies, it can't validate anything and will always return unauthenticated tickets, which is why it doesn't work until you add the magical Identity.Application, which corresponds to the main cookie handler instance used by ASP.NET Core Identity.
The constant you're looking for is in IdentityConstants.
Note: be extremely careful before adding a default policy that accepts both authentication cookies and bearer tokens, as your app might be vulnerable to CSRF if it doesn't use antiforgery (which is normally the case when developing an API).

What is AdalDistributedTokenCache when using OpenID Connect in ASP.NET Core 2.0?

The code shown here is my attempt to perform authentication in ASP.NET Core 2.0 against my Azure AD tenant.
The interesting part is my next set of objectives upon receiving an authentication code.
I want put the authenticated user's AD Groups into claims and have them passed along to my policy-based authorisation registrations.
To achieve this, I exchange the authorisation code for an access token.
Upon obtaining access token, I use Microsoft Graph SDK to retrieve the authenticated user's AD Groups.
Question 1: I have seen examples where the access token is stored in a cache IDistributedCache. Why is this important and what risk is there in not performing this step and what exactly is AdalDistributedTokenCache?
e.g.
var cache = new AdalDistributedTokenCache(distributedCache, userId);
var authContext = new AuthenticationContext(ctx.Options.Authority, cache);
I find the access token is always at hand via
string accessToken = await HttpContext.GetTokenAsync("access_token");
Question 2: After retrieving groups, if I add these as claims to the Principal, can I then use them to drive authorization policies as described here?
Policy-based authorisation in ASP.NET Core
Question 3: Does the access token and id token along with the claims I add end up inside the cookie?
Question 4: How can I force Azure AD to return AD Roles as claims (not groups as I can get these via Graph) without having to change some kind of manifest?
Full code
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie()
.AddOpenIdConnect(options =>
{
Configuration.GetSection("OpenIdConnect").Bind(options);
options.SaveTokens = true;
options.Events = new OpenIdConnectEvents
{
OnAuthorizationCodeReceived = async ctx =>
{
// Exchange authorization code for access token
var request = ctx.HttpContext.Request;
var currentUri = UriHelper.BuildAbsolute(request.Scheme, request.Host, request.PathBase, request.Path);
var credential = new ClientCredential(ctx.Options.ClientId, ctx.Options.ClientSecret);
var authContext = new AuthenticationContext(ctx.Options.Authority);
var result = await authContext.AcquireTokenByAuthorizationCodeAsync(
ctx.ProtocolMessage.Code, new Uri(currentUri), credential, ctx.Options.Resource);
// Use Microsoft Graph SDK to retrieve AD Groups
var email = ctx.Principal.Claims.First(f => f.Type == ClaimTypes.Upn).Value;
GraphServiceClient client = new GraphServiceClient(
new DelegateAuthenticationProvider(
async requestMessage => {
var accessToken = result.AccessToken;
requestMessage.Headers.Authorization =
new AuthenticationHeaderValue("Bearer", accessToken);
}));
var groups = await client.Users[email].GetMemberGroups(false).Request()
.PostAsync();
// Do something with groups
ctx.HandleCodeRedemption(result.AccessToken, result.IdToken);
}
};
});
services.AddMvc()
.AddRazorPagesOptions(options =>
{
options.Conventions.AuthorizePage("/Index");
});
}
Question 1: I have seen examples where the access token is stored in a cache IDistributedCache. Why is this important and what risk is there in not performing this step and what exactly is AdalDistributedTokenCache?
ADAL uses an in-memory token cache by default where it keeps the access and refresh tokens it acquires.
By using a distributed cache backed by e.g. Redis, all of the instances hosting the app can access the token cache.
This is required if the app runs behind a load balancer, and also prevents the data from being lost when the app restarts.
Question 2: After retrieving groups, if I add these as claims to the Principal, can I then use them to drive authorization policies as described here?
You can add a new identity on the user principal, similar to my article: https://joonasw.net/view/adding-custom-claims-aspnet-core-2.
It should work if you add the identity in the OnAuthorizationCodeReceived handler.
They will be stored as claims using the default sign-in scheme, which is Cookies in your case.
So yes, you can use them in policies then.
Question 3: Does the access token and id token along with the claims I add end up inside the cookie?
Yes, they are all persisted in the cookie.
However, you should use ADAL to get the access token when you need it.
The option to save tokens is not really needed in your case, as long as you set up the ADAL token cache correctly.
Acquiring the token: https://github.com/juunas11/aspnetcore2aadauth/blob/master/Core2AadAuth/Startup.cs#L75
Using a token: https://github.com/juunas11/aspnetcore2aadauth/blob/master/Core2AadAuth/Controllers/HomeController.cs#L89
The sample app first creates a token cache for the signed-in user.
Then, we use ADAL's AcquireTokenSilentAsync method to get an access token silently.
This means ADAL will return the cached access token, or if it has expired, uses the cached refresh token to get a new access token.
If both of those fail, an exception is thrown.
In the case of the sample app, there is an exception filter that catches the exception and redirects the user to login: https://github.com/juunas11/aspnetcore2aadauth/blob/master/Core2AadAuth/Filters/AdalTokenAcquisitionExceptionFilter.cs
Question 4: How can I force Azure AD to return AD Roles as claims (not groups as I can get these via Graph) without having to change some kind of manifest?
If you mean roles like Global Administrator, you cannot get that in claims.
Roles which you define in the app manifest, and assign to users/groups are always included in the token. https://joonasw.net/view/defining-permissions-and-roles-in-aad