DEV Community

Cover image for Authenticate Next.js SPA with ASP.NET 6 Identity and Duende Identity Server Part 1
Bao Thanh Nguyen
Bao Thanh Nguyen

Posted on • Updated on

Authenticate Next.js SPA with ASP.NET 6 Identity and Duende Identity Server Part 1

This article was originally posted at my Blog Page

The goal

In this tutorial, we are going to provide step by step to create an API Application and protect it with Duende Identity Server. We are going to implement authorization for Swagger UI and a Next.js SPA application.

Github Repo

To easily follow along with this post, you can look into the Github repo:
SPA Identity Server Authenticate Sample

Solution Structure

Our applications will contain these projects.

Next.js Duende Identity Server Protect API Structure

Authentication Flows

We have 3 authorization flows. In this part of the tutorial, we will cover Flow 1 and Flow 2.

Flow 1: Using Swagger UI

Flow: Using Swagger UI

Flow 2: Using Next.js SPA and Identity Server MVC UI

Flow: Using Next.js SPA and Identity Server MVC UI

Flow 3: Using Next.js SPA to request authorization directly from Identity Server APIs

Flow 3: Using Next.js SPA to request authorizattion directly from Identity Server APIs

Prerequisites

API Project - The resource we need to protect

Create the Solution and the WebAPI project

# Create the folder and navigate into it
mkdir spa_identity_server_authenticate_sample && cd spa_identity_server_authenticate_sample

# Create a new solution to contain our projects
dotnet new sln

# create a new project named WebApiDemo
dotnet new webapi -n WebApiDemo

# Add the created project to our solution
dotnet sln add WebApiDemo\WebApiDemo.csproj
Enter fullscreen mode Exit fullscreen mode

This will create a sample API project for us named WebApiDemo. Which already contains an endpoint GET /WeatherForecast inside the WeatherController.cs file.

You can run the project by executing the command dotnet run (or if you are using Visual Studio Community, set project WebApiDemo as the startup project and then start debugging it)

Then navigate to https://localhost:7101/swagger/index.html which is the default Url of the Swagger UI.
You can test the predefined endpoint. Try it out, and you see that right now we don't need any authorization to fetch the data. This is very risky if we are doing some real-world projects when data is something very important and needs to be protected.

Duende Identity Server Project

Setup Identity Server Project

As ASP.NET suggested in their document. We will use ASP.NET Core Identity for authenticating and combine it with Duende Identity Server for API authorization: https://docs.microsoft.com/en-us/aspnet/core/security/authentication/identity-api-authorization?view=aspnetcore-6.0

Fortunately, Duende Identity Server comes up with a nearly perfect document and supporting templates. The Quickstarts has a template that integrates with ASP.NET Core Identity already. We will use that template to roll out the new project

To use Duende IdentityServer templates, the first thing we have to do is add their templates to our dotnet cli template by using the command:

dotnet new --install Duende.IdentityServer.Templates
Enter fullscreen mode Exit fullscreen mode

Now if you run the command dotnet new -l to list out all the installed templates. you would notice the Duende IdentityServer templates

Duende Identity Server Template CLI

We are going to use the Duende IdentityServer with ASP.NET Core Identity (isaspid) template.
Back to our bash shell, still inside the solution folder

# create the IdentityServer Project named IdentityServerAspNetIdentity
dotnet new isaspid -n IdentityServerAspNetIdentity

# Add the project to our soution to make it appear in Visual Studio Community Explorer
dotnet sln add IdentityServerAspNetIdentity\IdentityServerAspNetIdentity.csproj
Enter fullscreen mode Exit fullscreen mode

The cli will prompt to ask if you want to seed the data. choose “Y” for “yes” if you want to use the default SQLite database. Otherwise, you want to use a different database, simply choose “N” for “no”, update the ConnectionStrings inside applicationsettings.json file, then run the command dotnet run /seed to seed the data again.

The seed action will populate the user database with “alice” and “bob” users. The default passwords are “Pass123$”.

Run the project by executing the command cd IdentityServerAspNetIdentity && dotnet run. The project will run on port 5001 and you can login using alice/Pass123$ or bob/Pass123$.

Try to login and now we have an authenticated session on our IdentityServer, we need to map this session to authorize our API.

Right now we can define the API Scope for our WebApiDemo. This template uses a "code as configuration" approach, i.e. we will config our API directly within the code.
Open the Config.cs file, there are already 3 pre-defined collections: IdentityResources, ApiScopes, and Clients. We are going to modify the ApiScopes and the Clients fields.

Define our scope

// Config.cs
public static IEnumerable<ApiScope> ApiScopes =>
        new ApiScope[]
        {
            new ApiScope("SampleAPI"),
        };
Enter fullscreen mode Exit fullscreen mode

Using Swagger UI to authorize

Back to our WebApiDemo project, we will add the configuration to protect our API first

We will use JWT Bearer scheme to protect our API, and this bearer authentication token is used by the IdentityServer project.
We will add authentication middleware to the pipeline from Microsoft.AspNetCore.Authentication.JwtBearer nuget package.

Run this command in the WebApi folder

# Add JWT Bearer Authentication. We want to authenticate users of our API using tokens issues by the IdentityServer
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer
Enter fullscreen mode Exit fullscreen mode

Then add JWT Bearer authentication services to ASP.NET services collection and configure Bearer as the default Authentication Scheme.
Also we need to configure the Authorization Service to accept only request from the clients which have the API Scope.

// Configure Bearer as the default Authentication Scheme
// Add Jwt Bearer authentication services to the service collection to allow for dependency injection
builder.Services.AddAuthentication("Bearer")
    .AddJwtBearer("Bearer", options => {
                options.Authority = "https://localhost:5001";
// Our API app will call to the IdentityServer to get the authority

        options.TokenValidationParameters = new TokenValidationParameters()
        {
            ValidateAudience = false, // Validate 
        };
        });

builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("ApiScope", policy =>
    {
        policy.RequireAuthenticatedUser();
        policy.RequireClaim("scope", "SampleAPI");
    });
});

Enter fullscreen mode Exit fullscreen mode

SampleAPI is the Scope Id we use to call our API resource

Config the endpoints to require the Authorization which has the "ApiScope" policy

// Program.cs
// Map API Endpoint with the Authorization Policy
app.MapControllers().RequireAuthorization("ApiScope");
Enter fullscreen mode Exit fullscreen mode

Add the [Authorize] attribute to WeatherForcastController to apply the authorization

[Authorize]
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
// ...
}
Enter fullscreen mode Exit fullscreen mode

Run the project and test the endpoint, you will see that this time the request is failed, and the server response with status 401 indicates that the request is not authorized

Define the client for Swagger UI

Back to the Config.cs file. We are going to modify the Client fields

public static IEnumerable<Client> Clients =>
        new List<Client>
            {
                // Swagger client
                new Client
                {
                    ClientId = "api_swagger",
                    ClientName = "Swagger UI for Sample API",
                    ClientSecrets = {new Secret("secret".Sha256())}, // change me!

                    AllowedGrantTypes = GrantTypes.Code,

                    RedirectUris = {"https://localhost:7101/swagger/oauth2-redirect.html"},
                    AllowedCorsOrigins = {"https://localhost:7101"},
                    AllowedScopes = new List<string>
                    {
                        "SampleAPI"
                    }
                },
            };
Enter fullscreen mode Exit fullscreen mode

Adding OAuth Support to Swagger UI.

The WebApiDemo project is using the Swashbuckle nuget package to build the API documentation.
We're going to modify the SwaggerGen service.

We have to describe how our API is secured by defining one or more security schemes. We are securing our API using the OAuth2 scheme. Since our Swagger UI is going to run in the end-user’s browser, and access tokens are going to be required by JavaScript running in that browser. So we will define an OAuth2 Authorization Code Flow

We need to tell Swashbuckle the location of our authorization Url, token endpoint Url and what scopes it is going to use. You can get these 2 links through the IdentityServer disco document (start your IdentityServer, locate to the Url https://localhost:5001/.well-known/openid-configuration. You may need a JSON reader to read the JSON data, I’m using the JSON formatter Chrome extension
Local Identity Server disco doc

// Program.cs
builder.Services.AddSwaggerGen(options =>
{
    options.AddSecurityDefinition("oauth2", new OpenApiSecurityScheme
    {
        Type = SecuritySchemeType.OAuth2,
        Flows = new OpenApiOAuthFlows
        {
            AuthorizationCode = new OpenApiOAuthFlow
            {
                AuthorizationUrl = new Uri("https://localhost:5001/connect/authorize"),
                TokenUrl = new Uri("https://localhost:5001/connect/token"),
                Scopes = new Dictionary<string, string>
                            {
                                {"SampleAPI", "API - full access"}
                            }
            },
        }
    });
});
Enter fullscreen mode Exit fullscreen mode

Next, we need to indicate which operations the scheme is applicable to. You can apply the scheme globally using the AddSecurityRequirement method.

In the end, our Swagger Service definition would look like

// Program.cs
builder.Services.AddSwaggerGen(options =>
{
    // Scheme Definition 
        options.AddSecurityDefinition("oauth2", new OpenApiSecurityScheme
    {
        Type = SecuritySchemeType.OAuth2,
        Flows = new OpenApiOAuthFlows
        {
            AuthorizationCode = new OpenApiOAuthFlow
            {
                AuthorizationUrl = new Uri("https://localhost:5001/connect/authorize"),
                TokenUrl = new Uri("https://localhost:5001/connect/token"),
                Scopes = new Dictionary<string, string>
                            {
                                {"SampleAPI", "Sample API - full access"}
                            }
            },
        }
    });

        // Apply Scheme globally
        options.AddSecurityRequirement(new OpenApiSecurityRequirement
    {
        {
            new OpenApiSecurityScheme
            {
                Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = "oauth2" }
            },
            new[] { "SampleAPI" }
        }
    });
});
Enter fullscreen mode Exit fullscreen mode

