diff --git a/back/src/Kyoo.Abstractions/Models/Resources/User.cs b/back/src/Kyoo.Abstractions/Models/Resources/User.cs
index 2f049b7f..6afeccb8 100644
--- a/back/src/Kyoo.Abstractions/Models/Resources/User.cs
+++ b/back/src/Kyoo.Abstractions/Models/Resources/User.cs
@@ -100,14 +100,19 @@ namespace Kyoo.Abstractions.Models
public string Id { get; set; }
///
- /// A jwt token used to interact with the service.
- /// Do not forget to refresh it when using it if necessary.
+ /// The username on the external service.
///
- public JwtToken Token { get; set; }
+ public string Username { get; set; }
///
/// The link to the user profile on this website. Null if it does not exist.
///
public string? ProfileUrl { get; set; }
+
+ ///
+ /// A jwt token used to interact with the service.
+ /// Do not forget to refresh it when using it if necessary.
+ ///
+ public JwtToken Token { get; set; }
}
}
diff --git a/back/src/Kyoo.Authentication/Controllers/OidcController.cs b/back/src/Kyoo.Authentication/Controllers/OidcController.cs
new file mode 100644
index 00000000..0a1a4f72
--- /dev/null
+++ b/back/src/Kyoo.Authentication/Controllers/OidcController.cs
@@ -0,0 +1,113 @@
+// 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 .
+
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.Net.Http;
+using System.Net.Http.Json;
+using System.Text;
+using System.Threading.Tasks;
+using Kyoo.Abstractions.Models;
+using Kyoo.Authentication.Models;
+using Kyoo.Authentication.Models.DTO;
+using Kyoo.Core.Controllers;
+
+namespace Kyoo.Authentication;
+
+public class OidcController(
+ UserRepository users,
+ IHttpClientFactory clientFactory, PermissionOption options)
+{
+ private async Task<(User, ExternalToken)> _TranslateCode(string provider, string code)
+ {
+ OidcProvider prov = options.OIDC[provider];
+
+ HttpClient client = clientFactory.CreateClient();
+
+ string auth = Convert.ToBase64String(
+ Encoding.UTF8.GetBytes($"{prov.ClientId}:{prov.Secret}")
+ );
+ client.DefaultRequestHeaders.Add("Authorization", $"Basic {auth}");
+
+ HttpResponseMessage resp = await client.PostAsync(
+ prov.TokenUrl,
+ new FormUrlEncodedContent(
+ new Dictionary()
+ {
+ ["code"] = code,
+ ["client_id"] = prov.ClientId,
+ ["client_secret"] = prov.Secret,
+ ["redirect_uri"] =
+ $"{options.PublicUrl.TrimEnd('/')}/api/auth/logged/{provider}",
+ ["grant_type"] = "authorization_code",
+ }
+ )
+ );
+ if (!resp.IsSuccessStatusCode)
+ throw new ValidationException(
+ $"Invalid code or configuration. {resp.StatusCode}: {await resp.Content.ReadAsStringAsync()}"
+ );
+ JwtToken? token = await resp.Content.ReadFromJsonAsync();
+ if (token is null)
+ throw new ValidationException("Could not retrive token.");
+
+ client.DefaultRequestHeaders.Remove("Authorization");
+ client.DefaultRequestHeaders.Add("Authorization", $"{token.TokenType} {token.AccessToken}");
+ JwtProfile? profile = await client.GetFromJsonAsync(prov.ProfileUrl);
+ if (profile is null || profile.Sub is null)
+ throw new ValidationException("Missing sub on user object");
+ ExternalToken extToken = new() { Id = profile.Sub, Token = token, };
+ User newUser = new();
+ if (profile.Email is not null)
+ newUser.Email = profile.Email;
+ string? username = profile.Username ?? profile.Name;
+ if (username is null)
+ {
+ throw new ValidationException(
+ $"Could not find a username for the user. You may need to add more scopes. Fields: {string.Join(',', profile.Extra)}"
+ );
+ }
+ extToken.Username = username;
+ newUser.Username = username;
+ newUser.Slug = Utils.Utility.ToSlug(newUser.Username);
+ newUser.ExternalId.Add(provider, extToken);
+ newUser.Permissions = options.NewUser;
+ return (newUser, extToken);
+ }
+
+ public async Task LoginViaCode(string provider, string code)
+ {
+ (User newUser, ExternalToken extToken) = await _TranslateCode(provider, code);
+ User? user = await users.GetByExternalId(provider, extToken.Id);
+ if (user == null)
+ {
+ try
+ {
+ user = await users.Create(newUser);
+ }
+ catch
+ {
+ throw new ValidationException(
+ "A user already exists with the same username. If this is you, login via username and then link your account."
+ );
+ }
+ }
+ return user;
+ }
+}
diff --git a/back/src/Kyoo.Authentication/Views/AuthApi.cs b/back/src/Kyoo.Authentication/Views/AuthApi.cs
index e926cc4e..58ae93a7 100644
--- a/back/src/Kyoo.Authentication/Views/AuthApi.cs
+++ b/back/src/Kyoo.Authentication/Views/AuthApi.cs
@@ -49,9 +49,9 @@ namespace Kyoo.Authentication.Views
[ApiDefinition("Authentication", Group = UsersGroup)]
public class AuthApi(
UserRepository users,
+ OidcController oidc,
ITokenController tokenController,
IThumbnailsManager thumbs,
- IHttpClientFactory clientFactory,
PermissionOption options
) : ControllerBase
{
@@ -174,77 +174,8 @@ namespace Kyoo.Authentication.Views
}
if (code == null)
return BadRequest(new RequestError("Invalid code."));
- OidcProvider prov = options.OIDC[provider];
- HttpClient client = clientFactory.CreateClient();
-
- string auth = Convert.ToBase64String(
- Encoding.UTF8.GetBytes($"{prov.ClientId}:{prov.Secret}")
- );
- client.DefaultRequestHeaders.Add("Authorization", $"Basic {auth}");
-
- HttpResponseMessage resp = await client.PostAsync(
- prov.TokenUrl,
- new FormUrlEncodedContent(
- new Dictionary()
- {
- ["code"] = code,
- ["client_id"] = prov.ClientId,
- ["client_secret"] = prov.Secret,
- ["redirect_uri"] =
- $"{options.PublicUrl.TrimEnd('/')}/api/auth/logged/{provider}",
- ["grant_type"] = "authorization_code",
- }
- )
- );
- if (!resp.IsSuccessStatusCode)
- return BadRequest(
- $"Invalid code or configuration. {resp.StatusCode}: {await resp.Content.ReadAsStringAsync()}"
- );
- JwtToken? token = await resp.Content.ReadFromJsonAsync();
- if (token is null)
- return BadRequest("Could not retrive token.");
-
- client.DefaultRequestHeaders.Remove("Authorization");
- client.DefaultRequestHeaders.Add(
- "Authorization",
- $"{token.TokenType} {token.AccessToken}"
- );
- JwtProfile? profile = await client.GetFromJsonAsync(prov.ProfileUrl);
- if (profile is null || profile.Sub is null)
- return BadRequest("Missing sub on user object");
- ExternalToken extToken = new() { Id = profile.Sub, Token = token, };
- User newUser = new();
- if (profile.Email is not null)
- newUser.Email = profile.Email;
- string? username = profile.Username ?? profile.Name;
- if (username is null)
- {
- return BadRequest(
- new RequestError(
- $"Could not find a username for the user. You may need to add more scopes. Fields: {string.Join(',', profile.Extra)}"
- )
- );
- }
- newUser.Username = username;
- newUser.Slug = Utils.Utility.ToSlug(newUser.Username);
- newUser.ExternalId.Add(provider, extToken);
- newUser.Permissions = options.NewUser;
-
- User? user = await users.GetByExternalId(provider, extToken.Id);
- if (user == null)
- {
- try
- {
- user = await users.Create(newUser);
- }
- catch
- {
- return BadRequest(
- "A user already exists with the same username. If this is you, login via username and then link your account."
- );
- }
- }
+ User user = await oidc.LoginViaCode(provider, code);
return new JwtToken(
tokenController.CreateAccessToken(user, out TimeSpan expireIn),
await tokenController.CreateRefreshToken(user),