I need to configure my .Net Core Web Api (.Net Framework) to use ADFS 3.0 (2012) to validate the Bearer tokens sent by our mobile clients.
I am able to generate the access_token from the ADFS server, and I pass it in the Authorization header.
My problem is in the API: how do I configure it to validate and autorize the user?
I searched in many places and I could not find a definitive method of doing it.
What I tried so far:
Used IdentityServer4 (Failed because it uses JWT and ADFS doesn't offer OpenID
Tried UseOpenIdConnectAuthentication (found example at IdentityServer4)
a custom Middleware
I can't use another method, I need to support oAuth2.
So, how do I do it?
Here is my latest try:
var connectOptions = new OpenIdConnectOptions
{
AuthenticationScheme = "adfs",
SignInScheme = "idsrv.external", //IdentityServerConstants.ExternalCookieAuthenticationScheme,
SignOutScheme = "idsrv", //IdentityServerConstants.SignoutScheme,
AutomaticChallenge = false,
DisplayName = "ADFS",
Authority = $"https://{options.AdfsHostName}/adfs/oauth2",
ClientId = options.ClientID,
ResponseType = "id_token",
Scope = { "openid profile" },
CallbackPath = new PathString("/signin-adfs"),
SignedOutCallbackPath = new PathString("/signout-callback-adfs"),
RemoteSignOutPath = new PathString("/signout-adfs"),
ClaimsIssuer = $"https://{options.AdfsHostName}/adfs/services/trust",
//TokenValidationParameters = new TokenValidationParameters
//{
// ValidateIssuer = true,
// ValidIssuer = $"https://{options.AdfsHostName}/adfs/services/trust"
//},
};
app.UseOpenIdConnectAuthentication(connectOptions);
I get a very quick 401 on every calls, with a valid token. In fact, while I see the connection in the console window, I don't see any other log in the Roslyn console window regarding the security validation.
I'm currently using ASP.Net Core 1.1.X, and if I can I'd avoid moving to .Net Core 2.0, as we are late in the project and it contains many breaking changes...
Feel free to ask for more info, and I'll appreciate all the good advices!
As it turns out, we can use the JwtBearerAuthentication with ADFS 3.0.
My initial problem with it was that it went to fetch the metadata at /.well-known/openid-configuration, but ADFS 3.0 does not support OpenID and this returns a 404.
I read in another post (I'll update it when I find it) that if with the right configuration, it won't need to fetch the config. But what configuration?
Well I found deep in the (MS) code that if one pass an OpenIdConnectConfiguration object to the Configuration property of the JwtBearerOptions, it wont fetch the metadata.
So here is my code now:
var rawCertData = Convert.FromBase64String(options.X509SigninCertificate);
X509Certificate2 cert = new X509Certificate2(rawCertData);
SecurityKey signingKey = new X509SecurityKey(cert);
The X509 cert data comes from the supported adfs metadata at this url
https://Your.ADFS.Site/FederationMetadata/2007-06/FederationMetadata.xml
It contains this:
<KeyDescriptor use="signing">
<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<X509Data>
<X509Certificate>SOMEUUENCDODEDSTRING=</X509Certificate>
</X509Data>
</KeyInfo>
</KeyDescriptor>
I simply copied the UUEncoded string in my settings' X509SigninCertificate property.
var tokenValidationParameters = new TokenValidationParameters
{
// The signing key must match!
ValidateIssuerSigningKey = true,
IssuerSigningKey = signingKey,
// Validate the JWT Issuer (iss) claim
ValidateIssuer = true,
ValidIssuer = $"https://{options.AdfsHostName}/adfs/services/trust",
// Validate the JWT Audience (aud) claim
ValidateAudience = true,
ValidAudience = options.ClientUri, //"https://YOUR-AUDIENCE/",
// Validate the token expiry
ValidateLifetime = true,
// If you want to allow a certain amount of clock drift, set that here:
ClockSkew = TimeSpan.Zero
};
var connectOptions = new OpenIdConnectConfiguration
{
Issuer = $"https://{options.AdfsHostName}/adfs/services/trust",
};
app.UseJwtBearerAuthentication(new JwtBearerOptions
{
AutomaticAuthenticate = true,
AutomaticChallenge = true,
TokenValidationParameters = tokenValidationParameters,
Configuration = connectOptions
});
The important line here is
Configuration = connectOptions
By doing this, you tell the validator to not fetch the metadata. Simple as that.
I was able to validate my token (AUD, ISS and SIGN) and I can use ADFS in my project.
Only ADFS 2016 supports OpenID Connect. If you want to use the OAuth endpoint in 2012, you need to write your own authorisation handler. An example to build on would be ASP.NET Core's own Twitter implementation. Note that these handlers need to be implemented differently in ASP.NET Core 1.* vs 2.0+.
Related
On startup I have the following
var keys = GetKeys();
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
RequireExpirationTime = true,
ValidateIssuer = false,
ValidateAudience = false,
IssuerSigningKeys = keys
};
});
The list of valid keys can change at runtime - is it possible to update the list that the authentication layer is using without having to restart the server?
The idea is that we have multiple client servers calling our API and they all have their own keys they use for signing the JWT.
New clients can be added at any time (via another API).
This is a requirement that I've received from above and it's unlikely I can get the design changed.
There is a delegate IssuerSigningKeyResolver, in the TokenValidationParameters, that you can set while configuring the other options. On every request authentication, your delegate will be executed. You can dynamically return the the SecurityKey.This post can refer toļ¼ASP.NET Core - change JWT SecurityKey during runtime
I have a few endpoints in an ASP.NET core application that's also hosting IdentityServer.
When I issue a JWT token to a client, and that client calls an [AllowAnonymous] endpoint with the JWT as a bearer token, the User principal is empty. It looks like the token isn't being parsed by any middleware, and there doesn't seem to be an option to specify always attempting to parse a JWT.
Is there a way to get this handled automatically or do I need to use the .AddJwtBearer extension? If the latter I can't seem to find an easy way for me to populate the signingkey such that it matches the ones configured for identity server.
var tokenValidationParameters = new TokenValidationParameters
{
// The signing key must match!
ValidateIssuerSigningKey = true,
IssuerSigningKey = ?? signing keys from Identity Server,
// Validate the JWT Issuer (iss) claim
ValidateIssuer = true,
ValidIssuer = // I guess this is typically the public URL for this instance,
// Validate the token expiry
ValidateLifetime = true,
// If you want to allow a certain amount of clock drift, set that here:
ClockSkew = TimeSpan.Zero
};
app.UseJwtBearerAuthentication(new JwtBearerOptions
{
AutomaticAuthenticate = true,
AutomaticChallenge = true,
TokenValidationParameters = tokenValidationParameters
});
I would guess that for the SigningKey I could pass through the X509 that I'm configuring on IdentityServer via the x509SecurityKey class
UseJwtBearerAuthentication is obsolete. Configure JwtBearer authentication with AddAuthentication().AddJwtBearer in ConfigureServices.
Also make sure app.UseAuthentication(); is added in your startup class.
I find a best practice is to always host IdentityServer, client and API on separate ASP.NET Core instances, because otherwise its hard to reason about what is what. IdentityServer issues its own User and cookie so I think your system is confused about what user to use?
Reference:
JwtBearerAppBuilderExtensions.UseJwtBearerAuthentication Method
Auth 2.0 Migration announcement
I'm having some difficulty understanding how to use Windows Authentication to authenticate a user, but then return a JWT to the client containing that authenticated user's claims. This is using .Net Core 2.0.
I've put the following in Startup.cs.
services.AddAuthentication
(
IISDefaults.AuthenticationScheme
).AddJwtBearer("Bearer", options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateAudience = false,
ValidateIssuer = false,
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("someKey")),
ValidateLifetime = true,
};
}
);
However, it doesn't seem that this is sufficient to generate the token. Some other guidance I've read suggests the token should be generate manually as part of an authorization routine but this seems to be tailored for when validating a username/password against a database or other provider. (For instance: https://fullstackmark.com/post/13/jwt-authentication-with-aspnet-core-2-web-api-angular-5-net-core-identity-and-facebook-login.) But with windows auth there does not appear to be an event or other structure that allows me to do this.
Any ideas?
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.
I have an IdentityServer4 app based on the IS4 Identity sample, and an API using bearer tokens for it's Authorization via IS4.AccessTokenValidation. This is working fine on localhost via VisualStudio, and when I deploy to a Windows 2012 VM and hosted via IIS. When I deploy the Identity server to Azure as an App Service website, all is fine too. However when the API is deployed as an App Service using same domain and certificate as the VM, any method with an Authorize attribute (with a policy or none it doesn't matter) always returns a 401 with the header message:
Www-Authenticate: Bearer error="invalid_token", error_description="The signature key was not found"
We're using .NET 4.5.2, with the latest releases of IdentityServer4, and IdentityServer4.AccessTokenValidation packages. I've also pulled the latest of these packages from GitHub from 30/08/16 with no change. I don't think it's a bug is IS4 Validator anyway, but I don't know what might cause this. Any suggestions? Is it an Azure host bug?
I'd love to be able to debug this, but I can't get Remote Debug working to this app even when I rebuilt from scratch, and app logs tell me nothing. I've had a rummage in the ASP.NET Security repo, but without more logging or debug access, I'm pretty clueless how to fix this problem.
API Configure is very basic:
var jwtBearerOptions = new JwtBearerOptions()
{
Authority = Configuration["Authentication:IdentityServer:Server"],
Audience = Configuration["Authentication:IdentityServer:Server"]+"/resources",
RequireHttpsMetadata = false,
AutomaticAuthenticate = true,
AutomaticChallenge = true,
};
app.UseJwtBearerAuthentication(jwtBearerOptions);
and the Identity Server is straight out of the samples, and using a purchased certificate for signing.
Has anyone else got this configuration fully working as 2 Azure App Services? Or what might possibly cause this error given the same bearer token sent to the VM hosted API is acceptable.
It turned out you need to explicitly set the IssuerSigningKey in TokenValidationParameters. So I get the certificate from the App Service store, and add it via JwtBearerOptions.TokenValidationParameters. So Startup config looks like this:
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
...
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap = new Dictionary<string, string>();
var tokenValidationParameters = new TokenValidationParameters
{
// The signing key must match!
ValidateIssuerSigningKey = true,
IssuerSigningKey = new X509SecurityKey(GetSigningCertificate()),
// Validate the JWT Issuer (iss) claim
ValidateIssuer = false,
//ValidIssuer = "local",
// Validate the JWT Audience (aud) claim
ValidateAudience = false,
//ValidAudience = "ExampleAudience",
// Validate the token expiry
ValidateLifetime = true,
// If you want to allow a certain amount of clock drift, set that here:
ClockSkew = TimeSpan.Zero
};
var jwtBearerOptions = new JwtBearerOptions()
{
Authority = Configuration["Authentication:IdentityServer:Server"],
Audience = Configuration["Authentication:IdentityServer:Server"]+"/resources",
RequireHttpsMetadata = false,
AutomaticAuthenticate = true,
AutomaticChallenge = true,
TokenValidationParameters = tokenValidationParameters
};
app.UseJwtBearerAuthentication(jwtBearerOptions);
app.UseMvc();
...
}
No idea why this is only needed on the Azure App Service and not on a server or development machine. Can anyone else explain it? It would suggest ValidateIssuerSigningKey default to true for App Service and false anywhere else.