Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .env
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ PUBLIC_COMMIT_SHA=
ALLOW_INSECURE_COOKIES=false # LEGACY! Use COOKIE_SECURE and COOKIE_SAMESITE instead
PARQUET_EXPORT_SECRET=#DEPRECATED, use ADMIN_API_SECRET instead
RATE_LIMIT= # /!\ DEPRECATED definition of messages per minute. Use USAGE_LIMITS.messagesPerMinute instead
OPENID_CLIENT_ID=
OPENID_CLIENT_ID="" # You can set to "__CIMD__" for automatic oauth app creation when deployed
OPENID_CLIENT_SECRET=
OPENID_SCOPES="openid profile" # Add "email" for some providers like Google that do not provide preferred_username
OPENID_NAME_CLAIM="name" # Change to "username" for some providers that do not provide name
Expand Down
3 changes: 2 additions & 1 deletion src/hooks.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,8 @@ export const handle: Handle = async ({ event, resolve }) => {

const auth = await authenticateRequest(
{ type: "svelte", value: event.request.headers },
{ type: "svelte", value: event.cookies }
{ type: "svelte", value: event.cookies },
event.url
);

event.locals.sessionId = auth.sessionId;
Expand Down
4 changes: 4 additions & 0 deletions src/lib/server/api/authPlugin.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
import Elysia from "elysia";
import { authenticateRequest } from "../auth";
import { config } from "../config";

export const authPlugin = new Elysia({ name: "auth" }).derive(
{ as: "scoped" },
async ({
headers,
cookie,
request,
}): Promise<{
locals: App.Locals;
}> => {
request.url;
const auth = await authenticateRequest(
{ type: "elysia", value: headers },
{ type: "elysia", value: cookie },
new URL(request.url, config.PUBLIC_ORIGIN),
true
);
return {
Expand Down
34 changes: 23 additions & 11 deletions src/lib/server/auth.ts
Copy link
Member Author

@coyotte508 coyotte508 Nov 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

instead of forwarding url everywhere, we could use contextStore like here: https://github.com/huggingface/Mongoku/blob/2a6c715c5bf4a7f7351aa3b3ce90ecd857e61d70/src/hooks.server.ts#L17-L48

(not sure for compat with non-node runtimes)

could also be used for structued logs (eg request id, ...)

Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,8 @@ export function refreshSessionCookie(cookies: Cookies, sessionId: string) {

export async function findUser(
sessionId: string,
coupledCookieHash?: string
coupledCookieHash: string | undefined,
url: URL
): Promise<{
user: User | null;
invalidateSession: boolean;
Expand Down Expand Up @@ -121,7 +122,8 @@ export async function findUser(
// Attempt to refresh the token
const newTokenSet = await refreshOAuthToken(
{ redirectURI: `${config.PUBLIC_ORIGIN}${base}/login/callback` },
session.oauth.refreshToken
session.oauth.refreshToken,
url
);

if (!newTokenSet || !newTokenSet.access_token) {
Expand Down Expand Up @@ -236,7 +238,7 @@ export async function generateCsrfToken(

let lastIssuer: Issuer<BaseClient> | null = null;
let lastIssuerFetchedAt: Date | null = null;
async function getOIDCClient(settings: OIDCSettings): Promise<BaseClient> {
async function getOIDCClient(settings: OIDCSettings, url: URL): Promise<BaseClient> {
if (
lastIssuer &&
lastIssuerFetchedAt &&
Expand All @@ -261,6 +263,13 @@ async function getOIDCClient(settings: OIDCSettings): Promise<BaseClient> {
id_token_signed_response_alg: OIDConfig.ID_TOKEN_SIGNED_RESPONSE_ALG || undefined,
};

if (OIDConfig.CLIENT_ID === "__CIMD__") {
OIDConfig.CLIENT_ID = new URL(
"/.well-known/oauth-cimd",
config.PUBLIC_ORIGIN || url.origin
).toString();
}

const alg_supported = issuer.metadata["id_token_signing_alg_values_supported"];

if (Array.isArray(alg_supported)) {
Expand All @@ -272,9 +281,9 @@ async function getOIDCClient(settings: OIDCSettings): Promise<BaseClient> {

export async function getOIDCAuthorizationUrl(
settings: OIDCSettings,
params: { sessionId: string; next?: string }
params: { sessionId: string; next?: string; url: URL }
): Promise<string> {
const client = await getOIDCClient(settings);
const client = await getOIDCClient(settings, params.url);
const csrfToken = await generateCsrfToken(
params.sessionId,
settings.redirectURI,
Expand All @@ -291,9 +300,10 @@ export async function getOIDCAuthorizationUrl(
export async function getOIDCUserData(
settings: OIDCSettings,
code: string,
iss?: string
iss: string | undefined,
url: URL
): Promise<OIDCUserInfo> {
const client = await getOIDCClient(settings);
const client = await getOIDCClient(settings, url);
const token = await client.callback(settings.redirectURI, { code, iss });
const userData = await client.userinfo(token);

Expand All @@ -305,9 +315,10 @@ export async function getOIDCUserData(
*/
export async function refreshOAuthToken(
settings: OIDCSettings,
refreshToken: string
refreshToken: string,
url: URL
): Promise<TokenSet | null> {
const client = await getOIDCClient(settings);
const client = await getOIDCClient(settings, url);
const tokenSet = await client.refresh(refreshToken);
return tokenSet;
}
Expand Down Expand Up @@ -371,6 +382,7 @@ export async function getCoupledCookieHash(cookie: CookieRecord): Promise<string
export async function authenticateRequest(
headers: HeaderRecord,
cookie: CookieRecord,
url: URL,
isApi?: boolean
): Promise<App.Locals & { secretSessionId: string }> {
// once the entire API has been moved to elysia
Expand Down Expand Up @@ -415,7 +427,7 @@ export async function authenticateRequest(
secretSessionId = token;
sessionId = await sha256(token);

const result = await findUser(sessionId, await getCoupledCookieHash(cookie));
const result = await findUser(sessionId, await getCoupledCookieHash(cookie), url);

if (result.invalidateSession) {
secretSessionId = crypto.randomUUID();
Expand Down Expand Up @@ -539,7 +551,7 @@ export async function triggerOauthFlow({

const authorizationUrl = await getOIDCAuthorizationUrl(
{ redirectURI },
{ sessionId: locals.sessionId, next }
{ sessionId: locals.sessionId, next, url }
);

throw redirect(302, authorizationUrl);
Expand Down
27 changes: 27 additions & 0 deletions src/routes/.well-known/.oauth-cimd/+server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { OIDConfig } from "$lib/server/auth";
import { config } from "$lib/server/config";

export const GET = ({ url }) => {
if (!OIDConfig.CLIENT_ID) {
return new Response("Client ID not found", { status: 404 });
}
if (OIDConfig.CLIENT_ID !== "__CIMD__") {
return new Response("Client ID is manually set to something other than '__CIMD__'", {
status: 404,
});
}
return new Response(
JSON.stringify({
client_id: new URL("/.well-known/oauth-cimd", config.PUBLIC_ORIGIN || url.origin).toString(),
client_name: config.PUBLIC_APP_NAME,
redirect_uris: [new URL("/login/callback", config.PUBLIC_ORIGIN || url.origin).toString()],
token_endpoint_auth_method: "none",
scopes: OIDConfig.SCOPES,
}),
{
headers: {
"Content-Type": "application/json",
},
}
);
};
3 changes: 2 additions & 1 deletion src/routes/login/callback/+server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,8 @@ export async function GET({ url, locals, cookies, request, getClientAddress }) {
const { userData, token } = await getOIDCUserData(
{ redirectURI: validatedToken.redirectUrl },
code,
iss
iss,
url
);

// Filter by allowed user emails or domains
Expand Down
Loading