ASP .Net Core Access the DistributedRedisCache in the JWTBearerOptions.Events OnTokenValidated Event - asp.net

I have an ASP .Net Core API Project. In this project I am using JWTBearer Authentication. I am also using the AddDistributedRedisCache feature of the .Net Core Dependency Injection. (Both shown below)
We have a need to blacklist the tokens on occasion (Admin user removing rights, logout, etc) so that these can take immediate effect. Essentially forcing a user to log back in before the next call can be made.
We are adding the JWT Tokens to the redis cache as well as removing them from the client side cache on logout. But a user could (in theory) store the JWT token, and still gain access until the token expires, unless we intercept the call and check it against the blacklist.
How can I access the distributed cache object in the "OnTokenValidated" event in the code below? Do I have to manually create a new connection each time? We are only checking valid tokens, as that will stop invalid requests from even being checked against the blacklist.
Bearer Token Config:
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = "localhost:5000",
ValidAudience = "localhost:5000",
IssuerSigningKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(Configuration.GetValue<string>("SigningKey"))),
};
options.Events = new JwtBearerEvents
{
OnTokenValidated = context => {
//context.Fail("User has been logged out");
return Task.CompletedTask;
}
};
});
Redis Cache Config:
services.AddDistributedRedisCache(option =>
{
option.Configuration = Configuration.GetValue<string>("RedisCacheAddress");
option.InstanceName = Configuration.GetValue<string>("RedisCacheInstance");
});

You can access services in DI utilizing the HttpContext available there:
OnTokenValidated = ctx =>
{
var cache = ctx.HttpContext.RequestServices.GetRequiredService<IDistributedCache>();
return Task.CompletedTask;
}
GetRequiredService will throw an exception if the service is not found. You can use GetService<T>() if you want the service to be optional.

Related

Asp Net ABP: how to map claims from JWT token to ABP User?

