Ajout authentification sur l'API

This commit is contained in:
YuukanOO 2021-04-29 14:12:56 +02:00
parent db34091d6b
commit a05110becb
10 changed files with 360 additions and 4 deletions

View File

@ -5,6 +5,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="5.0.5" />
<PackageReference Include="NSwag.AspNetCore" Version="13.10.9" />
<PackageReference Include="NSwag.MSBuild" Version="13.10.9">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>

View File

@ -0,0 +1,94 @@
using System;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Threading.Tasks;
using Infrastructure.Identity;
using Infrastructure.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.IdentityModel.Tokens;
namespace Api.Controllers
{
[Route("api/accounts")]
[ApiController]
[AllowAnonymous]
public class AccountsController : ControllerBase
{
private readonly UserManager<User> _userManager;
private readonly SignInManager<User> _signinManager;
private readonly TokenOptions _options;
private readonly IUserClaimsPrincipalFactory<User> _claimsFactory;
public AccountsController(UserManager<User> userManager, SignInManager<User> signinManager, TokenOptions options, IUserClaimsPrincipalFactory<User> claimsFactory)
{
_userManager = userManager;
_signinManager = signinManager;
_options = options;
_claimsFactory = claimsFactory;
}
[HttpGet("me")]
[ProducesResponseType(StatusCodes.Status200OK)]
public IActionResult Me()
{
return Ok(new
{
Id = User.FindFirstValue(ClaimTypes.NameIdentifier),
Name = User.Identity.Name,
Authenticated = User.Identity.IsAuthenticated,
});
}
[HttpPost]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task<IActionResult> Register(RegisterViewModel cmd)
{
var result = await _userManager.CreateAsync(
new Infrastructure.Identity.User { UserName = cmd.Username }, cmd.Password);
if (!result.Succeeded)
{
return BadRequest();
}
return NoContent();
}
[HttpPost("token")]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(string), StatusCodes.Status200OK)]
public async Task<IActionResult> Login(LoginViewModel cmd)
{
var user = await _userManager.FindByNameAsync(cmd.Username);
if (user == null)
{
return BadRequest();
}
var result = await _signinManager.CheckPasswordSignInAsync(user, cmd.Password, false);
if (!result.Succeeded)
{
return BadRequest();
}
var principal = await _claimsFactory.CreateAsync(user);
var descriptor = new SecurityTokenDescriptor
{
Subject = (ClaimsIdentity)principal.Identity,
Expires = DateTime.UtcNow.AddDays(7),
Issuer = _options.Issuer,
Audience = _options.Audience,
SigningCredentials = new SigningCredentials(_options.Key, SecurityAlgorithms.HmacSha256Signature)
};
var token = new JwtSecurityTokenHandler().CreateEncodedJwt(descriptor);
return Ok(token);
}
}
}

View File

