Handling autologin and logout

This commit is contained in:
Zoe Roux 2021-05-09 15:58:55 +02:00
parent d7972704dd
commit 440e5f4f14
7 changed files with 42 additions and 104 deletions

View File

@ -86,6 +86,12 @@ namespace Kyoo.Authentication
IdentityModelEventSource.ShowPII = true; IdentityModelEventSource.ShowPII = true;
services.AddControllers(); services.AddControllers();
// TODO handle direct-videos with bearers (probably add a ?token query param and a app.Use to translate that for videos)
// TODO Support sign-out, check if login work, check if tokens should be stored.
// TODO remove unused/commented code, add documentation.
// services.AddIdentityCore<User>() // services.AddIdentityCore<User>()
// .AddSignInManager() // .AddSignInManager()
@ -157,8 +163,7 @@ namespace Kyoo.Authentication
services.AddAuthorization(options => services.AddAuthorization(options =>
{ {
AuthorizationPolicyBuilder scheme = new(IdentityConstants.ApplicationScheme, AuthorizationPolicyBuilder scheme = new(JwtBearerDefaults.AuthenticationScheme);
JwtBearerDefaults.AuthenticationScheme);
options.DefaultPolicy = scheme.RequireAuthenticatedUser().Build(); options.DefaultPolicy = scheme.RequireAuthenticatedUser().Build();
string[] permissions = {"Read", "Write", "Play", "Admin"}; string[] permissions = {"Read", "Write", "Play", "Admin"};

View File

@ -1,6 +1,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Security.Claims; using System.Security.Claims;
using IdentityModel; using IdentityModel;
using IdentityServer4;
using Kyoo.Models; using Kyoo.Models;
namespace Kyoo.Authentication namespace Kyoo.Authentication
@ -24,16 +25,18 @@ namespace Kyoo.Authentication
new Claim(JwtClaimTypes.Picture, $"api/account/picture/{user.Slug}") new Claim(JwtClaimTypes.Picture, $"api/account/picture/{user.Slug}")
}; };
} }
/// <summary> /// <summary>
/// Convert a user to a ClaimsPrincipal. /// Convert a user to an <see cref="IdentityServerUser"/>.
/// </summary> /// </summary>
/// <param name="user">The user to convert</param> /// <param name="user">The user to convert</param>
/// <returns>A ClaimsPrincipal representing the user</returns> /// <returns>The corresponding identity server user.</returns>
public static ClaimsPrincipal ToPrincipal(this User user) public static IdentityServerUser ToIdentityUser(this User user)
{ {
ClaimsIdentity id = new (user.GetClaims()); return new(user.ID.ToString())
return new ClaimsPrincipal(id); {
DisplayName = user.Username
};
} }
} }
} }

View File

@ -1,47 +0,0 @@
// using System.Threading.Tasks;
// using IdentityServer4.EntityFramework.Entities;
// using IdentityServer4.EntityFramework.Extensions;
// using IdentityServer4.EntityFramework.Interfaces;
// using IdentityServer4.EntityFramework.Options;
// using Kyoo.Models;
// using Microsoft.AspNetCore.Identity;
// using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
// using Microsoft.EntityFrameworkCore;
// using Microsoft.Extensions.Options;
//
// namespace Kyoo
// {
// // The configuration's database is named ConfigurationDbContext.
// public class IdentityDatabase : IdentityDbContext<User>, IPersistedGrantDbContext
// {
// private readonly IOptions<OperationalStoreOptions> _operationalStoreOptions;
//
// public IdentityDatabase(DbContextOptions<IdentityDatabase> options, IOptions<OperationalStoreOptions> operationalStoreOptions)
// : base(options)
// {
// _operationalStoreOptions = operationalStoreOptions;
// }
//
// public DbSet<User> Accounts { get; set; }
//
// protected override void OnModelCreating(ModelBuilder modelBuilder)
// {
// base.OnModelCreating(modelBuilder);
// modelBuilder.ConfigurePersistedGrantContext(_operationalStoreOptions.Value);
//
// modelBuilder.Entity<User>().ToTable("User");
// modelBuilder.Entity<IdentityUserRole<string>>().ToTable("UserRole");
// modelBuilder.Entity<IdentityUserLogin<string>>().ToTable("UserLogin");
// modelBuilder.Entity<IdentityUserClaim<string>>().ToTable("UserClaim");
// modelBuilder.Entity<IdentityRole>().ToTable("UserRoles");
// modelBuilder.Entity<IdentityRoleClaim<string>>().ToTable("UserRoleClaim");
// modelBuilder.Entity<IdentityUserToken<string>>().ToTable("UserToken");
// }
//
// public Task<int> SaveChangesAsync() => base.SaveChangesAsync();
//
// public DbSet<PersistedGrant> PersistedGrants { get; set; }
// public DbSet<DeviceFlowCodes> DeviceFlowCodes { get; set; }
//
// }
// }

