Web API giving 401 on JWT - c#

I have the following problem:
The web api uses JWT to authorize people. I have been following this tutorial: here
The token provider works fine as shown on the postman picture:
But when I try to pass the token in postman to the following controller:
[Authorize]
[Route("ChangePassword")]
public async Task<IHttpActionResult> ChangePassword(ChangePasswordBindingModel model) {
if (!ModelState.IsValid) {
return BadRequest(ModelState);
}
IdentityResult result = await this.AppUserManager.ChangePasswordAsync(User.Identity.GetUserId(), model.OldPassword, model.NewPassword);
if (!result.Succeeded)
return GetErrorResult(result);
return Ok();
}
Then this will be the result:
I can't see what the problem should be. I do in the startup file also start the API last.
public class Startup {
public void Configuration(IAppBuilder app) {
HttpConfiguration httpConfig = new HttpConfiguration();
ConfigureOAuthTokenGeneration(app);
ConfigureOAuthTokenConsumption(app);
ConfigureWebApi(httpConfig);
app.UseCors(Microsoft.Owin.Cors.CorsOptions.AllowAll);
app.UseWebApi(httpConfig);
}
private void ConfigureWebApi(HttpConfiguration httpConfig) {
httpConfig.MapHttpAttributeRoutes();
var jsonFormatter = httpConfig.Formatters.OfType<JsonMediaTypeFormatter>().First();
jsonFormatter.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();
}
private void ConfigureOAuthTokenGeneration(IAppBuilder app) {
app.CreatePerOwinContext(ApplicationDbContext.Create);
app.CreatePerOwinContext<ApplicationUserManager>(ApplicationUserManager.Create);
OAuthAuthorizationServerOptions OAuthServerOptions = new OAuthAuthorizationServerOptions() {
//Set to false in production
AllowInsecureHttp = true,
TokenEndpointPath = new PathString("/oauth/token"),
AccessTokenExpireTimeSpan = TimeSpan.FromDays(1),
Provider = new CustomOAuthProvider(),
AccessTokenFormat = new CustomJwtFormat("http://localhost:44300")
};
app.UseOAuthAuthorizationServer(OAuthServerOptions);
}
private void ConfigureOAuthTokenConsumption(IAppBuilder app) {
var issuer = "http://localhost:44300";
var audienceId = "414e1927a3884f68abc79f7283837fd1";
var audienceSecret = TextEncodings.Base64Url.Decode("qMCdFDQuF23RV1Y-1Gq9L3cF3VmuFwVbam4fMTdAfpo");
// Api controllers with an [Authorize] attribute will be validated with JWT
app.UseJwtBearerAuthentication(
new JwtBearerAuthenticationOptions
{
AuthenticationMode = AuthenticationMode.Active,
AllowedAudiences = new[] { audienceId },
IssuerSecurityTokenProviders = new IIssuerSecurityTokenProvider[]
{
new SymmetricKeyIssuerSecurityTokenProvider(issuer, audienceSecret)
}
});
}
}

I've recently went through the same tutorial and had a similar problem. All endpoints with an [Authorize] attribute were returning 401. I completely broke apart the JwtBearerAuthentication middleware and discovered a problem with the JWTSecurityTokenHandler determining whether an audience was valid.
For starters, as most guides will tell you, verify your audience, issuer, and secret are the same where you generate your JWT token and where you ConfigureOAuthConsumption. I found it was easy to confuse these on the JWT creation side. If they are both correct look at the code below.
I ended up creating my own JWT Handler which derives from JwtSecurityTokenHandler. It mostly just calls the base methods, but it does give you great insight into how the validation process works. Please note the code change in ValidateToken.
class CustomJWTTokenHandler : JwtSecurityTokenHandler
{
public CustomJWTTokenHandler():base()
{
}
public override bool CanReadToken(string tokenString)
{
var rtn = base.CanReadToken(tokenString);
return rtn;
}
public override bool CanValidateToken
{
get
{
return base.CanValidateToken;
}
}
protected override ClaimsIdentity CreateClaimsIdentity(JwtSecurityToken jwt, string issuer, TokenValidationParameters validationParameters)
{
return base.CreateClaimsIdentity(jwt, issuer, validationParameters);
}
public override ReadOnlyCollection<ClaimsIdentity> ValidateToken(SecurityToken token)
{
try
{
var rtn = base.ValidateToken(token);
return rtn;
}
catch (Exception)
{
throw;
}
}
public override ClaimsPrincipal ValidateToken(string securityToken, TokenValidationParameters validationParameters, out SecurityToken validatedToken)
{
var jwt = this.ValidateSignature(securityToken, validationParameters);
if (validationParameters.ValidateAudience)
{
if (validationParameters.AudienceValidator != null)
{
if (!validationParameters.AudienceValidator(jwt.Audiences, jwt, validationParameters))
{
throw new SecurityTokenInvalidAudienceException(string.Format(CultureInfo.InvariantCulture, ErrorMessages.IDX10231, jwt.ToString()));
}
}
else
{
base.ValidateAudience(validationParameters.ValidAudiences, jwt, validationParameters);
}
}
string issuer = jwt.Issuer;
if (validationParameters.ValidateIssuer)
{
if (validationParameters.IssuerValidator != null)
{
issuer = validationParameters.IssuerValidator(issuer, jwt, validationParameters);
}
else
{
issuer = ValidateIssuer(issuer, jwt, validationParameters);
}
}
if (validationParameters.ValidateActor && !string.IsNullOrWhiteSpace(jwt.Actor))
{
SecurityToken actor = null;
ValidateToken(jwt.Actor, validationParameters, out actor);
}
ClaimsIdentity identity = this.CreateClaimsIdentity(jwt, issuer, validationParameters);
if (validationParameters.SaveSigninToken)
{
identity.BootstrapContext = new BootstrapContext(securityToken);
}
validatedToken = jwt;
return new ClaimsPrincipal(identity);
}
protected override JwtSecurityToken ValidateSignature(string token, TokenValidationParameters validationParameters)
{
var rtn = base.ValidateSignature(token, validationParameters);
var issuer = rtn.Issuer;
return rtn;
}
protected override void ValidateAudience(IEnumerable<string> audiences, SecurityToken securityToken, TokenValidationParameters validationParameters)
{
if (audiences !=null && audiences.Any())
{
var jwt = securityToken as JwtSecurityToken;
if (!jwt.Audiences.Any())
{
throw new Exception("token has no audiences defined");
}
var inBothList= audiences.Where(X => jwt.Audiences.Contains(X)).ToList();
if (!inBothList.Any()){
throw new Exception("token not in audience list");
}
}
//base.ValidateAudience(audiences, securityToken, validationParameters);
}
public override SecurityToken ReadToken(string tokenString)
{
var rtnToken = base.ReadToken(tokenString);
//var validations = this.ValidateToken(rtnToken);
return rtnToken;
}
}
This handler is wired up when you setup the UseJwtBearerAuthentication middleware:
app.UseJwtBearerAuthentication(
new JwtBearerAuthenticationOptions
{
AuthenticationMode = Microsoft.Owin.Security.AuthenticationMode.Active,
AllowedAudiences = new List<string>() { JWTConfigs.audience },
IssuerSecurityTokenProviders = new IIssuerSecurityTokenProvider[]
{
new CustomSymmetricKeyIssuerSecurityTokenProvider(JWTConfigs.issuer, key)
},
TokenHandler = new CustomJWTTokenHandler()
}
);
Hopefully this works for you or at least points you to why your Token is failing.

