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.
Related
I am trying to add multiple ways to authenticate users in our app. All but one are working flawlessly. I am able to log in with ASP.NET Core Identity, GitHub, Azure AD, and even API auth, but JWT is giving me a bit of a headache as I always get a 401 response when I pass in an bearer token to the authorization header.
This might have something to do with a custom middleware class that works on this header:
app.UseMiddleware<JwtAuthMiddleware>()
.UseAuthentication()
.UseAuthorization()
The middleware class in question:
public class JwtAuthMiddleware
{
private readonly RequestDelegate _next;
public JwtAuthMiddleware(RequestDelegate next)
{
_next = next;
}
public Task Invoke(HttpContext context)
{
string authHeader = context.Request.Headers["Authorization"];
if (authHeader != null)
{
string jwtEncodedString = authHeader[7..]; // Fetch token without Bearer
JwtSecurityToken token = new(jwtEncodedString: jwtEncodedString);
ClaimsIdentity identity = new(token.Claims, "jwt");
context.User = new ClaimsPrincipal(identity);
}
return _next(context);
}
}
The identity setup is fairly simple.
services
.AddAuthentication()
.AddCookie(/*config*/)
.AddGitHub(/*config*/)
.AddJwtBearer(/*config*/)
.AddAzureAd(/*config*/);
string[] authSchemes = new string[]
{
IdentityConstants.ApplicationScheme,
CookieAuthenticationDefaults.AuthenticationScheme,
GitHubAuthenticationDefaults.AuthenticationScheme,
JwtBearerDefaults.AuthenticationScheme,
"InHeader"
};
AuthorizationPolicy authnPolicy = new AuthorizationPolicyBuilder(authSchemes)
.RequireAuthenticatedUser()
.Build();
services.AddAuthorization(options =>
{
options.FallbackPolicy = authnPolicy;
});
I also set a filter in AddMvc:
.AddMvc(options =>
{
AuthorizationPolicy policy = new AuthorizationPolicyBuilder(authSchemes)
.RequireAuthenticatedUser()
.Build();
options.Filters.Add(new AuthorizeFilter(policy));
})
JWT works when options.FallbackPolicy is not set. Otherwise, I get a 401 response. Inspecting the DenyAnonymousAuthorizationRequirement in debug mode confirms this as there is no principal set with the right claims filled out. So it looks like context.User is ignored or reset.
Ideally, I would want to get rid of JwtAuthMiddleware altogether, but I still have to figure out how to combine JWT with cookie authentication in this particular setup. Any thoughts?
You don't need to use this custom JwtAuthMiddleware and define the custom ClaimsPrincipal like that. The problem is that you are just extracting the claims from the token. Setting the ClaimsPrincipal like that wouldn't automatically authenticate the user i.e. Context.User.Identity.IsAuthenticated would be false. Hence you get 401.
The token needs to be validated. You are already using AddJwtBearer which you can customize the code like below if you haven't done that.
JWT bearer authentication performs authentication automatically by extracting and validating a JWT token from the Authorization request header. However, you need to set the TokenValidationParameters which tells the handler what to validate.
services
.AddAuthentication()
.AddJwtBearer("Bearer", options =>
{
// you don't have to validate all of the parameters below
// but whatever you need to, but see the documentation for more details
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuer = config.JwtToken.Issuer, // presuming config is your appsettings
ValidateAudience = true,
ValidAudience = config.JwtToken.Audience,
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(config.JwtToken.SigningKey))
};
})
However, there is a caveat. When combining multiple authentication mechanisms, you need to utilise multiple authentication schemes. They play a role in defining the logic to select the correct authentication mechanism. That is because you can call AddJwtBearer or AddCookie multiple times for different scenarios. If the scheme name is not passed, it would try to utilize the default scheme and the authentication will fail, as it would have no way of knowing which mechanism to use JWT or AzureAd as they both use the Bearer token.
You mentioned that JWT works when FallbackPolicy is not set but you get 401 otherwise. That is because your authorization policy requires a user to be authenticated. The user was never authenticated as I mentioned in the beginning. If FallbackPolicy is not set it would work in the context of JWT being passed in the alright, but there is no requirement to check e.g user is authenticated and has a specific claim or role, so it works.
You would need to define an authentication policy scheme and set the ForwardDefaultSelector property of the PolicySchemeOptions. ForwardDefaultSecltor is used to select a default scheme for the current request that authentication handlers should forward all authentication operations to by default.
So basically you need to set the ForwardDefaultSecltor delegate which uses some logic to forward the request to the correct scheme to handle the authentication.
So the above code would change to:
// scheme names below can be any string you like
services
.AddAuthentication("MultiAuth") // virtual scheme that has logic to delegate
.AddGitHub(GitHubAuthenticationDefaults.AuthenticationScheme, /*config*/) // uses GitHub scheme
.AddAzureAd("AzureAd", /*config*/) // uses AzureAd scheme
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, /*config*/) // uses Cookies scheme
.AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options => { // above code for AddJwtBearer }) // uses Bearer scheme
// this is quite important to use the same virtual scheme name below
// which was used in the authentication call.
.AddPolicyScheme("MultiAuth", null, options =>
{
// runs on each request should return the exact scheme name defined above
options.ForwardDefaultSelector = context =>
{
// if the authorization header is present, use the Bearer scheme
string authorization = context.Request.Headers[HeaderNames.Authorization];
if (!string.IsNullOrEmpty(authorization) && authorization.StartsWith("Bearer "))
{
return "Bearer";
}
// custom logic for GitHub
...
return GitHubAuthenticationDefaults.AuthenticationScheme;
// custom logic for AzureAd
...
return "AzureAd";
// otherwise fallback to Cookies or whichever is the default authentication scheme
return CookieAuthenticationDefaults.AuthenticationScheme;
};
});
That way, you just remove the JwtAuthMiddleware. AddJwtBearer would automatically add the claims in the Claims object when the token is validated, which you can then use to define custom authorization policies to authorize the user.
I hope that helps.
#Shazad Hassan made some good points in his answer. The custom middleware was in fact not necessary in the end because the System.IdentityModel.Tokens.XXXX assemblies were throwing errors that the tokens were invalid. I never really knew the token was invalid because the middleware assumed it was correct (and content-wise, it was), while all this time the context was running for an unauthenticated user, which became apparent when I applied the fallback policy.
So I rewrote the token generation code and now I no longer get this issue, and I can safely remove the custom middleware. So far so good.
While I still have to bone up on authentication schemes and all that, for the moment it doesn't even seem necessary to have multiple calls to AddAuthentication because this works:
services
.AddAuthentication()
.AddCookie(configureCookieAuthOptions)
.AddGitHub(configureGithub)
.AddAzureAd(config)
.AddJwtBearer(configureJwtBearerOptions)
.AddApiKeyInHeader<ApiKeyProvider>("InHeader", options =>
{
options.Realm = "My realm";
options.KeyName = "X-API-KEY";
});
With this, I am able to log in with every method in the list. Looks promising, but might need to be reviewed by someone who knows the ins and outs of authentication in ASP.NET Core.
So: some clients will be sending a cookie, and some will be sending Authorize header with a bearer token.
These are authentication schemes that you set up with AddAuthentication.
After that you need to set up authorization (AddAuthorization) to grant access to users who are authentication with either of these schemes.
(Authentication means checking that you are who you say you are (users), authorization means what you are and aren't allowed to do (roles) -- it's confusing.)
This follows on from a previous post which started as a general issue and is now more specific.
In short, I've been following guidance (such as this from Microsoft, this from Scott Hanselman, and this from Barry Dorrans) to allow me to share the authentication cookie issued by a legacy ASP.NET web app with a new dotnet core app running on the same domain.
I'm confident that I'm using the recommended Microsoft.Owin.Security.Interop library correctly. On that side (the old ASP.NET app), the CookieAuthenticationOptions are configured with AuthenticationType and CookieName both set to the same value - SiteIdentity. This same value is also used in the interop data protector setup:
var appName = "SiteIdentity";
var encryptionSettings = new AuthenticatedEncryptorConfiguration
{
EncryptionAlgorithm = EncryptionAlgorithm.AES_256_CBC,
ValidationAlgorithm = ValidationAlgorithm.HMACSHA256
};
var interopProvider = DataProtectionProvider.Create(
new DirectoryInfo(keyRingSharePath),
builder =>
{
builder.SetApplicationName(appName);
builder.SetDefaultKeyLifetime(TimeSpan.FromDays(365 * 20));
builder.UseCryptographicAlgorithms(encryptionSettings);
if (!generateNewKey)
{
builder.DisableAutomaticKeyGeneration();
}
});
ShimmedDataProtector = new DataProtectorShim(
interopProvider.CreateProtector(
"Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationMiddleware",
appName,
"v2"));
I log in using this app, confirm I have a cookie named SiteIdentity then switch to a new dotnet core app running on the same domain.
There, without adding authentication middleware I can confirm that I can unprotect and deserialize the cookie. I do this by setting up data protection in Startup to match the other app:
var appName = "SiteIdentity";
services.AddDataProtection()
.PersistKeysToFileSystem(new DirectoryInfo(keyRingSharePath))
.SetDefaultKeyLifetime(TimeSpan.FromDays(365 * 20))
.DisableAutomaticKeyGeneration()
.UseCryptographicAlgorithms(new AuthenticatedEncryptorConfiguration()
{
EncryptionAlgorithm = EncryptionAlgorithm.AES_256_CBC,
ValidationAlgorithm = ValidationAlgorithm.HMACSHA256
})
.SetApplicationName(appName);
Then in my controller I can use a data protector to manually unprotect the cookie:
var appName = "SiteIdentity";
var protector = _dataProtectionProvider.CreateProtector(
"Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationMiddleware",
appName,
"v2");
var cookieValue = Request.Cookies[appName];
var format = new TicketDataFormat(protector);
var ticket = format.Unprotect(cookieValue);
I can confirm that ticket.Principal does indeed reference a claims principal representing the account which I signed in with on the other app.
However, I've found it impossible to wire up the cookie authentication middleware to properly protect my endpoints using this cookie. This is what I've added to Startup, after the data protection code above:
var protectionProvider = services.BuildServiceProvider().GetService<IDataProtectionProvider>();
var dataProtector = protectionProvider.CreateProtector(
"Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationMiddleware",
appName,
"v2");
services
.AddAuthentication(appName)
.AddCookie(appName, options =>
{
options.TicketDataFormat = new TicketDataFormat(dataProtector);
options.Cookie.Name = appName;
});
By my understanding this is telling the middleware that I have an authentication scheme named "SiteIdentity" (the advice is that authentication scheme must match the ASP.NET authentication type) which expects a cookie also called "SiteIdentity" which will contain protected data that the supplied data protector can interpret.
But when I add the attribute [Authorize(AuthenticationSchemes = "SiteIdentity")] to my controller I'm kicked away to a login page.
I can't understand what I'm doing wrong. As I've shown, I can confirm that it is indeed possible to use this data protector and ticket format to interpret the authentication cookie, so I guess I must have something wrong in this middleware wiring, but I'm not sure what.
Please ignore. It turns out that my code is actually correct. I had been working on this solution for long enough that the session represented by the cookie value I was using to test had expireed. Will leave this question here in case the code benefits anyone trying to achieve the same.
I am using the following code to successfully get a token from my MVC web app. However I am unsure how to retrieve the claims that I have added. Should they be returned in the same response as my token?
Thanks!
Startup.cs:
app.UseJwtBearerAuthentication(options =>
{
options.AutomaticAuthenticate = true;
options.Audience = "resource_server";
options.Authority = "https://www.example.com/";
options.RequireHttpsMetadata = false;
});
app.UseOpenIdConnectServer(options =>
{
options.ApplicationCanDisplayErrors = true;
options.AllowInsecureHttp = false;
options.Provider = new AuthorizationProvider();
options.TokenEndpointPath = "/connect/token";
});
Adding claims:
identity.AddClaim("custom_claim", "value", "token id_token");
foreach (string role in await userManager.GetRolesAsync(user))
{
identity.AddClaim(ClaimTypes.Role, role, "id_token token");
}
This is my PostAsync result:
{"resource":"resource_server","scope":"openid profile","token_type":"bearer","access_token":"eyJhbGciOiJSU....","expires_in":"3600"}
Should they be returned in the same response as my token?
Since you're specifying both the id_token and token destinations, your claims should be copied in both the access token and the identity token. You should be able to use any JSON parser to extract the claims you're looking for from the access_token/id_token properties.
Two remarks:
In ASOS beta4, you had to explicitly add scope=openid to your token request to get back an identity token, even if you called ticket.SetScopes("openid"), which probably explains why there's no id_token property in the response you shared. This policy was relaxed in the next version.
In ASOS beta4 (for ASP.NET Core RC1), access tokens were serialized using the JWT format. This is no longer true with the beta5 version, that uses an encrypted format by default. Don't try to read them from the client application: instead, use the id_token, which is meant to be consumed by the client app.
I am currently struggling with setting the timeout on the cookie/auth token when authenticating my .NET Core App using Azure AD via the OpenIdConnect authentication model.
The sign-in scheme is being set in the ConfigureServices method via the following:
services.AddAuthentication(options => options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme);
I am then setting up my configuration as follows:
app.UseCookieAuthentication(new CookieAuthenticationOptions()
{
CookieName = "MyCookie",
ExpireTimeSpan = TimeSpan.FromHours(2)
});
app.UseOpenIdConnectAuthentication(new OpenIdConnectOptions()
{
Authority = authorityUri.AbsoluteUri,
ClientId = azureOptions.ClientId,
ClientSecret = azureOptions.ClientSecret,
ResponseType = OpenIdConnectResponseTypes.CodeIdToken,
Events = new OpenIdConnectEvents()
{
OnAuthorizationCodeReceived = async context =>
{
await aAuthenticateMiddleware.OnAuthenticate(context, logger);
}
}
});
app.UseMiddleware<aAuthenticateMiddleware>();
Note, that I am not using the built in Identity (as its not practical for our purposes) but rather using a custom middleware.
Within the middleware layer I am checking whether the user is authenticated and if not a challenge is issued:
var authenticationProperties = new AuthenticationProperties() { RedirectUri = context.Request.Path.Value ?? "/" };
authenticationProperties.AllowRefresh = false;
authenticationProperties.IssuedUtc = DateTime.Now;
authenticationProperties.ExpiresUtc = DateTime.Now.AddHours(2);
await context.Authentication.ChallengeAsync(
authenticationManager.IdentityProvider.AuthenticationScheme,
authenticationProperties,
ChallengeBehavior.Automatic
);
This is all works fine and authenticates the user correctly etc however this is issuing the auth token (and cookie) with a 15 minute expiry and ignoring my 2 hour expiry that I have tried setting.
I have been referring to the latest source examples from GitHub from the aspnet/security repository for examples.... however none of these mention anything about overriding the default expiry issued.
https://github.com/aspnet/Security/tree/dev/samples/OpenIdConnect.AzureAdSample
Most examples I have found are still referencing the old AspNet libraries rather than the AspNetCore libraries.
Some articles suggest that using the SignInAsync with persistent set to True allows the ExpireTimeSpan to be honored, however this throws a "Not Supported Exception" when calling it. Perhaps SignInAsync is not supported via Azure AD?
Does anyone have any insight on how to achieve this?
in UseOpenIdConnectAuthentication set UseTokenLifetime = false
I have a web application. My requirement is that i need to generate oauth2 bearer token on every login. Currently we are using thinktecture to generate token, but this procedure is taking almost 7 seconds to generate token everytime. Is there any way i can generate token without using thinktecture ?
If you have created a new ASP.NET Web Application -> Web API with Individual User Accounts. Have a look at App_Start -> Startup.Auth.cs.
It should contain something like this:
PublicClientId = "self";
OAuthOptions = new OAuthAuthorizationServerOptions
{
TokenEndpointPath = new PathString("/Token"),
Provider = new ApplicationOAuthProvider(PublicClientId),
AuthorizeEndpointPath = new PathString("/api/Account/ExternalLogin"),
AccessTokenExpireTimeSpan = TimeSpan.FromDays(14),
// In production mode set AllowInsecureHttp = false
AllowInsecureHttp = true
};
// Enable the application to use bearer tokens to authenticate users
app.UseOAuthBearerTokens(OAuthOptions);
This means that you can send a request for an access token, example request:
You can then verify that the access token works:
With this token you can now access all protected resources that the user has access to.
Asp.net default implementation will use DPAPI in your Authorization Server, so it will use the “validationKey” value in machineKey node stored in machine.config file to issue the access token and protect it. The same case applies when you send the access token to your Resource Server, it will use the same machineKey to decrypt the access token and extract the authentication ticket from it.
ASP.NET
If you want to generate a JWT encoded Bearer Token, you should override ISecureDataFormat<AuthenticationTicket>.Protect() Method:
CustomJwtFormat.cs
string symmetricKeyAsBase64 = audience.Base64Secret;
var keyByteArray = TextEncodings.Base64Url.Decode(symmetricKeyAsBase64);
var signingKey = new HmacSigningCredentials(keyByteArray);
var issued = data.Properties.IssuedUtc; var expires = data.Properties.ExpiresUtc;
JwtSecurityToken token = new JwtSecurityToken(_issuer, audienceId, data.Identity.Claims, issued.Value.UtcDateTime,expires.Value.UtcDateTime, signingKey);
var handler = new JwtSecurityTokenHandler();
//serialize the JSON Web Token to a string
var jwt = handler.WriteToken(token);
return jwt;
Add your custom JWT formatter to OAuth Option
OAuthAuthorizationServerOptions OAuthServerOptions = new
OAuthAuthorizationServerOptions()
{
//For Dev enviroment only (on production should be AllowInsecureHttp = false)
AllowInsecureHttp = true,
TokenEndpointPath = new PathString("/oauth/token"),
AccessTokenExpireTimeSpan = TimeSpan.FromMinutes(30),
AccessTokenFormat = new CustomJwtFormat("http://localhost:5001")
};
// Generation and validation
app.UseOAuthBearerTokens(OAuthServerOptions);
The app.UseOAuthBearerTokens helper method creates both the token server and the middleware to validate tokens for requests in the same application.
If this is an Authorization server(generate token) , you should use app.UseOAuthAuthorizationServer(OAuthServerOptions) in the last line
ASP.NET Core
Unforturnately, the ASP.NET team simply decided not to port OAuthAuthorizationServerMiddleware to asp.net core: https://github.com/aspnet/Security/issues/83
community-provided, open source authentication options for ASP.NET Core:
AspNet.Security.OpenIdConnect.Server:low-level, protocol-first OpenID Connect server framework for ASP.NET Core and OWIN/Katana.
IdentityServer:OpenID Connect and OAuth 2.0 framework for ASP.NET Core, officially certified by the OpenID Foundation and under governance of the .NET Foundation.
OpenIddict: easy-to-use OpenID Connect server for ASP.NET Core.
I followed below article http://bitoftech.net/2014/06/01/token-based-authentication-asp-net-web-api-2-owin-asp-net-identity/
Downloaded their sourcecode and checked it. They have good example on how to create token.