What is AdalDistributedTokenCache when using OpenID Connect in ASP.NET Core 2.0? - asp.net-core

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

Related

Microsoft Graph access token refresh

I am writing an application that uses the "OAuth 2.0 client credentials grant flow" to get an access token for calling the Microsoft Graph API. The application authenticates as itself, not on behalf of a signed in user.
I based my code off of this example from Microsoft.
This is how I initialize the GraphServiceClient:
// Read application settings from appsettings.json (tenant ID, app ID, client secret, etc.)
AppSettings config = AppSettingsFile.ReadFromJsonFile();
// Initialize the client credential auth provider
var scopes = new[] { "https://graph.microsoft.com/.default" };
var clientSecretCredential = new ClientSecretCredential(config.TenantId, config.AppId, config.ClientSecret);
var graphClient = new GraphServiceClient(clientSecretCredential, scopes);
And this is how I later use it (for example):
var users = await graphClient.Users.Request().GetAsync();
My application is an API. It is not an application that runs once and done. It will be continuously running for a long time. So I am concerned about what will happen when the access token expires. How do I make sure that when I need to use the graphClient the access token will not be expired?
According to your code snippet above, I think you are using the graph SDK and using the client credential flow as the authentication.
So we are no need to generate access token here but just using the graphClient to call the graph api and gather the information you needed. And due to this mode, it won't appear the token expired situation as each time you call an api you will new clientSecretCredential before it.
And let's come back to the refresh, azure ad provide refresh token for refreshing the access token when it expired as refresh token has much longer expire time than access token, when we try to get the refresh token, we need to append offline_access to the scope when generate the access. But using client credential flow means your app requests a new token with it's own credentials, so it's no need to using refresh token to avoid making signed-in user sign in again. Using credential flow shouldn't return refresh token.
Then you may have some ideas that you insist on using refresh the expired token process, then what you only can do is generate an access token first and save the token with its expired time in some place, and using the access token as the http request header and calling graph api. Then the code should like this, but I don't think you're willing to using this kind of code, you may also refer to this document for more details:
var scopes = new[] { "https://graph.microsoft.com/.default" };
var tenantId = "tenant_name.onmicrosoft.com";
var clientId = "your_azuread_clientid";
var clientSecret = "corresponding_client_secret";
var clientSecretCredential = new ClientSecretCredential(
tenantId, clientId, clientSecret);
var tokenRequestContext = new TokenRequestContext(scopes);
var token = clientSecretCredential.GetTokenAsync(tokenRequestContext).Result.Token;
//using http sender with the token
httpClient = new HttpClient();
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token );
// Call the web API.
HttpResponseMessage response = await _httpClient.GetAsync(apiUri);
...
}

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.

AddOpenIdConnect and Refresh Tokens in ASP.NET Core

