Add authentication documentation to the swagger

This commit is contained in:
Zoe Roux 2022-05-09 22:50:28 +02:00
parent d257901c64
commit b5821b0d02
No known key found for this signature in database
GPG Key ID: 54F19BB73170955D
10 changed files with 63 additions and 71 deletions

View File

@ -0,0 +1,31 @@
// Kyoo - A portable and vast media library solution.
// Copyright (c) Kyoo.
//
// See AUTHORS.md and LICENSE file in the project root for full license information.
//
// Kyoo is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
//
// Kyoo is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
using System;
namespace Kyoo.Abstractions.Models.Permissions
{
/// <summary>
/// The annotated route can only be accessed by a logged in user.
/// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true)]
public class UserOnlyAttribute : Attribute
{
// TODO: Implement a Filter Attribute to make this work. For now, this attribute is only useful as documentation.
}
}

View File

@ -18,8 +18,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Text; using System.Text;
using Autofac; using Autofac;
using Kyoo.Abstractions; using Kyoo.Abstractions;
@ -27,10 +25,8 @@ using Kyoo.Abstractions.Controllers;
using Kyoo.Authentication.Models; using Kyoo.Authentication.Models;
using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.FileProviders;
using Microsoft.IdentityModel.Tokens; using Microsoft.IdentityModel.Tokens;
namespace Kyoo.Authentication namespace Kyoo.Authentication
@ -105,25 +101,7 @@ namespace Kyoo.Authentication
/// <inheritdoc /> /// <inheritdoc />
public IEnumerable<IStartupAction> ConfigureSteps => new IStartupAction[] public IEnumerable<IStartupAction> ConfigureSteps => new IStartupAction[]
{ {
SA.New<IApplicationBuilder>(app =>
{
PhysicalFileProvider provider = new(Path.Combine(
Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!,
"login"));
app.UseDefaultFiles(new DefaultFilesOptions
{
RequestPath = new PathString("/login"),
FileProvider = provider,
RedirectToAppendTrailingSlash = true
});
app.UseStaticFiles(new StaticFileOptions
{
RequestPath = new PathString("/login"),
FileProvider = provider
});
}, SA.StaticFiles),
SA.New<IApplicationBuilder>(app => app.UseAuthentication(), SA.Authentication), SA.New<IApplicationBuilder>(app => app.UseAuthentication(), SA.Authentication),
// SA.New<IApplicationBuilder>(app => app.UseAuthorization(), SA.Authorization)
}; };
} }
} }

View File

@ -18,7 +18,6 @@
using System; using System;
using System.Threading.Tasks; using System.Threading.Tasks;
using JetBrains.Annotations;
using Kyoo.Abstractions.Models; using Kyoo.Abstractions.Models;
using Microsoft.IdentityModel.Tokens; using Microsoft.IdentityModel.Tokens;
@ -35,14 +34,14 @@ namespace Kyoo.Authentication
/// <param name="user">The user to create a token for.</param> /// <param name="user">The user to create a token for.</param>
/// <param name="expireIn">When this token will expire.</param> /// <param name="expireIn">When this token will expire.</param>
/// <returns>A new, valid access token.</returns> /// <returns>A new, valid access token.</returns>
string CreateAccessToken([NotNull] User user, out TimeSpan expireIn); string CreateAccessToken(User user, out TimeSpan expireIn);
/// <summary> /// <summary>
/// Create a new refresh token for the given user. /// Create a new refresh token for the given user.
/// </summary> /// </summary>
/// <param name="user">The user to create a token for.</param> /// <param name="user">The user to create a token for.</param>
/// <returns>A new, valid refresh token.</returns> /// <returns>A new, valid refresh token.</returns>
Task<string> CreateRefreshToken([NotNull] User user); Task<string> CreateRefreshToken(User user);
/// <summary> /// <summary>
/// Check if the given refresh token is valid and if it is, retrieve the id of the user this token belongs to. /// Check if the given refresh token is valid and if it is, retrieve the id of the user this token belongs to.

View File

@ -105,7 +105,7 @@ namespace Kyoo.Authentication
}, },
expires: DateTime.UtcNow.AddYears(1) expires: DateTime.UtcNow.AddYears(1)
); );
// TODO refresh keys are unique (thanks to the guid) but we could store them in DB to invalidate them if requested by the user. // TODO: refresh keys are unique (thanks to the guid) but we could store them in DB to invalidate them if requested by the user.
return Task.FromResult(new JwtSecurityTokenHandler().WriteToken(token)); return Task.FromResult(new JwtSecurityTokenHandler().WriteToken(token));
} }

View File

@ -13,11 +13,4 @@
<ProjectReference Include="../Kyoo.Abstractions/Kyoo.Abstractions.csproj" /> <ProjectReference Include="../Kyoo.Abstractions/Kyoo.Abstractions.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<None Remove="$(LoginRoot)**;" />
<Content Include="$(LoginRoot)**" Visible="false">
<Link>login/%(RecursiveDir)%(Filename)%(Extension)</Link>
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup>
</Project> </Project>

