How can I determine why a bear token failed - asp.net-core

Running a basic ASP.NET Core RESTful server, I'm having my endpoint secured using a JWT token that I provide on an endpoint (this is a proof of concept for the moment). When I test using Postman, I am able to authenticate properly, however, coming from a console app, getting 401, Unauthorized.
Here is what I have in a ServiceExtensions class:
public static IServiceCollection ConfigureJwtAuthentication(this IServiceCollection services,
IConfiguration config)
{
var section = config.GetSection("Jwt");
var jwtOptions = section.Get<JwtConfigOptions>();
services.AddAuthentication(options =>
{
options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
//options.Authority = jwtOptions.AuthorityUrl;
options.RequireHttpsMetadata = false;
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = jwtOptions.Issuer,
ValidAudience = jwtOptions.Audience,
IssuerSigningKey = jwtOptions.SymmetricSecurityKey
};
});
return services;
}
This is my JwtConfigOptions class:
public class JwtConfigOptions
{
public string Key { get; set; }
public string Issuer { get; set; }
public string Audience { get; set; }
public string AuthorityUrl { get; set; }
public string AudienceUrl { get; set; }
public SymmetricSecurityKey SymmetricSecurityKey => new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Key));
}
And have the values in appsettings.json, so I'm using the same values for creating as vetting the token.
Is there any way to log why a given token is being rejected?

I was able to find an answer. I found this site which had link to a github solution that had the source code for the projects in the Microsoft.AspNetCore.Authentication.JwtBearer assembly. I attached to the JwtBearerHandler project and was able to step through the code. Turns out I encoded the bearer token incorrectly in the header. Actually had the correct code commented out the line before /redface

Related

.Net Core microservice and inspecting Cognito JWT claims for use in authorization

I am developing a .net service that I am passing a Cognito generated JWT in the client that has a group claim that I hope to use to restrict API access as the JWT is passed in as a Bearer token with each API call from the front-end. e.g.
"cognito:groups":["Guest"]
In my code now I have added:
services.AddAuthentication(DefaultScheme = JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateAudience = false,
RoleClaimType = "cognito:groups"
};
});
I have setup up my roles and my permissions using the HeimGuard project. In testing as I change roles in the Bearer token none of my APIs that restrict access to Guest do not deny access when decorated with [Authorize]. So, nothing is restricted. Any thoughts or ideas you have in how to solve this problem helps immensly. Thank you!!
I tried custom AuthorizeAttribute as below:
To create the Token:
public class UserModel
{
public string Username { get; set; }
public string Role { get; set; }
public string EmailAddress { get; set; }
public string PassWord { get; set; }
public string Right { get; set; }
}
[HttpPost]
public IActionResult Authenticate(UserModel someuser)
{
var claims = new Claim[]
{
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
new Claim(ClaimTypes.Role, someuser.Role),
new Claim(ClaimTypes.Name, someuser.Username),
new Claim("Right", someuser.Right)
};
var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_config["Jwt:Key"]));
var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256);
var token = new JwtSecurityToken(
_config["Jwt:Issuer"],
_config["Jwt:Issuer"],
signingCredentials: credentials,
expires: DateTime.Now.AddMinutes(2),
claims: claims
);
string jwtToken = new JwtSecurityTokenHandler().WriteToken(token);
return Content(jwtToken);
}
codes in appsettings.json:
"Jwt": {
"Key": "ThisismySecretKey",
"Issuer": "Test.com"
}
Custom requirement:
public class WriteRequirement : AuthorizationHandler<WriteRequirement>, IAuthorizationRequirement
{
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, WriteRequirement requirement)
{
var right = context.User.FindFirst("Right").Value;
if(right=="Write")
{
context.Succeed(requirement);
}
else
{
context.Fail();
}
return Task.CompletedTask;
}
}
in startup class:
public void ConfigureServices(IServiceCollection services)
{
...
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = false,
ValidateAudience = false,
ValidateLifetime = false,
ValidateIssuerSigningKey = false,
ValidIssuer = Configuration["Jwt:Issuer"],
ValidAudience = Configuration["Jwt:Issuer"],
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["Jwt:Key"]))
};
});
services.AddSingleton<IAuthorizationHandler, WriteRequirement>();
services.AddAuthorization(opyions => { opyions.AddPolicy("Write", policy => policy.Requirements.Add(new WriteRequirement())); });
...
}
The Action need Authrization:
[HttpGet]
[Authorize(Roles ="Admin")]
[Authorize(Policy="Write")]
public ActionResult<IEnumerable<string>> Get()
{
return new string[] { "value1", "value2", "value3", "value4", "value5" };
}
The Result:

