@better-auth/sso 1.4.6-beta.2 → 1.4.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +8 -8
- package/dist/client.d.mts +1 -1
- package/dist/{index-DCyJckhH.d.mts → index-D-JmJR9N.d.mts} +12 -12
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +174 -146
- package/package.json +8 -6
- package/src/oidc.test.ts +164 -0
- package/src/routes/domain-verification.ts +6 -6
- package/src/routes/sso.ts +336 -333
- package/src/saml.test.ts +346 -0
- package/src/utils.ts +31 -0
package/src/routes/sso.ts
CHANGED
|
@@ -22,45 +22,19 @@ import type { IdentityProvider } from "samlify/types/src/entity-idp";
|
|
|
22
22
|
import type { FlowResult } from "samlify/types/src/flow";
|
|
23
23
|
import * as z from "zod/v4";
|
|
24
24
|
import type { OIDCConfig, SAMLConfig, SSOOptions, SSOProvider } from "../types";
|
|
25
|
-
import { validateEmailDomain } from "../utils";
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Safely parses a value that might be a JSON string or already a parsed object
|
|
29
|
-
* This handles cases where ORMs like Drizzle might return already parsed objects
|
|
30
|
-
* instead of JSON strings from TEXT/JSON columns
|
|
31
|
-
*/
|
|
32
|
-
function safeJsonParse<T>(value: string | T | null | undefined): T | null {
|
|
33
|
-
if (!value) return null;
|
|
34
|
-
|
|
35
|
-
// If it's already an object (not a string), return it as-is
|
|
36
|
-
if (typeof value === "object") {
|
|
37
|
-
return value as T;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// If it's a string, try to parse it
|
|
41
|
-
if (typeof value === "string") {
|
|
42
|
-
try {
|
|
43
|
-
return JSON.parse(value) as T;
|
|
44
|
-
} catch (error) {
|
|
45
|
-
// If parsing fails, this might indicate the string is not valid JSON
|
|
46
|
-
throw new Error(
|
|
47
|
-
`Failed to parse JSON: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
48
|
-
);
|
|
49
|
-
}
|
|
50
|
-
}
|
|
25
|
+
import { safeJsonParse, validateEmailDomain } from "../utils";
|
|
51
26
|
|
|
52
|
-
|
|
53
|
-
|
|
27
|
+
const spMetadataQuerySchema = z.object({
|
|
28
|
+
providerId: z.string(),
|
|
29
|
+
format: z.enum(["xml", "json"]).default("xml"),
|
|
30
|
+
});
|
|
54
31
|
|
|
55
32
|
export const spMetadata = () => {
|
|
56
33
|
return createAuthEndpoint(
|
|
57
34
|
"/sso/saml2/sp/metadata",
|
|
58
35
|
{
|
|
59
36
|
method: "GET",
|
|
60
|
-
query:
|
|
61
|
-
providerId: z.string(),
|
|
62
|
-
format: z.enum(["xml", "json"]).default("xml"),
|
|
63
|
-
}),
|
|
37
|
+
query: spMetadataQuerySchema,
|
|
64
38
|
metadata: {
|
|
65
39
|
openapi: {
|
|
66
40
|
operationId: "getSSOServiceProviderMetadata",
|
|
@@ -128,213 +102,211 @@ export const spMetadata = () => {
|
|
|
128
102
|
);
|
|
129
103
|
};
|
|
130
104
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
105
|
+
const ssoProviderBodySchema = z.object({
|
|
106
|
+
providerId: z.string({}).meta({
|
|
107
|
+
description:
|
|
108
|
+
"The ID of the provider. This is used to identify the provider during login and callback",
|
|
109
|
+
}),
|
|
110
|
+
issuer: z.string({}).meta({
|
|
111
|
+
description: "The issuer of the provider",
|
|
112
|
+
}),
|
|
113
|
+
domain: z.string({}).meta({
|
|
114
|
+
description: "The domain of the provider. This is used for email matching",
|
|
115
|
+
}),
|
|
116
|
+
oidcConfig: z
|
|
117
|
+
.object({
|
|
118
|
+
clientId: z.string({}).meta({
|
|
119
|
+
description: "The client ID",
|
|
120
|
+
}),
|
|
121
|
+
clientSecret: z.string({}).meta({
|
|
122
|
+
description: "The client secret",
|
|
123
|
+
}),
|
|
124
|
+
authorizationEndpoint: z
|
|
125
|
+
.string({})
|
|
126
|
+
.meta({
|
|
127
|
+
description: "The authorization endpoint",
|
|
128
|
+
})
|
|
129
|
+
.optional(),
|
|
130
|
+
tokenEndpoint: z
|
|
131
|
+
.string({})
|
|
132
|
+
.meta({
|
|
133
|
+
description: "The token endpoint",
|
|
134
|
+
})
|
|
135
|
+
.optional(),
|
|
136
|
+
userInfoEndpoint: z
|
|
137
|
+
.string({})
|
|
138
|
+
.meta({
|
|
139
|
+
description: "The user info endpoint",
|
|
140
|
+
})
|
|
141
|
+
.optional(),
|
|
142
|
+
tokenEndpointAuthentication: z
|
|
143
|
+
.enum(["client_secret_post", "client_secret_basic"])
|
|
144
|
+
.optional(),
|
|
145
|
+
jwksEndpoint: z
|
|
146
|
+
.string({})
|
|
147
|
+
.meta({
|
|
148
|
+
description: "The JWKS endpoint",
|
|
149
|
+
})
|
|
150
|
+
.optional(),
|
|
151
|
+
discoveryEndpoint: z.string().optional(),
|
|
152
|
+
scopes: z
|
|
153
|
+
.array(z.string(), {})
|
|
154
|
+
.meta({
|
|
145
155
|
description:
|
|
146
|
-
"The
|
|
147
|
-
})
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
156
|
+
"The scopes to request. Defaults to ['openid', 'email', 'profile', 'offline_access']",
|
|
157
|
+
})
|
|
158
|
+
.optional(),
|
|
159
|
+
pkce: z
|
|
160
|
+
.boolean({})
|
|
161
|
+
.meta({
|
|
162
|
+
description: "Whether to use PKCE for the authorization flow",
|
|
163
|
+
})
|
|
164
|
+
.default(true)
|
|
165
|
+
.optional(),
|
|
166
|
+
mapping: z
|
|
167
|
+
.object({
|
|
168
|
+
id: z.string({}).meta({
|
|
169
|
+
description: "Field mapping for user ID (defaults to 'sub')",
|
|
170
|
+
}),
|
|
171
|
+
email: z.string({}).meta({
|
|
172
|
+
description: "Field mapping for email (defaults to 'email')",
|
|
173
|
+
}),
|
|
174
|
+
emailVerified: z
|
|
175
|
+
.string({})
|
|
176
|
+
.meta({
|
|
177
|
+
description:
|
|
178
|
+
"Field mapping for email verification (defaults to 'email_verified')",
|
|
179
|
+
})
|
|
180
|
+
.optional(),
|
|
181
|
+
name: z.string({}).meta({
|
|
182
|
+
description: "Field mapping for name (defaults to 'name')",
|
|
183
|
+
}),
|
|
184
|
+
image: z
|
|
185
|
+
.string({})
|
|
186
|
+
.meta({
|
|
187
|
+
description: "Field mapping for image (defaults to 'picture')",
|
|
188
|
+
})
|
|
189
|
+
.optional(),
|
|
190
|
+
extraFields: z.record(z.string(), z.any()).optional(),
|
|
191
|
+
})
|
|
192
|
+
.optional(),
|
|
193
|
+
})
|
|
194
|
+
.optional(),
|
|
195
|
+
samlConfig: z
|
|
196
|
+
.object({
|
|
197
|
+
entryPoint: z.string({}).meta({
|
|
198
|
+
description: "The entry point of the provider",
|
|
199
|
+
}),
|
|
200
|
+
cert: z.string({}).meta({
|
|
201
|
+
description: "The certificate of the provider",
|
|
202
|
+
}),
|
|
203
|
+
callbackUrl: z.string({}).meta({
|
|
204
|
+
description: "The callback URL of the provider",
|
|
205
|
+
}),
|
|
206
|
+
audience: z.string().optional(),
|
|
207
|
+
idpMetadata: z
|
|
208
|
+
.object({
|
|
209
|
+
metadata: z.string().optional(),
|
|
210
|
+
entityID: z.string().optional(),
|
|
211
|
+
cert: z.string().optional(),
|
|
212
|
+
privateKey: z.string().optional(),
|
|
213
|
+
privateKeyPass: z.string().optional(),
|
|
214
|
+
isAssertionEncrypted: z.boolean().optional(),
|
|
215
|
+
encPrivateKey: z.string().optional(),
|
|
216
|
+
encPrivateKeyPass: z.string().optional(),
|
|
217
|
+
singleSignOnService: z
|
|
218
|
+
.array(
|
|
219
|
+
z.object({
|
|
220
|
+
Binding: z.string().meta({
|
|
221
|
+
description: "The binding type for the SSO service",
|
|
205
222
|
}),
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
.meta({
|
|
209
|
-
description:
|
|
210
|
-
"Field mapping for email verification (defaults to 'email_verified')",
|
|
211
|
-
})
|
|
212
|
-
.optional(),
|
|
213
|
-
name: z.string({}).meta({
|
|
214
|
-
description: "Field mapping for name (defaults to 'name')",
|
|
223
|
+
Location: z.string().meta({
|
|
224
|
+
description: "The URL for the SSO service",
|
|
215
225
|
}),
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
})
|
|
222
|
-
.optional(),
|
|
223
|
-
extraFields: z.record(z.string(), z.any()).optional(),
|
|
224
|
-
})
|
|
225
|
-
.optional(),
|
|
226
|
-
})
|
|
227
|
-
.optional(),
|
|
228
|
-
samlConfig: z
|
|
229
|
-
.object({
|
|
230
|
-
entryPoint: z.string({}).meta({
|
|
231
|
-
description: "The entry point of the provider",
|
|
232
|
-
}),
|
|
233
|
-
cert: z.string({}).meta({
|
|
234
|
-
description: "The certificate of the provider",
|
|
235
|
-
}),
|
|
236
|
-
callbackUrl: z.string({}).meta({
|
|
237
|
-
description: "The callback URL of the provider",
|
|
238
|
-
}),
|
|
239
|
-
audience: z.string().optional(),
|
|
240
|
-
idpMetadata: z
|
|
241
|
-
.object({
|
|
242
|
-
metadata: z.string().optional(),
|
|
243
|
-
entityID: z.string().optional(),
|
|
244
|
-
cert: z.string().optional(),
|
|
245
|
-
privateKey: z.string().optional(),
|
|
246
|
-
privateKeyPass: z.string().optional(),
|
|
247
|
-
isAssertionEncrypted: z.boolean().optional(),
|
|
248
|
-
encPrivateKey: z.string().optional(),
|
|
249
|
-
encPrivateKeyPass: z.string().optional(),
|
|
250
|
-
singleSignOnService: z
|
|
251
|
-
.array(
|
|
252
|
-
z.object({
|
|
253
|
-
Binding: z.string().meta({
|
|
254
|
-
description: "The binding type for the SSO service",
|
|
255
|
-
}),
|
|
256
|
-
Location: z.string().meta({
|
|
257
|
-
description: "The URL for the SSO service",
|
|
258
|
-
}),
|
|
259
|
-
}),
|
|
260
|
-
)
|
|
261
|
-
.optional()
|
|
262
|
-
.meta({
|
|
263
|
-
description: "Single Sign-On service configuration",
|
|
264
|
-
}),
|
|
265
|
-
})
|
|
266
|
-
.optional(),
|
|
267
|
-
spMetadata: z.object({
|
|
268
|
-
metadata: z.string().optional(),
|
|
269
|
-
entityID: z.string().optional(),
|
|
270
|
-
binding: z.string().optional(),
|
|
271
|
-
privateKey: z.string().optional(),
|
|
272
|
-
privateKeyPass: z.string().optional(),
|
|
273
|
-
isAssertionEncrypted: z.boolean().optional(),
|
|
274
|
-
encPrivateKey: z.string().optional(),
|
|
275
|
-
encPrivateKeyPass: z.string().optional(),
|
|
226
|
+
}),
|
|
227
|
+
)
|
|
228
|
+
.optional()
|
|
229
|
+
.meta({
|
|
230
|
+
description: "Single Sign-On service configuration",
|
|
276
231
|
}),
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
"Field mapping for user ID (defaults to 'nameID')",
|
|
289
|
-
}),
|
|
290
|
-
email: z.string({}).meta({
|
|
291
|
-
description: "Field mapping for email (defaults to 'email')",
|
|
292
|
-
}),
|
|
293
|
-
emailVerified: z
|
|
294
|
-
.string({})
|
|
295
|
-
.meta({
|
|
296
|
-
description: "Field mapping for email verification",
|
|
297
|
-
})
|
|
298
|
-
.optional(),
|
|
299
|
-
name: z.string({}).meta({
|
|
300
|
-
description:
|
|
301
|
-
"Field mapping for name (defaults to 'displayName')",
|
|
302
|
-
}),
|
|
303
|
-
firstName: z
|
|
304
|
-
.string({})
|
|
305
|
-
.meta({
|
|
306
|
-
description:
|
|
307
|
-
"Field mapping for first name (defaults to 'givenName')",
|
|
308
|
-
})
|
|
309
|
-
.optional(),
|
|
310
|
-
lastName: z
|
|
311
|
-
.string({})
|
|
312
|
-
.meta({
|
|
313
|
-
description:
|
|
314
|
-
"Field mapping for last name (defaults to 'surname')",
|
|
315
|
-
})
|
|
316
|
-
.optional(),
|
|
317
|
-
extraFields: z.record(z.string(), z.any()).optional(),
|
|
318
|
-
})
|
|
319
|
-
.optional(),
|
|
320
|
-
})
|
|
321
|
-
.optional(),
|
|
322
|
-
organizationId: z
|
|
323
|
-
.string({})
|
|
324
|
-
.meta({
|
|
325
|
-
description:
|
|
326
|
-
"If organization plugin is enabled, the organization id to link the provider to",
|
|
327
|
-
})
|
|
328
|
-
.optional(),
|
|
329
|
-
overrideUserInfo: z
|
|
330
|
-
.boolean({})
|
|
331
|
-
.meta({
|
|
332
|
-
description:
|
|
333
|
-
"Override user info with the provider info. Defaults to false",
|
|
334
|
-
})
|
|
335
|
-
.default(false)
|
|
336
|
-
.optional(),
|
|
232
|
+
})
|
|
233
|
+
.optional(),
|
|
234
|
+
spMetadata: z.object({
|
|
235
|
+
metadata: z.string().optional(),
|
|
236
|
+
entityID: z.string().optional(),
|
|
237
|
+
binding: z.string().optional(),
|
|
238
|
+
privateKey: z.string().optional(),
|
|
239
|
+
privateKeyPass: z.string().optional(),
|
|
240
|
+
isAssertionEncrypted: z.boolean().optional(),
|
|
241
|
+
encPrivateKey: z.string().optional(),
|
|
242
|
+
encPrivateKeyPass: z.string().optional(),
|
|
337
243
|
}),
|
|
244
|
+
wantAssertionsSigned: z.boolean().optional(),
|
|
245
|
+
signatureAlgorithm: z.string().optional(),
|
|
246
|
+
digestAlgorithm: z.string().optional(),
|
|
247
|
+
identifierFormat: z.string().optional(),
|
|
248
|
+
privateKey: z.string().optional(),
|
|
249
|
+
decryptionPvk: z.string().optional(),
|
|
250
|
+
additionalParams: z.record(z.string(), z.any()).optional(),
|
|
251
|
+
mapping: z
|
|
252
|
+
.object({
|
|
253
|
+
id: z.string({}).meta({
|
|
254
|
+
description: "Field mapping for user ID (defaults to 'nameID')",
|
|
255
|
+
}),
|
|
256
|
+
email: z.string({}).meta({
|
|
257
|
+
description: "Field mapping for email (defaults to 'email')",
|
|
258
|
+
}),
|
|
259
|
+
emailVerified: z
|
|
260
|
+
.string({})
|
|
261
|
+
.meta({
|
|
262
|
+
description: "Field mapping for email verification",
|
|
263
|
+
})
|
|
264
|
+
.optional(),
|
|
265
|
+
name: z.string({}).meta({
|
|
266
|
+
description: "Field mapping for name (defaults to 'displayName')",
|
|
267
|
+
}),
|
|
268
|
+
firstName: z
|
|
269
|
+
.string({})
|
|
270
|
+
.meta({
|
|
271
|
+
description:
|
|
272
|
+
"Field mapping for first name (defaults to 'givenName')",
|
|
273
|
+
})
|
|
274
|
+
.optional(),
|
|
275
|
+
lastName: z
|
|
276
|
+
.string({})
|
|
277
|
+
.meta({
|
|
278
|
+
description:
|
|
279
|
+
"Field mapping for last name (defaults to 'surname')",
|
|
280
|
+
})
|
|
281
|
+
.optional(),
|
|
282
|
+
extraFields: z.record(z.string(), z.any()).optional(),
|
|
283
|
+
})
|
|
284
|
+
.optional(),
|
|
285
|
+
})
|
|
286
|
+
.optional(),
|
|
287
|
+
organizationId: z
|
|
288
|
+
.string({})
|
|
289
|
+
.meta({
|
|
290
|
+
description:
|
|
291
|
+
"If organization plugin is enabled, the organization id to link the provider to",
|
|
292
|
+
})
|
|
293
|
+
.optional(),
|
|
294
|
+
overrideUserInfo: z
|
|
295
|
+
.boolean({})
|
|
296
|
+
.meta({
|
|
297
|
+
description:
|
|
298
|
+
"Override user info with the provider info. Defaults to false",
|
|
299
|
+
})
|
|
300
|
+
.default(false)
|
|
301
|
+
.optional(),
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
export const registerSSOProvider = <O extends SSOOptions>(options: O) => {
|
|
305
|
+
return createAuthEndpoint(
|
|
306
|
+
"/sso/register",
|
|
307
|
+
{
|
|
308
|
+
method: "POST",
|
|
309
|
+
body: ssoProviderBodySchema,
|
|
338
310
|
use: [sessionMiddleware],
|
|
339
311
|
metadata: {
|
|
340
312
|
openapi: {
|
|
@@ -683,12 +655,12 @@ export const registerSSOProvider = <O extends SSOOptions>(options: O) => {
|
|
|
683
655
|
|
|
684
656
|
return ctx.json({
|
|
685
657
|
...provider,
|
|
686
|
-
oidcConfig:
|
|
658
|
+
oidcConfig: safeJsonParse<OIDCConfig>(
|
|
687
659
|
provider.oidcConfig as unknown as string,
|
|
688
|
-
)
|
|
689
|
-
samlConfig:
|
|
660
|
+
),
|
|
661
|
+
samlConfig: safeJsonParse<SAMLConfig>(
|
|
690
662
|
provider.samlConfig as unknown as string,
|
|
691
|
-
)
|
|
663
|
+
),
|
|
692
664
|
redirectURI: `${ctx.context.baseURL}/sso/callback/${provider.providerId}`,
|
|
693
665
|
...(options?.domainVerification?.enabled ? { domainVerified } : {}),
|
|
694
666
|
...(options?.domainVerification?.enabled
|
|
@@ -699,76 +671,77 @@ export const registerSSOProvider = <O extends SSOOptions>(options: O) => {
|
|
|
699
671
|
);
|
|
700
672
|
};
|
|
701
673
|
|
|
674
|
+
const signInSSOBodySchema = z.object({
|
|
675
|
+
email: z
|
|
676
|
+
.string({})
|
|
677
|
+
.meta({
|
|
678
|
+
description:
|
|
679
|
+
"The email address to sign in with. This is used to identify the issuer to sign in with. It's optional if the issuer is provided",
|
|
680
|
+
})
|
|
681
|
+
.optional(),
|
|
682
|
+
organizationSlug: z
|
|
683
|
+
.string({})
|
|
684
|
+
.meta({
|
|
685
|
+
description: "The slug of the organization to sign in with",
|
|
686
|
+
})
|
|
687
|
+
.optional(),
|
|
688
|
+
providerId: z
|
|
689
|
+
.string({})
|
|
690
|
+
.meta({
|
|
691
|
+
description:
|
|
692
|
+
"The ID of the provider to sign in with. This can be provided instead of email or issuer",
|
|
693
|
+
})
|
|
694
|
+
.optional(),
|
|
695
|
+
domain: z
|
|
696
|
+
.string({})
|
|
697
|
+
.meta({
|
|
698
|
+
description: "The domain of the provider.",
|
|
699
|
+
})
|
|
700
|
+
.optional(),
|
|
701
|
+
callbackURL: z.string({}).meta({
|
|
702
|
+
description: "The URL to redirect to after login",
|
|
703
|
+
}),
|
|
704
|
+
errorCallbackURL: z
|
|
705
|
+
.string({})
|
|
706
|
+
.meta({
|
|
707
|
+
description: "The URL to redirect to after login",
|
|
708
|
+
})
|
|
709
|
+
.optional(),
|
|
710
|
+
newUserCallbackURL: z
|
|
711
|
+
.string({})
|
|
712
|
+
.meta({
|
|
713
|
+
description: "The URL to redirect to after login if the user is new",
|
|
714
|
+
})
|
|
715
|
+
.optional(),
|
|
716
|
+
scopes: z
|
|
717
|
+
.array(z.string(), {})
|
|
718
|
+
.meta({
|
|
719
|
+
description: "Scopes to request from the provider.",
|
|
720
|
+
})
|
|
721
|
+
.optional(),
|
|
722
|
+
loginHint: z
|
|
723
|
+
.string({})
|
|
724
|
+
.meta({
|
|
725
|
+
description:
|
|
726
|
+
"Login hint to send to the identity provider (e.g., email or identifier). If supported, will be sent as 'login_hint'.",
|
|
727
|
+
})
|
|
728
|
+
.optional(),
|
|
729
|
+
requestSignUp: z
|
|
730
|
+
.boolean({})
|
|
731
|
+
.meta({
|
|
732
|
+
description:
|
|
733
|
+
"Explicitly request sign-up. Useful when disableImplicitSignUp is true for this provider",
|
|
734
|
+
})
|
|
735
|
+
.optional(),
|
|
736
|
+
providerType: z.enum(["oidc", "saml"]).optional(),
|
|
737
|
+
});
|
|
738
|
+
|
|
702
739
|
export const signInSSO = (options?: SSOOptions) => {
|
|
703
740
|
return createAuthEndpoint(
|
|
704
741
|
"/sign-in/sso",
|
|
705
742
|
{
|
|
706
743
|
method: "POST",
|
|
707
|
-
body:
|
|
708
|
-
email: z
|
|
709
|
-
.string({})
|
|
710
|
-
.meta({
|
|
711
|
-
description:
|
|
712
|
-
"The email address to sign in with. This is used to identify the issuer to sign in with. It's optional if the issuer is provided",
|
|
713
|
-
})
|
|
714
|
-
.optional(),
|
|
715
|
-
organizationSlug: z
|
|
716
|
-
.string({})
|
|
717
|
-
.meta({
|
|
718
|
-
description: "The slug of the organization to sign in with",
|
|
719
|
-
})
|
|
720
|
-
.optional(),
|
|
721
|
-
providerId: z
|
|
722
|
-
.string({})
|
|
723
|
-
.meta({
|
|
724
|
-
description:
|
|
725
|
-
"The ID of the provider to sign in with. This can be provided instead of email or issuer",
|
|
726
|
-
})
|
|
727
|
-
.optional(),
|
|
728
|
-
domain: z
|
|
729
|
-
.string({})
|
|
730
|
-
.meta({
|
|
731
|
-
description: "The domain of the provider.",
|
|
732
|
-
})
|
|
733
|
-
.optional(),
|
|
734
|
-
callbackURL: z.string({}).meta({
|
|
735
|
-
description: "The URL to redirect to after login",
|
|
736
|
-
}),
|
|
737
|
-
errorCallbackURL: z
|
|
738
|
-
.string({})
|
|
739
|
-
.meta({
|
|
740
|
-
description: "The URL to redirect to after login",
|
|
741
|
-
})
|
|
742
|
-
.optional(),
|
|
743
|
-
newUserCallbackURL: z
|
|
744
|
-
.string({})
|
|
745
|
-
.meta({
|
|
746
|
-
description:
|
|
747
|
-
"The URL to redirect to after login if the user is new",
|
|
748
|
-
})
|
|
749
|
-
.optional(),
|
|
750
|
-
scopes: z
|
|
751
|
-
.array(z.string(), {})
|
|
752
|
-
.meta({
|
|
753
|
-
description: "Scopes to request from the provider.",
|
|
754
|
-
})
|
|
755
|
-
.optional(),
|
|
756
|
-
loginHint: z
|
|
757
|
-
.string({})
|
|
758
|
-
.meta({
|
|
759
|
-
description:
|
|
760
|
-
"Login hint to send to the identity provider (e.g., email or identifier). If supported, will be sent as 'login_hint'.",
|
|
761
|
-
})
|
|
762
|
-
.optional(),
|
|
763
|
-
requestSignUp: z
|
|
764
|
-
.boolean({})
|
|
765
|
-
.meta({
|
|
766
|
-
description:
|
|
767
|
-
"Explicitly request sign-up. Useful when disableImplicitSignUp is true for this provider",
|
|
768
|
-
})
|
|
769
|
-
.optional(),
|
|
770
|
-
providerType: z.enum(["oidc", "saml"]).optional(),
|
|
771
|
-
}),
|
|
744
|
+
body: signInSSOBodySchema,
|
|
772
745
|
metadata: {
|
|
773
746
|
openapi: {
|
|
774
747
|
operationId: "signInWithSSO",
|
|
@@ -1101,17 +1074,19 @@ export const signInSSO = (options?: SSOOptions) => {
|
|
|
1101
1074
|
);
|
|
1102
1075
|
};
|
|
1103
1076
|
|
|
1077
|
+
const callbackSSOQuerySchema = z.object({
|
|
1078
|
+
code: z.string().optional(),
|
|
1079
|
+
state: z.string(),
|
|
1080
|
+
error: z.string().optional(),
|
|
1081
|
+
error_description: z.string().optional(),
|
|
1082
|
+
});
|
|
1083
|
+
|
|
1104
1084
|
export const callbackSSO = (options?: SSOOptions) => {
|
|
1105
1085
|
return createAuthEndpoint(
|
|
1106
1086
|
"/sso/callback/:providerId",
|
|
1107
1087
|
{
|
|
1108
1088
|
method: "GET",
|
|
1109
|
-
query:
|
|
1110
|
-
code: z.string().optional(),
|
|
1111
|
-
state: z.string(),
|
|
1112
|
-
error: z.string().optional(),
|
|
1113
|
-
error_description: z.string().optional(),
|
|
1114
|
-
}),
|
|
1089
|
+
query: callbackSSOQuerySchema,
|
|
1115
1090
|
allowedMediaTypes: [
|
|
1116
1091
|
"application/x-www-form-urlencoded",
|
|
1117
1092
|
"application/json",
|
|
@@ -1374,6 +1349,11 @@ export const callbackSSO = (options?: SSOOptions) => {
|
|
|
1374
1349
|
}/error?error=invalid_provider&error_description=missing_user_info`,
|
|
1375
1350
|
);
|
|
1376
1351
|
}
|
|
1352
|
+
const isTrustedProvider =
|
|
1353
|
+
"domainVerified" in provider &&
|
|
1354
|
+
(provider as { domainVerified?: boolean }).domainVerified === true &&
|
|
1355
|
+
validateEmailDomain(userInfo.email, provider.domain);
|
|
1356
|
+
|
|
1377
1357
|
const linked = await handleOAuthUserInfo(ctx, {
|
|
1378
1358
|
userInfo: {
|
|
1379
1359
|
email: userInfo.email,
|
|
@@ -1397,6 +1377,7 @@ export const callbackSSO = (options?: SSOOptions) => {
|
|
|
1397
1377
|
callbackURL,
|
|
1398
1378
|
disableSignUp: options?.disableImplicitSignUp && !requestSignUp,
|
|
1399
1379
|
overrideUserInfo: config.overrideUserInfo,
|
|
1380
|
+
isTrustedProvider,
|
|
1400
1381
|
});
|
|
1401
1382
|
if (linked.error) {
|
|
1402
1383
|
throw ctx.redirect(
|
|
@@ -1468,15 +1449,17 @@ export const callbackSSO = (options?: SSOOptions) => {
|
|
|
1468
1449
|
);
|
|
1469
1450
|
};
|
|
1470
1451
|
|
|
1452
|
+
const callbackSSOSAMLBodySchema = z.object({
|
|
1453
|
+
SAMLResponse: z.string(),
|
|
1454
|
+
RelayState: z.string().optional(),
|
|
1455
|
+
});
|
|
1456
|
+
|
|
1471
1457
|
export const callbackSSOSAML = (options?: SSOOptions) => {
|
|
1472
1458
|
return createAuthEndpoint(
|
|
1473
1459
|
"/sso/saml2/callback/:providerId",
|
|
1474
1460
|
{
|
|
1475
1461
|
method: "POST",
|
|
1476
|
-
body:
|
|
1477
|
-
SAMLResponse: z.string(),
|
|
1478
|
-
RelayState: z.string().optional(),
|
|
1479
|
-
}),
|
|
1462
|
+
body: callbackSSOSAMLBodySchema,
|
|
1480
1463
|
metadata: {
|
|
1481
1464
|
isAction: false,
|
|
1482
1465
|
allowedMediaTypes: [
|
|
@@ -1717,6 +1700,35 @@ export const callbackSSOSAML = (options?: SSOOptions) => {
|
|
|
1717
1700
|
});
|
|
1718
1701
|
|
|
1719
1702
|
if (existingUser) {
|
|
1703
|
+
const account = await ctx.context.adapter.findOne<Account>({
|
|
1704
|
+
model: "account",
|
|
1705
|
+
where: [
|
|
1706
|
+
{ field: "userId", value: existingUser.id },
|
|
1707
|
+
{ field: "providerId", value: provider.providerId },
|
|
1708
|
+
{ field: "accountId", value: userInfo.id },
|
|
1709
|
+
],
|
|
1710
|
+
});
|
|
1711
|
+
if (!account) {
|
|
1712
|
+
const isTrustedProvider =
|
|
1713
|
+
ctx.context.options.account?.accountLinking?.trustedProviders?.includes(
|
|
1714
|
+
provider.providerId,
|
|
1715
|
+
) ||
|
|
1716
|
+
("domainVerified" in provider &&
|
|
1717
|
+
provider.domainVerified &&
|
|
1718
|
+
validateEmailDomain(userInfo.email, provider.domain));
|
|
1719
|
+
if (!isTrustedProvider) {
|
|
1720
|
+
const redirectUrl =
|
|
1721
|
+
RelayState || parsedSamlConfig.callbackUrl || ctx.context.baseURL;
|
|
1722
|
+
throw ctx.redirect(`${redirectUrl}?error=account_not_linked`);
|
|
1723
|
+
}
|
|
1724
|
+
await ctx.context.internalAdapter.createAccount({
|
|
1725
|
+
userId: existingUser.id,
|
|
1726
|
+
providerId: provider.providerId,
|
|
1727
|
+
accountId: userInfo.id,
|
|
1728
|
+
accessToken: "",
|
|
1729
|
+
refreshToken: "",
|
|
1730
|
+
});
|
|
1731
|
+
}
|
|
1720
1732
|
user = existingUser;
|
|
1721
1733
|
} else {
|
|
1722
1734
|
// if implicit sign up is disabled, we should not create a new user nor a new account.
|
|
@@ -1732,19 +1744,6 @@ export const callbackSSOSAML = (options?: SSOOptions) => {
|
|
|
1732
1744
|
name: userInfo.name,
|
|
1733
1745
|
emailVerified: userInfo.emailVerified,
|
|
1734
1746
|
});
|
|
1735
|
-
}
|
|
1736
|
-
|
|
1737
|
-
// Create or update account link
|
|
1738
|
-
const account = await ctx.context.adapter.findOne<Account>({
|
|
1739
|
-
model: "account",
|
|
1740
|
-
where: [
|
|
1741
|
-
{ field: "userId", value: user.id },
|
|
1742
|
-
{ field: "providerId", value: provider.providerId },
|
|
1743
|
-
{ field: "accountId", value: userInfo.id },
|
|
1744
|
-
],
|
|
1745
|
-
});
|
|
1746
|
-
|
|
1747
|
-
if (!account) {
|
|
1748
1747
|
await ctx.context.internalAdapter.createAccount({
|
|
1749
1748
|
userId: user.id,
|
|
1750
1749
|
providerId: provider.providerId,
|
|
@@ -1815,18 +1814,22 @@ export const callbackSSOSAML = (options?: SSOOptions) => {
|
|
|
1815
1814
|
);
|
|
1816
1815
|
};
|
|
1817
1816
|
|
|
1817
|
+
const acsEndpointParamsSchema = z.object({
|
|
1818
|
+
providerId: z.string().optional(),
|
|
1819
|
+
});
|
|
1820
|
+
|
|
1821
|
+
const acsEndpointBodySchema = z.object({
|
|
1822
|
+
SAMLResponse: z.string(),
|
|
1823
|
+
RelayState: z.string().optional(),
|
|
1824
|
+
});
|
|
1825
|
+
|
|
1818
1826
|
export const acsEndpoint = (options?: SSOOptions) => {
|
|
1819
1827
|
return createAuthEndpoint(
|
|
1820
1828
|
"/sso/saml2/sp/acs/:providerId",
|
|
1821
1829
|
{
|
|
1822
1830
|
method: "POST",
|
|
1823
|
-
params:
|
|
1824
|
-
|
|
1825
|
-
}),
|
|
1826
|
-
body: z.object({
|
|
1827
|
-
SAMLResponse: z.string(),
|
|
1828
|
-
RelayState: z.string().optional(),
|
|
1829
|
-
}),
|
|
1831
|
+
params: acsEndpointParamsSchema,
|
|
1832
|
+
body: acsEndpointBodySchema,
|
|
1830
1833
|
metadata: {
|
|
1831
1834
|
isAction: false,
|
|
1832
1835
|
allowedMediaTypes: [
|