oidc-spa 8.6.19 → 8.7.1

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.
Files changed (140) hide show
  1. package/backend.d.ts +3 -20
  2. package/backend.js +50 -242
  3. package/backend.js.map +1 -1
  4. package/core/OidcMetadata.d.ts +2 -2
  5. package/core/OidcMetadata.js.map +1 -1
  6. package/core/createOidc.d.ts +2 -4
  7. package/core/createOidc.js +49 -3
  8. package/core/createOidc.js.map +1 -1
  9. package/core/dpop.d.ts +20 -0
  10. package/core/dpop.js +389 -0
  11. package/core/dpop.js.map +1 -0
  12. package/core/earlyInit.js +2 -0
  13. package/core/earlyInit.js.map +1 -1
  14. package/core/oidcClientTsUserToTokens.d.ts +1 -0
  15. package/core/oidcClientTsUserToTokens.js +15 -5
  16. package/core/oidcClientTsUserToTokens.js.map +1 -1
  17. package/core/tokenExfiltrationDefense.js +49 -6
  18. package/core/tokenExfiltrationDefense.js.map +1 -1
  19. package/esm/angular.d.ts +2 -0
  20. package/esm/angular.mjs.map +1 -1
  21. package/esm/backend.d.ts +3 -20
  22. package/esm/backend.mjs +50 -242
  23. package/esm/backend.mjs.map +1 -1
  24. package/esm/core/OidcMetadata.d.ts +2 -2
  25. package/esm/core/OidcMetadata.mjs.map +1 -1
  26. package/esm/core/createOidc.d.ts +2 -4
  27. package/esm/core/createOidc.mjs +49 -3
  28. package/esm/core/createOidc.mjs.map +1 -1
  29. package/esm/core/dpop.d.ts +20 -0
  30. package/esm/core/dpop.mjs +384 -0
  31. package/esm/core/dpop.mjs.map +1 -0
  32. package/esm/core/earlyInit.mjs +2 -0
  33. package/esm/core/earlyInit.mjs.map +1 -1
  34. package/esm/core/oidcClientTsUserToTokens.d.ts +1 -0
  35. package/esm/core/oidcClientTsUserToTokens.mjs +15 -5
  36. package/esm/core/oidcClientTsUserToTokens.mjs.map +1 -1
  37. package/esm/core/tokenExfiltrationDefense.mjs +49 -6
  38. package/esm/core/tokenExfiltrationDefense.mjs.map +1 -1
  39. package/esm/react-spa/createOidcSpaApi.mjs +2 -1
  40. package/esm/react-spa/createOidcSpaApi.mjs.map +1 -1
  41. package/esm/react-spa/types.d.ts +2 -0
  42. package/esm/server/createOidcSpaUtils.d.ts +5 -0
  43. package/esm/server/createOidcSpaUtils.mjs +639 -0
  44. package/esm/server/createOidcSpaUtils.mjs.map +1 -0
  45. package/esm/server/index.d.ts +2 -0
  46. package/esm/server/index.mjs +3 -0
  47. package/esm/server/index.mjs.map +1 -0
  48. package/esm/server/types.d.ts +79 -0
  49. package/esm/server/types.mjs +2 -0
  50. package/esm/server/types.mjs.map +1 -0
  51. package/esm/server/utilsBuilder.d.ts +10 -0
  52. package/esm/server/utilsBuilder.mjs +13 -0
  53. package/esm/server/utilsBuilder.mjs.map +1 -0
  54. package/esm/tanstack-start/react/accessTokenValidation_rfc9068.d.ts +1 -1
  55. package/esm/tanstack-start/react/accessTokenValidation_rfc9068.mjs +102 -94
  56. package/esm/tanstack-start/react/accessTokenValidation_rfc9068.mjs.map +1 -1
  57. package/esm/tanstack-start/react/createOidcSpaApi.d.ts +2 -2
  58. package/esm/tanstack-start/react/createOidcSpaApi.mjs +60 -51
  59. package/esm/tanstack-start/react/createOidcSpaApi.mjs.map +1 -1
  60. package/esm/tanstack-start/react/index.d.ts +1 -1
  61. package/esm/tanstack-start/react/index.mjs +2 -2
  62. package/esm/tanstack-start/react/index.mjs.map +1 -1
  63. package/esm/tanstack-start/react/types.d.ts +36 -11
  64. package/esm/tanstack-start/react/{apiBuilder.d.ts → utilsBuilder.d.ts} +9 -9
  65. package/esm/tanstack-start/react/{apiBuilder.mjs → utilsBuilder.mjs} +6 -6
  66. package/esm/tanstack-start/react/utilsBuilder.mjs.map +1 -0
  67. package/esm/tools/generateES256DPoPProof.d.ts +8 -0
  68. package/esm/tools/generateES256DPoPProof.mjs +48 -0
  69. package/esm/tools/generateES256DPoPProof.mjs.map +1 -0
  70. package/esm/tools/getServerDateNow.d.ts +5 -0
  71. package/esm/tools/getServerDateNow.mjs +7 -0
  72. package/esm/tools/getServerDateNow.mjs.map +1 -0
  73. package/esm/vendor/{backend → server}/evt.mjs +84 -140
  74. package/esm/vendor/{backend → server}/jose.mjs +5 -27
  75. package/esm/vendor/{backend → server}/tsafe.d.ts +1 -0
  76. package/esm/vendor/{backend → server}/tsafe.mjs +6 -0
  77. package/esm/vendor/{backend → server}/zod.mjs +196 -50
  78. package/package.json +6 -1
  79. package/react-spa/createOidcSpaApi.js +2 -1
  80. package/react-spa/createOidcSpaApi.js.map +1 -1
  81. package/react-spa/types.d.ts +2 -0
  82. package/server/createOidcSpaUtils.d.ts +5 -0
  83. package/server/createOidcSpaUtils.js +642 -0
  84. package/server/createOidcSpaUtils.js.map +1 -0
  85. package/server/index.d.ts +2 -0
  86. package/server/index.js +6 -0
  87. package/server/index.js.map +1 -0
  88. package/server/types.d.ts +79 -0
  89. package/server/types.js +3 -0
  90. package/server/types.js.map +1 -0
  91. package/server/utilsBuilder.d.ts +10 -0
  92. package/server/utilsBuilder.js +16 -0
  93. package/server/utilsBuilder.js.map +1 -0
  94. package/src/angular.ts +3 -0
  95. package/src/backend.ts +63 -364
  96. package/src/core/OidcMetadata.ts +4 -2
  97. package/src/core/createOidc.ts +62 -6
  98. package/src/core/dpop.ts +583 -0
  99. package/src/core/earlyInit.ts +3 -0
  100. package/src/core/oidcClientTsUserToTokens.ts +18 -4
  101. package/src/core/tokenExfiltrationDefense.ts +60 -5
  102. package/src/react-spa/createOidcSpaApi.ts +2 -1
  103. package/src/react-spa/types.tsx +3 -0
  104. package/src/server/createOidcSpaUtils.ts +848 -0
  105. package/src/server/index.ts +4 -0
  106. package/src/server/types.tsx +99 -0
  107. package/src/server/utilsBuilder.ts +41 -0
  108. package/src/tanstack-start/react/accessTokenValidation_rfc9068.ts +134 -124
  109. package/src/tanstack-start/react/createOidcSpaApi.ts +73 -69
  110. package/src/tanstack-start/react/index.ts +2 -2
  111. package/src/tanstack-start/react/types.tsx +44 -12
  112. package/src/tanstack-start/react/{apiBuilder.ts → utilsBuilder.ts} +14 -14
  113. package/src/tools/generateES256DPoPProof.ts +74 -0
  114. package/src/tools/getServerDateNow.ts +11 -0
  115. package/src/vendor/{backend → server}/tsafe.ts +1 -0
  116. package/tools/generateES256DPoPProof.d.ts +8 -0
  117. package/tools/generateES256DPoPProof.js +51 -0
  118. package/tools/generateES256DPoPProof.js.map +1 -0
  119. package/tools/getServerDateNow.d.ts +5 -0
  120. package/tools/getServerDateNow.js +10 -0
  121. package/tools/getServerDateNow.js.map +1 -0
  122. package/vendor/server/evt.js +3 -0
  123. package/vendor/server/jose.js +3 -0
  124. package/vendor/{backend → server}/tsafe.d.ts +1 -0
  125. package/vendor/server/tsafe.js +2 -0
  126. package/vendor/server/zod.js +3 -0
  127. package/esm/tanstack-start/react/apiBuilder.mjs.map +0 -1
  128. package/vendor/backend/evt.js +0 -3
  129. package/vendor/backend/jose.js +0 -3
  130. package/vendor/backend/tsafe.js +0 -2
  131. package/vendor/backend/zod.js +0 -3
  132. /package/esm/vendor/{backend → server}/evt.d.ts +0 -0
  133. /package/esm/vendor/{backend → server}/jose.d.ts +0 -0
  134. /package/esm/vendor/{backend → server}/zod.d.ts +0 -0
  135. /package/src/vendor/{backend → server}/evt.ts +0 -0
  136. /package/src/vendor/{backend → server}/jose.ts +0 -0
  137. /package/src/vendor/{backend → server}/zod.ts +0 -0
  138. /package/vendor/{backend → server}/evt.d.ts +0 -0
  139. /package/vendor/{backend → server}/jose.d.ts +0 -0
  140. /package/vendor/{backend → server}/zod.d.ts +0 -0
