@tdacorp/identity-client 0.2.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 +27 -9
- package/dist/{chunk-6WGVTO7C.js → chunk-S5WGUOJA.js} +5 -1
- package/dist/index.cjs +5 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/next.cjs +17 -1
- package/dist/next.d.cts +13 -3
- package/dist/next.d.ts +13 -3
- package/dist/next.js +14 -2
- package/dist/{verify-DTUDjR4v.d.cts → verify-C1Ga6JbS.d.cts} +13 -6
- package/dist/{verify-DTUDjR4v.d.ts → verify-C1Ga6JbS.d.ts} +13 -6
- package/package.json +1 -1
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.
|
|
@@ -199,11 +215,13 @@ if (outcome.outcome === 'success') {
|
|
|
199
215
|
Machine-to-machine calls with no end user use `clientCredentialsGrant` (RFC
|
|
200
216
|
6749 §4.4) instead, always with a confidential `clientSecret` since there is
|
|
201
217
|
no PKCE verifier to authenticate a public client with. A token minted this
|
|
202
|
-
way carries no `sub`
|
|
203
|
-
`
|
|
204
|
-
`
|
|
205
|
-
|
|
206
|
-
|
|
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.
|
|
207
225
|
|
|
208
226
|
## Verification
|
|
209
227
|
|
|
@@ -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);
|
|
@@ -322,7 +326,7 @@ async function verifyAccessToken(accessToken, options) {
|
|
|
322
326
|
errors: [{ code: "malformed-claims", message: "Access token has neither a sub (user) nor an azp (machine client id)" }]
|
|
323
327
|
};
|
|
324
328
|
}
|
|
325
|
-
return { success: true, data: { kind: "machine", clientId: payload.azp, claims } };
|
|
329
|
+
return { success: true, data: { kind: "machine", clientId: payload.azp, roles, claims } };
|
|
326
330
|
}
|
|
327
331
|
|
|
328
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);
|
|
@@ -359,7 +363,7 @@ async function verifyAccessToken(accessToken, options) {
|
|
|
359
363
|
errors: [{ code: "malformed-claims", message: "Access token has neither a sub (user) nor an azp (machine client id)" }]
|
|
360
364
|
};
|
|
361
365
|
}
|
|
362
|
-
return { success: true, data: { kind: "machine", clientId: payload.azp, claims } };
|
|
366
|
+
return { success: true, data: { kind: "machine", clientId: payload.azp, roles, claims } };
|
|
363
367
|
}
|
|
364
368
|
// Annotate the CommonJS export names for ESM import in node:
|
|
365
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 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-
|
|
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 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-
|
|
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);
|
|
@@ -356,6 +360,17 @@ async function unseal(sealedValue, options) {
|
|
|
356
360
|
var TRANSACTION_TTL_SECONDS = 600;
|
|
357
361
|
var DEFAULT_TRANSACTION_COOKIE = "identity_oauth_txn";
|
|
358
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
|
+
}
|
|
359
374
|
function transactionCookieOptions(maxAge) {
|
|
360
375
|
return {
|
|
361
376
|
httpOnly: true,
|
|
@@ -370,7 +385,8 @@ function createLoginRoute(config) {
|
|
|
370
385
|
const state = generateState();
|
|
371
386
|
const codeVerifier = generateCodeVerifier();
|
|
372
387
|
const codeChallenge = await generateCodeChallenge(codeVerifier);
|
|
373
|
-
const
|
|
388
|
+
const rawReturnTo = config.getReturnTo?.(request) ?? request.nextUrl.searchParams.get("returnTo") ?? void 0;
|
|
389
|
+
const returnTo = sanitizeReturnTo(rawReturnTo);
|
|
374
390
|
const transaction = { state, codeVerifier, returnTo };
|
|
375
391
|
const sealedTransaction = await seal(transaction, {
|
|
376
392
|
secret: config.cookieSecret,
|
package/dist/next.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { NextResponse, NextRequest } from 'next/server';
|
|
2
|
-
import { d as TokenSet, I as IdTokenClaims } from './verify-
|
|
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 { d as TokenSet, I as IdTokenClaims } from './verify-
|
|
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,
|
|
@@ -178,12 +178,14 @@ declare function refreshTokens(options: RefreshTokensOptions): Promise<TokenRefr
|
|
|
178
178
|
* read as "no roles" or a machine's `clientId` misread as a user's `sub`.
|
|
179
179
|
*
|
|
180
180
|
* The concrete reason this matters here: this identity server's
|
|
181
|
-
* `client_credentials` grant mints access tokens with no end user
|
|
182
|
-
* `
|
|
183
|
-
*
|
|
184
|
-
*
|
|
185
|
-
*
|
|
186
|
-
*
|
|
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.
|
|
187
189
|
*/
|
|
188
190
|
type VerifiedAccessToken = {
|
|
189
191
|
kind: 'user';
|
|
@@ -196,6 +198,11 @@ type VerifiedAccessToken = {
|
|
|
196
198
|
} | {
|
|
197
199
|
kind: 'machine';
|
|
198
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;
|
|
199
206
|
claims: Record<string, unknown>;
|
|
200
207
|
};
|
|
201
208
|
|
|
@@ -178,12 +178,14 @@ declare function refreshTokens(options: RefreshTokensOptions): Promise<TokenRefr
|
|
|
178
178
|
* read as "no roles" or a machine's `clientId` misread as a user's `sub`.
|
|
179
179
|
*
|
|
180
180
|
* The concrete reason this matters here: this identity server's
|
|
181
|
-
* `client_credentials` grant mints access tokens with no end user
|
|
182
|
-
* `
|
|
183
|
-
*
|
|
184
|
-
*
|
|
185
|
-
*
|
|
186
|
-
*
|
|
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.
|
|
187
189
|
*/
|
|
188
190
|
type VerifiedAccessToken = {
|
|
189
191
|
kind: 'user';
|
|
@@ -196,6 +198,11 @@ type VerifiedAccessToken = {
|
|
|
196
198
|
} | {
|
|
197
199
|
kind: 'machine';
|
|
198
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;
|
|
199
206
|
claims: Record<string, unknown>;
|
|
200
207
|
};
|
|
201
208
|
|
package/package.json
CHANGED