简体   繁体   中英

Use JWT (Authorization: Bearer) in Swagger in ASP.NET Core

I'm creating a REST api in ASP.NET Core 1.0. I was using Swagger to test but now I added JWT authorization for some routes. (with UseJwtBearerAuthentication )

Is it possible to modify the header of the Swagger requests so the routes with the [Authorize] attribute can be tested?

I struggled with the same problem and found a working solution in this blogpost: http://blog.sluijsveld.com/28/01/2016/CustomSwaggerUIField

It comes down to adding this in your configurationoptions

services.ConfigureSwaggerGen(options =>
{
   options.OperationFilter<AuthorizationHeaderParameterOperationFilter>();
});

and the code for the operationfilter

public class AuthorizationHeaderParameterOperationFilter : IOperationFilter
{
   public void Apply(Operation operation, OperationFilterContext context)
   {
      var filterPipeline = context.ApiDescription.ActionDescriptor.FilterDescriptors;
      var isAuthorized = filterPipeline.Select(filterInfo => filterInfo.Filter).Any(filter => filter is AuthorizeFilter);
      var allowAnonymous = filterPipeline.Select(filterInfo => filterInfo.Filter).Any(filter => filter is IAllowAnonymousFilter);

      if (isAuthorized && !allowAnonymous)
      {
          if (operation.Parameters == null)
             operation.Parameters = new List<IParameter>();

          operation.Parameters.Add(new NonBodyParameter
          {                    
             Name = "Authorization",
             In = "header",
             Description = "access token",
             Required = true,
             Type = "string"
         });
      }
   }
}

Then you will see an extra Authorization TextBox in your swagger where you can add your token in the format 'Bearer {jwttoken}' and you should be authorized in your swagger requests.

Currently Swagger has functionality for authentication with JWT-token and can automatically add token into header (I'm using Swashbuckle.AspNetCore 1.1.0).

在此处输入图片说明

The following code should help achieve this.

In the Startup.ConfigureServices():

services.AddSwaggerGen(c =>
{
    // Your custom configuration
    c.SwaggerDoc("v1", new Info { Title = "My API", Version = "v1" });
    c.DescribeAllEnumsAsStrings();
    // JWT-token authentication by password
    c.AddSecurityDefinition("oauth2", new OAuth2Scheme
    {
        Type = "oauth2",
        Flow = "password",
        TokenUrl = Path.Combine(HostingEnvironment.WebRootPath, "/token"),
        // Optional scopes
        //Scopes = new Dictionary<string, string>
        //{
        //    { "api-name", "my api" },
        //}
    });
});

Check and configure TokenUrl if your endpoint is different.

In the Startup.Configure():

app.UseSwagger();
app.UseSwaggerUI(c =>
{
    c.SwaggerEndpoint("/swagger/v1/swagger.json", "API V1");

    // Provide client ID, client secret, realm and application name (if need)

    // Swashbuckle.AspNetCore 4.0.1
    c.OAuthClientId("swagger-ui");
    c.OAuthClientSecret("swagger-ui-secret");
    c.OAuthRealm("swagger-ui-realm");
    c.OAuthAppName("Swagger UI");

    // Swashbuckle.AspNetCore 1.1.0
    // c.ConfigureOAuth2("swagger-ui", "swagger-ui-secret", "swagger-ui-realm", "Swagger UI");
});

If your endpoint for authentication by token follows the OAuth2 standard, all should work. But just in case, I have added sample of this endpoint:

public class AccountController : Controller
{
    [ProducesResponseType(typeof(AccessTokens), (int)HttpStatusCode.OK)]
    [ProducesResponseType((int)HttpStatusCode.BadRequest)]
    [ProducesResponseType((int)HttpStatusCode.Unauthorized)]
    [HttpPost("/token")]
    public async Task<IActionResult> Token([FromForm] LoginModel loginModel)
    {
        switch (loginModel.grant_type)
        {
            case "password":
                var accessTokens = // Authentication logic
                if (accessTokens == null)
                    return BadRequest("Invalid user name or password.");
                return new ObjectResult(accessTokens);

            case "refresh_token":
                var accessTokens = // Refresh token logic
                if (accessTokens == null)
                    return Unauthorized();
                return new ObjectResult(accessTokens);

            default:
                return BadRequest("Unsupported grant type");
        }
    }
}

public class LoginModel
{
    [Required]
    public string grant_type { get; set; }

    public string username { get; set; }
    public string password { get; set; }
    public string refresh_token { get; set; }
    // Optional
    //public string scope { get; set; }
}

public class AccessTokens
{
    public string access_token { get; set; }
    public string refresh_token { get; set; }
    public string token_type { get; set; }
    public int expires_in { get; set; }
}

To expand on HansVG answer which worked for me (thanks) and since I don't have enough contribution points I can't answer emseetea question directly. Once you have the Authorization textbox you will need to call the endpoint that generate the token which will be outside your must [Authorize] area of endpoints.

Once you have called that endpoint to generate the token from the endpoint you can copy it out of the results for that endpoint. Then you have the token to use in your other areas that are must [Authorize]. Just paste it in the textbox. Make sure, as HansVG mentioned, to add it in the correct format, which needs to include "bearer ". Format = "bearer {token}".

Thanks to the Pavel K.'s answer , this is the way I finally resolved this issue in ASP.NET Core 2.2 with Swagger 4.0.1.

In the Startup.cs ConfigureServices():

public void ConfigureServices(IServiceCollection services)
{
    .
    .
    .
    services.AddSwaggerGen(c =>
    {
        c.SwaggerDoc("v1", new Info { Title = "...", Version = "v1" });
        .
        .
        .
        c.AddSecurityDefinition("Bearer", new OAuth2Scheme
        {
            Flow = "password",
            TokenUrl = "/token"
        });

       // It must be here so the Swagger UI works correctly (Swashbuckle.AspNetCore.SwaggerUI, Version=4.0.1.0)
       c.AddSecurityRequirement(new Dictionary<string, IEnumerable<string>>
       {
           {"Bearer", new string[] { }}
       });
    });
    .
    .
    .
}

In the Startup.cs Configure():

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    .
    .
    .
    app.UseSwagger();
    app.UseSwaggerUI(c =>
    {
        c.SwaggerEndpoint("/swagger/v1/swagger.json", "...");
        // Provide client ID, client secret, realm and application name (if need)
        c.OAuthClientId("...");
        c.OAuthClientSecret("...");
        c.OAuthRealm("...");
        c.OAuthAppName("...");
    });
    .
    .
    .
}

