Adding a field to claim for Azure AD in ASP.NET Core 6 - asp.net-core

My MVC web application previously used cookie based authentication handled by identity framework and usernames and passwords stored in my db. However, I have since moved to Azure AD for authentication and it is working great however I use a field in my database for each user called InternalUserNumber and this is used in many stored procedures and table valued functions called from my web application's dapper queries. So on authenticating to Azure AD the user is redirected to the homepage which on load I was planning to run a sql query using the preferred_username (email address) against our database to get the UserID and I was going to store it as a session variable which I can use later in my code. However, this got me thinking I use to store the UserID as a claim in the cookie as follows:
new Claim(ClaimTypes.Name, user.InternalUserNumber.ToString())
Is this still doable using the Azure AD cookie which is generated after logging in? I do not have access to the admin panel in Azure AD so this has to be all code based. Or is my only option storing this in the session as a variable and if so whats the best route for that? I would note i do plan to implement SignalR which I read you cannot use session if you are going to use SignalR so theres that potential issue for that route.
Thanks
Edit :
I have actually added some code which uses an event passed in program.cs. I was trying to implement a service to do the following code but I failed to be able to figure it out but instead put directly inside the event code as shown below. My issue is I cannot access the preferred_username in HttpContext object as it returns null? I am able to use HttpContext in my controllers so why am I not able to use it here in Program.CS during this event being triggered?
builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(options =>
{
builder.Configuration.Bind("AzureAd", options);
options.Events = new OpenIdConnectEvents
{
OnTokenValidated = context =>
{
//var service = context.HttpContext.RequestServices.GetRequiredService<ISomeService>();
//var result = await service.DoSomethingAsync();
string query = "select dbo.A2F_0013_ReturnUserIDForEmail(#Email) as UserID";
string connectionString = builder.Configuration.GetValue<string>("ConnectionStrings:DBContext");
string signInEmailAddress = context.HttpContext.User.FindFirstValue("preferred_username");
using (var connection = new SqlConnection(connectionString))
{
var queryResult = connection.QueryFirst(query, new { Email = signInEmailAddress });
// add claims
var claims = new List<Claim>
{
new Claim("UserID", queryResult.UserID.ToString())
};
var appIdentity = new ClaimsIdentity(claims);
context.Principal.AddIdentity(appIdentity);
}
return Task.CompletedTask;
},
};
})
.EnableTokenAcquisitionToCallDownstreamApi(initialScopes)
.AddMicrosoftGraph(builder.Configuration.GetSection("DownstreamApi"))
.AddInMemoryTokenCaches();

Related

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

Getting access token within Claims Transformer in ASP.NET Core

