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.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Text;
using Autofac;
using Kyoo.Abstractions;
@ -27,10 +25,8 @@ using Kyoo.Abstractions.Controllers;
using Kyoo.Authentication.Models;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.FileProviders;
using Microsoft.IdentityModel.Tokens;
namespace Kyoo.Authentication
@ -105,25 +101,7 @@ namespace Kyoo.Authentication
/// <inheritdoc />
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.UseAuthorization(), SA.Authorization)
};
}
}

View File

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

View File

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

View File

@ -49,7 +49,7 @@ namespace Kyoo.Authentication
public string RefreshToken { get; set; }
/// <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
/// </summary>
[JsonProperty("expire_in")]

View File

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

View File

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

View File

@ -16,6 +16,7 @@
// 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;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
@ -36,12 +37,19 @@ namespace Kyoo.Swagger
public bool Process(OperationProcessorContext context)
{
context.OperationDescription.Operation.Security ??= new List<OpenApiSecurityRequirement>();
OpenApiSecurityRequirement perms = context.MethodInfo.GetCustomAttributes<PermissionAttribute>()
OpenApiSecurityRequirement perms = context.MethodInfo.GetCustomAttributes<UserOnlyAttribute>()
.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);
permissions.Add($"{cur.Type}.{cur.Kind.ToString().ToLower()}");
agg[cur.Group.ToString()] = permissions;
agg[nameof(Kyoo)] = permissions;
return agg;
});
@ -61,7 +69,7 @@ namespace Kyoo.Swagger
: cur.Kind;
ICollection<string> permissions = _GetPermissionsList(agg, group);
permissions.Add($"{type}.{kind.ToString().ToLower()}");
agg[group.ToString()] = permissions;
agg[nameof(Kyoo)] = permissions;
return agg;
});
}
@ -70,7 +78,7 @@ namespace Kyoo.Swagger
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)
? perms.ToList()

View File

@ -120,35 +120,14 @@ namespace Kyoo.Swagger
}));
document.SchemaProcessors.Add(new ThumbnailProcessor());
document.AddSecurity("Kyoo", new OpenApiSecurityScheme
document.AddSecurity(nameof(Kyoo), new OpenApiSecurityScheme
{
Type = OpenApiSecuritySchemeType.OpenIdConnect,
OpenIdConnectUrl = "/.well-known/openid-configuration",
Description = "You can login via an OIDC client, clients must be first registered in kyoo. " +
"Documentation coming soon."
Type = OpenApiSecuritySchemeType.Http,
Scheme = "Bearer",
BearerFormat = "JWT",
Description = "The user's bearer"
});
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."
});
});
}