Secure AspNetCore .NET 6 API with Auth0 - Works with SwaggerUI

oliverrc

Oliver Rivett-Carnac

Posted on June 21, 2022

Secure AspNetCore .NET 6 API with Auth0 - Works with SwaggerUI

I am not going into great detail into the inner workings of JWT authentication in AspNetCore.

But to get started we need to add a Nuget package that validates JWT tokens for us which is what Auth0 will return on successful login.

Change your working directory to the project where you want to add the authentication. Then run the following.

dotnet add Microsoft.AspNetCore.Authentication.JwtBearer

Next we will head over to our Program.cs file where we will add the following:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddAuthentication(options =>
    {
        options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
        options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
    })
    .AddJwtBearer(options =>
    {
        options.Authority = "https://<auth0-domain>/";
        options.Audience = "https://<auth0-domain>/userinfo"; // or "<your audience>"
    });
Enter fullscreen mode Exit fullscreen mode

As an illustration I've kept it simple but I advise using AspNetCore's configuration system to store the values.

Essentially, this is going to configure the AspNetCore pipeline to look for an a JWT in the Authorization header arriving who's value is Bearer <auth0 access token jwt>. The library we added is going to validate the JWT from Auth0 against our configuration.

Next we need to add two additional calls.

app.UseAuthentication();
app.UseAuthorization();
Enter fullscreen mode Exit fullscreen mode
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(options =>
{
    options.AddSecurityDefinition("Auth0", new OpenApiSecurityScheme()
    {
        Type = SecuritySchemeType.OAuth2,
        Flows = new OpenApiOAuthFlows
        {
            AuthorizationCode = new OpenApiOAuthFlow()
            {
                // you need to set the urls as they are specific to Auth0
                AuthorizationUrl = new Uri("https://<auth0-domain>/authorize"),
                TokenUrl = new Uri("https://<auth0-domain>/oauth/token"),
                Scopes = new Dictionary<string, string>
                {
                    {"openid", "openid"},
                    {"email", "email"},
                    {"profile", "profile"},
                    // any additional custom scopes you want
                }
            }
        }
    });
    // optional if you are not using [Authorize] attributes
    options.OperationFilter<SecurityRequirementsOperationFilter>();
});
Enter fullscreen mode Exit fullscreen mode

The operation filter is only required if you are using global authorization policies. For example: app.MapControllers().RequireAuthorization(...) I've included the class code at the end.

This next part is largely optional as I've described in the comments below. There are some additional values you can tweak on Swagger UI.

Do watch out for the audience with Auth0. I spent many hours wondering why I was not getting back the right access token, until I saw this:

In addition, if you have chosen to allow users to log in through an Identity Provider (IdP), such as Facebook, the IdP will issue its own access token to allow your application to call the IDP's API. For example, if your user authenticates using Facebook, the access token issued by Facebook can be used to call the Facebook Graph API. These tokens are controlled by the IdP and can be issued in any format. See Identity Provider Access Tokens for details. - Auth0 - Access Tokens

// optional: only want to have Swagger UI available in Development 
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI(options =>
    {
        // optional: just prefills the Swagger UI input boxes, if left off you must supply these values. Would make sense to get these from configuration and better yet user-secrets 
        options.OAuthClientId("<auth0 client id>");
        options.OAuthClientSecret("<auth0 client secret>");
        // optional & gotcha: but if you are using Auth0 with a Social Connection you MUST supply an audience otherwise you will get back the underlying identity providers access token, NOT Auth0's
        options.OAuthAdditionalQueryStringParams(new Dictionary<string, string>
        {
            {"audience", "<your audience>"}
        });
    });
}
Enter fullscreen mode Exit fullscreen mode

Now that everything is setup from a code perspective you can run you API and navigate to /swagger/. Now you should see an Authorize button.

Swagger UI Authorize

You will need to configure Auth0 with the SwaggerUI callback url e.g https://<your api url>/swagger/oauth2-redirect.html

The final Program.cs file should look something like this:
https://gist.github.com/OliverRC/650436fbae77371a55b84c646c35ba3a

Credit

A huge shout out to his article which inspired most of what I've written about here:

https://dotnetcoretutorials.com/2021/02/14/using-auth0-with-an-asp-net-core-api-part-3-swagger/

Addendum - SecurityRequirementsOperationFilter

public class SecurityRequirementsOperationFilter : IOperationFilter
{
    /// <summary>
    /// Applies the this filter on swagger documentation generation.
    /// </summary>
    /// <param name="operation"></param>
    /// <param name="context"></param>
    public void Apply(OpenApiOperation operation, OperationFilterContext context)
    {
        // then check if there is a method-level 'AllowAnonymous', as this overrides any controller-level 'Authorize'
        var anonControllerScope = context
                .MethodInfo
                .DeclaringType?.GetCustomAttributes(true)
                .OfType<AllowAnonymousAttribute>();

        var anonMethodScope = context
                .MethodInfo
                .GetCustomAttributes(true)
                .OfType<AllowAnonymousAttribute>();

        // only add authorization specification information if there is at least one 'Authorize' in the chain and NO method-level 'AllowAnonymous'
        if (anonMethodScope.Any()) return;
        if (anonControllerScope != null && anonControllerScope.Any()) return;

        // add generic message if the controller methods dont already specify the response type
        if (!operation.Responses.ContainsKey("401"))
            operation.Responses.Add("401", new OpenApiResponse { Description = "If Authorization header is not present, the value is empty or value is not a valid jwt bearer token" });
        if (!operation.Responses.ContainsKey("403"))
            operation.Responses.Add("403", new OpenApiResponse { Description = "If user is not authorized to perform requested action" });

        var jwtAuthScheme = new OpenApiSecurityScheme
        {
            Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = "Auth0" }
        };

        operation.Security = new List<OpenApiSecurityRequirement>
        {
            new OpenApiSecurityRequirement
            {
                [ jwtAuthScheme ] = new List<string>()
            }
        };
    }
}
Enter fullscreen mode Exit fullscreen mode
💖 💪 🙅 🚩
oliverrc
Oliver Rivett-Carnac

Posted on June 21, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related