mirror of
				https://github.com/zoriya/Kyoo.git
				synced 2025-11-04 03:27:14 -05: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