mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-05-24 02:02:36 -04:00
Add oidc callback handling on the front
This commit is contained in:
parent
2b93076146
commit
15d479f1eb
@ -84,12 +84,16 @@ namespace Kyoo.Authentication.Views
|
||||
/// <remarks>
|
||||
/// Login via a registered oauth provider.
|
||||
/// </remarks>
|
||||
/// <param name="provider">The provider code.</param>
|
||||
/// <param name="redirectUrl">
|
||||
/// A url where you will be redirected with the query params provider, code and error. It can be a deep link.
|
||||
/// </param>
|
||||
/// <returns>A redirect to the provider's login page.</returns>
|
||||
/// <response code="404">The provider is not register with this instance of kyoo.</response>
|
||||
[HttpGet("login/{provider}")]
|
||||
[ProducesResponseType(StatusCodes.Status302Found)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(RequestError))]
|
||||
public ActionResult<JwtToken> LoginVia(string provider)
|
||||
public ActionResult<JwtToken> LoginVia(string provider, [FromQuery] string redirectUrl)
|
||||
{
|
||||
if (!options.OIDC.ContainsKey(provider) || !options.OIDC[provider].Enabled)
|
||||
{
|
||||
@ -108,15 +112,16 @@ namespace Kyoo.Authentication.Views
|
||||
["response_type"] = "code",
|
||||
["client_id"] = prov.ClientId,
|
||||
["redirect_uri"] =
|
||||
$"{options.PublicUrl.TrimEnd('/')}/api/auth/callback/{provider}",
|
||||
$"{options.PublicUrl.TrimEnd('/')}/api/auth/logged/{provider}",
|
||||
["scope"] = prov.Scope,
|
||||
["state"] = redirectUrl,
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Oauth Login Callback.
|
||||
/// Oauth Code Redirect.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This route is not meant to be called manually, the user should be redirected automatically here
|
||||
@ -124,9 +129,39 @@ namespace Kyoo.Authentication.Views
|
||||
/// </remarks>
|
||||
/// <returns>A redirect to the provider's login page.</returns>
|
||||
/// <response code="403">The provider gave an error.</response>
|
||||
[HttpGet("callback/{provider}")]
|
||||
[HttpGet("logged/{provider}")]
|
||||
[ProducesResponseType(StatusCodes.Status302Found)]
|
||||
public ActionResult OauthCodeRedirect(
|
||||
string provider,
|
||||
string code,
|
||||
string state,
|
||||
string? error
|
||||
)
|
||||
{
|
||||
return Redirect(
|
||||
_BuildUrl(
|
||||
state,
|
||||
new()
|
||||
{
|
||||
["provider"] = provider,
|
||||
["code"] = code,
|
||||
["error"] = error,
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Oauth callback
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This route should be manually called by the page that got redirected to after a call to /login/{provider}.
|
||||
/// </remarks>
|
||||
/// <returns>A jwt token</returns>
|
||||
/// <response code="400">Bad provider or code</response>
|
||||
[HttpPost("callback/{provider}")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden, Type = typeof(RequestError))]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))]
|
||||
public async Task<ActionResult<JwtToken>> OauthCallback(string provider, string code)
|
||||
{
|
||||
if (!options.OIDC.ContainsKey(provider) || !options.OIDC[provider].Enabled)
|
||||
@ -157,7 +192,7 @@ namespace Kyoo.Authentication.Views
|
||||
["client_id"] = prov.ClientId,
|
||||
["client_secret"] = prov.Secret,
|
||||
["redirect_uri"] =
|
||||
$"{options.PublicUrl.TrimEnd('/')}/api/auth/callback/{provider}",
|
||||
$"{options.PublicUrl.TrimEnd('/')}/api/auth/logged/{provider}",
|
||||
["grant_type"] = "authorization_code",
|
||||
}
|
||||
),
|
||||
|
25
front/apps/web/src/pages/login/callback.tsx
Normal file
25
front/apps/web/src/pages/login/callback.tsx
Normal file
@ -0,0 +1,25 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
import { OidcCallbackPage } from "@kyoo/ui";
|
||||
import { withRoute } from "~/router";
|
||||
|
||||
export default withRoute(OidcCallbackPage);
|
||||
|
@ -68,6 +68,31 @@ export const login = async (
|
||||
}
|
||||
};
|
||||
|
||||
export const oidcLogin = async (provider: string, code: string, apiUrl?: string) => {
|
||||
try {
|
||||
const token = await queryFn(
|
||||
{
|
||||
path: ["auth", "callback", provider, `?code=${code}`],
|
||||
method: "POST",
|
||||
authenticated: false,
|
||||
apiUrl,
|
||||
},
|
||||
TokenP,
|
||||
);
|
||||
const user = await queryFn(
|
||||
{ path: ["auth", "me"], method: "GET", apiUrl },
|
||||
UserP,
|
||||
`Bearer ${token.access_token}`,
|
||||
);
|
||||
const account: Account = { ...user, apiUrl: apiUrl ?? "/api", token, selected: true };
|
||||
addAccount(account);
|
||||
return { ok: true, value: account };
|
||||
} catch (e) {
|
||||
console.error("oidcLogin", e);
|
||||
return { ok: false, error: (e as KyooErrors).errors[0] };
|
||||
}
|
||||
};
|
||||
|
||||
let running: ReturnType<typeof getTokenWJ> | null = null;
|
||||
|
||||
export const getTokenWJ = async (account?: Account | null): ReturnType<typeof run> => {
|
||||
|
@ -19,7 +19,7 @@
|
||||
*/
|
||||
|
||||
import { z } from "zod";
|
||||
import { imageFn } from "..";
|
||||
import { baseAppUrl, imageFn } from "..";
|
||||
|
||||
export const OidcInfoP = z.object({
|
||||
/*
|
||||
@ -42,7 +42,7 @@ export const ServerInfoP = z.object({
|
||||
Object.fromEntries(
|
||||
Object.entries(x).map(([provider, info]) => [
|
||||
provider,
|
||||
{ ...info, link: imageFn(`/auth/login/${provider}`) },
|
||||
{ ...info, link: imageFn(`/auth/login/${provider}?redirectUrl=${baseAppUrl()}/login/callback`) },
|
||||
]),
|
||||
),
|
||||
),
|
||||
|
@ -24,6 +24,8 @@ import { kyooApiUrl } from "..";
|
||||
|
||||
export const imageFn = (url: string) => (Platform.OS === "web" ? `/api${url}` : kyooApiUrl + url);
|
||||
|
||||
export const baseAppUrl = () => Platform.OS === "web" ? window.location.origin : "kyoo://";
|
||||
|
||||
export const Img = z.object({
|
||||
source: z.string(),
|
||||
blurhash: z.string(),
|
||||
|
@ -25,7 +25,7 @@ export { MovieDetails, ShowDetails } from "./details";
|
||||
export { CollectionPage } from "./collection";
|
||||
export { Player } from "./player";
|
||||
export { SearchPage } from "./search";
|
||||
export { LoginPage, RegisterPage } from "./login";
|
||||
export { LoginPage, RegisterPage, OidcCallbackPage } from "./login";
|
||||
export { DownloadPage, DownloadProvider } from "./downloads";
|
||||
export { SettingsPage } from "./settings";
|
||||
export { AdminPage } from "./admin";
|
||||
|
@ -20,3 +20,4 @@
|
||||
|
||||
export { LoginPage } from "./login";
|
||||
export { RegisterPage } from "./register";
|
||||
export { OidcCallbackPage } from "./oidc";
|
||||
|
@ -18,11 +18,20 @@
|
||||
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { QueryIdentifier, ServerInfo, ServerInfoP, useFetch } from "@kyoo/models";
|
||||
import {
|
||||
QueryIdentifier,
|
||||
QueryPage,
|
||||
ServerInfo,
|
||||
ServerInfoP,
|
||||
oidcLogin,
|
||||
useFetch,
|
||||
} from "@kyoo/models";
|
||||
import { Button, HR, P, Skeleton, ts } from "@kyoo/primitives";
|
||||
import { View, ImageBackground } from "react-native";
|
||||
import { percent, rem, useYoshiki } from "yoshiki/native";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useRouter } from "solito/router";
|
||||
import { ErrorView } from "../errors";
|
||||
|
||||
export const OidcLogin = () => {
|
||||
@ -80,3 +89,29 @@ OidcLogin.query = (): QueryIdentifier<ServerInfo> => ({
|
||||
path: ["info"],
|
||||
parser: ServerInfoP,
|
||||
});
|
||||
|
||||
export const OidcCallbackPage: QueryPage<{ provider: string; code: string; error?: string }> = ({
|
||||
provider,
|
||||
code,
|
||||
error,
|
||||
}) => {
|
||||
const [err, setErr] = useState<string | undefined>();
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
async function run() {
|
||||
if (error) {
|
||||
setErr(error);
|
||||
return;
|
||||
}
|
||||
const { error: loginError } = await oidcLogin(provider, code);
|
||||
setErr(loginError);
|
||||
if (loginError) return;
|
||||
router.replace("/", undefined, {
|
||||
experimental: { nativeBehavior: "stack-replace", isNestedNavigator: false },
|
||||
});
|
||||
}
|
||||
run();
|
||||
}, [provider, code, router, error]);
|
||||
return <P>{err ?? "Loading"}</P>;
|
||||
};
|
||||
|
Loading…
x
Reference in New Issue
Block a user