The blazor logo, but instead of straight lines and good proportions, it's slightly badly drawn and green

OIDC Authentication in server-side Blazor apps

17 July 2024

Today, we're going to create a server-side Blazor app that uses Azure OIDC to authenticate. You’ll need the DotNetCore SDK. I’m going to be using DotNetCore 6 - the oldest possible version that's still supported.

First I’m going to set things up. I’m not going to go into a whole lot of detail until we get into authorization section, so if this is your first time creating a DotNetCore app, you might want to find a more introductory post. I'll also be using the command line and VSCode, so this may not be for Visual Studio fans.

Let’s start by creating a new server-side blazor app:

dotnet new blazorserver -o BlazorServerApp

Now cd into the app, then there’s a command you might need to run to get the nuget install going:

    cd BlazorServerApp
    dotnet nuget add source --name nuget.org https://api.nuget.org/v3/index.json

Now let’s run the app. I like to use the watch command so the server will reload when I make a change:

    dotnet watch -- run

Head to https://localhost:7092/ (or whichever port it has decided to host from) and you should be looking at a brand new server-side blazor app.

Setting up Azure as an OIDC provider #

Now let’s create an OIDC server with Azure. Basically we’re going to follow this blogpost’s instructions.

We’re going to create an app registration. You’ll need to have some kind of elevated permissions to do this.

Head to the Azure portal, then we’re going to go to Microsoft Entra app registration.

Give your app a name - I’m going to call it “blazorserverapp”. After it’s created, head to “clients and secrets” and create a new client secret. Grab the secret and the client ID and keep these in a text file for now.

You’ll also need to add a redirect URL so that Azure knows where your app is based and isn’t going to send a load of private information to someone else’s domain. Click into “Authentication” then under “redirect URIs”, add https://localhost:7102/signin-oidc. We want our site’s address with /signin-oidc at the end. You can add multiple URLs for development and production if you need.

Adding OpenIDConnect to your Blazor app #

Now, server-side blazor doesn't link super well with the OpenIDConnect package. This package expects that client and server will be communicating using HTTP, which isn't the protocol that server-side blazor uses. Instead, Server-side Blazor uses a protocol called SignalR. Don't fear though! We're going to (partly) follow the methods of this stack overflow answer to get around these issues.

First let’s install the OpenIdConnect package:

    dotnet add package Microsoft.AspNetCore.Authentication.OpenIdConnect

Running DotNet 6 means I needed to add this to install an older version:

    dotnet add package Microsoft.AspNetCore.Authentication.OpenIdConnect --version 6.0.2

Let’s start by updating our Startup.cs file to configure our app to use the OpenIdConnect authentication package. First add the using statement at the top:

    using Microsoft.AspNetCore.Authentication.OpenIdConnect;

Then anywhere before the app = builder.Build(); statement add in this to register the authentication:

    builder.Services.AddAuthentication(options =>
    {
        options.DefaultScheme = "Cookies";
        options.DefaultChallengeScheme = "oidc";
    })
    .AddCookie("Cookies")
    .AddOpenIdConnect("oidc", options =>
    {
        options.Authority = "https://login.microsoftonline.com/mytenant/v2.0/";
        options.ClientId = "my client id"; 
        options.ClientSecret = "my client secret";
    });

Then after the app = builder.Build(); statement, and before app.UseRouting();, we tell it to use authentication in the middleware pipeline:

    app.UseAuthentication();

This sets up the authentication. Now when we we start the app the OpenIdConnect middleware will be listening on the /signin-oidc endpoint for when Azure sends back user information.

There will still be no difference to the app until we start to configure which pages are public and which pages only authenticated users can see.

Hiding pages behind authentication #

We're going to update the App.razor file to check whether the user is logged in and redirect them to a login page if not:

    <!-- App.razor -->
    @inject NavigationManager NavigationManager
    <CascadingAuthenticationState>
    <Router AppAssembly="@typeof(Program).Assembly">
        <Found Context="routeData">
            <AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)">
                <NotAuthorized>
                    @{
                        
                        var returnUrl = NavigationManager.ToBaseRelativePath(NavigationManager.Uri);
                        NavigationManager.NavigateTo($"login?redirectUri={returnUrl}", forceLoad: true);
                    }
                </NotAuthorized>
                <Authorizing>
                    Wait...
                </Authorizing>
            </AuthorizeRouteView>
        </Found>
        <NotFound>
            <LayoutView Layout="@typeof(MainLayout)">
                <p>Sorry, there's nothing at this address.</p>
            </LayoutView>
        </NotFound>
    </Router>
    </CascadingAuthenticationState>