We are using ASP.NET Zero based on ASP.NET Boilerplate (.NET Core). We have a Mobile App that authenticates with AWS Cognito and needs to consume some API's and Application Services on our application. (We already make sure that each user in Cognito also has a corresponding user in AbpUsers with permissions.)
The Mobile App will authenticate with a JWT Bearer Token from Cognito. We have added JWT Validation to our Startup.cs as follows:
services.AddAuthentication(options => {
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultSignInScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options => {
options.ClaimsIssuer = CognitoIssuer;
options.SaveToken = true;
options.TokenValidationParameters = new TokenValidationParameters
{
IssuerSigningKeyResolver = (s, securityToken, identifier, parameters) =>
{
var json = new WebClient().DownloadString(CognitoIssuer + "/.well-known/jwks.json");
var keys = JsonConvert.DeserializeObject<JsonWebKeySet>(json)!.Keys;
return keys;
},
RoleClaimType = "cognito:groups",
NameClaimType = "username",
ValidIssuer = CognitoIssuer,
ValidateIssuerSigningKey = true,
ValidateIssuer = true,
ValidateLifetime = true,
ValidateAudience = false
};
});
This validation works in so far as when I decorate an API with the [Authorize] attribute, the API only executes if a valid JWT token is provided. (With an invalid token, a 401 error is returned as expected.)
The problem we have is that the user identity is not being set. I can see that the Claims Principal contains the claims from Cognito. But inside the API the current user and tenant appears as NULL. I assume that after JWT validation I'm missing steps to "map" the claims/user from Cognito to an actual user that exists in the ABP database so that the AbpSession can reflect this user. But how do I do that? How do I set the Identity/User/Tenant/AbpSession so that it can correctly and seamlessly be picked up in my Application Services and Controllers after I've validated the JWT token? (So that Application Services and Controllers can work just as seamlessly as if the user had actually logged on in the ABP app?)

Microsoft.Owin.Security.OpenIdConnect AuthenticationTicket is null on AuthorizationCodeReceived

I'm using OpenIdConnectAuthentication with code flow to implement the OpenIdConnect login. But on the AuthorizationCodeReceived, the property notification.AuthenticationTicket is null value. Any advice?
Here is my startup:
public void Configuration(IAppBuilder app)
{
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
string auth0RedirectUri = "http://localhost:44335/";
app.UseCookieAuthentication(new CookieAuthenticationOptions(){});
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
AuthenticationType = "OIDC",
ClientId = "qKu-JoUguDjzrvBm*****",
ClientSecret = "w7JPnYYIttT8aDYPrZL9lvQzNaXP0QDqyVMu4AHZYWkUrczG4WJThmo3blHEvfz*******",
Authority = "https://******/authorize",
RedirectUri= auth0RedirectUri,
ResponseType = OpenIdConnectResponseType.Code,
Scope = OpenIdConnectScope.Email+" "+OpenIdConnectScope.OpenIdProfile,
TokenValidationParameters = new TokenValidationParameters()
{
ValidateIssuer = false // This is a simplification
},
Notifications = new OpenIdConnectAuthenticationNotifications
{
AuthorizationCodeReceived = (notification) =>
{
Debug.WriteLine("*** AuthorizationCodeReceived");
//TODO: get access token from token endpoint later
var authClaim = new ClaimsIdentity("OIDC", ClaimsIdentity.DefaultNameClaimType, ClaimsIdentity.DefaultRoleClaimType);
authClaim.AddClaim(new System.Security.Claims.Claim("Email","abc#mail.com"));
// notification.AuthenticationTicket is null
notification.AuthenticationTicket = new AuthenticationTicket(authClaim, notification.AuthenticationTicket.Properties);
return Task.FromResult(0);
},
AuthenticationFailed = (context) =>
{
Debug.WriteLine("*** AuthenticationFailed");
return Task.FromResult(0);
},
},
UsePkce = false
});
}
I was recently diving into OpenIDConnect on old ASP.NET framework and had a lot of truble as well.
It will be very difficult to answer you question - becasue I don't know what exactly you want to achieve. Basically as far as I understand the flow AuthorizationCodeReceived gets triggered when user logs-in on authentication server side and gets navigated back with Code query parameter. So at this point AuthenticationTicket should be null because nothing really set it yet.
Now developer has a choice if you want to handle CodeRedemption themselves or leave that to the OpenIDConnectAuthenticationHandler.
I personally did not use first option. I used this step only to transform code into jwe token because my Auth server requires it. But if your choice is to handle it youself then probably you need to do something like in samples available on Katana github project:
AuthorizationCodeReceived = async n =>
{
var _configuration = await n.Options.ConfigurationManager.GetConfigurationAsync(n.OwinContext.Request.CallCancelled);
var requestMessage = new System.Net.Http.HttpRequestMessage(System.Net.Http.HttpMethod.Post, _configuration.TokenEndpoint);
requestMessage.Content = new System.Net.Http.FormUrlEncodedContent(n.TokenEndpointRequest.Parameters);
var responseMessage = await n.Options.Backchannel.SendAsync(requestMessage);
responseMessage.EnsureSuccessStatusCode();
var responseContent = await responseMessage.Content.ReadAsStringAsync();
Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectMessage message = new Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectMessage(responseContent);
n.HandleCodeRedemption(message);
}
https://github.com/aspnet/AspNetKatana/blob/beb224c88712b08ce45f1d14bb8cf0cd9d4a8503/samples/Katana.Sandbox.WebServer/Startup.cs#L157
If you will choose not to do it yourself then you will have to set RedeemCode = true on OpenIdConnectAuthenticationOptions. Then handler will get the token and will set context properly.
There is not much of a documentation anywhere but for me very usefull was Katana project on github. Almost whole flow is implemented in https://github.com/aspnet/AspNetKatana/blob/main/src/Microsoft.Owin.Security.OpenIdConnect/OpenidConnectAuthenticationHandler.cs
You can check what is the flow and what each Notfication is used for.
Unfortunately I cannot help you much more because each flow might be different and only way to say in detail what needs to be done is to reproduce your specific environment.

