Skip to content

Latest commit

 

History

History
535 lines (290 loc) · 25.8 KB

README.md

File metadata and controls

535 lines (290 loc) · 25.8 KB

blazor-identity-api

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:

image

Helpful links

  1. Choosing an Identity Solution: Identity vs OIDC (Read it!)
  2. Blazor OIDC with Aspire Example
  3. ASP.NET Core Identity system is designed to authenticate a single web application

How I created the projects

BlazorServerClient project

  1. Installed ef tool dotnet tool update --global dotnet-ef --prerelease

  2. I used Rider to create the project: image

    Hit Create

  3. I ran the migrations dotnet ef database update

  4. Added some missing middleware not included in the template

    image
  5. Launched the app, created a new user and signed right in.

ProtectedWebAPI project

  1. I used Rider to create the project

    image
  2. Added API Key authentication to it. Take a look at the code to see how I implemented it. I referenced mostly this and this.

Issues in the BlazorServerClient project

  1. The Logout doesn't work:

    Click Logout on the bottom left.

    image

    You'll get this error and the user will be never logged out. image

  2. Lot of errors show up. Could be a bug in Rider. (The app runs fine though).

    image

Call ProtectedWebAPI from BlazorServerClient

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:

image

Program.cs

image

WeatherForecastService.cs:

image

Add Microsoft Authentication to your app. Reference.

Create an app in Microsoft Developer Portal (Azure)

image

Store your config

Store the secret in user-secrets. Store ClientId in appsettings.json.

image

Add Nuget package

image

Setup Program.cs

image

Take OAuth authentication for a test drive

  1. Click "Microsoft"

    image
  2. Give consent

    image
  3. These are the claims, MSFT sends to the app

    image

How the claims showed up in the UI

The services are setup at the last line of MicrosoftAccountExtensions where there's a call to .AddOAuth. Here you can see the MicrosoftAccountHandler.

image

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:

image

How Microsoft Authentication is setup under the covers

Command + Click on .AddMicrosoftAccount method:

image

Check what AuthenticationScheme was used:

image

By going to MicrosoftAccountDefaults:

image

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.

Register an app in Github

image

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.

  1. Authorize

    image

    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

  2. Token

    image
  3. User Information endpoint to get user info

    image

Store your config

image

Setup Program.cs

Just look at the code.

AuthN and AuthZ Basics Reference.

Big picture

app.UseRouting(): URL is matched to the endpoint.

app.UseEndpoints(): Actual endpoints are registered.

image

Cookie

This is what cookie contains

image

You can retrieve those Items from AuthenticateResult.AuthenticationProperties.Items:

image

External Authentication

It is handled by RemoteAuthenticationHandler.

image

Propietary provider support:

Google, Facebook, Twitter, Microsoft Account etc

Standard Protocol support:

OpenID connect, WS-Federation, SAML etc.

Taking a look at Authentication middleware

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:

image

Handlers:

See how IAuthenticationHandler looks like:

image

See how IAuthenticationRequestHandler looks like (this is relevant in var handler = await handlers.GetHandlerAsync(context, scheme.Name) as IAuthenticationRequestHandler; line shown below):

image

I have setup Microsoft and Github (OAuth) authentication, so I've got 2 handlers now:

image

Those 2 handlers come from the service registration section:

image

The middleware determines if it should handle auth request on those handlers (using IAuthenticationRequestHandler.HandleRequestAsync).

image

For eg: For my case (Microsoft, GitHub handlers), it's determined by HandleRequestAsync() in RemoteAuthenticationHandler.cs:

image

Since, I'm going to "/counter" page now, the HandleRequestAsync method short circuits.

DefaultAuthentication:

Default AuthenticationScheme is whatever I setup in .AddAuthentication:

image

Using the default authentication scheme, it tries to authenticate the current request. If it succeeds, you get .User in the HttpContext.

image

To view the handler for your default auth scheme, navigate from here:

image

To here:

image

To here:

image

To here:

image

To finally here:

image

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:

image

It just calls .AuthenticateAsync on the AuthenticationService:

image

Let's see this in action:

See the SchemeName and Handler:

image

Now we get into AuthenticateService's AuthenticateAsync method:

image

Then into AuthenticationHandler's AuthenticateAsync method:

image

Then into AuthenticationHandler's HandleAuthenticateOnceAsync method:

image

Then into AuthenticationHandler's HandleAuthenticateAsync method:

image

It's an abstract method that is implemented in a class that derives it, CookieAuthenticationHandler in this case, so we end up here:

image

Then we finally get this result:

image

Taking a look at GitHub Authentication in detail

The schemes the app has:

image

ExternalLoginPicker.razor shows the external logins:

image

Click "OAuth" button.

image

This will call the POST endpoint:

image

/Account/PerformExternalLogin in Identity/Extensions/IdentityComponentsEndpointRouteBuilderExtensions.cs

This is where I want GitHub to redirect me after completing authentication:

image

Use properties to preserve data between Challenge phase and Callback phase

image

Challenge (scheme)

image

Now we're in OAuthHandler.cs

image image

Redirect to external provider

https://localhost:7074/Account/PerformExternalLogin redirects us to GitHub's authorization endpoint:

image

So the app goes to that location:

image

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.

Callback

User gets redirected with the one time code to the callback url:

image

AuthN middleware asks handlers who wants to process request

image

OAuthHandler says "I will" because ShouldHandleRequestAsync() returns true as seen in RemoteAuthenticationHandler.cs:

image

Handler does the protocol post-processing

When line 87 shown above runs, we end up in OAuthHandler.cs:

image

The query has code and state.

image

The state has redirect url and login provider we set earlier:

image

The code is exchanged for the token here:

image

Dummy identity is created:

image

Towards the end of this method, a ticket is created:

image

By calling this callback:

image

Now we're back in RemoteAuthenticationHandler.cs.

Inside HandleRequestAsync(), we set which scheme produced this identity:

image

Call sign-in handler (Set "External" cookie)

In RemoteAuthenticationHandler.cs

image

The Principal looks like this:

image

And the properties:

image

Now we're in CookieAuthenticationHandler.cs's HandleSignInAsync method:

image

A new ticket is created:

image image image

The method completes:

image

This method also completes:

image

Now we're back in RemoteAuthenticationHandler.cs's HandleRequestAsync() method and about to get redirected to our original ReturnUri:

image

Now we're redirected with the "External" cookie

image

Run app-level post processing

Now we're in /Account/ExternalLogin (In ExternalLogin.razor)

image

We try to authenticate using "External" cookie to get external user info:

image

The method looks like this: image

Now we have userinfo:

image image

ProviderKey is the Id in Github.

Now we go into OnLoginCallbackAsync():

image

Here:

image

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).

image

Note 1:

Whenever AuthenticateAsync() method in AuthenticationService.cs runs AND AuthenticateResult.Succeeded is true, ClaimsTransformation is run.

image

Here:

image

Note 2:

The action in the query is this:

image

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.

image

Like here:

image

The message comes from this component in the Login.razor page:

image image

Signout "External" cookie and Signin "Application" (primary) cookie

We're still inside SignInManager.cs.

Here we Signout external cookie:

image

And Signin primary cookie:

image

By calling this method:

image

Looks like it adds provider name (github) as an additional claim.

image

Then call this method to sign in on primary cookie:

image

Now we're back in ExternalLogin.razor by logging in the user successfully.

Redirect to final url

image

We get to homepage now.

image

Like this:

image

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.

image image

Success!

image