I'm developing a set of applications including an Identity Server using IdentityServer4, a .NET Core MVC app, a .NET Core WebAPI.
As part of a asp.net core mvc application I am using AddOpenIdConnect to do authentication and doing options.SaveTokens = true.
However, as part of Claims Transformation, in TransformAsync I would like to be able to have access to the access token provided by the identityserver. This is to be able to call a permissions endpoint on the api to populate the principal with claims that I can use to do authorization on my controllers etc.
If I call HttpContext.GetTokenAsync("access_token") I get a stackoverflowexception due to the infinite loop created by authenticate being called, which then calls TransformAsync again.
Is this a sound approach in the first place? Typically, TransformAsync is where I would populate application permissions. Is there any way of accessing the token without triggering the authenticate again?
Would appreciate any help as we're a bit stumped! Thanks
Edit: I've seen suggestions around doing transformations in the OnTicketReceived
event. It looks like I'd have access to the token through the properties in there. Is this a better place to do it?
I came across the same problem. My solution was,
Override JwtBearerEvents.TokenValidated event called by IdentityServer4.AccessTokenValidation middleware.
private Task OnTokenValidated(TokenValidatedContext tokenValidatedContext)
{
tokenValidatedContext.HttpContext.Items["access_token"] = (tokenValidatedContext.SecurityToken as JwtSecurityToken).RawData;
return Task.CompletedTask;
}
This will utilize HttpContext.Items collection which is request scoped. Now you can retreive this access token in TransformAsync method, like below.
var access_token = _httpContextAccessor.HttpContext.Items["access_token"] as string;
Please note that you need to inject IHttpContextAccessor to access HttpContext in ClaimsTransformer.
It has been many years since this question was posted, but if you are still looking for a solution to the issue, you can get the access token in the OnTokenValidated event.
OnTokenValidated = tokenValidatedContext =>
{
var handler = new JwtSecurityTokenHandler();
// get access token
var jsonToken = handler.ReadJwtToken(tokenValidatedContext.TokenEndpointResponse.AccessToken);
var claims = new List<Claim>();
claims.Add(new Claim("customClaimType", "customClaimValue"));
var appIdentity = new ClaimsIdentity(claims);
tokenValidatedContext.Principal.AddIdentity(appIdentity);
return Task.CompletedTask;
}
Reference : Adding Custom Claims During Authentication
I think you can inject the IAuthenticationHandlerProvider service and use following:
Get the authentication handler by scheme name.
get the AuthenticateResult by invoking AuthenticateAsync
get the token from the authentication properties
var token = string.Empty;
var handler = await Handlers.GetHandlerAsync(context, scheme); // i.e. "OIDC"
var result = await handler.AuthenticateAsync();
if(result?.Succeeded == true) {
token = result?.Properties?.GetTokenValue(tokenName);
}
haven't tested it but i think it should work

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.

How to delegate Identity from Web-Application to WebAPI

I am trying to build a website, where the user logs in at the and can use an backend web-API.
Calls to the backend web-API will always be proxied by the frontend website, since the backend is not publicly available.
Back- and frontend are MVC 6 (or MVC Core?) projects based on ASP.net Core.
The frontend currently authenticates (successfully) by using OpenId-Connect.
The backend should use JwtBearerToken.
The authentication so far requests the response type is id_token code and the scope is openid profile.
After the roundtrip to the Auth-Server (ADFS 2016), I will end up in the AuthorizationCodeReceived-Event from ASP.NET, but I have no luck in exchanging the code for authorization token. I tried the following using ADAL:
public override async Task AuthorizationCodeReceived(AuthorizationCodeReceivedContext context)
{
await base.AuthorizationCodeReceived(context);
var clientCredential = new ClientCredential(context.Options.ClientId, context.Options.ClientSecret);
var oAuthContext = new AuthenticationContext(context.Options.Authority, false);
var oAuthResult = await oAuthContext.AcquireTokenByAuthorizationCodeAsync(context.Code, new Uri(context.RedirectUri), clientCredential);
}
I had to disable the authority validation (which I do not like) and I do not get results other than Http-Status 400.
I'd be happy for any advice how to move on.
Update
Further Investigation Shows, that the OpenIdConnect-Configuration allows to save auth and refresh Tokens into the Claims. Nevertheless I don't see the possibility to convert it in the first place.
I also tried exchanging the code by hand (PS: Invoke-WebRequest ...) but had no success. Perhaps this is a problem of ADFS TP4...
I've managed to get this scenario to work with TP4.
AuthorizationCodeReceived = async n =>
{
string code = n.Code;
AuthenticationContext ac = new AuthenticationContext(BaseAddress, false);
ClientCredential client = new ClientCredential("clientid", "secret");
string resourceId = "https://myservices/myapi";
AuthenticationResult ar = await ac.AcquireTokenByAuthorizationCodeAsync(code, new Uri("https://localhost:44300/"), client, resourceId);
}
You can then use the access token from a controller method like this:
AuthenticationContext ac = new AuthenticationContext(Startup.BaseAddress, false);
ClientCredential cred = new ClientCredential("clientid", "secret");
string resourceId = "https://myservices/myapi";
AuthenticationResult ar = ac.AcquireTokenSilent(resourceId, cred, UserIdentifier.AnyUser);
var client = new HttpClient();
client.SetBearerToken(ar.AccessToken);
var result = await client.GetStringAsync("http://localhost:2727/identity");

