Sign in users using AspNetCore.Identity
(.NET 8 RC2
) in a Blazor Server app using cookie authentication and call a protected API using API Key authentication.
Run both of the projects, login using Username: ashish@example.com
and Password: Password123!
.
Navigate to Weather page and you can see the weather data being fetched from a secured API:
- Choosing an Identity Solution: Identity vs OIDC (Read it!)
- Blazor OIDC with Aspire Example
- ASP.NET Core Identity system is designed to authenticate a single web application
-
Installed ef tool
dotnet tool update --global dotnet-ef --prerelease
-
I used Rider to create the project:
Hit Create
-
I ran the migrations
dotnet ef database update
-
Added some missing middleware not included in the template
-
Launched the app, created a new user and signed right in.
-
I used Rider to create the project
-
Added API Key authentication to it. Take a look at the code to see how I implemented it. I referenced mostly this and this.
-
The Logout doesn't work:
Click
Logout
on the bottom left.You'll get this error and the user will be never logged out.
-
Lot of errors show up. Could be a bug in Rider. (The app runs fine though).
In the client project, I setup the ProtectedWebAPI Url and ApiKey in appsettings.json and used that info in Program.cs to call the API.
appsettings.json:
Program.cs
WeatherForecastService.cs:
Add Microsoft Authentication to your app. Reference.
Store the secret in user-secrets
. Store ClientId in appsettings.json.
The services are setup at the last line of MicrosoftAccountExtensions
where there's a call to .AddOAuth
.
Here you can see the MicrosoftAccountHandler
.
To see how the above claims were fetched, you can see it in the MicrosoftAccountOptions
class added from the package. Here you can see that it had asked for the scope of user.read
and Claims were mapped this way:
Command + Click on .AddMicrosoftAccount
method:
Check what AuthenticationScheme
was used:
By going to MicrosoftAccountDefaults
:
You can see AuthenticationScheme
used was "Microsoft"
and also see the 3 most important endpoints in OAuth: AuthorizationEndpoint
, TokenEndpoint
and UserInformationEndpoint
.
Add GitHub authentication to your app. Reference.
Grab clientid
and clientsecret
.
Go to GitHub's OAuth docs to find 3 important endpoints as part of OAuth: Authorize, Token and User endpoints.Reference.
-
Authorize
After the user is logged in, GitHub sends us the one time code (that can be used to exchange for a token) to the redirect url that we set during registration.
https://localhost:7074/signin-github
-
Token
-
User Information endpoint to get user info
Just look at the code.
AuthN and AuthZ Basics Reference.
app.UseRouting()
: URL is matched to the endpoint.
app.UseEndpoints()
: Actual endpoints are registered.
This is what cookie contains
You can retrieve those Items
from AuthenticateResult
.AuthenticationProperties
.Items
:
It is handled by RemoteAuthenticationHandler
.
Google, Facebook, Twitter, Microsoft Account etc
OpenID connect, WS-Federation, SAML etc.
Every time you navigate to ANY page in the app, the Authentication
middleware runs (It's middleware duh!).
It's the bit that's inside app.UseAuthentication
:
See how IAuthenticationHandler
looks like:
See how IAuthenticationRequestHandler
looks like (this is relevant in var handler = await handlers.GetHandlerAsync(context, scheme.Name) as IAuthenticationRequestHandler;
line shown below):
I have setup Microsoft and Github (OAuth) authentication, so I've got 2 handlers now:
Those 2 handlers come from the service registration section:
The middleware determines if it should handle auth request on those handlers (using IAuthenticationRequestHandler.HandleRequestAsync
).
For eg: For my case (Microsoft, GitHub handlers), it's determined by HandleRequestAsync()
in RemoteAuthenticationHandler.cs
:
Since, I'm going to "/counter" page now, the HandleRequestAsync
method short circuits.
Default AuthenticationScheme is whatever I setup in .AddAuthentication
:
Using the default authentication scheme, it tries to authenticate the current request. If it succeeds, you get .User
in the HttpContext.
To view the handler for your default auth scheme, navigate from here:
To here:
To here:
To here:
To finally here:
Here you can see that this service has Schemes, Handlers etc. to authenticate a request.
Now let's get back to see how this line in AuthenticationMiddleware.cs
executes:
It just calls .AuthenticateAsync
on the AuthenticationService
:
See the SchemeName and Handler:
Now we get into AuthenticateService
's AuthenticateAsync
method:
Then into AuthenticationHandler
's AuthenticateAsync
method:
Then into AuthenticationHandler
's HandleAuthenticateOnceAsync
method:
Then into AuthenticationHandler
's HandleAuthenticateAsync
method:
It's an abstract method that is implemented in a class that derives it, CookieAuthenticationHandler
in this case, so we end up here:
Then we finally get this result:
The schemes the app has:
ExternalLoginPicker.razor
shows the external logins:
This will call the POST endpoint:
/Account/PerformExternalLogin
in Identity/Extensions/IdentityComponentsEndpointRouteBuilderExtensions.cs
This is where I want GitHub to redirect me after completing authentication:
Use properties
to preserve data between Challenge phase and Callback phase
Now we're in OAuthHandler.cs
https://localhost:7074/Account/PerformExternalLogin
redirects us to GitHub's authorization endpoint:
So the app goes to that location:
At this ppint the user authenicates with GitHub (NOT this app) and the user authorizes this app to fetch user info from GitHub by accepting the consent screen.
User gets redirected with the one time code to the callback url:
OAuthHandler
says "I will" because ShouldHandleRequestAsync()
returns true
as seen in RemoteAuthenticationHandler.cs
:
When line 87 shown above runs, we end up in OAuthHandler.cs
:
The query has code and state.
The state has redirect url and login provider we set earlier:
The code is exchanged for the token here:
Dummy identity is created:
Towards the end of this method, a ticket is created:
By calling this callback:
Now we're back in RemoteAuthenticationHandler.cs
.
Inside HandleRequestAsync()
, we set which scheme produced this identity:
In RemoteAuthenticationHandler.cs
The Principal
looks like this:
And the properties:
Now we're in CookieAuthenticationHandler.cs
's HandleSignInAsync
method:
A new ticket is created:
The method completes:
This method also completes:
Now we're back in RemoteAuthenticationHandler.cs
's HandleRequestAsync()
method and about to get redirected to our original ReturnUri:
Now we're redirected with the "External" cookie
Now we're in /Account/ExternalLogin
(In ExternalLogin.razor)
We try to authenticate using "External" cookie to get external user info:
Now we have userinfo:
ProviderKey
is the Id in Github.
Now we go into OnLoginCallbackAsync()
:
Here:
Now we get into SignInManager.cs
:
Looks like there's this neat method to get user by loginProvider
(for eg: github) and providerKey
(for eg: 30603497).
Whenever AuthenticateAsync()
method in AuthenticationService.cs
runs AND AuthenticateResult.Succeeded
is true, ClaimsTransformation is run.
Here:
The action in the query is this:
Also notice the user is not authenticated at this point because we haven't successfully authenticated with the default authentication scheme (Identity.Application).
If for some reason, we're not able to get externalLoginInfo
, an error message is passed through the cookie in the redirect to be shown in the UI.
Like here:
The message comes from this component in the Login.razor
page:
We're still inside SignInManager.cs
.
Here we Signout external cookie:
And Signin primary cookie:
By calling this method:
Looks like it adds provider name (github) as an additional claim.
Then call this method to sign in on primary cookie:
Now we're back in ExternalLogin.razor
by logging in the user successfully.
We get to homepage now.
Like this:
Now we hit AuthenticationMiddleware
and try to authenticate the user. The Identity.External
cookie is removed and Identity.Application
cookie is present at this point.
Success!