View File

@ -1,22 +0,0 @@
// using System;
// using IdentityModel;
// using Microsoft.AspNetCore.Identity;
//
// namespace Kyoo.Models
// {
// public class User : IdentityUser
// {
// public string OTAC { get; set; }
// public DateTime? OTACExpires { get; set; }
//
// public string GenerateOTAC(TimeSpan validFor)
// {
// string otac = CryptoRandom.CreateUniqueId();
// string hashed = otac; // TODO should add a good hashing here.
//
// OTAC = hashed;
// OTACExpires = DateTime.UtcNow.Add(validFor);
// return otac;
// }
// }
// }

View File

@ -5,7 +5,6 @@ using System.IO;
using System.Linq; using System.Linq;
using System.Security.Claims; using System.Security.Claims;
using System.Threading.Tasks; using System.Threading.Tasks;
using IdentityServer4;
using IdentityServer4.Extensions; using IdentityServer4.Extensions;
using IdentityServer4.Models; using IdentityServer4.Models;
using IdentityServer4.Services; using IdentityServer4.Services;
@ -35,15 +34,9 @@ namespace Kyoo.Authentication.Views
/// </summary> /// </summary>
private readonly IUserRepository _users; private readonly IUserRepository _users;
/// <summary> /// <summary>
/// The identity server interaction service to login users.
/// </summary>
// private readonly IIdentityServerInteractionService _interaction;
/// <summary>
/// A file manager to send profile pictures /// A file manager to send profile pictures
/// </summary> /// </summary>
private readonly IFileManager _files; private readonly IFileManager _files;
// private readonly SignInManager<User> _signInManager;
/// <summary> /// <summary>
/// Options about authentication. Those options are monitored and reloads are supported. /// Options about authentication. Those options are monitored and reloads are supported.
/// </summary> /// </summary>
@ -54,20 +47,15 @@ namespace Kyoo.Authentication.Views
/// Create a new <see cref="AccountApi"/> handle to handle login/users requests. /// Create a new <see cref="AccountApi"/> handle to handle login/users requests.
/// </summary> /// </summary>
/// <param name="users">The user repository to create and manage users</param> /// <param name="users">The user repository to create and manage users</param>
/// <param name="interaction">The identity server interaction service to login users.</param>
/// <param name="files">A file manager to send profile pictures</param> /// <param name="files">A file manager to send profile pictures</param>
/// <param name="options">Authentication options (this may be hot reloaded)</param> /// <param name="options">Authentication options (this may be hot reloaded)</param>
public AccountApi(IUserRepository users, public AccountApi(IUserRepository users,
// IIdentityServerInteractionService interaction,
IFileManager files, IFileManager files,
IOptions<AuthenticationOption> options) IOptions<AuthenticationOption> options)
//, SignInManager<User> signInManager)
{ {
_users = users; _users = users;
// _interaction = interaction;
_files = files; _files = files;
_options = options; _options = options;
// _signInManager = signInManager;
} }
@ -119,7 +107,6 @@ namespace Kyoo.Authentication.Views
[HttpPost("login")] [HttpPost("login")]
public async Task<IActionResult> Login([FromBody] LoginRequest login) public async Task<IActionResult> Login([FromBody] LoginRequest login)
{ {
// AuthorizationRequest context = await _interaction.GetAuthorizationContextAsync(login.ReturnURL);
User user = await _users.GetOrDefault(x => x.Username == login.Username); User user = await _users.GetOrDefault(x => x.Username == login.Username);
if (user == null) if (user == null)
@ -127,7 +114,7 @@ namespace Kyoo.Authentication.Views
if (!PasswordUtils.CheckPassword(login.Password, user.Password)) if (!PasswordUtils.CheckPassword(login.Password, user.Password))
return Unauthorized(); return Unauthorized();
// await _signInManager.SignInAsync(user, login.StayLoggedIn); await HttpContext.SignInAsync(user.ToIdentityUser(), StayLogged(login.StayLoggedIn));
return Ok(new { RedirectUrl = login.ReturnURL, IsOk = true }); return Ok(new { RedirectUrl = login.ReturnURL, IsOk = true });
} }
@ -143,20 +130,16 @@ namespace Kyoo.Authentication.Views
User user = (await _users.GetAll()).FirstOrDefault(x => x.ExtraData.GetValueOrDefault("otac") == otac.Otac); User user = (await _users.GetAll()).FirstOrDefault(x => x.ExtraData.GetValueOrDefault("otac") == otac.Otac);
if (user == null) if (user == null)
return Unauthorized(); return Unauthorized();
if (DateTime.ParseExact(user.ExtraData["otac-expire"], "s", CultureInfo.InvariantCulture) <= DateTime.UtcNow) if (DateTime.ParseExact(user.ExtraData["otac-expire"], "s", CultureInfo.InvariantCulture) <=
DateTime.UtcNow)
{
return BadRequest(new return BadRequest(new
{ {
code = "ExpiredOTAC", description = "The OTAC has expired. Try to login with your password." code = "ExpiredOTAC", description = "The OTAC has expired. Try to login with your password."
}); });
}
await HttpContext.SignInAsync(user.ToIdentityUser(), StayLogged(otac.StayLoggedIn));
IdentityServerUser iduser = new(user.ID.ToString())
{
DisplayName = user.Username
};
await HttpContext.SignInAsync(iduser, StayLogged(otac.StayLoggedIn));
// await _signInManager.SignInAsync(user, otac.StayLoggedIn);
return Ok(); return Ok();
} }
@ -167,11 +150,11 @@ namespace Kyoo.Authentication.Views
[Authorize] [Authorize]
public async Task<IActionResult> Logout() public async Task<IActionResult> Logout()
{ {
// await _signInManager.SignOutAsync(); await HttpContext.SignOutAsync();
return Ok(); return Ok();
} }
// TODO check with the extension method /// <inheritdoc />
public async Task GetProfileDataAsync(ProfileDataRequestContext context) public async Task GetProfileDataAsync(ProfileDataRequestContext context)
{ {
User user = await _users.GetOrDefault(int.Parse(context.Subject.GetSubjectId())); User user = await _users.GetOrDefault(int.Parse(context.Subject.GetSubjectId()));
@ -181,12 +164,18 @@ namespace Kyoo.Authentication.Views
context.IssuedClaims.Add(new Claim("permissions", string.Join(',', user.Permissions))); context.IssuedClaims.Add(new Claim("permissions", string.Join(',', user.Permissions)));
} }
/// <inheritdoc />
public async Task IsActiveAsync(IsActiveContext context) public async Task IsActiveAsync(IsActiveContext context)
{ {
User user = await _users.GetOrDefault(int.Parse(context.Subject.GetSubjectId())); User user = await _users.GetOrDefault(int.Parse(context.Subject.GetSubjectId()));
context.IsActive = user != null; context.IsActive = user != null;
} }
/// <summary>
/// Get the user's profile picture.
/// </summary>
/// <param name="slug">The user slug</param>
/// <returns>The profile picture of the user or 404 if not found</returns>
[HttpGet("picture/{slug}")] [HttpGet("picture/{slug}")]
public async Task<IActionResult> GetPicture(string slug) public async Task<IActionResult> GetPicture(string slug)
{ {
@ -197,6 +186,11 @@ namespace Kyoo.Authentication.Views
return _files.FileResult(path); return _files.FileResult(path);
} }
/// <summary>
/// Update profile information (email, username, profile picture...)
/// </summary>
/// <param name="data">The new information</param>
/// <returns>The edited user</returns>
[HttpPut] [HttpPut]
[Authorize] [Authorize]
public async Task<ActionResult<User>> Update([FromForm] AccountUpdateRequest data) public async Task<ActionResult<User>> Update([FromForm] AccountUpdateRequest data)
@ -218,6 +212,10 @@ namespace Kyoo.Authentication.Views
return await _users.Edit(user, false); return await _users.Edit(user, false);
} }
/// <summary>
/// Get permissions for a non connected user.
/// </summary>
/// <returns>The list of permissions of a default user.</returns>
[HttpGet("permissions")] [HttpGet("permissions")]
public ActionResult<IEnumerable<string>> GetDefaultPermissions() public ActionResult<IEnumerable<string>> GetDefaultPermissions()
{ {

View File

@ -19,7 +19,8 @@ namespace Kyoo.Controllers
/// or proxy them from a distant server /// or proxy them from a distant server
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// If no file exists at the given path, you should return a NotFoundResult or handle it gracefully. /// If no file exists at the given path or if the path is null, a NotFoundResult is returned
/// to handle it gracefully.
/// </remarks> /// </remarks>
/// <param name="path">The path of the file.</param> /// <param name="path">The path of the file.</param>
/// <param name="rangeSupport"> /// <param name="rangeSupport">

@ -1 +1 @@
Subproject commit d3a860fa8ffccade9e3b17022482e11c9a18303e Subproject commit a0f8fe4de48a0f0770646d6052a09c551b6442dd