AddDownstreamWebApi fails after adding multiple auth schemes

Problem: My as an app calls to a downstream web api throw a null exception error after adding my own jwt bearer authentication.
I have a .net 5 web API, call it AppAPI, whose ConfigureServices has the following code:
var accessTokenKey = Convert.FromBase64String(Configuration.GetValue<string>("AccessCodeSecret"));
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer("AccessToken", o =>
{
o.RequireHttpsMetadata = false;
o.SaveToken = true;
o.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(accessTokenKey),
ValidateIssuer = false,
ValidateAudience = false
};
})
.AddMicrosoftIdentityWebApi(Configuration, "AzureAd", "AzureAd")
.EnableTokenAcquisitionToCallDownstreamApi()
.AddDownstreamWebApi("CommonServicesApi", Configuration.GetSection("CommonServicesApi"))
.AddInMemoryTokenCaches();
//services.AddAuthorization();
services.AddAuthorization(options =>
{
options.DefaultPolicy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.AddAuthenticationSchemes("AccessToken")
.Build();
});
I have an /auth endpoint that accepts an access token from Azure AD, and generates a new access token with my own custom claims based on the database. The controller uses an authorize attribute to ensure it uses the correct mechanism:
[Authorize(AuthenticationSchemes = "AzureAd")]
The default policy from above ensures the rest of the endpoints use this access token for every other endpoint that doesnt specify a scheme.
I have a 2nd web API, called CommonServices, that is only accessible from other APIs, not clients directly. So AppAPI uses AddDownstreamwebapi to handle those calls. This worked previously to me adding my own app access tokens, meaning I only had one auth mechanism - AddMicrosoftIdentityWebApi. I started receiving my error when I added my own JwtBearer auth - "AccessToken".
The controller that has the error injects IDownstreamWebApi commonServicesApi. It uses the default auth scheme of "AccessToken". The code looks like this:
var response = await _commonServicesApi.CallWebApiForAppAsync("CommonServicesApi", "AzureAd",
options => { options.RelativePath = "Projects"});
var json = await response.Content.ReadAsStringAsync();
The 2nd parameter "AzureAd" was my attempt to have the commonservicesApi use the correct scheme. I am not even sure if that's the right scheme to use, or if .EnbleTokenAcquisitionToCallDownstreamApi adds a 3rd scheme that should be specified.
It is this call that I receive
System.NullReferenceException: 'Object reference not set to an instance of an object.'
at Microsoft.Identity.Web.MergedOptions.PrepareAuthorityInstanceForMsal()
This exception was originally thrown at this call stack:
Microsoft.Identity.Web.MergedOptions.PrepareAuthorityInstanceForMsal()
Microsoft.Identity.Web.TokenAcquisition.BuildConfidentialClientApplication(Microsoft.Identity.Web.MergedOptions)
Microsoft.Identity.Web.TokenAcquisition.GetOrBuildConfidentialClientApplication(Microsoft.Identity.Web.MergedOptions)
Microsoft.Identity.Web.TokenAcquisition.GetAuthenticationResultForAppAsync(string, string, string, Microsoft.Identity.Web.TokenAcquisitionOptions)
Microsoft.Identity.Web.DownstreamWebApi.CallWebApiForAppAsync(string, string, System.Action<Microsoft.Identity.Web.DownstreamWebApiOptions>, System.Net.Http.StringContent)
System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
I can't seem to figure out what is null, or how to approach solving this problem.

New JWT token has same expiry as old token

