@tdacorp/identity-client 0.1.0 → 0.2.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.
- package/README.md +44 -15
- package/dist/{chunk-VJSMXIZB.js → chunk-S5WGUOJA.js} +42 -17
- package/dist/index.cjs +42 -17
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/next.cjs +34 -9
- package/dist/next.d.cts +13 -3
- package/dist/next.d.ts +13 -3
- package/dist/next.js +14 -2
- package/dist/{verify-BiGhwaIz.d.cts → verify-C1Ga6JbS.d.cts} +48 -7
- package/dist/{verify-BiGhwaIz.d.ts → verify-C1Ga6JbS.d.ts} +48 -7
- package/package.json +11 -11
package/README.md
CHANGED
|
@@ -53,6 +53,9 @@ export const GET = createCallbackRoute({
|
|
|
53
53
|
redirectUri: `${process.env.APP_URL}/api/auth/callback`,
|
|
54
54
|
cookieSecret: process.env.COOKIE_SECRET!,
|
|
55
55
|
onSuccess: async (tokens, idClaims, { returnTo }) => {
|
|
56
|
+
// returnTo, when present, has already been validated to a same-app
|
|
57
|
+
// relative path (single leading `/`, never an absolute URL) by
|
|
58
|
+
// createLoginRoute -- safe to resolve against APP_URL like this.
|
|
56
59
|
const response = NextResponse.redirect(new URL(returnTo ?? '/', process.env.APP_URL))
|
|
57
60
|
// Store tokens.accessToken / tokens.refreshToken in your own session --
|
|
58
61
|
// this package stops at verified claims, session storage is your call.
|
|
@@ -61,6 +64,18 @@ export const GET = createCallbackRoute({
|
|
|
61
64
|
})
|
|
62
65
|
```
|
|
63
66
|
|
|
67
|
+
`createLoginRoute` rejects anything but a relative path for `returnTo` --
|
|
68
|
+
an absolute URL, a protocol-relative `//host` value, a backslash, or a
|
|
69
|
+
control character is silently dropped (coerced to `undefined`) rather than
|
|
70
|
+
sealed into the transaction cookie, closing the open-redirect this shape
|
|
71
|
+
would otherwise allow. This validation applies regardless of whether
|
|
72
|
+
`returnTo` came from the default `returnTo` search param or a custom
|
|
73
|
+
`getReturnTo`, since this package has no per-app trusted-origin registry to
|
|
74
|
+
check an absolute URL's origin against. If your app legitimately needs to
|
|
75
|
+
redirect to an absolute URL after login, carry it through your own
|
|
76
|
+
mechanism (a separate cookie your app validates against its own known
|
|
77
|
+
origins) rather than this package's `returnTo`.
|
|
78
|
+
|
|
64
79
|
Both handlers seal the PKCE verifier and CSRF `state` into an encrypted
|
|
65
80
|
cookie (via `./sealed`'s `seal()`/`unseal()`) rather than storing them in
|
|
66
81
|
plaintext — you never handle that transaction cookie directly.
|
|
@@ -119,10 +134,11 @@ const discovery = await fetchDiscovery('https://identity.tdacorp.in')
|
|
|
119
134
|
deduplication — concurrent callers for the same issuer during a cold start
|
|
120
135
|
share one fetch rather than each firing their own. Before the result is
|
|
121
136
|
trusted, it is validated: the document's `issuer` must match the URL you
|
|
122
|
-
called with, and `jwks_uri` / `token_endpoint`
|
|
123
|
-
excepted) and same-origin with the issuer.
|
|
124
|
-
edge cache or a cached error page from
|
|
125
|
-
endpoint
|
|
137
|
+
called with, and `jwks_uri` / `token_endpoint` / `authorization_endpoint`
|
|
138
|
+
must be https (loopback hosts excepted) and same-origin with the issuer.
|
|
139
|
+
This is what stops a compromised edge cache or a cached error page from
|
|
140
|
+
redirecting key material, the token endpoint, or the login page itself
|
|
141
|
+
somewhere untrusted.
|
|
126
142
|
|
|
127
143
|
Most callers reach `fetchDiscovery` indirectly — every function below calls
|
|
128
144
|
it internally.
|
|
@@ -160,6 +176,23 @@ const tokens = await exchangeAuthorizationCode({
|
|
|
160
176
|
})
|
|
161
177
|
```
|
|
162
178
|
|
|
179
|
+
If your relying party is registered on the identity platform for
|
|
180
|
+
`client_secret_post` rather than the default `client_secret_basic`, pass
|
|
181
|
+
`clientAuthMethod` so credentials are sent the way the token endpoint
|
|
182
|
+
actually expects them:
|
|
183
|
+
|
|
184
|
+
```ts
|
|
185
|
+
const tokens = await exchangeAuthorizationCode({
|
|
186
|
+
issuer: 'https://identity.tdacorp.in',
|
|
187
|
+
clientId: process.env.IDENTITY_CLIENT_ID!,
|
|
188
|
+
clientSecret: process.env.IDENTITY_CLIENT_SECRET!,
|
|
189
|
+
clientAuthMethod: 'client_secret_post',
|
|
190
|
+
code,
|
|
191
|
+
redirectUri,
|
|
192
|
+
codeVerifier,
|
|
193
|
+
})
|
|
194
|
+
```
|
|
195
|
+
|
|
163
196
|
Refresh (RFC 6749 §6), returning a classified outcome instead of throwing —
|
|
164
197
|
so a transient infrastructure failure is never mistaken for a rejected
|
|
165
198
|
refresh token:
|
|
@@ -182,11 +215,13 @@ if (outcome.outcome === 'success') {
|
|
|
182
215
|
Machine-to-machine calls with no end user use `clientCredentialsGrant` (RFC
|
|
183
216
|
6749 §4.4) instead, always with a confidential `clientSecret` since there is
|
|
184
217
|
no PKCE verifier to authenticate a public client with. A token minted this
|
|
185
|
-
way carries no `sub`
|
|
186
|
-
`
|
|
187
|
-
`
|
|
188
|
-
|
|
189
|
-
|
|
218
|
+
way carries no `sub` — verifying it with `verifyAccessToken` returns the
|
|
219
|
+
`kind: 'machine'` variant of `VerifiedAccessToken`, distinct from the
|
|
220
|
+
`kind: 'user'` variant a normal login produces, so reading `.sub` off the
|
|
221
|
+
wrong branch is a compile error rather than `undefined`. It can still carry a
|
|
222
|
+
`roles` claim: request `roles` in the client_credentials scope and the server
|
|
223
|
+
resolves it against the requesting client itself as the subject, exactly as
|
|
224
|
+
it does for a user token requesting the same scope.
|
|
190
225
|
|
|
191
226
|
## Verification
|
|
192
227
|
|
|
@@ -269,12 +304,6 @@ instead.
|
|
|
269
304
|
|
|
270
305
|
## Limitations in v0.1
|
|
271
306
|
|
|
272
|
-
- **Only `client_secret_basic` client authentication is supported** for the
|
|
273
|
-
authorization-code exchange (`exchangeAuthorizationCode` sends an HTTP
|
|
274
|
-
Basic `Authorization` header when a `clientSecret` is given). A relying
|
|
275
|
-
party configured on TDACorp Identity for `client_secret_post` cannot use
|
|
276
|
-
`exchangeAuthorizationCode` as-is today — the token endpoint will reject
|
|
277
|
-
the request.
|
|
278
307
|
- **No built-in fallback for an `indeterminate` `permits()` result.** That is
|
|
279
308
|
`@tdacorp/identity-authz`'s concern, not this package's, but worth knowing
|
|
280
309
|
up front since most integrations hit both together — see that package's
|
|
@@ -35,6 +35,7 @@ function validateDiscoveryDocument(document, issuerUrl) {
|
|
|
35
35
|
assertHttps(issuerUrl, "issuer");
|
|
36
36
|
assertHttps(document.jwks_uri, "jwks_uri");
|
|
37
37
|
assertHttps(document.token_endpoint, "token_endpoint");
|
|
38
|
+
assertHttps(document.authorization_endpoint, "authorization_endpoint");
|
|
38
39
|
if (document.issuer !== issuerUrl) {
|
|
39
40
|
throw new Error(`Discovery: document issuer "${document.issuer}" does not match the configured issuer "${issuerUrl}"`);
|
|
40
41
|
}
|
|
@@ -45,6 +46,9 @@ function validateDiscoveryDocument(document, issuerUrl) {
|
|
|
45
46
|
if (new URL(document.token_endpoint).origin !== issuerOrigin) {
|
|
46
47
|
throw new Error(`Discovery: token_endpoint "${document.token_endpoint}" is not same-origin as issuer "${issuerUrl}"`);
|
|
47
48
|
}
|
|
49
|
+
if (new URL(document.authorization_endpoint).origin !== issuerOrigin) {
|
|
50
|
+
throw new Error(`Discovery: authorization_endpoint "${document.authorization_endpoint}" is not same-origin as issuer "${issuerUrl}"`);
|
|
51
|
+
}
|
|
48
52
|
}
|
|
49
53
|
async function fetchDiscovery(issuerUrl) {
|
|
50
54
|
const cached = discoveryCache.get(issuerUrl);
|
|
@@ -135,13 +139,16 @@ function toTokenSet(body) {
|
|
|
135
139
|
scope: body.scope
|
|
136
140
|
};
|
|
137
141
|
}
|
|
138
|
-
async function postTokenRequest(tokenEndpoint, clientId, clientSecret, params) {
|
|
142
|
+
async function postTokenRequest(tokenEndpoint, clientId, clientSecret, clientAuthMethod, params) {
|
|
139
143
|
const body = new URLSearchParams(params);
|
|
140
144
|
const headers = {
|
|
141
145
|
"Content-Type": "application/x-www-form-urlencoded",
|
|
142
146
|
Accept: "application/json"
|
|
143
147
|
};
|
|
144
|
-
if (clientSecret) {
|
|
148
|
+
if (clientSecret && clientAuthMethod === "client_secret_post") {
|
|
149
|
+
body.set("client_id", clientId);
|
|
150
|
+
body.set("client_secret", clientSecret);
|
|
151
|
+
} else if (clientSecret) {
|
|
145
152
|
headers.Authorization = `Basic ${btoa(`${clientId}:${clientSecret}`)}`;
|
|
146
153
|
} else {
|
|
147
154
|
body.set("client_id", clientId);
|
|
@@ -153,12 +160,18 @@ async function readTokenResponseBody(response) {
|
|
|
153
160
|
}
|
|
154
161
|
async function exchangeAuthorizationCode(options) {
|
|
155
162
|
const discovery = await fetchDiscovery(options.issuer);
|
|
156
|
-
const response = await postTokenRequest(
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
163
|
+
const response = await postTokenRequest(
|
|
164
|
+
discovery.token_endpoint,
|
|
165
|
+
options.clientId,
|
|
166
|
+
options.clientSecret,
|
|
167
|
+
options.clientAuthMethod ?? "client_secret_basic",
|
|
168
|
+
{
|
|
169
|
+
grant_type: "authorization_code",
|
|
170
|
+
code: options.code,
|
|
171
|
+
redirect_uri: options.redirectUri,
|
|
172
|
+
code_verifier: options.codeVerifier
|
|
173
|
+
}
|
|
174
|
+
);
|
|
162
175
|
const body = await readTokenResponseBody(response);
|
|
163
176
|
if (!response.ok || !body || body.error || typeof body.access_token !== "string") {
|
|
164
177
|
throw new TokenRequestError({ status: response.status, error: body?.error, errorDescription: body?.error_description });
|
|
@@ -167,10 +180,16 @@ async function exchangeAuthorizationCode(options) {
|
|
|
167
180
|
}
|
|
168
181
|
async function clientCredentialsGrant(options) {
|
|
169
182
|
const discovery = await fetchDiscovery(options.issuer);
|
|
170
|
-
const response = await postTokenRequest(
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
183
|
+
const response = await postTokenRequest(
|
|
184
|
+
discovery.token_endpoint,
|
|
185
|
+
options.clientId,
|
|
186
|
+
options.clientSecret,
|
|
187
|
+
options.clientAuthMethod ?? "client_secret_basic",
|
|
188
|
+
{
|
|
189
|
+
grant_type: "client_credentials",
|
|
190
|
+
...options.scope ? { scope: options.scope } : {}
|
|
191
|
+
}
|
|
192
|
+
);
|
|
174
193
|
const body = await readTokenResponseBody(response);
|
|
175
194
|
if (!response.ok || !body || body.error || typeof body.access_token !== "string") {
|
|
176
195
|
throw new TokenRequestError({ status: response.status, error: body?.error, errorDescription: body?.error_description });
|
|
@@ -185,10 +204,16 @@ async function refreshTokens(options) {
|
|
|
185
204
|
let response;
|
|
186
205
|
try {
|
|
187
206
|
const discovery = await fetchDiscovery(options.issuer);
|
|
188
|
-
response = await postTokenRequest(
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
207
|
+
response = await postTokenRequest(
|
|
208
|
+
discovery.token_endpoint,
|
|
209
|
+
options.clientId,
|
|
210
|
+
options.clientSecret,
|
|
211
|
+
options.clientAuthMethod ?? "client_secret_basic",
|
|
212
|
+
{
|
|
213
|
+
grant_type: "refresh_token",
|
|
214
|
+
refresh_token: options.refreshToken
|
|
215
|
+
}
|
|
216
|
+
);
|
|
192
217
|
} catch (error) {
|
|
193
218
|
return { outcome: "transient", error };
|
|
194
219
|
}
|
|
@@ -301,7 +326,7 @@ async function verifyAccessToken(accessToken, options) {
|
|
|
301
326
|
errors: [{ code: "malformed-claims", message: "Access token has neither a sub (user) nor an azp (machine client id)" }]
|
|
302
327
|
};
|
|
303
328
|
}
|
|
304
|
-
return { success: true, data: { kind: "machine", clientId: payload.azp, claims } };
|
|
329
|
+
return { success: true, data: { kind: "machine", clientId: payload.azp, roles, claims } };
|
|
305
330
|
}
|
|
306
331
|
|
|
307
332
|
export {
|
package/dist/index.cjs
CHANGED
|
@@ -72,6 +72,7 @@ function validateDiscoveryDocument(document, issuerUrl) {
|
|
|
72
72
|
assertHttps(issuerUrl, "issuer");
|
|
73
73
|
assertHttps(document.jwks_uri, "jwks_uri");
|
|
74
74
|
assertHttps(document.token_endpoint, "token_endpoint");
|
|
75
|
+
assertHttps(document.authorization_endpoint, "authorization_endpoint");
|
|
75
76
|
if (document.issuer !== issuerUrl) {
|
|
76
77
|
throw new Error(`Discovery: document issuer "${document.issuer}" does not match the configured issuer "${issuerUrl}"`);
|
|
77
78
|
}
|
|
@@ -82,6 +83,9 @@ function validateDiscoveryDocument(document, issuerUrl) {
|
|
|
82
83
|
if (new URL(document.token_endpoint).origin !== issuerOrigin) {
|
|
83
84
|
throw new Error(`Discovery: token_endpoint "${document.token_endpoint}" is not same-origin as issuer "${issuerUrl}"`);
|
|
84
85
|
}
|
|
86
|
+
if (new URL(document.authorization_endpoint).origin !== issuerOrigin) {
|
|
87
|
+
throw new Error(`Discovery: authorization_endpoint "${document.authorization_endpoint}" is not same-origin as issuer "${issuerUrl}"`);
|
|
88
|
+
}
|
|
85
89
|
}
|
|
86
90
|
async function fetchDiscovery(issuerUrl) {
|
|
87
91
|
const cached = discoveryCache.get(issuerUrl);
|
|
@@ -172,13 +176,16 @@ function toTokenSet(body) {
|
|
|
172
176
|
scope: body.scope
|
|
173
177
|
};
|
|
174
178
|
}
|
|
175
|
-
async function postTokenRequest(tokenEndpoint, clientId, clientSecret, params) {
|
|
179
|
+
async function postTokenRequest(tokenEndpoint, clientId, clientSecret, clientAuthMethod, params) {
|
|
176
180
|
const body = new URLSearchParams(params);
|
|
177
181
|
const headers = {
|
|
178
182
|
"Content-Type": "application/x-www-form-urlencoded",
|
|
179
183
|
Accept: "application/json"
|
|
180
184
|
};
|
|
181
|
-
if (clientSecret) {
|
|
185
|
+
if (clientSecret && clientAuthMethod === "client_secret_post") {
|
|
186
|
+
body.set("client_id", clientId);
|
|
187
|
+
body.set("client_secret", clientSecret);
|
|
188
|
+
} else if (clientSecret) {
|
|
182
189
|
headers.Authorization = `Basic ${btoa(`${clientId}:${clientSecret}`)}`;
|
|
183
190
|
} else {
|
|
184
191
|
body.set("client_id", clientId);
|
|
@@ -190,12 +197,18 @@ async function readTokenResponseBody(response) {
|
|
|
190
197
|
}
|
|
191
198
|
async function exchangeAuthorizationCode(options) {
|
|
192
199
|
const discovery = await fetchDiscovery(options.issuer);
|
|
193
|
-
const response = await postTokenRequest(
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
200
|
+
const response = await postTokenRequest(
|
|
201
|
+
discovery.token_endpoint,
|
|
202
|
+
options.clientId,
|
|
203
|
+
options.clientSecret,
|
|
204
|
+
options.clientAuthMethod ?? "client_secret_basic",
|
|
205
|
+
{
|
|
206
|
+
grant_type: "authorization_code",
|
|
207
|
+
code: options.code,
|
|
208
|
+
redirect_uri: options.redirectUri,
|
|
209
|
+
code_verifier: options.codeVerifier
|
|
210
|
+
}
|
|
211
|
+
);
|
|
199
212
|
const body = await readTokenResponseBody(response);
|
|
200
213
|
if (!response.ok || !body || body.error || typeof body.access_token !== "string") {
|
|
201
214
|
throw new TokenRequestError({ status: response.status, error: body?.error, errorDescription: body?.error_description });
|
|
@@ -204,10 +217,16 @@ async function exchangeAuthorizationCode(options) {
|
|
|
204
217
|
}
|
|
205
218
|
async function clientCredentialsGrant(options) {
|
|
206
219
|
const discovery = await fetchDiscovery(options.issuer);
|
|
207
|
-
const response = await postTokenRequest(
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
220
|
+
const response = await postTokenRequest(
|
|
221
|
+
discovery.token_endpoint,
|
|
222
|
+
options.clientId,
|
|
223
|
+
options.clientSecret,
|
|
224
|
+
options.clientAuthMethod ?? "client_secret_basic",
|
|
225
|
+
{
|
|
226
|
+
grant_type: "client_credentials",
|
|
227
|
+
...options.scope ? { scope: options.scope } : {}
|
|
228
|
+
}
|
|
229
|
+
);
|
|
211
230
|
const body = await readTokenResponseBody(response);
|
|
212
231
|
if (!response.ok || !body || body.error || typeof body.access_token !== "string") {
|
|
213
232
|
throw new TokenRequestError({ status: response.status, error: body?.error, errorDescription: body?.error_description });
|
|
@@ -222,10 +241,16 @@ async function refreshTokens(options) {
|
|
|
222
241
|
let response;
|
|
223
242
|
try {
|
|
224
243
|
const discovery = await fetchDiscovery(options.issuer);
|
|
225
|
-
response = await postTokenRequest(
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
244
|
+
response = await postTokenRequest(
|
|
245
|
+
discovery.token_endpoint,
|
|
246
|
+
options.clientId,
|
|
247
|
+
options.clientSecret,
|
|
248
|
+
options.clientAuthMethod ?? "client_secret_basic",
|
|
249
|
+
{
|
|
250
|
+
grant_type: "refresh_token",
|
|
251
|
+
refresh_token: options.refreshToken
|
|
252
|
+
}
|
|
253
|
+
);
|
|
229
254
|
} catch (error) {
|
|
230
255
|
return { outcome: "transient", error };
|
|
231
256
|
}
|
|
@@ -338,7 +363,7 @@ async function verifyAccessToken(accessToken, options) {
|
|
|
338
363
|
errors: [{ code: "malformed-claims", message: "Access token has neither a sub (user) nor an azp (machine client id)" }]
|
|
339
364
|
};
|
|
340
365
|
}
|
|
341
|
-
return { success: true, data: { kind: "machine", clientId: payload.azp, claims } };
|
|
366
|
+
return { success: true, data: { kind: "machine", clientId: payload.azp, roles, claims } };
|
|
342
367
|
}
|
|
343
368
|
// Annotate the CommonJS export names for ESM import in node:
|
|
344
369
|
0 && (module.exports = {
|
package/dist/index.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createRemoteJWKSet } from 'jose';
|
|
2
|
-
export { A as AuthorizationCodeExchangeOptions, C as CLOCK_SKEW_TOLERANCE_SECONDS, a as ClientCredentialsGrantOptions, I as IdTokenClaims, R as RefreshTokensOptions, T as
|
|
2
|
+
export { A as AuthorizationCodeExchangeOptions, C as CLOCK_SKEW_TOLERANCE_SECONDS, a as ClientCredentialsGrantOptions, I as IdTokenClaims, R as RefreshTokensOptions, T as TokenEndpointAuthMethod, b as TokenRefreshOutcome, c as TokenRequestError, d as TokenSet, e as TokenVerificationError, f as TokenVerificationErrorCode, V as VerificationResult, g as VerifiedAccessToken, h as VerifyTokenOptions, i as clientCredentialsGrant, j as exchangeAuthorizationCode, r as refreshTokens, v as verifyAccessToken, k as verifyIdToken } from './verify-C1Ga6JbS.cjs';
|
|
3
3
|
import '@tdacorp/identity-authz';
|
|
4
4
|
|
|
5
5
|
/**
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createRemoteJWKSet } from 'jose';
|
|
2
|
-
export { A as AuthorizationCodeExchangeOptions, C as CLOCK_SKEW_TOLERANCE_SECONDS, a as ClientCredentialsGrantOptions, I as IdTokenClaims, R as RefreshTokensOptions, T as
|
|
2
|
+
export { A as AuthorizationCodeExchangeOptions, C as CLOCK_SKEW_TOLERANCE_SECONDS, a as ClientCredentialsGrantOptions, I as IdTokenClaims, R as RefreshTokensOptions, T as TokenEndpointAuthMethod, b as TokenRefreshOutcome, c as TokenRequestError, d as TokenSet, e as TokenVerificationError, f as TokenVerificationErrorCode, V as VerificationResult, g as VerifiedAccessToken, h as VerifyTokenOptions, i as clientCredentialsGrant, j as exchangeAuthorizationCode, r as refreshTokens, v as verifyAccessToken, k as verifyIdToken } from './verify-C1Ga6JbS.js';
|
|
3
3
|
import '@tdacorp/identity-authz';
|
|
4
4
|
|
|
5
5
|
/**
|
package/dist/index.js
CHANGED
package/dist/next.cjs
CHANGED
|
@@ -63,6 +63,7 @@ function validateDiscoveryDocument(document, issuerUrl) {
|
|
|
63
63
|
assertHttps(issuerUrl, "issuer");
|
|
64
64
|
assertHttps(document.jwks_uri, "jwks_uri");
|
|
65
65
|
assertHttps(document.token_endpoint, "token_endpoint");
|
|
66
|
+
assertHttps(document.authorization_endpoint, "authorization_endpoint");
|
|
66
67
|
if (document.issuer !== issuerUrl) {
|
|
67
68
|
throw new Error(`Discovery: document issuer "${document.issuer}" does not match the configured issuer "${issuerUrl}"`);
|
|
68
69
|
}
|
|
@@ -73,6 +74,9 @@ function validateDiscoveryDocument(document, issuerUrl) {
|
|
|
73
74
|
if (new URL(document.token_endpoint).origin !== issuerOrigin) {
|
|
74
75
|
throw new Error(`Discovery: token_endpoint "${document.token_endpoint}" is not same-origin as issuer "${issuerUrl}"`);
|
|
75
76
|
}
|
|
77
|
+
if (new URL(document.authorization_endpoint).origin !== issuerOrigin) {
|
|
78
|
+
throw new Error(`Discovery: authorization_endpoint "${document.authorization_endpoint}" is not same-origin as issuer "${issuerUrl}"`);
|
|
79
|
+
}
|
|
76
80
|
}
|
|
77
81
|
async function fetchDiscovery(issuerUrl) {
|
|
78
82
|
const cached = discoveryCache.get(issuerUrl);
|
|
@@ -163,13 +167,16 @@ function toTokenSet(body) {
|
|
|
163
167
|
scope: body.scope
|
|
164
168
|
};
|
|
165
169
|
}
|
|
166
|
-
async function postTokenRequest(tokenEndpoint, clientId, clientSecret, params) {
|
|
170
|
+
async function postTokenRequest(tokenEndpoint, clientId, clientSecret, clientAuthMethod, params) {
|
|
167
171
|
const body = new URLSearchParams(params);
|
|
168
172
|
const headers = {
|
|
169
173
|
"Content-Type": "application/x-www-form-urlencoded",
|
|
170
174
|
Accept: "application/json"
|
|
171
175
|
};
|
|
172
|
-
if (clientSecret) {
|
|
176
|
+
if (clientSecret && clientAuthMethod === "client_secret_post") {
|
|
177
|
+
body.set("client_id", clientId);
|
|
178
|
+
body.set("client_secret", clientSecret);
|
|
179
|
+
} else if (clientSecret) {
|
|
173
180
|
headers.Authorization = `Basic ${btoa(`${clientId}:${clientSecret}`)}`;
|
|
174
181
|
} else {
|
|
175
182
|
body.set("client_id", clientId);
|
|
@@ -181,12 +188,18 @@ async function readTokenResponseBody(response) {
|
|
|
181
188
|
}
|
|
182
189
|
async function exchangeAuthorizationCode(options) {
|
|
183
190
|
const discovery = await fetchDiscovery(options.issuer);
|
|
184
|
-
const response = await postTokenRequest(
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
191
|
+
const response = await postTokenRequest(
|
|
192
|
+
discovery.token_endpoint,
|
|
193
|
+
options.clientId,
|
|
194
|
+
options.clientSecret,
|
|
195
|
+
options.clientAuthMethod ?? "client_secret_basic",
|
|
196
|
+
{
|
|
197
|
+
grant_type: "authorization_code",
|
|
198
|
+
code: options.code,
|
|
199
|
+
redirect_uri: options.redirectUri,
|
|
200
|
+
code_verifier: options.codeVerifier
|
|
201
|
+
}
|
|
202
|
+
);
|
|
190
203
|
const body = await readTokenResponseBody(response);
|
|
191
204
|
if (!response.ok || !body || body.error || typeof body.access_token !== "string") {
|
|
192
205
|
throw new TokenRequestError({ status: response.status, error: body?.error, errorDescription: body?.error_description });
|
|
@@ -347,6 +360,17 @@ async function unseal(sealedValue, options) {
|
|
|
347
360
|
var TRANSACTION_TTL_SECONDS = 600;
|
|
348
361
|
var DEFAULT_TRANSACTION_COOKIE = "identity_oauth_txn";
|
|
349
362
|
var TRANSACTION_PURPOSE = "identity-client:oauth-transaction";
|
|
363
|
+
function sanitizeReturnTo(returnTo) {
|
|
364
|
+
if (!returnTo) return void 0;
|
|
365
|
+
if (/[\x00-\x1f]/.test(returnTo)) return void 0;
|
|
366
|
+
if (returnTo.startsWith("//")) return void 0;
|
|
367
|
+
if (returnTo.includes("\\")) return void 0;
|
|
368
|
+
const lower = returnTo.toLowerCase().trim();
|
|
369
|
+
if (lower.startsWith("javascript:") || lower.startsWith("data:") || lower.startsWith("vbscript:") || lower.startsWith("file:")) {
|
|
370
|
+
return void 0;
|
|
371
|
+
}
|
|
372
|
+
return returnTo.startsWith("/") ? returnTo : void 0;
|
|
373
|
+
}
|
|
350
374
|
function transactionCookieOptions(maxAge) {
|
|
351
375
|
return {
|
|
352
376
|
httpOnly: true,
|
|
@@ -361,7 +385,8 @@ function createLoginRoute(config) {
|
|
|
361
385
|
const state = generateState();
|
|
362
386
|
const codeVerifier = generateCodeVerifier();
|
|
363
387
|
const codeChallenge = await generateCodeChallenge(codeVerifier);
|
|
364
|
-
const
|
|
388
|
+
const rawReturnTo = config.getReturnTo?.(request) ?? request.nextUrl.searchParams.get("returnTo") ?? void 0;
|
|
389
|
+
const returnTo = sanitizeReturnTo(rawReturnTo);
|
|
365
390
|
const transaction = { state, codeVerifier, returnTo };
|
|
366
391
|
const sealedTransaction = await seal(transaction, {
|
|
367
392
|
secret: config.cookieSecret,
|
package/dist/next.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { NextResponse, NextRequest } from 'next/server';
|
|
2
|
-
import {
|
|
2
|
+
import { d as TokenSet, I as IdTokenClaims } from './verify-C1Ga6JbS.cjs';
|
|
3
3
|
import { SealSecret } from './sealed.cjs';
|
|
4
4
|
import '@tdacorp/identity-authz';
|
|
5
5
|
import 'jose';
|
|
@@ -22,7 +22,14 @@ interface CreateLoginRouteConfig {
|
|
|
22
22
|
cookieSecret: SealSecret;
|
|
23
23
|
transactionCookieName?: string;
|
|
24
24
|
/** Where to send the user after login completes. Defaults to reading a
|
|
25
|
-
* `returnTo` search param off the incoming request.
|
|
25
|
+
* `returnTo` search param off the incoming request. Validated to a
|
|
26
|
+
* same-app relative path (single leading `/`, no `//` prefix, no
|
|
27
|
+
* backslash, no C0 control character, no `javascript:`/`data:`/
|
|
28
|
+
* `vbscript:`/`file:` scheme) before being sealed into the transaction
|
|
29
|
+
* cookie -- see `sanitizeReturnTo`. An invalid value is silently coerced
|
|
30
|
+
* to `undefined` rather than rejected with an error, since this package
|
|
31
|
+
* has no per-app trusted-origin registry to validate an absolute URL
|
|
32
|
+
* against and a bad `returnTo` shouldn't break the login flow. */
|
|
26
33
|
getReturnTo?: (request: NextRequest) => string | undefined;
|
|
27
34
|
}
|
|
28
35
|
/** Builds a GET Route Handler that starts the OIDC login: generates PKCE and
|
|
@@ -67,7 +74,10 @@ interface CreateCallbackRouteConfig {
|
|
|
67
74
|
* Called once the code has been exchanged and the id_token verified. This
|
|
68
75
|
* package does not decide what happens next -- session creation, cookie
|
|
69
76
|
* choice, redirect target are the consuming app's own concerns -- so this
|
|
70
|
-
* callback's return value IS the route handler's response.
|
|
77
|
+
* callback's return value IS the route handler's response. `ctx.returnTo`,
|
|
78
|
+
* if present, already passed `sanitizeReturnTo`'s validation in
|
|
79
|
+
* `createLoginRoute`, so it is always a same-app relative path, never an
|
|
80
|
+
* absolute URL -- safe to pass straight into `new URL(returnTo, base)`.
|
|
71
81
|
*/
|
|
72
82
|
onSuccess: (tokens: TokenSet, claims: IdTokenClaims, ctx: {
|
|
73
83
|
returnTo?: string;
|
package/dist/next.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { NextResponse, NextRequest } from 'next/server';
|
|
2
|
-
import {
|
|
2
|
+
import { d as TokenSet, I as IdTokenClaims } from './verify-C1Ga6JbS.js';
|
|
3
3
|
import { SealSecret } from './sealed.js';
|
|
4
4
|
import '@tdacorp/identity-authz';
|
|
5
5
|
import 'jose';
|
|
@@ -22,7 +22,14 @@ interface CreateLoginRouteConfig {
|
|
|
22
22
|
cookieSecret: SealSecret;
|
|
23
23
|
transactionCookieName?: string;
|
|
24
24
|
/** Where to send the user after login completes. Defaults to reading a
|
|
25
|
-
* `returnTo` search param off the incoming request.
|
|
25
|
+
* `returnTo` search param off the incoming request. Validated to a
|
|
26
|
+
* same-app relative path (single leading `/`, no `//` prefix, no
|
|
27
|
+
* backslash, no C0 control character, no `javascript:`/`data:`/
|
|
28
|
+
* `vbscript:`/`file:` scheme) before being sealed into the transaction
|
|
29
|
+
* cookie -- see `sanitizeReturnTo`. An invalid value is silently coerced
|
|
30
|
+
* to `undefined` rather than rejected with an error, since this package
|
|
31
|
+
* has no per-app trusted-origin registry to validate an absolute URL
|
|
32
|
+
* against and a bad `returnTo` shouldn't break the login flow. */
|
|
26
33
|
getReturnTo?: (request: NextRequest) => string | undefined;
|
|
27
34
|
}
|
|
28
35
|
/** Builds a GET Route Handler that starts the OIDC login: generates PKCE and
|
|
@@ -67,7 +74,10 @@ interface CreateCallbackRouteConfig {
|
|
|
67
74
|
* Called once the code has been exchanged and the id_token verified. This
|
|
68
75
|
* package does not decide what happens next -- session creation, cookie
|
|
69
76
|
* choice, redirect target are the consuming app's own concerns -- so this
|
|
70
|
-
* callback's return value IS the route handler's response.
|
|
77
|
+
* callback's return value IS the route handler's response. `ctx.returnTo`,
|
|
78
|
+
* if present, already passed `sanitizeReturnTo`'s validation in
|
|
79
|
+
* `createLoginRoute`, so it is always a same-app relative path, never an
|
|
80
|
+
* absolute URL -- safe to pass straight into `new URL(returnTo, base)`.
|
|
71
81
|
*/
|
|
72
82
|
onSuccess: (tokens: TokenSet, claims: IdTokenClaims, ctx: {
|
|
73
83
|
returnTo?: string;
|
package/dist/next.js
CHANGED
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
generateCodeVerifier,
|
|
6
6
|
generateState,
|
|
7
7
|
verifyIdToken
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-S5WGUOJA.js";
|
|
9
9
|
import {
|
|
10
10
|
seal,
|
|
11
11
|
unseal
|
|
@@ -16,6 +16,17 @@ import { NextResponse } from "next/server";
|
|
|
16
16
|
var TRANSACTION_TTL_SECONDS = 600;
|
|
17
17
|
var DEFAULT_TRANSACTION_COOKIE = "identity_oauth_txn";
|
|
18
18
|
var TRANSACTION_PURPOSE = "identity-client:oauth-transaction";
|
|
19
|
+
function sanitizeReturnTo(returnTo) {
|
|
20
|
+
if (!returnTo) return void 0;
|
|
21
|
+
if (/[\x00-\x1f]/.test(returnTo)) return void 0;
|
|
22
|
+
if (returnTo.startsWith("//")) return void 0;
|
|
23
|
+
if (returnTo.includes("\\")) return void 0;
|
|
24
|
+
const lower = returnTo.toLowerCase().trim();
|
|
25
|
+
if (lower.startsWith("javascript:") || lower.startsWith("data:") || lower.startsWith("vbscript:") || lower.startsWith("file:")) {
|
|
26
|
+
return void 0;
|
|
27
|
+
}
|
|
28
|
+
return returnTo.startsWith("/") ? returnTo : void 0;
|
|
29
|
+
}
|
|
19
30
|
function transactionCookieOptions(maxAge) {
|
|
20
31
|
return {
|
|
21
32
|
httpOnly: true,
|
|
@@ -30,7 +41,8 @@ function createLoginRoute(config) {
|
|
|
30
41
|
const state = generateState();
|
|
31
42
|
const codeVerifier = generateCodeVerifier();
|
|
32
43
|
const codeChallenge = await generateCodeChallenge(codeVerifier);
|
|
33
|
-
const
|
|
44
|
+
const rawReturnTo = config.getReturnTo?.(request) ?? request.nextUrl.searchParams.get("returnTo") ?? void 0;
|
|
45
|
+
const returnTo = sanitizeReturnTo(rawReturnTo);
|
|
34
46
|
const transaction = { state, codeVerifier, returnTo };
|
|
35
47
|
const sealedTransaction = await seal(transaction, {
|
|
36
48
|
secret: config.cookieSecret,
|
|
@@ -31,6 +31,10 @@ declare class TokenRequestError extends Error {
|
|
|
31
31
|
errorDescription?: string;
|
|
32
32
|
});
|
|
33
33
|
}
|
|
34
|
+
/** The token endpoint client-authentication methods this package supports --
|
|
35
|
+
* see RFC 6749 §2.3.1 and OIDC Discovery 1.0's
|
|
36
|
+
* `token_endpoint_auth_methods_supported`. */
|
|
37
|
+
type TokenEndpointAuthMethod = 'client_secret_basic' | 'client_secret_post';
|
|
34
38
|
/** Options for `exchangeAuthorizationCode`. */
|
|
35
39
|
interface AuthorizationCodeExchangeOptions {
|
|
36
40
|
/** The identity server's issuer URL, used to look up `token_endpoint`. */
|
|
@@ -46,6 +50,16 @@ interface AuthorizationCodeExchangeOptions {
|
|
|
46
50
|
/** The PKCE verifier generated (and kept) alongside the code challenge
|
|
47
51
|
* sent on the authorization request; see `generateCodeVerifier`. */
|
|
48
52
|
codeVerifier: string;
|
|
53
|
+
/** How to send `clientSecret` to the token endpoint. Defaults to
|
|
54
|
+
* `'client_secret_basic'`, which is this package's original and only
|
|
55
|
+
* behavior -- omitting this field changes nothing for an existing caller.
|
|
56
|
+
* Set it to `'client_secret_post'` if the relying party is registered on
|
|
57
|
+
* the identity platform for that method instead: sending Basic auth to a
|
|
58
|
+
* `client_secret_post`-only client gets `invalid_client` back, since the
|
|
59
|
+
* token endpoint never receives credentials in the form it expects. An
|
|
60
|
+
* issuer's OIDC discovery document's `token_endpoint_auth_methods_supported`
|
|
61
|
+
* says which methods it actually accepts. */
|
|
62
|
+
clientAuthMethod?: TokenEndpointAuthMethod;
|
|
49
63
|
}
|
|
50
64
|
/** RFC 6749 §4.1.3 / RFC 7636 §4.5: exchanges an authorization code for a
|
|
51
65
|
* token set. Throws `TokenRequestError` on any non-success response. */
|
|
@@ -60,6 +74,16 @@ interface ClientCredentialsGrantOptions {
|
|
|
60
74
|
clientSecret: string;
|
|
61
75
|
/** Space-separated scopes to request for the resulting token. */
|
|
62
76
|
scope?: string;
|
|
77
|
+
/** How to send `clientSecret` to the token endpoint. Defaults to
|
|
78
|
+
* `'client_secret_basic'`, which is this package's original and only
|
|
79
|
+
* behavior -- omitting this field changes nothing for an existing caller.
|
|
80
|
+
* Set it to `'client_secret_post'` if the relying party is registered on
|
|
81
|
+
* the identity platform for that method instead: sending Basic auth to a
|
|
82
|
+
* `client_secret_post`-only client gets `invalid_client` back, since the
|
|
83
|
+
* token endpoint never receives credentials in the form it expects. An
|
|
84
|
+
* issuer's OIDC discovery document's `token_endpoint_auth_methods_supported`
|
|
85
|
+
* says which methods it actually accepts. */
|
|
86
|
+
clientAuthMethod?: TokenEndpointAuthMethod;
|
|
63
87
|
}
|
|
64
88
|
/** RFC 6749 §4.4: mints a token with no end user, for service-to-service
|
|
65
89
|
* calls. Always confidential -- `clientSecret` is required, not optional,
|
|
@@ -75,6 +99,16 @@ interface RefreshTokensOptions {
|
|
|
75
99
|
clientSecret?: string;
|
|
76
100
|
/** The refresh token to redeem. */
|
|
77
101
|
refreshToken: string;
|
|
102
|
+
/** How to send `clientSecret` to the token endpoint. Defaults to
|
|
103
|
+
* `'client_secret_basic'`, which is this package's original and only
|
|
104
|
+
* behavior -- omitting this field changes nothing for an existing caller.
|
|
105
|
+
* Set it to `'client_secret_post'` if the relying party is registered on
|
|
106
|
+
* the identity platform for that method instead: sending Basic auth to a
|
|
107
|
+
* `client_secret_post`-only client gets `invalid_client` back, since the
|
|
108
|
+
* token endpoint never receives credentials in the form it expects. An
|
|
109
|
+
* issuer's OIDC discovery document's `token_endpoint_auth_methods_supported`
|
|
110
|
+
* says which methods it actually accepts. */
|
|
111
|
+
clientAuthMethod?: TokenEndpointAuthMethod;
|
|
78
112
|
}
|
|
79
113
|
type TokenRefreshOutcome = {
|
|
80
114
|
outcome: 'success';
|
|
@@ -144,12 +178,14 @@ declare function refreshTokens(options: RefreshTokensOptions): Promise<TokenRefr
|
|
|
144
178
|
* read as "no roles" or a machine's `clientId` misread as a user's `sub`.
|
|
145
179
|
*
|
|
146
180
|
* The concrete reason this matters here: this identity server's
|
|
147
|
-
* `client_credentials` grant mints access tokens with no end user
|
|
148
|
-
* `
|
|
149
|
-
*
|
|
150
|
-
*
|
|
151
|
-
*
|
|
152
|
-
*
|
|
181
|
+
* `client_credentials` grant mints access tokens with no end user -- there
|
|
182
|
+
* is no `sub` for this variant, ever. It CAN still carry a `roles` claim,
|
|
183
|
+
* resolved against the requesting client itself as the subject, whenever the
|
|
184
|
+
* client_credentials request's scope included `roles`; unlike `sub`, `roles`
|
|
185
|
+
* is not tied to there being an end user. Before this type existed,
|
|
186
|
+
* `verifyAccessToken` returned one flat claims object for both cases, so a
|
|
187
|
+
* caller checking `.sub` on a machine token silently got `undefined` instead
|
|
188
|
+
* of a type error telling it this branch has no subject.
|
|
153
189
|
*/
|
|
154
190
|
type VerifiedAccessToken = {
|
|
155
191
|
kind: 'user';
|
|
@@ -162,6 +198,11 @@ type VerifiedAccessToken = {
|
|
|
162
198
|
} | {
|
|
163
199
|
kind: 'machine';
|
|
164
200
|
clientId: string;
|
|
201
|
+
/** Present only when the client_credentials request's scope included
|
|
202
|
+
* `roles` -- the server resolves the claim against the requesting
|
|
203
|
+
* client itself as the subject in that case. Pass to `permits()` from
|
|
204
|
+
* `@tdacorp/identity-authz` to decide a permission locally. */
|
|
205
|
+
roles?: RolesClaim;
|
|
165
206
|
claims: Record<string, unknown>;
|
|
166
207
|
};
|
|
167
208
|
|
|
@@ -303,4 +344,4 @@ declare function verifyIdToken(idToken: string, options: VerifyTokenOptions): Pr
|
|
|
303
344
|
* contract as `verifyIdToken`. */
|
|
304
345
|
declare function verifyAccessToken(accessToken: string, options: VerifyTokenOptions): Promise<VerificationResult<VerifiedAccessToken>>;
|
|
305
346
|
|
|
306
|
-
export { type AuthorizationCodeExchangeOptions as A, CLOCK_SKEW_TOLERANCE_SECONDS as C, type IdTokenClaims as I, type RefreshTokensOptions as R, type
|
|
347
|
+
export { type AuthorizationCodeExchangeOptions as A, CLOCK_SKEW_TOLERANCE_SECONDS as C, type IdTokenClaims as I, type RefreshTokensOptions as R, type TokenEndpointAuthMethod as T, type VerificationResult as V, type ClientCredentialsGrantOptions as a, type TokenRefreshOutcome as b, TokenRequestError as c, type TokenSet as d, type TokenVerificationError as e, type TokenVerificationErrorCode as f, type VerifiedAccessToken as g, type VerifyTokenOptions as h, clientCredentialsGrant as i, exchangeAuthorizationCode as j, verifyIdToken as k, refreshTokens as r, verifyAccessToken as v };
|
|
@@ -31,6 +31,10 @@ declare class TokenRequestError extends Error {
|
|
|
31
31
|
errorDescription?: string;
|
|
32
32
|
});
|
|
33
33
|
}
|
|
34
|
+
/** The token endpoint client-authentication methods this package supports --
|
|
35
|
+
* see RFC 6749 §2.3.1 and OIDC Discovery 1.0's
|
|
36
|
+
* `token_endpoint_auth_methods_supported`. */
|
|
37
|
+
type TokenEndpointAuthMethod = 'client_secret_basic' | 'client_secret_post';
|
|
34
38
|
/** Options for `exchangeAuthorizationCode`. */
|
|
35
39
|
interface AuthorizationCodeExchangeOptions {
|
|
36
40
|
/** The identity server's issuer URL, used to look up `token_endpoint`. */
|
|
@@ -46,6 +50,16 @@ interface AuthorizationCodeExchangeOptions {
|
|
|
46
50
|
/** The PKCE verifier generated (and kept) alongside the code challenge
|
|
47
51
|
* sent on the authorization request; see `generateCodeVerifier`. */
|
|
48
52
|
codeVerifier: string;
|
|
53
|
+
/** How to send `clientSecret` to the token endpoint. Defaults to
|
|
54
|
+
* `'client_secret_basic'`, which is this package's original and only
|
|
55
|
+
* behavior -- omitting this field changes nothing for an existing caller.
|
|
56
|
+
* Set it to `'client_secret_post'` if the relying party is registered on
|
|
57
|
+
* the identity platform for that method instead: sending Basic auth to a
|
|
58
|
+
* `client_secret_post`-only client gets `invalid_client` back, since the
|
|
59
|
+
* token endpoint never receives credentials in the form it expects. An
|
|
60
|
+
* issuer's OIDC discovery document's `token_endpoint_auth_methods_supported`
|
|
61
|
+
* says which methods it actually accepts. */
|
|
62
|
+
clientAuthMethod?: TokenEndpointAuthMethod;
|
|
49
63
|
}
|
|
50
64
|
/** RFC 6749 §4.1.3 / RFC 7636 §4.5: exchanges an authorization code for a
|
|
51
65
|
* token set. Throws `TokenRequestError` on any non-success response. */
|
|
@@ -60,6 +74,16 @@ interface ClientCredentialsGrantOptions {
|
|
|
60
74
|
clientSecret: string;
|
|
61
75
|
/** Space-separated scopes to request for the resulting token. */
|
|
62
76
|
scope?: string;
|
|
77
|
+
/** How to send `clientSecret` to the token endpoint. Defaults to
|
|
78
|
+
* `'client_secret_basic'`, which is this package's original and only
|
|
79
|
+
* behavior -- omitting this field changes nothing for an existing caller.
|
|
80
|
+
* Set it to `'client_secret_post'` if the relying party is registered on
|
|
81
|
+
* the identity platform for that method instead: sending Basic auth to a
|
|
82
|
+
* `client_secret_post`-only client gets `invalid_client` back, since the
|
|
83
|
+
* token endpoint never receives credentials in the form it expects. An
|
|
84
|
+
* issuer's OIDC discovery document's `token_endpoint_auth_methods_supported`
|
|
85
|
+
* says which methods it actually accepts. */
|
|
86
|
+
clientAuthMethod?: TokenEndpointAuthMethod;
|
|
63
87
|
}
|
|
64
88
|
/** RFC 6749 §4.4: mints a token with no end user, for service-to-service
|
|
65
89
|
* calls. Always confidential -- `clientSecret` is required, not optional,
|
|
@@ -75,6 +99,16 @@ interface RefreshTokensOptions {
|
|
|
75
99
|
clientSecret?: string;
|
|
76
100
|
/** The refresh token to redeem. */
|
|
77
101
|
refreshToken: string;
|
|
102
|
+
/** How to send `clientSecret` to the token endpoint. Defaults to
|
|
103
|
+
* `'client_secret_basic'`, which is this package's original and only
|
|
104
|
+
* behavior -- omitting this field changes nothing for an existing caller.
|
|
105
|
+
* Set it to `'client_secret_post'` if the relying party is registered on
|
|
106
|
+
* the identity platform for that method instead: sending Basic auth to a
|
|
107
|
+
* `client_secret_post`-only client gets `invalid_client` back, since the
|
|
108
|
+
* token endpoint never receives credentials in the form it expects. An
|
|
109
|
+
* issuer's OIDC discovery document's `token_endpoint_auth_methods_supported`
|
|
110
|
+
* says which methods it actually accepts. */
|
|
111
|
+
clientAuthMethod?: TokenEndpointAuthMethod;
|
|
78
112
|
}
|
|
79
113
|
type TokenRefreshOutcome = {
|
|
80
114
|
outcome: 'success';
|
|
@@ -144,12 +178,14 @@ declare function refreshTokens(options: RefreshTokensOptions): Promise<TokenRefr
|
|
|
144
178
|
* read as "no roles" or a machine's `clientId` misread as a user's `sub`.
|
|
145
179
|
*
|
|
146
180
|
* The concrete reason this matters here: this identity server's
|
|
147
|
-
* `client_credentials` grant mints access tokens with no end user
|
|
148
|
-
* `
|
|
149
|
-
*
|
|
150
|
-
*
|
|
151
|
-
*
|
|
152
|
-
*
|
|
181
|
+
* `client_credentials` grant mints access tokens with no end user -- there
|
|
182
|
+
* is no `sub` for this variant, ever. It CAN still carry a `roles` claim,
|
|
183
|
+
* resolved against the requesting client itself as the subject, whenever the
|
|
184
|
+
* client_credentials request's scope included `roles`; unlike `sub`, `roles`
|
|
185
|
+
* is not tied to there being an end user. Before this type existed,
|
|
186
|
+
* `verifyAccessToken` returned one flat claims object for both cases, so a
|
|
187
|
+
* caller checking `.sub` on a machine token silently got `undefined` instead
|
|
188
|
+
* of a type error telling it this branch has no subject.
|
|
153
189
|
*/
|
|
154
190
|
type VerifiedAccessToken = {
|
|
155
191
|
kind: 'user';
|
|
@@ -162,6 +198,11 @@ type VerifiedAccessToken = {
|
|
|
162
198
|
} | {
|
|
163
199
|
kind: 'machine';
|
|
164
200
|
clientId: string;
|
|
201
|
+
/** Present only when the client_credentials request's scope included
|
|
202
|
+
* `roles` -- the server resolves the claim against the requesting
|
|
203
|
+
* client itself as the subject in that case. Pass to `permits()` from
|
|
204
|
+
* `@tdacorp/identity-authz` to decide a permission locally. */
|
|
205
|
+
roles?: RolesClaim;
|
|
165
206
|
claims: Record<string, unknown>;
|
|
166
207
|
};
|
|
167
208
|
|
|
@@ -303,4 +344,4 @@ declare function verifyIdToken(idToken: string, options: VerifyTokenOptions): Pr
|
|
|
303
344
|
* contract as `verifyIdToken`. */
|
|
304
345
|
declare function verifyAccessToken(accessToken: string, options: VerifyTokenOptions): Promise<VerificationResult<VerifiedAccessToken>>;
|
|
305
346
|
|
|
306
|
-
export { type AuthorizationCodeExchangeOptions as A, CLOCK_SKEW_TOLERANCE_SECONDS as C, type IdTokenClaims as I, type RefreshTokensOptions as R, type
|
|
347
|
+
export { type AuthorizationCodeExchangeOptions as A, CLOCK_SKEW_TOLERANCE_SECONDS as C, type IdTokenClaims as I, type RefreshTokensOptions as R, type TokenEndpointAuthMethod as T, type VerificationResult as V, type ClientCredentialsGrantOptions as a, type TokenRefreshOutcome as b, TokenRequestError as c, type TokenSet as d, type TokenVerificationError as e, type TokenVerificationErrorCode as f, type VerifiedAccessToken as g, type VerifyTokenOptions as h, clientCredentialsGrant as i, exchangeAuthorizationCode as j, verifyIdToken as k, refreshTokens as r, verifyAccessToken as v };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tdacorp/identity-client",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "OIDC relying-party client for TDACorp Identity: discovery, PKCE, token exchange, token verification and Next.js route helpers.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"bugs": {
|
|
@@ -50,17 +50,12 @@
|
|
|
50
50
|
"publishConfig": {
|
|
51
51
|
"access": "public"
|
|
52
52
|
},
|
|
53
|
-
"scripts": {
|
|
54
|
-
"build": "tsup",
|
|
55
|
-
"typecheck": "tsc --noEmit",
|
|
56
|
-
"test": "vitest run"
|
|
57
|
-
},
|
|
58
53
|
"files": [
|
|
59
54
|
"dist"
|
|
60
55
|
],
|
|
61
56
|
"dependencies": {
|
|
62
|
-
"
|
|
63
|
-
"
|
|
57
|
+
"jose": "^6.2.9",
|
|
58
|
+
"@tdacorp/identity-authz": "^0.2.0"
|
|
64
59
|
},
|
|
65
60
|
"peerDependencies": {
|
|
66
61
|
"next": ">=14"
|
|
@@ -71,10 +66,15 @@
|
|
|
71
66
|
}
|
|
72
67
|
},
|
|
73
68
|
"devDependencies": {
|
|
74
|
-
"@tdacorp/typescript-config": "workspace:*",
|
|
75
69
|
"next": "^16.3.2",
|
|
76
70
|
"tsup": "^8.5.1",
|
|
77
71
|
"typescript": "^5.7.3",
|
|
78
|
-
"vitest": "^4.1.11"
|
|
72
|
+
"vitest": "^4.1.11",
|
|
73
|
+
"@tdacorp/typescript-config": "0.0.0"
|
|
74
|
+
},
|
|
75
|
+
"scripts": {
|
|
76
|
+
"build": "tsup",
|
|
77
|
+
"typecheck": "tsc --noEmit",
|
|
78
|
+
"test": "vitest run"
|
|
79
79
|
}
|
|
80
|
-
}
|
|
80
|
+
}
|