• Camilo Terevinto

OAuth PKCE flow for ASP.NET Core with Swagger

Updated: Mar 19

In this short blog post, I want to show how the PKCE flow can be set in a Swagger client (through ASP.NET Core), to authenticate with an OpenID Connect server to generate a JWT that can be used to call the ASP.NET Core API.

The important part here is that Swagger is just an example application, albeit a common one, and that the same idea applies to other JavaScript applications (like Angular, React, etc).


Source code

If you want to skip reading and head straight to the sample code, you can find it on my GitHub samples repository.


The OpenID Connect server

There are a lot of OpenID Connect certified implementations, so the following is just an example. I used DuendeSoftware's IdentityServer simply because I really enjoy their implementation, and using the dotnet templates is really quick.


The important bit is the client registration:

new Client
{
    ClientId = "api-swagger",
    RequireClientSecret = false,
    AllowedGrantTypes = GrantTypes.Code,
    RequirePkce = true,
    RedirectUris = { "https://localhost:44301/swagger/oauth2-redirect.html" },
    AllowedCorsOrigins = { "https://localhost:44301" },
    AllowOfflineAccess = true,
    AllowedScopes = { "openid", "profile", "api" }
},

There are many crucial points in that snippet:

  • RequireClientSecret = false -> how you configure this varies greatly per server, but it's important that a public client (one in which users have access to the source code, such as desktop, web and mobile applications) does not use a client secret.

  • AllowedGrantTypes = GrantTypes.Code -> "code" is the only grant type that should be accepted for a public client that uses the PKCE flow.

  • RedirectUris -> the URI that will take care of finalizing the PKCE flow after the user logs in.

  • AllowedCorsOrigins -> for web applications, the Origin/domain that will host the application, this allows CORS access to the /token endpoint.

Missing any of these configurations, or having them incorrectly configured, will prevent you from moving forward in most cases.


The API that hosts Swagger

Let's first look at a sample configuration for the API:

"Auth": {
  "Authority": "https://localhost:44300",
  "Swagger": {
      "AuthorizationUrl": "https://localhost:44300/connect/authorize",
      "TokenUrl": "https://localhost:44300/connect/token"
  }
}

There are, obviously, 3 interesting links there:

  • Authority -> this is used by the API itself, and it's the URL of the OpenID Connect server. Note: this assumes that the OpenID Connect configuration metadata endpoint is under /.well-known/openid-configuration, if this is not the case for your server, you will need to add the extra URL.

  • Swagger's AuthorizationUrl -> this is the endpoint that the Swagger UI client will use to begin the PKCE flow.

  • Swagger's TokenUrl -> this is the endpoint that the Swagger UI client will use to exchange the code for an access token (and/or an id token, depending on the client and server configuration).


API authentication services configuration

The builder configuration for using JWT against the OpenID Connect server is quite simple:

builder.Services
    .AddAuthentication("Bearer")
    .AddJwtBearer("Bearer", options =>
    {
        options.Authority = configuration.GetSection("Auth:Authority").Get<string>();
    });

Unless the OpenID Connect configuration metadata is different (see above), that's all that is required for ASP.NET Core to validate JWT tokens from that server.


Adding Swagger

The next step is to add the Swagger generation services to the pipeline. Since we need to add a security definition and requirement, this is a bit longer.

builder.Services.AddSwaggerGen(options =>
{
    var scheme = new OpenApiSecurityScheme
    {
        In = ParameterLocation.Header,
        Name = "Authorization",
        Flows = new OpenApiOAuthFlows
        {
            AuthorizationCode = new OpenApiOAuthFlow
            {
                AuthorizationUrl = new Uri(configuration.GetSection("Auth:Swagger:AuthorizationUrl").Get<string>()),
                TokenUrl = new Uri(configuration.GetSection("Auth:Swagger:TokenUrl").Get<string>())
            }
        },
        Type = SecuritySchemeType.OAuth2
    };

    options.AddSecurityDefinition("OAuth", scheme);

    options.AddSecurityRequirement(new OpenApiSecurityRequirement
    {
        { 
            new OpenApiSecurityScheme
            {
                Reference = new OpenApiReference { Id = "OAuth", Type = ReferenceType.SecurityScheme }
            }, 
            new List<string> { } 
        }
    });
});

Basically, the above adds the metadata to Swagger so that it knows how to authenticate using the OAuth Authorization Code flow, and send the resulting access token on a header called Authorization (as usual).


Adding Swagger UI

Finally, the last step is to add Swagger UI to the pipeline and configure it to use the PKCE flow.

app.UseSwagger()
    .UseSwaggerUI(options =>
    {
        options.OAuthClientId("api-swagger");
        options.OAuthScopes("profile", "openid", "api");
        options.OAuthUsePkce();
    });

With this, we have the entire configuration done, and if everything was configured correctly, authentication should work for the client and back-end API.


Some Swagger UI extras

The following is not at all mandatory, but I found them recently and I think it's a lot of value added for a very quick change:

app.UseSwagger()
    .UseSwaggerUI(options =>
    {
        // ...
        options.EnablePersistAuthorization();
        options.InjectStylesheet("/content/swagger-extras.css");
    });
   

  • EnablePersistAuthorization -> this should be fairly obvious, but this simple call allows you to persist the authentication details on the client side, so that your users don't have to authenticate each time they open up Swagger. This saves a lot of time during development.

  • InjectStylesheet -> this allows you to include extra css files as you see fit. Of interest here, is that you can use the following simple css to get rid of fields in the authentication form. For example, this gets rid of the client-id and client-secret fields:

.auth-container .wrapper {
    display: none;
}

Seeing the results

The following video shows the process of logging in to the OpenID Connect server and getting a token back from it, which can be used to call the API.


Finishing up

There are many OAuth/OIDC flows, and there is a lot of information online, but there is a lot of dangerously incorrect content online. So, I hope you have found this post useful and that it makes it easier to grasp how to achieve a correct Authorization Code + PKCE flow.


Remember that you can find a sample solution on my GitHub samples repository.

381 views0 comments

Related Posts

See All