mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-05-24 02:02:36 -04:00
Parse user profile and get jwt
This commit is contained in:
parent
35a69edfa2
commit
98e9ba0fa7
@ -89,14 +89,14 @@ namespace Kyoo.Authentication
|
||||
return acc;
|
||||
if (val.Key.Split("_") is not ["OIDC", string provider, string key])
|
||||
{
|
||||
logger.LogError("Invalid oidc config value: {}", val.Key);
|
||||
logger.LogError("Invalid oidc config value: {Key}", val.Key);
|
||||
return acc;
|
||||
}
|
||||
provider = provider.ToLowerInvariant();
|
||||
key = key.ToLowerInvariant();
|
||||
|
||||
if (!acc.ContainsKey(provider))
|
||||
acc.Add(provider, new());
|
||||
acc.Add(provider, new(provider));
|
||||
switch (key)
|
||||
{
|
||||
case "clientid":
|
||||
@ -111,11 +111,15 @@ namespace Kyoo.Authentication
|
||||
case "authorization":
|
||||
acc[provider].AuthorizationUrl = val.Value;
|
||||
break;
|
||||
case "token":
|
||||
acc[provider].TokenUrl = val.Value;
|
||||
break;
|
||||
case "userinfo":
|
||||
acc[provider].UserinfoUrl = val.Value;
|
||||
case "profile":
|
||||
acc[provider].ProfileUrl = val.Value;
|
||||
break;
|
||||
default:
|
||||
logger.LogError("Invalid oidc config value: {}", key);
|
||||
logger.LogError("Invalid oidc config value: {Key}", key);
|
||||
return acc;
|
||||
}
|
||||
return acc;
|
||||
|
33
back/src/Kyoo.Authentication/Models/DTO/JwtProfile.cs
Normal file
33
back/src/Kyoo.Authentication/Models/DTO/JwtProfile.cs
Normal file
@ -0,0 +1,33 @@
|
||||
// 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.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Kyoo.Authentication.Models.DTO;
|
||||
|
||||
public class JwtProfile
|
||||
{
|
||||
public string Sub { get; set; }
|
||||
public string? Name { get; set; }
|
||||
public string? Username { get; set; }
|
||||
public string? Email { get; set; }
|
||||
|
||||
[JsonExtensionData]
|
||||
public Dictionary<string, object> Extra { get; set; }
|
||||
}
|
@ -64,8 +64,31 @@ public class PermissionOption
|
||||
public class OidcProvider
|
||||
{
|
||||
public string AuthorizationUrl { get; set; }
|
||||
public string UserinfoUrl { get; set; }
|
||||
public string TokenUrl { get; set; }
|
||||
public string ProfileUrl { get; set; }
|
||||
public string ClientId { get; set; }
|
||||
public string Secret { get; set; }
|
||||
public string? Scope { get; set; }
|
||||
|
||||
public bool Enabled =>
|
||||
AuthorizationUrl != null
|
||||
&& TokenUrl != null
|
||||
&& ProfileUrl != null
|
||||
&& ClientId != null
|
||||
&& Secret != null;
|
||||
|
||||
public OidcProvider(string provider)
|
||||
{
|
||||
switch (provider)
|
||||
{
|
||||
case "google":
|
||||
AuthorizationUrl = "https://accounts.google.com/o/oauth2/v2/auth";
|
||||
TokenUrl = "https://oauth2.googleapis.com/token";
|
||||
ProfileUrl = "https://openidconnect.googleapis.com/v1/userinfo";
|
||||
Scope = "email profile";
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -17,7 +17,12 @@
|
||||
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Json;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Kyoo.Abstractions.Controllers;
|
||||
using Kyoo.Abstractions.Models;
|
||||
@ -31,6 +36,9 @@ using Kyoo.Models;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Converters;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using static Kyoo.Abstractions.Models.Utils.Constants;
|
||||
using BCryptNet = BCrypt.Net.BCrypt;
|
||||
|
||||
@ -46,6 +54,7 @@ namespace Kyoo.Authentication.Views
|
||||
IRepository<User> users,
|
||||
ITokenController tokenController,
|
||||
IThumbnailsManager thumbs,
|
||||
IHttpClientFactory clientFactory,
|
||||
PermissionOption options
|
||||
) : ControllerBase
|
||||
{
|
||||
@ -59,6 +68,19 @@ namespace Kyoo.Authentication.Views
|
||||
return new ObjectResult(value) { StatusCode = StatusCodes.Status403Forbidden };
|
||||
}
|
||||
|
||||
private static string _BuildUrl(string baseUrl, Dictionary<string, string?> queryParams)
|
||||
{
|
||||
char querySep = baseUrl.Contains('?') ? '&' : '?';
|
||||
foreach ((string key, string? val) in queryParams)
|
||||
{
|
||||
if (val is null)
|
||||
continue;
|
||||
baseUrl += $"{querySep}{key}={val}";
|
||||
querySep = '&';
|
||||
}
|
||||
return baseUrl;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Oauth Login.
|
||||
/// </summary>
|
||||
@ -67,12 +89,12 @@ namespace Kyoo.Authentication.Views
|
||||
/// </remarks>
|
||||
/// <returns>A redirect to the provider's login page.</returns>
|
||||
/// <response code="404">The provider is not register with this instance of kyoo.</response>
|
||||
[HttpPost("login/{provider}")]
|
||||
[HttpGet("login/{provider}")]
|
||||
[ProducesResponseType(StatusCodes.Status302Found)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(RequestError))]
|
||||
public ActionResult<JwtToken> LoginVia(string provider)
|
||||
{
|
||||
if (!options.OIDC.ContainsKey(provider))
|
||||
if (!options.OIDC.ContainsKey(provider) || !options.OIDC[provider].Enabled)
|
||||
{
|
||||
return NotFound(
|
||||
new RequestError(
|
||||
@ -81,13 +103,19 @@ namespace Kyoo.Authentication.Views
|
||||
);
|
||||
}
|
||||
OidcProvider prov = options.OIDC[provider];
|
||||
char querySep = prov.AuthorizationUrl.Contains('?') ? '&' : '?';
|
||||
string url = $"{prov.AuthorizationUrl}{querySep}response_type=code";
|
||||
url += $"&client_id={prov.ClientId}";
|
||||
url += $"&redirect_uri={options.PublicUrl.TrimEnd('/')}/api/auth/callback/{provider}";
|
||||
if (prov.Scope is not null)
|
||||
url += $"&scope={prov.Scope}";
|
||||
return Redirect(url);
|
||||
return Redirect(
|
||||
_BuildUrl(
|
||||
prov.AuthorizationUrl,
|
||||
new()
|
||||
{
|
||||
["response_type"] = "code",
|
||||
["client_id"] = prov.ClientId,
|
||||
["redirect_uri"] =
|
||||
$"{options.PublicUrl.TrimEnd('/')}/api/auth/callback/{provider}",
|
||||
["scope"] = prov.Scope,
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -99,22 +127,88 @@ namespace Kyoo.Authentication.Views
|
||||
/// </remarks>
|
||||
/// <returns>A redirect to the provider's login page.</returns>
|
||||
/// <response code="403">The provider gave an error.</response>
|
||||
[HttpPost("callback/{provider}")]
|
||||
[HttpGet("callback/{provider}")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden, Type = typeof(RequestError))]
|
||||
public async Task<ActionResult<JwtToken>> OauthCallback(string provider, dynamic val)
|
||||
public async Task<ActionResult<JwtToken>> OauthCallback(string provider, string code)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
// User? user = await users.GetOrDefault(
|
||||
// new Filter<User>.Lambda(x => x.ExternalId[provider].Id == val.Id)
|
||||
// );
|
||||
// if (user == null)
|
||||
// user = await users.Create(val);
|
||||
// return new JwtToken(
|
||||
// tokenController.CreateAccessToken(user, out TimeSpan expireIn),
|
||||
// await tokenController.CreateRefreshToken(user),
|
||||
// expireIn
|
||||
// );
|
||||
if (!options.OIDC.ContainsKey(provider) || !options.OIDC[provider].Enabled)
|
||||
{
|
||||
return NotFound(
|
||||
new RequestError(
|
||||
$"Invalid provider. {provider} is not registered no this instance of kyoo."
|
||||
)
|
||||
);
|
||||
}
|
||||
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(
|
||||
_BuildUrl(
|
||||
prov.TokenUrl,
|
||||
new()
|
||||
{
|
||||
["code"] = code,
|
||||
["client_id"] = prov.ClientId,
|
||||
["client_secret"] = prov.Secret,
|
||||
["redirect_uri"] =
|
||||
$"{options.PublicUrl.TrimEnd('/')}/api/auth/callback/{provider}",
|
||||
["grant_type"] = "authorization_code",
|
||||
}
|
||||
),
|
||||
null
|
||||
);
|
||||
if (!resp.IsSuccessStatusCode)
|
||||
return BadRequest("Invalid code or configuration.");
|
||||
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;
|
||||
if (profile.Username is not null)
|
||||
newUser.Username = profile.Username;
|
||||
else if (profile.Name is not null)
|
||||
newUser.Username = profile.Name;
|
||||
else
|
||||
{
|
||||
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.Slug = Utils.Utility.ToSlug(newUser.Username);
|
||||
newUser.ExternalId.Add(provider, extToken);
|
||||
|
||||
User? user = await users.GetOrDefault(
|
||||
new Filter<User>.Lambda(x => x.ExternalId[provider].Id == extToken.Id)
|
||||
);
|
||||
if (user == null)
|
||||
user = await users.Create(newUser);
|
||||
return new JwtToken(
|
||||
tokenController.CreateAccessToken(user, out TimeSpan expireIn),
|
||||
await tokenController.CreateRefreshToken(user),
|
||||
expireIn
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
Loading…
x
Reference in New Issue
Block a user