I have added AddOpenIdConnect to the ConfigureServices method of my ASP.NET Core 3.1 Razor application. It works great until the token expires, then I get 401 responses from my IDP.
I have seen an example that shows a way to wire up refresh tokens manually.
But I am hesitant to do that. It seems super unlikely that the folks at Microsoft did not think about refresh tokens.
Does ASP.NET Core 3.1 have a way to have refresh tokens automatically update the access token?
Here is what I came up with. Since there are not very many examples that I could find on how to do refresh tokens in ASP.NET Core with cookies, I thought I would post this here. (The one I link to in the question has issues.)
This is just my attempt at getting this working. It has not been used in any production setting. This code goes in the ConfigureServices method.
services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, options =>
{
options.Events = new CookieAuthenticationEvents
{
// After the auth cookie has been validated, this event is called.
// In it we see if the access token is close to expiring. If it is
// then we use the refresh token to get a new access token and save them.
// If the refresh token does not work for some reason then we redirect to
// the login screen.
OnValidatePrincipal = async cookieCtx =>
{
var now = DateTimeOffset.UtcNow;
var expiresAt = cookieCtx.Properties.GetTokenValue("expires_at");
var accessTokenExpiration = DateTimeOffset.Parse(expiresAt);
var timeRemaining = accessTokenExpiration.Subtract(now);
// TODO: Get this from configuration with a fall back value.
var refreshThresholdMinutes = 5;
var refreshThreshold = TimeSpan.FromMinutes(refreshThresholdMinutes);
if (timeRemaining < refreshThreshold)
{
var refreshToken = cookieCtx.Properties.GetTokenValue("refresh_token");
// TODO: Get this HttpClient from a factory
var response = await new HttpClient().RequestRefreshTokenAsync(new RefreshTokenRequest
{
Address = tokenUrl,
ClientId = clientId,
ClientSecret = clientSecret,
RefreshToken = refreshToken
});
if (!response.IsError)
{
var expiresInSeconds = response.ExpiresIn;
var updatedExpiresAt = DateTimeOffset.UtcNow.AddSeconds(expiresInSeconds);
cookieCtx.Properties.UpdateTokenValue("expires_at", updatedExpiresAt.ToString());
cookieCtx.Properties.UpdateTokenValue("access_token", response.AccessToken);
cookieCtx.Properties.UpdateTokenValue("refresh_token", response.RefreshToken);
// Indicate to the cookie middleware that the cookie should be remade (since we have updated it)
cookieCtx.ShouldRenew = true;
}
else
{
cookieCtx.RejectPrincipal();
await cookieCtx.HttpContext.SignOutAsync();
}
}
}
};
})
.AddOpenIdConnect(options =>
{
options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.Authority = oidcDiscoveryUrl;
options.ClientId = clientId;
options.ClientSecret = clientSecret;
options.RequireHttpsMetadata = true;
options.ResponseType = OidcConstants.ResponseTypes.Code;
options.UsePkce = true;
// This scope allows us to get roles in the service.
options.Scope.Add("openid");
options.Scope.Add("profile");
options.Scope.Add("offline_access");
// This aligns the life of the cookie with the life of the token.
// Note this is not the actual expiration of the cookie as seen by the browser.
// It is an internal value stored in "expires_at".
options.UseTokenLifetime = false;
options.SaveTokens = true;
});
This code has two parts:
AddOpenIdConnect: This part of the code sets up OIDC for the application. Key settings here are:
SignInScheme: This lets ASP.NET Core know you want to use cookies to store your authentication information.
*UseTokenLifetime: As I understand it, this sets an internal "expires_at" value in the cookie to be the lifespan of the access token. (Not the actual cookie expiration, which stays at the session level.)
*SaveTokens: As I understand it, this is what causes the tokens to be saved in the cookie.
OnValidatePrincipal: This section is called when the cookie has been validated. In this section we check to see if the access token is near or past expiration. If it is then it gets refreshed and the updated values are stored in the cookie. If the token cannot be refreshed then the user is redirected to the login screen.
The code uses these values that must come from your configuration file:
clientId: OAuth2 Client ID. Also called Client Key, Consumer Key, etc.
clientSecret: OAuth2 Client Secret. Also called Consumer Secret, etc.
oidcDiscoveryUrl: Base part of the URL to your IDP's Well Known Configuration document. If your Well Known Configuration document is at https://youridp.domain.com/oauth2/oidcdiscovery/.well-known/openid-configuration then this value would be https://youridp.domain.com/oauth2/oidcdiscovery.
tokenUrl: Url to your IDP's token endpoint. For example: https:/youridp.domain.com/oauth2/token
refreshThresholdMinutes: If you wait till the access token is very close to expiring, then you run the risk of failing calls that rely on the access token. (If it is 5 miliseconds from expiration then it could expire, and fail a call, before you get a chance to refresh it.) This setting is the number of minutes before expiration you want to consider an access token ready to be refreshed.
* I am new to ASP.NET Core. As such I am not 100% sure that those settings do what I think. This is just a bit of code that is working for me and I thought I would share it. It may or may not work for you.
As far as I know, there's nothing built-in in ASP.NET Core 3.1 to refresh access tokens automatically. But I've found this convenient library from the IdentityServer4 authors which stores access and refresh tokens in memory (this can be overriden) and refreshes access tokens automatically when you request them from the library.
How to use the library: https://identitymodel.readthedocs.io/en/latest/aspnetcore/web.html.
NuGet package: https://www.nuget.org/packages/IdentityModel.AspNetCore/.
Source code: https://github.com/IdentityModel/IdentityModel.AspNetCore.
I implemented token refresh in a .NET 7.0 sample recently. There has always been an option to refresh tokens and rewrite cookies, in many MS OIDC stacks, including older ones: Owin, .NET Core etc. It seems poorly documented though, and I had to dig around in the aspnet source code to figure out the cookie rewrite step. So I thought I'd add to this thread in case useful to future readers.
REFRESH TOKEN GRANT
First send a standards based refresh token grant request:
private async Task<JsonNode> RefreshTokens(HttpContext context)
{
var tokenEndpoint = "https://login.example.com/oauth/v2/token";
var clientId = "myclientid";
var clientSecret = "myclientsecret";
var refreshToken = await context.GetTokenAsync("refresh_token");
var requestData = new[]
{
new KeyValuePair<string, string>("client_id", clientId),
new KeyValuePair<string, string>("client_secret", clientSecret),
new KeyValuePair<string, string>("grant_type", "refresh_token"),
new KeyValuePair<string, string>("refresh_token", refreshToken),
};
using (var client = new HttpClient())
{
client.DefaultRequestHeaders.Add("accept", "application/json");
var response = await client.PostAsync(tokenEndpoint, new FormUrlEncodedContent(requestData));
response.EnsureSuccessStatusCode();
var json = await response.Content.ReadAsStringAsync();
return JsonNode.Parse(json).AsObject();
}
}
REWRITE COOKIES
Then rewrite cookies, which is done by 'signing in' with a new set of tokens. A better method name might have been something like 'update authentication state'. If you then look at the HTTP response you will see an updated set-cookie header, with the new tokens.
Note that in a refresh token grant response, you may or may not receive a new refresh token and new ID token. If not, then supply the existing values.
private async Task RewriteCookies(JsonNode tokens, HttpContext context)
{
var accessToken = tokens["access_token"]?.ToString();
var refreshToken = tokens["refresh_token"]?.ToString();
var idToken = tokens["id_token"]?.ToString();
var newTokens = new List<AuthenticationToken>();
newTokens.Add(new AuthenticationToken{ Name = "access_token", Value = accessToken });
if (string.IsNullOrWhiteSpace(refreshToken))
{
refreshToken = await context.GetTokenAsync("refresh_token");
}
newTokens.Add(new AuthenticationToken{ Name = "refresh_token", Value = refreshToken });
if (string.IsNullOrWhiteSpace(idToken))
{
idToken = await context.GetTokenAsync("id_token");
}
newTokens.Add(new AuthenticationToken{ Name = "id_token", Value = idToken });
var properties = context.Features.Get<IAuthenticateResultFeature>().AuthenticateResult.Properties;
properties.StoreTokens(newTokens);
await context.SignInAsync(context.User, properties);
}
SUMMARY
Being able to refresh access tokens when you receive a 401 response from an API is an essential capability in any web app. Use short lived access tokens and then code similar to the above, to renew them with good usability.
Note that relying on an expiry time is not fully reliable. API token validation can fail due to infrastructure events in some cases. APIs then return 401 for access tokens that are not expired. The web app should handle this via a refresh, followed by a retry of the API request.
AddOpenIdConnect is used to configure the handler that performs the OpenID Connect protocol to get tokens from your identity provider. But it doesn't know where you want to save the tokens. It could be any of the following:
Cookie
Memory
Database
You could store the tokens in a cookie then check the token's expire time and refresh the tokens by intercepting the cookie's validation event (as the example shows).
But AddOpenIdConnect doesn't have the logic to control where the user want to store the tokens and automatically implement token refresh.
You can also try to wrap the middleware as the ADAL.NET/MSAL.NET to provide cache features and then you can acquire/refresh tokens silently.