I think I figured out the problem with both tutorials (this and this). Take a look at CustomJwtFormat class Protect() method
public string Protect(AuthenticationTicket data) {
...
var issued = data.Properties.IssuedUtc;
...
var token = new JwtSecurityToken(_issuer, audienceId, data.Identity.Claims,
issued.Value.UtcDateTime, expires.Value.UtcDateTime, signingKey);
...
}
It looks like JwtSecurityToken is expecting notBefore parameter in local and not in UTC timezone. If the author is in Jordan (UTC+2), it will mean that notBefore will be 2 hours earlier than now, but it will still work. However, since I am in California (UTC-8), notBefore is set to 8 hours later and token validation fails! The solution is to have
DateTimeOffset issued = data.Properties.IssuedUtc?.ToLocalTime();
in the new C# 6 format, or
DateTimeOffset issued = data.Properties.IssuedUtc.Value.ToLocalTime()
using classic syntax. Thank you #Treetopvt for pushing me to debug through middleware. With this change standard JwtSecurityTokenHandler works fine

Related

Role Base Authorization with UseJwtBearerAuthentication on Net 4.7.2 WebApi

My desktop app call a Net WebApi using access token coming from keycloak. The Authorization attribute on my webapi running well but my WebApi it can't work with authorization. I tried to inject role claims but the api keep on error 401. Here is my Startup.cs
[assembly: OwinStartup(typeof(Demo.Startup))]
namespace Demo
{
public class CustomOAuthBearerProvider : OAuthBearerAuthenticationProvider
{
public override Task ValidateIdentity(OAuthValidateIdentityContext context)
{
var claims = context.Ticket.Identity.Claims;
context.Ticket.Identity.AddClaim(new Claim(ClaimTypes.Role, "Admin")); // injected claims here
base.ValidateIdentity(context);
return Task.FromResult<object>(null);
}
}
public partial class Startup
{
static string _realm = "myrealm";
static string _authority = "http://localhost:8080/realms/myrealm";
public void Configuration(IAppBuilder app)
{
IdentityModelEventSource.ShowPII = true;
var domain = _authority;
var apiIdentifier = "account";
var keyResolver = new OpenIdConnectSigningKeyResolver(domain);
app.UseJwtBearerAuthentication(
new JwtBearerAuthenticationOptions
{
AuthenticationMode = AuthenticationMode.Active,
TokenValidationParameters = new TokenValidationParameters()
{
ValidAudience = apiIdentifier,
ValidIssuer = domain.TrimEnd('/'),
IssuerSigningKeyResolver = (token, securityToken, kid, parameters) => keyResolver.GetSigningKey(kid),
},
Provider = new CustomOAuthBearerProvider()
});
System.Net.ServicePointManager.ServerCertificateValidationCallback = delegate { return true; };
}
}
My controller:
[Route("test")]
[Authorize(Roles = "Admin")]
public Strig Test()
{
return "Hello";
}
test method always returning 401 eventhough I inject an Admin role from ValidateIdentity. The method working properly if i take out Roles decoration.

Minimal api using AddJwtBearer token - Response always Unauthorized

I have a minimal api here. It creates me a JWT for authorization, and then it tests it. using the [Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] attribute.
app.MapPost("/login", [AllowAnonymous]
async (HttpContext http, ITokenService tokenService, IUserRepositoryService userRepositoryService) =>
{
var userLogin = await http.Request.ReadFromJsonAsync<UserModel>();
var userDto = userRepositoryService.GetUser(userLogin);
if (userDto == null)
{
http.Response.StatusCode = 401;
return;
}
var token = tokenService.BuildToken(builder.Configuration["Jwt:Key"], builder.Configuration["Jwt:Issuer"],
builder.Configuration["Jwt:Audience"], userDto);
await http.Response.WriteAsJsonAsync(new { Token = token });
});
app.MapGet("/secretAction",
(Func<string>)([Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]() => "Action Succeeded")
);
The first method works just fine. A post to the login endpoint with a json string containing the login and password returns to me a jwt.
The second endpoint is always returning unauthorized.
I have been back and forth with this trying to understand why its not able to parse the token that it creates.
Any help would be greatly appreciated.
appsettings.json
"Jwt": {
"Key": "this-is-the-secret",
"Issuer": "https://jwtauth.example.com",
"Audience": "api1"
}
program.cs
using System.ComponentModel.DataAnnotations;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization;
using Microsoft.IdentityModel.Tokens;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton<ITokenService>(new TokenService());
builder.Services.AddSingleton<IUserRepositoryService>(new UserRepositoryService());
builder.Services.AddAuthorization();
var key = Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"]);
builder.Services
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(opt =>
{
opt.RequireHttpsMetadata = false;
opt.SaveToken = true;
opt.TokenValidationParameters = new TokenValidationParameters
{
IssuerSigningKey = new SymmetricSecurityKey(key),
ValidateIssuerSigningKey = true,
ValidIssuer = builder.Configuration["Jwt:Issuer"],
ValidateIssuer = false,
ValidAudience = builder.Configuration["Jwt:Audience"],
ValidateAudience = true,
ValidateLifetime = true,
};
});
await using var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
if (app.Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.MapGet("/",
(Func<string>)(() =>
"Test JWT Authentication using Minimalist Web API .net 6. <br> /login UserName: user1, Password: test <br> /secretAction "));
app.MapPost("/login", [AllowAnonymous]
async (HttpContext http, ITokenService tokenService, IUserRepositoryService userRepositoryService) =>
{
var userLogin = await http.Request.ReadFromJsonAsync<UserModel>();
var userDto = userRepositoryService.GetUser(userLogin);
if (userDto == null)
{
http.Response.StatusCode = 401;
return;
}
var token = tokenService.BuildToken(builder.Configuration["Jwt:Key"], builder.Configuration["Jwt:Issuer"],
builder.Configuration["Jwt:Audience"], userDto);
await http.Response.WriteAsJsonAsync(new { Token = token });
});
app.MapGet("/secretAction",
(Func<string>)([Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]() => "Action Succeeded")
);
await app.RunAsync();
public record UserDto(string UserName, string Password);
public record UserModel
{
[Required] public string UserName { get; set; }
[Required] public string Password { get; set; }
}
public interface IUserRepositoryService
{
UserDto GetUser(UserModel userModel);
}
public class UserRepositoryService : IUserRepositoryService
{
private List<UserDto> _users => new()
{
new("User1", "test"),
};
public UserDto GetUser(UserModel userModel)
{
return _users.FirstOrDefault(x =>
string.Equals(x.UserName, userModel.UserName) && string.Equals(x.Password, userModel.Password));
}
}
public interface ITokenService
{
string BuildToken(string key, string issuer, string audience, UserDto user);
}
public class TokenService : ITokenService
{
private TimeSpan ExpiryDuration = new TimeSpan(0, 30, 0);
public string BuildToken(string key, string issuer, string audience, UserDto user)
{
var keyBytes = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(key));
var claims = new[]
{
new Claim(ClaimTypes.Name, user.UserName),
new Claim(ClaimTypes.NameIdentifier, Guid.NewGuid().ToString())
};
var descriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(claims),
Issuer = issuer,
Audience = audience,
SigningCredentials = new SigningCredentials(keyBytes, SecurityAlgorithms.HmacSha256Signature),
IssuedAt = DateTime.Now,
NotBefore = DateTime.Now,
Expires = DateTime.Now.AddDays(1)
};
var jwtHandler = new JwtSecurityTokenHandler();
var token = jwtHandler.CreateToken(descriptor);
return jwtHandler.WriteToken(token);
// alternatively
// return jwtHandler.CreateEncodedJwt(descriptor);
}
To test it.
const string jwtUrl = "https://localhost:7080/login";
var content = new StringContent("{\"UserName\" : \"User1\",\"Password\" : \"test\"}", Encoding.UTF8, "application/json");
var httpResponseMessage = await client.PostAsync(jwtUrl,content);
var jwt = await httpResponseMessage.Content.ReadAsStringAsync();
var queueMessage = JsonSerializer.Deserialize<TokenResponse>(jwt);
Console.WriteLine(queueMessage.token);
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", queueMessage.token);
const string protectedUrl = "https://localhost:7080/secretAction";
var result = await client.GetStringAsync(protectedUrl);
Console.WriteLine(result);
Logs
Here is a JWT just created.
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiVXNlcjEiLCJuYW1laWQiOiI2OWJmOTg5Ni0wYTIyLTQ1N2UtODkyMy00ZTM4MGQzMTEyNTkiLCJuYmYiOjE2NjcyOTA5NTgsImV4cCI6MTY2NzM3NzM1OCwiaWF0IjoxNjY3MjkwOTU4LCJpc3MiOiJodHRwczovL2p3dGF1dGguZXhhbXBsZS5jb20i
LCJhdWQiOiJhcGkxIn0.aUxhJuOrNOHeId6vHpHe1ZqnuC2MJ4TaYi577Cc37oU
The logs from trying to access the secretAction sending the jwt as a bearer token.
System.Net.Http.HttpRequestException: Response status code does not indicate success: 401 (Unauthorized).
at System.Net.Http.HttpResponseMessage.EnsureSuccessStatusCode()
at System.Net.Http.HttpClient.GetStringAsyncCore(HttpRequestMessage request, CancellationToken cancellationToken)
at Program.$(String[] args) in C:\Development\FreeLance\Glassix\asp-net-core-auth-with-self-generated-jwt\ConsoleApp1\Program.cs:line 27
So with some hints from friends on twitter. and Khellang I added event logging to the error
opt.Events = new JwtBearerEvents()
{
OnAuthenticationFailed = context =>
{
var err = context.Exception.ToString();
return context.Response.WriteAsync(err);
}
};
This lead to the error message
Method not found: 'Void Microsoft.IdentityModel.Tokens.InternalValidators.ValidateLifetimeAndIssuerAfterSignatureNotValidatedJwt
Followed by this question on Stack Unauthorized (Invalid Token) when authenticating with JWT Bearer Token after update to .NET 6
After installing the recommended package System.IdentityModel.Tokens.Jwt everything magically works.

Adding Bearer Token to ASP.NET Web API that has Basic Authentication Attribute

I have an ASP.Net Web API 2 with BasicAuthenticationAttribute that is working as expected. In my application, there are different controllers and I want to add bearer token-based authentication to one of my controllers. I added those NuGet packages:
Microsoft.AspNet.WebApi.Owin
Microsoft.Owin.Host.SystemWeb
Microsoft.Owin.Security.OAuth
Here is the WebApiConfig:
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
config.Formatters.JsonFormatter.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore;
config.Formatters.JsonFormatter.SerializerSettings.DateTimeZoneHandling =
DateTimeZoneHandling.Local;
config.MapHttpAttributeRoutes();
config.Routes.MapHttpRoute(
"DefaultApi",
"api/{controller}/{id}",
new {id = RouteParameter.Optional}
);
config.MessageHandlers.Add(new RequestResponseHandler());
config.Filters.Add(new CustomExceptionFilter());
var resolver = config.DependencyResolver; //Assuming one is set.
var basicAuth = (BasicAuthenticationAttribute)resolver.GetService(typeof(BasicAuthenticationAttribute));
// Web API configuration and services
if (basicAuth != null) config.Filters.Add(basicAuth);
}
}
Here is the Owin Startup
public class Startup
{
public void Configuration(IAppBuilder app)
{
var configuration = GlobalConfiguration.Configuration;
WebApiConfig.Register(configuration);
app.UseWebApi(configuration);
Configure(app);
}
private static void Configure(IAppBuilder app)
{
var options = new OAuthAuthorizationServerOptions()
{
TokenEndpointPath = new Microsoft.Owin.PathString("/token"),
AccessTokenExpireTimeSpan = TimeSpan.FromHours(1),
AllowInsecureHttp = true,
Provider = new AuthorizationServerProvider()
};
app.UseOAuthAuthorizationServer(options);
app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions());
}
}
Here is the controller
[RoutePrefix("api/v2/game/abc101")]
public class A101Controller : ApiController
{
private readonly IGameServicesABC101 _gameServices;
private readonly IMapper _mapper;
public A101Controller(IGameServicesABC101 gameServices, IMapper mapper)
{
_gameServices = gameServices;
_mapper = mapper;
}
[HttpPost]
[Authorize]
[Route("purchase")]
public async Task<IHttpActionResult> PurchaseGame(RequestDto game)
{
if (!ModelState.IsValid) return BadRequest(ModelState);
...
Basic Authentication Attribute
public class BasicAuthenticationAttribute : AuthorizationFilterAttribute
{
private const string Realm = "My Realm";
private readonly Func<IUserValidate> _factory;
public BasicAuthenticationAttribute(Func<IUserValidate> factory)
{
_factory = factory;
}
public override void OnAuthorization(HttpActionContext actionContext)
{
if (actionContext.Request.Headers.Authorization == null)
{
actionContext.Response = actionContext.Request
.CreateResponse(HttpStatusCode.Unauthorized);
if (actionContext.Response.StatusCode == HttpStatusCode.Unauthorized)
actionContext.Response.Headers.Add("WWW-Authenticate",
$"Basic realm=\"{Realm}\"");
}
else
{
var authenticationToken = actionContext.Request.Headers
.Authorization.Parameter;
try
{
//Decode the string
var decodedAuthenticationToken = Encoding.UTF8.GetString(
Convert.FromBase64String(authenticationToken));
var usernamePasswordArray = decodedAuthenticationToken.Split(':');
var username = usernamePasswordArray[0];
var password = usernamePasswordArray[1];
var uv = _factory();
if (uv.Login(username, password))
{
var identity = new GenericIdentity(username);
IPrincipal principal = new GenericPrincipal(identity, null);
Thread.CurrentPrincipal = principal;
if (HttpContext.Current != null) HttpContext.Current.User = principal;
}
else
{
actionContext.Response = actionContext.Request
.CreateResponse(HttpStatusCode.Unauthorized);
}
}
catch
{
actionContext.Response = actionContext.Request
.CreateResponse(HttpStatusCode.Unauthorized);
}
}
}
}
I am using Unity in my application. Basic Authentication works as expected. When I make a request without a token to ...api/v2/game/abc101/purchase I get a response either. Shouldn't I get 401? What I am missing?
UPDATE
I am searching and trying to find how to use both basic authentication and token-based authentication for different controllers. Here is my status update.
There is no code in the Global.asax
Here is the Owin Startup
public class Startup
{
public void Configuration(IAppBuilder app)
{
var config = GlobalConfiguration.Configuration;
WebApiConfig.Register(config);
app.UseWebApi(config);
Configure(app, config.DependencyResolver);
config.EnsureInitialized();
}
private static void Configure(IAppBuilder app, IDependencyResolver resolver)
{
var options = new OAuthAuthorizationServerOptions()
{
TokenEndpointPath = new Microsoft.Owin.PathString("/token"),
AccessTokenExpireTimeSpan = TimeSpan.FromHours(1),
AllowInsecureHttp = true,
Provider = new AuthorizationServerProvider((IUserValidate)resolver.GetService(typeof(IUserValidate)))
};
app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions());
app.UseOAuthAuthorizationServer(options);
}
}
Here is AuthorizationServerProvider
public class AuthorizationServerProvider : OAuthAuthorizationServerProvider
{
private readonly IUserValidate _userValidate;
public AuthorizationServerProvider(IUserValidate userValidate)
{
_userValidate = userValidate;
}
public override async Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context)
{
if (!context.TryGetBasicCredentials(out var clientId, out var clientSecret))
{
context.SetError("Error", "Error...");
}
if (_userValidate.Login(clientId, clientSecret))
{
context.Validated();
}
}
public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
{
context.OwinContext.Response.Headers.Add("Access-Control-Allow-Origin", new[] { "*" });
context.OwinContext.Response.Headers.Add("Access-Control-Allow-Headers", new[] { "Content-Type" });
if (_userValidate.Login(context.UserName, context.Password))
{
var identity = new ClaimsIdentity(context.Options.AuthenticationType);
identity.AddClaim(new Claim("sub", context.UserName));
identity.AddClaim(new Claim("role", "admin"));
context.Validated(identity);
}
else
{
context.SetError("Error", "Error...");
}
}
}
The rest is the same as the previous code samples.
When I call ...api/v2/game/abc101/purchase I am getting 401, it is progress. But when I call http://localhost:52908/token I am getting unsupported_grant_type. I am sending requests via Postman and I am sending a POST requests with content-type x-www-form-urlencoded. Grant-Type is password and username/password is also correct.
When I call another controller http://localhost:52908/api/v2/game/purchase basic authentication does NOT work!
Hope someone can help.
UPDATE 1
Now I am getting the token, one step at a time :) How can I also use Basic authentication for another controller?
Here is Startup
public class Startup
{
public void Configuration(IAppBuilder app)
{
var config = GlobalConfiguration.Configuration;
Configure(app, config.DependencyResolver);
WebApiConfig.Register(config);
app.UseCors(Microsoft.Owin.Cors.CorsOptions.AllowAll);
app.UseWebApi(config);
config.EnsureInitialized();
}
private static void Configure(IAppBuilder app, IDependencyResolver resolver)
{
var options = new OAuthAuthorizationServerOptions()
{
AllowInsecureHttp = true,
TokenEndpointPath = new Microsoft.Owin.PathString("/token"),
AccessTokenExpireTimeSpan = TimeSpan.FromHours(1),
Provider = new AuthorizationServerProvider((IUserValidate)resolver.GetService(typeof(IUserValidate)))
};
app.UseOAuthAuthorizationServer(options);
app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions());
}
}
Here is the Authorization Server Provider
public class AuthorizationServerProvider : OAuthAuthorizationServerProvider
{
private readonly IUserValidate _userValidate;
public AuthorizationServerProvider(IUserValidate userValidate)
{
_userValidate = userValidate;
}
public override async Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context)
{
if (!context.TryGetBasicCredentials(out var clientId, out var clientSecret))
{
context.SetError("invalid_grant", "The user name or password is incorrect.");
}
if (_userValidate.Login(clientId, clientSecret))
{
context.Validated();
}
}
public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
{
if (_userValidate.Login(context.UserName, context.Password))
{
var identity = new ClaimsIdentity(context.Options.AuthenticationType);
identity.AddClaim(new Claim("sub", context.UserName));
identity.AddClaim(new Claim("role", "admin"));
context.Validated(identity);
}
else
{
context.SetError("invalid_grant", "The user name or password is incorrect.");
}
}
}
As I mentioned before, I have Basic Authentication Attribute and somehow I have to use it in my other controller.
UPDATE 2
How can I use OverrideAuthentication and my basic authentication attribute?
public class BasicAuthenticationAttribute : AuthorizationFilterAttribute
{
private const string Realm = "My Realm";
private readonly Func<IUserValidate> _factory;
public BasicAuthenticationAttribute(Func<IUserValidate> factory)
{
_factory = factory;
}
...
UPDATE 3
I tried this in my basic authentication attribute OnAuthorization method;
var authentication = DependencyResolver.Current.GetService<IUserValidate>();
if (authentication.Login(username, password))
{
var identity = new GenericIdentity(username);
IPrincipal principal = new GenericPrincipal(identity, null);
Thread.CurrentPrincipal = principal;
if (HttpContext.Current != null) HttpContext.Current.User = principal;
}
There are 2 problems, authentication is null, and somehow authentication token in the attribute is the bearer authentication username/password even though I use basic authentication username/password in the request. It's very weird!
/Get the authentication token from the request header
var authenticationToken = actionContext.Request.Headers
.Authorization.Parameter;
Any help please?
Thanks in advance.
After long googling, here is how I managed to use both basic authentication and bearer authentication for my different controllers.
In Custom basic authentication Attribute I used dependency and requestScope.GetService.
public class CustomBasicAuthenticationAttribute : AuthorizationFilterAttribute
{
[Dependency] public static IUserValidate authentication { get; set; }
private const string Realm = "My Realm";
public override void OnAuthorization(HttpActionContext actionContext)
{
var requestScope = actionContext.Request.GetDependencyScope();
//If the Authorization header is empty or null
//then return Unauthorized
if (actionContext.Request.Headers.Authorization == null)
{
actionContext.Response = actionContext.Request
.CreateResponse(HttpStatusCode.Unauthorized);
// If the request was unauthorized, add the WWW-Authenticate header
// to the response which indicates that it require basic authentication
if (actionContext.Response.StatusCode == HttpStatusCode.Unauthorized)
actionContext.Response.Headers.Add("WWW-Authenticate",
$"Basic realm=\"{Realm}\"");
}
else
{
//Get the authentication token from the request header
var authenticationToken = actionContext.Request.Headers
.Authorization.Parameter;
try
{
//Decode the string
var decodedAuthenticationToken = Encoding.UTF8.GetString(
Convert.FromBase64String(authenticationToken));
//Convert the string into an string array
var usernamePasswordArray = decodedAuthenticationToken.Split(':');
//First element of the array is the username
var username = usernamePasswordArray[0];
//Second element of the array is the password
var password = usernamePasswordArray[1];
authentication = requestScope.GetService(typeof(IUserValidate)) as IUserValidate;
if (authentication != null && authentication.Login(username, password))
{
var identity = new GenericIdentity(username);
IPrincipal principal = new GenericPrincipal(identity, null);
Thread.CurrentPrincipal = principal;
if (HttpContext.Current != null) HttpContext.Current.User = principal;
}
else
{
actionContext.Response = actionContext.Request
.CreateResponse(HttpStatusCode.Unauthorized);
}
}
catch
{
actionContext.Response = actionContext.Request
.CreateResponse(HttpStatusCode.Unauthorized);
}
}
}
}
In one of my controller, I added those attributes
[OverrideAuthentication]
[CustomBasicAuthentication]
[HttpPost, Route("purchase")]
public async Task<IHttpActionResult> PurchaseGame(RequestDto game)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
...
Now I can use bearer token authentication for ...api/v2/game/abc101/purchase and basic authentication for ...api/v2/game/purchase.
Update
The vital part is the dependency and actionContext.Request.GetDependencyScope();. Without OverrideAuthentication it is working as expected.
Hope this solution helps for others.

I get cookie instead of token with authorization code grant

Summary
I have ASP.NET MVC 5 web app with Identity authentication and I have to develop an API with "grant_type" = "authorization_code". This API will be to provide users data to another "well-known" web service that needs a custom error responses. My IDE is Visual Studio Professional 2017. I use Postman to make requests to my Web API.
Documentation I read
In the OWIN and Katana documentation the OWIN OAuth 2.0 Authorization Server link redirects again to main OWIN and Katana page, but I think that I found the source on GitHub: OWIN OAuth 2.0 Authorization Server. I tried to follow this documentation, but there are no examples about this question.
Problem
I can create a new authorization code in my AuthorizationCodeProvider class (with Create() method) when a user authenticates and authorizes the "well-known" web service client to access user's resources. I store this code in a database. When I request a Token AuthorizationCodeProvider.Receive() method is called and the token is deserialized correctly. Then GrantAuthorizationCode() method is called, Postman receives OK response (200 status code) but without token information in body (.AspNet.ApplicationCookie is in cookies).
Detailed explanation and code
This is the Startup class:
public partial class Startup
{
public static OAuthAuthorizationServerOptions OAuthOptions { get; private set; }
public void ConfigureAuth(IAppBuilder app)
{
app.CreatePerOwinContext(ApplicationDbContext.Create);
app.CreatePerOwinContext<ApplicationUserManager>(ApplicationUserManager.Create);
app.CreatePerOwinContext<ApplicationSignInManager>(ApplicationSignInManager.Create);
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
LoginPath = new PathString("/Account/Login"),
Provider = new CookieAuthenticationProvider
{
OnValidateIdentity = SecurityStampValidator.OnValidateIdentity<ApplicationUserManager, ApplicationUser>(
validateInterval: TimeSpan.FromMinutes(30),
regenerateIdentity: (manager, user) => user.GenerateUserIdentityAsync(manager)),
OnApplyRedirect = (context =>
{
// This code is to return custom error response
string path = null;
if (context.Request.Path.HasValue)
path = context.Request.Path.Value;
if (!(path != null && path.Contains("/api"))) // Don't redirect to login page
context.Response.Redirect(context.RedirectUri);
})
}
});
app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);
app.UseTwoFactorSignInCookie(DefaultAuthenticationTypes.TwoFactorCookie, TimeSpan.FromMinutes(5));
app.UseTwoFactorRememberBrowserCookie(DefaultAuthenticationTypes.TwoFactorRememberBrowserCookie);
this.ConfigureAuthorization(app);
}
private void ConfigureAuthorization(IAppBuilder app)
{
app.UseCors(Microsoft.Owin.Cors.CorsOptions.AllowAll);
OAuthOptions = new OAuthAuthorizationServerOptions
{
AllowInsecureHttp = false,
TokenEndpointPath = new PathString("/api/token"),
AccessTokenExpireTimeSpan = TimeSpan.FromDays(1),
Provider = new TokenAuthorizationServerProvider(),
AuthorizationCodeProvider = new AuthorizationCodeProvider()
};
app.Use<AuthenticationMiddleware>(); //Customize responses in Token middleware
app.UseOAuthAuthorizationServer(OAuthOptions);
app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions());
}
}
ConfigureAuthorization() method configures the authorization. It uses classes implemented by me:
AuthenticationMiddleware: the well-known web service wants 401 status responses with custom error JONS instead of the usual 400 status response. It is based on the answer of the question Replace response body using owin middleware.
public class AuthenticationMiddleware : OwinMiddleware
{
public AuthenticationMiddleware(OwinMiddleware next) : base(next) { }
public override async Task Invoke(IOwinContext context)
{
var owinResponse = context.Response;
var owinResponseStream = owinResponse.Body;
var responseBuffer = new MemoryStream();
owinResponse.Body = responseBuffer;
await Next.Invoke(context);
if (context.Response.StatusCode == (int)HttpStatusCode.BadRequest &&
context.Response.Headers.ContainsKey(BearerConstants.CustomUnauthorizedHeaderKey))
{
context.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
string headerValue = context.Response.Headers.Get(BearerConstants.CustomUnauthorizedHeaderKey);
context.Response.Headers.Remove(BearerConstants.CustomUnauthorizedHeaderKey);
ErrorMessage errorMessage = new ErrorMessage(headerValue);
string json = JsonConvert.SerializeObject(errorMessage, Formatting.Indented);
var customResponseBody = new StringContent(json);
var customResponseStream = await customResponseBody.ReadAsStreamAsync();
await customResponseStream.CopyToAsync(owinResponseStream);
owinResponse.ContentType = "application/json";
owinResponse.ContentLength = customResponseStream.Length;
owinResponse.Body = owinResponseStream;
}
}
}
When ErrorMessage is serialized to JSON returns an array of errors:
{
"errors":
[
"message": "the error message"
]
}
I set the BearerConstants.CustomUnauthorizedHeaderKey header in TokenAuthorizationServerProvider.ValidateClientAuthentication() method using a extension method:
public static void Rejected(this OAuthValidateClientAuthenticationContext context, string message)
{
Debug.WriteLine($"\t\t{message}");
context.SetError(message);
context.Response.Headers.Add(BearerConstants.CustomUnauthorizedHeaderKey, new string[] { message });
context.Rejected();
}
This is how TokenAuthorizationServerProvider is implemented:
public class TokenAuthorizationServerProvider : OAuthAuthorizationServerProvider
{
public override Task AuthorizeEndpoint(OAuthAuthorizeEndpointContext context)
{
// Only for breakpoint. Never stops.
return base.AuthorizeEndpoint(context);
}
public override async Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context)
{
// Check if grant_type is authorization_code
string grantType = context.Parameters[BearerConstants.GrantTypeKey];
if (string.IsNullOrEmpty(grantType) || grantType != BearerConstants.GrantTypeAuthorizationCode)
{
context.Rejected("Invalid grant type"); // Sets header for custom response
return;
}
// Check if client_id and client_secret are in the request
string clientId = context.Parameters[BearerConstants.ClientIdKey];
string clientSecret = context.Parameters[BearerConstants.ClientSecretKey];
if (string.IsNullOrEmpty(clientId) || string.IsNullOrEmpty(clientSecret))
{
context.Rejected("Client credentials missing"); // Sets header for custom response
return;
}
//Check if client_id and client_secret are valid
ApiClient client = await (new ApiClientService()).ValidateClient(clientId, clientSecret);
if (client != null)
{
// Client has been verified.
Debug.WriteLine($"\t\tClient has been verified");
context.OwinContext.Set<ApiClient>("oauth:client", client);
context.Validated(clientId);
}
else
{
// Client could not be validated.
context.Rejected("Invalid client"); // Sets header for custom response
}
}
public override async Task GrantAuthorizationCode(OAuthGrantAuthorizationCodeContext context)
{
TokenRequestParameters parameters = await context.Request.GetBodyParameters();
using (IUserService userService = new UserService())
{
ApplicationUser user = await userService.ValidateUser(parameters.Code);
if (user == null)
{
context.Rejected("Invalid code");
return;
}
// Initialization.
var claims = new List<Claim>();
// Setting
claims.Add(new Claim(ClaimTypes.Name, user.UserName));
// Setting Claim Identities for OAUTH 2 protocol.
ClaimsIdentity oAuthClaimIdentity = new ClaimsIdentity(claims, OAuthDefaults.AuthenticationType);
ClaimsIdentity cookiesClaimIdentity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationType);
// Setting user authentication.
IDictionary<string, string> data = new Dictionary<string, string>{ { "userName", user.UserName } };
AuthenticationProperties properties = new AuthenticationProperties(data);
AuthenticationTicket ticket = new AuthenticationTicket(oAuthClaimIdentity, properties);
// Grant access to authorize user.
context.Validated(ticket);
context.Request.Context.Authentication.SignIn(cookiesClaimIdentity);
}
}
}
ApiClientService.ValidateClient() checks on database that cliend ID and Secret are correct.
GrantAuthorizationCode() is based on the step 8 from ASP.NET MVC - OAuth 2.0 REST Web API Authorization Using Database First Approach tutorial. But this tutorial for grant_type = password and I think that something is wrong in here.
And the AuthorizationCodeProvider class:
public class AuthorizationCodeProvider : AuthenticationTokenProvider
{
public override void Create(AuthenticationTokenCreateContext context)
{
AuthenticationTicket ticket = context.Ticket;
string serializedTicket = context.SerializeTicket();
context.SetToken(serializedTicket);
}
public override void Receive(AuthenticationTokenReceiveContext context)
{
context.DeserializeTicket(context.Token);
// At this point context.Ticket.Identity.IsAuthenticated is true
}
}
I call to create method from the AuthorizationController that shows the Allow/Deny view. It is decorated with System.Web.Mvc.Authorize attribute, so if the user isn't authenticated he or she has to login using the default login page from MVC template project (/account/login):
[Authorize]
public class AuthorizationController : Controller
{
private const string ServiceScope = "service-name";
[HttpGet]
public async Task<ActionResult> Index(string client_id, string response_type, string redirect_uri, string scope, string state)
{
AuthorizationViewModel vm = new AuthorizationViewModel()
{
ClientId = client_id,
RedirectUri = redirect_uri,
Scope = scope,
State = state
};
if (scope == ServiceScope)
{
var authentication = HttpContext.GetOwinContext().Authentication;
authentication.SignIn(
new AuthenticationProperties { IsPersistent = true, RedirectUri = redirect_uri },
new ClaimsIdentity(new[] { new Claim(ClaimsIdentity.DefaultNameClaimType, User.Identity.Name) },
"Bearer"));
}
return View(vm);
}
[HttpPost]
[ValidateAntiForgeryToken]
[MultiButton(MatchFormKey = "authorization", MatchFormValue = "Allow")]
public async Task<ActionResult> Allow(AuthorizationViewModel vm)
{
if (ModelState.IsValid)
{
string code = await this.SetAuthorizationCode(vm.ClientId, vm.RedirectUri);
if (vm.Scope == ServiceScope)
{
string url = $"{vm.RedirectUri}?code={code}&state={vm.State}";
return Redirect(url);
}
else
{
return Redirect(vm.RedirectUri);
}
}
return View(vm);
}
[HttpPost]
[ValidateAntiForgeryToken]
[MultiButton(MatchFormKey = "authorization", MatchFormValue = "Deny")]
public async Task<ActionResult> Deny(AuthorizationViewModel vm)
{
// Removed for brevity
return View(vm);
}
private async Task<string> SetAuthorizationCode(string clientId, string redirectUri)
{
string userId = User.Identity.GetUserId();
ClaimsIdentity identity = new ClaimsIdentity(new GenericIdentity(clientId, OAuthDefaults.AuthenticationType));
AuthenticationTokenCreateContext authorizeCodeContext = new AuthenticationTokenCreateContext(
HttpContext.GetOwinContext(),
Startup.OAuthOptions.AuthorizationCodeFormat,
new AuthenticationTicket(
identity,
new AuthenticationProperties(new Dictionary<string, string>
{
{ "user_id", userId },
{ "client_id", clientId },
{ "redirect_uri", redirectUri }
})
{
IssuedUtc = DateTimeOffset.UtcNow,
ExpiresUtc = DateTimeOffset.UtcNow.Add(Startup.OAuthOptions.AuthorizationCodeExpireTimeSpan)
}));
Startup.OAuthOptions.AuthorizationCodeProvider.Create(authorizeCodeContext);
string code = authorizeCodeContext.Token;
IUserService userService = new UserService();
await userService.SetAuthorization(userId, true, code); // save to database
userService.Dispose();
return code;
}
}
The authorization code is created in SetAuthorizationCode() method, which is called in Allow() action. This SetAuthorizationCode() method code is based on this answer.
Questions
I now that is very long with a lot of code, but I'm stuck for some days and I didn't find the solution. I don't know the complete flow of the authorization, I think that I'm missing something.
What happens when I call /api/token? I mean, what are the steps in this part of the authentication/authorization flow?
What happens after AuthorizationCodeProvider.GrantAuthorizationCode()?
Why a cookie returned instead of token in the body?
I found the solution of the problem, it was the AuthenticationMiddleware. Once the body of the response is read, it remains empty and does not reach the client. So you have to rewrite the response body.
public class AuthenticationMiddleware : OwinMiddleware
{
public AuthenticationMiddleware(OwinMiddleware next) : base(next) { }
public override async Task Invoke(IOwinContext context)
{
var owinResponse = context.Response;
var owinResponseStream = owinResponse.Body;
var responseBuffer = new MemoryStream();
owinResponse.Body = responseBuffer;
await Next.Invoke(context);
if (context.Response.StatusCode == (int)HttpStatusCode.BadRequest &&
context.Response.Headers.ContainsKey(BearerConstants.CustomUnauthorizedHeaderKey))
{
// Customize the response
}
else
{
// Set body again with the same content
string body = Encoding.UTF8.GetString(responseBuffer.ToArray());
StringContent customResponseBody = new StringContent(body);
Stream customResponseStream = await customResponseBody.ReadAsStreamAsync();
await customResponseStream.CopyToAsync(owinResponseStream);
}
}
}

