Why does my authentication middleware pass a request to the authorization filter when no token is supplied? - authentication

I have a .NET Core 1.1 Web API which uses JWT authentication middleware and a custom authorization filter. They are defined like this:
services.AddSingleton<IAuthorizationRequirement, MercuryEndpointAuthorizationHandler>();
services.AddSingleton<IEndpointAuthorizeDictionary, EndpointRights>();
services.AddSingleton<IAuthorizationHandler, MercuryEndpointAuthorizationHandler>();
services.AddAuthorization(options =>
{
options.AddPolicy("AuthorizeMercuryEndpoint", policy =>
{
policy.AddRequirements(services.BuildServiceProvider().GetService<IAuthorizationRequirement>());
});
});
and
app.UseJwtBearerAuthentication(new JwtBearerOptions
{
AutomaticAuthenticate = true,
AutomaticChallenge = true,
TokenValidationParameters = tokenValidationParameters,
Events = new JwtBearerEvents()
{
OnMessageReceived = async (context) =>
{
Debug.WriteLine("====> JWT Message received");
},
OnTokenValidated = async (context) =>
{
Debug.WriteLine("====> JWT token validated");
context.HttpContext.Items["JwtTokenIsValid"] = true;
},
OnAuthenticationFailed = async (context) =>
{
Debug.WriteLine("====> JWT token failed auth");
context.HttpContext.Items["JwtTokenIsValid"] = false;
if ((AuthenticationType.IdServer & authTypes) != 0)
context.SkipToNextMiddleware();
}
}
});
When I call an endpoint protected with [Authorize("AuthorizeMercuryEndpoint")] and a valid JWT token, the call succeeds as expected, and I see the following debug written by the process:
====> JWT Message received
====> JWT token validated
====> Request authorized (when the auth filter succeeds)
However, if I do not pass a token, the JWT middleware appears to pass the request straight to the authorization filter without attempting authentication - neither OnTokenValidated nor OnAuthenticationFailed are called, and when authorization fails (because there is no authenticated identity) the pipeline is terminated.
This is a problem because I want to pass the request to a second authentication middleware if the JWT token validation fails the initial authentication.
Does anyone know what the recommended approach for this is?

The JWT middleware does apply the validation part only if it finds a token in the HTTP request, as you can see here.
The Authorize attribute now contains an ActiveAuthenticationSchemes property which defines which authentication schemes will be executed to try to authenticate the user. The advantage of that is you can now remove the AutomaticAuthenticate in the JWT middleware options, and it will be executed lazily when needed - like, if the user hits an action that doesn't require authentication, it will not execute.
One thing you might not like, though, is that MVC won't stop after an authentication scheme was successful, as you can see here. If that's a trade-off you're willing to take, I think it's a good way to go.

Related

How to set up OpenIddict to rely on AzureAd without using Microsoft.AspNetCore.Identity.UI