I've got JWT token generation working in my ASP.NET Core 2.0 Web API, but I'm running into an issue where subsequent new access tokens have the same expiry as previously generated ones.
For instance, I post login credentials, and return an access token. The access token works as expected on [Authorize] API endpoints. For testing purposed, I set the token to expire after 1 minute. After 1 minute, the token expires and the authenticated endpoints return a 401, as expected.
I'm handling the 401's in my client side application. The login form appears, and the user logs in again. A new token is generated and returned. The only issue is, this new token has the exact same 'ValidTo" DateTime as the initially generated token. Causing any calls after using this new token to return 401 because the token is already expired. I've confirmed that two different tokens are being checked, so it's not an issue with me passing the wrong token
First token failure (expected, as token expired):
Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerHandler: Information: Failed to validate the token eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZGFtQHBytNDRiYS1...Do1NzM5NS8ifQ.t8DjvlGV7GZ3xucwu-1hlJRXA5owPdP9t7kfYiiJHyQ.
Microsoft.IdentityModel.Tokens.SecurityTokenExpiredException: IDX10223: Lifetime validation failed. The token is expired.
ValidTo: '11/08/2017 19:23:09'
Current time: '11/08/2017 19:23:13'.
Second token failure (not expected, ValidTo same as previous token)
Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerHandler: Information: Failed to validate the token eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZGFtQ...dDo1NzM5NS8ifQ.2TMPJvYnQl1Jw78M2nj40uD3qejBEciXfKC845saGNI.
Microsoft.IdentityModel.Tokens.SecurityTokenExpiredException: IDX10223: Lifetime validation failed. The token is expired.
ValidTo: '11/08/2017 19:23:09'
Current time: '11/08/2017 19:23:34'.
JWT Configuration in Startup.cs
services.Configure<JwtIssuerOptions>(options => {
options.Issuer = jwtAppSettingOptions[nameof(JwtIssuerOptions.Issuer)];
options.Audience = jwtAppSettingOptions[nameof(JwtIssuerOptions.Audience)];
options.SigningCredentials = new SigningCredentials(SigningKey, SecurityAlgorithms.HmacSha256);
options.ValidFor = TimeSpan.FromMinutes(1);
});
services.AddAuthentication(o =>
{
o.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
o.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(o =>
{
o.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuer = jwtAppSettingOptions[nameof(JwtIssuerOptions.Issuer)],
ValidateAudience = true,
ValidAudience = jwtAppSettingOptions[nameof(JwtIssuerOptions.Audience)],
ValidateIssuerSigningKey = true,
IssuerSigningKey = SigningKey,
RequireExpirationTime = true,
ValidateLifetime = true,
ClockSkew = TimeSpan.Zero,
};
});
Login action where Token is created:
[HttpPost]
public async Task<IActionResult> Login([FromBody]CredentialsViewModel credentials)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
var identity = await GetClaimsIdentity(credentials.UserName, credentials.Password);
if (identity == null)
{
return BadRequest(Errors.AddErrorToModelState("login_failure", "Invalid username or password.", ModelState));
}
// Serialize and return the response
var response = new
{
id = identity.Claims.Single(c => c.Type == "id").Value,
auth_token = await _jwtFactory.GenerateEncodedToken(credentials.UserName, identity),
expires_in = (int)_jwtOptions.ValidFor.TotalSeconds
};
var json = JsonConvert.SerializeObject(response, _serializerSettings);
return new OkObjectResult(json);
}
JwtFactory method where token is being generated:
private readonly JwtIssuerOptions _jwtOptions;
public JwtFactory(IOptions<JwtIssuerOptions> jwtOptions)
{
_jwtOptions = jwtOptions.Value;
ThrowIfInvalidOptions(_jwtOptions);
}
public async Task<string> GenerateEncodedToken(string userName, ClaimsIdentity identity)
{
var claims = new[]
{
new Claim(JwtRegisteredClaimNames.Sub, userName),
new Claim(JwtRegisteredClaimNames.Jti, await _jwtOptions.JtiGenerator()),
new Claim(JwtRegisteredClaimNames.Iat, ToUnixEpochDate(_jwtOptions.IssuedAt).ToString(), ClaimValueTypes.Integer64),
identity.FindFirst("rol"),
identity.FindFirst("id")
};
// Create the JWT security token and encode it.
var jwt = new JwtSecurityToken(
issuer: _jwtOptions.Issuer,
audience: _jwtOptions.Audience,
claims: claims,
notBefore: _jwtOptions.NotBefore,
expires: _jwtOptions.Expiration,
signingCredentials: _jwtOptions.SigningCredentials);
var encodedJwt = new JwtSecurityTokenHandler().WriteToken(jwt);
return encodedJwt;
}
The issue was in my jwtFactory, I was dependency injecting IOptions. Since this is defined in the startup, and has several properties that are automatically filled when the object is created (such as IssuedAt, which gets DateTime.NowUtc), IOptions was only returning the configuration of the first time it was loaded.
I was able to solve this by injecting IOptionsSnapshot, which grabs a new version of JwtIssuerOptions, which would have an updated IssuedAt property.
private readonly JwtIssuerOptions _jwtOptions;
public JwtFactory(IOptionsSnapshot<JwtIssuerOptions> jwtOptions)
{
_jwtOptions = jwtOptions.Value;
ThrowIfInvalidOptions(_jwtOptions);
}
Just a suggestion, the _jwtOptions.Expiration should be a timespan, i.e. how long the token should be valid for, so let's say it's 20 and in minutes, in that case you should have expires: DateTime.UtcNow.AddMinutes(_jwtOptions.Expiration) or something similar. And may be even rename it to reflect that.