OpenId Connect middleware not setting auth cookie in an WebForms app

I'm trying to integrate OpenId Connect into long-time existing webforms application. I was able to migrate the app to use OWIN and I'm using OpenIdConnectAuthenticationMiddleware to authenticate against my IdP provider. All goes fine until the point where I need to construct new identity obtained from IdP and set the cookie - which part I think is not happening.
Important parts of my Startup.Configure method:
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
LoginPath = new PathString("/login.aspx"),
CookieManager = new SystemWebCookieManager() //custom cookie manager
});
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
Authority = "https://[development_domain]/core",
ClientId = "VDWeb",
ResponseType = "code id_token token",
Scope = "openid profile",
UseTokenLifetime = true,
SignInAsAuthenticationType = "Cookies",
Notifications = new OpenIdConnectAuthenticationNotifications
{
SecurityTokenValidated = async n =>
{
var userInfo = await EndpointAndTokenHelper.CallUserInfoEndpoint(n.ProtocolMessage.AccessToken);
//now store Preferred name :
var prefNameClaim = new Claim(
Thinktecture.IdentityModel.Client.JwtClaimTypes.PreferredUserName,
userInfo.Value<string>("preferred_username"));
var myIdentity = new ClaimsIdentity(
n.AuthenticationTicket.Identity.AuthenticationType,
Thinktecture.IdentityModel.Client.JwtClaimTypes.PreferredUserName,
Thinktecture.IdentityModel.Client.JwtClaimTypes.Role);
myIdentity.AddClaim(prefNameClaim);
//add unique_user_key claim
var subjectClaim = n.AuthenticationTicket.Identity.FindFirst(Thinktecture.IdentityModel.Client.JwtClaimTypes.Subject);
myIdentity.AddClaim(new Claim("unique_user_key", subjectClaim.Value));
myIdentity.AddClaim(new Claim("id_token", n.ProtocolMessage.IdToken));
var ticket = new AuthenticationTicket(myIdentity, n.AuthenticationTicket.Properties);
var currentUtc = new SystemClock().UtcNow;
ticket.Properties.IssuedUtc = currentUtc;
ticket.Properties.ExpiresUtc = currentUtc.Add(TimeSpan.FromHours(12));
n.AuthenticationTicket = ticket;
},
}
});
I can confirm AuthentocationTicket is populated properly but auth cookie is NOT set. I do know about this issue https://katanaproject.codeplex.com/workitem/197 and I have tried all workarounds offered for this issue but none helped. Interestingly enough, when I try to drop my own cookie inside of SecurityTokenValidated event - n.Response.Cookies.Append("Test", "Test");, I can see the cookie is set properly.
One of the workarounds suggest implementing your own CookieManager. What makes me curious is that when I put a breakpoint into cookie setter in this custom manager, it is not hit, i.e. middleware seems not even trying to set the cookie. So the main question I have - at what point exactly the middleware will try to set the cookie? Is it when I set my AuthenticationTicket?
Edit 1: adding more information. I tried to compare with another web app, this time MVC, that I configured to use the same IdP and that works as expected. Startup code for both apps is the same. When debugging thru SecurityTokenValidated event, I can see that MVC app (working) has created System.Security.Principal.WindowsPrincipal identity while webforms app (non-working) created System.Security.Principal.GenericIdentity identity.
I have also added this little snipped
app.UseStageMarker(PipelineStage.Authenticate);
app.Use((context, next) =>
{
var identity = context.Request.User.Identity;
return next.Invoke();
});
just to see what identity get populated on this pipeline stage. For MVC app (working) I see the identity I added by setting AuthenticationTicket, for webforms app I still see non-authenticated GenericIdentity.
OK, this is embarrassing - the problem was in CookieAuthenticationOptions, apparently AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie is NOT the same as AuthenticationType = "Cookies". Once set this later way, it is working fine.
Can you use the default cookie manager and see if that results in a cookie being set?