mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-06-03 05:34:23 -04:00
Add simkl oidc
This commit is contained in:
parent
115b9fa4b3
commit
44bb88910f
@ -41,7 +41,7 @@ THEMOVIEDB_APIKEY=
|
|||||||
# The url you can use to reach your kyoo instance. This is used during oidc to redirect users to your instance.
|
# The url you can use to reach your kyoo instance. This is used during oidc to redirect users to your instance.
|
||||||
PUBLIC_URL=http://localhost:5000
|
PUBLIC_URL=http://localhost:5000
|
||||||
|
|
||||||
# Use a builtin oidc service (google or discord):
|
# Use a builtin oidc service (google, discord, or simkl):
|
||||||
# When you create a client_id, secret combo you may be asked for a redirect url. You need to specify https://YOUR-PUBLIC-URL/api/auth/logged/YOUR-SERVICE-NAME
|
# When you create a client_id, secret combo you may be asked for a redirect url. You need to specify https://YOUR-PUBLIC-URL/api/auth/logged/YOUR-SERVICE-NAME
|
||||||
OIDC_DISCORD_CLIENTID=
|
OIDC_DISCORD_CLIENTID=
|
||||||
OIDC_DISCORD_SECRET=
|
OIDC_DISCORD_SECRET=
|
||||||
|
@ -22,6 +22,7 @@ using System.ComponentModel.DataAnnotations;
|
|||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Net.Http.Json;
|
using System.Net.Http.Json;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Kyoo.Abstractions.Controllers;
|
using Kyoo.Abstractions.Controllers;
|
||||||
using Kyoo.Abstractions.Models;
|
using Kyoo.Abstractions.Models;
|
||||||
@ -46,21 +47,18 @@ public class OidcController(
|
|||||||
Encoding.UTF8.GetBytes($"{prov.ClientId}:{prov.Secret}")
|
Encoding.UTF8.GetBytes($"{prov.ClientId}:{prov.Secret}")
|
||||||
);
|
);
|
||||||
client.DefaultRequestHeaders.Add("Authorization", $"Basic {auth}");
|
client.DefaultRequestHeaders.Add("Authorization", $"Basic {auth}");
|
||||||
|
Dictionary<string, string> data =
|
||||||
HttpResponseMessage resp = await client.PostAsync(
|
new()
|
||||||
prov.TokenUrl,
|
{
|
||||||
new FormUrlEncodedContent(
|
["code"] = code,
|
||||||
new Dictionary<string, string>()
|
["client_id"] = prov.ClientId,
|
||||||
{
|
["client_secret"] = prov.Secret,
|
||||||
["code"] = code,
|
["redirect_uri"] = $"{options.PublicUrl.TrimEnd('/')}/api/auth/logged/{provider}",
|
||||||
["client_id"] = prov.ClientId,
|
["grant_type"] = "authorization_code",
|
||||||
["client_secret"] = prov.Secret,
|
};
|
||||||
["redirect_uri"] =
|
HttpResponseMessage resp = prov.TokenUseJsonBody
|
||||||
$"{options.PublicUrl.TrimEnd('/')}/api/auth/logged/{provider}",
|
? await client.PostAsJsonAsync(prov.TokenUrl, data)
|
||||||
["grant_type"] = "authorization_code",
|
: await client.PostAsync(prov.TokenUrl, new FormUrlEncodedContent(data));
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
|
||||||
if (!resp.IsSuccessStatusCode)
|
if (!resp.IsSuccessStatusCode)
|
||||||
throw new ValidationException(
|
throw new ValidationException(
|
||||||
$"Invalid code or configuration. {resp.StatusCode}: {await resp.Content.ReadAsStringAsync()}"
|
$"Invalid code or configuration. {resp.StatusCode}: {await resp.Content.ReadAsStringAsync()}"
|
||||||
@ -71,22 +69,36 @@ public class OidcController(
|
|||||||
|
|
||||||
client.DefaultRequestHeaders.Remove("Authorization");
|
client.DefaultRequestHeaders.Remove("Authorization");
|
||||||
client.DefaultRequestHeaders.Add("Authorization", $"{token.TokenType} {token.AccessToken}");
|
client.DefaultRequestHeaders.Add("Authorization", $"{token.TokenType} {token.AccessToken}");
|
||||||
|
Dictionary<string, string>? extraHeaders = prov.GetExtraHeaders?.Invoke(prov);
|
||||||
|
if (extraHeaders is not null)
|
||||||
|
{
|
||||||
|
foreach ((string key, string value) in extraHeaders)
|
||||||
|
client.DefaultRequestHeaders.Add(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
JwtProfile? profile = await client.GetFromJsonAsync<JwtProfile>(prov.ProfileUrl);
|
JwtProfile? profile = await client.GetFromJsonAsync<JwtProfile>(prov.ProfileUrl);
|
||||||
if (profile is null || profile.Sub is null)
|
if (profile is null || profile.Sub is null)
|
||||||
throw new ValidationException("Missing sub on user object");
|
throw new ValidationException(
|
||||||
ExternalToken extToken = new() { Id = profile.Sub, Token = token, };
|
$"Missing sub on user object. Got: {JsonSerializer.Serialize(profile)}"
|
||||||
|
);
|
||||||
|
ExternalToken extToken =
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Id = profile.Sub,
|
||||||
|
Token = token,
|
||||||
|
ProfileUrl = prov.GetProfileUrl?.Invoke(profile),
|
||||||
|
};
|
||||||
User newUser = new();
|
User newUser = new();
|
||||||
if (profile.Email is not null)
|
if (profile.Email is not null)
|
||||||
newUser.Email = profile.Email;
|
newUser.Email = profile.Email;
|
||||||
string? username = profile.Username ?? profile.Name;
|
if (profile.Username is null)
|
||||||
if (username is null)
|
|
||||||
{
|
{
|
||||||
throw new ValidationException(
|
throw new ValidationException(
|
||||||
$"Could not find a username for the user. You may need to add more scopes. Fields: {string.Join(',', profile.Extra)}"
|
$"Could not find a username for the user. You may need to add more scopes. Fields: {string.Join(',', profile.Extra)}"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
extToken.Username = username;
|
extToken.Username = profile.Username;
|
||||||
newUser.Username = username;
|
newUser.Username = profile.Username;
|
||||||
newUser.Slug = Utils.Utility.ToSlug(newUser.Username);
|
newUser.Slug = Utils.Utility.ToSlug(newUser.Username);
|
||||||
newUser.ExternalId.Add(provider, extToken);
|
newUser.ExternalId.Add(provider, extToken);
|
||||||
return (newUser, extToken);
|
return (newUser, extToken);
|
||||||
|
@ -17,30 +17,57 @@
|
|||||||
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Text.Json.Nodes;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace Kyoo.Authentication.Models.DTO;
|
namespace Kyoo.Authentication.Models.DTO;
|
||||||
|
|
||||||
public class JwtProfile
|
public class JwtProfile
|
||||||
{
|
{
|
||||||
public string Sub { get; set; }
|
public string? Sub { get; set; }
|
||||||
public string Uid
|
public string? Uid
|
||||||
{
|
{
|
||||||
set => Sub = value;
|
set => Sub ??= value;
|
||||||
}
|
}
|
||||||
public string Id
|
public string? Id
|
||||||
{
|
{
|
||||||
set => Sub = value;
|
set => Sub ??= value;
|
||||||
}
|
}
|
||||||
public string Guid
|
public string? Guid
|
||||||
{
|
{
|
||||||
set => Sub = value;
|
set => Sub ??= value;
|
||||||
}
|
}
|
||||||
|
|
||||||
public string? Name { get; set; }
|
|
||||||
public string? Username { get; set; }
|
public string? Username { get; set; }
|
||||||
|
public string? Name
|
||||||
|
{
|
||||||
|
set => Username ??= value;
|
||||||
|
}
|
||||||
|
|
||||||
public string? Email { get; set; }
|
public string? Email { get; set; }
|
||||||
|
|
||||||
|
public JsonObject? Account
|
||||||
|
{
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (value is null)
|
||||||
|
return;
|
||||||
|
// simkl store their ids there.
|
||||||
|
Sub ??= value["id"]?.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public JsonObject? User
|
||||||
|
{
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (value is null)
|
||||||
|
return;
|
||||||
|
// simkl store their name there.
|
||||||
|
Username ??= value["name"]?.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[JsonExtensionData]
|
[JsonExtensionData]
|
||||||
public Dictionary<string, object> Extra { get; set; }
|
public Dictionary<string, object> Extra { get; set; }
|
||||||
}
|
}
|
||||||
|
@ -20,6 +20,7 @@ using System;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Kyoo.Abstractions.Models.Permissions;
|
using Kyoo.Abstractions.Models.Permissions;
|
||||||
|
using Kyoo.Authentication.Models.DTO;
|
||||||
|
|
||||||
namespace Kyoo.Authentication.Models;
|
namespace Kyoo.Authentication.Models;
|
||||||
|
|
||||||
@ -72,11 +73,20 @@ public class OidcProvider
|
|||||||
public string? LogoUrl { get; set; }
|
public string? LogoUrl { get; set; }
|
||||||
public string AuthorizationUrl { get; set; }
|
public string AuthorizationUrl { get; set; }
|
||||||
public string TokenUrl { get; set; }
|
public string TokenUrl { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Some token endpoints do net respect the spec and require a json body instead of a form url encoded.
|
||||||
|
/// </summary>
|
||||||
|
public bool TokenUseJsonBody { get; set; }
|
||||||
|
|
||||||
public string ProfileUrl { get; set; }
|
public string ProfileUrl { get; set; }
|
||||||
public string? Scope { get; set; }
|
public string? Scope { get; set; }
|
||||||
public string ClientId { get; set; }
|
public string ClientId { get; set; }
|
||||||
public string Secret { get; set; }
|
public string Secret { get; set; }
|
||||||
|
|
||||||
|
public Func<JwtProfile, string?>? GetProfileUrl { get; init; }
|
||||||
|
public Func<OidcProvider, Dictionary<string, string>>? GetExtraHeaders { get; init; }
|
||||||
|
|
||||||
public bool Enabled =>
|
public bool Enabled =>
|
||||||
AuthorizationUrl != null
|
AuthorizationUrl != null
|
||||||
&& TokenUrl != null
|
&& TokenUrl != null
|
||||||
@ -97,6 +107,9 @@ public class OidcProvider
|
|||||||
Scope = KnownProviders[provider].Scope;
|
Scope = KnownProviders[provider].Scope;
|
||||||
ClientId = KnownProviders[provider].ClientId;
|
ClientId = KnownProviders[provider].ClientId;
|
||||||
Secret = KnownProviders[provider].Secret;
|
Secret = KnownProviders[provider].Secret;
|
||||||
|
TokenUseJsonBody = KnownProviders[provider].TokenUseJsonBody;
|
||||||
|
GetProfileUrl = KnownProviders[provider].GetProfileUrl;
|
||||||
|
GetExtraHeaders = KnownProviders[provider].GetExtraHeaders;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -120,6 +133,20 @@ public class OidcProvider
|
|||||||
TokenUrl = "https://discord.com/api/oauth2/token",
|
TokenUrl = "https://discord.com/api/oauth2/token",
|
||||||
ProfileUrl = "https://discord.com/api/users/@me",
|
ProfileUrl = "https://discord.com/api/users/@me",
|
||||||
Scope = "email+identify",
|
Scope = "email+identify",
|
||||||
}
|
},
|
||||||
|
["simkl"] = new("simkl")
|
||||||
|
{
|
||||||
|
DisplayName = "Simkl",
|
||||||
|
LogoUrl = "https://logo.clearbit.com/simkl.com",
|
||||||
|
AuthorizationUrl = "https://simkl.com/oauth/authorize",
|
||||||
|
TokenUrl = "https://api.simkl.com/oauth/token",
|
||||||
|
ProfileUrl = "https://api.simkl.com/users/settings",
|
||||||
|
// does not seems to have scopes
|
||||||
|
Scope = null,
|
||||||
|
TokenUseJsonBody = true,
|
||||||
|
GetProfileUrl = (profile) => $"https://simkl.com/{profile.Sub}/dashboard/",
|
||||||
|
GetExtraHeaders = (OidcProvider self) =>
|
||||||
|
new() { ["simkl-api-key"] = self.ClientId },
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -101,7 +101,6 @@ export const OidcSettings = () => {
|
|||||||
text={t("settings.oidc.link")}
|
text={t("settings.oidc.link")}
|
||||||
as={Link}
|
as={Link}
|
||||||
href={x.link}
|
href={x.link}
|
||||||
target="_blank"
|
|
||||||
{...css({ minWidth: rem(6) })}
|
{...css({ minWidth: rem(6) })}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user