Plain and simple token authentication in ASP.NET Core 2.2
I am going to go over using JWTBearer authentication and using your own custom token-based authentication in Core 2.2.
I am going to go over using JWTBearer authentication and using your own custom token-based authentication in Core 2.2.
This is a follow up to a previous post that was for core 1.x where I already took care of figuring out the hard parts. It just needed to be updated for Core 2.2, it took a while for me to get around to it. https://www.frakkingsweet.com/plain-and-simple-token-authentication-in-asp-net-core/
For this post I am keeping things simple, I started with a clean Dot Net Core 2.2 web application with authentication that is stored in the application, which really means using the Entity Framework.
There were 2 changes from my previous post that needed to be worked through.
- The place you register the authentication pipeline moved from
Configure
toConfigureServices
. - With the move you don't have access to the final
ServiceProvider
which is needed to gain access to theUserManager
and other services.
Both issues are pretty simple to resolve. The fix for the first was to use the AddJwtBearer
extension method. The fix for the second is to use the HttpContextAccessor
. It would have been nice if the framework would have passed the HttpContext
into the ValidateToken
method.
If you just want to go to the code and see what was needed to do it:
Full source: https://github.com/veccsolutions/Vecc.TokenAuthentication
Only the commit containing the changes: https://github.com/veccsolutions/Vecc.TokenAuthentication/commit/43ac7c02cec9dbd42287172a89b0c9f37accc9dd
In my example I added 3 additional actions to the home controller, ListLogins
, AddToken
and TestToken
.
ListLogins
will return a JSON object containing all of login entries in the database for the currently logged in user.AddToken
will add a new login with the optionally specified value. If no value is specified, a new GUID will be generated and that will be used. It returns theAuthorization
header that you can copy/paste.TestToken
is configured to use theBearer
token and dumps out a JSON object of the user that owns the token. That user is set through the authentication pipeline to prove it all works.
Now to go into detail on how I implemented the ISecurityTokenValidator
.
You don't have dependency injection handed to you like you do everywhere else. To gain access to the services you will need to use the HttpContextAccessor
.
For my implementation, I used the multiple Logins
feature of the identity framework from Microsoft. It comes with the standard framework and handles everything I needed without any extra code or libraries. In order to use it, I need the UserManager<IdentityUser>
service. Calling the FindUserByLoginAsync
got me the user.
Once I have the user, I need to create the claims principal for that user. There is another service for that, the IUserClaimsPrincipalFactory<IdentityUser>
. Calling the CreateAsync
method will handle that.
Now that we have a ClaimsPrincipal
from that token we need to create our security token object. It has some built in security in the class you need to inherit so you'll need to set the SecurityKey
and SigningKey
properties to a security key. Microsoft provides a SymmetricSecurityKey
that handles that, so that's what I used.
Because the ValidateToken
method is not async I used Task.WaitAll
and run the async code in there.
Task.WaitAll(Task.Run(async () =>
{
var applicationUser = await userManager.FindByLoginAsync(LoginProvider, securityToken.Substring(TokenPrefix.Length));
if (applicationUser == null)
{
// In Core 2.2 you need to throw an exception to signal that validation failed, core 1.x was return null.
throw new SecurityTokenException("Token not recognized");
}
result = await principalFactory.CreateAsync(applicationUser);
token = new CustomSecurityToken(applicationUser.Id, LoginProvider,
new SymmetricSecurityKey(this._signingSecurityKey),
new SymmetricSecurityKey(this._signingSecurityKey),
DateTime.MinValue,
DateTime.MaxValue);
}));
The underlying handler uses exceptions as the way to skip validators that failed for any reason. If you return null it thinks it's the token was validated successfully and that a claims principal was created. It has to be an exception. Here's the chunk of code in the underlying framework that handles it: https://github.com/aspnet/AspNetCore/blob/69feac263332424ceccfb673cbf205e31855abdc/src/Security/Authentication/JwtBearer/src/JwtBearerHandler.cs#L108-L129
Here is an example raw request I made to my example project using Fiddler:
GET https://localhost:44314/Home/TestToken HTTP/1.1
User-Agent: Fiddler
Host: localhost:44314
Authorization: Bearer Vecc-7353a697-c8dd-4d24-b46b-0b3c29e4ca09
That's really all there is too it. It's not very difficult once you go diving through the security framework and get lost in there for a while.
Again, the links to the source code:
Full source: https://github.com/veccsolutions/Vecc.TokenAuthentication
Only the commit containing the changes: https://github.com/veccsolutions/Vecc.TokenAuthentication/commit/43ac7c02cec9dbd42287172a89b0c9f37accc9dd