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; }
}