Then we need to tell our SwaggerUI to authorize using PKCE

app.UseSwagger();
    app.UseSwaggerUI(options =>
    {
        options.OAuthUsePkce();
    });
Enter fullscreen mode Exit fullscreen mode

And that’s it. now you start authorizing using Swagger UI and call the Sample API endpoint’s directly inside your development environment.

Start the two projects and test the functions. you will notice we have a new Authorize button inside Swagger UI.
Swagger UI Authorize popup

Input the client_id and client_secret, which we’ve specified inside the IdentityServer config file (should be api_swagger/secret. Also, remember to tick on the SampleAPI scope.

You will be navigated to the Identity Server MVC UI for login, input alice/Pass123$ to login then the Application will redirect you back to the Swagger application.

Try it out on the endpoint then you should be able to get the response data.

Setup our main application - Next.js SPA Project

If you get through the process of authenticating SwaggerUI successfully, then implementing authentication for Next.js will not trouble you. They’re the same 2 steps, configure the client inside IdentityServer then add the Next.js Client to connect with it.

Adding Next.js Client configuration in IdentityServer

// Config.cs
public static IEnumerable<Client> Clients =>
        new List<Client>
            {
                // SwaggerUI Client 
                // ...

                // NextJs client
                new Client
                {
                    ClientId = "nextjs_web_app",
                    ClientName = "NextJs Web App",
                    ClientSecrets = {new Secret("secret".Sha256())}, // change me!
                    AllowedGrantTypes =  new[] { GrantType.AuthorizationCode },

                    // where to redirect to after login
                    RedirectUris = { "http://localhost:3000/api/auth/callback/sample-identity-server" },
                    // where to redirect to after logout
                    PostLogoutRedirectUris = { "http://localhost:3000" },
                    AllowedCorsOrigins= { "http://localhost:3000" },

                    AllowedScopes = new List<string>
                    {
                        IdentityServerConstants.StandardScopes.OpenId,
                        IdentityServerConstants.StandardScopes.Profile,
                        "SampleAPI"
                    },
                }
            };
Enter fullscreen mode Exit fullscreen mode
  • Our Next.js is going to run on port 3000, so I set the CORS origin to allow the request from http://localhost:3000. PostLogoutRedirectUris is an optional
  • RedirectUris is a list of allowing redirect Uri, the current value is the default url we will config using NextAuth.js

Create the Next.js SPA client

Next.js community has an awesome open source project helping us authenticate called NextAuth.js.

It has many built-in support for popular services including IdentityServer4 (Predecessor of Duende IdentityServer, and we’re going to use that provider to connect with our IdentityServer)

We’ll start with its Getting Started Typescript Example

Create a folder name apps inside our solution folder and then place the example into it. Open the folder using your favorite Text IDE

Run the command npm install to install the dependencies.

Copy the .env.local.example file in the directory to .env. Next.js will get the environment variables from this file when starting the application.

Inside the .env file, at the end, add this line NODE_TLS_REJECT_UNAUTHORIZED=0 to make Next.js bypass SSL checking in localhost.

This template already added a [...nextauth].ts file for you inside pages/api/auth folder. There’re several default providers, feel free to remove them.

We are going to add our IdentityServer4 Provider, take a look at the NextAuth.js documentation about how to config it.

Simply, we just have to add this into the provider array:

import IdentityServer4Provider from 'next-auth/providers/identity-server4'
// ...
providers: [
  IdentityServer4Provider({
      id: "demo-identity-server",
      name: "Demo IdentityServer",
      authorization: { params: { scope: "openid profile SampleAPI" } },
      issuer:  "https://localhost:5001/",
      clientId: "nextjs_web_app",
      clientSecret: "secret",
  })
}
// ...

Enter fullscreen mode Exit fullscreen mode

And it’s all set. now start the Identity Server and the Next.js applications to check what we’ve done so far

Next.js successfully authorization

Summary

That's it. We could connect call a protected endpoint from our Next.js SPA. But it's still not perfect. In the real world, there's sometimes you don't want to stay on the same page when doing the authorization. And that is Flow 3 that we will cover in the next article.

Thanks for reading!

References

Discussion (1)

Collapse
pedrosphericode profile image
Pedro Pereira • Edited on

That's a great article, but I couldn't make it work. The WeatherForecast get method would respond with an http 401 even after I complete the authorization process with success (inform client id/secret; been redirected to IS login screen; inform user/password). Also, after updating all NuGet packages, the Swagger UI would not show the "client_secret" field anymore. I've added a RequireClientSecret = false to the client configuration. The authorization process works fine, but I'm still getting an http 401. Would you mind updating this article using the updated version of all NuGet packages within this solution? Thanks.

-- UPDATE
Never mind, I've found what I did wrong.
I was missing an app.UseAuthentication(); right before the app.UseAuthorization();. Also, I have put all the auth config before the default template services. Not sure it makes any difference though.