IHttpContextAccessor returns null Username claims in AspNetCore 5 API Controller

I have some Api Controllers in my AspNetCore 5 web application containing some methods which are available to unauthenticated users and some of them are only available for authorized users.
Assume that there is a ReviewController which all users can add review, but if the user is authenticated, we should fill username field.
Let's say in TestController:
[Route("api/[controller]/[action]/")]
[ApiController]
public class TestController
{
private readonly IHttpContextAccessor _httpContextAccessor;
public TestController(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
}
[HttpGet]
public string TestA()
{
return _httpContextAccessor.HttpContext.User.Identity.Name;
}
[HttpGet]
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
public string TestB()
{
return _httpContextAccessor.HttpContext.User.Identity.Name;
}
}
The only difference between two methods is Authorize attribute.
I setup JwtBearer authentication in the application with below setup.
ConfigureServices:
public override void ConfigureServices(IServiceCollection services)
{
var jwtConfig = new JwtConfig
{
Secret = "some secrets!",
Audience = "http://localhost:5000",
Expires = 600,
Issuer = "http://localhost:5000"
};
services.AddAuthorization();
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(jwt =>
{
var key = Encoding.ASCII.GetBytes(jwtConfig.Secret);
jwt.SaveToken = true;
jwt.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(key),
ValidateIssuer = true,
ValidateAudience = true,
RequireExpirationTime = true,
ValidateLifetime = true,
SaveSigninToken = true,
ValidAudience = jwtConfig.Audience,
ValidIssuer = jwtConfig.Issuer
};
});
services.AddHttpContextAccessor();
services.AddMvcCore()
.AddDataAnnotations()
.AddApiExplorer()
.AddFormatterMappings()
.AddCors();
}
And Configure:
public void Configure(IApplicationBuilder app)
{
app.UseDefaultFiles();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
When I am calling TestB it returns the logged in username. But, when I am calling TestA it returns null.
Why IHttpContextAccessor's user claims are not loaded and how can I access to IHttpContextAccessor's username and user claims in any condition? Assume that I am using that in other layers than controllers.

OpenIdConnect Redirect on Form POST

Why does a form POST with an expired access_token result in a GET when using the Microsoft.AspNetCore.Authentication.OpenIdConnect middleware? When this happens, any data entered into a form is lost, since it doesn't reach the HttpPost endpoint. Instead, the request is redirected to the same URI with a GET, following the signin-oidc redirect. Is this a limitation, or do I have something configured incorrectly?
I noticed this issue after shortening the AccessTokenLifetime with the intent of forcing the user's claims to be renewed more frequently (i.e. if the user were disabled or they had claims revoked). I have only reproduced this when the OpenIdConnect middleware's OpenIdConnectionOptions are set to true options.UseTokenLifetime = true; (setting this to false results in the authenticated user's claims not being updated as expected).
I was able to recreate and demonstrate this behavior using the IdentityServer4 sample quickstart 5_HybridFlowAuthenticationWithApiAccess with the following changes below. Basically, there is an authorized form that has an HttpGet and an HttpPost method. If you wait longer than the AccessTokenLifetime (configured to only 30 seconds in this example) to submit the form, the HttpGet method is called instead of the HttpPost method.
Modifications to MvcClient/Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
services.AddAuthentication(options =>
{
options.DefaultScheme = "Cookies";
options.DefaultChallengeScheme = "oidc";
})
.AddCookie("Cookies", options =>
{
// the following was added
options.SlidingExpiration = false;
})
.AddOpenIdConnect("oidc", options =>
{
options.SignInScheme = "Cookies";
options.Authority = "http://localhost:5000";
options.RequireHttpsMetadata = false;
options.ClientId = "mvc";
options.ClientSecret = "secret";
options.ResponseType = "code id_token";
options.SaveTokens = true;
options.GetClaimsFromUserInfoEndpoint = true;
options.Scope.Add("openid");
options.Scope.Add("api1");
options.ClaimActions.MapJsonKey("website", "website");
// the following were changed
options.UseTokenLifetime = true;
options.Scope.Add("offline_access");
});
}
Modifications to the Client list in IdentityServer/Config.cs
new Client
{
ClientId = "mvc",
ClientName = "MVC Client",
AllowedGrantTypes = GrantTypes.Hybrid,
ClientSecrets =
{
new Secret("secret".Sha256())
},
RedirectUris = { "http://localhost:5002/signin-oidc" },
PostLogoutRedirectUris = { "http://localhost:5002/signout-callback-oidc" },
AllowedScopes =
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
"api1",
IdentityServerConstants.StandardScopes.OfflineAccess,
},
AllowOfflineAccess = true,
// the following properties were configured:
AbsoluteRefreshTokenLifetime = 14*60*60,
AccessTokenLifetime = 30,
IdentityTokenLifetime = 15,
AuthorizationCodeLifetime = 15,
SlidingRefreshTokenLifetime = 60,
RefreshTokenUsage = TokenUsage.OneTimeOnly,
UpdateAccessTokenClaimsOnRefresh = true,
RequireConsent = false,
}
Added to MvcClient/Controllers/HomeController
[Authorize]
[HttpGet]
[Route("home/test", Name = "TestRouteGet")]
public async Task<IActionResult> Test()
{
TestViewModel viewModel = new TestViewModel
{
Message = "GET at " + DateTime.Now,
TestData = DateTime.Now.ToString(),
AccessToken = await this.HttpContext.GetTokenAsync("access_token"),
RefreshToken = await this.HttpContext.GetTokenAsync("refresh_token"),
};
return View("Test", viewModel);
}
[Authorize]
[HttpPost]
[Route("home/test", Name = "TestRoutePost")]
public async Task<IActionResult> Test(TestViewModel viewModel)
{
viewModel.Message = "POST at " + DateTime.Now;
viewModel.AccessToken = await this.HttpContext.GetTokenAsync("access_token");
viewModel.RefreshToken = await this.HttpContext.GetTokenAsync("refresh_token");
return View("Test", viewModel);
}
After further research and investigation, I came to the conclusion that completing a form POST that is redirected to the OIDC provider is not supported out of the box (at least for Identity Server, but I suspect that is also true for other identity connect providers as well). Here is the only mention I can find of this: Sending Custom Parameters to Login Page
I was able to come up with a workaround for the issue, which I've outlined below and is hopefully useful to others. The key components are the following OpenIdConnect and Cookie middleware events:
OpenIdConnectEvents.OnRedirectToIdentityProvider - save Post requests for later retrieval
CookieAuthenticationEvents.OnValidatePrincipal - check for saved Post requests and update the current request with saved state
The OpenIdConnect middleware exposes the OnRedirectToIdentityProvider event which gives us the opportunity to:
determine if this is a form post for an expired access token
modify the RedirectContext to include a custom request id with the AuthenticationProperties Items dictionary
Map the current HttpRequest to an HttpRequestLite object that can be persisted to a cache store, I recommend using an expiring, distributed cache for load-balanced environments. I'm using a static dictionary here for simplicity
new OpenIdConnectEvents
{
OnRedirectToIdentityProvider = async (context) =>
{
if (context.HttpContext.Request.Method == HttpMethods.Post && context.Properties.ExpiresUtc == null)
{
string requestId = Guid.NewGuid().ToString();
context.Properties.Items["OidcPostRedirectRequestId"] = requestId;
HttpRequest requestToSave = context.HttpContext.Request;
// EXAMPLE - saving this to memory which would work on a non-loadbalanced or stateful environment. Recommend persisting to external store such as Redis.
postedRequests[requestId] = await HttpRequestLite.BuildHttpRequestLite(requestToSave);
}
return;
},
};
The Cookie middleware exposes the OnValidatePrincipal event which gives us the opportunity to:
check the CookieValidatePrincipalContext for AuthenticationProperties Items for custom dictionary items. We check it for the ID of our saved/cached request
it's important that we remove the item after we read it so that subsequent requests do not replay the wrong form submission, setting ShouldRenew to true persists any changes on subsequent requests
check our external cache for items that match our key, I recommend using an expiring, distributed cache for load-balanced environments. I'm using a static dictionary here for simplicity
read our custom HttpRequestLite object and override the Request object in the CookieValidatePrincipalContext object
new CookieAuthenticationEvents
{
OnValidatePrincipal = (context) =>
{
if (context.Properties.Items.ContainsKey("OidcPostRedirectRequestId"))
{
string requestId = context.Properties.Items["OidcPostRedirectRequestId"];
context.Properties.Items.Remove("OidcPostRedirectRequestId");
context.ShouldRenew = true;
if (postedRequests.ContainsKey(requestId))
{
HttpRequestLite requestLite = postedRequests[requestId];
postedRequests.Remove(requestId);
if (requestLite.Body?.Any() == true)
{
context.Request.Body = new MemoryStream(requestLite.Body);
}
context.Request.ContentLength = requestLite.ContentLength;
context.Request.ContentLength = requestLite.ContentLength;
context.Request.ContentType = requestLite.ContentType;
context.Request.Method = requestLite.Method;
context.Request.Headers.Clear();
foreach (var header in requestLite.Headers)
{
context.Request.Headers.Add(header);
}
}
}
return Task.CompletedTask;
},
};
We need a class to map HttpRequest to/from for serialization purposes. This reads the HttpRequest and it's body without modifying the contents, it leaves the HttpRequest untouched for additional middleware that may try to read it after we do (this is important when trying to read the Body stream which can only be read once by default).
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Internal;
using Microsoft.Extensions.Primitives;
public class HttpRequestLite
{
public static async Task<HttpRequestLite> BuildHttpRequestLite(HttpRequest request)
{
HttpRequestLite requestLite = new HttpRequestLite();
try
{
request.EnableRewind();
using (var reader = new StreamReader(request.Body))
{
string body = await reader.ReadToEndAsync();
request.Body.Seek(0, SeekOrigin.Begin);
requestLite.Body = Encoding.ASCII.GetBytes(body);
}
//requestLite.Form = request.Form;
}
catch
{
}
requestLite.Cookies = request.Cookies;
requestLite.ContentLength = request.ContentLength;
requestLite.ContentType = request.ContentType;
foreach (var header in request.Headers)
{
requestLite.Headers.Add(header);
}
requestLite.Host = request.Host;
requestLite.IsHttps = request.IsHttps;
requestLite.Method = request.Method;
requestLite.Path = request.Path;
requestLite.PathBase = request.PathBase;
requestLite.Query = request.Query;
requestLite.QueryString = request.QueryString;
requestLite.Scheme = request.Scheme;
return requestLite;
}
public QueryString QueryString { get; set; }
public byte[] Body { get; set; }
public string ContentType { get; set; }
public long? ContentLength { get; set; }
public IRequestCookieCollection Cookies { get; set; }
public IHeaderDictionary Headers { get; } = new HeaderDictionary();
public IQueryCollection Query { get; set; }
public IFormCollection Form { get; set; }
public PathString Path { get; set; }
public PathString PathBase { get; set; }
public HostString Host { get; set; }
public bool IsHttps { get; set; }
public string Scheme { get; set; }
public string Method { get; set; }
}

