mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-05-31 12:14:46 -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>
|
/// <remarks>
|
||||||
/// Login via a registered oauth provider.
|
/// Login via a registered oauth provider.
|
||||||
/// </remarks>
|
/// </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>
|
/// <returns>A redirect to the provider's login page.</returns>
|
||||||
/// <response code="404">The provider is not register with this instance of kyoo.</response>
|
/// <response code="404">The provider is not register with this instance of kyoo.</response>
|
||||||
[HttpGet("login/{provider}")]
|
[HttpGet("login/{provider}")]
|
||||||
[ProducesResponseType(StatusCodes.Status302Found)]
|
[ProducesResponseType(StatusCodes.Status302Found)]
|
||||||
[ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(RequestError))]
|
[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)
|
if (!options.OIDC.ContainsKey(provider) || !options.OIDC[provider].Enabled)
|
||||||
{
|
{
|
||||||
@ -108,15 +112,16 @@ namespace Kyoo.Authentication.Views
|
|||||||
["response_type"] = "code",
|
["response_type"] = "code",
|
||||||
["client_id"] = prov.ClientId,
|
["client_id"] = prov.ClientId,
|
||||||
["redirect_uri"] =
|
["redirect_uri"] =
|
||||||
$"{options.PublicUrl.TrimEnd('/')}/api/auth/callback/{provider}",
|
$"{options.PublicUrl.TrimEnd('/')}/api/auth/logged/{provider}",
|
||||||
["scope"] = prov.Scope,
|
["scope"] = prov.Scope,
|
||||||
|
["state"] = redirectUrl,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Oauth Login Callback.
|
/// Oauth Code Redirect.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// This route is not meant to be called manually, the user should be redirected automatically here
|
/// 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>
|
/// </remarks>
|
||||||
/// <returns>A redirect to the provider's login page.</returns>
|
/// <returns>A redirect to the provider's login page.</returns>
|
||||||
/// <response code="403">The provider gave an error.</response>
|
/// <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.Status200OK)]
|
||||||
[ProducesResponseType(StatusCodes.Status403Forbidden, Type = typeof(RequestError))]
|
[ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))]
|
||||||
public async Task<ActionResult<JwtToken>> OauthCallback(string provider, string code)
|
public async Task<ActionResult<JwtToken>> OauthCallback(string provider, string code)
|
||||||
{
|
{
|
||||||
if (!options.OIDC.ContainsKey(provider) || !options.OIDC[provider].Enabled)
|
if (!options.OIDC.ContainsKey(provider) || !options.OIDC[provider].Enabled)
|
||||||
@ -157,7 +192,7 @@ namespace Kyoo.Authentication.Views
|
|||||||
["client_id"] = prov.ClientId,
|
["client_id"] = prov.ClientId,
|
||||||
["client_secret"] = prov.Secret,
|
["client_secret"] = prov.Secret,
|
||||||
["redirect_uri"] =
|
["redirect_uri"] =
|
||||||
$"{options.PublicUrl.TrimEnd('/')}/api/auth/callback/{provider}",
|
$"{options.PublicUrl.TrimEnd('/')}/api/auth/logged/{provider}",
|
||||||
["grant_type"] = "authorization_code",
|
["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;
|
let running: ReturnType<typeof getTokenWJ> | null = null;
|
||||||
|
|
||||||
export const getTokenWJ = async (account?: Account | null): ReturnType<typeof run> => {
|
export const getTokenWJ = async (account?: Account | null): ReturnType<typeof run> => {
|
||||||
|
@ -19,7 +19,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { imageFn } from "..";
|
import { baseAppUrl, imageFn } from "..";
|
||||||
|
|
||||||
export const OidcInfoP = z.object({
|
export const OidcInfoP = z.object({
|
||||||
/*
|
/*
|
||||||
@ -42,7 +42,7 @@ export const ServerInfoP = z.object({
|
|||||||
Object.fromEntries(
|
Object.fromEntries(
|
||||||
Object.entries(x).map(([provider, info]) => [
|
Object.entries(x).map(([provider, info]) => [
|
||||||
provider,
|
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 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({
|
export const Img = z.object({
|
||||||
source: z.string(),
|
source: z.string(),
|
||||||
blurhash: z.string(),
|
blurhash: z.string(),
|
||||||
|
@ -25,7 +25,7 @@ export { MovieDetails, ShowDetails } from "./details";
|
|||||||
export { CollectionPage } from "./collection";
|
export { CollectionPage } from "./collection";
|
||||||
export { Player } from "./player";
|
export { Player } from "./player";
|
||||||
export { SearchPage } from "./search";
|
export { SearchPage } from "./search";
|
||||||
export { LoginPage, RegisterPage } from "./login";
|
export { LoginPage, RegisterPage, OidcCallbackPage } from "./login";
|
||||||
export { DownloadPage, DownloadProvider } from "./downloads";
|
export { DownloadPage, DownloadProvider } from "./downloads";
|
||||||
export { SettingsPage } from "./settings";
|
export { SettingsPage } from "./settings";
|
||||||
export { AdminPage } from "./admin";
|
export { AdminPage } from "./admin";
|
||||||
|
@ -20,3 +20,4 @@
|
|||||||
|
|
||||||
export { LoginPage } from "./login";
|
export { LoginPage } from "./login";
|
||||||
export { RegisterPage } from "./register";
|
export { RegisterPage } from "./register";
|
||||||
|
export { OidcCallbackPage } from "./oidc";
|
||||||
|
@ -18,11 +18,20 @@
|
|||||||
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
* 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 { Button, HR, P, Skeleton, ts } from "@kyoo/primitives";
|
||||||
import { View, ImageBackground } from "react-native";
|
import { View, ImageBackground } from "react-native";
|
||||||
import { percent, rem, useYoshiki } from "yoshiki/native";
|
import { percent, rem, useYoshiki } from "yoshiki/native";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useRouter } from "solito/router";
|
||||||
import { ErrorView } from "../errors";
|
import { ErrorView } from "../errors";
|
||||||
|
|
||||||
export const OidcLogin = () => {
|
export const OidcLogin = () => {
|
||||||
@ -80,3 +89,29 @@ OidcLogin.query = (): QueryIdentifier<ServerInfo> => ({
|
|||||||
path: ["info"],
|
path: ["info"],
|
||||||
parser: ServerInfoP,
|
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