Spaces:
Running
Running
Enhance Dynamic User Attribute Handling in OIDC Integration (#885)
Browse files* Refactor `updateUser` to Support Dynamic `nameClaim` Configuration
* style: run Prettier to fix lint errors
* Validate NAME_CLAIM in OIDConfig schema
* Refactor userData parsing
---------
Co-authored-by: Nathan Sarrazin <[email protected]>
Co-authored-by: ‘XGungo’ <‘[email protected]’>
- .env +3 -1
- src/lib/server/auth.ts +6 -1
- src/routes/login/callback/updateUser.ts +16 -1
.env
CHANGED
|
@@ -29,13 +29,15 @@ OPENID_CONFIG=`{
|
|
| 29 |
"PROVIDER_URL": "",
|
| 30 |
"CLIENT_ID": "",
|
| 31 |
"CLIENT_SECRET": "",
|
| 32 |
-
"SCOPES": ""
|
|
|
|
| 33 |
}`
|
| 34 |
|
| 35 |
# /!\ legacy openid settings, prefer the config above
|
| 36 |
OPENID_CLIENT_ID=
|
| 37 |
OPENID_CLIENT_SECRET=
|
| 38 |
OPENID_SCOPES="openid profile" # Add "email" for some providers like Google that do not provide preferred_username
|
|
|
|
| 39 |
OPENID_PROVIDER_URL=https://huggingface.co # for Google, use https://accounts.google.com
|
| 40 |
OPENID_TOLERANCE=
|
| 41 |
OPENID_RESOURCE=
|
|
|
|
| 29 |
"PROVIDER_URL": "",
|
| 30 |
"CLIENT_ID": "",
|
| 31 |
"CLIENT_SECRET": "",
|
| 32 |
+
"SCOPES": "",
|
| 33 |
+
"NAME_CLAIM": ""
|
| 34 |
}`
|
| 35 |
|
| 36 |
# /!\ legacy openid settings, prefer the config above
|
| 37 |
OPENID_CLIENT_ID=
|
| 38 |
OPENID_CLIENT_SECRET=
|
| 39 |
OPENID_SCOPES="openid profile" # Add "email" for some providers like Google that do not provide preferred_username
|
| 40 |
+
OPENID_NAME_CLAIM="name" # Change to "username" for some providers that do not provide name
|
| 41 |
OPENID_PROVIDER_URL=https://huggingface.co # for Google, use https://accounts.google.com
|
| 42 |
OPENID_TOLERANCE=
|
| 43 |
OPENID_RESOURCE=
|
src/lib/server/auth.ts
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
| 6 |
OPENID_CLIENT_SECRET,
|
| 7 |
OPENID_PROVIDER_URL,
|
| 8 |
OPENID_SCOPES,
|
|
|
|
| 9 |
OPENID_TOLERANCE,
|
| 10 |
OPENID_RESOURCE,
|
| 11 |
OPENID_CONFIG,
|
|
@@ -32,12 +33,16 @@ const stringWithDefault = (value: string) =>
|
|
| 32 |
.default(value)
|
| 33 |
.transform((el) => (el ? el : value));
|
| 34 |
|
| 35 |
-
const OIDConfig = z
|
| 36 |
.object({
|
| 37 |
CLIENT_ID: stringWithDefault(OPENID_CLIENT_ID),
|
| 38 |
CLIENT_SECRET: stringWithDefault(OPENID_CLIENT_SECRET),
|
| 39 |
PROVIDER_URL: stringWithDefault(OPENID_PROVIDER_URL),
|
| 40 |
SCOPES: stringWithDefault(OPENID_SCOPES),
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
TOLERANCE: stringWithDefault(OPENID_TOLERANCE),
|
| 42 |
RESOURCE: stringWithDefault(OPENID_RESOURCE),
|
| 43 |
})
|
|
|
|
| 6 |
OPENID_CLIENT_SECRET,
|
| 7 |
OPENID_PROVIDER_URL,
|
| 8 |
OPENID_SCOPES,
|
| 9 |
+
OPENID_NAME_CLAIM,
|
| 10 |
OPENID_TOLERANCE,
|
| 11 |
OPENID_RESOURCE,
|
| 12 |
OPENID_CONFIG,
|
|
|
|
| 33 |
.default(value)
|
| 34 |
.transform((el) => (el ? el : value));
|
| 35 |
|
| 36 |
+
export const OIDConfig = z
|
| 37 |
.object({
|
| 38 |
CLIENT_ID: stringWithDefault(OPENID_CLIENT_ID),
|
| 39 |
CLIENT_SECRET: stringWithDefault(OPENID_CLIENT_SECRET),
|
| 40 |
PROVIDER_URL: stringWithDefault(OPENID_PROVIDER_URL),
|
| 41 |
SCOPES: stringWithDefault(OPENID_SCOPES),
|
| 42 |
+
NAME_CLAIM: stringWithDefault(OPENID_NAME_CLAIM).refine(
|
| 43 |
+
(el) => !["preferred_username", "email", "picture", "sub"].includes(el),
|
| 44 |
+
{ message: "nameClaim cannot be one of the restricted keys." }
|
| 45 |
+
),
|
| 46 |
TOLERANCE: stringWithDefault(OPENID_TOLERANCE),
|
| 47 |
RESOURCE: stringWithDefault(OPENID_RESOURCE),
|
| 48 |
})
|
src/routes/login/callback/updateUser.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { error, type Cookies } from "@sveltejs/kit";
|
|
| 8 |
import crypto from "crypto";
|
| 9 |
import { sha256 } from "$lib/utils/sha256";
|
| 10 |
import { addWeeks } from "date-fns";
|
|
|
|
| 11 |
|
| 12 |
export async function updateUser(params: {
|
| 13 |
userData: UserinfoResponse;
|
|
@@ -38,10 +39,24 @@ export async function updateUser(params: {
|
|
| 38 |
sub: z.string(),
|
| 39 |
email: z.string().email().optional(),
|
| 40 |
})
|
|
|
|
| 41 |
.refine((data) => data.preferred_username || data.email, {
|
| 42 |
message: "Either preferred_username or email must be provided by the provider.",
|
| 43 |
})
|
| 44 |
-
.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
|
| 46 |
// check if user already exists
|
| 47 |
const existingUser = await collections.users.findOne({ hfUserId });
|
|
|
|
| 8 |
import crypto from "crypto";
|
| 9 |
import { sha256 } from "$lib/utils/sha256";
|
| 10 |
import { addWeeks } from "date-fns";
|
| 11 |
+
import { OIDConfig } from "$lib/server/auth";
|
| 12 |
|
| 13 |
export async function updateUser(params: {
|
| 14 |
userData: UserinfoResponse;
|
|
|
|
| 39 |
sub: z.string(),
|
| 40 |
email: z.string().email().optional(),
|
| 41 |
})
|
| 42 |
+
.setKey(OIDConfig.NAME_CLAIM, z.string())
|
| 43 |
.refine((data) => data.preferred_username || data.email, {
|
| 44 |
message: "Either preferred_username or email must be provided by the provider.",
|
| 45 |
})
|
| 46 |
+
.transform((data) => ({
|
| 47 |
+
...data,
|
| 48 |
+
name: data[OIDConfig.NAME_CLAIM],
|
| 49 |
+
}))
|
| 50 |
+
.parse(userData) as {
|
| 51 |
+
preferred_username?: string;
|
| 52 |
+
email?: string;
|
| 53 |
+
picture?: string;
|
| 54 |
+
sub: string;
|
| 55 |
+
name: string;
|
| 56 |
+
} & Record<string, string>;
|
| 57 |
+
|
| 58 |
+
// Dynamically access user data based on NAME_CLAIM from environment
|
| 59 |
+
// This approach allows us to adapt to different OIDC providers flexibly.
|
| 60 |
|
| 61 |
// check if user already exists
|
| 62 |
const existingUser = await collections.users.findOne({ hfUserId });
|