Bearer authentication in Swagger UI, when migrating to Swashbuckle.AspNetCore version 5

56,058

Solution 1

Got this working in the end by trial and error. This is the code that works for me:

c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
{
    Description =
        "JWT Authorization header using the Bearer scheme. \r\n\r\n Enter 'Bearer' [space] and then your token in the text input below.\r\n\r\nExample: \"Bearer 12345abcdef\"",
    Name = "Authorization",
    In = ParameterLocation.Header,
    Type = SecuritySchemeType.ApiKey,
    Scheme = "Bearer"
});

c.AddSecurityRequirement(new OpenApiSecurityRequirement()
{
    {
        new OpenApiSecurityScheme
        {
            Reference = new OpenApiReference
            {
                Type = ReferenceType.SecurityScheme,
                Id = "Bearer"
            },
            Scheme = "oauth2",
            Name = "Bearer",
            In = ParameterLocation.Header,

        },
        new List<string>()
    }
});

I suspect there are probably properties being set there that don't actually need to be explicitly set, but the above is working for me.

Solution 2

OpenAPI 3.0 comes with Bearer authentication, which is a security scheme with type: http and scheme: bearer.

So instead of using an API key scheme you have to set the security scheme type to HTTP Authentication and then define the name of the HTTP Authorization scheme as defined in RFC7235. In this case "bearer".

After you've defined the security scheme you can apply it by adding it as a security requirement.

//First we define the security scheme
c.AddSecurityDefinition("Bearer", //Name the security scheme
    new OpenApiSecurityScheme{
    Description = "JWT Authorization header using the Bearer scheme.",
    Type = SecuritySchemeType.Http, //We set the scheme type to http since we're using bearer authentication
    Scheme = "bearer" //The name of the HTTP Authorization scheme to be used in the Authorization header. In this case "bearer".
});

c.AddSecurityRequirement(new OpenApiSecurityRequirement{ 
    {
        new OpenApiSecurityScheme{
            Reference = new OpenApiReference{
                Id = "Bearer", //The name of the previously defined security scheme.
                Type = ReferenceType.SecurityScheme
            }
        },new List<string>()
    }
});

This omits the need to prefix the token with "Bearer ".

Solution 3

These answers were great in helping me along the path. In my case, I was always just missing one more thing - the SwaggerUI wasn't passing the header name/value I chose (X-API-KEY) to my authentication handler when decorating actions/controllers with [Authorize]. My project uses .NET Core 3.1 and Swashbuckle 5. I made a custom class that inherits IOperationFilter that uses the Swashbuckle.AspNetCore.Filters nuget package below to piggyback off their implementation for oauth2.

// Startup.cs
// ...
services.AddSwaggerGen(options =>
{
  options.SwaggerDoc("v1", new OpenApiInfo { Title = nameof(BoardMinutes), Version = "v1" });

  // Adds authentication to the generated json which is also picked up by swagger.
  options.AddSecurityDefinition(ApiKeyAuthenticationOptions.DefaultScheme, new OpenApiSecurityScheme
  {
      In = ParameterLocation.Header,
      Name = ApiKeyAuthenticationHandler.ApiKeyHeaderName,
      Type = SecuritySchemeType.ApiKey
  });

  options.OperationFilter<ApiKeyOperationFilter>();
});