View File

@ -49,7 +49,7 @@ namespace Kyoo.Authentication
public string RefreshToken { get; set; } public string RefreshToken { get; set; }
/// <summary> /// <summary>
/// When the access token will expire. After this tume, the refresh token should be used to retrieve. /// When the access token will expire. After this time, the refresh token should be used to retrieve.
/// a new token.cs /// a new token.cs
/// </summary> /// </summary>
[JsonProperty("expire_in")] [JsonProperty("expire_in")]

View File

@ -23,9 +23,9 @@ using Kyoo.Abstractions.Controllers;
using Kyoo.Abstractions.Models; using Kyoo.Abstractions.Models;
using Kyoo.Abstractions.Models.Attributes; using Kyoo.Abstractions.Models.Attributes;
using Kyoo.Abstractions.Models.Exceptions; using Kyoo.Abstractions.Models.Exceptions;
using Kyoo.Abstractions.Models.Permissions;
using Kyoo.Abstractions.Models.Utils; using Kyoo.Abstractions.Models.Utils;
using Kyoo.Authentication.Models.DTO; using Kyoo.Authentication.Models.DTO;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.IdentityModel.Tokens; using Microsoft.IdentityModel.Tokens;
@ -84,7 +84,7 @@ namespace Kyoo.Authentication.Views
/// <response code="403">The user and password does not match.</response> /// <response code="403">The user and password does not match.</response>
[HttpPost("login")] [HttpPost("login")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))] [ProducesResponseType(StatusCodes.Status403Forbidden, Type = typeof(RequestError))]
public async Task<ActionResult<JwtToken>> Login([FromBody] LoginRequest request) public async Task<ActionResult<JwtToken>> Login([FromBody] LoginRequest request)
{ {
User user = await _users.GetOrDefault(x => x.Username == request.Username); User user = await _users.GetOrDefault(x => x.Username == request.Username);
@ -139,10 +139,11 @@ namespace Kyoo.Authentication.Views
/// </remarks> /// </remarks>
/// <param name="token">A valid refresh token.</param> /// <param name="token">A valid refresh token.</param>
/// <returns>A new access and refresh token.</returns> /// <returns>A new access and refresh token.</returns>
/// <response code="400">The given refresh token is invalid.</response> /// <response code="403">The given refresh token is invalid.</response>
[HttpGet("refresh")] [HttpGet("refresh")]
[UserOnly]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))] [ProducesResponseType(StatusCodes.Status403Forbidden, Type = typeof(RequestError))]
public async Task<ActionResult<JwtToken>> Refresh([FromQuery] string token) public async Task<ActionResult<JwtToken>> Refresh([FromQuery] string token)
{ {
try try
@ -157,11 +158,11 @@ namespace Kyoo.Authentication.Views
} }
catch (ItemNotFoundException) catch (ItemNotFoundException)
{ {
return BadRequest(new RequestError("Invalid refresh token.")); return Forbid(new RequestError("Invalid refresh token."));
} }
catch (SecurityTokenException ex) catch (SecurityTokenException ex)
{ {
return BadRequest(new RequestError(ex.Message)); return Forbid(new RequestError(ex.Message));
} }
} }
@ -175,6 +176,7 @@ namespace Kyoo.Authentication.Views
/// <returns>The currently authenticated user.</returns> /// <returns>The currently authenticated user.</returns>
/// <response code="403">The given access token is invalid.</response> /// <response code="403">The given access token is invalid.</response>
[HttpGet("me")] [HttpGet("me")]
[UserOnly]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status403Forbidden)]
public async Task<ActionResult<User>> GetMe() public async Task<ActionResult<User>> GetMe()
@ -204,6 +206,7 @@ namespace Kyoo.Authentication.Views
/// <returns>The currently authenticated user after modifications.</returns> /// <returns>The currently authenticated user after modifications.</returns>
/// <response code="403">The given access token is invalid.</response> /// <response code="403">The given access token is invalid.</response>
[HttpPut("me")] [HttpPut("me")]
[UserOnly]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status403Forbidden)]
public async Task<ActionResult<User>> EditMe(User user, [FromQuery] bool resetOld = true) public async Task<ActionResult<User>> EditMe(User user, [FromQuery] bool resetOld = true)
@ -230,6 +233,7 @@ namespace Kyoo.Authentication.Views
/// <returns>The currently authenticated user after modifications.</returns> /// <returns>The currently authenticated user after modifications.</returns>
/// <response code="403">The given access token is invalid.</response> /// <response code="403">The given access token is invalid.</response>
[HttpDelete("me")] [HttpDelete("me")]
[UserOnly]
[ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status403Forbidden)]
public async Task<ActionResult<User>> DeleteMe() public async Task<ActionResult<User>> DeleteMe()