@@ -0,0 +1,4 @@
1
+ export type * from "./types";
2
+ import { oidcSpaUtilsBuilder } from "./utilsBuilder";
3
+
4
+ export const oidcSpa = oidcSpaUtilsBuilder;
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Claims defined by RFC 9068: "JSON Web Token (JWT) Profile for OAuth 2.0 Access Tokens"
3
+ * https://datatracker.ietf.org/doc/html/rfc9068
4
+ *
5
+ * These tokens are intended for consumption by resource servers.
6
+ */
7
+ export type DecodedAccessToken_RFC9068 = {
8
+ // --- REQUIRED (MUST) ---
9
+ iss: string; // Issuer Identifier
10
+ sub: string; // Subject Identifier
11
+ aud: string | string[]; // Audience(s)
12
+ exp: number; // Expiration time (seconds since epoch)
13
+ iat: number; // Issued-at time (seconds since epoch)
14
+
15
+ // --- RECOMMENDED (SHOULD) ---
16
+ client_id?: string; // OAuth2 Client ID that requested the token
17
+ scope?: string; // Space-separated list of granted scopes
18
+ jti?: string; // Unique JWT ID (for replay detection)
19
+
20
+ // --- OPTIONAL / EXTENSION CLAIMS ---
21
+ nbf?: number; // Not-before time (standard JWT claim)
22
+ auth_time?: number; // Time of user authentication (optional)
23
+ cnf?: Record<string, unknown>; // Confirmation (e.g. proof-of-possession)
24
+ [key: string]: unknown; // Allow custom claims (e.g. roles, groups)
25
+ };
26
+
27
+ export type ValidateAndDecodeAccessToken<DecodedAccessToken> = (params: {
28
+ request: {
29
+ method: string;
30
+ url: string;
31
+ headers: Record<"Authorization" | "DPoP", string | null | undefined>;
32
+ };
33
+ }) => Promise<ValidateAndDecodeAccessToken.ReturnType<DecodedAccessToken>>;
34
+
35
+ export namespace ValidateAndDecodeAccessToken {
36
+ export type ReturnType<DecodedAccessToken> =
37
+ | (ReturnType.Success<DecodedAccessToken> & { errorCause?: never; debugErrorMessage?: never })
38
+ | (ReturnType.Errored & {
39
+ decodedAccessToken?: never;
40
+ decodedAccessToken_original?: never;
41
+ accessToken?: never;
42
+ });
43
+
44
+ export namespace ReturnType {
45
+ export type Success<DecodedAccessToken> = {
46
+ isSuccess: true;
47
+ decodedAccessToken: DecodedAccessToken;
48
+ decodedAccessToken_original: DecodedAccessToken_RFC9068;
49
+ accessToken: string;
50
+ };
51
+
52
+ export type Errored = {
53
+ isSuccess: false;
54
+ errorCause:
55
+ | "missing Authorization header"
56
+ | "validation error"
57
+ | "validation error - access token expired"
58
+ | "validation error - invalid signature";
59
+ debugErrorMessage: string;
60
+ };
61
+ }
62
+ }
63
+
64
+ export type ParamsOfBootstrap<DecodedAccessToken> =
65
+ | ParamsOfBootstrap.Real
66
+ | ParamsOfBootstrap.Mock<DecodedAccessToken>;
67
+
68
+ export namespace ParamsOfBootstrap {
69
+ export type Real = {
70
+ implementation: "real";
71
+ issuerUri: string;
72
+ expectedAudience: string | undefined;
73
+ };
74
+
75
+ export type Mock<DecodedAccessToken> = Mock.DecodeOnly | Mock.UseStaticIdentity<DecodedAccessToken>;
76
+
77
+ export namespace Mock {
78
+ type Common = {
79
+ implementation: "mock";
80
+ };
81
+
82
+ export type DecodeOnly = Common & {
83
+ behavior: "decode only";
84
+ };
85
+
86
+ export type UseStaticIdentity<DecodedAccessToken> = Common & {
87
+ behavior: "use static identity";
88
+ decodedAccessToken_mock: DecodedAccessToken;
89
+ decodedAccessToken_original_mock?: DecodedAccessToken_RFC9068;
90
+ accessToken_mock?: string;
91
+ };
92
+ }
93
+ }
94
+
95
+ export type OidcSpaUtils<DecodedAccessToken> = {
96
+ bootstrapAuth: (params: ParamsOfBootstrap<DecodedAccessToken>) => Promise<void>;
97
+ validateAndDecodeAccessToken: ValidateAndDecodeAccessToken<DecodedAccessToken>;
98
+ ofTypeDecodedAccessToken: DecodedAccessToken;
99
+ };
@@ -0,0 +1,41 @@
1
+ import type { OidcSpaUtils } from "./types";
2
+ import type { ZodSchemaLike } from "../tools/ZodSchemaLike";
3
+ import { createOidcSpaUtils } from "./createOidcSpaUtils";
4
+ import type { DecodedAccessToken_RFC9068 } from "./types";
5
+
6
+ export type OidcSpaUtilsBuilder<
7
+ DecodedAccessToken extends Record<string, unknown> = DecodedAccessToken_RFC9068,
8
+ ExcludedMethod extends "withExpectedDecodedAccessTokenShape" | "createUtils" = never
9
+ > = Omit<
10
+ {
11
+ withExpectedDecodedAccessTokenShape: <
12
+ DecodedAccessToken extends Record<string, unknown>
13
+ >(params: {
14
+ decodedAccessTokenSchema: ZodSchemaLike<DecodedAccessToken_RFC9068, DecodedAccessToken>;
15
+ }) => OidcSpaUtilsBuilder<
16
+ DecodedAccessToken,
17
+ ExcludedMethod | "withExpectedDecodedAccessTokenShape"
18
+ >;
19
+ createUtils: () => OidcSpaUtils<DecodedAccessToken>;
20
+ },
21
+ ExcludedMethod
22
+ >;
23
+
24
+ function createOidcSpaUtilsBuilder<
25
+ DecodedAccessToken extends Record<string, unknown> = DecodedAccessToken_RFC9068
26
+ >(params: {
27
+ decodedAccessTokenSchema: ZodSchemaLike<DecodedAccessToken_RFC9068, DecodedAccessToken> | undefined;
28
+ }): OidcSpaUtilsBuilder<DecodedAccessToken> {
29
+ return {
30
+ withExpectedDecodedAccessTokenShape: ({ decodedAccessTokenSchema }) =>
31
+ createOidcSpaUtilsBuilder({ decodedAccessTokenSchema }),
32
+ createUtils: () =>
33
+ createOidcSpaUtils<DecodedAccessToken>({
34
+ decodedAccessTokenSchema: params.decodedAccessTokenSchema
35
+ })
36
+ };
37
+ }
38
+
39
+ export const oidcSpaUtilsBuilder = createOidcSpaUtilsBuilder({
40
+ decodedAccessTokenSchema: undefined
41
+ });
@@ -1,8 +1,12 @@
1
- import type { CreateValidateAndGetAccessTokenClaims, ParamsOfBootstrap } from "./types";
2
- import type { DecodedAccessToken_RFC9068 as AccessTokenClaims_RFC9068 } from "../../backend";
1
+ import type {
2
+ CreateValidateAndGetAccessTokenClaims,
3
+ ParamsOfBootstrap,
4
+ ValidateAndGetAccessTokenClaims
5
+ } from "./types";
6
+ import type { DecodedAccessToken_RFC9068 as AccessTokenClaims_RFC9068 } from "../../server/types";
3
7
  import type { ZodSchemaLike } from "../../tools/ZodSchemaLike";
