- Why you may want to read this article
- General Authentication Services
- Registering services
- Follow up
Why you may want to read this article
I am a technical guy, and I like to research the
internals of the things I am using daily, and how they work
under the hood.
There is some information about how to add authentication to your application. And guides sound like “copy this, paste there and it should work”. The problem here is that there are a lot of ways to do authentication in your app:
- Simple and / or custom schemes: using JWT, using Cookies, Custom auth handlers
- Using external identity providers: Google auth, and Facebook auth, with a combination of JWT, Cookies on your side.
- Implementing our own OAuth service could use Google auth, Facebook auth, Cookies, return using JWT, etc.
For me, it never was clear how it works how those things are connected, and how they can be combined and used together.
This is the entry article about authentication internals in .NET 5. Here I’m going to go through the source code of the Authentication Core of .NET and explain the basic shared services.
After that, I will launch a bunch of more specific auth internals overviews: cookies-based, JWT-based, Google-based, OAuth-based, etc.
Authenticate - means checking whether the current request contains any auth and contracting User (ClaimsPrincipal) from this info.
JwtBearerHandlerauthentication means checking the “Authorize” header and checking the token validity.
CookieHandlerauthentication means checking and validating cookies.
- External auth (
Microsoft) means emitting OAuth flow with configured API keys and secrets and ensuring external auth result is successful. Note, this is only in case if our application didn’t set its own cookies before, I have explained it in this article.
Sign In & Sign Out
Sign In - means making a future set of requests authenticated. It basically tries to persist the user (
ClaimsPrincipal). Keep in mind not all handlers can sign in.
A good example of it is
CookieHandler. It sets Cookies in response so that all subsequent requests include cookies and be authenticated.
Sign out - means making all subsequent requests not authenticated. Keep in mind not all handlers can sign out.
A good example is still
General Authentication Services
Every auth implementation starts with the Registration of necessary services and then using the appropriate middleware that we’ve registered. Those are services and middlewares that are used in each authentication type: Cookie-based, JWT-based, Google-based, etc.
Before this part, let’s take a look at what will be registered. So high-level components & connectors overview (runtime) is the following:
AuthenticationScheme source code
The most important concept is
Authentication Scheme, put in simple words - it is the thing that represents what auth you are going to use. There are different auth schemes:
And many others. The scheme is a way to tell auth middleware how it should process the request to make auth for you.
Authentication Scheme is tied to a particular
AuthenticationRequestHandler by having a specific HandlerType at the 39th line above. This type is used to instantiate (or resolve using DI)
AuthenticationHandler for specific Schemes to handle auth process.
IAuthenticationHandler source code
Authentication Scheme has a dedicated
Authentication Handler that implements the interface above. Most usually we will try to cast to another derived interface to be able to sign in, sign out, etc.
For example, the Cookie-based handler interface has these
SignOut interfaces and classes extended.
IAuthenticationRequestHandler source code
Some of the
AuthenticationHandlers could implement
This interface extends
IAuthenticationHandler we have discussed above and is used to handle auth and returns whether we need to authenticate with HttpContext later or not (line 19).
For now, it could be not that clear why we need this
IAuthenticationRequestHandler if we already have
IAuthenticationHandler. We will discuss this along with
AuthenticationMiddleware. But most usually they are used to perform OAuth flow with Google, Facebook, Github, etc.
One thing to remember: if this method returns true - the auth request is considered processed and we should stop.
AuthenticationSchemeProvider source code
This service takes care of managing all schemes and providing them whenever required in an optimal and thread-safe manner.
The most important part here is part of adding a particular scheme:
This provider keeps two types of schemes:
- All schemes (line 140)
- Schemes with the IAuthenticationRequestHandler handler type we have discussed above (line 137).
AuthenticationHandlerProvider source code
This provider does not have any rocket science.
It resolves particular
AuthenticationHandler based on the scheme, using
AuthenticationSchemeProvider (remember this HandlerType field in AuthenticationScheme class).
AuthenticationMiddleware source code
It is impossible to do auth without
AuthenticationMiddleware. This middleware is invoked for each request to do auth process. By doing auth process it verifies the authentication ticket and sets
HttpContext.User property so we can access it from controllers.
Line 55-63: we try each scheme that is tied to the
IAuthenticationRequestHandler handler type to do auth process. Usually implementation of this interface are remote auth implementation like Google, Facebook, Github. This handler performs OAuth flow, and Sign In request, as explained here. If a particular request handler returned true then it means no need to continue auth, we return.
Line 65-79: In case of all
IAuthenticationRequestHandlers skipped auth, or they were not found at all - we try to authenticate using context. Authentication using context will be considered later.
A good example of the handler that is implementing
IAuthenticationRequestHandler is RemoteAuthenticationHandler.
It is used in most External auth handlers: Google, Facebook, Microsoft, Github, etc.
There is just part of this method:
It is a good question why we might want to keep authenticating after handling the request using some
I see a few possible scenarios for this:
- Let’s say we have Microsoft and Google auth in place, we will iterate through both of them. So there is a collection of schemes: [
MicrosoftHandler, GoogleHandler]. First goes
MicrosoftHandler. If we got a Google auth request the
MicrosoftHandlerwill skip the request and return false, but after this
GoogleHandlerwill handle it and return true.
- Let’s say we have JwtBearerHandler and GoogleHandler. If the request for auth is for JWT the GoogleHandler will skip and return false. Then the code flow will go out of foreach.
HttpContext.AuthenticateAsync simply resolves
AuthenticationService which in turn authenticates the request.
HttpContext.SignInAsync resolves AuthenticationService as well which signs in the request.
AuthenticateAsync resolves particular IAuthenticationHandler from IAuthenticationHandlerProvider by the authentication scheme. No rocket science - just resolve from DI container by Handler Type.
Then it calls the AuthenticateAsync method on the handler and creates
AuthenticateResult with the corresponding auth ticket, claims, and user.
SignInAsync is acting pretty similar to AuthenticateAsync. It resolves particular
IAuthenticationHandler from IAuthenticationHandlerProvider by the authentication scheme. Then it calls the SignInAsync method on the handler.
This is a kind of wrapper on every provider that calls necessary
IAuthenticationHandler methods, the main magic is in those handlers.
AuthenticateResult source code
This is a container with related auth info.
If authentication is successful AuthenticateResult will contain related data and authentication:
If authentication is not successful then it will contain failure details.
ClaimsPrincipal. For a simpler understanding, it is a kind of authenticated user object.
ClaimsPrincipal in turn contains a collection of
ClaimsIdentity which in turn contains a collection of
Claims. So it looks like this:
It is a bag with user-associated identity information. The most important part of this class for us is
For ClaimsIdentity what we are interested in is a collection of claims that are related to the user.
A Claim is one piece of information about the authenticated user: id, email, first name, role, etc. It has type and value.
Now let’s look at registration for all of those services we have discussed.
Registration starts with
.AddAuthentication source code
This method in turn registers different general services for authentication. The most interesting for us is the following:
.AddAuthenticationCore source code
These are general services needed for authentication, the most important we already discussed above.
.AddDataProtection source code
It registers different services for the security of the auth data, for example, encryption of cookies.
After we registered all services we need to add middlewares to allow them to authenticate and authorize requests before they go to controllers/pages/etc.
For registering authentication we are using .UseAuthentication
This method just registers important middleware which does all work named
AuthenticationMiddleware which we have already discussed.
These are the basic services that are used for different auth handlers. In the next articles - we will consider particular authentication handlers: Cookie-based, Jwt-based, Google-based, etc. They contain most of the magic inside.