Add oidc callback handling on the front

This commit is contained in:
Zoe Roux 2024-03-03 19:01:53 +01:00
parent 2b93076146
commit 15d479f1eb
8 changed files with 133 additions and 10 deletions

View File

@ -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",
}
),

View 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);

View File

@ -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> => {

View File

@ -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`) },
]),
),
),

View File

@ -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(),

View File

@ -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";

View File

@ -20,3 +20,4 @@
export { LoginPage } from "./login";
export { RegisterPage } from "./register";
export { OidcCallbackPage } from "./oidc";

View File

@ -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>;
};