4
8
  import { createObjectThatThrowsIfAccessed } from "../../tools/createObjectThatThrowsIfAccessed";
5
- import { assert, type Equals, is } from "../../tools/tsafe/assert";
9
+ import { assert, type Equals, is, id } from "../../vendor/server/tsafe";
6
10
 
7
11
  export function createCreateValidateAndGetAccessTokenClaims_rfc9068<
8
12
  AccessTokenClaims extends Record<string, unknown>
@@ -23,12 +27,84 @@ export function createCreateValidateAndGetAccessTokenClaims_rfc9068<
23
27
  const createValidateAndGetAccessTokenClaims: CreateValidateAndGetAccessTokenClaims<
24
28
  AccessTokenClaims
25
29
  > = ({ paramsOfBootstrap }) => {
26
- if (paramsOfBootstrap.implementation === "mock") {
27
- return {
28
- validateAndGetAccessTokenClaims: async () => {
29
- return {
30
- isValid: true,
31
- accessTokenClaims: (() => {
30
+ const prValidateAndDecodeAccessToken = (async () => {
31
+ const { oidcSpa: oidcSpa_server } = await import("../../server");
32
+
33
+ const { bootstrapAuth, validateAndDecodeAccessToken } =
34
+ accessTokenClaimsSchema === undefined
35
+ ? (oidcSpa_server.createUtils() as never)
36
+ : oidcSpa_server
37
+ .withExpectedDecodedAccessTokenShape({
38
+ decodedAccessTokenSchema: accessTokenClaimsSchema
39
+ })
40
+ .createUtils();
41
+
42
+ switch (paramsOfBootstrap.implementation) {
43
+ case "real":
44
+ {
45
+ const expectedAudience = (() => {
46
+ if (expectedAudienceGetter === undefined) {
47
+ return undefined;
48
+ }
49
+
50
+ const missingEnvNames = new Set<string>();
51
+
52
+ const env_proxy = new Proxy<Record<string, string>>(
53
+ {},
54
+ {
55
+ get: (...[, envName]) => {
56
+ assert(typeof envName === "string");
57
+
58
+ const value = process.env[envName];
59
+
60
+ if (value === undefined) {
61
+ missingEnvNames.add(envName);
62
+ return "";
63
+ }
64
+
65
+ return value;
66
+ },
67
+ has: (...[, envName]) => {
68
+ assert(typeof envName === "string");
69
+ return true;
70
+ }
71
+ }
72
+ );
73
+
74
+ const expectedAudience = expectedAudienceGetter?.({
75
+ paramsOfBootstrap,
76
+ process: { env: env_proxy }
77
+ });
78
+
79
+ if (!expectedAudience) {
80
+ throw new Error(
81
+ [
82
+ "oidc-spa: The expectedAudience() you provided returned empty.",
83
+ "If you specified the expectedAudience in withAccessTokenValidation",
84
+ "it's probably an error.",
85
+ missingEnvNames.size !== 0 &&
86
+ `Did you forget to set the env var: ${Array.from(
87
+ missingEnvNames
88
+ ).join(", ")} ?`
89
+ ]
90
+ .filter(line => typeof line === "string")
91
+ .join(" ")
92
+ );
93
+ }
94
+
95
+ return expectedAudience;
96
+ })();
97
+
98
+ await bootstrapAuth({
99
+ implementation: "real",
100
+ issuerUri: paramsOfBootstrap.issuerUri,
101
+ expectedAudience
102
+ });
103
+ }
104
+ break;
105
+ case "mock":
106
+ {
107
+ const decodedAccessToken_mock = (() => {
32
108
  if (paramsOfBootstrap.accessTokenClaims_mock !== undefined) {
33
109
  assert(is<AccessTokenClaims>(paramsOfBootstrap.accessTokenClaims_mock));
34
110
  return paramsOfBootstrap.accessTokenClaims_mock;
@@ -46,131 +122,65 @@ export function createCreateValidateAndGetAccessTokenClaims_rfc9068<
46
122
  "specify accessTokenClaims_mock when calling bootstrapOidc()"
47
123
  ].join(" ")
48
124
  });
49
- })()
50
- };
51
- }
52
- };
53
- }
54
- assert<Equals<(typeof paramsOfBootstrap)["implementation"], "real">>;
55
-
56
- const prVerifyAndDecodeAccessToken = (async () => {
57
- const { createOidcBackend } = await import("../../backend");
58
-
59
- const { verifyAndDecodeAccessToken } = await createOidcBackend({
60
- issuerUri: paramsOfBootstrap.issuerUri,
61
- decodedAccessTokenSchema: accessTokenClaimsSchema
62
- });
63
-
64
- return verifyAndDecodeAccessToken;
65
- })();
66
-
67
- const expectedAudience = (() => {
68
- if (expectedAudienceGetter === undefined) {
69
- return undefined;
70
- }
71
-
72
- const missingEnvNames = new Set<string>();
125
+ })();
73
126
 
74
- const env_proxy = new Proxy<Record<string, string>>(
75
- {},
76
- {
77
- get: (...[, envName]) => {
78
- assert(typeof envName === "string");
79
-
80
- const value = process.env[envName];
81
-
82
- if (value === undefined) {
83
- missingEnvNames.add(envName);
84
- return "";
85
- }
86
-
87
- return value;
88
- },
89
- has: (...[, envName]) => {
90
- assert(typeof envName === "string");
91
- return true;
127
+ await bootstrapAuth({
128
+ implementation: "mock",
129
+ behavior: "use static identity",
130
+ decodedAccessToken_mock
131
+ });
92
132
  }
93
- }
94
- );
95
-
96
- const expectedAudience = expectedAudienceGetter?.({
97
- paramsOfBootstrap,
98
- process: { env: env_proxy }
99
- });
100
-
101
- if (!expectedAudience) {
102
- throw new Error(
103
- [
104
- "oidc-spa: The expectedAudience() you provided returned empty.",
105
- "If you specified the expectedAudience in withAccessTokenValidation",
106
- "it's probably and error.",
107
- missingEnvNames.size !== 0 &&
108
- `Did you forget to set the env var: ${Array.from(missingEnvNames).join(
109
- ", "
110
- )} ?`
111
- ]
112
- .filter(line => typeof line === "string")
113
- .join(" ")
114
- );
133
+ break;
134
+ default:
135
+ assert<Equals<typeof paramsOfBootstrap, never>>(false);
115
136
  }
116
137
 
117
- return expectedAudience;
138
+ return validateAndDecodeAccessToken;
118
139
  })();
119
140
 
120
- return {
121
- validateAndGetAccessTokenClaims: async ({ accessToken }) => {
122
- const verifyAndDecodeAccessToken = await prVerifyAndDecodeAccessToken;
123
-
124
- const {
125
- isValid,
126
- errorCase,
127
- errorMessage,
128
- decodedAccessToken,
129
- decodedAccessToken_original
130
- } = await verifyAndDecodeAccessToken({ accessToken });
131
-
132
- if (!isValid) {
133
- return {
134
- isValid: false,
135
- errorMessage: `${errorCase}: ${errorMessage}`,
136
- wwwAuthenticateHeaderErrorDescription: (() => {
137
- switch (errorCase) {
138
- case "does not respect schema":
139
- return "The access token is malformed or missing required claims";
140
- case "expired":
141
- return "The access token expired";
142
- case "invalid signature":
143
- return "The access token signature is invalid";
144
- }
145
- })()
146
- };
147
- }
148
-
149
- if (expectedAudience !== undefined) {
150
- const aud_array =
151
- typeof decodedAccessToken_original.aud === "string"
152
- ? [decodedAccessToken_original.aud]
153
- : decodedAccessToken_original.aud;
154
-
155
- if (!aud_array.includes(expectedAudience)) {
156
- return {
157
- isValid: false,
158
- errorMessage: [
159
- "Access token is not for the expected audience.",
160
- `Got aud claim: ${JSON.stringify(decodedAccessToken_original.aud)}`,
161
- `Expected: ${expectedAudience}`
162
- ].join(" "),
163
- wwwAuthenticateHeaderErrorDescription: "The access token audience is invalid"
164
- };
165
- }
141
+ const validateAndGetAccessTokenClaims: ValidateAndGetAccessTokenClaims<
142
+ AccessTokenClaims
143
+ > = async ({ request }) => {
144
+ const validateAndDecodeAccessToken = await prValidateAndDecodeAccessToken;
145
+
146
+ const { isSuccess, errorCause, debugErrorMessage, decodedAccessToken, accessToken } =
147
+ await validateAndDecodeAccessToken({
148
+ request
149
+ });
150
+
151
+ if (!isSuccess) {
152
+ if (errorCause === "missing Authorization header") {
153
+ return id<ValidateAndGetAccessTokenClaims.ReturnType.Errored.AnonymousRequest>({
154
+ isSuccess: false,
155
+ isAnonymousRequest: true
156
+ });
166
157
  }
167
158
 
168
- return {
169
- isValid: true,
170
- accessTokenClaims: decodedAccessToken
171
- };
159
+ return id<ValidateAndGetAccessTokenClaims.ReturnType.Errored.ValidationFailed>({
160
+ isSuccess: false,
161
+ isAnonymousRequest: false,
162
+ debugErrorMessage: `${errorCause}: ${debugErrorMessage}`,
163
+ wwwAuthenticateResponseHeaderValue: `Bearer error="invalid_token", error_description="${(() => {
164
+ switch (errorCause) {
165
+ case "validation error":
166
+ case "validation error - invalid signature":
167
+ case "validation error - access token expired":
168
+ return "Validation Failed";
169
+ default:
170
+ assert<Equals<typeof errorCause, never>>(false);
171
+ }
172
+ })()}"`
173
+ });
172
174
  }
175
+
176
+ return id<ValidateAndGetAccessTokenClaims.ReturnType.Success<AccessTokenClaims>>({
177
+ isSuccess: true,
178
+ accessTokenClaims: decodedAccessToken,
179
+ accessToken
180
+ });
173
181
  };
182
+
183
+ return { validateAndGetAccessTokenClaims };
174
184
  };
175
185
 
176
186
  return { createValidateAndGetAccessTokenClaims };
@@ -1,7 +1,7 @@
1
1
  import { useState, useEffect, useReducer } from "react";
2
2
  import type {
3
3
  CreateValidateAndGetAccessTokenClaims,
4
- OidcSpaApi,
4
+ OidcSpaUtils,
5
5
  UseOidc,
6
6
  GetOidc,
7
7
  ParamsOfBootstrap,
@@ -21,6 +21,7 @@ import type { GetterOrDirectValue } from "../../tools/GetterOrDirectValue";
21
21
  import { createServerFn, createMiddleware } from "@tanstack/react-start";
22
22
  // @ts-expect-error: Since our module is not labeled as ESM we don't have the types here.
23
23
  import { getRequest, setResponseHeader, setResponseStatus } from "@tanstack/react-start/server";
24
+ //import { getRequest, setResponseHeader, setResponseStatus } from "@tanstack/react-start-server";
24
25
  import { toFullyQualifiedUrl } from "../../tools/toFullyQualifiedUrl";
25
26
  import { BEFORE_LOAD_FN_BRAND_PROPERTY_NAME } from "./disableSsrIfLoginEnforced";
26
27
  import { setDesiredPostLoginRedirectUrl } from "../../core/desiredPostLoginRedirectUrl";
@@ -40,7 +41,7 @@ export function createOidcSpaApi<
40
41
  createValidateAndGetAccessTokenClaims:
41
42
  | CreateValidateAndGetAccessTokenClaims<AccessTokenClaims>
42
43
  | undefined;
43
- }): OidcSpaApi<AutoLogin, DecodedIdToken, AccessTokenClaims> {
44
+ }): OidcSpaUtils<AutoLogin, DecodedIdToken, AccessTokenClaims> {
44
45
  const {
45
46
  autoLogin,
46
47
  decodedIdTokenSchema,
@@ -667,7 +668,8 @@ export function createOidcSpaApi<
667
668
  __metadata: paramsOfBootstrap.__metadata,
668
669
  __unsafe_useIdTokenAsAccessToken:
669
670
  paramsOfBootstrap.__unsafe_useIdTokenAsAccessToken,
670
- autoLogoutParams: paramsOfBootstrap.autoLogoutParams
671
+ autoLogoutParams: paramsOfBootstrap.autoLogoutParams,
672
+ dpop: paramsOfBootstrap.dpop
671
673
  });
672
674
  } catch (error) {
673
675
  if (!(error instanceof OidcInitializationError)) {
@@ -786,84 +788,86 @@ export function createOidcSpaApi<
786
788
  const { next } = options;
787
789
 
788
790
  const unauthorized = (params: {
789
- errorMessage: string;
790
- wwwAuthenticateHeaderErrorDescription: string;
791
+ code: 401 | 403;
792
+ wwwAuthenticateResponseHeaderValue: string;
793
+ debugErrorMessage: string;
791
794
  }) => {
792
- const { errorMessage, wwwAuthenticateHeaderErrorDescription } = params;
793
-
794
- setResponseHeader(
795
- "WWW-Authenticate",
796
- `Bearer error="invalid_token", error_description="${wwwAuthenticateHeaderErrorDescription}"`
795
+ const { code, wwwAuthenticateResponseHeaderValue, debugErrorMessage } = params;
796
+
797
+ setResponseHeader("WWW-Authenticate", wwwAuthenticateResponseHeaderValue);
798
+ setResponseStatus(
799
+ code,
800
+ (() => {
801
+ switch (code) {
802
+ case 401:
803
+ return "Unauthorized";
804
+ case 403:
805
+ return "Forbidden";
806
+ default:
807
+ assert<Equals<typeof code, never>>(false);
808
+ }
809
+ })()
797
810
  );
798
- setResponseStatus(401, "Unauthorized");
799
811
 
800
- return new Error(`oidc-spa: ${errorMessage}`);
812
+ if (process.env.NODE_ENV === "development") {
813
+ console.error(`oidc-spa: ${debugErrorMessage}`);
814
+ }
815
+
816
+ return new Error(`oidc-spa: ${wwwAuthenticateResponseHeaderValue}`);
801
817
  };
802
818
 
803
- const { headers } = getRequest();
819
+ assert(prValidateAndGetAccessTokenClaims !== undefined);
804
820
 
805
- const authorizationHeaderValue = headers.get("Authorization");
821
+ const { validateAndGetAccessTokenClaims } = await prValidateAndGetAccessTokenClaims;
806
822
 
807
- if (authorizationHeaderValue === null) {
808
- if (params?.assert === "user logged in") {
809
- const errorMessage = [
810
- "Asserted user logged in for that serverFn request",
811
- "but no access token was attached to the request"
812
- ].join(" ");
823
+ const { headers, url, method } = getRequest();
813
824
 
814
- throw unauthorized({
815
- errorMessage,
816
- wwwAuthenticateHeaderErrorDescription: errorMessage
817
- });
825
+ const resultOfValidate = await validateAndGetAccessTokenClaims({
826
+ request: {
827
+ url,
828
+ method,
829
+ headers: {
830
+ Authorization: headers.get("Authorization"),
831
+ DPoP: headers.get("DPoP")
832
+ }
818
833
  }
834
+ });
819
835
 
820
- return next({
821
- context: {
822
- oidc: id<OidcServerContext<AccessTokenClaims>>(
823
- id<OidcServerContext.NotLoggedIn>({
824
- isUserLoggedIn: false
825
- })
826
- )
836
+ if (!resultOfValidate.isSuccess) {
837
+ if (resultOfValidate.isAnonymousRequest) {
838
+ if (params?.assert === "user logged in") {
839
+ throw unauthorized({
840
+ code: 401,
841
+ wwwAuthenticateResponseHeaderValue:
842
+ 'Bearer error="invalid_request", error_description="Missing access token"',
843
+ debugErrorMessage: [
844
+ "Asserted user logged in for that serverFn request",
845
+ "but no access token was attached to the request"
846
+ ].join(" ")
847
+ });
827
848
  }
828
- });
829
- }
830
849
 
831
- const accessToken = (() => {
832
- const prefix = "Bearer ";
833
-
834
- if (!authorizationHeaderValue.startsWith(prefix)) {
835
- return undefined;
850
+ return next({
851
+ context: {
852
+ oidc: id<OidcServerContext<AccessTokenClaims>>(
853
+ id<OidcServerContext.NotLoggedIn>({
854
+ isUserLoggedIn: false
855
+ })
856
+ )
857
+ }
858
+ });
836
859
  }
837
860
 
838
- return authorizationHeaderValue.slice(prefix.length);
839
- })();
840
-
841
- if (accessToken === undefined) {
842
- const errorMessage =
843
- "Missing well formed Authorization header with Bearer <access_token>";
844
-
845
- throw unauthorized({
846
- errorMessage,
847
- wwwAuthenticateHeaderErrorDescription: errorMessage
848
- });
849
- }
850
-
851
- assert(prValidateAndGetAccessTokenClaims !== undefined);
852
-
853
- const { validateAndGetAccessTokenClaims } = await prValidateAndGetAccessTokenClaims;
854
-
855
- const resultOfValidate = await validateAndGetAccessTokenClaims({ accessToken });
856
-
857
- if (!resultOfValidate.isValid) {
858
- const { errorMessage, wwwAuthenticateHeaderErrorDescription } = resultOfValidate;
861
+ const { debugErrorMessage, wwwAuthenticateResponseHeaderValue } = resultOfValidate;
859
862
 
860
863
  throw unauthorized({
861
- errorMessage,
862
- wwwAuthenticateHeaderErrorDescription
864
+ code: 401,
865
+ wwwAuthenticateResponseHeaderValue,
866
+ debugErrorMessage
863
867
  });
864
868
  }
865
869
 
866
- const { accessTokenClaims } = resultOfValidate;
870
+ const { accessTokenClaims, accessToken } = resultOfValidate;
867
871
 
868
872
  assert(is<Exclude<AccessTokenClaims, undefined>>(accessTokenClaims));
869
873
 
@@ -900,14 +904,14 @@ export function createOidcSpaApi<
900
904
  break check_required_claims;
901
905
  }
902
906
 
903
- const errorMessage = [
904
- "Missing or invalid required access token claim.",
905
- `Related to claims: ${Array.from(accessedClaimNames).join(" and/or ")}`
906
- ].join(" ");
907
-
908
907
  throw unauthorized({
909
- errorMessage,
910
- wwwAuthenticateHeaderErrorDescription: errorMessage
908
+ code: 403,
909
+ wwwAuthenticateResponseHeaderValue:
910
+ 'Bearer error="insufficient_scope", error_description="Insufficient privileges"',
911
+ debugErrorMessage: [
912
+ "Missing or invalid required access token claim.",
913
+ `Related to claims: ${Array.from(accessedClaimNames).join(" and/or ")}`
914
+ ].join(" ")
911
915
  });
912
916
  }
913
917
 
@@ -1,6 +1,6 @@
1
1
  export { __disableSsrIfLoginEnforced } from "./disableSsrIfLoginEnforced";
2
2
  export { __withOidcSpaServerEntry } from "./withOidcSpaServerEntry";
3
3
  export type * from "./types";
4
- import { oidcSpaApiBuilder } from "./apiBuilder";
4
+ import { oidcSpaUtilsBuilder } from "./utilsBuilder";
5
5
 
6
- export const oidcSpa = oidcSpaApiBuilder;
6
+ export const oidcSpa = oidcSpaUtilsBuilder;