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