Owin/Katana UseJwtBearerAuthentication Always Returns 401

I need to implement JWT Bearer Authentication in both a .NET 4.6.1 Web API project, as well as a .NET Core 2.0 web project.
I successfully got the core project up and running with the this sample from Microsoft.
I'm following this sample from Scott Allen for the .NET 4.6.1 project.
My 4.6.1 code looks like this:
public void ConfigureAuth(IAppBuilder app)
{
// Application requuires AD Bearer tokens for access
app.UseJwtBearerAuthentication(
new JwtBearerAuthenticationOptions
{
AuthenticationMode = AuthenticationMode.Active,
TokenValidationParameters = new TokenValidationParameters
{
ValidIssuer = "https://validissuer.blahblah.com/",
ValidAudience = "https://validaudience.blahblah.com"
}
});
}
The same token will correctly validate in the .NET Core API, but not the .NET 4.6.1 API, and I believe I'm missing something minor. Any ideas?
Side question: What are the best practices for what should be validated in a production environment? Both instances attempt to validate the issuer and audience, but should I consider validating anything else?
-Tim
You need to set IssuerSigningKey property. If you don't know how to get the key manually, Here is an example to automatically resovle the IssuerSigningKey by authority:
using System.Linq;
using System.Threading;
using Microsoft.IdentityModel.Protocols;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
using Microsoft.IdentityModel.Tokens;
using Microsoft.Owin.Security;
using Microsoft.Owin.Security.Jwt;
public class Startup
{
public void Configuration(IAppBuilder app)
{
var authority = "https://<your-authority>/";
var keyResolver = new OpenIdConnectSigningKeyResolver(authority);
app.UseJwtBearerAuthentication(
new JwtBearerAuthenticationOptions
{
AuthenticationMode = AuthenticationMode.Active,
TokenValidationParameters = new TokenValidationParameters()
{
AuthenticationType = "Bearer",
ValidIssuer = "https://<your-authority>/",
ValidateAudience = false,
ValidateIssuer = true,
RequireExpirationTime = true,
ValidateLifetime = true,
IssuerSigningKeyResolver = (token, securityToken, kid, parameters) => keyResolver.GetSigningKey(kid)
}
});
}
private class OpenIdConnectSigningKeyResolver
{
private readonly OpenIdConnectConfiguration openIdConfig;
public OpenIdConnectSigningKeyResolver(string authority)
{
var cm = new ConfigurationManager<OpenIdConnectConfiguration>($"{authority.TrimEnd('/')}/.well-known/openid-configuration", new OpenIdConnectConfigurationRetriever());
openIdConfig = AsyncHelper.RunSync(async () => await cm.GetConfigurationAsync());
}
public SecurityKey[] GetSigningKey(string kid)
{
// Find the security token which matches the identifier
return new[] { openIdConfig.JsonWebKeySet.GetSigningKeys().FirstOrDefault(t => t.KeyId == kid) };
}
}
private static class AsyncHelper
{
private static readonly TaskFactory TaskFactory = new TaskFactory(CancellationToken.None, TaskCreationOptions.None, TaskContinuationOptions.None, TaskScheduler.Default);
public static void RunSync(Func<Task> func)
{
TaskFactory.StartNew(func).Unwrap().GetAwaiter().GetResult();
}
public static TResult RunSync<TResult>(Func<Task<TResult>> func)
{
return TaskFactory.StartNew(func).Unwrap().GetAwaiter().GetResult();
}
}
}
Since you've assigned new instance of TokenValidationParameters to the corresponding property, I think, you should also provide a valid IssuerSigningKey.
builder.UseJwtBearerAuthentication(new JwtBearerAuthenticationOptions
{
AuthenticationMode = AuthenticationMode.Active,
TokenValidationParameters = new TokenValidationParameters
{
AuthenticationType = "Bearer",
ValidIssuer = "https://validissuer.blahblah.com/",
ValidAudience = "https://validaudience.blahblah.com",
IssuerSigningKey = new InMemorySymmetricSecurityKey(Encoding.UTF8.GetBytes("MySecret")),
}
});

