diff --git a/back/src/Kyoo.Authentication/Views/AuthApi.cs b/back/src/Kyoo.Authentication/Views/AuthApi.cs
index d3a264d3..1dd0e1b5 100644
--- a/back/src/Kyoo.Authentication/Views/AuthApi.cs
+++ b/back/src/Kyoo.Authentication/Views/AuthApi.cs
@@ -84,12 +84,16 @@ namespace Kyoo.Authentication.Views
///
/// Login via a registered oauth provider.
///
+ /// The provider code.
+ ///
+ /// A url where you will be redirected with the query params provider, code and error. It can be a deep link.
+ ///
/// A redirect to the provider's login page.
/// The provider is not register with this instance of kyoo.
[HttpGet("login/{provider}")]
[ProducesResponseType(StatusCodes.Status302Found)]
[ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(RequestError))]
- public ActionResult LoginVia(string provider)
+ public ActionResult 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,
}
)
);
}
///
- /// Oauth Login Callback.
+ /// Oauth Code Redirect.
///
///
/// This route is not meant to be called manually, the user should be redirected automatically here
@@ -124,9 +129,39 @@ namespace Kyoo.Authentication.Views
///
/// A redirect to the provider's login page.
/// The provider gave an error.
- [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,
+ }
+ )
+ );
+ }
+
+ ///
+ /// Oauth callback
+ ///
+ ///
+ /// This route should be manually called by the page that got redirected to after a call to /login/{provider}.
+ ///
+ /// A jwt token
+ /// Bad provider or code
+ [HttpPost("callback/{provider}")]
[ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesResponseType(StatusCodes.Status403Forbidden, Type = typeof(RequestError))]
+ [ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))]
public async Task> 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",
}
),
diff --git a/front/apps/web/src/pages/login/callback.tsx b/front/apps/web/src/pages/login/callback.tsx
new file mode 100644
index 00000000..bc5c8c55
--- /dev/null
+++ b/front/apps/web/src/pages/login/callback.tsx
@@ -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 .
+ */
+
+import { OidcCallbackPage } from "@kyoo/ui";
+import { withRoute } from "~/router";
+
+export default withRoute(OidcCallbackPage);
+
diff --git a/front/packages/models/src/login.ts b/front/packages/models/src/login.ts
index 6ae89d6e..80815b42 100644
--- a/front/packages/models/src/login.ts
+++ b/front/packages/models/src/login.ts
@@ -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 | null = null;
export const getTokenWJ = async (account?: Account | null): ReturnType => {
diff --git a/front/packages/models/src/resources/server-info.ts b/front/packages/models/src/resources/server-info.ts
index 598f21c0..b2749e51 100644
--- a/front/packages/models/src/resources/server-info.ts
+++ b/front/packages/models/src/resources/server-info.ts
@@ -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`) },
]),
),
),
diff --git a/front/packages/models/src/traits/images.ts b/front/packages/models/src/traits/images.ts
index f7978d8f..47667ab6 100644
--- a/front/packages/models/src/traits/images.ts
+++ b/front/packages/models/src/traits/images.ts
@@ -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(),
diff --git a/front/packages/ui/src/index.ts b/front/packages/ui/src/index.ts
index 3fae9f53..e1823fca 100644
--- a/front/packages/ui/src/index.ts
+++ b/front/packages/ui/src/index.ts
@@ -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";
diff --git a/front/packages/ui/src/login/index.ts b/front/packages/ui/src/login/index.ts
index e36fb536..c4cfd5ea 100644
--- a/front/packages/ui/src/login/index.ts
+++ b/front/packages/ui/src/login/index.ts
@@ -20,3 +20,4 @@
export { LoginPage } from "./login";
export { RegisterPage } from "./register";
+export { OidcCallbackPage } from "./oidc";
diff --git a/front/packages/ui/src/login/oidc.tsx b/front/packages/ui/src/login/oidc.tsx
index 618ddfca..299bc7e8 100644
--- a/front/packages/ui/src/login/oidc.tsx
+++ b/front/packages/ui/src/login/oidc.tsx
@@ -18,11 +18,20 @@
* along with Kyoo. If not, see .
*/
-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 => ({
path: ["info"],
parser: ServerInfoP,
});
+
+export const OidcCallbackPage: QueryPage<{ provider: string; code: string; error?: string }> = ({
+ provider,
+ code,
+ error,
+}) => {
+ const [err, setErr] = useState();
+ 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 {err ?? "Loading"}
;
+};