@ -1,5 +1,6 @@
using System;
using Application;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
@ -24,6 +25,7 @@ namespace Api.Controllers
[HttpGet("{id:guid}")]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status200OK)]
[AllowAnonymous]
public CommentDTO Show(Guid id)
{
return _commentService.GetCommentById(id);

View File

@ -1,5 +1,6 @@
using System;
using Application;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
@ -23,6 +24,7 @@ namespace Api.Controllers
/// </summary>
/// <returns></returns>
[HttpGet]
[AllowAnonymous]
public LinkDTO[] GetLatest()
{
return _linkService.GetAllLinks();
@ -36,6 +38,7 @@ namespace Api.Controllers
[HttpGet("{id:guid}")]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status200OK)]
[AllowAnonymous]
public LinkDTO GetById(Guid id)
{
return _linkService.GetLinkById(id);
@ -47,6 +50,7 @@ namespace Api.Controllers
/// <param name="id"></param>
/// <returns></returns>
[HttpGet("{id:guid}/comments")]
[AllowAnonymous]
public CommentDTO[] Comments(Guid id)
{
return _commentService.GetAllLinkComments(id);

View File

@ -4,28 +4,81 @@ using System.Linq;
using System.Threading.Tasks;
using Infrastructure;
using Infrastructure.Filters;
using Infrastructure.Identity;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc.Authorization;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.IdentityModel.Tokens;
namespace Api
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
// For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
public void ConfigureServices(IServiceCollection services)
{
var tokenOptions = Configuration.GetSection("Token").Get<TokenOptions>();
services.AddSingleton(tokenOptions);
// services.AddHNServicesInMemory();
services.AddHNServicesEF();
services
.AddIdentityCore<User>(options =>
{
// FIXME uniquement pour nos besoins :)
options.Password.RequiredLength = options.Password.RequiredUniqueChars = 0;
options.Password.RequireDigit = options.Password.RequireLowercase = options.Password.RequireUppercase = options.Password.RequireNonAlphanumeric = false;
})
.AddRoles<Role>()
.AddSignInManager()
.AddEntityFrameworkStores<HNDbContext>();
services
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = tokenOptions.Issuer,
ValidAudience = tokenOptions.Audience,
IssuerSigningKey = tokenOptions.Key
};
});
services.AddControllers(options =>
{
options.Filters.Add<CustomExceptionFilter>();
options.Filters.Add(new AuthorizeFilter());
});
services.AddOpenApiDocument(doc =>
{
doc.AddSecurity("JWT", Enumerable.Empty<string>(), new NSwag.OpenApiSecurityScheme
{
Type = NSwag.OpenApiSecuritySchemeType.ApiKey,
Name = "Authorization",
In = NSwag.OpenApiSecurityApiKeyLocation.Header,
Description = "Jeton: Bearer {votre jeton}"
});
doc.PostProcess = od =>
{
od.Info.Title = "Hacker News Clone API";
@ -57,6 +110,9 @@ namespace Api
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapGet("/", async context =>

14
Apps/Api/TokenOptions.cs Normal file
View File

@ -0,0 +1,14 @@
using System.Text;
using Microsoft.IdentityModel.Tokens;
namespace Api
{
public class TokenOptions
{
public string Issuer { get; set; }
public string Audience { get; set; }
public string SecurityKey { get; set; }
public SecurityKey Key => new SymmetricSecurityKey(Encoding.UTF8.GetBytes(SecurityKey));
}
}

View File

@ -1,4 +1,5 @@
@url = http://localhost:5000
@token = Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1laWQiOiJkNzlhYzRmZS1hZTEwLTQ5NTEtODJkOS01YjRhNzk3MjNjZTgiLCJ1bmlxdWVfbmFtZSI6InRlc3QiLCJBc3BOZXQuSWRlbnRpdHkuU2VjdXJpdHlTdGFtcCI6IjNSSlRNTFhLTTU3SjJFRUxITURINUlVV1VSNVFJSE9IIiwibmJmIjoxNjE5Njk3NDA1LCJleHAiOjE2MjAzMDIyMDUsImlhdCI6MTYxOTY5NzQwNSwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdCIsImF1ZCI6Imh0dHA6Ly9sb2NhbGhvc3QifQ.f-kr-35Q7h4D4KsRWj2SC2zAUJ-CD9XVAfl8ORFPXqg
GET {{url}}/api/links
@ -6,9 +7,10 @@ GET {{url}}/api/links
POST {{url}}/api/links
Content-Type: application/json
Authorization: {{token}}
{
"url": "http://google.com"
"url": "http://google.com/other"
}
###
@ -28,3 +30,29 @@ Content-Type: application/json
###
GET {{url}}/api/Comments/a8b67787-9e92-4011-b0ed-3688d06043da
###
POST {{url}}/api/accounts
Content-Type: application/json
{
"Username": "toto",
"Password": "test",
"ConfirmPassword": "test"
}
###
POST {{url}}/api/accounts/token
Content-Type: application/json
{
"Username": "test",
"Password": "test"
}
###
GET {{url}}/api/accounts/me
Authorization: {{token}}

View File

@ -9,5 +9,10 @@
"ConnectionStrings": {
"Default": "Data Source=../Website/hn.db"
},
"AllowedHosts": "*"
"AllowedHosts": "*",
"Token": {
"Issuer": "http://localhost",
"Audience": "http://localhost",
"SecurityKey": "tS4wW5bA1cI5iY6j"
}
}

View File

@ -6,6 +6,96 @@
"version": "1.0.0"
},
"paths": {
"/api/accounts/me": {
"get": {
"tags": [
"Accounts"
],
"operationId": "Accounts_Me",
"responses": {
"200": {
"description": ""
}
}
}
},
"/api/accounts": {
"post": {
"tags": [
"Accounts"
],
"operationId": "Accounts_Register",
"requestBody": {
"x-name": "cmd",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/RegisterViewModel"
}
}
},
"required": true,
"x-position": 1
},
"responses": {
"400": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ProblemDetails"
}
}
}
},
"204": {
"description": ""
}
}
}
},
"/api/accounts/token": {
"post": {
"tags": [
"Accounts"
],
"operationId": "Accounts_Login",
"requestBody": {
"x-name": "cmd",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/LoginViewModel"
}
}
},
"required": true,
"x-position": 1
},
"responses": {
"400": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ProblemDetails"
}
}
}
},
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"type": "string"
}
}
}
}
}
}
},
"/api/Comments/{id}": {
"get": {
"tags": [
@ -269,6 +359,47 @@
}
}
},
"RegisterViewModel": {
"type": "object",
"additionalProperties": false,
"required": [
"username",
"password",
"confirmPassword"
],
"properties": {
"username": {
"type": "string",
"minLength": 1
},
"password": {
"type": "string",
"minLength": 1
},
"confirmPassword": {
"type": "string",
"minLength": 1
}
}
},
"LoginViewModel": {
"type": "object",
"additionalProperties": false,
"required": [
"username",
"password"
],
"properties": {
"username": {
"type": "string",
"minLength": 1
},
"password": {
"type": "string",
"minLength": 1
}
}
},
"CommentDTO": {
"type": "object",
"additionalProperties": false,
@ -288,6 +419,10 @@
"downvotesCount": {
"type": "integer",
"format": "int32"
},
"createdByUsername": {
"type": "string",
"nullable": true
}
}
},
@ -337,6 +472,10 @@
"commentsCount": {
"type": "integer",
"format": "int32"
},
"createdByUsername": {
"type": "string",
"nullable": true
}
}
},
@ -354,6 +493,19 @@
}
}
}
},
"securitySchemes": {
"JWT": {
"type": "apiKey",
"description": "Jeton: Bearer {votre jeton}",
"name": "Authorization",
"in": "header"
}
}
},
"security": [
{
"JWT": []
}
]
}

Binary file not shown.