mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-05-24 02:02:36 -04:00
Move oidc logic inside a service
This commit is contained in:
parent
d9022fde9f
commit
079a2cdbe3
@ -100,14 +100,19 @@ namespace Kyoo.Abstractions.Models
|
||||
public string Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public JwtToken Token { get; set; }
|
||||
public string Username { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The link to the user profile on this website. Null if it does not exist.
|
||||
/// </summary>
|
||||
public string? ProfileUrl { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// A jwt token used to interact with the service.
|
||||
/// Do not forget to refresh it when using it if necessary.
|
||||
/// </summary>
|
||||
public JwtToken Token { get; set; }
|
||||
}
|
||||
}
|
||||
|
113
back/src/Kyoo.Authentication/Controllers/OidcController.cs
Normal file
113
back/src/Kyoo.Authentication/Controllers/OidcController.cs
Normal file
@ -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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
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<string, string>()
|
||||
{
|
||||
["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<JwtToken>();
|
||||
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<JwtProfile>(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<User> 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;
|
||||
}
|
||||
}
|
@ -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<string, string>()
|
||||
{
|
||||
["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<JwtToken>();
|
||||
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<JwtProfile>(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),
|
||||
|
Loading…
x
Reference in New Issue
Block a user