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?
Related
I have a .net mvc project based on b2c-webapi-dotnet ([https://github.com/Azure-Samples/active-directory-b2c-dotnet-webapp-and-webapi][1]) where I can register users or allow them to sign in via b2c (both operations having the same SignUpSignIn policy). This works as I expect.
I'm now trying to split this single policy into two, so having a SignUp policy and an additional SignIn policy
As part of this, both policies need to hit the AuthorizationCodeReceived hook so that I can pull out the b2c oid guid. This is used to find additional information on a user stored in a separate database.
What I am finding is that OnAuthorizationCodeReceived is only called when using one of my policies, its the default policy that I set into the MetadataAddress property when setting up the authorization.
In the code below, whichever policy I set for Settings.B2C_DefaultPolicyId (either SignIn or SignUp) is the one that has OnAuthorizationCodeReceived called.
Can anyone tell me if there is a way to have both policies be able to call OnAuthorizationCodeReceived ?
I appreciate any help you can give.
public void ConfigureAuth(IAppBuilder app)
{
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
// ASP.NET web host compatible cookie manager
CookieManager = new SystemWebChunkingCookieManager()
});
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
// Generate the metadata address using the tenant and policy information
MetadataAddress = String.Format(WellKnownMetadata, Common.Settings.AppSettings.B2C_Tenant, Settings.B2C_DefaultPolicyId),
// These are standard OpenID Connect parameters, with values pulled from web.config
ClientId = Settings.AppSettings.B2C_ClientID,
RedirectUri = RedirectUri,
PostLogoutRedirectUri = PostLogoutRedirectUri,
// Specify the callbacks for each type of notifications
Notifications = new OpenIdConnectAuthenticationNotifications
{
RedirectToIdentityProvider = OnRedirectToIdentityProvider,
AuthorizationCodeReceived = OnAuthorizationCodeReceived,
AuthenticationFailed = OnAuthenticationFailed,
},
// Specify the claim type that specifies the Name property.
TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = "name",
ValidateIssuer = false
},
// Specify the scope by appending all of the scopes requested into one string (separated by a blank space)
Scope = $"openid profile offline_access {Settings.AppSettings.B2C_ReadTasksScope} {Settings.AppSettings.B2C_WriteTasksScope}",
// ASP.NET web host compatible cookie manager
CookieManager = new SystemWebCookieManager()
}
);
}
I've figured it out with the help of a colleague
From the example code I had, one of the callbacks had some code that needed to be updated
private Task OnRedirectToIdentityProvider(RedirectToIdentityProviderNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions> notification)
{
var policy = notification.OwinContext.Get<string>("Policy");
if (!string.IsNullOrEmpty(policy) && !policy.Equals(Globals.DefaultPolicy))
{
notification.ProtocolMessage.Scope = OpenIdConnectScope.OpenId;
notification.ProtocolMessage.ResponseType = OpenIdConnectResponseType.IdToken;
notification.ProtocolMessage.IssuerAddress = notification.ProtocolMessage.IssuerAddress.ToLower().Replace(Globals.DefaultPolicy.ToLower(), policy.ToLower());
}
return Task.FromResult(0);
}
The ResponseType needs to be changed from IdToken to CodeIdToken. Doing this means the non default Policy also hits the callback AuthorizationCodeReceived
One side affect of doing this is that after OnAuthorizationCodeReceived is hit, the callback for AuthenticationFailed is also hit.
To handle this, I've just put a conditional statement in the OnAuthenticationFailed method that returns to the root view (notification.Response.Redirect("/"))
I am trying to add support for SAML authentication to an ASP.NET Core MVC application with ASP.NET Core Identity (not IdentityServer). The flow "works" when testing with StubIdp - the SAMLResponse is POSTed to /Saml2/Acs and I'm redirected to the app with an Identity.External cookie, but my ClaimsPrincipal is empty and unauthenticated. Even if I use the NameID of a user who already exists in the database, the claims are completely empty.
I also see the following in the console log:
Sustainsys.Saml2.AspNetCore2.Saml2Handler: Information: Successfully processed SAML response Microsoft.IdentityModel.Tokens.Saml2.Saml2Id and authenticated JohnDoe
I installed the Sustainsys.Saml2.AspNetCore2 package, and added the service configuration to startup.cs as follows:
services.AddAuthentication()
.AddSaml2(async options =>
{
var azureServiceTokenProvider = new AzureServiceTokenProvider();
var keyVaultClient = new KeyVaultClient(
new KeyVaultClient.AuthenticationCallback(
azureServiceTokenProvider.KeyVaultTokenCallback));
var certificateSecret = await keyVaultClient.GetSecretAsync($"https://{Configuration["KeyVaultName"]}.vault.azure.net/", Configuration["ServiceProviderCertName"]);
var privateKeyBytes = Convert.FromBase64String(certificateSecret.Value);
options.SPOptions.EntityId = new EntityId(Configuration["BaseUrl"] + "/Saml2");
options.SPOptions.ReturnUrl = new Uri(Configuration["BaseUrl"]);
IdentityProvider idp = new IdentityProvider(
new EntityId("https://stubidp.sustainsys.com/Metadata"), options.SPOptions)
{
LoadMetadata = true,
MetadataLocation = "https://stubidp.sustainsys.com/Metadata",
AllowUnsolicitedAuthnResponse = true
};
options.IdentityProviders.Add(idp);
options.SPOptions.ServiceCertificates.Add(new X509Certificate2(privateKeyBytes));
});
Configuration["BaseUrl"] is the base URL of my app, in this case a localhost port.
I'm obviously missing something, but I can't figure out what. Do I need to somehow explicitly connect/map the Saml2 service to ASP.NET Core Identity?
Was able to resolve this based on comments in this GitHub issue.
My comment explaining how I was able to implement the workaround: https://github.com/Sustainsys/Saml2/issues/1030#issuecomment-616842796
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.
This should be redirecting my app to my AdFs signOut Page, and then redirect me back to my app.
However, it simply redirects me to my route "/logout".
Watching the log on my ADFS server nothing happens.
[AllowAnonymous]
[HttpGet]
[Route("api/logout")]
public async Task<IActionResult> Logout()
{
return SignOut(new AuthenticationProperties()
{
RedirectUri = "/logout"
},
Saml2Defaults.Scheme);
}
SignIn works fine. I even tried this same approach, but does not work. Here, the ReturnUrl method gets the location from HttpContext.Response.Header. When I try this for the logout, the location is always null.
[AllowAnonymous]
[HttpGet]
[Route("api/login")]
public async Task<string> LoginAdfs()
{
string redirectUri = _appSettings.Saml.SpEntityId;
await HttpContext.ChallengeAsync(new AuthenticationProperties
{
RedirectUri = string.Concat(redirectUri, "/autenticado")
});
return ReturnUrl();
}
Any idea what could be happening?
UPDATE 21/11/2019
Turns out the Saml2Handler is simply not trying to send the request to the server. I'm getting these messages on my output window:
Sustainsys.Saml2.AspNetCore2.Saml2Handler: Debug: Initiating logout, checking requirements for federated logout
Issuer of LogoutNameIdentifier claim (should be Idp entity id):
Issuer is a known Idp: False
Session index claim (should have a value):
Idp has SingleLogoutServiceUrl:
There is a signingCertificate in SPOptions: True
Idp configured to DisableOutboundLogoutRequests (should be false):
Sustainsys.Saml2.AspNetCore2.Saml2Handler: Information: Federated logout not possible, redirecting to post-logout
Here is my StartUp Configuration, I don't get what is wrong here:
ServiceCertificate se = new ServiceCertificate()
{
Certificate = new X509Certificate2(SpCert, "",X509KeyStorageFlags.MachineKeySet),
Use = CertificateUse.Signing
};
SPOptions sp = new SPOptions
{
AuthenticateRequestSigningBehavior = SigningBehavior.Never,
EntityId = new EntityId(SpEntityId),
ReturnUrl = new Uri("/login"),
NameIdPolicy = new Sustainsys.Saml2.Saml2P.Saml2NameIdPolicy(null, Sustainsys.Saml2.Saml2P.NameIdFormat.Unspecified),
};
sp.ServiceCertificates.Add(se);
IdentityProvider idp = new IdentityProvider(new EntityId(appSettings.Saml.EntityId), sp);
idp.Binding = Saml2BindingType.HttpPost;
idp.AllowUnsolicitedAuthnResponse = true;
//idp.WantAuthnRequestsSigned = true;
idp.SingleSignOnServiceUrl = new Uri("/login");
//idp.LoadMetadata = true;
idp.SigningKeys.AddConfiguredKey(new X509Certificate2(IdpCert));
idp.MetadataLocation = theMetadata;
idp.DisableOutboundLogoutRequests = true;
For the logout to work, two special claims "LogoutNameIdentifier" and "SessionIndex" (full names are http://Sustainsys.se/Saml2/LogoutNameIdentifier and http://Sustainsys.se/Saml2/SessionIndex need to be present on the user. Those carries information about the current session that the Saml2 library needs to be able to do a logout.
Now I don't see your entire Startup, so I cannot understand your application's flow. But those claims should be present in the identity returned by the library - possibly stored in an External cookie (if you are using asp.net identity). When your application then sets the application cookie those two claims must be carried over to the session identity.
Also you have actually disabled outbound logout with DisableOutboundLogoutRequests. But that's not the main problem here as your logs indicates that the required claims are not present.
From my own experience, the two claims, as mentioned by Anders Abel, should be present on the user. I had not seen these claims until I passed all of the claims along with the sign-in request. ASP.NET Core recreates the principal on SignInAsync and needs claims to be passed in with the request.
With the following, I am able to fulfill a SingleLogout with my service:
await HttpContext.SignInAsync(user.SubjectId, user.Username, props, user.Claims.ToArray());
what you are using as a service provider.
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.