Our roles model is different so we can't use the stock Microsoft identity database model and all UX that goes with it, more's the pity.
All I want to do is
use OpenIdDict
have AzureAd do authentication
put my own claims into the claims principal so they go into the identity token when OpenIdDict creates it
I'm not interested in IdentityServer for assorted reasons.
I worked through a tutorial and had no trouble building all this using cookie based authn handled in an AccountController but I cannot figure out how to switch over to Azure and could really use some help.
Startup looks like this
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews();
// services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
// .AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, options =>
// {
// options.LoginPath = "/account/login";
// });
services.AddAuthentication()
.AddMicrosoftIdentityWebApi(Configuration.GetSection("AzureAd"));
// from package `Microsoft.Identity.Web`
services.AddDbContext<DbContext>(options =>
{
// Configure the context to use an in-memory store.
options.UseInMemoryDatabase(nameof(DbContext));
// Register the entity sets needed by OpenIddict.
options.UseOpenIddict();
});
services.AddHostedService<TestData>();
var openiddictBuilder = services.AddOpenIddict();
// Register the OpenIddict core components.
openiddictBuilder.AddCore(options =>
{
// Configure OpenIddict to use the EF Core stores/models.
options.UseEntityFrameworkCore()
.UseDbContext<DbContext>();
});
// Register the OpenIddict server components.
openiddictBuilder.AddServer(options =>
{
options
.AllowAuthorizationCodeFlow().RequireProofKeyForCodeExchange()
.AllowClientCredentialsFlow()
.AllowRefreshTokenFlow()
.SetAuthorizationEndpointUris("/connect/authorize")
.SetTokenEndpointUris("/connect/token")
// Encryption and signing of tokens
.AddEphemeralEncryptionKey()
.AddEphemeralSigningKey()
.DisableAccessTokenEncryption()
// Register scopes (permissions)
.RegisterScopes("api")
// Register the ASP.NET Core host and configure the ASP.NET Core-specific options.
.UseAspNetCore()
.EnableTokenEndpointPassthrough()
.EnableAuthorizationEndpointPassthrough()
;
});
}
There's an AuthorizeController with an Authorize method that looks like this
[HttpGet("~/connect/authorize")]
[HttpPost("~/connect/authorize")]
[IgnoreAntiforgeryToken]
public async Task<IActionResult> Authorize()
{
var request = HttpContext.GetOpenIddictServerRequest() ??
throw new InvalidOperationException("The OpenID Connect request cannot be retrieved.");
// Retrieve the user principal stored in the authentication cookie.
// var result = await HttpContext.AuthenticateAsync(CookieAuthenticationDefaults.AuthenticationScheme);
var result = await HttpContext.AuthenticateAsync(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
// If the user principal can't be extracted, redirect the user to the login page.
if (!result.Succeeded)
{
var authprops = new AuthenticationProperties
{
RedirectUri = Request.PathBase + Request.Path + QueryString.Create(
Request.HasFormContentType ? Request.Form.ToList() : Request.Query.ToList())
};
return Challenge(
authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
properties: authprops);
}
// Create a new claims principal
var claims = new List<Claim>
{
// 'subject' claim which is required
new Claim(OpenIddictConstants.Claims.Subject, result.Principal.Identity.Name),
new Claim(OpenIddictConstants.Claims.Role,"admin").SetDestinations(
OpenIddictConstants.Destinations.IdentityToken),
new Claim(OpenIddictConstants.Claims.Role,"gerbil wrangler").SetDestinations(
OpenIddictConstants.Destinations.IdentityToken)
};
var claimsIdentity = new ClaimsIdentity(claims, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
var claimsPrincipal = new ClaimsPrincipal(claimsIdentity);
// Set requested scopes (this is not done automatically)
claimsPrincipal.SetScopes(request.GetScopes());
// Signing in with the OpenIdDict authentiction scheme causes OpenIdDict
// to issue a code which can be exchanged for an access token
return SignIn(claimsPrincipal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
}
As I understand the theory of operation, OpenIddict proxies authentication and then issues a new token. That implies that the AzureAd redirect_uri ought to be set to an endpoint provided by OpenIddict, probably something like signin-openiddict and assuming that I'm right about all that, the client talking to OpenIddict will in turn provide a completely distinct and unrelated redirect_uri. But I haven't found any documentation covering this yet, so feel free to educate me.
In OpenIddict (and IdentityServer) the login and token generation are separated.
Those 2 parts are generally:
The user logs in using arbitrary methods and the authentication cookie is created.
The token endpoint reads the authentication cookie and creates tokens.
To use Azure Ad Authentication, you need to alter the first part to generate an authentication cookie using Azure Ad. To achieve this you'll need to implement the ExternalLogin and ExternalLoginCallback as seen in this example: https://github.com/openiddict/openiddict-core/blob/cda55862bcba67bf3de4ba08cf512ee9e2269cf5/samples/Mvc.Server/Controllers/AccountController.cs#L141
Instead of using the SignInManager, you need to create the authentication cookie yourself in the ExternalLoginCallback method. This can be done by using the HttpContext.SignInAsync method.
The second part (token generation) is left unchanged and should work without modification.

Can I create an Identity Server 4 ASP.NET Core API using 2 different token authentication middleware?

I am trying to figure out if its possible to write an ASP.NET Core API that consumes an identity server token using either Reference Tokens or JWT tokens based on whatever I've configured my identity server to use. The back-end configuration for IS4 is pretty easy, I'm just not convinced that I can configure 2 different token middlewares and my service will both be ok with it and know what to do.
So the idea is:
If my API gets a jwtToken, it attempts to use the jwt middleware for authorization back to identity server.
If my API gets a reference token, it attempts to use the introspection middleware for authorization back to identity server.
Obviously, if the wrong type of token is provided for whatever is configured on the IS4 service, it will fail.
Handling the token endpoint and revocation endpoint should also be easy enough, it's just the middleware magic I'm concerned with.
I know typically you wouldn't want to do this but we have a niche use case for it. All I'm currently concerned with is whether or not its even possible. I'm not familiar with how the auth middleware works in the back-end.
According to the Identity Server 4 Protecting APIs document, we can see that it supports to use both JWTs and reference tokens in asp.net core.
You can setup ASP.NET Core to dispatch to the right handler based on the incoming token, see this blog post for more information.
services.AddAuthentication("token")
// JWT tokens
.AddJwtBearer("token", options =>
{
options.Authority = Constants.Authority;
options.Audience = "resource1";
options.TokenValidationParameters.ValidTypes = new[] { "at+jwt" };
// if token does not contain a dot, it is a reference token
options.ForwardDefaultSelector = Selector.ForwardReferenceToken("introspection");
})
// reference tokens
.AddOAuth2Introspection("introspection", options =>
{
options.Authority = Constants.Authority;
options.ClientId = "resource1";
options.ClientSecret = "secret";
});
Supporting both JWTs and reference tokens
In addition to #Zhi Lv post you might need to add Authorization policy, Authentication Schemes to allow validating JWT and reference tokens.
Here is the sample code template replace api name, api secret and audience appropriatly.
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddAuthentication(IdentityServerAuthenticationDefaults.AuthenticationScheme)
.AddJwtBearer(Options =>
{
Options.Authority = "https://identity.domain.com/identity/";
Options.Audience = "resource1"; //your api baseurl e.g if you want userinfo_endpoint specify https://identity.domain.com/identity/connect/userinfo
Options.TokenValidationParameters.ValidTypes = new[] { "at+jwt" };
})
.AddIdentityServerAuthentication(options =>
{
options.Authority = "https://identity.domain.com/identity/";
options.ApiName = "api name / scope";
options.ApiSecret = "api secret / scope secret";
});
services.AddAuthorization(options =>
{
options.AddPolicy("tokens", x =>
{
x.AddAuthenticationSchemes("jwt", "introspection");
x.RequireAuthenticatedUser();
});
});
}
The way I would do it is to use introspection and claims caching in both cases, so that the API does not need to know or care which type of access token it receives.
The introspection would only occur when an access token is first received. Subsequent requests with the same token then use cached claims.
RESOURCES
Blog Post
Sample C# Code

AddOpenIdConnect and Refresh Tokens in ASP.NET Core

I have added AddOpenIdConnect to the ConfigureServices method of my ASP.NET Core 3.1 Razor application. It works great until the token expires, then I get 401 responses from my IDP.
I have seen an example that shows a way to wire up refresh tokens manually.
But I am hesitant to do that. It seems super unlikely that the folks at Microsoft did not think about refresh tokens.
Does ASP.NET Core 3.1 have a way to have refresh tokens automatically update the access token?
Here is what I came up with. Since there are not very many examples that I could find on how to do refresh tokens in ASP.NET Core with cookies, I thought I would post this here. (The one I link to in the question has issues.)
This is just my attempt at getting this working. It has not been used in any production setting. This code goes in the ConfigureServices method.
services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, options =>
{
options.Events = new CookieAuthenticationEvents
{
// After the auth cookie has been validated, this event is called.
// In it we see if the access token is close to expiring. If it is
// then we use the refresh token to get a new access token and save them.
// If the refresh token does not work for some reason then we redirect to
// the login screen.
OnValidatePrincipal = async cookieCtx =>
{
var now = DateTimeOffset.UtcNow;
var expiresAt = cookieCtx.Properties.GetTokenValue("expires_at");
var accessTokenExpiration = DateTimeOffset.Parse(expiresAt);
var timeRemaining = accessTokenExpiration.Subtract(now);
// TODO: Get this from configuration with a fall back value.
var refreshThresholdMinutes = 5;
var refreshThreshold = TimeSpan.FromMinutes(refreshThresholdMinutes);
if (timeRemaining < refreshThreshold)
{
var refreshToken = cookieCtx.Properties.GetTokenValue("refresh_token");
// TODO: Get this HttpClient from a factory
var response = await new HttpClient().RequestRefreshTokenAsync(new RefreshTokenRequest
{
Address = tokenUrl,
ClientId = clientId,
ClientSecret = clientSecret,
RefreshToken = refreshToken
});
if (!response.IsError)
{
var expiresInSeconds = response.ExpiresIn;
var updatedExpiresAt = DateTimeOffset.UtcNow.AddSeconds(expiresInSeconds);
cookieCtx.Properties.UpdateTokenValue("expires_at", updatedExpiresAt.ToString());
cookieCtx.Properties.UpdateTokenValue("access_token", response.AccessToken);
cookieCtx.Properties.UpdateTokenValue("refresh_token", response.RefreshToken);
// Indicate to the cookie middleware that the cookie should be remade (since we have updated it)
cookieCtx.ShouldRenew = true;
}
else
{
cookieCtx.RejectPrincipal();
await cookieCtx.HttpContext.SignOutAsync();
}
}
}
};
})
.AddOpenIdConnect(options =>
{
options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.Authority = oidcDiscoveryUrl;
options.ClientId = clientId;
options.ClientSecret = clientSecret;
options.RequireHttpsMetadata = true;
options.ResponseType = OidcConstants.ResponseTypes.Code;
options.UsePkce = true;
// This scope allows us to get roles in the service.
options.Scope.Add("openid");
options.Scope.Add("profile");
options.Scope.Add("offline_access");
// This aligns the life of the cookie with the life of the token.
// Note this is not the actual expiration of the cookie as seen by the browser.
// It is an internal value stored in "expires_at".
options.UseTokenLifetime = false;
options.SaveTokens = true;
});
This code has two parts:
AddOpenIdConnect: This part of the code sets up OIDC for the application. Key settings here are:
SignInScheme: This lets ASP.NET Core know you want to use cookies to store your authentication information.
*UseTokenLifetime: As I understand it, this sets an internal "expires_at" value in the cookie to be the lifespan of the access token. (Not the actual cookie expiration, which stays at the session level.)
*SaveTokens: As I understand it, this is what causes the tokens to be saved in the cookie.
OnValidatePrincipal: This section is called when the cookie has been validated. In this section we check to see if the access token is near or past expiration. If it is then it gets refreshed and the updated values are stored in the cookie. If the token cannot be refreshed then the user is redirected to the login screen.
The code uses these values that must come from your configuration file:
clientId: OAuth2 Client ID. Also called Client Key, Consumer Key, etc.
clientSecret: OAuth2 Client Secret. Also called Consumer Secret, etc.
oidcDiscoveryUrl: Base part of the URL to your IDP's Well Known Configuration document. If your Well Known Configuration document is at https://youridp.domain.com/oauth2/oidcdiscovery/.well-known/openid-configuration then this value would be https://youridp.domain.com/oauth2/oidcdiscovery.
tokenUrl: Url to your IDP's token endpoint. For example: https:/youridp.domain.com/oauth2/token
refreshThresholdMinutes: If you wait till the access token is very close to expiring, then you run the risk of failing calls that rely on the access token. (If it is 5 miliseconds from expiration then it could expire, and fail a call, before you get a chance to refresh it.) This setting is the number of minutes before expiration you want to consider an access token ready to be refreshed.
* I am new to ASP.NET Core. As such I am not 100% sure that those settings do what I think. This is just a bit of code that is working for me and I thought I would share it. It may or may not work for you.
As far as I know, there's nothing built-in in ASP.NET Core 3.1 to refresh access tokens automatically. But I've found this convenient library from the IdentityServer4 authors which stores access and refresh tokens in memory (this can be overriden) and refreshes access tokens automatically when you request them from the library.
How to use the library: https://identitymodel.readthedocs.io/en/latest/aspnetcore/web.html.
NuGet package: https://www.nuget.org/packages/IdentityModel.AspNetCore/.
Source code: https://github.com/IdentityModel/IdentityModel.AspNetCore.
I implemented token refresh in a .NET 7.0 sample recently. There has always been an option to refresh tokens and rewrite cookies, in many MS OIDC stacks, including older ones: Owin, .NET Core etc. It seems poorly documented though, and I had to dig around in the aspnet source code to figure out the cookie rewrite step. So I thought I'd add to this thread in case useful to future readers.
REFRESH TOKEN GRANT
First send a standards based refresh token grant request:
private async Task<JsonNode> RefreshTokens(HttpContext context)
{
var tokenEndpoint = "https://login.example.com/oauth/v2/token";
var clientId = "myclientid";
var clientSecret = "myclientsecret";
var refreshToken = await context.GetTokenAsync("refresh_token");
var requestData = new[]
{
new KeyValuePair<string, string>("client_id", clientId),
new KeyValuePair<string, string>("client_secret", clientSecret),
new KeyValuePair<string, string>("grant_type", "refresh_token"),
new KeyValuePair<string, string>("refresh_token", refreshToken),
};
using (var client = new HttpClient())
{
client.DefaultRequestHeaders.Add("accept", "application/json");
var response = await client.PostAsync(tokenEndpoint, new FormUrlEncodedContent(requestData));
response.EnsureSuccessStatusCode();
var json = await response.Content.ReadAsStringAsync();
return JsonNode.Parse(json).AsObject();
}
}
REWRITE COOKIES
Then rewrite cookies, which is done by 'signing in' with a new set of tokens. A better method name might have been something like 'update authentication state'. If you then look at the HTTP response you will see an updated set-cookie header, with the new tokens.
Note that in a refresh token grant response, you may or may not receive a new refresh token and new ID token. If not, then supply the existing values.
private async Task RewriteCookies(JsonNode tokens, HttpContext context)
{
var accessToken = tokens["access_token"]?.ToString();
var refreshToken = tokens["refresh_token"]?.ToString();
var idToken = tokens["id_token"]?.ToString();
var newTokens = new List<AuthenticationToken>();
newTokens.Add(new AuthenticationToken{ Name = "access_token", Value = accessToken });
if (string.IsNullOrWhiteSpace(refreshToken))
{
refreshToken = await context.GetTokenAsync("refresh_token");
}
newTokens.Add(new AuthenticationToken{ Name = "refresh_token", Value = refreshToken });
if (string.IsNullOrWhiteSpace(idToken))
{
idToken = await context.GetTokenAsync("id_token");
}
newTokens.Add(new AuthenticationToken{ Name = "id_token", Value = idToken });
var properties = context.Features.Get<IAuthenticateResultFeature>().AuthenticateResult.Properties;
properties.StoreTokens(newTokens);
await context.SignInAsync(context.User, properties);
}
SUMMARY
Being able to refresh access tokens when you receive a 401 response from an API is an essential capability in any web app. Use short lived access tokens and then code similar to the above, to renew them with good usability.
Note that relying on an expiry time is not fully reliable. API token validation can fail due to infrastructure events in some cases. APIs then return 401 for access tokens that are not expired. The web app should handle this via a refresh, followed by a retry of the API request.
AddOpenIdConnect is used to configure the handler that performs the OpenID Connect protocol to get tokens from your identity provider. But it doesn't know where you want to save the tokens. It could be any of the following:
Cookie
Memory
Database
You could store the tokens in a cookie then check the token's expire time and refresh the tokens by intercepting the cookie's validation event (as the example shows).
But AddOpenIdConnect doesn't have the logic to control where the user want to store the tokens and automatically implement token refresh.
You can also try to wrap the middleware as the ADAL.NET/MSAL.NET to provide cache features and then you can acquire/refresh tokens silently.

OpenIddict Get User Id in the token Response

Using ASP.NET Core with OpenIddict password grant.
When calling an authentication end point, I am getting this:
{
"token_type": "Bearer",
"access_token": "eyJhbGciOiJ...",
"expires_in": 1800
}
How can I include the user id in the response? I can see it in the decoded token, but my user app will not be decoding it.
How can I include the user id in the response?
Ideally, consider using the identity token - always a JWT by definition - returned by OpenIddict when you specify scope=openid.
Alternatively, you can also enable the userinfo endpoint and send a userinfo request to get back a sub claim containing the user identifier: http://openid.net/specs/openid-connect-core-1_0.html#UserInfo.
If you really prefer returning the user identifier as a token response property, you have two options:
Using a special "public" property (in your authorization controller, where authentication tickets are created):
ticket.SetProperty("user_id" + OpenIddictConstants.PropertyTypes.String, user.Id);
Note: OpenIddictConstants.PropertyTypes.String is a special suffix indicating the authentication property added to the ticket can be exposed as part of the token response. Other constants are available if you prefer returning your identifier as a JSON number or a more complex JSON structure.
Using the events model (in Startup.cs):
services.AddOpenIddict()
// Register the OpenIddict core services.
.AddCore(options =>
{
// ...
})
// Register the OpenIddict server handler.
.AddServer(options =>
{
// ...
options.AddEventHandler<OpenIddictServerEvents.ApplyTokenResponse>(
notification =>
{
if (string.IsNullOrEmpty(notification.Context.Error))
{
var principal = notification.Context.Ticket.Principal;
var response = notification.Context.Response;
response["user_id"] = principal.FindFirst(OpenIddictConstants.Claims.Subject).Value;
}
return Task.FromResult(OpenIddictServerEventState.Unhandled);
});
})
// Register the OpenIddict validation handler.
.AddValidation();

Meanjs user role authorization [duplicate]

I'm working on a MEAN application with authentication using JSON web tokens. basically on every request, I am checking to see if user has a valid token. if so they can go through to the route, otherwise they are returned to login page.
I want to make certain routes /admin/etc... only accessible to logged in users who are also admin. I have set up an isAdmin flag in mongo. I am new to nodejs and wondering what is the best way to check this. Do I do it on the angular side in routes? Or can I somehow create permission-based tokens on authentication? For reference, I am following the code from the MEAN Machine book, in particular here -
https://github.com/scotch-io/mean-machine-code/tree/master/17-user-crm
First, authorization decisions must be done on the server side. Doing it on the client side in Angular.js as you suggested is also a good idea, but this is only for the purpose of improving the user's experience, for example not showing the user a link to something they don't have access to.
With JWTs, you can embed claims about the user inside the token, like this:
var jwt = require('jsonwebtoken');
var token = jwt.sign({ role: 'admin' }, 'your_secret');
To map permissions to express routes, you can use connect-roles to build clean and readable authorization middleware functions. Suppose for example your JWT is sent in the HTTP header and you have the following (naive) authorization middleware:
// Naive authentication middleware, just for demonstration
// Assumes you're issuing JWTs somehow and the client is including them in headers
// Like this: Authorization: JWT {token}
app.use(function(req, res, next) {
var token = req.headers.authorization.replace(/^JWT /, '');
jwt.verify(token, 'your_secret', function(err, decoded) {
if(err) {
next(err);
} else {
req.user = decoded;
next();
}
});
})
With that, you can enforce your authorization policy on routes, like this:
var ConnectRoles = require('connect-roles');
var user = new ConnectRoles();
user.use('admin', function(req) {
return req.user && req.user.role === 'admin';
})
app.get('/admin', user.is('admin'), function(req, res, next) {
res.end();
})
Note that there are much better options for issuing & validating JWTs, like express-jwt, or using passport in conjunction with passort-jwt