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.
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 2: Using Next.js SPA and Identity Server MVC UI
Flow 3: Using Next.js SPA to request authorization directly from Identity Server APIs
Prerequisites
- .NET SDK: .NET 6.0 (recommended): https://dotnet.microsoft.com/en-us/download/dotnet/6.0
- Node.js: 16.14.2 LTS (recommended): https://nodejs.org/en/
- (Optional) yarn: You can use yarn as an alternative to npm: https://yarnpkg.com/getting-started/install
- NextJs CLI: https://nextjs.org/docs/api-reference/cli
- Your favourite Text IDE
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
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
Now if you run the command dotnet new -l
to list out all the installed templates. you would notice the Duende IdentityServer templates
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
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"),
};
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
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");
});
});
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");
Add the [Authorize] attribute to WeatherForcastController to apply the authorization
[Authorize]
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
// ...
}
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"
}
},
};
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
// 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"}
}
},
}
});
});
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" }
}
});
});
Then we need to tell our SwaggerUI to authorize using PKCE
app.UseSwagger();
app.UseSwaggerUI(options =>
{
options.OAuthUsePkce();
});
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.
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"
},
}
};
- 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",
})
}
// ...
And it’s all set. now start the Identity Server and the Next.js applications to check what we’ve done so far
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!
Top comments (3)
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.
I have been registered there to give you a like - you save my mind from explosion )))
This post was exactly what I was looking for. OAuth and OIDC are important security topics every engineer must be familiar with for developing secure applications. This is the best post that I have seen so far for setting up a dedicated authentication/authorization server for a web application while maintaining separation of concerns.
Thank you @r3lazx for writing this up.