@vantageos/cloud-identity 0.7.0 → 0.8.0
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/CHANGELOG.md +51 -0
- package/README.md +100 -4
- package/dist/authorize-flow.d.ts +169 -0
- package/dist/authorize-flow.d.ts.map +1 -0
- package/dist/authorize-flow.js +262 -0
- package/dist/authorize-flow.js.map +1 -0
- package/dist/authorize-shared.d.ts +44 -0
- package/dist/authorize-shared.d.ts.map +1 -0
- package/dist/authorize-shared.js +134 -0
- package/dist/authorize-shared.js.map +1 -0
- package/dist/clerk-session.d.ts +52 -0
- package/dist/clerk-session.d.ts.map +1 -0
- package/dist/clerk-session.js +93 -0
- package/dist/clerk-session.js.map +1 -0
- package/dist/human-path.d.ts +1 -1
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -1
- package/dist/oidc.d.ts +76 -0
- package/dist/oidc.d.ts.map +1 -0
- package/dist/oidc.js +83 -0
- package/dist/oidc.js.map +1 -0
- package/dist/token-exchange.d.ts +53 -0
- package/dist/token-exchange.d.ts.map +1 -0
- package/dist/token-exchange.js +60 -0
- package/dist/token-exchange.js.map +1 -0
- package/package.json +17 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,57 @@
|
|
|
3
3
|
All notable changes to `@vantageos/cloud-identity` are documented here.
|
|
4
4
|
This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
5
5
|
|
|
6
|
+
## [Unreleased]
|
|
7
|
+
|
|
8
|
+
**Additive — authorize a PERSON and bind an organisation (proposed 0.8.0).**
|
|
9
|
+
A connector's `/authorize` that auto-approves with no user authentication, and
|
|
10
|
+
derives the code's user from a client-registration profile, makes every client
|
|
11
|
+
an anonymous app. This release is the flow that replaces it: Clerk session,
|
|
12
|
+
verified organisation membership, a code bound to both, and an exchange that
|
|
13
|
+
re-derives nothing from the client's profile.
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
|
|
17
|
+
- `startAuthorize` / `resumeAuthorize` (`src/authorize-flow.ts`): request
|
|
18
|
+
validation (exact `redirect_uri`, PKCE challenge required, allowed `resource`,
|
|
19
|
+
supported scopes); no Clerk session -> redirect to sign-in with an
|
|
20
|
+
HMAC-signed, short-lived state blob and no code; verified session -> one
|
|
21
|
+
organisation auto-picked, several -> an `OrgPickerModel`, a posted `orgId`
|
|
22
|
+
honoured only if it is in the user's own membership list; `requireConsent`
|
|
23
|
+
(default ON; opt out explicitly for first-party clients only). The code is single-use, short-lived and stored as a digest
|
|
24
|
+
through a consumer-supplied `AuthorizationCodeStore` whose `consume` must be
|
|
25
|
+
atomic.
|
|
26
|
+
- `exchangeAuthorizationCode` (`src/token-exchange.ts`): consumes the code
|
|
27
|
+
first, then checks expiry, client, `redirect_uri`, `resource` and the PKCE
|
|
28
|
+
verifier; returns `{ sub, org_id, org_slug, org_role, aud, client_id, scope }`.
|
|
29
|
+
- `verifyClerkSessionToken` (`src/clerk-session.ts`): RS256 against a
|
|
30
|
+
consumer-supplied key set; issuer, expiry, `nbf`, optional audience and
|
|
31
|
+
authorized parties.
|
|
32
|
+
- `buildDiscoveryDocument`, `buildUserInfo` (`src/oidc.ts`). The document
|
|
33
|
+
advertises only what is implemented (no id_token alg, no refresh grant, no
|
|
34
|
+
client-auth methods).
|
|
35
|
+
- `AuthorizeRefusal` / `AuthorizeRefusalReason` (`{ code: "AUTHORIZE_REFUSED",
|
|
36
|
+
reason }`), `oauthErrorFor`, `pkceChallengeFromVerifier`, and the types listed
|
|
37
|
+
in the README section "Authorize a person and bind an organisation".
|
|
38
|
+
- Subpath exports `/authorize`, `/token-exchange`, `/clerk-session`, `/oidc`.
|
|
39
|
+
|
|
40
|
+
### Security
|
|
41
|
+
|
|
42
|
+
- Consent is bound to a server-issued `consentToken` (HMAC over state, user and
|
|
43
|
+
organisation set). `approved: true` without a token valid for that state and
|
|
44
|
+
that session's user is refused with `consent-required`; a client can no
|
|
45
|
+
longer mint its own state and approve on a victim's behalf.
|
|
46
|
+
|
|
47
|
+
### Changed
|
|
48
|
+
|
|
49
|
+
- README: the "does not verify a signed JWT" limit now names the one exception.
|
|
50
|
+
- `package.json` version set to 0.8.0 (proposed; not published).
|
|
51
|
+
|
|
52
|
+
### Unchanged
|
|
53
|
+
|
|
54
|
+
- Every existing export keeps its behaviour and signature; no existing test
|
|
55
|
+
was edited. No new runtime dependency (Web Crypto only).
|
|
56
|
+
|
|
6
57
|
## [0.7.0]
|
|
7
58
|
|
|
8
59
|
**Additive — resolution of a PRESENTED, non-master bearer.** The published
|
package/README.md
CHANGED
|
@@ -275,6 +275,101 @@ want to catch by type, and `BearerPayload`, the type returned by
|
|
|
275
275
|
// -> { tenant: "org_abc", subject: "user_123", role: "admin" }
|
|
276
276
|
```
|
|
277
277
|
|
|
278
|
+
## Authorize a person and bind an organisation
|
|
279
|
+
|
|
280
|
+
A connector that signs in a PERSON (a Claude or ChatGPT user) must end up with
|
|
281
|
+
a token naming that person and one organisation they belong to, never an
|
|
282
|
+
anonymous app. Clerk is the identity provider; this package holds the
|
|
283
|
+
decisions and takes every collaborator as a parameter.
|
|
284
|
+
|
|
285
|
+
```ts
|
|
286
|
+
// GET /authorize
|
|
287
|
+
const out = await startAuthorize(query, clerkSessionToken, cfg, deps);
|
|
288
|
+
// GET /authorize/callback (the return from Clerk) and the picker's POST
|
|
289
|
+
const out2 = await resumeAuthorize({ state, sessionToken, orgId, approved, consentToken }, cfg, deps);
|
|
290
|
+
// POST /token (authorization_code grant)
|
|
291
|
+
const res = await exchangeAuthorizationCode(
|
|
292
|
+
{ code, codeVerifier, redirectUri, clientId, resource },
|
|
293
|
+
{ codeStore },
|
|
294
|
+
);
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
- `startAuthorize(params, sessionToken, config, deps)` validates the request
|
|
298
|
+
(registered client, exact `redirect_uri`, a PKCE challenge using the SHA-256
|
|
299
|
+
method, an allowed RFC 8707 `resource`, supported scopes). With no valid Clerk
|
|
300
|
+
session it returns `{ kind: "redirect-to-sign-in", url }` and issues nothing;
|
|
301
|
+
the return URL carries a state blob signed with HMAC (`AuthorizeConfig.stateSecret`,
|
|
302
|
+
at least 32 characters) and valid for 600 seconds by default.
|
|
303
|
+
- `resumeAuthorize(input, config, deps)` rebuilds the request ONLY from that
|
|
304
|
+
signed state (a tampered or expired blob is refused), verifies the Clerk
|
|
305
|
+
session token (`verifyClerkSessionToken`: RS256 against the key set the
|
|
306
|
+
consumer supplies, issuer, expiry, optional audience and authorized parties),
|
|
307
|
+
lists the user's organisations through `deps.listMemberships`, and answers
|
|
308
|
+
with an `AuthorizeOutcome`:
|
|
309
|
+
`redirect-to-sign-in`, `org-picker` (an `OrgPickerModel` the consumer renders;
|
|
310
|
+
it lists only the user's own organisations), `redirect-to-client` (the URL
|
|
311
|
+
carrying the code and the client's `state`) or `refused`.
|
|
312
|
+
A posted `orgId` is honoured only if the verified user's own membership list
|
|
313
|
+
contains it.
|
|
314
|
+
Consent is ON by default (`requireConsent` defaults to true). What it
|
|
315
|
+
protects: a code is issued only after the person was SHOWN the picker and
|
|
316
|
+
approved it. The picker's `OrgPickerModel` carries a `consentToken`:
|
|
317
|
+
`HMAC(stateSecret, "consent-v1" NUL state NUL clerkUserId NUL sorted
|
|
318
|
+
organisation ids)`. It is bound to the exact signed state (useless for any
|
|
319
|
+
other request), to the verified user (useless under another session) and to
|
|
320
|
+
the organisation set that user was shown. `resumeAuthorize` with
|
|
321
|
+
`approved: true` refuses with `consent-required` unless the presented
|
|
322
|
+
`consentToken` verifies, compared in constant time, for that state AND the
|
|
323
|
+
session's user. Without this, a client could mint its own state (the sign-in
|
|
324
|
+
redirect hands it one), replay it with a victim's session and
|
|
325
|
+
`approved: true`, and receive a code the victim never saw a screen for.
|
|
326
|
+
The consumer renders `consentToken` as a hidden field of the picker's POST
|
|
327
|
+
form and posts it back as `consentToken`. The token IS the CSRF protection
|
|
328
|
+
for that POST, because a cross-site form cannot know it; do not add a weaker
|
|
329
|
+
substitute and do not accept the POST without it. Also set
|
|
330
|
+
`session.authorizedParties` to your own origin(s) so a Clerk session token
|
|
331
|
+
minted for another application (`azp`) is not accepted here.
|
|
332
|
+
Set `requireConsent: false` explicitly ONLY for first-party clients whose
|
|
333
|
+
redirect URIs you control; exactly one organisation is then auto-picked and
|
|
334
|
+
no token is involved.
|
|
335
|
+
- The code is bound to `{ clerkUserId, orgId, orgSlug, orgRole, clientId,
|
|
336
|
+
redirectUri, codeChallenge, resource }`, stored as a digest through the
|
|
337
|
+
consumer's `AuthorizationCodeStore` (`put`, and an ATOMIC `consume`), single
|
|
338
|
+
use, 60 seconds by default. Records are `AuthorizationCodeRecord`; a consume
|
|
339
|
+
answers `ConsumeCodeResult`. `AuthorizeClient`, `ClerkOrgMembership`,
|
|
340
|
+
`AuthorizeDeps`, `ResumeInput` and `ClerkJwk` / `ClerkJwks` /
|
|
341
|
+
`VerifiedClerkSession` / `VerifyClerkSessionResult` /
|
|
342
|
+
`ClerkSessionVerifierConfig` are the shapes the consumer supplies or reads.
|
|
343
|
+
- `exchangeAuthorizationCode(input, deps)` consumes the code FIRST (every
|
|
344
|
+
attempt burns it), then checks expiry, client, `redirect_uri`, resource and
|
|
345
|
+
the PKCE verifier (`pkceChallengeFromVerifier` derives a challenge). It
|
|
346
|
+
returns `AuthorizedTokenClaims`:
|
|
347
|
+
`{ sub, org_id, org_slug, org_role, aud, client_id, scope }` (`ExchangeInput`,
|
|
348
|
+
`ExchangeDeps`, `ExchangeResult`). No field comes from a client-registration
|
|
349
|
+
profile.
|
|
350
|
+
- `buildDiscoveryDocument(config)` builds the discovery document
|
|
351
|
+
(`DiscoveryConfig`, `DiscoveryDocument`, `DiscoveryResult`); the issuer must
|
|
352
|
+
be https with no query. It advertises only what is implemented: the code
|
|
353
|
+
flow, PKCE, the `authorization_code` grant and UserInfo. It omits
|
|
354
|
+
`id_token_signing_alg_values_supported` (OIDC Discovery lists it as required
|
|
355
|
+
for a provider that issues id_tokens; this package issues none, so that is a
|
|
356
|
+
documented deviation), refresh tokens and client-authentication methods. A
|
|
357
|
+
consumer that implements any of them adds those keys itself. `buildUserInfo(claims, user)` returns `sub`, and
|
|
358
|
+
`email` / `email_verified` when the token carries `email`
|
|
359
|
+
(`ClerkUserLike`, `UserInfo`, `UserInfoResult`).
|
|
360
|
+
- Every refusal is `{ code: "AUTHORIZE_REFUSED", reason }`
|
|
361
|
+
(`AuthorizeRefusal`, `AuthorizeRefusalReason`), the same shape as
|
|
362
|
+
`RoleRefusal`. `oauthErrorFor(refusal)` maps it to an OAuth error code and an
|
|
363
|
+
HTTP status (503 for a collaborator that failed, so an outage is not reported
|
|
364
|
+
as a bad credential). A collaborator that throws is always a refusal.
|
|
365
|
+
|
|
366
|
+
Also exported as subpaths: `/authorize`, `/token-exchange`, `/clerk-session`,
|
|
367
|
+
`/oidc`.
|
|
368
|
+
|
|
369
|
+
Limits, stated: `redirect_uri` matching is exact (no loopback port variation);
|
|
370
|
+
the package signs no access token and no id token and issues no refresh token (the consumer mints the token
|
|
371
|
+
from the claims); it fetches no key set and calls no Clerk API itself.
|
|
372
|
+
|
|
278
373
|
## What this package does not do
|
|
279
374
|
|
|
280
375
|
Worth reading before you rely on it.
|
|
@@ -296,10 +391,11 @@ Worth reading before you rely on it.
|
|
|
296
391
|
a token, writing its row, marking it revoked and expiring it are yours; the
|
|
297
392
|
package only insists that a row it is handed be believed in constant time,
|
|
298
393
|
and refused identically however it fails.
|
|
299
|
-
- **It does not verify
|
|
300
|
-
OPAQUE token — one whose only meaning is the row it matches.
|
|
301
|
-
|
|
302
|
-
|
|
394
|
+
- **It does not verify an arbitrary signed JWT.** The presented-bearer path is
|
|
395
|
+
for an OPAQUE token — one whose only meaning is the row it matches. The one
|
|
396
|
+
signed token it verifies is a Clerk session token (`verifyClerkSessionToken`,
|
|
397
|
+
RS256 only). For any other signed credential, verify the signature with a
|
|
398
|
+
JWT library first.
|
|
303
399
|
|
|
304
400
|
## Upgrading to 0.4.0
|
|
305
401
|
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { type AuthorizeRefusal } from "./authorize-shared.js";
|
|
2
|
+
import { type ClerkSessionVerifierConfig } from "./clerk-session.js";
|
|
3
|
+
/**
|
|
4
|
+
* The authorize flow for a PERSON: a Clerk session, a verified organisation
|
|
5
|
+
* membership, and an authorization code bound to both. Framework-agnostic:
|
|
6
|
+
* every function takes plain values and returns a plain outcome; the consumer
|
|
7
|
+
* owns HTTP, rendering and storage.
|
|
8
|
+
*
|
|
9
|
+
* The code is bound to `{ clerkUserId, orgId, orgSlug, orgRole, clientId,
|
|
10
|
+
* redirectUri, codeChallenge, resource }`. Nothing is read from a
|
|
11
|
+
* client-registration profile: a client's own fields only decide whether the
|
|
12
|
+
* request is acceptable, never what the person may reach.
|
|
13
|
+
*/
|
|
14
|
+
/** A registered OAuth client, as the consumer's own registry knows it. */
|
|
15
|
+
export interface AuthorizeClient {
|
|
16
|
+
clientId: string;
|
|
17
|
+
/** Exact-match list; no prefix, wildcard or partial match is ever applied. */
|
|
18
|
+
redirectUris: readonly string[];
|
|
19
|
+
clientName?: string;
|
|
20
|
+
revoked?: boolean;
|
|
21
|
+
}
|
|
22
|
+
/** One organisation membership, in the shape Clerk's Backend API returns. */
|
|
23
|
+
export interface ClerkOrgMembership {
|
|
24
|
+
role: string;
|
|
25
|
+
organization: {
|
|
26
|
+
id: string;
|
|
27
|
+
slug: string | null;
|
|
28
|
+
name: string;
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
/** A stored authorization code. Only the digest of the code is stored. */
|
|
32
|
+
export interface AuthorizationCodeRecord {
|
|
33
|
+
codeHash: string;
|
|
34
|
+
clerkUserId: string;
|
|
35
|
+
orgId: string;
|
|
36
|
+
orgSlug: string | null;
|
|
37
|
+
orgRole: string;
|
|
38
|
+
clientId: string;
|
|
39
|
+
redirectUri: string;
|
|
40
|
+
codeChallenge: string;
|
|
41
|
+
resource: string;
|
|
42
|
+
scope: string;
|
|
43
|
+
/** Epoch milliseconds. */
|
|
44
|
+
expiresAt: number;
|
|
45
|
+
}
|
|
46
|
+
export type ConsumeCodeResult = {
|
|
47
|
+
status: "ok";
|
|
48
|
+
record: AuthorizationCodeRecord;
|
|
49
|
+
} | {
|
|
50
|
+
status: "unknown";
|
|
51
|
+
} | {
|
|
52
|
+
status: "already-used";
|
|
53
|
+
};
|
|
54
|
+
/**
|
|
55
|
+
* Where codes live. `consume` MUST be atomic: of two concurrent calls for the
|
|
56
|
+
* same digest exactly one gets `ok`. A read-then-write is not an
|
|
57
|
+
* implementation of this interface.
|
|
58
|
+
*/
|
|
59
|
+
export interface AuthorizationCodeStore {
|
|
60
|
+
put(record: AuthorizationCodeRecord): Promise<void>;
|
|
61
|
+
consume(codeHash: string): Promise<ConsumeCodeResult>;
|
|
62
|
+
}
|
|
63
|
+
export interface AuthorizeConfig {
|
|
64
|
+
/** HMAC secret for the state blob; at least 32 characters. Never sent to a client. */
|
|
65
|
+
stateSecret: string;
|
|
66
|
+
/** Clerk sign-in page. The return URL travels in `redirect_url`. */
|
|
67
|
+
signInUrl: string;
|
|
68
|
+
/** The consumer's endpoint that calls `resumeAuthorize`. */
|
|
69
|
+
callbackUrl: string;
|
|
70
|
+
/** Resource identifiers (RFC 8707) this server issues codes for. Exact match. */
|
|
71
|
+
allowedResources: readonly string[];
|
|
72
|
+
/** When set, every requested scope must be listed. */
|
|
73
|
+
supportedScopes?: readonly string[];
|
|
74
|
+
/** Scope granted when the request names none. Default `"openid"`. */
|
|
75
|
+
defaultScope?: string;
|
|
76
|
+
/** Verification of the Clerk session token. */
|
|
77
|
+
session: ClerkSessionVerifierConfig;
|
|
78
|
+
/** Seconds the signed state stays valid. Default 600. */
|
|
79
|
+
stateTtlSeconds?: number;
|
|
80
|
+
/** Seconds an authorization code stays valid. Default 60. */
|
|
81
|
+
codeTtlSeconds?: number;
|
|
82
|
+
/**
|
|
83
|
+
* The person must explicitly approve (the outcome is a consent model, not a
|
|
84
|
+
* code, until `approved: true` is posted back). DEFAULT TRUE: without it, any
|
|
85
|
+
* dynamically registered client could obtain a code for a signed-in victim
|
|
86
|
+
* who merely follows a link. Set `false` explicitly only for first-party
|
|
87
|
+
* clients whose redirect URIs you control; a single organisation is then
|
|
88
|
+
* auto-picked.
|
|
89
|
+
*/
|
|
90
|
+
requireConsent?: boolean;
|
|
91
|
+
}
|
|
92
|
+
export interface AuthorizeDeps {
|
|
93
|
+
lookupClient(clientId: string): Promise<AuthorizeClient | null>;
|
|
94
|
+
/** Clerk Backend API: the user's organisation memberships. */
|
|
95
|
+
listMemberships(clerkUserId: string): Promise<readonly ClerkOrgMembership[]>;
|
|
96
|
+
codeStore: AuthorizationCodeStore;
|
|
97
|
+
/** Epoch milliseconds. */
|
|
98
|
+
now?: () => number;
|
|
99
|
+
/** Opaque code generator; default 32 random bytes, base64url. */
|
|
100
|
+
generateCode?: () => string;
|
|
101
|
+
}
|
|
102
|
+
export interface OrgPickerModel {
|
|
103
|
+
/** Signed state; post it back, untouched, to `resumeAuthorize`. */
|
|
104
|
+
state: string;
|
|
105
|
+
client: {
|
|
106
|
+
clientId: string;
|
|
107
|
+
clientName: string | null;
|
|
108
|
+
};
|
|
109
|
+
resource: string;
|
|
110
|
+
scope: string;
|
|
111
|
+
/** Only organisations the verified user is a member of. */
|
|
112
|
+
organizations: readonly {
|
|
113
|
+
id: string;
|
|
114
|
+
slug: string | null;
|
|
115
|
+
name: string;
|
|
116
|
+
role: string;
|
|
117
|
+
}[];
|
|
118
|
+
/** True when the person must tick approval even with one organisation. */
|
|
119
|
+
consentRequired: boolean;
|
|
120
|
+
/**
|
|
121
|
+
* Proof that THIS picker was shown to THIS verified user for THIS state.
|
|
122
|
+
* Render it as a hidden field and post it back as `consentToken`. Absent
|
|
123
|
+
* when consent is not required.
|
|
124
|
+
*/
|
|
125
|
+
consentToken: string | null;
|
|
126
|
+
}
|
|
127
|
+
export type AuthorizeOutcome = {
|
|
128
|
+
kind: "redirect-to-sign-in";
|
|
129
|
+
url: string;
|
|
130
|
+
} | {
|
|
131
|
+
kind: "org-picker";
|
|
132
|
+
model: OrgPickerModel;
|
|
133
|
+
} | {
|
|
134
|
+
kind: "redirect-to-client";
|
|
135
|
+
url: string;
|
|
136
|
+
} | {
|
|
137
|
+
kind: "refused";
|
|
138
|
+
refusal: AuthorizeRefusal;
|
|
139
|
+
};
|
|
140
|
+
export interface ResumeInput {
|
|
141
|
+
/** The `authorize_state` query parameter on the return URL (or the picker's posted state). */
|
|
142
|
+
state: string;
|
|
143
|
+
/** The Clerk session token presented on return. */
|
|
144
|
+
sessionToken: string | undefined;
|
|
145
|
+
/** The organisation the person chose in the picker. */
|
|
146
|
+
orgId?: string;
|
|
147
|
+
/** The person's explicit approval, when `requireConsent` is on. */
|
|
148
|
+
approved?: boolean;
|
|
149
|
+
/** The `consentToken` of the picker the person was shown. Required with `approved: true` when consent is on. */
|
|
150
|
+
consentToken?: string;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Handles `GET /authorize`. Validates the request against the registered
|
|
154
|
+
* client, then:
|
|
155
|
+
* - no Clerk session (absent, expired or forged) -> `redirect-to-sign-in`,
|
|
156
|
+
* carrying a signed, short-lived state blob and issuing NO code;
|
|
157
|
+
* - a verified session -> continues exactly as `resumeAuthorize` does.
|
|
158
|
+
* A Clerk key set that cannot be reached is a refusal (`jwks-unavailable`),
|
|
159
|
+
* never a redirect and never a grant.
|
|
160
|
+
*/
|
|
161
|
+
export declare function startAuthorize(params: Readonly<Record<string, string | undefined>>, sessionToken: string | undefined, cfg: AuthorizeConfig, deps: AuthorizeDeps): Promise<AuthorizeOutcome>;
|
|
162
|
+
/**
|
|
163
|
+
* Handles the return from Clerk (and the picker's form post). The request is
|
|
164
|
+
* rebuilt ONLY from the signed state: no query parameter other than the blob
|
|
165
|
+
* itself is read. A tampered or expired blob is refused; the client is looked
|
|
166
|
+
* up again so a client revoked mid-flow gets no code.
|
|
167
|
+
*/
|
|
168
|
+
export declare function resumeAuthorize(input: ResumeInput, cfg: AuthorizeConfig, deps: AuthorizeDeps): Promise<AuthorizeOutcome>;
|
|
169
|
+
//# sourceMappingURL=authorize-flow.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"authorize-flow.d.ts","sourceRoot":"","sources":["../src/authorize-flow.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import { MIN_SECRET_LENGTH, PKCE_METHOD, isWellFormedChallenge, refusal, signBlob, signMac, utf8, toBase64Url, verifyBlob, } from "./authorize-shared.js";
|
|
2
|
+
import { sha256Hex, timingSafeEqual } from "./crypto.js";
|
|
3
|
+
import { verifyClerkSessionToken, } from "./clerk-session.js";
|
|
4
|
+
const STATE_PARAM = "authorize_state";
|
|
5
|
+
const refused = (reason) => ({
|
|
6
|
+
kind: "refused",
|
|
7
|
+
refusal: refusal(reason),
|
|
8
|
+
});
|
|
9
|
+
function nowOf(deps) {
|
|
10
|
+
return (deps.now ?? Date.now)();
|
|
11
|
+
}
|
|
12
|
+
function configOk(cfg) {
|
|
13
|
+
return (typeof cfg.stateSecret === "string" &&
|
|
14
|
+
cfg.stateSecret.length >= MIN_SECRET_LENGTH &&
|
|
15
|
+
cfg.allowedResources.length > 0);
|
|
16
|
+
}
|
|
17
|
+
function validateRequest(params, client, cfg) {
|
|
18
|
+
if (client.revoked === true)
|
|
19
|
+
return { ok: false, reason: "unknown-client" };
|
|
20
|
+
const redirectUri = params.redirect_uri;
|
|
21
|
+
if (!redirectUri || !client.redirectUris.includes(redirectUri)) {
|
|
22
|
+
return { ok: false, reason: "redirect-uri-mismatch" };
|
|
23
|
+
}
|
|
24
|
+
if (params.response_type !== undefined && params.response_type !== "code") {
|
|
25
|
+
return { ok: false, reason: "unsupported-response-type" };
|
|
26
|
+
}
|
|
27
|
+
const challenge = params.code_challenge;
|
|
28
|
+
const method = params.code_challenge_method ?? PKCE_METHOD;
|
|
29
|
+
if (!challenge || method !== PKCE_METHOD || !isWellFormedChallenge(challenge)) {
|
|
30
|
+
return { ok: false, reason: "pkce-required" };
|
|
31
|
+
}
|
|
32
|
+
const resource = params.resource;
|
|
33
|
+
if (!resource || !cfg.allowedResources.includes(resource)) {
|
|
34
|
+
return { ok: false, reason: "resource-not-allowed" };
|
|
35
|
+
}
|
|
36
|
+
const scope = (params.scope ?? "").trim() || (cfg.defaultScope ?? "openid");
|
|
37
|
+
if (cfg.supportedScopes) {
|
|
38
|
+
const supported = cfg.supportedScopes;
|
|
39
|
+
if (!scope.split(/\s+/).every((s) => supported.includes(s))) {
|
|
40
|
+
return { ok: false, reason: "scope-not-supported" };
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
ok: true,
|
|
45
|
+
req: {
|
|
46
|
+
clientId: client.clientId,
|
|
47
|
+
redirectUri,
|
|
48
|
+
state: params.state ?? null,
|
|
49
|
+
codeChallenge: challenge,
|
|
50
|
+
resource,
|
|
51
|
+
scope,
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
async function signState(req, cfg, deps) {
|
|
56
|
+
const payload = {
|
|
57
|
+
v: 1,
|
|
58
|
+
cid: req.clientId,
|
|
59
|
+
ru: req.redirectUri,
|
|
60
|
+
st: req.state,
|
|
61
|
+
cc: req.codeChallenge,
|
|
62
|
+
rs: req.resource,
|
|
63
|
+
sc: req.scope,
|
|
64
|
+
exp: nowOf(deps) + (cfg.stateTtlSeconds ?? 600) * 1000,
|
|
65
|
+
};
|
|
66
|
+
return signBlob(payload, cfg.stateSecret);
|
|
67
|
+
}
|
|
68
|
+
function signInRedirect(cfg, stateBlob) {
|
|
69
|
+
const back = new URL(cfg.callbackUrl);
|
|
70
|
+
back.searchParams.set(STATE_PARAM, stateBlob);
|
|
71
|
+
const url = new URL(cfg.signInUrl);
|
|
72
|
+
url.searchParams.set("redirect_url", back.toString());
|
|
73
|
+
return { kind: "redirect-to-sign-in", url: url.toString() };
|
|
74
|
+
}
|
|
75
|
+
async function lookupClientSafe(deps, clientId) {
|
|
76
|
+
try {
|
|
77
|
+
return { ok: true, client: await deps.lookupClient(clientId) };
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
return { ok: false };
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
async function bindAndIssue(req, stateBlob, session, clientName, choice, cfg, deps) {
|
|
84
|
+
let memberships;
|
|
85
|
+
try {
|
|
86
|
+
memberships = await deps.listMemberships(session.userId);
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
return refused("membership-lookup-unavailable");
|
|
90
|
+
}
|
|
91
|
+
if (memberships.length === 0)
|
|
92
|
+
return refused("no-organization");
|
|
93
|
+
let picked;
|
|
94
|
+
if (choice.orgId !== undefined) {
|
|
95
|
+
// The chosen id is a CLAIM from the browser: it is honoured only if the
|
|
96
|
+
// verified user's own membership list contains it.
|
|
97
|
+
picked = memberships.find((m) => m.organization.id === choice.orgId);
|
|
98
|
+
if (!picked)
|
|
99
|
+
return refused("org-not-a-member");
|
|
100
|
+
}
|
|
101
|
+
else if (memberships.length === 1) {
|
|
102
|
+
picked = memberships[0];
|
|
103
|
+
}
|
|
104
|
+
const consentRequired = cfg.requireConsent !== false;
|
|
105
|
+
// The token binds the picker to the verified user, the signed state and the
|
|
106
|
+
// set of organisations that user belongs to (the set is what was shown).
|
|
107
|
+
const expectedToken = consentRequired
|
|
108
|
+
? await consentTokenFor(cfg.stateSecret, stateBlob, session.userId, memberships.map((m) => m.organization.id))
|
|
109
|
+
: null;
|
|
110
|
+
if (consentRequired && picked && choice.approved === true) {
|
|
111
|
+
// `approved: true` is a CLAIM in a POST body. An attacker client can mint
|
|
112
|
+
// its own state and replay it with a victim's session; only a token the
|
|
113
|
+
// server issued to this user for this state proves the picker was shown.
|
|
114
|
+
const presented = choice.consentToken;
|
|
115
|
+
const ok = typeof presented === "string" &&
|
|
116
|
+
expectedToken !== null &&
|
|
117
|
+
(await timingSafeEqual(utf8(presented), utf8(expectedToken)));
|
|
118
|
+
if (!ok)
|
|
119
|
+
return refused("consent-required");
|
|
120
|
+
}
|
|
121
|
+
if (!picked || (consentRequired && choice.approved !== true)) {
|
|
122
|
+
return {
|
|
123
|
+
kind: "org-picker",
|
|
124
|
+
model: {
|
|
125
|
+
state: stateBlob,
|
|
126
|
+
client: { clientId: req.clientId, clientName },
|
|
127
|
+
resource: req.resource,
|
|
128
|
+
scope: req.scope,
|
|
129
|
+
organizations: memberships.map((m) => ({
|
|
130
|
+
id: m.organization.id,
|
|
131
|
+
slug: m.organization.slug,
|
|
132
|
+
name: m.organization.name,
|
|
133
|
+
role: m.role,
|
|
134
|
+
})),
|
|
135
|
+
consentRequired,
|
|
136
|
+
consentToken: expectedToken,
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
const code = (deps.generateCode ?? randomCode)();
|
|
141
|
+
const record = {
|
|
142
|
+
codeHash: await sha256Hex(code),
|
|
143
|
+
clerkUserId: session.userId,
|
|
144
|
+
orgId: picked.organization.id,
|
|
145
|
+
orgSlug: picked.organization.slug,
|
|
146
|
+
orgRole: picked.role,
|
|
147
|
+
clientId: req.clientId,
|
|
148
|
+
redirectUri: req.redirectUri,
|
|
149
|
+
codeChallenge: req.codeChallenge,
|
|
150
|
+
resource: req.resource,
|
|
151
|
+
scope: req.scope,
|
|
152
|
+
expiresAt: nowOf(deps) + (cfg.codeTtlSeconds ?? 60) * 1000,
|
|
153
|
+
};
|
|
154
|
+
try {
|
|
155
|
+
await deps.codeStore.put(record);
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
return refused("code-store-unavailable");
|
|
159
|
+
}
|
|
160
|
+
const back = new URL(req.redirectUri);
|
|
161
|
+
back.searchParams.set("code", code);
|
|
162
|
+
if (req.state !== null)
|
|
163
|
+
back.searchParams.set("state", req.state);
|
|
164
|
+
return { kind: "redirect-to-client", url: back.toString() };
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* `HMAC(stateSecret, "consent-v1\0" + state + "\0" + clerkUserId + "\0" +
|
|
168
|
+
* sorted organisation ids joined by ",")`. Bound to the exact state blob (so a
|
|
169
|
+
* token for one request is useless for another), to the verified user (so it
|
|
170
|
+
* cannot be replayed under another session) and to the organisation set shown
|
|
171
|
+
* (the chosen org is separately required to be in the live membership list).
|
|
172
|
+
*/
|
|
173
|
+
async function consentTokenFor(secret, state, userId, orgIds) {
|
|
174
|
+
const set = [...orgIds].sort().join(",");
|
|
175
|
+
return signMac(`consent-v1\0${state}\0${userId}\0${set}`, secret);
|
|
176
|
+
}
|
|
177
|
+
function randomCode() {
|
|
178
|
+
return toBase64Url(crypto.getRandomValues(new Uint8Array(32)));
|
|
179
|
+
}
|
|
180
|
+
// ---------------------------------------------------------------------------
|
|
181
|
+
// Public functions
|
|
182
|
+
// ---------------------------------------------------------------------------
|
|
183
|
+
/**
|
|
184
|
+
* Handles `GET /authorize`. Validates the request against the registered
|
|
185
|
+
* client, then:
|
|
186
|
+
* - no Clerk session (absent, expired or forged) -> `redirect-to-sign-in`,
|
|
187
|
+
* carrying a signed, short-lived state blob and issuing NO code;
|
|
188
|
+
* - a verified session -> continues exactly as `resumeAuthorize` does.
|
|
189
|
+
* A Clerk key set that cannot be reached is a refusal (`jwks-unavailable`),
|
|
190
|
+
* never a redirect and never a grant.
|
|
191
|
+
*/
|
|
192
|
+
export async function startAuthorize(params, sessionToken, cfg, deps) {
|
|
193
|
+
if (!configOk(cfg))
|
|
194
|
+
return refused("server-misconfigured");
|
|
195
|
+
const clientId = params.client_id;
|
|
196
|
+
if (!clientId)
|
|
197
|
+
return refused("invalid-request");
|
|
198
|
+
const found = await lookupClientSafe(deps, clientId);
|
|
199
|
+
if (!found.ok)
|
|
200
|
+
return refused("client-lookup-unavailable");
|
|
201
|
+
if (!found.client)
|
|
202
|
+
return refused("unknown-client");
|
|
203
|
+
const v = validateRequest(params, found.client, cfg);
|
|
204
|
+
if (!v.ok)
|
|
205
|
+
return refused(v.reason);
|
|
206
|
+
const stateBlob = await signState(v.req, cfg, deps);
|
|
207
|
+
if (!sessionToken)
|
|
208
|
+
return signInRedirect(cfg, stateBlob);
|
|
209
|
+
const verified = await verifyClerkSessionToken(sessionToken, cfg.session, nowOf(deps));
|
|
210
|
+
if (!verified.ok) {
|
|
211
|
+
return verified.refusal.reason === "jwks-unavailable"
|
|
212
|
+
? { kind: "refused", refusal: verified.refusal }
|
|
213
|
+
: signInRedirect(cfg, stateBlob);
|
|
214
|
+
}
|
|
215
|
+
return bindAndIssue(v.req, stateBlob, verified.session, found.client.clientName ?? null, {}, cfg, deps);
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Handles the return from Clerk (and the picker's form post). The request is
|
|
219
|
+
* rebuilt ONLY from the signed state: no query parameter other than the blob
|
|
220
|
+
* itself is read. A tampered or expired blob is refused; the client is looked
|
|
221
|
+
* up again so a client revoked mid-flow gets no code.
|
|
222
|
+
*/
|
|
223
|
+
export async function resumeAuthorize(input, cfg, deps) {
|
|
224
|
+
if (!configOk(cfg))
|
|
225
|
+
return refused("server-misconfigured");
|
|
226
|
+
const raw = typeof input.state === "string" ? await verifyBlob(input.state, cfg.stateSecret) : null;
|
|
227
|
+
const p = raw;
|
|
228
|
+
if (!p ||
|
|
229
|
+
p.v !== 1 ||
|
|
230
|
+
typeof p.cid !== "string" ||
|
|
231
|
+
typeof p.ru !== "string" ||
|
|
232
|
+
typeof p.cc !== "string" ||
|
|
233
|
+
typeof p.rs !== "string" ||
|
|
234
|
+
typeof p.sc !== "string" ||
|
|
235
|
+
typeof p.exp !== "number" ||
|
|
236
|
+
(p.st !== null && typeof p.st !== "string")) {
|
|
237
|
+
return refused("state-invalid");
|
|
238
|
+
}
|
|
239
|
+
if (p.exp <= nowOf(deps))
|
|
240
|
+
return refused("state-expired");
|
|
241
|
+
const found = await lookupClientSafe(deps, p.cid);
|
|
242
|
+
if (!found.ok)
|
|
243
|
+
return refused("client-lookup-unavailable");
|
|
244
|
+
if (!found.client)
|
|
245
|
+
return refused("unknown-client");
|
|
246
|
+
const v = validateRequest({
|
|
247
|
+
redirect_uri: p.ru,
|
|
248
|
+
code_challenge: p.cc,
|
|
249
|
+
resource: p.rs,
|
|
250
|
+
scope: p.sc,
|
|
251
|
+
...(p.st !== null && p.st !== undefined ? { state: p.st } : {}),
|
|
252
|
+
}, found.client, cfg);
|
|
253
|
+
if (!v.ok)
|
|
254
|
+
return refused(v.reason);
|
|
255
|
+
if (!input.sessionToken)
|
|
256
|
+
return refused("session-required");
|
|
257
|
+
const verified = await verifyClerkSessionToken(input.sessionToken, cfg.session, nowOf(deps));
|
|
258
|
+
if (!verified.ok)
|
|
259
|
+
return { kind: "refused", refusal: verified.refusal };
|
|
260
|
+
return bindAndIssue(v.req, input.state, verified.session, found.client.clientName ?? null, { orgId: input.orgId, approved: input.approved, consentToken: input.consentToken }, cfg, deps);
|
|
261
|
+
}
|
|
262
|
+
//# sourceMappingURL=authorize-flow.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"authorize-flow.js","sourceRoot":"","sources":["../src/authorize-flow.ts"],"names":[],"mappings":"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"}
|