WebApi 2 always returning 401 on authorization using custom JWT token

I have followed this tutorial Create a RESTful API with authentication using Web API and Jwt
I managed to get the authentication part working but the authorization part is not working(towards end of tutorial). If I add the jwt token with the word bearer in the authorization header it gives me 401 authorization denied.
I'm thinking maybe I need to create a custom authorization attribute.
Is there any way to use the existing authorize attribute?
What does the existing Authorize attribute look for in order to authorize a user(not including roles or users parameters in the authorize attribute) ?
Startup.Auth.cs
public partial class Startup
{
public static OAuthAuthorizationServerOptions OAuthOptions { get; private set; }
public static string PublicClientId { get; private set; }
// For more information on configuring authentication, please visit https://go.microsoft.com/fwlink/?LinkId=301864
public void ConfigureAuth(IAppBuilder app)
{
var issuer = ConfigurationManager.AppSettings["Issuer"];
var secret = TextEncodings.Base64Url.Decode(ConfigurationManager.AppSettings["Secret"]);
// Configure the db context and user manager to use a single instance per request
app.CreatePerOwinContext(ApplicationDbContext.Create);
app.CreatePerOwinContext<ApplicationUserManager>(ApplicationUserManager.Create);
app.UseJwtBearerAuthentication(new JwtBearerAuthenticationOptions
{
AuthenticationMode = AuthenticationMode.Active,
AllowedAudiences = new[] { "Any" },
IssuerSecurityTokenProviders = new IIssuerSecurityTokenProvider[]
{
new SymmetricKeyIssuerSecurityTokenProvider(issuer, secret)
}
});
app.UseOAuthAuthorizationServer(new OAuthAuthorizationServerOptions
{
AllowInsecureHttp = true,
TokenEndpointPath = new PathString("/Token"),
AccessTokenExpireTimeSpan = TimeSpan.FromDays(1),
Provider = new CustomOAuthProvider(),
AccessTokenFormat = new CustomJwtFormat(issuer)
});
}
}
CustomOAuthProvider
public class CustomOAuthProvider : OAuthAuthorizationServerProvider
{
public override Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
{
context.OwinContext.Response.Headers.Add("Access-Control-Allow-Origin", new[] { "*" });
var blSecurity = new BLSecurity();
var user = blSecurity.LogonUser(context.UserName, context.Password);
if (!(user.ResponseType == Global.Response.ResponseTypes.Success))
{
context.SetError("Authentication Error", "The user name or password is incorrect");
return Task.FromResult<object>(null);
}
var ticket = new AuthenticationTicket(SetClaimsIdentity(context, user.LoggedOnUser), new AuthenticationProperties());
context.Validated(ticket);
return Task.FromResult<object>(null);
}
public override Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context)
{
context.Validated();
return Task.FromResult<object>(null);
}
private static ClaimsIdentity SetClaimsIdentity(OAuthGrantResourceOwnerCredentialsContext context, User user)
{
//Add User Claims
var identity = new ClaimsIdentity("JWT");
identity.AddClaim(new Claim(ClaimTypes.Name, context.UserName));
identity.AddClaim(new Claim("sub", context.UserName));
identity.AddClaim(new Claim("bn", user.BranchName));
identity.AddClaim(new Claim("fn", user.FirstName));
identity.AddClaim(new Claim("ln", user.LastName));
//Add User Role Claims
var blRole = new BLRole();
var roles = blRole.GetRolesByUserId(user.UserID);
if (roles != null && roles.Count > 0)
{
foreach (var role in roles)
{
identity.AddClaim(new Claim(ClaimTypes.Role, role.RoleName));
}
}
return identity;
}
}
CustomJwtFormat
public class CustomJwtFormat : ISecureDataFormat<AuthenticationTicket>
{
private static readonly byte[] _secret = TextEncodings.Base64Url.Decode(ConfigurationManager.AppSettings["secret"]);
private readonly string _issuer;
public CustomJwtFormat(string issuer)
{
_issuer = issuer;
}
public string Protect(AuthenticationTicket data)
{
if (data == null)
{
throw new ArgumentNullException(nameof(data));
}
var signingKey = new HmacSigningCredentials(_secret);
var issued = data.Properties.IssuedUtc;
var expires = data.Properties.ExpiresUtc;
return new JwtSecurityTokenHandler().WriteToken(new JwtSecurityToken(_issuer, null, data.Identity.Claims, issued.Value.UtcDateTime.ToLocalTime(), expires.Value.UtcDateTime.ToLocalTime(), signingKey));
}
public AuthenticationTicket Unprotect(string protectedText)
{
throw new NotImplementedException();
}
}
I managed to solve it by applying what a guy posted in the comments section. see below quoted.
Roman Shramko
June 23, 2016
There’s kinda a bug in the code above. The JwtBearerAuthenticationOptions is configured with
AllowedAudiences = new[] { “Any” },
but in fact, the token content does not contain any audience, so that your request gets rejected.
The fastest way to fix that (and not the best one), is to change the way you create a token in the Protect method of the CustomJwtFormat class from
new JwtSecurityToken(_issuer, null, data.Identity.Claims, issued.Value.UtcDateTime, expires.Value.UtcDateTime, signingKey);
to this one
new JwtSecurityToken(_issuer, “Any”, data.Identity.Claims, issued.Value.UtcDateTime, expires.Value.UtcDateTime, signingKey);
i.e. to pass “Any” instead of null as a second constructor parameter.

Categories

Resources