Add CIMD support for automatic oauth app creation, and PKCE support (#1978)
Browse files* Add CIMD support
* update env file
* Add PKCE and fix CIMD
* add spec refs
- .env +1 -4
- src/hooks.server.ts +5 -8
- src/lib/server/api/authPlugin.ts +3 -0
- src/lib/server/auth.ts +50 -21
- src/routes/.well-known/oauth-cimd/+server.ts +35 -0
- src/routes/login/+server.ts +2 -2
- src/routes/login/callback/+server.ts +8 -1
.env
CHANGED
|
@@ -32,7 +32,7 @@ PUBLIC_APPLE_APP_ID=
|
|
| 32 |
|
| 33 |
COUPLE_SESSION_WITH_COOKIE_NAME=
|
| 34 |
# when OPEN_ID is configured, users are required to login after the welcome modal
|
| 35 |
-
OPENID_CLIENT_ID=
|
| 36 |
OPENID_CLIENT_SECRET=
|
| 37 |
OPENID_SCOPES="openid profile inference-api read-mcp"
|
| 38 |
USE_USER_TOKEN=
|
|
@@ -173,9 +173,6 @@ PUBLIC_COMMIT_SHA=
|
|
| 173 |
ALLOW_INSECURE_COOKIES=false # LEGACY! Use COOKIE_SECURE and COOKIE_SAMESITE instead
|
| 174 |
PARQUET_EXPORT_SECRET=#DEPRECATED, use ADMIN_API_SECRET instead
|
| 175 |
RATE_LIMIT= # /!\ DEPRECATED definition of messages per minute. Use USAGE_LIMITS.messagesPerMinute instead
|
| 176 |
-
OPENID_CLIENT_ID=
|
| 177 |
-
OPENID_CLIENT_SECRET=
|
| 178 |
-
OPENID_SCOPES="openid profile" # Add "email" for some providers like Google that do not provide preferred_username
|
| 179 |
OPENID_NAME_CLAIM="name" # Change to "username" for some providers that do not provide name
|
| 180 |
OPENID_PROVIDER_URL=https://huggingface.co # for Google, use https://accounts.google.com
|
| 181 |
OPENID_TOLERANCE=
|
|
|
|
| 32 |
|
| 33 |
COUPLE_SESSION_WITH_COOKIE_NAME=
|
| 34 |
# when OPEN_ID is configured, users are required to login after the welcome modal
|
| 35 |
+
OPENID_CLIENT_ID="" # You can set to "__CIMD__" for automatic oauth app creation when deployed, see https://datatracker.ietf.org/doc/draft-ietf-oauth-client-id-metadata-document/
|
| 36 |
OPENID_CLIENT_SECRET=
|
| 37 |
OPENID_SCOPES="openid profile inference-api read-mcp"
|
| 38 |
USE_USER_TOKEN=
|
|
|
|
| 173 |
ALLOW_INSECURE_COOKIES=false # LEGACY! Use COOKIE_SECURE and COOKIE_SAMESITE instead
|
| 174 |
PARQUET_EXPORT_SECRET=#DEPRECATED, use ADMIN_API_SECRET instead
|
| 175 |
RATE_LIMIT= # /!\ DEPRECATED definition of messages per minute. Use USAGE_LIMITS.messagesPerMinute instead
|
|
|
|
|
|
|
|
|
|
| 176 |
OPENID_NAME_CLAIM="name" # Change to "username" for some providers that do not provide name
|
| 177 |
OPENID_PROVIDER_URL=https://huggingface.co # for Google, use https://accounts.google.com
|
| 178 |
OPENID_TOLERANCE=
|
src/hooks.server.ts
CHANGED
|
@@ -137,12 +137,13 @@ export const handle: Handle = async ({ event, resolve }) => {
|
|
| 137 |
|
| 138 |
const auth = await authenticateRequest(
|
| 139 |
{ type: "svelte", value: event.request.headers },
|
| 140 |
-
{ type: "svelte", value: event.cookies }
|
|
|
|
| 141 |
);
|
| 142 |
|
| 143 |
event.locals.sessionId = auth.sessionId;
|
| 144 |
|
| 145 |
-
if (loginEnabled && !auth.user) {
|
| 146 |
if (config.AUTOMATIC_LOGIN === "true") {
|
| 147 |
// AUTOMATIC_LOGIN: always redirect to OAuth flow (unless already on login or healthcheck pages)
|
| 148 |
if (
|
|
@@ -151,11 +152,7 @@ export const handle: Handle = async ({ event, resolve }) => {
|
|
| 151 |
) {
|
| 152 |
// To get the same CSRF token after callback
|
| 153 |
refreshSessionCookie(event.cookies, auth.secretSessionId);
|
| 154 |
-
return await triggerOauthFlow(
|
| 155 |
-
request: event.request,
|
| 156 |
-
url: event.url,
|
| 157 |
-
locals: event.locals,
|
| 158 |
-
});
|
| 159 |
}
|
| 160 |
} else {
|
| 161 |
// Redirect to OAuth flow unless on the authorized pages (home, shared conversation, login, healthcheck, model thumbnails)
|
|
@@ -171,7 +168,7 @@ export const handle: Handle = async ({ event, resolve }) => {
|
|
| 171 |
!event.url.pathname.startsWith(`${base}/api`)
|
| 172 |
) {
|
| 173 |
refreshSessionCookie(event.cookies, auth.secretSessionId);
|
| 174 |
-
return triggerOauthFlow(
|
| 175 |
}
|
| 176 |
}
|
| 177 |
}
|
|
|
|
| 137 |
|
| 138 |
const auth = await authenticateRequest(
|
| 139 |
{ type: "svelte", value: event.request.headers },
|
| 140 |
+
{ type: "svelte", value: event.cookies },
|
| 141 |
+
event.url
|
| 142 |
);
|
| 143 |
|
| 144 |
event.locals.sessionId = auth.sessionId;
|
| 145 |
|
| 146 |
+
if (loginEnabled && !auth.user && !event.url.pathname.startsWith(`${base}/.well-known/`)) {
|
| 147 |
if (config.AUTOMATIC_LOGIN === "true") {
|
| 148 |
// AUTOMATIC_LOGIN: always redirect to OAuth flow (unless already on login or healthcheck pages)
|
| 149 |
if (
|
|
|
|
| 152 |
) {
|
| 153 |
// To get the same CSRF token after callback
|
| 154 |
refreshSessionCookie(event.cookies, auth.secretSessionId);
|
| 155 |
+
return await triggerOauthFlow(event);
|
|
|
|
|
|
|
|
|
|
|
|
|
| 156 |
}
|
| 157 |
} else {
|
| 158 |
// Redirect to OAuth flow unless on the authorized pages (home, shared conversation, login, healthcheck, model thumbnails)
|
|
|
|
| 168 |
!event.url.pathname.startsWith(`${base}/api`)
|
| 169 |
) {
|
| 170 |
refreshSessionCookie(event.cookies, auth.secretSessionId);
|
| 171 |
+
return triggerOauthFlow(event);
|
| 172 |
}
|
| 173 |
}
|
| 174 |
}
|
src/lib/server/api/authPlugin.ts
CHANGED
|
@@ -1,17 +1,20 @@
|
|
| 1 |
import Elysia from "elysia";
|
| 2 |
import { authenticateRequest } from "../auth";
|
|
|
|
| 3 |
|
| 4 |
export const authPlugin = new Elysia({ name: "auth" }).derive(
|
| 5 |
{ as: "scoped" },
|
| 6 |
async ({
|
| 7 |
headers,
|
| 8 |
cookie,
|
|
|
|
| 9 |
}): Promise<{
|
| 10 |
locals: App.Locals;
|
| 11 |
}> => {
|
| 12 |
const auth = await authenticateRequest(
|
| 13 |
{ type: "elysia", value: headers },
|
| 14 |
{ type: "elysia", value: cookie },
|
|
|
|
| 15 |
true
|
| 16 |
);
|
| 17 |
return {
|
|
|
|
| 1 |
import Elysia from "elysia";
|
| 2 |
import { authenticateRequest } from "../auth";
|
| 3 |
+
import { config } from "../config";
|
| 4 |
|
| 5 |
export const authPlugin = new Elysia({ name: "auth" }).derive(
|
| 6 |
{ as: "scoped" },
|
| 7 |
async ({
|
| 8 |
headers,
|
| 9 |
cookie,
|
| 10 |
+
request,
|
| 11 |
}): Promise<{
|
| 12 |
locals: App.Locals;
|
| 13 |
}> => {
|
| 14 |
const auth = await authenticateRequest(
|
| 15 |
{ type: "elysia", value: headers },
|
| 16 |
{ type: "elysia", value: cookie },
|
| 17 |
+
new URL(request.url, config.PUBLIC_ORIGIN || undefined),
|
| 18 |
true
|
| 19 |
);
|
| 20 |
return {
|
src/lib/server/auth.ts
CHANGED
|
@@ -4,7 +4,9 @@ import {
|
|
| 4 |
type UserinfoResponse,
|
| 5 |
type TokenSet,
|
| 6 |
custom,
|
|
|
|
| 7 |
} from "openid-client";
|
|
|
|
| 8 |
import { addHours, addWeeks, differenceInMinutes, subMinutes } from "date-fns";
|
| 9 |
import { config } from "$lib/server/config";
|
| 10 |
import { sha256 } from "$lib/utils/sha256";
|
|
@@ -54,7 +56,7 @@ export const OIDConfig = z
|
|
| 54 |
})
|
| 55 |
.parse(JSON5.parse(config.OPENID_CONFIG || "{}"));
|
| 56 |
|
| 57 |
-
export const loginEnabled = !!OIDConfig.CLIENT_ID
|
| 58 |
|
| 59 |
const sameSite = z
|
| 60 |
.enum(["lax", "none", "strict"])
|
|
@@ -92,7 +94,8 @@ export function refreshSessionCookie(cookies: Cookies, sessionId: string) {
|
|
| 92 |
|
| 93 |
export async function findUser(
|
| 94 |
sessionId: string,
|
| 95 |
-
coupledCookieHash
|
|
|
|
| 96 |
): Promise<{
|
| 97 |
user: User | null;
|
| 98 |
invalidateSession: boolean;
|
|
@@ -121,7 +124,8 @@ export async function findUser(
|
|
| 121 |
// Attempt to refresh the token
|
| 122 |
const newTokenSet = await refreshOAuthToken(
|
| 123 |
{ redirectURI: `${config.PUBLIC_ORIGIN}${base}/login/callback` },
|
| 124 |
-
session.oauth.refreshToken
|
|
|
|
| 125 |
);
|
| 126 |
|
| 127 |
if (!newTokenSet || !newTokenSet.access_token) {
|
|
@@ -236,7 +240,7 @@ export async function generateCsrfToken(
|
|
| 236 |
|
| 237 |
let lastIssuer: Issuer<BaseClient> | null = null;
|
| 238 |
let lastIssuerFetchedAt: Date | null = null;
|
| 239 |
-
async function getOIDCClient(settings: OIDCSettings): Promise<BaseClient> {
|
| 240 |
if (
|
| 241 |
lastIssuer &&
|
| 242 |
lastIssuerFetchedAt &&
|
|
@@ -261,6 +265,14 @@ async function getOIDCClient(settings: OIDCSettings): Promise<BaseClient> {
|
|
| 261 |
id_token_signed_response_alg: OIDConfig.ID_TOKEN_SIGNED_RESPONSE_ALG || undefined,
|
| 262 |
};
|
| 263 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 264 |
const alg_supported = issuer.metadata["id_token_signing_alg_values_supported"];
|
| 265 |
|
| 266 |
if (Array.isArray(alg_supported)) {
|
|
@@ -272,16 +284,29 @@ async function getOIDCClient(settings: OIDCSettings): Promise<BaseClient> {
|
|
| 272 |
|
| 273 |
export async function getOIDCAuthorizationUrl(
|
| 274 |
settings: OIDCSettings,
|
| 275 |
-
params: { sessionId: string; next?: string }
|
| 276 |
): Promise<string> {
|
| 277 |
-
const client = await getOIDCClient(settings);
|
| 278 |
const csrfToken = await generateCsrfToken(
|
| 279 |
params.sessionId,
|
| 280 |
settings.redirectURI,
|
| 281 |
sanitizeReturnPath(params.next)
|
| 282 |
);
|
| 283 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 284 |
return client.authorizationUrl({
|
|
|
|
|
|
|
| 285 |
scope: OIDConfig.SCOPES,
|
| 286 |
state: csrfToken,
|
| 287 |
resource: OIDConfig.RESOURCE || undefined,
|
|
@@ -291,10 +316,19 @@ export async function getOIDCAuthorizationUrl(
|
|
| 291 |
export async function getOIDCUserData(
|
| 292 |
settings: OIDCSettings,
|
| 293 |
code: string,
|
| 294 |
-
|
|
|
|
|
|
|
| 295 |
): Promise<OIDCUserInfo> {
|
| 296 |
-
const client = await getOIDCClient(settings);
|
| 297 |
-
const token = await client.callback(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 298 |
const userData = await client.userinfo(token);
|
| 299 |
|
| 300 |
return { token, userData };
|
|
@@ -305,9 +339,10 @@ export async function getOIDCUserData(
|
|
| 305 |
*/
|
| 306 |
export async function refreshOAuthToken(
|
| 307 |
settings: OIDCSettings,
|
| 308 |
-
refreshToken: string
|
|
|
|
| 309 |
): Promise<TokenSet | null> {
|
| 310 |
-
const client = await getOIDCClient(settings);
|
| 311 |
const tokenSet = await client.refresh(refreshToken);
|
| 312 |
return tokenSet;
|
| 313 |
}
|
|
@@ -371,6 +406,7 @@ export async function getCoupledCookieHash(cookie: CookieRecord): Promise<string
|
|
| 371 |
export async function authenticateRequest(
|
| 372 |
headers: HeaderRecord,
|
| 373 |
cookie: CookieRecord,
|
|
|
|
| 374 |
isApi?: boolean
|
| 375 |
): Promise<App.Locals & { secretSessionId: string }> {
|
| 376 |
// once the entire API has been moved to elysia
|
|
@@ -415,7 +451,7 @@ export async function authenticateRequest(
|
|
| 415 |
secretSessionId = token;
|
| 416 |
sessionId = await sha256(token);
|
| 417 |
|
| 418 |
-
const result = await findUser(sessionId, await getCoupledCookieHash(cookie));
|
| 419 |
|
| 420 |
if (result.invalidateSession) {
|
| 421 |
secretSessionId = crypto.randomUUID();
|
|
@@ -502,14 +538,7 @@ export async function authenticateRequest(
|
|
| 502 |
return { user: undefined, sessionId, secretSessionId, isAdmin: false };
|
| 503 |
}
|
| 504 |
|
| 505 |
-
export async function triggerOauthFlow({
|
| 506 |
-
url,
|
| 507 |
-
locals,
|
| 508 |
-
}: {
|
| 509 |
-
request: Request;
|
| 510 |
-
url: URL;
|
| 511 |
-
locals: App.Locals;
|
| 512 |
-
}): Promise<Response> {
|
| 513 |
// const referer = request.headers.get("referer");
|
| 514 |
// let redirectURI = `${(referer ? new URL(referer) : url).origin}${base}/login/callback`;
|
| 515 |
let redirectURI = `${url.origin}${base}/login/callback`;
|
|
@@ -539,7 +568,7 @@ export async function triggerOauthFlow({
|
|
| 539 |
|
| 540 |
const authorizationUrl = await getOIDCAuthorizationUrl(
|
| 541 |
{ redirectURI },
|
| 542 |
-
{ sessionId: locals.sessionId, next }
|
| 543 |
);
|
| 544 |
|
| 545 |
throw redirect(302, authorizationUrl);
|
|
|
|
| 4 |
type UserinfoResponse,
|
| 5 |
type TokenSet,
|
| 6 |
custom,
|
| 7 |
+
generators,
|
| 8 |
} from "openid-client";
|
| 9 |
+
import type { RequestEvent } from "@sveltejs/kit";
|
| 10 |
import { addHours, addWeeks, differenceInMinutes, subMinutes } from "date-fns";
|
| 11 |
import { config } from "$lib/server/config";
|
| 12 |
import { sha256 } from "$lib/utils/sha256";
|
|
|
|
| 56 |
})
|
| 57 |
.parse(JSON5.parse(config.OPENID_CONFIG || "{}"));
|
| 58 |
|
| 59 |
+
export const loginEnabled = !!OIDConfig.CLIENT_ID;
|
| 60 |
|
| 61 |
const sameSite = z
|
| 62 |
.enum(["lax", "none", "strict"])
|
|
|
|
| 94 |
|
| 95 |
export async function findUser(
|
| 96 |
sessionId: string,
|
| 97 |
+
coupledCookieHash: string | undefined,
|
| 98 |
+
url: URL
|
| 99 |
): Promise<{
|
| 100 |
user: User | null;
|
| 101 |
invalidateSession: boolean;
|
|
|
|
| 124 |
// Attempt to refresh the token
|
| 125 |
const newTokenSet = await refreshOAuthToken(
|
| 126 |
{ redirectURI: `${config.PUBLIC_ORIGIN}${base}/login/callback` },
|
| 127 |
+
session.oauth.refreshToken,
|
| 128 |
+
url
|
| 129 |
);
|
| 130 |
|
| 131 |
if (!newTokenSet || !newTokenSet.access_token) {
|
|
|
|
| 240 |
|
| 241 |
let lastIssuer: Issuer<BaseClient> | null = null;
|
| 242 |
let lastIssuerFetchedAt: Date | null = null;
|
| 243 |
+
async function getOIDCClient(settings: OIDCSettings, url: URL): Promise<BaseClient> {
|
| 244 |
if (
|
| 245 |
lastIssuer &&
|
| 246 |
lastIssuerFetchedAt &&
|
|
|
|
| 265 |
id_token_signed_response_alg: OIDConfig.ID_TOKEN_SIGNED_RESPONSE_ALG || undefined,
|
| 266 |
};
|
| 267 |
|
| 268 |
+
if (OIDConfig.CLIENT_ID === "__CIMD__") {
|
| 269 |
+
// See https://datatracker.ietf.org/doc/draft-ietf-oauth-client-id-metadata-document/
|
| 270 |
+
client_config.client_id = new URL(
|
| 271 |
+
`${base}/.well-known/oauth-cimd`,
|
| 272 |
+
config.PUBLIC_ORIGIN || url.origin
|
| 273 |
+
).toString();
|
| 274 |
+
}
|
| 275 |
+
|
| 276 |
const alg_supported = issuer.metadata["id_token_signing_alg_values_supported"];
|
| 277 |
|
| 278 |
if (Array.isArray(alg_supported)) {
|
|
|
|
| 284 |
|
| 285 |
export async function getOIDCAuthorizationUrl(
|
| 286 |
settings: OIDCSettings,
|
| 287 |
+
params: { sessionId: string; next?: string; url: URL; cookies: Cookies }
|
| 288 |
): Promise<string> {
|
| 289 |
+
const client = await getOIDCClient(settings, params.url);
|
| 290 |
const csrfToken = await generateCsrfToken(
|
| 291 |
params.sessionId,
|
| 292 |
settings.redirectURI,
|
| 293 |
sanitizeReturnPath(params.next)
|
| 294 |
);
|
| 295 |
|
| 296 |
+
const codeVerifier = generators.codeVerifier();
|
| 297 |
+
const codeChallenge = generators.codeChallenge(codeVerifier);
|
| 298 |
+
|
| 299 |
+
params.cookies.set("hfChat-codeVerifier", codeVerifier, {
|
| 300 |
+
path: "/",
|
| 301 |
+
sameSite,
|
| 302 |
+
secure,
|
| 303 |
+
httpOnly: true,
|
| 304 |
+
expires: addHours(new Date(), 1),
|
| 305 |
+
});
|
| 306 |
+
|
| 307 |
return client.authorizationUrl({
|
| 308 |
+
code_challenge_method: "S256",
|
| 309 |
+
code_challenge: codeChallenge,
|
| 310 |
scope: OIDConfig.SCOPES,
|
| 311 |
state: csrfToken,
|
| 312 |
resource: OIDConfig.RESOURCE || undefined,
|
|
|
|
| 316 |
export async function getOIDCUserData(
|
| 317 |
settings: OIDCSettings,
|
| 318 |
code: string,
|
| 319 |
+
codeVerifier: string,
|
| 320 |
+
iss: string | undefined,
|
| 321 |
+
url: URL
|
| 322 |
): Promise<OIDCUserInfo> {
|
| 323 |
+
const client = await getOIDCClient(settings, url);
|
| 324 |
+
const token = await client.callback(
|
| 325 |
+
settings.redirectURI,
|
| 326 |
+
{
|
| 327 |
+
code,
|
| 328 |
+
iss,
|
| 329 |
+
},
|
| 330 |
+
{ code_verifier: codeVerifier }
|
| 331 |
+
);
|
| 332 |
const userData = await client.userinfo(token);
|
| 333 |
|
| 334 |
return { token, userData };
|
|
|
|
| 339 |
*/
|
| 340 |
export async function refreshOAuthToken(
|
| 341 |
settings: OIDCSettings,
|
| 342 |
+
refreshToken: string,
|
| 343 |
+
url: URL
|
| 344 |
): Promise<TokenSet | null> {
|
| 345 |
+
const client = await getOIDCClient(settings, url);
|
| 346 |
const tokenSet = await client.refresh(refreshToken);
|
| 347 |
return tokenSet;
|
| 348 |
}
|
|
|
|
| 406 |
export async function authenticateRequest(
|
| 407 |
headers: HeaderRecord,
|
| 408 |
cookie: CookieRecord,
|
| 409 |
+
url: URL,
|
| 410 |
isApi?: boolean
|
| 411 |
): Promise<App.Locals & { secretSessionId: string }> {
|
| 412 |
// once the entire API has been moved to elysia
|
|
|
|
| 451 |
secretSessionId = token;
|
| 452 |
sessionId = await sha256(token);
|
| 453 |
|
| 454 |
+
const result = await findUser(sessionId, await getCoupledCookieHash(cookie), url);
|
| 455 |
|
| 456 |
if (result.invalidateSession) {
|
| 457 |
secretSessionId = crypto.randomUUID();
|
|
|
|
| 538 |
return { user: undefined, sessionId, secretSessionId, isAdmin: false };
|
| 539 |
}
|
| 540 |
|
| 541 |
+
export async function triggerOauthFlow({ url, locals, cookies }: RequestEvent): Promise<Response> {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 542 |
// const referer = request.headers.get("referer");
|
| 543 |
// let redirectURI = `${(referer ? new URL(referer) : url).origin}${base}/login/callback`;
|
| 544 |
let redirectURI = `${url.origin}${base}/login/callback`;
|
|
|
|
| 568 |
|
| 569 |
const authorizationUrl = await getOIDCAuthorizationUrl(
|
| 570 |
{ redirectURI },
|
| 571 |
+
{ sessionId: locals.sessionId, next, url, cookies }
|
| 572 |
);
|
| 573 |
|
| 574 |
throw redirect(302, authorizationUrl);
|
src/routes/.well-known/oauth-cimd/+server.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { base } from "$app/paths";
|
| 2 |
+
import { OIDConfig } from "$lib/server/auth";
|
| 3 |
+
import { config } from "$lib/server/config";
|
| 4 |
+
|
| 5 |
+
/**
|
| 6 |
+
* See https://datatracker.ietf.org/doc/draft-ietf-oauth-client-id-metadata-document/
|
| 7 |
+
*/
|
| 8 |
+
export const GET = ({ url }) => {
|
| 9 |
+
if (!OIDConfig.CLIENT_ID) {
|
| 10 |
+
return new Response("Client ID not found", { status: 404 });
|
| 11 |
+
}
|
| 12 |
+
if (OIDConfig.CLIENT_ID !== "__CIMD__") {
|
| 13 |
+
return new Response(
|
| 14 |
+
`Client ID is manually set to something other than '__CIMD__': ${OIDConfig.CLIENT_ID}`,
|
| 15 |
+
{
|
| 16 |
+
status: 404,
|
| 17 |
+
}
|
| 18 |
+
);
|
| 19 |
+
}
|
| 20 |
+
return new Response(
|
| 21 |
+
JSON.stringify({
|
| 22 |
+
client_id: new URL(url, config.PUBLIC_ORIGIN || url.origin).toString(),
|
| 23 |
+
client_name: config.PUBLIC_APP_NAME,
|
| 24 |
+
client_uri: `${config.PUBLIC_ORIGIN || url.origin}${base}`,
|
| 25 |
+
redirect_uris: [new URL("/login/callback", config.PUBLIC_ORIGIN || url.origin).toString()],
|
| 26 |
+
token_endpoint_auth_method: "none",
|
| 27 |
+
scopes: OIDConfig.SCOPES,
|
| 28 |
+
}),
|
| 29 |
+
{
|
| 30 |
+
headers: {
|
| 31 |
+
"Content-Type": "application/json",
|
| 32 |
+
},
|
| 33 |
+
}
|
| 34 |
+
);
|
| 35 |
+
};
|
src/routes/login/+server.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
import { triggerOauthFlow } from "$lib/server/auth";
|
| 2 |
|
| 3 |
-
export async function GET(
|
| 4 |
-
return await triggerOauthFlow(
|
| 5 |
}
|
|
|
|
| 1 |
import { triggerOauthFlow } from "$lib/server/auth";
|
| 2 |
|
| 3 |
+
export async function GET(event) {
|
| 4 |
+
return await triggerOauthFlow(event);
|
| 5 |
}
|
src/routes/login/callback/+server.ts
CHANGED
|
@@ -52,10 +52,17 @@ export async function GET({ url, locals, cookies, request, getClientAddress }) {
|
|
| 52 |
throw error(403, "Invalid or expired CSRF token");
|
| 53 |
}
|
| 54 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 55 |
const { userData, token } = await getOIDCUserData(
|
| 56 |
{ redirectURI: validatedToken.redirectUrl },
|
| 57 |
code,
|
| 58 |
-
|
|
|
|
|
|
|
| 59 |
);
|
| 60 |
|
| 61 |
// Filter by allowed user emails or domains
|
|
|
|
| 52 |
throw error(403, "Invalid or expired CSRF token");
|
| 53 |
}
|
| 54 |
|
| 55 |
+
const codeVerifier = cookies.get("hfChat-codeVerifier");
|
| 56 |
+
if (!codeVerifier) {
|
| 57 |
+
throw error(403, "Code verifier cookie not found");
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
const { userData, token } = await getOIDCUserData(
|
| 61 |
{ redirectURI: validatedToken.redirectUrl },
|
| 62 |
code,
|
| 63 |
+
codeVerifier,
|
| 64 |
+
iss,
|
| 65 |
+
url
|
| 66 |
);
|
| 67 |
|
| 68 |
// Filter by allowed user emails or domains
|