Is there any way to do token base auth in asp.net core application without redundant stuffs?

Anyone have a good example to do token based authorization in asp.net core without this crap like IdentityContext and other? I just want to set up settings for token generating in order to my system can generate and check token in right way and I want to manage authentication process by myself. Thanks
Having used a solution from this article go.microsoft.com/fwlink/?linkid=84547:
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(o =>
{
o.Authority = Configuration["AuthOptions:Authority"];
o.RequireHttpsMetadata = false;
o.TokenValidationParameters = new TokenValidationParameters()
{
ValidIssuer = Configuration["AuthOptions:Issuer"],
ValidateAudience = false,
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["AuthOptions:Key"])),
ValidateLifetime = true,
};
});
services.AddMvc();
ConfigureDependincies(services);
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
var configuration = new SlackConfiguration
{
WebhookUrl = new Uri("https://hooks.slack.com/services/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"),
MinLevel = LogLevel.Warning
};
loggerFactory.AddSlack(configuration, env);
// loggerFactory.AddDebug();
app.UseDefaultFiles();
app.UseDeveloperExceptionPage();
app.UseAuthentication();
//app.UseJwtBearerAuthentication(new JwtBearerOptions()
//{
// AutomaticAuthenticate = true,
// AutomaticChallenge = true,
// RequireHttpsMetadata = false,
// TokenValidationParameters = new TokenValidationParameters()
// {
// ValidIssuer = Configuration["AuthOptions:Issuer"],
// ValidateAudience = false,
// ValidateIssuerSigningKey = true,
// IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["AuthOptions:Key"])),
// ValidateLifetime = true,
// }
//});
app.UseMvc();
}
var token = new JwtSecurityToken(
issuer: _root["AuthOptions:Issuer"],
notBefore: DateTime.UtcNow,
claims: identity.Claims,
expires: DateTime.UtcNow.Add(TimeSpan.FromMinutes(Convert.ToDouble(_root["AuthOptions:TokenLifeTime"]))),
signingCredentials: new SigningCredentials(new SymmetricSecurityKey(Encoding.ASCII.GetBytes(_root["AuthOptions:Key"])), SecurityAlgorithms.HmacSha512)
);
return token;
It's working for me.
It can be done in following way
Have a token generator API endpoint (controller) i.e. http://localhost/auth/token
In this, you generate token by verifying the authenticate user (check user against store)
The generated token can be validated by providing authentication schema in ASP.NET Core pipeline. app.AddAuthentication()
Any subsequent calls to API, should have Authorization header with generated token.
This approach can be refined accordingly.
I've done this very thing. I got sick of all the third party things so I wrote my own.
You want to create tokens and provide them/ validate them through an api.
Here is an example of the api controller that creates the token initially.
[Route("api/[controller]")]
public class TokenController : Controller
{
private readonly TokenCreatorOption _tco;
private readonly CryptoHash _ch;
public TokenController(IOptions<TokenCreatorOption> ioptTCO, IOptions<CryptoHash> ioptCH, IOptions<ConnectionStrings> ioptConn)
{
_tco = ioptTCO.Value;
_ch = ioptCH.Value;
}
[HttpPost("")]
public async Task<IActionResult> IssueToken([FromBody] CredentialUser model)
{
///if model is null, this is an incorrect format
if(model == null)
{
return BadRequest();
}
var user = GetUserFromDatabaseOrStore(model.userName, model.passWord);
if(user == null)
{
return NotFound();
}
TokenCreatorOption newTCO = _tco; ///get your initial instantiation of the TokenCreatorOption. This is set to default values based off appsettings or in configure services
newTCO.UserObject = user;
newTCO.Expiration = DateTime.UtcNow.AddMinutes(30).ToString("yyyy-MM-dd hh:mm:ss.ss tt");
///anything within the TokenCreatorOption will be hashed, anything in the token Provider is not going to be hashed (not secured), but acts as a good object to store just general things that are needed on client side.
TokenProvider _tpo = new TokenProvider();
_tpo.tco = TokenInteraction.CreateToken(newTCO, _ch.salt);
_tpo.listApp = xapp; ///put anything you wouldn't want to be hashed and claimed against outside of the object. so you always validate things inside the tco, but never exclude anything inside tco. This was a fatal flaw in tokens in the past.
///this is using messagepack to serialize, to make it smaller since this is going to be passed between every request/response. Consider zipping as well if large enough.
var serializer = MessagePackSerializer.Get<TokenProvider>();
byte[] obj = null;
using (var byteStream = new MemoryStream())
{
serializer.Pack(byteStream, _tpo);
obj = byteStream.ToArray();
}
return File(obj, "application/octet-stream");
}
TokenCreatorOption Class
public class TokenCreatorOption
{
public string Issuer { get; set; }
public UserFromThatDatabaseOrStore UserObject { get; set; }
public string Expiration { get; set; }
public string HashValue { get; set; }
}
Notice that all these objects in TokenCreatorOption are claims. Every single one is checked in the hash function.
Here is the Token Creator and the Token Validator, once a token is valid, you can reissue a new one.
TokenInteraction
public static class TokenInteraction
{
public static TokenCreatorOption CreateToken(TokenCreatorOption _tco, byte[] salt)
{
byte[] exp = Encoding.UTF8.GetBytes(_tco.Expiration);
byte[] issuer = Encoding.UTF8.GetBytes(_tco.Issuer);
byte[] user = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(_tco.UserObject));
byte[] salty = salt;
IEnumerable<byte> rv = exp.Concat(issuer).Concat(user).Concat(salty);
HashAlgorithm alg = SHA512.Create();
_tco.HashValue = Convert.ToBase64String(alg.ComputeHash(rv.ToArray()));
return _tco;
}
public static bool ValidateToken(TokenCreatorOption _tco, byte[] salt)
{
byte[] exp = Encoding.UTF8.GetBytes(_tco.Expiration);
byte[] issuer = Encoding.UTF8.GetBytes(_tco.Issuer);
byte[] user = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(_tco.UserObject));
byte[] salty = salt;
IEnumerable<byte> rv = exp.Concat(issuer).Concat(user).Concat(salty);
HashAlgorithm alg = SHA512.Create();
if (_tco.HashValue != Convert.ToBase64String(alg.ComputeHash(rv.ToArray())))
{
return false;
}
else
{
return true;
}
}
Notice in TokenInteraction The order of bytes added to rv needs to be in the same order when we validate the token.
Now we can have a validate controller.
[Route("api/[controller]")]
public class ValidateController : Controller
{
private readonly TokenCreatorOption _tco;
private readonly CryptoHash _ch;
public ValidateController(IOptions<TokenCreatorOption> ioptTCO, IOptions<CryptoHash> ioptCH)
{
_tco = ioptTCO.Value;
_ch = ioptCH.Value;
}
[HttpPost("")]
public async Task<IActionResult> ValidateToken([FromBody] TokenCreatorOption model)
{
if (model == null)
{
return BadRequest("Model Cannot be Null");
}
///Kick them right now if session is expired, so we don't have to do the full hashing.
if (DateTime.ParseExact(model.Expiration, "yyyy-MM-dd hh:mm:ss.ss tt", CultureInfo.InvariantCulture) < DateTime.UtcNow)
{
return BadRequest("Expired Datetime");
}
if(!TokenInteraction.ValidateToken(model, _ch.salt))
{
return Unauthorized();
}
model.Expiration = DateTime.UtcNow.AddMinutes(30).ToString("yyyy-MM-dd hh:mm:ss.ss tt");
TokenProvider _tpo = new TokenProvider();
_tpo.tco = TokenInteraction.CreateToken(model, _ch.salt);
var serializer = MessagePackSerializer.Get<TokenProvider>();
byte[] obj = null;
using (var byteStream = new MemoryStream())
{
serializer.Pack(byteStream, _tpo);
obj = byteStream.ToArray();
}
return File(obj, "application/octet-stream");
}
And of course when you are initially registering the services. Create your salt value either through a cert, or through a random crypto number generator.
public void ConfigureServices(IServiceCollection services)
{
// Add framework services.
services.AddCors();
services.AddOptions();
services.AddSwaggerGen();
services.ConfigureSwaggerGen(options =>
{
options.SingleApiVersion(new Swashbuckle.Swagger.Model.Info
{
Version = "v1"
});
services.Configure<TokenCreatorOption>(myopt =>
{
myopt.Issuer = "Issuer"; //either from appsettings or manually
myopt.Expiration = null;
myopt.UserObject = null;
myopt.HashValue = "";
});
byte[] salty;
new RNGCryptoServiceProvider().GetBytes(salty = new byte[64]);
services.Configure<CryptoHash>(copt =>
{
copt.salt = (new Rfc2898DeriveBytes("Super!SecretKey!123456789!##$", salty, 1000)).GetBytes(64);
});
services.AddSingleton<IConfiguration>(Configuration);