@better-auth/sso 1.4.7-beta.3 → 1.4.7-beta.4
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 +6 -6
- package/dist/client.d.mts +1 -1
- package/dist/{index-m7FISidt.d.mts → index-GoyGoP_a.d.mts} +314 -13
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +401 -16
- package/package.json +3 -3
- package/src/index.ts +27 -0
- package/src/oidc/discovery.test.ts +823 -0
- package/src/oidc/discovery.ts +355 -0
- package/src/oidc/errors.ts +86 -0
- package/src/oidc/index.ts +31 -0
- package/src/oidc/types.ts +210 -0
- package/src/oidc.test.ts +0 -164
- package/src/routes/sso.ts +191 -23
- package/src/saml.test.ts +301 -57
- package/src/types.ts +32 -0
package/src/oidc.test.ts
CHANGED
|
@@ -571,167 +571,3 @@ describe("provisioning", async (ctx) => {
|
|
|
571
571
|
expect(res.url).toContain("http://localhost:8080/authorize");
|
|
572
572
|
});
|
|
573
573
|
});
|
|
574
|
-
|
|
575
|
-
describe("OIDC account linking with domainVerified", async () => {
|
|
576
|
-
const { auth, signInWithTestUser, customFetchImpl, cookieSetter } =
|
|
577
|
-
await getTestInstance({
|
|
578
|
-
account: {
|
|
579
|
-
accountLinking: {
|
|
580
|
-
enabled: true,
|
|
581
|
-
trustedProviders: [],
|
|
582
|
-
},
|
|
583
|
-
},
|
|
584
|
-
plugins: [
|
|
585
|
-
sso({
|
|
586
|
-
domainVerification: {
|
|
587
|
-
enabled: true,
|
|
588
|
-
},
|
|
589
|
-
}),
|
|
590
|
-
],
|
|
591
|
-
});
|
|
592
|
-
|
|
593
|
-
const authClient = createAuthClient({
|
|
594
|
-
plugins: [ssoClient()],
|
|
595
|
-
baseURL: "http://localhost:3000",
|
|
596
|
-
fetchOptions: {
|
|
597
|
-
customFetchImpl,
|
|
598
|
-
},
|
|
599
|
-
});
|
|
600
|
-
|
|
601
|
-
beforeAll(async () => {
|
|
602
|
-
await server.issuer.keys.generate("RS256");
|
|
603
|
-
await server.start(8080, "localhost");
|
|
604
|
-
});
|
|
605
|
-
|
|
606
|
-
afterAll(async () => {
|
|
607
|
-
await server.stop().catch(() => {});
|
|
608
|
-
});
|
|
609
|
-
|
|
610
|
-
async function simulateOAuthFlow(authUrl: string, headers: Headers) {
|
|
611
|
-
let location: string | null = null;
|
|
612
|
-
await betterFetch(authUrl, {
|
|
613
|
-
method: "GET",
|
|
614
|
-
redirect: "manual",
|
|
615
|
-
onError(context) {
|
|
616
|
-
location = context.response.headers.get("location");
|
|
617
|
-
},
|
|
618
|
-
});
|
|
619
|
-
|
|
620
|
-
if (!location) throw new Error("No redirect location found");
|
|
621
|
-
|
|
622
|
-
let callbackURL = "";
|
|
623
|
-
const newHeaders = new Headers();
|
|
624
|
-
await betterFetch(location, {
|
|
625
|
-
method: "GET",
|
|
626
|
-
customFetchImpl,
|
|
627
|
-
headers,
|
|
628
|
-
onError(context) {
|
|
629
|
-
callbackURL = context.response.headers.get("location") || "";
|
|
630
|
-
cookieSetter(newHeaders)(context);
|
|
631
|
-
},
|
|
632
|
-
});
|
|
633
|
-
|
|
634
|
-
return { callbackURL, headers: newHeaders };
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
it("should allow account linking when domain is verified and email domain matches", async () => {
|
|
638
|
-
const testEmail = "linking-test@verified-oidc.com";
|
|
639
|
-
const testDomain = "verified-oidc.com";
|
|
640
|
-
|
|
641
|
-
server.service.on("beforeTokenSigning", (token) => {
|
|
642
|
-
token.payload.email = testEmail;
|
|
643
|
-
token.payload.email_verified = false;
|
|
644
|
-
token.payload.name = "Domain Verified User";
|
|
645
|
-
token.payload.sub = "oidc-domain-verified-user";
|
|
646
|
-
});
|
|
647
|
-
|
|
648
|
-
const { headers } = await signInWithTestUser();
|
|
649
|
-
|
|
650
|
-
const provider = await auth.api.registerSSOProvider({
|
|
651
|
-
body: {
|
|
652
|
-
providerId: "domain-verified-oidc",
|
|
653
|
-
issuer: server.issuer.url!,
|
|
654
|
-
domain: testDomain,
|
|
655
|
-
oidcConfig: {
|
|
656
|
-
clientId: "test",
|
|
657
|
-
clientSecret: "test",
|
|
658
|
-
authorizationEndpoint: `${server.issuer.url}/authorize`,
|
|
659
|
-
tokenEndpoint: `${server.issuer.url}/token`,
|
|
660
|
-
jwksEndpoint: `${server.issuer.url}/jwks`,
|
|
661
|
-
discoveryEndpoint: `${server.issuer.url}/.well-known/openid-configuration`,
|
|
662
|
-
mapping: {
|
|
663
|
-
id: "sub",
|
|
664
|
-
email: "email",
|
|
665
|
-
emailVerified: "email_verified",
|
|
666
|
-
name: "name",
|
|
667
|
-
},
|
|
668
|
-
},
|
|
669
|
-
},
|
|
670
|
-
headers,
|
|
671
|
-
});
|
|
672
|
-
|
|
673
|
-
expect(provider.domainVerified).toBe(false);
|
|
674
|
-
|
|
675
|
-
const ctx = await auth.$context;
|
|
676
|
-
await ctx.adapter.update({
|
|
677
|
-
model: "ssoProvider",
|
|
678
|
-
where: [{ field: "providerId", value: provider.providerId }],
|
|
679
|
-
update: {
|
|
680
|
-
domainVerified: true,
|
|
681
|
-
},
|
|
682
|
-
});
|
|
683
|
-
|
|
684
|
-
const updatedProvider = await ctx.adapter.findOne<{
|
|
685
|
-
domainVerified: boolean;
|
|
686
|
-
domain: string;
|
|
687
|
-
}>({
|
|
688
|
-
model: "ssoProvider",
|
|
689
|
-
where: [{ field: "providerId", value: provider.providerId }],
|
|
690
|
-
});
|
|
691
|
-
expect(updatedProvider?.domainVerified).toBe(true);
|
|
692
|
-
|
|
693
|
-
await ctx.adapter.create({
|
|
694
|
-
model: "user",
|
|
695
|
-
data: {
|
|
696
|
-
id: "existing-oidc-domain-user",
|
|
697
|
-
email: testEmail,
|
|
698
|
-
name: "Existing User",
|
|
699
|
-
emailVerified: true,
|
|
700
|
-
createdAt: new Date(),
|
|
701
|
-
updatedAt: new Date(),
|
|
702
|
-
},
|
|
703
|
-
forceAllowId: true,
|
|
704
|
-
});
|
|
705
|
-
|
|
706
|
-
const newHeaders = new Headers();
|
|
707
|
-
const res = await authClient.signIn.sso({
|
|
708
|
-
providerId: "domain-verified-oidc",
|
|
709
|
-
callbackURL: "/dashboard",
|
|
710
|
-
fetchOptions: {
|
|
711
|
-
throw: true,
|
|
712
|
-
onSuccess: cookieSetter(newHeaders),
|
|
713
|
-
},
|
|
714
|
-
});
|
|
715
|
-
|
|
716
|
-
expect(res.url).toContain("http://localhost:8080/authorize");
|
|
717
|
-
|
|
718
|
-
const { callbackURL } = await simulateOAuthFlow(res.url, newHeaders);
|
|
719
|
-
|
|
720
|
-
expect(callbackURL).toContain("/dashboard");
|
|
721
|
-
expect(callbackURL).not.toContain("error");
|
|
722
|
-
|
|
723
|
-
const accounts = await ctx.adapter.findMany<{
|
|
724
|
-
providerId: string;
|
|
725
|
-
accountId: string;
|
|
726
|
-
userId: string;
|
|
727
|
-
}>({
|
|
728
|
-
model: "account",
|
|
729
|
-
where: [{ field: "userId", value: "existing-oidc-domain-user" }],
|
|
730
|
-
});
|
|
731
|
-
const linkedAccount = accounts.find(
|
|
732
|
-
(a) => a.providerId === "domain-verified-oidc",
|
|
733
|
-
);
|
|
734
|
-
expect(linkedAccount).toBeTruthy();
|
|
735
|
-
expect(linkedAccount?.accountId).toBe("oidc-domain-verified-user");
|
|
736
|
-
});
|
|
737
|
-
});
|
package/src/routes/sso.ts
CHANGED
|
@@ -24,11 +24,98 @@ import type { FlowResult } from "samlify/types/src/flow";
|
|
|
24
24
|
import * as z from "zod/v4";
|
|
25
25
|
import type { AuthnRequestRecord } from "../authn-request-store";
|
|
26
26
|
import { DEFAULT_AUTHN_REQUEST_TTL_MS } from "../authn-request-store";
|
|
27
|
+
import type { HydratedOIDCConfig } from "../oidc";
|
|
28
|
+
import {
|
|
29
|
+
DiscoveryError,
|
|
30
|
+
discoverOIDCConfig,
|
|
31
|
+
mapDiscoveryErrorToAPIError,
|
|
32
|
+
} from "../oidc";
|
|
27
33
|
import type { OIDCConfig, SAMLConfig, SSOOptions, SSOProvider } from "../types";
|
|
28
34
|
|
|
29
35
|
import { safeJsonParse, validateEmailDomain } from "../utils";
|
|
30
36
|
|
|
31
37
|
const AUTHN_REQUEST_KEY_PREFIX = "saml-authn-request:";
|
|
38
|
+
|
|
39
|
+
/** Default clock skew tolerance: 5 minutes */
|
|
40
|
+
export const DEFAULT_CLOCK_SKEW_MS = 5 * 60 * 1000;
|
|
41
|
+
|
|
42
|
+
export interface TimestampValidationOptions {
|
|
43
|
+
clockSkew?: number;
|
|
44
|
+
requireTimestamps?: boolean;
|
|
45
|
+
logger?: {
|
|
46
|
+
warn: (message: string, data?: Record<string, unknown>) => void;
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Conditions extracted from SAML assertion */
|
|
51
|
+
export interface SAMLConditions {
|
|
52
|
+
notBefore?: string;
|
|
53
|
+
notOnOrAfter?: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Validates SAML assertion timestamp conditions (NotBefore/NotOnOrAfter).
|
|
58
|
+
* Prevents acceptance of expired or future-dated assertions.
|
|
59
|
+
* @throws {APIError} If timestamps are invalid, expired, or not yet valid
|
|
60
|
+
*/
|
|
61
|
+
export function validateSAMLTimestamp(
|
|
62
|
+
conditions: SAMLConditions | undefined,
|
|
63
|
+
options: TimestampValidationOptions = {},
|
|
64
|
+
): void {
|
|
65
|
+
const clockSkew = options.clockSkew ?? DEFAULT_CLOCK_SKEW_MS;
|
|
66
|
+
const hasTimestamps = conditions?.notBefore || conditions?.notOnOrAfter;
|
|
67
|
+
|
|
68
|
+
if (!hasTimestamps) {
|
|
69
|
+
if (options.requireTimestamps) {
|
|
70
|
+
throw new APIError("BAD_REQUEST", {
|
|
71
|
+
message: "SAML assertion missing required timestamp conditions",
|
|
72
|
+
details:
|
|
73
|
+
"Assertions must include NotBefore and/or NotOnOrAfter conditions",
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
// Log warning for missing timestamps when not required
|
|
77
|
+
options.logger?.warn(
|
|
78
|
+
"SAML assertion accepted without timestamp conditions",
|
|
79
|
+
{ hasConditions: !!conditions },
|
|
80
|
+
);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const now = Date.now();
|
|
85
|
+
|
|
86
|
+
if (conditions?.notBefore) {
|
|
87
|
+
const notBeforeTime = new Date(conditions.notBefore).getTime();
|
|
88
|
+
if (Number.isNaN(notBeforeTime)) {
|
|
89
|
+
throw new APIError("BAD_REQUEST", {
|
|
90
|
+
message: "SAML assertion has invalid NotBefore timestamp",
|
|
91
|
+
details: `Unable to parse NotBefore value: ${conditions.notBefore}`,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
if (now < notBeforeTime - clockSkew) {
|
|
95
|
+
throw new APIError("BAD_REQUEST", {
|
|
96
|
+
message: "SAML assertion is not yet valid",
|
|
97
|
+
details: `Current time is before NotBefore (with ${clockSkew}ms clock skew tolerance)`,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (conditions?.notOnOrAfter) {
|
|
103
|
+
const notOnOrAfterTime = new Date(conditions.notOnOrAfter).getTime();
|
|
104
|
+
if (Number.isNaN(notOnOrAfterTime)) {
|
|
105
|
+
throw new APIError("BAD_REQUEST", {
|
|
106
|
+
message: "SAML assertion has invalid NotOnOrAfter timestamp",
|
|
107
|
+
details: `Unable to parse NotOnOrAfter value: ${conditions.notOnOrAfter}`,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
if (now > notOnOrAfterTime + clockSkew) {
|
|
111
|
+
throw new APIError("BAD_REQUEST", {
|
|
112
|
+
message: "SAML assertion has expired",
|
|
113
|
+
details: `Current time is after NotOnOrAfter (with ${clockSkew}ms clock skew tolerance)`,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
32
119
|
const spMetadataQuerySchema = z.object({
|
|
33
120
|
providerId: z.string(),
|
|
34
121
|
format: z.enum(["xml", "json"]).default("xml"),
|
|
@@ -154,6 +241,13 @@ const ssoProviderBodySchema = z.object({
|
|
|
154
241
|
})
|
|
155
242
|
.optional(),
|
|
156
243
|
discoveryEndpoint: z.string().optional(),
|
|
244
|
+
skipDiscovery: z
|
|
245
|
+
.boolean()
|
|
246
|
+
.meta({
|
|
247
|
+
description:
|
|
248
|
+
"Skip OIDC discovery during registration. When true, you must provide authorizationEndpoint, tokenEndpoint, and jwksEndpoint manually.",
|
|
249
|
+
})
|
|
250
|
+
.optional(),
|
|
157
251
|
scopes: z
|
|
158
252
|
.array(z.string(), {})
|
|
159
253
|
.meta({
|
|
@@ -573,6 +667,80 @@ export const registerSSOProvider = <O extends SSOOptions>(options: O) => {
|
|
|
573
667
|
});
|
|
574
668
|
}
|
|
575
669
|
|
|
670
|
+
let hydratedOIDCConfig: HydratedOIDCConfig | null = null;
|
|
671
|
+
if (body.oidcConfig && !body.oidcConfig.skipDiscovery) {
|
|
672
|
+
try {
|
|
673
|
+
hydratedOIDCConfig = await discoverOIDCConfig({
|
|
674
|
+
issuer: body.issuer,
|
|
675
|
+
existingConfig: {
|
|
676
|
+
discoveryEndpoint: body.oidcConfig.discoveryEndpoint,
|
|
677
|
+
authorizationEndpoint: body.oidcConfig.authorizationEndpoint,
|
|
678
|
+
tokenEndpoint: body.oidcConfig.tokenEndpoint,
|
|
679
|
+
jwksEndpoint: body.oidcConfig.jwksEndpoint,
|
|
680
|
+
userInfoEndpoint: body.oidcConfig.userInfoEndpoint,
|
|
681
|
+
tokenEndpointAuthentication:
|
|
682
|
+
body.oidcConfig.tokenEndpointAuthentication,
|
|
683
|
+
},
|
|
684
|
+
});
|
|
685
|
+
} catch (error) {
|
|
686
|
+
if (error instanceof DiscoveryError) {
|
|
687
|
+
throw mapDiscoveryErrorToAPIError(error);
|
|
688
|
+
}
|
|
689
|
+
throw error;
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
const buildOIDCConfig = () => {
|
|
694
|
+
if (!body.oidcConfig) return null;
|
|
695
|
+
|
|
696
|
+
if (body.oidcConfig.skipDiscovery) {
|
|
697
|
+
return JSON.stringify({
|
|
698
|
+
issuer: body.issuer,
|
|
699
|
+
clientId: body.oidcConfig.clientId,
|
|
700
|
+
clientSecret: body.oidcConfig.clientSecret,
|
|
701
|
+
authorizationEndpoint: body.oidcConfig.authorizationEndpoint,
|
|
702
|
+
tokenEndpoint: body.oidcConfig.tokenEndpoint,
|
|
703
|
+
tokenEndpointAuthentication:
|
|
704
|
+
body.oidcConfig.tokenEndpointAuthentication ||
|
|
705
|
+
"client_secret_basic",
|
|
706
|
+
jwksEndpoint: body.oidcConfig.jwksEndpoint,
|
|
707
|
+
pkce: body.oidcConfig.pkce,
|
|
708
|
+
discoveryEndpoint:
|
|
709
|
+
body.oidcConfig.discoveryEndpoint ||
|
|
710
|
+
`${body.issuer}/.well-known/openid-configuration`,
|
|
711
|
+
mapping: body.oidcConfig.mapping,
|
|
712
|
+
scopes: body.oidcConfig.scopes,
|
|
713
|
+
userInfoEndpoint: body.oidcConfig.userInfoEndpoint,
|
|
714
|
+
overrideUserInfo:
|
|
715
|
+
ctx.body.overrideUserInfo ||
|
|
716
|
+
options?.defaultOverrideUserInfo ||
|
|
717
|
+
false,
|
|
718
|
+
});
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
if (!hydratedOIDCConfig) return null;
|
|
722
|
+
|
|
723
|
+
return JSON.stringify({
|
|
724
|
+
issuer: hydratedOIDCConfig.issuer,
|
|
725
|
+
clientId: body.oidcConfig.clientId,
|
|
726
|
+
clientSecret: body.oidcConfig.clientSecret,
|
|
727
|
+
authorizationEndpoint: hydratedOIDCConfig.authorizationEndpoint,
|
|
728
|
+
tokenEndpoint: hydratedOIDCConfig.tokenEndpoint,
|
|
729
|
+
tokenEndpointAuthentication:
|
|
730
|
+
hydratedOIDCConfig.tokenEndpointAuthentication,
|
|
731
|
+
jwksEndpoint: hydratedOIDCConfig.jwksEndpoint,
|
|
732
|
+
pkce: body.oidcConfig.pkce,
|
|
733
|
+
discoveryEndpoint: hydratedOIDCConfig.discoveryEndpoint,
|
|
734
|
+
mapping: body.oidcConfig.mapping,
|
|
735
|
+
scopes: body.oidcConfig.scopes,
|
|
736
|
+
userInfoEndpoint: hydratedOIDCConfig.userInfoEndpoint,
|
|
737
|
+
overrideUserInfo:
|
|
738
|
+
ctx.body.overrideUserInfo ||
|
|
739
|
+
options?.defaultOverrideUserInfo ||
|
|
740
|
+
false,
|
|
741
|
+
});
|
|
742
|
+
};
|
|
743
|
+
|
|
576
744
|
const provider = await ctx.context.adapter.create<
|
|
577
745
|
Record<string, any>,
|
|
578
746
|
SSOProvider<O>
|
|
@@ -582,29 +750,7 @@ export const registerSSOProvider = <O extends SSOOptions>(options: O) => {
|
|
|
582
750
|
issuer: body.issuer,
|
|
583
751
|
domain: body.domain,
|
|
584
752
|
domainVerified: false,
|
|
585
|
-
oidcConfig:
|
|
586
|
-
? JSON.stringify({
|
|
587
|
-
issuer: body.issuer,
|
|
588
|
-
clientId: body.oidcConfig.clientId,
|
|
589
|
-
clientSecret: body.oidcConfig.clientSecret,
|
|
590
|
-
authorizationEndpoint: body.oidcConfig.authorizationEndpoint,
|
|
591
|
-
tokenEndpoint: body.oidcConfig.tokenEndpoint,
|
|
592
|
-
tokenEndpointAuthentication:
|
|
593
|
-
body.oidcConfig.tokenEndpointAuthentication,
|
|
594
|
-
jwksEndpoint: body.oidcConfig.jwksEndpoint,
|
|
595
|
-
pkce: body.oidcConfig.pkce,
|
|
596
|
-
discoveryEndpoint:
|
|
597
|
-
body.oidcConfig.discoveryEndpoint ||
|
|
598
|
-
`${body.issuer}/.well-known/openid-configuration`,
|
|
599
|
-
mapping: body.oidcConfig.mapping,
|
|
600
|
-
scopes: body.oidcConfig.scopes,
|
|
601
|
-
userInfoEndpoint: body.oidcConfig.userInfoEndpoint,
|
|
602
|
-
overrideUserInfo:
|
|
603
|
-
ctx.body.overrideUserInfo ||
|
|
604
|
-
options?.defaultOverrideUserInfo ||
|
|
605
|
-
false,
|
|
606
|
-
})
|
|
607
|
-
: null,
|
|
753
|
+
oidcConfig: buildOIDCConfig(),
|
|
608
754
|
samlConfig: body.samlConfig
|
|
609
755
|
? JSON.stringify({
|
|
610
756
|
issuer: body.issuer,
|
|
@@ -1661,6 +1807,12 @@ export const callbackSSOSAML = (options?: SSOOptions) => {
|
|
|
1661
1807
|
|
|
1662
1808
|
const { extract } = parsedResponse!;
|
|
1663
1809
|
|
|
1810
|
+
validateSAMLTimestamp((extract as any).conditions, {
|
|
1811
|
+
clockSkew: options?.saml?.clockSkew,
|
|
1812
|
+
requireTimestamps: options?.saml?.requireTimestamps,
|
|
1813
|
+
logger: ctx.context.logger,
|
|
1814
|
+
});
|
|
1815
|
+
|
|
1664
1816
|
const inResponseTo = (extract as any).inResponseTo as string | undefined;
|
|
1665
1817
|
const shouldValidateInResponseTo =
|
|
1666
1818
|
options?.saml?.authnRequestStore ||
|
|
@@ -1685,6 +1837,13 @@ export const callbackSSOSAML = (options?: SSOOptions) => {
|
|
|
1685
1837
|
storedRequest = JSON.parse(
|
|
1686
1838
|
verification.value,
|
|
1687
1839
|
) as AuthnRequestRecord;
|
|
1840
|
+
// Validate expiration for database-stored records
|
|
1841
|
+
// Note: Cleanup of expired records is handled automatically by
|
|
1842
|
+
// findVerificationValue, but we still need to check expiration
|
|
1843
|
+
// since the record is returned before cleanup runs
|
|
1844
|
+
if (storedRequest && storedRequest.expiresAt < Date.now()) {
|
|
1845
|
+
storedRequest = null;
|
|
1846
|
+
}
|
|
1688
1847
|
} catch {
|
|
1689
1848
|
storedRequest = null;
|
|
1690
1849
|
}
|
|
@@ -2086,6 +2245,12 @@ export const acsEndpoint = (options?: SSOOptions) => {
|
|
|
2086
2245
|
|
|
2087
2246
|
const { extract } = parsedResponse!;
|
|
2088
2247
|
|
|
2248
|
+
validateSAMLTimestamp((extract as any).conditions, {
|
|
2249
|
+
clockSkew: options?.saml?.clockSkew,
|
|
2250
|
+
requireTimestamps: options?.saml?.requireTimestamps,
|
|
2251
|
+
logger: ctx.context.logger,
|
|
2252
|
+
});
|
|
2253
|
+
|
|
2089
2254
|
const inResponseToAcs = (extract as any).inResponseTo as
|
|
2090
2255
|
| string
|
|
2091
2256
|
| undefined;
|
|
@@ -2112,6 +2277,9 @@ export const acsEndpoint = (options?: SSOOptions) => {
|
|
|
2112
2277
|
storedRequest = JSON.parse(
|
|
2113
2278
|
verification.value,
|
|
2114
2279
|
) as AuthnRequestRecord;
|
|
2280
|
+
if (storedRequest && storedRequest.expiresAt < Date.now()) {
|
|
2281
|
+
storedRequest = null;
|
|
2282
|
+
}
|
|
2115
2283
|
} catch {
|
|
2116
2284
|
storedRequest = null;
|
|
2117
2285
|
}
|