View File

@ -37,7 +37,7 @@ namespace Kyoo.Core.Api
/// </summary> /// </summary>
[Route("subtitles")] [Route("subtitles")]
[Route("subtitle", Order = AlternativeRoute)] [Route("subtitle", Order = AlternativeRoute)]
[PartialPermission(nameof(SubtitleApi))] [PartialPermission("subtitle")]
[ApiController] [ApiController]
[ApiDefinition("Subtitles", Group = WatchGroup)] [ApiDefinition("Subtitles", Group = WatchGroup)]
public class SubtitleApi : ControllerBase public class SubtitleApi : ControllerBase

View File

@ -16,6 +16,7 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>. // along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
@ -36,12 +37,19 @@ namespace Kyoo.Swagger
public bool Process(OperationProcessorContext context) public bool Process(OperationProcessorContext context)
{ {
context.OperationDescription.Operation.Security ??= new List<OpenApiSecurityRequirement>(); context.OperationDescription.Operation.Security ??= new List<OpenApiSecurityRequirement>();
OpenApiSecurityRequirement perms = context.MethodInfo.GetCustomAttributes<PermissionAttribute>() OpenApiSecurityRequirement perms = context.MethodInfo.GetCustomAttributes<UserOnlyAttribute>()
.Aggregate(new OpenApiSecurityRequirement(), (agg, cur) => .Aggregate(new OpenApiSecurityRequirement(), (agg, cur) =>
{
agg[nameof(Kyoo)] = Array.Empty<string>();
return agg;
});
perms = context.MethodInfo.GetCustomAttributes<PermissionAttribute>()
.Aggregate(perms, (agg, cur) =>
{ {
ICollection<string> permissions = _GetPermissionsList(agg, cur.Group); ICollection<string> permissions = _GetPermissionsList(agg, cur.Group);
permissions.Add($"{cur.Type}.{cur.Kind.ToString().ToLower()}"); permissions.Add($"{cur.Type}.{cur.Kind.ToString().ToLower()}");
agg[cur.Group.ToString()] = permissions; agg[nameof(Kyoo)] = permissions;
return agg; return agg;
}); });
@ -61,7 +69,7 @@ namespace Kyoo.Swagger
: cur.Kind; : cur.Kind;
ICollection<string> permissions = _GetPermissionsList(agg, group); ICollection<string> permissions = _GetPermissionsList(agg, group);
permissions.Add($"{type}.{kind.ToString().ToLower()}"); permissions.Add($"{type}.{kind.ToString().ToLower()}");
agg[group.ToString()] = permissions; agg[nameof(Kyoo)] = permissions;
return agg; return agg;
}); });
} }
@ -70,7 +78,7 @@ namespace Kyoo.Swagger
return true; return true;
} }
private ICollection<string> _GetPermissionsList(OpenApiSecurityRequirement security, Group group) private static ICollection<string> _GetPermissionsList(OpenApiSecurityRequirement security, Group group)
{ {
return security.TryGetValue(group.ToString(), out IEnumerable<string> perms) return security.TryGetValue(group.ToString(), out IEnumerable<string> perms)
? perms.ToList() ? perms.ToList()

View File

@ -120,35 +120,14 @@ namespace Kyoo.Swagger
})); }));
document.SchemaProcessors.Add(new ThumbnailProcessor()); document.SchemaProcessors.Add(new ThumbnailProcessor());
document.AddSecurity("Kyoo", new OpenApiSecurityScheme document.AddSecurity(nameof(Kyoo), new OpenApiSecurityScheme
{ {
Type = OpenApiSecuritySchemeType.OpenIdConnect, Type = OpenApiSecuritySchemeType.Http,
OpenIdConnectUrl = "/.well-known/openid-configuration", Scheme = "Bearer",
Description = "You can login via an OIDC client, clients must be first registered in kyoo. " + BearerFormat = "JWT",
"Documentation coming soon." Description = "The user's bearer"
}); });
document.OperationProcessors.Add(new OperationPermissionProcessor()); document.OperationProcessors.Add(new OperationPermissionProcessor());
// This does not respect the swagger's specification but it works for swaggerUi and ReDoc so honestly this will do.
document.AddSecurity(Group.Overall.ToString(), new OpenApiSecurityScheme
{
ExtensionData = new Dictionary<string, object>
{
["type"] = "OpenID Connect or Api Key"
},
Description = "Kyoo's permissions work by groups. Permissions are attributed to " +
"a specific group and if a user has a group permission, it will be the same as having every " +
"permission in the group. For example, having overall.read gives you collections.read, " +
"shows.read and so on."
});
document.AddSecurity(Group.Admin.ToString(), new OpenApiSecurityScheme
{
ExtensionData = new Dictionary<string, object>
{
["type"] = "OpenID Connect or Api Key"
},
Description = "The permission group used for administrative items like tasks, account management " +
"and library creation."
});
}); });
} }