Which is the correct flow to get current user's groups from Microsoft graph?

Hi I am implementing Groups based authorization to my web api. I have client application swagger. Through swagger I am logging in and calling web api. In web api I want to implement groups based authorization through Microsoft graph. When I logging through swagger I will get one token and I am passing to my webapi. If I am not wrong, Now I required one token to call Microsoft graph. So can I use same token to call microsoft graph? I confused my self and implemented client credential flow. Client credential flow will get token for the app(here user signed in token has nothing to do).
public static async Task<GraphServiceClient> GetGraphServiceClient()
{
// Get Access Token and Microsoft Graph Client using access token and microsoft graph v1.0 endpoint
var delegateAuthProvider = await GetAuthProvider();
// Initializing the GraphServiceClient
graphClient = new GraphServiceClient(graphAPIEndpoint, delegateAuthProvider);
return graphClient;
}
private static async Task<IAuthenticationProvider> GetAuthProvider()
{
AuthenticationContext authenticationContext = new AuthenticationContext(authority);
ClientCredential clientCred = new ClientCredential(clientId, clientSecret);
// ADAL includes an in memory cache, so this call will only send a message to the server if the cached token is expired.
AuthenticationResult authenticationResult = await authenticationContext.AcquireTokenAsync(graphResource, clientCred).ConfigureAwait(false);
var token = authenticationResult.AccessToken;
var delegateAuthProvider = new DelegateAuthenticationProvider((requestMessage) =>
{
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("bearer", token.ToString());
return Task.FromResult(0);
});
return delegateAuthProvider;
}
Below code will return all the groups.
GraphServiceClient client = await MicrosoftGraphClient.GetGraphServiceClient();
var groupList = await client.Groups.Request().GetAsync();
but my requirement is to get current signed in users group. So can someone help me which flow I should use and In the above code only Is it possible to get current users group? Can someone help me in understanding these and implement correctly? Any help would be greatly appreciated. Thanks
As we have discussed before, you should call Microsoft Graph API from your webapi app.
So you should not use the same access token to call Microsoft Graph. You should specfy the Microsoft Graph endpoint (https://graph.microsoft.com) as the resource when you request a new access token to Microsoft Graph.
Secondly, client credential flow means app-only permission (without user). So if there is no signed in user, how could we get user's groups?
You should consider using AcquireTokenAsync(String, ClientAssertion, UserAssertion).
After that, using the following code to get the signed in user's groups.
GraphServiceClient client = await MicrosoftGraphClient.GetGraphServiceClient();
var memberOf = await graphClient.Me.MemberOf.Request().GetAsync();

Trouble getting ClaimsPrincipal populated when using EasyAuth to authenticate against AAD on Azure App Service in a Asp.Net Core web app

We have a web app built on Asp.Net core. It doesn't contain any authentication middleware configured in it.
We are hosting on Azure App Service and using the Authentication/Authorization option (EasyAuth) to authenticate against Azure AD.
The authentication works well - we get the requisite headers inserted and we can see the authenticated identity at /.auth/me. But the HttpContext.User property doesn't get populated.
Is this a compatibility issue for Asp.Net core? Or am I doing something wrong?
I've created a custom middleware that populates the User property until this gets solved by the Azure Team.
It reads the headers from the App Service Authentication and create a a user that will be recognized by the [Authorize] and has a claim on name.
// Azure app service will send the x-ms-client-principal-id when authenticated
app.Use(async (context, next) =>
{
// Create a user on current thread from provided header
if (context.Request.Headers.ContainsKey("X-MS-CLIENT-PRINCIPAL-ID"))
{
// Read headers from Azure
var azureAppServicePrincipalIdHeader = context.Request.Headers["X-MS-CLIENT-PRINCIPAL-ID"][0];
var azureAppServicePrincipalNameHeader = context.Request.Headers["X-MS-CLIENT-PRINCIPAL-NAME"][0];
// Create claims id
var claims = new Claim[] {
new System.Security.Claims.Claim("http://schemas.microsoft.com/identity/claims/objectidentifier", azureAppServicePrincipalIdHeader),
new System.Security.Claims.Claim("name", azureAppServicePrincipalNameHeader)
};
// Set user in current context as claims principal
var identity = new GenericIdentity(azureAppServicePrincipalIdHeader);
identity.AddClaims(claims);
// Set current thread user to identity
context.User = new GenericPrincipal(identity, null);
};
await next.Invoke();
});
Yes, this is a compatibility issue. ASP.NET Core does not support flowing identity info from an IIS module (like Easy Auth) to the app code, unfortunately. This means HttpContext.User and similar code won't work like it does with regular ASP.NET.
The workaround for now is to invoke your web app's /.auth/me endpoint from your server code to get the user claims. You can then cache this data as appropriate using the x-ms-client-principal-id request header value as the cache key. The /.auth/me call will need to be properly authenticated in the same way that calls to your web app need to be authenticated (auth cookie or request header token).
I wrote a small basic middleware to do this. It will create an identity based off of the .auth/me endpoint. The identity is created in the authentication pipeline so that [authorize] attributes and policies work with the identity.
You can find it here:
https://github.com/lpunderscore/azureappservice-authentication-middleware
or on nuget:
https://www.nuget.org/packages/AzureAppserviceAuthenticationMiddleware/
Once added, just add this line to your startup:
app.UseAzureAppServiceAuthentication();
The following code decrypts the AAD token from the Azure App Service HTTP header and populates HttpContext.User with the claims. It's rough as you'd want to cache the configuration rather than look it up on every request:
OpenIdConnectConfigurationRetriever r = new OpenIdConnectConfigurationRetriever();
ConfigurationManager<OpenIdConnectConfiguration> configManager = new ConfigurationManager<OpenIdConnectConfiguration>(options.Endpoint, r);
OpenIdConnectConfiguration config = await configManager.GetConfigurationAsync();
var tokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKeys = config.SigningKeys.ToList(),
ValidateIssuer = true,
ValidIssuer = config.Issuer,
ValidateAudience = true,
ValidAudience = options.Audience,
ValidateLifetime = true,
ClockSkew = new TimeSpan(0, 0, 10)
};
JwtSecurityTokenHandler handler = new JwtSecurityTokenHandler();
ClaimsPrincipal principal = null;
SecurityToken validToken = null;
string token = context.Request.Headers["X-MS-TOKEN-AAD-ID-TOKEN"];
if (!String.IsNullOrWhiteSpace(token))
{
principal = handler.ValidateToken(token, tokenValidationParameters, out validToken);
var validJwt = validToken as JwtSecurityToken;
if (validJwt == null) { throw new ArgumentException("Invalid JWT"); }
if (principal != null)
{
context.User.AddIdentities(principal.Identities);
}
}
It only works for Azure AD. To support other ID providers (Facebook, Twitter, etc) you'd have to detect the relevant headers and figure out how to parse each provider's token. However, it should just be variations on the above theme.
You can give this library a try. I faced a similar problem and created this to simplify the use.
https://github.com/dasiths/NEasyAuthMiddleware
Azure App Service Authentication (EasyAuth) middleware for ASP.NET
CORE with fully customizable components with support for local
debugging
It hydrates the HttpContext.User by registering a custom authentication handler. To make things easier when running locally, it even has the ability to use a json file to load mocked claims.