Multiple Authentication Middlewares ASP.NET Core

I am relatively new to the concept of middlewares. I am aware that a middleware calls the next middleware when it completes.
I am trying to authenticate a request using either Google or my Identity Server. The user can login on my mobile app with google or a local account. However, I can't figure out how to use both authentication middlewares. If I pass the id_token for google, it passes on the first middleware (UseJwtBearerAuthentication) but fails on the second one (UseIdentityServerAuthentication). How can I make it so that it doesn't throw error when it actually passes on at least 1 authentication middleware? For example, if it passes on the first middleware, the second middleware is ignored?
app.UseJwtBearerAuthentication(new JwtBearerOptions()
{
Authority = "https://accounts.google.com",
Audience = "secret.apps.googleusercontent.com",
TokenValidationParameters = new TokenValidationParameters()
{
ValidateAudience = true,
ValidIssuer = "accounts.google.com"
},
RequireHttpsMetadata = false
});
app.UseIdentityServerAuthentication(new IdentityServerAuthenticationOptions
{
Authority = "http://localhost:1000/",
RequireHttpsMetadata = false,
ScopeName = "MyApp.Api"
});
Normally, when an authentication middleware is failed(i don't mean throwing exception), this doesn't affect another successful authentication middleware. Probably your second middleware throws an exception(not a validation failure). First check error message and try to resolve it. If you can't, use AuthenticationFailed event to handle error. In this case your code should be something like below:
app.UseJwtBearerAuthentication(new JwtBearerOptions()
{
// ...
Events = new JwtBearerEvents()
{
OnAuthenticationFailed = async (context) =>
{
if (context.Exception is your exception)
{
context.SkipToNextMiddleware();
}
}
}
});
However, for your scenerio i wouldn't choose your way. I would use only identity server endpoint. For signing with google you can configure identity server like below:
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme,
AutomaticAuthenticate = false,
AutomaticChallenge = false
});
app.UseGoogleAuthentication(new GoogleOptions
{
AuthenticationScheme = "Google",
SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme,
ClientId = "",
ClientSecret = ""
});
app.UseIdentityServer();
Edit
It seems AuthenticationFailed event couldn't be used for IdentityServer4.AccessTokenValidation. I am not sure but if you will use identity server for only jwt token, you can use UseJwtBearerAuthentication for validation.

Resources