This will only redirect for pages where there is an @attribute [Authorize] on the page. All pages without this attribute will remain open to the public. Let’s update our index.razor page to the following:

    @page "/"
    @using Microsoft.AspNetCore.Components.Authorization
    @attribute [Authorize]
    
    <PageTitle>Index</PageTitle>
    
    <h1>Hello, world!</h1>
    Welcome to your new app.
    <SurveyPrompt Title="How is Blazor working for you?" />

If you run the app and head to the home page now, you’ll get redirected to a login URL in our site which does not exist.

Redirecting users to the OIDC provider #

Let’s create the endpoint the unauthenticated user will get redirected to. We use this endpoint to forward the user to our OIDC provider. Write a login.cshtml file in the Pages folder of the app (cshtml files get server-rendered and aren’t part of the blazor app):

    @page
    <!-- Login.cshtml -->
    @model LoginModel

That’s all we need in the login.cshtml file, but now we add the model file (Login.cshtml.cs) in the same directory to define the logic after a request:

    // Login.cshtml.cs
    using Microsoft.AspNetCore.Authentication;
    using Microsoft.AspNetCore.Mvc.RazorPages;
    
    public class LoginModel : PageModel
    {
        public async Task OnGet(string redirectUri)
        {
            if (!User.Identity.IsAuthenticated){
                await HttpContext.ChallengeAsync("oidc", new 
                    AuthenticationProperties { RedirectUri = redirectUri } );
            }
            else{
                // redirects user to redirectUri if they are already logged in
                if (redirectUri == null)
                {
                    redirectUri = "/";
                }
                Response.Redirect(redirectUri);
            }
        }  
    }

The HttpContext.ChallengeAsync function is the part which does the work, redirecting the user to Azure if they are not logged on.

Now after adding this and restarting your app, you should get redirected to Azure to login, then get redirected back to your application again. If you go through this process then look in the “Application” section of your browser’s dev tools you should notice that there are new cookies associated with your app. This is where your session is being kept track of.

Using claims from OIDC in your app #

How do we get access to user data we’ve received from Azure?

We can use blazor's AuthenticationStateProvider class. This can be injected into our blazor components to get accessnto the logged in user. This class will allow us to get a handle on all the claims we’re getting back from Azure. To demostrate this, update your index.razor file to look like this:

    @page "/"
    @using Microsoft.AspNetCore.Components.Authorization
    @using System.Security.Claims
    @attribute [Authorize]
    
    <PageTitle>Index</PageTitle>
    <h1>Hello, @User.Identity.Name!</h1>
    <ul>
        @foreach (var claim in User.Claims)
        {
            <li>
                <strong>@claim.Type:</strong> @claim.Value
            </li>
        }
    </ul>
    
    Welcome to your new app.
    <SurveyPrompt Title="How is Blazor working for you?" />
    @code{
      @inject AuthenticationStateProvider AuthenticationStateProvider
      
      @code {
          protected ClaimsPrincipal User;
          protected override async Task OnInitializedAsync()
          {
              var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
              User = authState.User;
          }
      }
    }

This will say hello to the user, pulling their name from the user identity data we get back from Azure, and will also list all of the other claims it receives. An important claim is preferred_username, which in my case was my email address - this could be used to link the logged in user to data in your database.

Wrapping up #

This is a small start, but from here the sky is the limit. You don’t have to manage authentication yourself. Your users don’t have to create another password. But after they log in, you know who they are and you can create logic around what they can see and do.

Where to from here? You can look into configuring Azure to send different claims. These can be used to decide what the user is allowed to see; You can implement claims-based authorization on your pages. Or if there is data in your application that should remain hidden, you can write logic to hide it from users without certain claims.

If you’re a blazor expert and you have more tips about OIDC authentication to server-side blazor apps, let me know! I’m on Mastodon and Bluesky.

Other resources #

Back to blog