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.
Related
I hope someone could clear up a couple of concepts for me.
I use WinForms and Blazor Server and I desire to send a message to a user.
For that purpose, I've decided to try JWT authentication.
I would also prefer the Blazor server have this JWT authentication built in as per documentation
Blazor server:
var builder = WebApplication.CreateBuilder(args);
var services = builder.Services;
// Add services to the container.
services.AddCors(option => option.AddPolicy("CorsPolicy", p => p.AllowAnyHeader().AllowAnyOrigin().AllowAnyMethod().AllowCredentials()));
//services.AddScoped<AuthenticationStateProvider, MyAuthenticationStateProvider>();
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters() //Guessing this section is for security of the token - ensures that I'm the one that made it and such.
{
ValidateAudience = false,
ValidateIssuer = false,
ValidateLifetime = false,
IssuerSigningKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes("test")),
ValidIssuer = "test",
ValidAudience = "test"
};
options.Events = new()
{
OnMessageReceived = (context) =>
{
var path = context.HttpContext.Request.Path;
if (path.StartsWithSegments("/hubs")) // || path.StartsWithSegments("/token")
{
var accessToken = string.IsNullOrWhiteSpace(context.Request.Query["access_token"]) ? context.Request.Headers["Authorization"] : context.Request.Query["access_token"];
if (!string.IsNullOrWhiteSpace(accessToken))
{
//Real
context.Token = accessToken; //another guess - this is adding the accesstoken to the httpContext so it can be used somewhere else probably.
//Test attach claims to context. I want to be able to do this somewhere else though.
var claims = new Claim[]
{
new(ClaimTypes.Name, "myUserName"),
};
var identity = new ClaimsIdentity(claims);
context.Principal = new(identity);
context.Success();
}
}
return Task.CompletedTask;
},
OnAuthenticationFailed = (context) =>
{
Debug.WriteLine("OnAuthenticationFailed: " + context.Exception.Message);
return Task.CompletedTask;
}
};
});
services.AddRazorPages();
services.AddServerSideBlazor();
services.AddSignalR()
.AddHubOptions<ChatHub>(options => options.EnableDetailedErrors = true);
var app = builder.Build();
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.MapBlazorHub();
app.MapHub<ChatHub>("/hubs/chathub");
app.MapFallbackToPage("/_Host");
Hub:
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] //Same as: [Authorize(AuthenticationSchemes = "Bearer")]
public class ChatHub : Hub
{
public Task SendMessageAsync(string user, string message)
{
Debug.WriteLine(Context.UserIdentifier); //null
bool test1 = Context.User.Identity.IsAuthenticated; //false
string test2 = Context?.User?.Identity?.Name; //myUserName
return Clients.User(Context?.User?.Identity?.Name).SendAsync("ReceiveMessage", user, message); //This does then not work ofc.
}
}
Client:
HubConnection connection;
string url = "https://localhost:7041/hubs/chathub";
string token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c";
public Form1()
{
InitializeComponent();
connection = new HubConnectionBuilder()
.WithUrl(url, options =>
{
options.AccessTokenProvider = () => Task.FromResult(token);
})
.WithAutomaticReconnect()
.Build();
}
private async void HubConnectBtn_Click(object sender, EventArgs e)
{
connection.On<string, string>("ReceiveMessage", (user, message) =>
{
this.Invoke(() =>
{
var newMessage = $"{user}: {message}";
MessagesLB.Items.Add(newMessage);
});
});
try
{
await connection.StartAsync();
MessagesLB.Items.Add("Connected!");
}
catch(Exception ex)
{
MessagesLB.Items.Add(ex.Message);
}
}
What I do not understand:
When connecting the Client, how do I pass username and password from the winform to the Blazor server, and how to I get access to the middleware authentication and return a JTW back to the client connection. Do I need to make an API on the Blazor Server, do a call and generate a JWT which I pass here: options.AccessTokenProvider = () => Task.FromResult(token); or is there a more logical way?
I've looked into the AuthenticationStateProvider, but couldn't produce the results I wanted.
Is JWT authentication even intended for this, or is there a better alternative?
I'm using an already existing database filled with users that I'm going to access when this server is looking up if the username and password is correct. Meaning I want a "userService" or something like that which contains all users. For now it's fine to mock users, but I need to know where I can do this and swap it out with a DBconnection/context later on.
Any help would be most appreciated!
EDIT:
After some time I found a approach that did work as I wanted it. By using a custom Authentication scheme I could add a token containing user data from and to user/group which means I have complete control over the communication flow. A great benefit of this is that you never need to have a user database to check the authenticity of the user it self, I solved this by having baked into the token a salt/token value that only me and whom ever other person would integrate with the signalR relay server knows about. The Token then contains licensenumber, department no, user info etc to make it unique when registering.
SERVER SIDE:
public class MyAuthenticationHandler : AuthenticationHandler<MyCustomAuthenticationSchemeOptions>
{
public MyAuthenticationHandler(IOptionsMonitor<MyCustomAuthenticationSchemeOptions> options,
ILoggerFactory logger,
UrlEncoder encoder,
ISystemClock clock) : base(options, logger, encoder, clock)
{
}
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
TokenModel tokenModel;
if (!Request.Headers.ContainsKey(HeaderNames.Authorization))
{
return Task.FromResult(AuthenticateResult.Fail("Header Not Found."));
}
AuthenticationHeaderValue auth;
if (!AuthenticationHeaderValue.TryParse(Request.Headers.Authorization, out auth))
{
return Task.FromResult(AuthenticateResult.Fail("No authentication header"));
}
if (!auth.Scheme.Equals("Bearer"))
{
return Task.FromResult(AuthenticateResult.Fail("Authentication Scheme was not Bearer"));
}
//var header = Request.Headers[HeaderNames.Authorization].ToString();
//var tokenMatch = Regex.Match(header, MyAuthenticationSchemeConstants.MyToken);
if (!string.IsNullOrWhiteSpace(auth.Parameter))
{
//string[] token = header.Split(" ");
try
{
string parsedToken = Encoding.UTF8.GetString(Convert.FromBase64String(auth.Parameter));
tokenModel = JsonConvert.DeserializeObject<TokenModel>(parsedToken);
}
catch(Exception ex)
{
Debug.WriteLine("Exception Occured while Deserializing: " + ex);
return Task.FromResult(AuthenticateResult.Fail("TokenParseException"));
}
if(tokenModel != null)
{
List<Claim> claims = new()
{
new Claim(ClaimTypes.Name, tokenModel.Name),
new Claim("Group", tokenModel?.GroupName)
};
if (tokenModel.UserId > 0)
{
claims.Add(new Claim(ClaimTypes.NameIdentifier, tokenModel.UserId.ToString()));
}
var claimsIdentity = new ClaimsIdentity(claims,
nameof(MyAuthenticationHandler));
var ticket = new AuthenticationTicket(
new ClaimsPrincipal(claimsIdentity), this.Scheme.Name);
// pass on the ticket to the middleware
return Task.FromResult(AuthenticateResult.Success(ticket));
}
}
return Task.FromResult(AuthenticateResult.Fail("Model is Empty"));
}
}
public class MyCustomAuthenticationSchemeOptions : AuthenticationSchemeOptions
{
}
I also use a UserSerivice (though not necessary) to relay some information like "user not connected", a work in progress. Just a addUser, getID, removeUser from a list of usermodel. This enables me to have the hub like this:
public class ChatHub : Hub
{
IUserService _userService;
public ChatHub(IUserService userService)
{
_userService = userService;
}
public async Task SendMessageAsync(int user, string message)
{
Debug.WriteLine(Context.UserIdentifier); //userID -> Claim.UserIdentifier
bool test1 = Context.User.Identity.IsAuthenticated; //true
string test2 = Context?.User?.Identity?.Name; //myUserName -> Claim.Name
if (_userService.GetById(user) == false)
await Clients.User(Context.UserIdentifier).SendAsync("ReceiveMessage", Context?.User?.Identity?.Name, $"user is not connected");
await Clients.User(user.ToString()).SendAsync("ReceiveMessage", Context?.User?.Identity?.Name, message);
}
public async Task SendMessageToGroupAsync(string groupname, string message)
{
await Clients.OthersInGroup(groupname).SendAsync("ReceiveMessage", groupname, message);
}
public async override Task OnConnectedAsync()
{
//TODO Register Context.UserIdentifier -
int conID = 0;
if(int.TryParse(Context.UserIdentifier, out conID) == true)
{
_userService.AddUser(conID, Context.ConnectionId);
}
await Groups.AddToGroupAsync(Context.ConnectionId, Context.User.Claims.First(c => c.Type == "Group").Value);
await base.OnConnectedAsync();
}
public override async Task OnDisconnectedAsync(Exception? exception)
{
int conID = 0;
if (int.TryParse(Context.ConnectionId, out conID) == true)
{
_userService.RemoveUser(conID, Context.ConnectionId); //TODO Should probably be async calls to service.
}
await base.OnDisconnectedAsync(exception); //According to https://github.com/dotnet/aspnetcore/issues/19043 users are removed from all groups by disconnecting. Test/resolve this.
}
}
Then in the program.cs you just add it like usual (the MyAuthenticationSchemeConstants are just the name and a random token value I've chosen for now) :
var builder = WebApplication.CreateBuilder(args);
var services = builder.Services;
// Add services to the container.
services.AddRazorPages();
services.AddServerSideBlazor();
services.AddAuthentication(options => options.DefaultAuthenticateScheme = MyAuthenticationSchemeConstants.MyAuthenticationSchemeName)
.AddScheme<MyCustomAuthenticationSchemeOptions, MyAuthenticationHandler>( MyAuthenticationSchemeConstants.MyAuthenticationSchemeName, options => { });
UserService _userService = new();
.
.
.
app.UseAuthentication();
app.UseAuthorization();
app.MapBlazorHub();
app.MapHub<ChatHub>("/hubs/chathub");
Client-Side example:
Just a winform with a couple of text fields and buttons using localhost for example purposes, but just swap out the url for a central hub url.
string url = "https://localhost:7185/hubs/chathub";
string token = "";
public Form1()
{
InitializeComponent();
SendBtn.Enabled = false;
//client = new(baseAddress);
}
private void BuildToken()
{
TokenModel tokenModel = new()
{
UserId = Int32.Parse(UserIDTB.Text),
Name = UsernameTB.Text,
GroupName = "Waiter",
EmailAddress = "someEmail#mydomain.com"
};
token = Convert.ToBase64String(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(tokenModel)));
}
private async void HubConnectBtn_Click(object sender, EventArgs e)
{
BuildToken();
connection = new HubConnectionBuilder()
.WithUrl(url, options =>
{
options.AccessTokenProvider = () => Task.FromResult(token);
})
.WithAutomaticReconnect()
.Build();
connection.On<string, string>("ReceiveMessage", (user, message) =>
{
this.Invoke(() =>
{
var newMessage = $"{user}: {message}";
MessagesLB.Items.Add(newMessage);
});
});
try
{
await connection.StartAsync();
SendBtn.Enabled = true;
HubConnectBtn.Enabled = false;
MessagesLB.Items.Add("Connected!");
}
catch (Exception ex)
{
MessagesLB.Items.Add(ex.Message);
}
}
private async void SendBtn_Click(object sender, EventArgs e)
{
if (string.IsNullOrWhiteSpace(RecieverTB.Text))
{
MessagesLB.Items.Add("Write a recipient");
return;
}
if (string.IsNullOrWhiteSpace(MessageTB.Text))
{
MessagesLB.Items.Add("Write a message");
return;
}
try
{
await connection.InvokeAsync("SendMessageAsync", Int32.Parse(RecieverTB.Text), MessageTB.Text);
}
catch (Exception ex)
{
MessagesLB.Items.Add(ex.Message);
}
}
I hope this can help someone else!
Side note - if anyone has an input on how I can pass the Task.FromResult text to my clients that would be great!
I use signalr and jwt and my startup is a little different:
.AddJwtBearer(options =>
{
options.SaveToken = true;
options.RequireHttpsMetadata = false;
options.TokenValidationParameters = new TokenValidationParameters()
{
ValidateIssuer = true,
ValidateAudience = true,
ValidAudience = Configuration["JWT: ValidAudience"],
ValidIssuer = Configuration["JWT: ValidIssuer"],
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["JWT: SecretKey"]))
};
options.Events = new JwtBearerEvents
{
OnMessageReceived = context =>
{
var accessToken = context.Request.Query["access_token"];
// If the request is for our hub...
var path = context.HttpContext.Request.Path;
if (!string.IsNullOrEmpty(accessToken) &&
(path.StartsWithSegments("/api/hubs/")))
{
// Read the token out of the query string
context.Token = accessToken;
}
return Task.CompletedTask;
}
};
});
As you see you just have to set context.Token. But the token that signalr sends is obtained from the api through the authentication process. As you said, you authenticate sending a post request to a route on your api which could be something like this:
[HttpPost("authenticate")]
public async Task<ActionResult<AuthenticateResponse>> Post([FromBody]
AuthenticateRequest request)
{
AuthenticateResponse response = new AuthenticateResponse();
var result = await signInManager.PasswordSignInAsync(request.User,
request.Password, true, false);
if (result.Succeeded)
{
var user = await userManager.FindByNameAsync(request.User);
string[] roles = (await userManager.GetRolesAsync(user)).ToArray();
response.UserId = user.Id;
response.UserName = user.UserName;
response.Roles = roles;
response.Token = Helpers.TokenService.CreateToken(user, roles,
configuration);
}
else response.Error = "Authentication Error";
return Ok(response);
}
Here is the TokenService:
public static class TokenService
{
private const double EXPIRE_HOURS = 10;
public static string CreateToken(ApplicationUser user, string[] roles,
IConfiguration configuration)
{
var authClaims = new List<Claim>
{
new Claim(ClaimTypes.NameIdentifier, user.Id)
};
foreach (var userRole in roles)
{
authClaims.Add(new Claim(ClaimTypes.Role, userRole));
}
var authSigningKey = new
SymmetricSecurityKey(Encoding.UTF8.GetBytes(configuration["JWT:
SecretKey"]));
var token = new JwtSecurityToken(
issuer: configuration["JWT: ValidIssuer"],
audience: configuration["JWT: ValidAudience"],
expires: DateTime.Now.AddHours(EXPIRE_HOURS),
claims: authClaims,
signingCredentials: new SigningCredentials(authSigningKey,
SecurityAlgorithms.HmacSha256)
);
return new JwtSecurityTokenHandler().WriteToken(token);
}
}
And you have to derive your ApplicationdbContext from IdentityDbContext
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);
}
}
}
I've posted this question before but being new I wanted to learn how to write a "proper" question (in case you've tried to help me and I didn't get back immediately, wanted to get it mostly right if not all hopefully I've got the hang of it)
This is just registration/login code whose purpose is self explanatory, the error am getting (could not load file or assembly 'microsoft aspnetcore razor runtime 3.1 1) happens in a service registration class in the AddControllers() method, I've tried adding the package specified in the error but it didn't work, I tried a few other similar packages but the result has been the same, when I build I get no errors or warnings but when the app runs I get the error, hope this is enough data to work with.
//controller class Login and registration
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
public class IdentifyMe : Controller
{
private readonly IIdentify _identify;
public IdentifyMe(IIdentify identifying)
{
_identify = identifying;
}
[HttpGet(Api_Routes.Identity.Register)]
public async Task<IActionResult> Register(UserRegistration register)
{
if (!ModelState.IsValid)
{
return BadRequest(new Unauthenticated
{
Errors=ModelState.Values.SelectMany(x=>x.Errors.Select(xx=>xx.ErrorMessage))
});
}
var authresponce = await _identify.RegisterAsync(register.Email, register.Password, register.User_Name);
if (!authresponce.Success)
{
return BadRequest(new Unauthenticated
{
Errors = authresponce.Errors
});
}
return Ok(new Authenticated
{
Token = authresponce.Token
});
}
[HttpGet(Api_Routes.Identity.Login)]
public async Task<IActionResult> LoginAsync(User_login login)
{
var authresponce = await _identify.LoginAsync(login.Password, login.email);
if (!authresponce.Success)
{
return BadRequest(new Unauthenticated
{
Errors = authresponce.Errors
});
}
return Ok(new Authenticated
{
Token = authresponce.Token
});
}
}
// service registration
public class Dbinstaller : IInstaller
{
public void Cleanner(IConfiguration configuration, IServiceCollection services)
{
var jwt = new JWTsettings();
configuration.Bind(nameof(jwt), jwt);
services.AddSingleton(jwt);
services.AddAuthentication(x =>
{
x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
x.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(x =>
{
x.SaveToken = true;
var TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(jwt.Secret)),
ValidateIssuer = false,
ValidateAudience = false,
RequireExpirationTime = false,
ValidateLifetime = true
};
});
services.AddSwaggerGen(x =>
{
x.SwaggerDoc("v1", new OpenApiInfo { Title = "TXC API", Version = "v1" });
var security = new Dictionary<string, IEnumerable<string>>
{
{"Bearer", new string[0]}
};
x.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
{
Description="JWT Authorization Header using the bearer scheme",
Name="Authorization",
In=ParameterLocation.Header,
Type=SecuritySchemeType.ApiKey
});
x.AddSecurityRequirement(new OpenApiSecurityRequirement
{
{new OpenApiSecurityScheme{Reference=new OpenApiReference
{
Id="Bearer",
Type=ReferenceType.SecurityScheme
}
},new List<string>() }
});
});
services.AddDbContext<DataContext>(opt =>
opt.UseSqlServer(configuration.GetConnectionString("TXC Connection")));
services.AddIdentityCore<IdentityUser>();
}
}
//service registration, specified error occurs in Add controllers()
public class In_App_Componentes : IInstaller
{
public void Cleanner(IConfiguration configuration, IServiceCollection services)
{
services.AddControllers().AddNewtonsoftJson(p => p.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver());
services.AddAutoMapper(AppDomain.CurrentDomain.GetAssemblies());
services.AddScoped<IX_Change, The_center>();
services.AddScoped<IIdentify, Identify>();
}
}
//service implementation class
public class Identify : IIdentify
{
private readonly UserManager<IdentityUser> _manager;
private readonly JWTsettings jwtset;
public Identify(UserManager<IdentityUser> userManager, JWTsettings jW)
{
_manager = userManager;
jwtset = jW;
}
public async Task<Authentication_result> RegisterAsync(string email, string password, string Username)
{
var exists = _manager.FindByEmailAsync(email);
if (exists != null)
{
return new Authentication_result
{
Errors = new[] { "User with this email already exists" }
};
}
var newPerson = new IdentityUser
{
Email = email,
UserName = Username
};
var Creation = await _manager.CreateAsync(newPerson, password);
if (!Creation.Succeeded)
{
return new Authentication_result
{
Errors = new[] { "Invalid user!" }
};
}
return Generate_Authentication_Result(newPerson);
}
public async Task<Authentication_result> LoginAsync(string email, string Password)
{
var user = await _manager.FindByEmailAsync(email);
if (user == null)
{
return new Authentication_result
{
Errors = new[] { "User does not exists" }
};
}
var validate_password = await _manager.CheckPasswordAsync(user, Password);
if (!validate_password)
{
return new Authentication_result
{
Errors = new[] { "" }
};
}
return Generate_Authentication_Result(user);
}
private Authentication_result Generate_Authentication_Result(IdentityUser newPerson)
{
var Tokenhandler = new JwtSecurityTokenHandler();
var key = Encoding.ASCII.GetBytes(jwtset.Secret);
var TokenDescripter = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(new[]
{
new Claim(JwtRegisteredClaimNames.Sub, newPerson.Email),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
new Claim(JwtRegisteredClaimNames.Email, newPerson.Email),
new Claim("id",newPerson.Id)
}),
Expires = DateTime.UtcNow.AddHours(2),
SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature)
};
var token = Tokenhandler.CreateToken(TokenDescripter);
return new Authentication_result
{
Success = true,
Token = Tokenhandler.WriteToken(token)
};
}
}
Ciao, I tried to search Microsoft.AspNetCore.Razor.Runtime v. 3.1.1 on NuGet but I found only 2.2.0 . I didn't found any reference to 3.1.1 . Anyway, downgrading Microsoft.AspNetCore.Identity.UI to 3.1.0 should solve your problem as mentioned here
I am getting the access_token from an external api (other app) but i couldn't authenticate/authorize my api using this token, here i am getting the token :
[HttpPost("/authorize/user")]
public async Task<IActionResult> Post([FromBody] LoginDTO login)
{
LoginDTO data = new LoginDTO
{
Email = login.Email,
Password = login.Password
};
var stringPayload = await Task.Run(() => JsonConvert.SerializeObject(data));
var httpContent = new StringContent(stringPayload, Encoding.UTF8, "application/json");
using (var client = new HttpClient())
{
client.BaseAddress = new Uri("myrul");
var response = client.PostAsync("auth/login", httpContent).Result;
var payload = JObject.Parse(await response.Content.ReadAsStringAsync())["token"];
var access_token = payload.Value<string>("access_token");
var refresh_token = payload.Value<string>("refresh_token");
var expires_in = payload.Value<string>("expires_in");
if (response.IsSuccessStatusCode)
return Ok(access_token);
else
return BadRequest();
}
}
and the startup.cs :
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(x =>
{
x.SaveToken = true;
x.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes("012345678901234567890123456789ab")),
ValidateIssuer = false,
ValidateAudience = false,
RequireExpirationTime = false,
ValidateLifetime = true
};
});
the API i need to authorize :
[HttpGet]
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
public IEnumerable<string> Get()
{
return new string[] { "value1", "value2" };
}
but the problem its always 401 unauthorized :
postman pic
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