The key components are the options.AddSecurityDefinition() (I have some open endpoints and didn't want to provide a global filter) as well as options.OperationFilter<ApiKeyOperationFilter>().

// ApiKeyOperationFilter.cs
// ...
internal class ApiKeyOperationFilter : IOperationFilter
{
    public void Apply(OpenApiOperation operation, OperationFilterContext context)
    {
        // Piggy back off of SecurityRequirementsOperationFilter from Swashbuckle.AspNetCore.Filters which has oauth2 as the default security scheme.
        var filter = new SecurityRequirementsOperationFilter(securitySchemaName: ApiKeyAuthenticationOptions.DefaultScheme);
        filter.Apply(operation, context);
    }
}

And finally - for the complete picture here's the authentication handler and authentication options

// ApiKeyAuthenticationOptions.cs
// ... 
public class ApiKeyAuthenticationOptions : AuthenticationSchemeOptions
{
    public const string DefaultScheme = "API Key";
    public string Scheme => DefaultScheme;
    public string AuthenticationType = DefaultScheme;
}

// ApiKeyAuthenticationHandler.cs
// ...
internal class ApiKeyAuthenticationHandler : AuthenticationHandler<ApiKeyAuthenticationOptions>
{
    private const string ProblemDetailsContentType = "application/problem+json";
    public const string ApiKeyHeaderName = "X-Api-Key";

    private readonly IApiKeyService _apiKeyService;
    private readonly ProblemDetailsFactory _problemDetailsFactory;

    public ApiKeyAuthenticationHandler(
        IOptionsMonitor<ApiKeyAuthenticationOptions> options,
        ILoggerFactory logger,
        UrlEncoder encoder,
        ISystemClock clock,
        IApiKeyService apiKeyService,
        ProblemDetailsFactory problemDetailsFactory) : base(options, logger, encoder, clock)
    {
        _apiKeyService = apiKeyService ?? throw new ArgumentNullException(nameof(apiKeyService));
        _problemDetailsFactory = problemDetailsFactory ?? throw new ArgumentNullException(nameof(problemDetailsFactory));
    }

    protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
    {
        if (!Request.Headers.TryGetValue(ApiKeyHeaderName, out var apiKeyHeaderValues))
        {
            return AuthenticateResult.NoResult();
        }

        Guid.TryParse(apiKeyHeaderValues.FirstOrDefault(), out var apiKey);

        if (apiKeyHeaderValues.Count == 0 || apiKey == Guid.Empty)
        {
            return AuthenticateResult.NoResult();
        }

        var existingApiKey = await _apiKeyService.FindApiKeyAsync(apiKey);

        if (existingApiKey == null)
        {
            return AuthenticateResult.Fail("Invalid API Key provided.");
        }

        var claims = new List<Claim>
        {
            new Claim(ClaimTypes.Name, existingApiKey.Owner)
        };

        var identity = new ClaimsIdentity(claims, Options.AuthenticationType);
        var identities = new List<ClaimsIdentity> { identity };
        var principal = new ClaimsPrincipal(identities);
        var ticket = new AuthenticationTicket(principal, Options.Scheme);

        return AuthenticateResult.Success(ticket);
    }

    protected override async Task HandleChallengeAsync(AuthenticationProperties properties)
    {
        Response.StatusCode = StatusCodes.Status401Unauthorized;
        Response.ContentType = ProblemDetailsContentType;
        var problemDetails = _problemDetailsFactory.CreateProblemDetails(Request.HttpContext, StatusCodes.Status401Unauthorized, nameof(HttpStatusCode.Unauthorized),
            detail: "Bad API key.");

        await Response.WriteAsync(JsonSerializer.Serialize(problemDetails));
    }

    protected override async Task HandleForbiddenAsync(AuthenticationProperties properties)
    {
        Response.StatusCode = StatusCodes.Status403Forbidden;
        Response.ContentType = ProblemDetailsContentType;
        var problemDetails = _problemDetailsFactory.CreateProblemDetails(Request.HttpContext, StatusCodes.Status403Forbidden, nameof(HttpStatusCode.Forbidden),
            detail: "This API Key cannot access this resource.");

        await Response.WriteAsync(JsonSerializer.Serialize(problemDetails));
    }
}
Share:
56,058

Related videos on Youtube

tomRedox
Author by

tomRedox

Director and tech evangelist at Redox Software. In my spare time I'm coding or on my bike.

Updated on January 14, 2021

Comments

  • tomRedox
    tomRedox almost 3 years

    I'm trying to migrate from version 4.0.1 to 5.0.0-rc2 of Swashbuckle in a .NET Core 3 Preview 5 Web API project.

    I've got the project compiling and the Swagger UI working, but I can't get Bearer authentication to work, which I think is due to me not setting up the new format security correctly.

    This is my old code that worked in version 4:

    c.AddSecurityDefinition("Bearer", new ApiKeyScheme
    {
        Description = "JWT Authorization header using the Bearer scheme. \r\n\r\n Enter 'Bearer' [space] and then your token in the text input below.\r\n\r\nExample: \"Bearer 12345abcdef\"",
        Name = "Authorization",
        In = "header",
        Type = "apiKey"
    });
    
    var security = new Dictionary<string, IEnumerable<string>>
    {
        {"Bearer", new string[] { }},
    };
    
    c.AddSecurityRequirement(security);
    

    And this is what I've changed it to for v5:

    c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
    {
        Description = "JWT Authorization header using the Bearer scheme. \r\n\r\n Enter 'Bearer' [space] and then your token in the text input below.\r\n\r\nExample: \"Bearer 12345abcdef\"",
        Name = "Authorization",
        In = ParameterLocation.Header,
        Type = SecuritySchemeType.ApiKey,
        Scheme = "tomsAuth"
    });
    
    c.AddSecurityRequirement(new OpenApiSecurityRequirement
    {
        {
            new OpenApiSecurityScheme
            {
                Reference = new OpenApiReference {
                    Type = ReferenceType.SecurityScheme,
                    Id = "tomsAuth" }
            }, new List<string>() }
    });
    

    I think my issue is probably in this part of the code:

            new OpenApiSecurityScheme
            {
                Reference = new OpenApiReference {
                    Type = ReferenceType.SecurityScheme,
                    Id = "tomsAuth" }
            }, new List<string>() }
    

    I think that bit should probably have "Bearer" in it somewhere, but I'm not sure where?

    Additional info

    This is how I'm setting up the JWT authentication in the first place. This code hasn't changed and was working when I was using Swashbuckle 4.0.1:

        var appSettings = appSettingsSection.Get<AppSettings>();
        var key = Encoding.ASCII.GetBytes(appSettings.Secret);
    
        services.AddAuthentication(x =>
        {
            x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
            x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
        })
        .AddJwtBearer(x =>
        {
            x.Events = new JwtBearerEvents
            {
                OnTokenValidated = context =>
                {
                    var userService = context.HttpContext.RequestServices.GetRequiredService<IApiUserService>();
                    var userId = int.Parse(context.Principal.Identity.Name);
                    var user = userService.GetById(userId);
                    if (user == null)
                    {
                        // return unauthorized if user no longer exists
                        context.Fail("Unauthorized");
                    }
    
                    return Task.CompletedTask;
                }
            };
            x.RequireHttpsMetadata = false;
            x.SaveToken = true;
            x.TokenValidationParameters = new TokenValidationParameters
            {
                ValidateIssuerSigningKey = true,
                IssuerSigningKey = new SymmetricSecurityKey(key),
                ValidateIssuer = false,
                ValidateAudience = false
            };
        });
    
  • Daniel
    Daniel about 4 years
    Note that when doing this the text to be pasted in the textfield no longer needs to be prefixed with the string "Bearer "
  • dawson
    dawson almost 4 years
    @Daniel thank you, I kept trying it while still prefixing it with "Bearer "
  • Pieter van der Heijden
    Pieter van der Heijden over 3 years
    Adding property Reference to OpenApiSecurityScheme did the trick for me. Thanks!
  • Evilripper
    Evilripper over 3 years
    This one is the only one that works with jwt standard bearer token.
  • RonRonDK
    RonRonDK almost 3 years
    @Pavlos answer got me going. But on the way I made a weird discovery. If the string "Bearer" is spelled with a capital B anywhere, it's not working. But this has to be a version specific issue, as I see many examples spelled with a capital B. We are using Swashbuckle.AspNetCore v5.4.1.
  • Liam
    Liam over 2 years
    Jeez, what a mess....Is this the best MS could come up with...
  • Kattabomane
    Kattabomane about 2 years
    Still we need to add the word "Bearer" + JWT in the swahbuckle ui. See the answer below from @Pavlos in order to avoid rewriting "Bearer" keyword when using swagger.
  • Patrick Szalapski
    Patrick Szalapski over 1 year
    Is there any way to allow or otherwise set up the ordinary browser redirects that can fetch a bearer token automatically, rather than asking the user for one?
  • Patrick Szalapski
    Patrick Szalapski over 1 year
    Is there any way to allow or otherwise set up the ordinary browser redirects that can fetch a bearer token automatically, rather than asking the user for one?
  • Patrick Szalapski
    Patrick Szalapski over 1 year
    (I think such would require Discovery, right?)
  • Patrick Szalapski
    Patrick Szalapski over 1 year
    How can I modify this to use Open ID Connect Discovery and avoid making the user supply the Bearer token?
  • Patrick Szalapski
    Patrick Szalapski over 1 year
    How can I modify this to use Open ID Connect Discovery and avoid making the user supply the Bearer token?
  • tomRedox
    tomRedox over 1 year
    @PatrickSzalapski I'm not sure on either of your questions in the comments above - it's probably worth adding those as completely new questions altogether on SO?
  • Patrick Szalapski
    Patrick Szalapski over 1 year
  • Patrick Szalapski
    Patrick Szalapski over 1 year
    I've asked more fully at: stackoverflow.com/questions/72187874/…