I am unclear on how to achieve multiple alternate authentication options with precedence -
I can successfully separately implement
OAuth (Single Sign On)
API key Authentication (passed as a header)
What I am unclear on - how do I configure it so that if the API key header is present and the API key is valid to process the request; that is if you have a valid API-Key, you do not need the SSO.
If no API-Key is presented then you should expect to pass SSO.
The default should be SSO - hence I have the following configured in my Program.cs
builder.Services
.AddAuthentication(
options =>
{
// If an authentication cookie is present, use it to get authentication information
options.DefaultAuthenticateScheme =
CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
// If authentication is required, and no cookie is present,
// use OAuth to sign in
options.DefaultChallengeScheme = "OAuth";
}
)
.AddCookie(
options =>
{
options.AccessDeniedPath = "/accessdenied";
}
)
.AddOAuth(
"OAuth",
(OAuthOptions options) =>
{
// abstracted out but takes care of claims, etc...
WebApp.Utils.OAuthHelper.Process(options, OAuthConfig);
}
);
But my question is - how do I configure this to say - if API Key Header is present, don't bother with OAuth.
The AuthenticationSchemeOptions class has ForwardDefaultSelector that allows any AuthenticationHandler to forward authentication events to another handler.
So what you can do is make your ApiKeyAuthenticationHandler the detault and make it forward authentication events to OAuth handler if API Key header is not present in the HTTP headers.
For example: Let's say you implement ApiKeyAuthenticationHandler as Follows:
// API Key options
public class ApiKeyAuthenticationOptions : AuthenticationSchemeOptions
{
public string ApiKeyHeaderName { get; set; } = "X-MY-API-KEY";
// Add more options here if needed
}
// API Key Authentication handler
public class ApiKeyAuthenticationHandler : AuthenticationHandler<ApiKeyAuthenticationOptions>
{
// Inject any additional services needed by your key handler here
public ApiKeyAuthenticationHandler(IOptionsMonitor<ApiKeyAuthenticationOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) : base(options, logger, encoder, clock)
{
}
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
// Validation logic here
// Check if the header exists
if (!Context.Request.Headers.TryGetValue(Options.ApiKeyHeaderName, out var headerValues))
{
// Header is not present fail
return AuthenticateResult.Fail("Invalid API key.");
}
// Header is present validate user
var user = await GetApiKeyUserAsync(headerValues.First());
if (user == null)
{
return AuthenticateResult.Fail("Invalid API key.");
}
// API Key is valid, generate a ClaimIdentity and Principal for the authenticated user
var identity = new ClaimsIdentity(new Claim[]
{
new Claim(ClaimTypes.Name, user)
}, Scheme.Name);
var principal = new ClaimsPrincipal(identity);
return AuthenticateResult.Success(new AuthenticationTicket(principal, null, Scheme.Name));
}
// Key validation logic here and you can map an API key to a user
private async Task<string?> GetApiKeyUserAsync(string key)
{
// Add your implementation here
if (key == "Alice's Key")
{
return "Alice";
}
if (key == "Bob's Key")
{
return "Bob";
}
return null;
}
}
Now in the startup class call AddAuthentication and make "ApiKey" the default, and add a Selector function that checks the HTTP Headers for the presence of a key, if the key is present, then return null and let the Api key handler handle the authentication, otherwiser forward to "OAuth".
builder.Services
.AddAuthentication(
options =>
{
options.DefaultAuthenticateScheme =
"ApiKey";
}
)
.AddScheme<ApiKeyAuthenticationOptions, ApiKeyAuthenticationHandler>("ApiKey", apiKeyOptions =>
{
// This scheme will forward Authentication to OAUTH if API Key header is not present
apiKeyOptions.ForwardDefaultSelector = (context) =>
{
// Check the HTTP Request for the presence of Header
if (!context.Request.Headers.TryGetValue(apiKeyOptions.ApiKeyHeaderName, out var headerValues))
{
// Header is not present, forward to OAuth
return "OAuth"; // "OAuth" is the scheme name you specified when you called "AddOAuth()
}
// Do not forward the authentication
return null;
};
})
// Add other schemes here
Related
I am using ASP.NetCore 2.2 (probably moving to 3.0 soon). I have an Azure App Service application.
I want to have an API that clients will use an API token (client secret) to authenticate with so that they can run without requiring interactive authorization.
The UI portion will require Azure Active Directory authentication.
How do I wire this up these two different auth methods my ASP.Net Core app?
How-to
Firstly, we need an AuthenticationHandler & Options to authenticate request with an API Token(Client Secret). Suppose you've created such an Authentication Handler & Options:
public class ClientSecretAuthenOpts : AuthenticationSchemeOptions
{
public const string DefaultAuthenticationSchemeName = "ClientSecret";
}
public class ClientSecretAuthenticationHandler : AuthenticationHandler<ClientSecretAuthenOpts>
{
public ClientSecretAuthenticationHandler(IOptionsMonitor<ClientSecretAuthenOpts> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock)
: base(options, logger, encoder, clock) { }
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
// ... authenticate request
}
}
And then register multiple Authentication Schemes one by one:
// add mulitple authentication schemes
services.AddAuthentication(AzureADDefaults.AuthenticationScheme) // set AzureAD as the default for users (using the UI)
.AddAzureAD(options => Configuration.Bind("AzureAD", options)) // setup AzureAD Authentication
.AddScheme<ClientSecretAuthenOpts,ClientSecretAuthenticationHandler>( // setup ClientSecret Authentication
ClientSecretAuthenOpts.DefaultAuthenticationSchemeName,
opts=>{ }
);
// post configuration for OIDC
services.Configure<OpenIdConnectOptions>(AzureADDefaults.OpenIdScheme, options =>{
options.Authority = options.Authority + "/v2.0/"; // Microsoft identity platform
options.TokenValidationParameters.ValidateIssuer = false; // accept several tenants
});
Finally, in order to enable multiple authentication schemes at the same time, we need override the default policy:
services.AddAuthorization(opts => {
// allow AzureAD & our own ClientSecret Authentication at the same time
var pb = new AuthorizationPolicyBuilder(
ClientSecretAuthenOpts.DefaultAuthenticationSchemeName,
"AzureAD"
);
opts.DefaultPolicy = pb.RequireAuthenticatedUser().Build();
});
Demo & Test
Suppose your API token (client secret) is sent in Request Header as below :
GET https://localhost:5001/Home/Privacy HTTP/1.1
Api-Subscription-Id: Smith
Api-Subscription-Key: top secret
To avoid hardcoding the header name, I create add two properties in the options:
public class ClientSecretAuthenOpts : AuthenticationSchemeOptions
{
public const string DefaultAuthenticationSchemeName = "ClientSecret";
public string ApiClientIdHeadername {get;set;}= "Api-Subscription-Id";
public string ApiClientTokenHeaderName {get;set;}= "Api-Subscription-Key";
}
In order to authenticate above request, I create a custom Authentication Handler as below:
public class ClientSecretAuthenticationHandler : AuthenticationHandler<ClientSecretAuthenOpts>
{
public ClientSecretAuthenticationHandler(IOptionsMonitor<ClientSecretAuthenOpts> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock)
: base(options, logger, encoder, clock) { }
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
// if there's no header for Client ID & Client Sercet, skip
if(
Context.Request.Headers.TryGetValue(Options.ApiClientIdHeadername, out var clientIdHeader) &&
Context.Request.Headers.TryGetValue(Options.ApiClientTokenHeaderName, out var clientSecretHeader)
){
// validate client's id & secret
var clientId = clientIdHeader.FirstOrDefault();
var clientKey = clientSecretHeader.FirstOrDefault();
var (valid, id) = await ValidateApiKeyAsync(clientId, clientKey);
if(!valid){
return AuthenticateResult.Fail($"invalid token:{clientKey}");
}else{
var principal = new ClaimsPrincipal(id);
var ticket = new AuthenticationTicket(principal, new AuthenticationProperties(), this.Scheme.Name);
return AuthenticateResult.Success(ticket);
}
}
return AuthenticateResult.NoResult();
}
private Task<(bool, ClaimsIdentity)> ValidateApiKeyAsync(string clientId,string clientSecret)
{
ClaimsIdentity id = null;
// fake: need check key against the Database or other service
if(clientId=="Smith" && clientSecret == "top secret"){
id = new ClaimsIdentity(
new Claim[]{
new Claim(ClaimTypes.NameIdentifier, "client id from db or from the request"),
new Claim("Add Any Claim", "add the value as you like"),
// ...
}
,this.Scheme.Name
);
return Task.FromResult((true, id));
}
return Task.FromResult((false,id));
}
}
Test
Let's say we have a controller Action annotated with the [Authorize] attribute
[Authorize]
public IActionResult Privacy()
{
return Ok("hello,world");
}
When accessing the url within a browser (UI, without the header), the user will be redirected to Azure AD Authentication if he's not signed in.
When testing the above request with a client secret,
And we'll get the "hello,world" response:
Is there a way to use JWT bearer authentication AND a custom authentication method in .net core? I want all actions to default to JWT, except in a few cases where I want to use a custom authentication header.
I finally figured out how to do it. This example uses JWT authentication by default and custom authentication in certain rare cases. Please note, from what I've read, Microsoft seems to discourage writing your own auth. Please use at your own risk.
First, add this code to the startup.cs ConfigureServices method to ensure that authentication gets applied globally.
services.AddMvc(options =>
{
var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
options.Filters.Add(new AuthorizeFilter(policy));
})
Then, add this to configure the schemes you wish to use (in our case JWT and Custom).
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
// Jwt Authentication
.AddJwtBearer(options =>
{
options.Audience = ".......";
options.Authority = "https://cognito-idp.us-east-1.amazonaws.com/us-east-1_...";
})
// Custom auth
.AddScheme<CustomAuthOptions,
CustomAuthHandler>(CustomAuthOptions.DefaultScheme, options => { });
Next create a class to hold your custom authentication options:
public class CustomAuthOptions : AuthenticationSchemeOptions
{
public const string Scheme = "custom auth";
public const string CustomAuthType = "custom auth type";
}
Finally, add an authentication handler to implement the custom authentication logic.
public class CustomAuthHandler : AuthenticationHandler<CustomAuthOptions>
{
public CustomAuthHandler(
IOptionsMonitor<CustomAuthOptions> options,
ILoggerFactory logger,
UrlEncoder encoder,
ISystemClock clock) : base(options, logger, encoder, clock)
{
}
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
// Auth logic goes here
if (!Request.Headers....)
{
return Task.FromResult(AuthenticateResult.Fail("Authentication Failed."));
}
// Create authenticated user
ClaimsPrincipal principal = .... ;
List<ClaimsIdentity> identities =
new List<ClaimsIdentity> {
new ClaimsIdentity(CustomAuthOptions.CustomAuthType)};
AuthenticationTicket ticket =
new AuthenticationTicket(
new ClaimsPrincipal(identities), CustomAuthOptions.Scheme);
return Task.FromResult(AuthenticateResult.Success(ticket));
}
}
Finally, to tie it all together, add an authorize attribute to the actions you wish to use custom authorization on.
[HttpGet]
[Authorize(AuthenticationSchemes = CustomAuthOptions.Scheme)]
public HttpResponseMessage Get()
{
....
}
Now JWT authentication will automatically get applied to all actions, and custom authentication will get added to only the actions with the Authorize attribute set to the custom scheme.
I hope this helps someone.
We have a .Net Core 2.0 Web API project. I have added the hangfire there. We don't have any web page in the project and I use JWT for authorization. So I'm not able to do the authorization for hangfire using the Authorize(DashboardContext context). Is there any way we can pass some sort of API key on the url to authorize the user for dashboard?
Thanks
Yes, you can do that by using cookies, I will explain the idea to you with some code...
Firstly once the user login (you generate the token) you must store the token you generated to cookies in the browser and then when you want to access to Hangfire dashboard you must read the token from cookies and then check the roles...
the code to store the token in cookies:
`
httpContext.Response.Cookies.Append("token", userToken.AccessToken,
new Microsoft.AspNetCore.Http.CookieOptions { Expires = DateTime.Now.AddMinutes(6000) });
`
make sure you enabled cookies by:
`
AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddCookie()
.AddJwtBearer(cfg => your configs);
`
then the authorize method will be like this:
`
public bool Authorize(DashboardContext context)
{
var httpContext = context.GetHttpContext();
var jwtToken = string.Empty;
if (httpContext.Request.Cookies.ContainsKey("token"))
{
httpContext.Request.Cookies.TryGetValue("token", out jwtToken);
}
else
return false;
if (string.IsNullOrEmpty(jwtToken))
{
return false;
}
var handler = new JwtSecurityTokenHandler();
try
{
var claim = _tokenService.GetClaimsPrincipal(jwtToken);
return claim != null && claim.IsInRole(RolesConstants.ADMIN);
}
catch (Exception exception)
{
throw exception;
}
}
`
Implement an IAuthorizationDashboardFilter class.
Try this post here.
You need to pass JWT token with each request, try storing token in a cookie or as a query string parameter. Then you can pull that from request context and decide whether user is authorized
public class MyAuthorizationFilter : IDashboardAuthorizationFilter
{
public bool Authorize(DashboardContext context)
{
var httpContext = context.GetHttpContext();
var token = context.Request.Cookies["access-token"];
// check token validity and set authentication accordingly.
return httpContext.User.Identity.IsAuthenticated;
}
}
Register the filter in your OWIN pipeline like this, after whatever authentication method you are using. Then the logged in user claim will be available in the filter
public void Configuration(IAppBuilder app)
{
app.UseCookieAuthentication(...); // Authentication - first
app.UseHangfireDashboard("/hangfire", new DashboardOptions
{
Authorization = new [] { new MyAuthorizationFilter() }
}); // Hangfire - last
}
My application logic depends on a claim existing, hence this claim is mandatory and needs to always be present in the token.
I am not interested in a Authorization Policy since policies applies to different users and this is a mandatory claim required to be present in all tokens.
Right now my controllers contains:
private const string MyCustomClaim = "foo";
private string _myCustomClaim;
public override void OnActionExecuting(ActionExecutingContext context)
{
_myCustomClaim = context.HttpContext.User.FindFirst(MyCustomClaim)?.Value;
}
If the field _myCustomClaim is null then things will fail later.
I could add a null check and throw an exception, but it would be better if the Authorization middleware did not authorize the user if the token did not contain the claim.
Is there any way to inform the Authorization middleware that a certain claim is mandatory?
In the Startup.cs file when configuring the authentication middleware handle the OnTokenValidated event.
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(options =>
{
options.Events = new JwtBearerEvents
{
OnTokenValidated = context =>
{
const string claimTypeFoo = "foo";
if (!context.Principal.HasClaim(c => c.Type == claimTypeFoo))
{
context.Fail($"The claim '{claimTypeFoo}' is not present in the token.");
}
return Task.CompletedTask;
}
};
});
This could also be done in a class:
File Startup.cs
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(options =>
{
options.Events = new MyJwtBearerEvents();
});
File MyJwtBearerEvents.cs
public class MyJwtBearerEvents : JwtBearerEvents
{
private const string ClaimTypeFoo = "foo";
public override Task TokenValidated(TokenValidatedContext context)
{
if (!context.Principal.HasClaim(c => c.Type == ClaimTypeFoo))
{
context.Fail($"The claim '{ClaimTypeFoo}' is not present in the token.");
}
return Task.CompletedTask;
}
}
Trying to configure my Web Api as Resource Server. My client logs into Auth0 and gets Bearer token, so Authorization Server is Auth0 not my Api. Then they send request along with the Bearer token to my Api. In my ASP.Net Web Api I have implemented following OWIN configuration in Startup class to validate the request JWT Bearer token issued by Auth0 as instructed here.
Statup:
public class Startup
{
public void Configuration(IAppBuilder app)
{
var auth0Options = new Auth0Options()
{
Issuer = $"https://{ConfigurationManager.AppSettings["Auth0ApiInternalDomain"]}/",
Audience = ConfigurationManager.AppSettings["Auth0ApiInternalAudience"],
ClientId = ConfigurationManager.AppSettings["Auth0ApiInternalClientID"]
};
Auth0Config.Configure(app, auth0Options);
// Configure Web API
WebApiConfig.Configure(app);
}
}
and Auth0Config class:
public class Auth0Config
{
public static void Configure(IAppBuilder app, Auth0Options options)
{
if (options == null)
throw new ArgumentNullException(nameof(options));
var keyResolver = new OpenIdConnectSigningKeyResolver(options.Issuer);
app.UseJwtBearerAuthentication(
new JwtBearerAuthenticationOptions
{
AuthenticationMode = AuthenticationMode.Active,
TokenValidationParameters = new TokenValidationParameters()
{
ValidAudience = options.Audience,
ValidIssuer = options.Issuer,
IssuerSigningKeyResolver = (token, securityToken, identifier, parameters) => keyResolver.GetSigningKey(identifier),
ValidateLifetime = true,
ValidateIssuer = true,
ValidateAudience = true,
LifetimeValidator = (DateTime? notBefore, DateTime? expires, SecurityToken securityToken, TokenValidationParameters validationParameters) =>
{
if (expires.Value < DateTime.UtcNow)
{
return false;
}
return true;
}
}
});
}
}
I pass Audience, Issuer and CliedntId from my app.config to this method. My intention is to figure out whether the Bearer token coming from the client to my Api is valid or not (here as first step I need to validate expiration date). When I debug my code for the incoming request, LifetimeValidator works fine and returns false for the expired token. I decorated my action with [Authorize] and expected to get 401 error but the actual response is 200 and it seems it ignores the LifetimeValidator implementation.
My action:
[Authorize]
public IHttpActionResult Get(string id)
{
var result = _bookingService.GetBooking(id);
if (result == null) return NotFound();
return Ok(result);
}
Am I missing something to get it right?
Is this a good approach to validate token expiration?
Is it possible to use OWIN only to validate the request Bearer token that has been issued out of web api application?
It turned out Invoke method of OwinMiddleware class had been overridden in my application to find Username from token and inject it to Request.User. Not sure why but somehow it ignores OWIN token validation functionality and didn't check Audience, Issuer or Expiration time.
public static void Configure(IAppBuilder app)
{
HttpConfiguration config = new HttpConfiguration();
app.Use<HttpUsernameInjector>();
app.UseWebApi(config);
}
public class HttpUsernameInjector : OwinMiddleware
{
public HttpUsernameInjector(OwinMiddleware next)
: base(next){}
public override async Task Invoke(IOwinContext context)
{
const string usernameClaimKey = "myUserNameClaimKey";
var bearerString = context.Request.Headers["Authorization"];
if (bearerString != null && bearerString.StartsWith("Bearer ", StringComparison.InvariantCultureIgnoreCase))
{
var tokenString = bearerString.Substring(7);
var token = new JwtSecurityToken(tokenString);
var claims = token.Claims.ToList();
var username = claims.FirstOrDefault(x => x.Type == usernameClaimKey);
if (username == null) throw new Exception("Token should have username");
// Add to HttpContext
var genericPrincipal = new GenericPrincipal(new GenericIdentity(username.Value), new string[] { });
IPrincipal principal = genericPrincipal;
context.Request.User = principal;
}
await Next.Invoke(context);
}
}
by removing this class, OWIN token validation works fine!
Base on my research, the best token validation approaches in Web Api are OWIN and also IAuthenticationFilter.
It is possible as Resource Server and Authorization Server are decoupled. More info can be found here
Update
Found the solution here to stop OwinMiddleware suppressing my token validation logic