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>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="5.0.5" />
<PackageReference Include="NSwag.AspNetCore" Version="13.10.9" /> <PackageReference Include="NSwag.AspNetCore" Version="13.10.9" />
<PackageReference Include="NSwag.MSBuild" Version="13.10.9"> <PackageReference Include="NSwag.MSBuild" Version="13.10.9">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <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 System;
using Application; using Application;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
@ -24,6 +25,7 @@ namespace Api.Controllers
[HttpGet("{id:guid}")] [HttpGet("{id:guid}")]
[ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[AllowAnonymous]
public CommentDTO Show(Guid id) public CommentDTO Show(Guid id)
{ {
return _commentService.GetCommentById(id); return _commentService.GetCommentById(id);

View File

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

View File

@ -4,28 +4,81 @@ using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Infrastructure; using Infrastructure;
using Infrastructure.Filters; using Infrastructure.Filters;
using Infrastructure.Identity;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc.Authorization;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
using Microsoft.IdentityModel.Tokens;
namespace Api namespace Api
{ {
public class Startup 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. // 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 // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
public void ConfigureServices(IServiceCollection services) public void ConfigureServices(IServiceCollection services)
{ {
var tokenOptions = Configuration.GetSection("Token").Get<TokenOptions>();
services.AddSingleton(tokenOptions);
// services.AddHNServicesInMemory(); // services.AddHNServicesInMemory();
services.AddHNServicesEF(); 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 => services.AddControllers(options =>
{ {
options.Filters.Add<CustomExceptionFilter>(); options.Filters.Add<CustomExceptionFilter>();
options.Filters.Add(new AuthorizeFilter());
}); });
services.AddOpenApiDocument(doc => 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 => doc.PostProcess = od =>
{ {
od.Info.Title = "Hacker News Clone API"; od.Info.Title = "Hacker News Clone API";
@ -57,6 +110,9 @@ namespace Api
app.UseRouting(); app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints => app.UseEndpoints(endpoints =>
{ {
endpoints.MapGet("/", async context => 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 @url = http://localhost:5000
@token = Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1laWQiOiJkNzlhYzRmZS1hZTEwLTQ5NTEtODJkOS01YjRhNzk3MjNjZTgiLCJ1bmlxdWVfbmFtZSI6InRlc3QiLCJBc3BOZXQuSWRlbnRpdHkuU2VjdXJpdHlTdGFtcCI6IjNSSlRNTFhLTTU3SjJFRUxITURINUlVV1VSNVFJSE9IIiwibmJmIjoxNjE5Njk3NDA1LCJleHAiOjE2MjAzMDIyMDUsImlhdCI6MTYxOTY5NzQwNSwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdCIsImF1ZCI6Imh0dHA6Ly9sb2NhbGhvc3QifQ.f-kr-35Q7h4D4KsRWj2SC2zAUJ-CD9XVAfl8ORFPXqg
GET {{url}}/api/links GET {{url}}/api/links
@ -6,9 +7,10 @@ GET {{url}}/api/links
POST {{url}}/api/links POST {{url}}/api/links
Content-Type: application/json Content-Type: application/json
Authorization: {{token}}
{ {
"url": "http://google.com" "url": "http://google.com/other"
} }
### ###
@ -27,4 +29,30 @@ Content-Type: application/json
### ###
GET {{url}}/api/Comments/a8b67787-9e92-4011-b0ed-3688d06043da 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": { "ConnectionStrings": {
"Default": "Data Source=../Website/hn.db" "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" "version": "1.0.0"
}, },
"paths": { "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}": { "/api/Comments/{id}": {
"get": { "get": {
"tags": [ "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": { "CommentDTO": {
"type": "object", "type": "object",
"additionalProperties": false, "additionalProperties": false,
@ -288,6 +419,10 @@
"downvotesCount": { "downvotesCount": {
"type": "integer", "type": "integer",
"format": "int32" "format": "int32"
},
"createdByUsername": {
"type": "string",
"nullable": true
} }
} }
}, },
@ -337,6 +472,10 @@
"commentsCount": { "commentsCount": {
"type": "integer", "type": "integer",
"format": "int32" "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.