I have the following requirements for my C# Web API 2 Service:
The service authenticates Users by the combination of Email and a temporary Passcode that gets sent to their inbox, as a factor of authentication.
I need to blend in this authentication mechanism with producing OAuth bearer tokens to secure the service and use standard ASP.NET Authorisation mechanism to check each request against the token via some kind of [Authorize] attribute.
I have successfully implemented these steps
User requests Passcode
System generates and emails Passcode to User with 30 days expiration
User authenticates with Email + Passcode
System checks validity of Passcode
But I am not sure how to begin implementing the remaining steps
If Passcode valid, system generates OAuth bearer token
OAuth bearer token lasts as long as Passcode expiration date
Use ASP.NET Identity authorization attributes to perform authentication and authorisation checks
Use OWIN Security and OAuth Middleware to create token
Use claims based authorisation and serialise claims into token
The cited process only describes using ASP.NET Identity Individual User accounts as a means to authenticate which is not how I want to authenticate.
http://www.asp.net/web-api/overview/security/individual-accounts-in-web-api
I actually need to authenticate by checking Email and Passcode.
I worked in a similar scenario and had implemented an authentication filter (IAuthenticationFilter) and a customized class inherited from OAuthAuthorizationServerProvider. In my case, I needed to authenticate the request with OAuth and a legacy token. I believe that in your case, you will need customize the AuthenticationFilter. See below an example of the AuthenticationFilter:
public class MyAuthenticationFilter : IAuthenticationFilter
{
private readonly string _authenticationType;
/// <summary>Initializes a new instance of the <see cref="HostAuthenticationFilter"/> class.</summary>
/// <param name="authenticationType">The authentication type of the OWIN middleware to use.</param>
public MyAuthenticationFilter(string authenticationType)
{
if (authenticationType == null)
{
throw new ArgumentNullException("authenticationType");
}
_authenticationType = authenticationType;
}
/// <summary>Gets the authentication type of the OWIN middleware to use.</summary>
public string AuthenticationType
{
get { return _authenticationType; }
}
/// <inheritdoc />
public async Task AuthenticateAsync(HttpAuthenticationContext context, CancellationToken cancellationToken)
{
if (context == null)
{
throw new ArgumentNullException("context");
}
HttpRequestMessage request = context.Request;
if (request == null)
{
throw new InvalidOperationException("Request mut not be null");
}
//In my case, i need try autenticate the request with BEARER token (Oauth)
IAuthenticationManager authenticationManager = GetAuthenticationManagerOrThrow(request);
cancellationToken.ThrowIfCancellationRequested();
AuthenticateResult result = await authenticationManager.AuthenticateAsync(_authenticationType);
ClaimsIdentity identity = null;
if (result != null)
{
identity = result.Identity;
if (identity != null)
{
context.Principal = new ClaimsPrincipal(identity);
}
}
else
{
//If havent success with oauth authentication, I need locate the legacy token
//If dont exists the legacy token, set error (will generate http 401)
if (!request.Headers.Contains("legacy-token-header"))
context.ErrorResult = new AuthenticationFailureResult(Resources.SAUTH_ERROR_LEGACYTOKENNOTFOUND, request);
else
{
try
{
var queryString = request.GetQueryNameValuePairs();
if (!queryString.Any(x => x.Key == "l"))
context.ErrorResult = new AuthenticationFailureResult(Resources.SAUTH_ERROR_USERTYPENOTFOUND, request);
else
{
var userType = queryString.First(x => x.Key == "l").Value;
String token = HttpUtility.UrlDecode(request.Headers.GetValues("tk").First());
identity = TokenLegacy.ValidateToken(token, userType);
identity.AddClaims(userType, (OwinRequest) ((OwinContext)context.Request.Properties["MS_OwinContext"]).Request);
if (identity != null)
{
context.Principal = new ClaimsPrincipal(identity);
}
}
}
catch (Exception e)
{
context.ErrorResult = new AuthenticationFailureResult(e.Message, request);
}
}
}
}
/// <inheritdoc />
public Task ChallengeAsync(HttpAuthenticationChallengeContext context, CancellationToken cancellationToken)
{
if (context == null)
{
throw new ArgumentNullException("context");
}
HttpRequestMessage request = context.Request;
if (request == null)
{
throw new InvalidOperationException("Request mut not be null");
}
IAuthenticationManager authenticationManager = GetAuthenticationManagerOrThrow(request);
// Control the challenges that OWIN middleware adds later.
authenticationManager.AuthenticationResponseChallenge = AddChallengeAuthenticationType(
authenticationManager.AuthenticationResponseChallenge, _authenticationType);
return TaskHelpers.Completed();
}
/// <inheritdoc />
public bool AllowMultiple
{
get { return true; }
}
private static AuthenticationResponseChallenge AddChallengeAuthenticationType(
AuthenticationResponseChallenge challenge, string authenticationType)
{
Contract.Assert(authenticationType != null);
List<string> authenticationTypes = new List<string>();
AuthenticationProperties properties;
if (challenge != null)
{
string[] currentAuthenticationTypes = challenge.AuthenticationTypes;
if (currentAuthenticationTypes != null)
{
authenticationTypes.AddRange(currentAuthenticationTypes);
}
properties = challenge.Properties;
}
else
{
properties = new AuthenticationProperties();
}
authenticationTypes.Add(authenticationType);
return new AuthenticationResponseChallenge(authenticationTypes.ToArray(), properties);
}
private static IAuthenticationManager GetAuthenticationManagerOrThrow(HttpRequestMessage request)
{
Contract.Assert(request != null);
var owinCtx = request.GetOwinContext();
IAuthenticationManager authenticationManager = owinCtx != null ? owinCtx.Authentication : null;
if (authenticationManager == null)
{
throw new InvalidOperationException("IAuthenticationManagerNotAvailable");
}
return authenticationManager;
}
}
In WebApiConfig.cs, you need the add the authentication filter like this:
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
// Web API configuration and services
// Configure Web API to use only bearer token authentication.
config.SuppressDefaultHostAuthentication();
config.Filters.Add(new MyAuthenticationFilter(OAuthDefaults.AuthenticationType));
}
}
I recommend reading the official WEB API poster:
https://www.asp.net/media/4071077/aspnet-web-api-poster.pdf
Related
Unfortunately, given the size of the project, I can’t easily share a reproducible version. However, hopefully what I have below will shed some light on my issue and you’ll see where I made a mistake.
I have two sites, an ASP.Net Core MVC application and a Login server, also ASP.Net Core MVC. Let’s call them http://mvc.mysite.com and http://login.mysite.com. Neither are significantly different from the IdentityServer4 Quickstart #6. The only real difference is that I have implemented an external login provider for AzureAd. My code for that is below.
Scenario 1
Given an internal login flow, where the user uses an internal login page at http://login.mysite.com everything works fine.
User visits http://mvc.mysite.com/clients/client-page-1
User is redirected to http://login.mysite.com/Account/Login
User logs in with correct username/password
User is redirected to http://mvc.mysite.com/clients/client-page-1
Scenario 2
However, if the login server’s AccountController::Login() method determines there is a single ExternalLoginProvider and executes the line “return await ExternalLogin(vm.ExternalLoginScheme, returnUrl);” then the original redirectUrl is lost.
User visits http://mvc.mysite.com/clients/client-page-1
User is redirected to http://login.mysite.com/Account/Login (receiving the output of AccountController::ExternalLogin)
User is redirected to AzureAd External OIDC Provider
User logs in with correct username/password
User is redirected to http://login.mysite.com/Account/ExternalLoginCallback
User is redirected to http://mvc.mysite.com (Notice that the user is redirected to the root of the MVC site instead of /clients/client-page-1)
For Scenario 1:
Given the MVC site
When using the debugger to inspect the Context provided to the OpenIdConnectEvents (e.g. OnMessageReceived, OnUserInformationReceived, etc.)
Then all Contexts have a Properties object that contains a RedirectUri == “http://mvc.mysite.com/clients/client-page-1”
For Scenario 2:
Given the MVC site
When using the debugger to inspect the Context provided to the OpenIdConnectEvents (e.g. OnMessageReceived, OnUserInformationReceived, etc.)
Then all Contexts have a Properties object that contains a RedirectUri == “http://mvc.mysite.com” (missing the /client.client-page-1)
In my login server’s Startup.cs I have added this to ConfigureServices:
services.AddAuthentication()
.AddAzureAd(options =>
{
Configuration.Bind("AzureAd", options);
AzureAdOptions.Settings = options;
});
The implementation of AddAzureAd is as follows: (You’ll see options objects handed around, I have replaced all uses of options with constant values except for ClientId and ClientSecret).
public static class AzureAdAuthenticationBuilderExtensions
{
public static AuthenticationBuilder AddAzureAd(this AuthenticationBuilder builder, Action<AzureAdOptions> configureOptions)
{
builder.AddOpenIdConnect("AzureAd", "Azure AD", options =>
{
var opts = new AzureAdOptions();
configureOptions(opts);
var config = new ConfigureAzureOptions(opts);
config.Configure(options);
});
return builder;
}
private class ConfigureAzureOptions : IConfigureNamedOptions<OpenIdConnectOptions>
{
private readonly AzureAdOptions _azureOptions;
public ConfigureAzureOptions(AzureAdOptions azureOptions)
{
_azureOptions = azureOptions;
}
public ConfigureAzureOptions(IOptions<AzureAdOptions> azureOptions) : this(azureOptions.Value) {}
public void Configure(string name, OpenIdConnectOptions options)
{
Configure(options);
}
public void Configure(OpenIdConnectOptions options)
{
options.ClientId = _azureOptions.ClientId;
options.Authority = "https://login.microsoftonline.com/common"; //_azureOptions.Authority;
options.UseTokenLifetime = true;
options.CallbackPath = "/signin-oidc"; // _azureOptions.CallbackPath;
options.RequireHttpsMetadata = false; // true in production // _azureOptions.RequireHttps;
options.ClientSecret = _azureOptions.ClientSecret;
// Add code for hybridflow
options.ResponseType = "id_token code";
options.TokenValidationParameters = new IdentityModel.Tokens.TokenValidationParameters
{
// instead of using the default validation (validating against a single issuer value, as we do in line of business apps),
// we inject our own multitenant validation logic
ValidateIssuer = false,
};
// Subscribing to the OIDC events
options.Events.OnAuthorizationCodeReceived = OnAuthorizationCodeReceived;
options.Events.OnAuthenticationFailed = OnAuthenticationFailed;
}
/// <summary>
/// Redeems the authorization code by calling AcquireTokenByAuthorizationCodeAsync in order to ensure
/// that the cache has a token for the signed-in user.
/// </summary>
private async Task OnAuthorizationCodeReceived(AuthorizationCodeReceivedContext context)
{
string userObjectId = (context.Principal.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier"))?.Value;
var authContext = new AuthenticationContext(context.Options.Authority, new NaiveSessionCache(userObjectId, context.HttpContext.Session));
var credential = new ClientCredential(context.Options.ClientId, context.Options.ClientSecret);
var authResult = await authContext.AcquireTokenByAuthorizationCodeAsync(context.TokenEndpointRequest.Code,
new Uri(context.TokenEndpointRequest.RedirectUri, UriKind.RelativeOrAbsolute), credential, context.Options.Resource);
// Notify the OIDC middleware that we already took care of code redemption.
context.HandleCodeRedemption(authResult.AccessToken, context.ProtocolMessage.IdToken);
}
private Task OnAuthenticationFailed(AuthenticationFailedContext context)
{
throw context.Exception;
}
}
}
public class NaiveSessionCache : TokenCache
{
private static readonly object FileLock = new object();
string UserObjectId = string.Empty;
string CacheId = string.Empty;
ISession Session = null;
public NaiveSessionCache(string userId, ISession session)
{
UserObjectId = userId;
CacheId = UserObjectId + "_TokenCache";
Session = session;
this.AfterAccess = AfterAccessNotification;
this.BeforeAccess = BeforeAccessNotification;
Load();
}
public void Load()
{
lock (FileLock)
this.Deserialize(Session.Get(CacheId));
}
public void Persist()
{
lock (FileLock)
{
// reflect changes in the persistent store
Session.Set(CacheId, this.Serialize());
// once the write operation took place, restore the HasStateChanged bit to false
this.HasStateChanged = false;
}
}
// Empties the persistent store.
public override void Clear()
{
base.Clear();
Session.Remove(CacheId);
}
public override void DeleteItem(TokenCacheItem item)
{
base.DeleteItem(item);
Persist();
}
// Triggered right before ADAL needs to access the cache.
// Reload the cache from the persistent store in case it changed since the last access.
void BeforeAccessNotification(TokenCacheNotificationArgs args)
{
Load();
}
// Triggered right after ADAL accessed the cache.
void AfterAccessNotification(TokenCacheNotificationArgs args)
{
// if the access operation resulted in a cache update
if (this.HasStateChanged)
Persist();
}
}
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
}
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
I am trying to wrap my head around several concepts here but I don't want this question to be too broad - basically what we are trying to do is use role claims as permissions to lock down our API but I am finding that the access_token is becoming too big.
We are using OpenIddict and ASP.NET Identity 3 on the server side. We have implemented the default AspNetRoleClaims table to store our claims for each role - using them as permissions.
We lock down our API endpoints using custom policy based claims authorization as shown here:
Custom Policy Based Authorization
The main issue I am finding is that our access_token containing our claims is becoming very large. We are attempting to make the ClaimType and Value to be very small in the database to make the claims footprint smaller. We have a basic CRUD type permission scheme, so for each "module" or screen in our SPA client app, there are 4 permissions. The more modules we add to our application, the more the claims are growing in the access_token and our Authorization Bearer header is becoming very large. I am worried about this becoming not very scalable as the app grows.
So the claims are embedded in the access_token and when I hit my endpoint that is locked down with a custom Policy like this...
[Authorize(Policy="MyModuleCanRead")]
[HttpGet]
public IEnumerable<MyViewModel> Get()
I can then access my ASP.NET Identity User and User.Claims in the AuthorizationHandler.
Sorry in advance if this is an obvious question - but I am wondering - in order to get the Custom Policy Based Authorization to work - does it absolutely require the claims to be in either the id_token or the access_token in order to call the handler?
If I remove the claims from the access_token, then my AuthorizationHandler code does not get hit and I cannot access my endpoint that is locked down with my custom Policy.
I am wondering if it is possible to use a custom claims policy but have the actual code that checks for the Claims inside the Authorization handler, so that the claims are not passed with each HTTP request, but are fetched server side from the Authorization cookie or from the database.
* UPDATE *
Pintpoint's answer using Authorization handlers along with the comment on how to remove additional role claims from the cookie achieved just what I was looking for.
In case this helps anyone else - here is the code to override the UserClaimsPrincipalFactory and prevent the role claims from being written to the cookie. (I had many role claims as permissions and the cookie(s) and request headers were becoming too large)
public class AppClaimsPrincipalFactory : UserClaimsPrincipalFactory<ApplicationUser, IdentityRole>
{
public AppClaimsPrincipalFactory(UserManager<ApplicationUser> userManager, RoleManager<IdentityRole> roleManager, IOptions<IdentityOptions> optionsAccessor) : base(userManager, roleManager, optionsAccessor)
{
}
public override async Task<ClaimsPrincipal> CreateAsync(ApplicationUser user)
{
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
var userId = await UserManager.GetUserIdAsync(user);
var userName = await UserManager.GetUserNameAsync(user);
var id = new ClaimsIdentity(Options.Cookies.ApplicationCookieAuthenticationScheme,
Options.ClaimsIdentity.UserNameClaimType,
Options.ClaimsIdentity.RoleClaimType);
id.AddClaim(new Claim(Options.ClaimsIdentity.UserIdClaimType, userId));
id.AddClaim(new Claim(Options.ClaimsIdentity.UserNameClaimType, userName));
if (UserManager.SupportsUserSecurityStamp)
{
id.AddClaim(new Claim(Options.ClaimsIdentity.SecurityStampClaimType,
await UserManager.GetSecurityStampAsync(user)));
}
// code removed that adds the role claims
if (UserManager.SupportsUserClaim)
{
id.AddClaims(await UserManager.GetClaimsAsync(user));
}
return new ClaimsPrincipal(id);
}
}
I am wondering if it is possible to use a custom claims policy but have the actual code that checks for the Claims inside the Authorization handler, so that the claims are not passed with each HTTP request, but are fetched server side from the Authorization cookie or from the database.
It's definitely possible. Here's how you could do that:
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddScoped<IAuthorizationHandler, PermissionAuthorizationHandler>();
services.AddAuthorization(options =>
{
options.AddPolicy("Has-Edit-User-Profiles-Permission", builder =>
{
builder.RequirePermission("Edit-User-Profiles");
});
});
}
}
public class PermissionAuthorizationRequirement : IAuthorizationRequirement
{
public PermissionAuthorizationRequirement(string permission)
{
if (string.IsNullOrEmpty(permission))
{
throw new ArgumentException("The permission cannot be null or empty.", nameof(permission));
}
Permission = permission;
}
public string Permission { get; set; }
}
public class PermissionAuthorizationHandler :
AuthorizationHandler<PermissionAuthorizationRequirement>
{
private readonly UserManager<ApplicationUser> _userManager;
public PermissionAuthorizationHandler(UserManager<ApplicationUser> userManager)
{
if (userManager == null)
{
throw new ArgumentNullException(nameof(userManager));
}
_userManager = userManager;
}
protected override async Task HandleRequirementAsync(
AuthorizationHandlerContext context,
PermissionAuthorizationRequirement requirement)
{
if (context.User == null)
{
return;
}
var user = await _userManager.GetUserAsync(context.User);
if (user == null)
{
return;
}
// Use whatever API you need to ensure the user has the requested permission.
if (await _userManager.IsInRoleAsync(user, requirement.Permission))
{
context.Succeed(requirement);
}
}
}
public static class PermissionAuthorizationExtensions
{
public static AuthorizationPolicyBuilder RequirePermission(
this AuthorizationPolicyBuilder builder, string permission)
{
if (builder == null)
{
throw new ArgumentNullException(nameof(builder));
}
if (string.IsNullOrEmpty(permission))
{
throw new ArgumentException("The permission cannot be null or empty.", nameof(permission));
}
return builder.AddRequirements(new PermissionAuthorizationRequirement(permission));
}
}
Question
How can I implement Basic Authentication with Custom Membership in an ASP.NET Core web application?
Notes
In MVC 5 I was using the instructions in this article which requires adding a module in the WebConfig.
I am still deploying my new MVC Coreapplication on IIS but this approach seems not working.
I also do not want to use the IIS built in support for Basic authentication, since it uses Windows credentials.
ASP.NET Security will not include Basic Authentication middleware due to its potential insecurity and performance problems.
If you require Basic Authentication middleware for testing purposes, then please look at https://github.com/blowdart/idunno.Authentication
ASP.NET Core 2.0 introduced breaking changes to Authentication and Identity.
On 1.x auth providers were configured via Middleware (as the accepted answer's implementation).
On 2.0 it's based on services.
Details on MS doc:
https://learn.microsoft.com/en-us/aspnet/core/migration/1x-to-2x/identity-2x
I've written a Basic Authentication implementation for ASP.NET Core 2.0 and publish to NuGet:
https://github.com/bruno-garcia/Bazinga.AspNetCore.Authentication.Basic
I'm disappointed by the ASP.NET Core authentication middleware design. As a framework it should simplify and led to greater productivity which isn't the case here.
Anyway, a simple yet secure approach is based on the Authorization filters e.g. IAsyncAuthorizationFilter. Note that an authorization filter will be executed after the other middlewares, when MVC picks a certain controller action and moves to filter processing. But within filters, authorization filters are executed first (details).
I was just going to comment on Clays comment to Hector's answer but didn't like Hectors example throwing exceptions and not having any challenge mechanism, so here is a working example.
Keep in mind:
Basic authentication without HTTPS in production is extremely bad. Make sure your HTTPS settings are hardened (e.g. disable all SSL and TLS < 1.2 etc.)
Today, most usage of basic authentication is when exposing an API that's protected by an API key (see Stripe.NET, Mailchimp etc). Makes for curl friendly APIs that are as secure as the HTTPS settings on the server.
With that in mind, don't buy into any of the FUD around basic authentication. Skipping something as basic as basic authentication is high on opinion and low on substance. You can see the frustration around this design in the comments here.
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.EntityFrameworkCore;
using System;
using System.Linq;
using System.Security.Claims;
using System.Text;
using System.Threading.Tasks;
namespace BasicAuthFilterDemo
{
public class BasicAuthenticationFilterAttribute : Attribute, IAsyncAuthorizationFilter
{
public string Realm { get; set; }
public const string AuthTypeName = "Basic ";
private const string _authHeaderName = "Authorization";
public BasicAuthenticationFilterAttribute(string realm = null)
{
Realm = realm;
}
public async Task OnAuthorizationAsync(AuthorizationFilterContext context)
{
try
{
var request = context?.HttpContext?.Request;
var authHeader = request.Headers.Keys.Contains(_authHeaderName) ? request.Headers[_authHeaderName].First() : null;
string encodedAuth = (authHeader != null && authHeader.StartsWith(AuthTypeName)) ? authHeader.Substring(AuthTypeName.Length).Trim() : null;
if (string.IsNullOrEmpty(encodedAuth))
{
context.Result = new BasicAuthChallengeResult(Realm);
return;
}
var (username, password) = DecodeUserIdAndPassword(encodedAuth);
// Authenticate credentials against database
var db = (ApplicationDbContext)context.HttpContext.RequestServices.GetService(typeof(ApplicationDbContext));
var userManager = (UserManager<User>)context.HttpContext.RequestServices.GetService(typeof(UserManager<User>));
var founduser = await db.Users.Where(u => u.Email == username).FirstOrDefaultAsync();
if (!await userManager.CheckPasswordAsync(founduser, password))
{
// writing to the Result property aborts rest of the pipeline
// see https://learn.microsoft.com/en-us/aspnet/core/mvc/controllers/filters?view=aspnetcore-3.0#cancellation-and-short-circuiting
context.Result = new StatusCodeOnlyResult(StatusCodes.Status401Unauthorized);
}
// Populate user: adjust claims as needed
var claims = new[] { new Claim(ClaimTypes.Name, username, ClaimValueTypes.String, AuthTypeName) };
var principal = new ClaimsPrincipal(new ClaimsIdentity(claims, AuthTypeName));
context.HttpContext.User = principal;
}
catch
{
// log and reject
context.Result = new StatusCodeOnlyResult(StatusCodes.Status401Unauthorized);
}
}
private static (string userid, string password) DecodeUserIdAndPassword(string encodedAuth)
{
var userpass = Encoding.UTF8.GetString(Convert.FromBase64String(encodedAuth));
var separator = userpass.IndexOf(':');
if (separator == -1)
return (null, null);
return (userpass.Substring(0, separator), userpass.Substring(separator + 1));
}
}
}
And these are the supporting classes
public class StatusCodeOnlyResult : ActionResult
{
protected int StatusCode;
public StatusCodeOnlyResult(int statusCode)
{
StatusCode = statusCode;
}
public override Task ExecuteResultAsync(ActionContext context)
{
context.HttpContext.Response.StatusCode = StatusCode;
return base.ExecuteResultAsync(context);
}
}
public class BasicAuthChallengeResult : StatusCodeOnlyResult
{
private string _realm;
public BasicAuthChallengeResult(string realm = "") : base(StatusCodes.Status401Unauthorized)
{
_realm = realm;
}
public override Task ExecuteResultAsync(ActionContext context)
{
context.HttpContext.Response.StatusCode = StatusCode;
context.HttpContext.Response.Headers.Add("WWW-Authenticate", $"{BasicAuthenticationFilterAttribute.AuthTypeName} Realm=\"{_realm}\"");
return base.ExecuteResultAsync(context);
}
}
We implemented Digest security for an internal service by using an ActionFilter:
public class DigestAuthenticationFilterAttribute : ActionFilterAttribute
{
private const string AUTH_HEADER_NAME = "Authorization";
private const string AUTH_METHOD_NAME = "Digest ";
private AuthenticationSettings _settings;
public DigestAuthenticationFilterAttribute(IOptions<AuthenticationSettings> settings)
{
_settings = settings.Value;
}
public override void OnActionExecuting(ActionExecutingContext context)
{
ValidateSecureChannel(context?.HttpContext?.Request);
ValidateAuthenticationHeaders(context?.HttpContext?.Request);
base.OnActionExecuting(context);
}
private void ValidateSecureChannel(HttpRequest request)
{
if (_settings.RequireSSL && !request.IsHttps)
{
throw new AuthenticationException("This service must be called using HTTPS");
}
}
private void ValidateAuthenticationHeaders(HttpRequest request)
{
string authHeader = GetRequestAuthorizationHeaderValue(request);
string digest = (authHeader != null && authHeader.StartsWith(AUTH_METHOD_NAME)) ? authHeader.Substring(AUTH_METHOD_NAME.Length) : null;
if (string.IsNullOrEmpty(digest))
{
throw new AuthenticationException("You must send your credentials using Authorization header");
}
if (digest != CalculateSHA1($"{_settings.UserName}:{_settings.Password}"))
{
throw new AuthenticationException("Invalid credentials");
}
}
private string GetRequestAuthorizationHeaderValue(HttpRequest request)
{
return request.Headers.Keys.Contains(AUTH_HEADER_NAME) ? request.Headers[AUTH_HEADER_NAME].First() : null;
}
public static string CalculateSHA1(string text)
{
var sha1 = System.Security.Cryptography.SHA1.Create();
var hash = sha1.ComputeHash(Encoding.UTF8.GetBytes(text));
return Convert.ToBase64String(hash);
}
}
Afterwards you can annotate the controllers or methods you want to be accessed with Digest security:
[Route("api/xxxx")]
[ServiceFilter(typeof(DigestAuthenticationFilterAttribute))]
public class MyController : Controller
{
[HttpGet]
public string Get()
{
return "HELLO";
}
}
To implement Basic security, simply change the DigestAuthenticationFilterAttribute to not use SHA1 but direct Base64 decoding of the Authorization header.
Super-Simple Basic Authentication in .NET Core:
1. Add this utility method:
static System.Text.Encoding ISO_8859_1_ENCODING = System.Text.Encoding.GetEncoding("ISO-8859-1");
public static (string, string) GetUsernameAndPasswordFromAuthorizeHeader(string authorizeHeader)
{
if (authorizeHeader == null || !authorizeHeader.Contains("Basic "))
return (null, null);
string encodedUsernamePassword = authorizeHeader.Substring("Basic ".Length).Trim();
string usernamePassword = ISO_8859_1_ENCODING.GetString(Convert.FromBase64String(encodedUsernamePassword));
string username = usernamePassword.Split(':')[0];
string password = usernamePassword.Split(':')[1];
return (username, password);
}
2. Update controller action to get username and password from Authorization header:
public async Task<IActionResult> Index([FromHeader]string Authorization)
{
(string username, string password) = GetUsernameAndPasswordFromAuthorizeHeader(Authorization);
// Now use username and password with whatever authentication process you want
return View();
}
Example
This example demonstrates using this with ASP.NET Core Identity.
public class HomeController : Controller
{
private readonly UserManager<IdentityUser> _userManager;
public HomeController(UserManager<IdentityUser> userManager)
{
_userManager = userManager;
}
[AllowAnonymous]
public async Task<IActionResult> MyApiEndpoint([FromHeader]string Authorization)
{
(string username, string password) = GetUsernameAndPasswordFromAuthorizeHeader(Authorization);
IdentityUser user = await _userManager.FindByNameAsync(username);
bool successfulAuthentication = await _userManager.CheckPasswordAsync(user, password);
if (successfulAuthentication)
return Ok();
else
return Unauthorized();
}
}