And here is how I made an endpoint to give out a JWT token:

[ApiController, Route("[controller]")]
public class TokenController : ControllerBase
{
    [HttpPost, AllowAnonymous]
    public async Task<ActionResult<AccessTokensResponse>> RequestToken([FromForm]LoginRequest request)
    {
        var claims = await ValidateCredentialAndGenerateClaims(request);

        var now = DateTime.UtcNow;
        var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_setting.SecurityKey));
        var signingCredentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256);

        var token = new JwtSecurityToken(
            issuer: _setting.Issuer,
            audience: _setting.Audience,
            claims: claims,
            notBefore: now,
            expires: now.AddMinutes(_setting.ValidDurationInMinute),
            signingCredentials: signingCredentials);

        return Ok(new AccessTokensResponse(token));
    }
}

All your rules and logic on validating user name and password (and/or client_id and clinet_secret) will be in ValidateCredentialAndGenerateClaims() .

If you just wonder, these are my request and response models:

/// <summary>
/// Encapsulates fields for login request.
/// </summary>
/// <remarks>
/// See: https://www.oauth.com/oauth2-servers/access-tokens/
/// </remarks>
public class LoginRequest
{
    [Required]
    public string grant_type { get; set; }
    public string username { get; set; }
    public string password { get; set; }
    public string refresh_token { get; set; }
    public string scope { get; set; }

    public string client_id { get; set; }
    public string client_secret { get; set; }
}

/// <summary>
/// JWT successful response.
/// </summary>
/// <remarks>
/// See: https://www.oauth.com/oauth2-servers/access-tokens/access-token-response/
/// </remarks>
public class AccessTokensResponse
{
    /// <summary>
    /// Initializes a new instance of <seealso cref="AccessTokensResponse"/>.
    /// </summary>
    /// <param name="securityToken"></param>
    public AccessTokensResponse(JwtSecurityToken securityToken)
    {
        access_token = new JwtSecurityTokenHandler().WriteToken(securityToken);
        token_type = "Bearer";
        expires_in = Math.Truncate((securityToken.ValidTo - DateTime.UtcNow).TotalSeconds);
    }

    public string access_token { get; set; }
    public string refresh_token { get; set; }
    public string token_type { get; set; }
    public double expires_in { get; set; }
}

You may add any additional header with API call by using this swagger configuration

// Register the Swagger generator, defining 1 or more Swagger documents
services.AddSwaggerGen(c =>
{
    c.SwaggerDoc("v1", new Info
    {
        Version = "v1",
        Title = "Core API",
        Description = "ASP.NET Core API",
        TermsOfService = "None",
        Contact = new Contact
        {
            Name = "Raj Kumar",
            Email = ""
        },
        License = new License
        {
            Name = "Demo"
        }
    });
    c.AddSecurityDefinition("Bearer", new ApiKeyScheme()
    {
        Description = "JWT Authorization header using the Bearer scheme. Example: \"Authorization: Bearer {token}\"",
        Name = "Authorization",
        In = "header",
        Type = "apiKey"
    });
    c.AddSecurityRequirement(new Dictionary<string, IEnumerable<string>>
    {
    {"Bearer",new string[]{}}
    });
});

在此处输入图片说明

I would also check for AuthorizeAttribute .

var filterDescriptor = context.ApiDescription.ActionDescriptor.FilterDescriptors;

var hasAuthorizedFilter = filterDescriptor.Select(filterInfo => filterInfo.Filter).Any(filter => filter is AuthorizeFilter);
var allowAnonymous = filterDescriptor.Select(filterInfo => filterInfo.Filter).Any(filter => filter is IAllowAnonymousFilter);
var hasAuthorizedAttribute = context.MethodInfo.ReflectedType?.CustomAttributes.First().AttributeType ==
                                     typeof(AuthorizeAttribute);

if ((hasAuthorizedFilter || hasAuthorizedAttribute) && !allowAnonymous)
{
    var oAuthScheme = new OpenApiSecurityScheme
    {
        Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = "Bearer" }
    };

    operation.Security = new List<OpenApiSecurityRequirement>
    {
        new OpenApiSecurityRequirement
        {
            [ oAuthScheme ] = new List<string>()
        }
    };
}

Controller Action:

[Authorize(Policy = AppConfiguration.PermissionReadWrite)]
[Route("api/[controller]")]
[ApiController]
public class FooController : ControllerBase
{
   ...
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM