diff --git a/auth/go.mod b/auth/go.mod index 5e600665..8cb35e78 100644 --- a/auth/go.mod +++ b/auth/go.mod @@ -15,6 +15,7 @@ require ( github.com/swaggo/echo-swagger v1.5.0 github.com/swaggo/swag v1.16.6 go.opentelemetry.io/contrib/bridges/otelslog v0.17.0 + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 go.opentelemetry.io/otel v1.42.0 go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.18.0 go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.18.0 @@ -31,6 +32,7 @@ require ( github.com/cenkalti/backoff/v5 v5.0.3 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-openapi/swag/conv v0.25.4 // indirect diff --git a/auth/main.go b/auth/main.go index e2aef3a2..2271f184 100644 --- a/auth/main.go +++ b/auth/main.go @@ -217,6 +217,35 @@ func (h *Handler) TokenToJwt(next echo.HandlerFunc) echo.HandlerFunc { } } +func (h *Handler) OptionalAuthToJwt(next echo.HandlerFunc) echo.HandlerFunc { + return func(c *echo.Context) error { + ctx := c.Request().Context() + + auth := c.Request().Header.Get("Authorization") + if auth == "" { + return next(c) + } + + if !strings.HasPrefix(auth, "Bearer ") { + return echo.NewHTTPError(http.StatusForbidden, "Invalid bearer format") + } + token := auth[len("Bearer "):] + + // this is only used to check if it is a session token or a jwt + _, err := base64.RawURLEncoding.DecodeString(token) + if err != nil { + return next(c) + } + + jwt, err := h.createJwt(ctx, token) + if err != nil { + return err + } + c.Request().Header.Set("Authorization", fmt.Sprintf("Bearer %s", jwt)) + return next(c) + } +} + // @title Keibi - Kyoo's auth // @version 1.0 // @description Auth system made for kyoo. @@ -348,15 +377,23 @@ func main() { g.POST("/users", h.Register) g.POST("/sessions", h.Login) - g.GET("/oidc/login/:provider", h.OidcLogin) - g.GET("/oidc/logged/:provider", h.OidcLogged) - g.GET("/oidc/callback/:provider", h.OidcCallback) r.GET("/sessions", h.ListMySessions) r.DELETE("/sessions", h.Logout) r.DELETE("/sessions/:id", h.Logout) - r.DELETE("/oidc/login/:provider", h.OidcUnlink) r.GET("/users/:id/sessions", h.ListUserSessions) + g.GET("/oidc/login/:provider", h.OidcLogin) + r.DELETE("/oidc/login/:provider", h.OidcUnlink) + g.GET("/oidc/logged/:provider", h.OidcLogged) + + or := e.Group("/auth") + or.Use(h.OptionalAuthToJwt) + or.Use(echojwt.WithConfig(echojwt.Config{ + SigningMethod: "RS256", + SigningKey: h.config.JwtPublicKey, + })) + or.GET("/oidc/callback/:provider", h.OidcCallback) + r.GET("/keys", h.ListApiKey) r.POST("/keys", h.CreateApiKey) r.DELETE("/keys/:id", h.DeleteApiKey) diff --git a/auth/oidc.go b/auth/oidc.go index b27c38ec..58543f84 100644 --- a/auth/oidc.go +++ b/auth/oidc.go @@ -6,6 +6,7 @@ import ( "encoding/base64" "encoding/json" "fmt" + "log/slog" "net/http" "net/url" "strings" @@ -241,15 +242,18 @@ func (h *Handler) exchangeOidcCode(c *echo.Context, provider OidcProviderConfig, resp, err := http.DefaultClient.Do(req) if err != nil { + slog.Error("Error calling oidc token endpoint: %v", "err", err) return Token{}, echo.NewHTTPError(http.StatusBadGateway, "Could not reach OIDC token endpoint") } defer resp.Body.Close() if resp.StatusCode < 200 || resp.StatusCode >= 300 { + slog.Error("Error on oidc token endpoint: %v", "err", err) return Token{}, echo.NewHTTPError(http.StatusBadGateway, "OIDC token exchange failed") } var ret Token if err := json.NewDecoder(resp.Body).Decode(&ret); err != nil { + slog.Error("Couldn't decode token: %v", "err", err) return Token{}, echo.NewHTTPError(http.StatusBadGateway, "Invalid OIDC token response") } return ret, nil @@ -286,15 +290,18 @@ func (h *Handler) fetchOidcProfile(c *echo.Context, provider OidcProviderConfig, resp, err := http.DefaultClient.Do(req) if err != nil { + slog.Error("Error calling oidc profile endpoint: %v", "err", err) return Profile{}, echo.NewHTTPError(http.StatusInternalServerError, "Could not reach OIDC profile endpoint") } defer resp.Body.Close() var profile RawProfile if resp.StatusCode < 200 || resp.StatusCode >= 300 { + slog.Error("Error on oidc profile endpoint: %v", "err", err) return Profile{}, echo.NewHTTPError(http.StatusInternalServerError, "Could not fetch OIDC profile") } if err := json.NewDecoder(resp.Body).Decode(&profile); err != nil { + slog.Error("Error parsing oidc profile: %v", "err", err) return Profile{}, echo.NewHTTPError(http.StatusInternalServerError, "Invalid OIDC profile response") } sub := cmp.Or(profile.Sub, profile.Uid, profile.Id, profile.Guid) diff --git a/auth/otel.go b/auth/otel.go index 21801906..42b2f003 100644 --- a/auth/otel.go +++ b/auth/otel.go @@ -4,11 +4,13 @@ import ( "context" "errors" "log/slog" + "net/http" "os" "strings" echootel "github.com/labstack/echo-opentelemetry" "github.com/labstack/echo/v5" + "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc" "go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp" @@ -135,6 +137,7 @@ func setupOtel(ctx context.Context) (func(context.Context) error, error) { logotelglobal.SetLoggerProvider(lp) otel.SetMeterProvider(mp) otel.SetTracerProvider(tp) + http.DefaultTransport = otelhttp.NewTransport(http.DefaultTransport) // configure shutting down // noop providers do not have a Shudown method diff --git a/front/src/models/user.ts b/front/src/models/user.ts index ab03df22..22f90b7e 100644 --- a/front/src/models/user.ts +++ b/front/src/models/user.ts @@ -2,8 +2,6 @@ import { z } from "zod/v4"; export const User = z .object({ - // // keep a default for older versions of the api - // .default({}), id: z.string(), username: z.string(), email: z.string(), @@ -33,17 +31,17 @@ export const User = z audioLanguage: "default", subtitleLanguage: null, }), - // externalId: z - // .record( - // z.string(), - // z.object({ - // id: z.string(), - // username: z.string().nullable().default(""), - // profileUrl: z.string().nullable(), - // }), - // ) - // .default({}), }), + oidc: z + .record( + z.string(), + z.object({ + id: z.string(), + username: z.string().nullable().default(""), + profileUrl: z.string().nullable(), + }), + ) + .default({}), }) .transform((x) => ({ ...x, diff --git a/front/src/primitives/chip.tsx b/front/src/primitives/chip.tsx index f085c805..fdd969e8 100644 --- a/front/src/primitives/chip.tsx +++ b/front/src/primitives/chip.tsx @@ -12,7 +12,6 @@ export const Chip = ({ label, href, replace, - target, className, ...props }: { @@ -21,7 +20,6 @@ export const Chip = ({ label: string; href: string | null; replace?: boolean; - target?: string; onPress?: (e: GestureResponderEvent) => void; className?: string; }) => { @@ -29,7 +27,6 @@ export const Chip = ({ { @@ -102,7 +101,6 @@ export const Link = ({ href?: string | null; replace?: boolean; download?: boolean; - target?: string; } & PressableProps) => { const linkProps = useLinkTo({ href, replace }); diff --git a/front/src/ui/admin/add.tsx b/front/src/ui/admin/add.tsx index ae707055..ae237785 100644 --- a/front/src/ui/admin/add.tsx +++ b/front/src/ui/admin/add.tsx @@ -67,8 +67,6 @@ const SearchResultItem = ({ icon={OpenInNew} as={Link} href={externalHref} - target="_blank" - className="absolute top-1 right-1 bg-gray-800/70 hover:bg-gray-800 focus:bg-gray-800" iconClassName="h-5 w-5 fill-slate-200 dark:fill-slate-200" /> )} diff --git a/front/src/ui/details/header.tsx b/front/src/ui/details/header.tsx index dd802d9b..f8cd56a7 100644 --- a/front/src/ui/details/header.tsx +++ b/front/src/ui/details/header.tsx @@ -121,7 +121,6 @@ const ButtonList = ({ icon={Theaters} as={Link} href={trailerUrl} - target="_blank" iconClassName={iconsClassName} {...tooltip(t("show.trailer"))} /> @@ -340,7 +339,6 @@ const ExternalIdChip = ({ {x.label ?? x.link} diff --git a/front/src/ui/login/logic.tsx b/front/src/ui/login/logic.tsx index 872fc0cb..50f6759b 100644 --- a/front/src/ui/login/logic.tsx +++ b/front/src/ui/login/logic.tsx @@ -55,22 +55,26 @@ export const login = async ( export const oidcLogin = async ( provider: string, code: string, + linkToToken: string | null, apiUrl?: string, ) => { apiUrl ??= defaultApiUrl; try { - const { token } = await queryFn({ + const ret = await queryFn({ method: "GET", url: `${apiUrl}/auth/oidc/callback/${provider}?token=${code}`, - authToken: null, - parser: z.object({ token: z.string() }), - }); - const user = await queryFn({ - method: "GET", - url: `${apiUrl}/auth/users/me`, - authToken: token, - parser: User, + authToken: linkToToken, + parser: linkToToken ? z.object({ token: z.string() }) : User, }); + const token = linkToToken ?? (ret as { token: string }).token; + const user = linkToToken + ? (ret as User) + : await queryFn({ + method: "GET", + url: `${apiUrl}/auth/users/me`, + authToken: token, + parser: User, + }); const account: Account = { ...user, apiUrl, token, selected: true }; addAccount(account); return { ok: true, value: account }; diff --git a/front/src/ui/login/oidc-callback.tsx b/front/src/ui/login/oidc-callback.tsx index 822481eb..f3248981 100644 --- a/front/src/ui/login/oidc-callback.tsx +++ b/front/src/ui/login/oidc-callback.tsx @@ -1,33 +1,38 @@ import { useRouter } from "expo-router"; -import { useEffect, useRef } from "react"; +import { useEffect } from "react"; import { P } from "~/primitives"; +import { useToken } from "~/providers/account-context"; import { useQueryState } from "~/utils"; import { oidcLogin } from "./logic"; export const OidcCallbackPage = () => { + const { authToken } = useToken(); const [apiUrl] = useQueryState("apiUrl", undefined!); const [provider] = useQueryState("provider", undefined!); const [code] = useQueryState("token", undefined!); const [error] = useQueryState("error", undefined!); + const [link] = useQueryState("link", undefined!); - const hasRun = useRef(false); const router = useRouter(); + // biome-ignore lint/correctness/useExhaustiveDependencies: useMountEffect useEffect(() => { - if (hasRun.current) return; - hasRun.current = true; - function onError(error: string) { router.replace({ pathname: "/login", params: { error, apiUrl } }); } async function run() { - const { error: loginError } = await oidcLogin(provider, code, apiUrl); + const { error: loginError } = await oidcLogin( + provider, + code, + link ? authToken : null, + apiUrl, + ); if (loginError) onError(loginError); - else router.replace("/"); + else router.replace(link ? "/settings" : "/"); } if (error) onError(error); else run(); - }, [provider, code, apiUrl, router, error]); + }, []); return

{"Loading"}

; }; diff --git a/front/src/ui/login/oidc.tsx b/front/src/ui/login/oidc.tsx index c8333b1c..5bea56c9 100644 --- a/front/src/ui/login/oidc.tsx +++ b/front/src/ui/login/oidc.tsx @@ -37,7 +37,7 @@ export const OidcLogin = ({