base-idp 1.0.0 → 1.1.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/dist/browser-paseto.d.ts +13 -0
- package/dist/browser-paseto.d.ts.map +1 -0
- package/dist/browser-paseto.js +116 -0
- package/dist/browser-paseto.js.map +1 -0
- package/dist/browser.d.ts +133 -0
- package/dist/browser.d.ts.map +1 -0
- package/dist/browser.js +382 -0
- package/dist/browser.js.map +1 -0
- package/dist/index.d.ts +7 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -3
- package/dist/index.js.map +1 -1
- package/dist/react.d.ts +100 -1
- package/dist/react.d.ts.map +1 -1
- package/dist/react.js +200 -0
- package/dist/react.js.map +1 -1
- package/dist/server.d.ts +36 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +51 -0
- package/dist/server.js.map +1 -1
- package/dist/types.d.ts +64 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/vite.d.ts +48 -4
- package/dist/vite.d.ts.map +1 -1
- package/dist/vite.js +59 -6
- package/dist/vite.js.map +1 -1
- package/package.json +16 -3
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { BaseIdpPublicKeySet, VerifiedPrincipal, VerifyAccessTokenOptions } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Verify a Base IDP PASETO v4.public token using the Web Crypto API.
|
|
4
|
+
*
|
|
5
|
+
* Unlike the server-side verifyPasetoV4Public in paseto.ts this function is
|
|
6
|
+
* fully async because crypto.subtle.verify is always asynchronous.
|
|
7
|
+
*/
|
|
8
|
+
export declare function verifyPasetoV4PublicBrowser(token: string, keySet: BaseIdpPublicKeySet, config: {
|
|
9
|
+
issuer: string;
|
|
10
|
+
audience?: string;
|
|
11
|
+
requiredScope?: string;
|
|
12
|
+
}, options?: VerifyAccessTokenOptions): Promise<VerifiedPrincipal>;
|
|
13
|
+
//# sourceMappingURL=browser-paseto.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"browser-paseto.d.ts","sourceRoot":"","sources":["../src/browser-paseto.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAEV,mBAAmB,EACnB,iBAAiB,EACjB,wBAAwB,EACzB,MAAM,YAAY,CAAC;AAWpB;;;;;GAKG;AACH,wBAAsB,2BAA2B,CAC/C,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,mBAAmB,EAC3B,MAAM,EAAE;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,aAAa,CAAC,EAAE,MAAM,CAAA;CAAE,EACrE,OAAO,GAAE,wBAA6B,GACrC,OAAO,CAAC,iBAAiB,CAAC,CAqF5B"}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser-compatible PASETO v4.public verifier.
|
|
3
|
+
*
|
|
4
|
+
* Identical protocol to paseto.ts but uses Web Crypto API (crypto.subtle)
|
|
5
|
+
* instead of node:crypto so it can be bundled into browser applications.
|
|
6
|
+
* Ed25519 verification via crypto.subtle requires Chrome 113+, Firefox 129+,
|
|
7
|
+
* Safari 17+ (all major modern browsers).
|
|
8
|
+
*/
|
|
9
|
+
import { base64UrlDecode, concatBytes, utf8Decode, utf8Encode } from "./base64url.js";
|
|
10
|
+
import { idpError } from "./errors.js";
|
|
11
|
+
const HEADER = utf8Encode("v4.public.");
|
|
12
|
+
const IMPLICIT_ASSERTION = utf8Encode("square-experience:idp:access:v1");
|
|
13
|
+
/**
|
|
14
|
+
* Verify a Base IDP PASETO v4.public token using the Web Crypto API.
|
|
15
|
+
*
|
|
16
|
+
* Unlike the server-side verifyPasetoV4Public in paseto.ts this function is
|
|
17
|
+
* fully async because crypto.subtle.verify is always asynchronous.
|
|
18
|
+
*/
|
|
19
|
+
export async function verifyPasetoV4PublicBrowser(token, keySet, config, options = {}) {
|
|
20
|
+
const parts = token.split(".");
|
|
21
|
+
if (parts.length !== 4 || parts[0] !== "v4" || parts[1] !== "public") {
|
|
22
|
+
throw idpError("invalid_token", "token is not PASETO v4.public");
|
|
23
|
+
}
|
|
24
|
+
const payload = base64UrlDecode(parts[2]);
|
|
25
|
+
const footerBytes = base64UrlDecode(parts[3]);
|
|
26
|
+
if (payload.length <= 64) {
|
|
27
|
+
throw idpError("invalid_token", "PASETO payload is too short");
|
|
28
|
+
}
|
|
29
|
+
const footer = JSON.parse(utf8Decode(footerBytes));
|
|
30
|
+
if (footer.alg !== "v4.public" || footer.typ !== "paseto" || !footer.kid) {
|
|
31
|
+
throw idpError("invalid_token", "PASETO footer is not a Base IDP v4.public footer");
|
|
32
|
+
}
|
|
33
|
+
const publicKey = keySet.keys.find((key) => key.kid === footer.kid && key.alg === "v4.public");
|
|
34
|
+
if (!publicKey) {
|
|
35
|
+
throw idpError("unknown_kid", "PASETO key id is not in the Base IDP key set");
|
|
36
|
+
}
|
|
37
|
+
const message = payload.subarray(0, payload.length - 64);
|
|
38
|
+
const signature = payload.subarray(payload.length - 64);
|
|
39
|
+
const rawPublicKey = base64UrlDecode(publicKey.public_key_base64);
|
|
40
|
+
if (rawPublicKey.length !== 32 || signature.length !== 64) {
|
|
41
|
+
throw idpError("invalid_key", "Ed25519 public key or signature has an unexpected size");
|
|
42
|
+
}
|
|
43
|
+
// Build the Pre-Authentication Encoding (PAE) message
|
|
44
|
+
const pae = preAuthEncode([HEADER, message, footerBytes, IMPLICIT_ASSERTION]);
|
|
45
|
+
// Verify using the Web Crypto API Ed25519 (no node:crypto dependency)
|
|
46
|
+
let valid;
|
|
47
|
+
try {
|
|
48
|
+
const cryptoKey = await crypto.subtle.importKey("raw", rawPublicKey.buffer.slice(rawPublicKey.byteOffset, rawPublicKey.byteOffset + rawPublicKey.byteLength), { name: "Ed25519" }, false, ["verify"]);
|
|
49
|
+
const paeBuffer = pae.buffer.slice(pae.byteOffset, pae.byteOffset + pae.byteLength);
|
|
50
|
+
const sigBuffer = signature.buffer.slice(signature.byteOffset, signature.byteOffset + signature.byteLength);
|
|
51
|
+
valid = await crypto.subtle.verify("Ed25519", cryptoKey, sigBuffer, paeBuffer);
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
throw idpError("invalid_key", "Ed25519 key import or verification failed");
|
|
55
|
+
}
|
|
56
|
+
if (!valid) {
|
|
57
|
+
throw idpError("invalid_signature", "PASETO signature verification failed");
|
|
58
|
+
}
|
|
59
|
+
const claims = JSON.parse(utf8Decode(message));
|
|
60
|
+
validateClaims(claims, {
|
|
61
|
+
issuer: options.issuer ?? config.issuer,
|
|
62
|
+
audience: options.audience ?? config.audience ?? "square-experience",
|
|
63
|
+
requiredScope: options.requiredScope ?? config.requiredScope,
|
|
64
|
+
maxClockSkewSeconds: options.maxClockSkewSeconds ?? 30,
|
|
65
|
+
});
|
|
66
|
+
return {
|
|
67
|
+
id: claims.gid,
|
|
68
|
+
subject: claims.sub,
|
|
69
|
+
email: claims.email,
|
|
70
|
+
name: claims.name,
|
|
71
|
+
role: claims.role,
|
|
72
|
+
scopes: claims.scp ?? [],
|
|
73
|
+
accountContext: claims.ctx,
|
|
74
|
+
claims,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
function validateClaims(claims, options) {
|
|
78
|
+
if (claims.token_use !== "access") {
|
|
79
|
+
throw idpError("invalid_claims", "token_use must be 'access'");
|
|
80
|
+
}
|
|
81
|
+
if (claims.iss !== options.issuer || claims.aud !== options.audience) {
|
|
82
|
+
throw idpError("invalid_claims", "issuer or audience mismatch");
|
|
83
|
+
}
|
|
84
|
+
const now = Date.now();
|
|
85
|
+
const skewMs = options.maxClockSkewSeconds * 1000;
|
|
86
|
+
if (Date.parse(claims.exp) <= now - skewMs) {
|
|
87
|
+
throw idpError("token_expired", "access token has expired");
|
|
88
|
+
}
|
|
89
|
+
if (Date.parse(claims.nbf) > now + skewMs) {
|
|
90
|
+
throw idpError("token_not_yet_valid", "access token is not yet valid");
|
|
91
|
+
}
|
|
92
|
+
if (options.requiredScope && !(claims.scp ?? []).includes(options.requiredScope)) {
|
|
93
|
+
throw idpError("insufficient_scope", `required scope '${options.requiredScope}' is missing`);
|
|
94
|
+
}
|
|
95
|
+
if (!claims.gid || !claims.sub || !claims.sid || !claims.ctx || !claims.role) {
|
|
96
|
+
throw idpError("invalid_claims", "required identity claims are missing from the token");
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
// Pre-Authentication Encoding as defined by PASETO spec
|
|
100
|
+
function preAuthEncode(pieces) {
|
|
101
|
+
const out = [uint64le(pieces.length)];
|
|
102
|
+
for (const piece of pieces) {
|
|
103
|
+
out.push(uint64le(piece.length), piece);
|
|
104
|
+
}
|
|
105
|
+
return concatBytes(out);
|
|
106
|
+
}
|
|
107
|
+
function uint64le(value) {
|
|
108
|
+
const out = new Uint8Array(8);
|
|
109
|
+
let current = BigInt(value);
|
|
110
|
+
for (let i = 0; i < 8; i++) {
|
|
111
|
+
out[i] = Number(current & 0xffn);
|
|
112
|
+
current >>= 8n;
|
|
113
|
+
}
|
|
114
|
+
return out;
|
|
115
|
+
}
|
|
116
|
+
//# sourceMappingURL=browser-paseto.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"browser-paseto.js","sourceRoot":"","sources":["../src/browser-paseto.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser Client SDK — BrowserBaseIdpClient
|
|
3
|
+
*
|
|
4
|
+
* This is the client-side entry point for every browser app.
|
|
5
|
+
* It owns the complete auth flow:
|
|
6
|
+
*
|
|
7
|
+
* 1. loginWithRedirect() — generates PKCE, stores state in sessionStorage, redirects to Base IDP
|
|
8
|
+
* 2. handleRedirectCallback() — validates state, exchanges code, verifies token, stores session
|
|
9
|
+
* 3. getSession() / getUser() / isAuthenticated() — synchronous session access
|
|
10
|
+
* 4. getAccessToken() — returns a live token, refreshes transparently if near expiry
|
|
11
|
+
* 5. refreshSession() — explicit refresh using the stored refresh token
|
|
12
|
+
* 6. logout() — clears session, optionally redirects
|
|
13
|
+
* 7. onSessionChange() — reactive session event subscription
|
|
14
|
+
*
|
|
15
|
+
* Zero Node.js dependencies. Runs in any modern browser or Deno/Cloudflare Workers.
|
|
16
|
+
*/
|
|
17
|
+
import { BaseIdPClient } from "./client.js";
|
|
18
|
+
import type { AuthSession, BrowserBaseIdpConfig, LoginWithRedirectOptions, SessionChangeListener, VerifiedPrincipal, VerifyAccessTokenOptions } from "./types.js";
|
|
19
|
+
export type HandleCallbackResult = {
|
|
20
|
+
principal: VerifiedPrincipal;
|
|
21
|
+
/** The `returnTo` path that was saved before the redirect, if any */
|
|
22
|
+
returnTo?: string;
|
|
23
|
+
};
|
|
24
|
+
export declare class BrowserBaseIdpClient extends BaseIdPClient {
|
|
25
|
+
private readonly browserCfg;
|
|
26
|
+
private readonly persistSession;
|
|
27
|
+
private readonly prefix;
|
|
28
|
+
private session;
|
|
29
|
+
private readonly listeners;
|
|
30
|
+
private refreshTimer;
|
|
31
|
+
constructor(browserCfg: BrowserBaseIdpConfig);
|
|
32
|
+
/**
|
|
33
|
+
* Discover and cache the client configuration from Base IDP.
|
|
34
|
+
* Call this once on app start before using any other method.
|
|
35
|
+
*/
|
|
36
|
+
init(): Promise<void>;
|
|
37
|
+
/**
|
|
38
|
+
* Start the PKCE authorization code flow.
|
|
39
|
+
*
|
|
40
|
+
* - Generates a fresh code_verifier + S256 challenge
|
|
41
|
+
* - Stores `{ state, codeVerifier, redirectUri, returnTo }` in sessionStorage
|
|
42
|
+
* - Redirects the browser to Base IDP's authorization endpoint
|
|
43
|
+
*
|
|
44
|
+
* Call `handleRedirectCallback()` on the redirect URI page to complete login.
|
|
45
|
+
*/
|
|
46
|
+
loginWithRedirect(options?: LoginWithRedirectOptions): Promise<void>;
|
|
47
|
+
/**
|
|
48
|
+
* Handle the OAuth2 redirect callback.
|
|
49
|
+
*
|
|
50
|
+
* Reads `code` and `state` from the URL (or the `url` argument),
|
|
51
|
+
* validates state against the pending transaction in sessionStorage,
|
|
52
|
+
* exchanges the code for tokens, verifies the access token, and
|
|
53
|
+
* stores the resulting session.
|
|
54
|
+
*
|
|
55
|
+
* Returns `{ principal, returnTo }` so the app can navigate to the
|
|
56
|
+
* original destination.
|
|
57
|
+
*/
|
|
58
|
+
handleRedirectCallback(url?: string): Promise<HandleCallbackResult>;
|
|
59
|
+
/**
|
|
60
|
+
* Synchronously return the current session, or null if not authenticated.
|
|
61
|
+
*/
|
|
62
|
+
getSession(): AuthSession | null;
|
|
63
|
+
/**
|
|
64
|
+
* Synchronously return the current user (VerifiedPrincipal), or null.
|
|
65
|
+
*/
|
|
66
|
+
getUser(): VerifiedPrincipal | null;
|
|
67
|
+
/**
|
|
68
|
+
* Returns true if there is a session whose access token has not yet expired.
|
|
69
|
+
*/
|
|
70
|
+
isAuthenticated(): boolean;
|
|
71
|
+
/**
|
|
72
|
+
* Return a live access token, transparently refreshing it if it is within
|
|
73
|
+
* REFRESH_THRESHOLD_MS (60 s) of expiry.
|
|
74
|
+
*
|
|
75
|
+
* Throws `not_authenticated` if there is no session at all.
|
|
76
|
+
* Throws `session_expired` if the refresh also fails.
|
|
77
|
+
*/
|
|
78
|
+
getAccessToken(options?: Pick<VerifyAccessTokenOptions, "requiredScope">): Promise<string>;
|
|
79
|
+
/**
|
|
80
|
+
* Force a token refresh using the stored refresh token.
|
|
81
|
+
* Updates the in-memory (and optionally persisted) session.
|
|
82
|
+
*/
|
|
83
|
+
refreshSession(): Promise<AuthSession>;
|
|
84
|
+
/**
|
|
85
|
+
* Clear the session and optionally redirect to a post-logout URL.
|
|
86
|
+
*/
|
|
87
|
+
logout(options?: {
|
|
88
|
+
returnTo?: string;
|
|
89
|
+
}): void;
|
|
90
|
+
/**
|
|
91
|
+
* Subscribe to session lifecycle events (`signed_in`, `signed_out`,
|
|
92
|
+
* `refreshed`, `expired`).
|
|
93
|
+
*
|
|
94
|
+
* Returns an unsubscribe function.
|
|
95
|
+
*
|
|
96
|
+
* @example
|
|
97
|
+
* const off = auth.onSessionChange(({ type, session }) => {
|
|
98
|
+
* if (type === "signed_out") router.push("/login");
|
|
99
|
+
* });
|
|
100
|
+
* // Later:
|
|
101
|
+
* off();
|
|
102
|
+
*/
|
|
103
|
+
onSessionChange(listener: SessionChangeListener): () => void;
|
|
104
|
+
private verifyTokenBrowser;
|
|
105
|
+
private storeSession;
|
|
106
|
+
private clearSession;
|
|
107
|
+
private loadPersistedSession;
|
|
108
|
+
private scheduleRefresh;
|
|
109
|
+
private toPublicSession;
|
|
110
|
+
private emit;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Create a browser auth client from config.
|
|
114
|
+
*
|
|
115
|
+
* @example
|
|
116
|
+
* // Vite / Vanilla browser app
|
|
117
|
+
* const auth = createBrowserBaseIdpAuth({
|
|
118
|
+
* key: import.meta.env.VITE_BASE_IDP_KEY,
|
|
119
|
+
* issuer: import.meta.env.VITE_BASE_IDP_ISSUER,
|
|
120
|
+
* persistSession: true,
|
|
121
|
+
* });
|
|
122
|
+
*
|
|
123
|
+
* // On app load
|
|
124
|
+
* await auth.init();
|
|
125
|
+
*
|
|
126
|
+
* // Start login
|
|
127
|
+
* await auth.loginWithRedirect({ returnTo: "/dashboard" });
|
|
128
|
+
*
|
|
129
|
+
* // On the callback page
|
|
130
|
+
* const { principal, returnTo } = await auth.handleRedirectCallback();
|
|
131
|
+
*/
|
|
132
|
+
export declare function createBrowserBaseIdpAuth(config: BrowserBaseIdpConfig): BrowserBaseIdpClient;
|
|
133
|
+
//# sourceMappingURL=browser.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"browser.d.ts","sourceRoot":"","sources":["../src/browser.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAI5C,OAAO,KAAK,EACV,WAAW,EACX,oBAAoB,EACpB,wBAAwB,EAExB,qBAAqB,EACrB,iBAAiB,EACjB,wBAAwB,EACzB,MAAM,YAAY,CAAC;AAwBpB,MAAM,MAAM,oBAAoB,GAAG;IACjC,SAAS,EAAE,iBAAiB,CAAC;IAC7B,qEAAqE;IACrE,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF,qBAAa,oBAAqB,SAAQ,aAAa;IAOzC,OAAO,CAAC,QAAQ,CAAC,UAAU;IANvC,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAU;IACzC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,OAAO,CAAgC;IAC/C,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAyC;IACnE,OAAO,CAAC,YAAY,CAA8C;gBAErC,UAAU,EAAE,oBAAoB;IAqB7D;;;OAGG;IACG,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAU3B;;;;;;;;OAQG;IACG,iBAAiB,CAAC,OAAO,GAAE,wBAA6B,GAAG,OAAO,CAAC,IAAI,CAAC;IAsD9E;;;;;;;;;;OAUG;IACG,sBAAsB,CAAC,GAAG,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,oBAAoB,CAAC;IAuDzE;;OAEG;IACH,UAAU,IAAI,WAAW,GAAG,IAAI;IAShC;;OAEG;IACH,OAAO,IAAI,iBAAiB,GAAG,IAAI;IAInC;;OAEG;IACH,eAAe,IAAI,OAAO;IAI1B;;;;;;OAMG;IACG,cAAc,CAAC,OAAO,GAAE,IAAI,CAAC,wBAAwB,EAAE,eAAe,CAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAuBpG;;;OAGG;IACG,cAAc,IAAI,OAAO,CAAC,WAAW,CAAC;IA2B5C;;OAEG;IACH,MAAM,CAAC,OAAO,GAAE;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAO,GAAG,IAAI;IASjD;;;;;;;;;;;;OAYG;IACH,eAAe,CAAC,QAAQ,EAAE,qBAAqB,GAAG,MAAM,IAAI;YAS9C,kBAAkB;IAShC,OAAO,CAAC,YAAY;IAqBpB,OAAO,CAAC,YAAY;IAmBpB,OAAO,CAAC,oBAAoB;IAc5B,OAAO,CAAC,eAAe;IAiBvB,OAAO,CAAC,eAAe;IAQvB,OAAO,CAAC,IAAI;CASb;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,oBAAoB,GAAG,oBAAoB,CAE3F"}
|
package/dist/browser.js
ADDED
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser Client SDK — BrowserBaseIdpClient
|
|
3
|
+
*
|
|
4
|
+
* This is the client-side entry point for every browser app.
|
|
5
|
+
* It owns the complete auth flow:
|
|
6
|
+
*
|
|
7
|
+
* 1. loginWithRedirect() — generates PKCE, stores state in sessionStorage, redirects to Base IDP
|
|
8
|
+
* 2. handleRedirectCallback() — validates state, exchanges code, verifies token, stores session
|
|
9
|
+
* 3. getSession() / getUser() / isAuthenticated() — synchronous session access
|
|
10
|
+
* 4. getAccessToken() — returns a live token, refreshes transparently if near expiry
|
|
11
|
+
* 5. refreshSession() — explicit refresh using the stored refresh token
|
|
12
|
+
* 6. logout() — clears session, optionally redirects
|
|
13
|
+
* 7. onSessionChange() — reactive session event subscription
|
|
14
|
+
*
|
|
15
|
+
* Zero Node.js dependencies. Runs in any modern browser or Deno/Cloudflare Workers.
|
|
16
|
+
*/
|
|
17
|
+
import { BaseIdPClient } from "./client.js";
|
|
18
|
+
import { generatePKCE } from "./pkce.js";
|
|
19
|
+
import { idpError } from "./errors.js";
|
|
20
|
+
import { verifyPasetoV4PublicBrowser } from "./browser-paseto.js";
|
|
21
|
+
// How many milliseconds before expiry we proactively refresh the token.
|
|
22
|
+
const REFRESH_THRESHOLD_MS = 60_000;
|
|
23
|
+
export class BrowserBaseIdpClient extends BaseIdPClient {
|
|
24
|
+
browserCfg;
|
|
25
|
+
persistSession;
|
|
26
|
+
prefix;
|
|
27
|
+
session = null;
|
|
28
|
+
listeners = new Set();
|
|
29
|
+
refreshTimer = null;
|
|
30
|
+
constructor(browserCfg) {
|
|
31
|
+
super(browserCfg);
|
|
32
|
+
this.browserCfg = browserCfg;
|
|
33
|
+
this.persistSession = browserCfg.persistSession ?? false;
|
|
34
|
+
this.prefix = browserCfg.storagePrefix ?? "base_idp";
|
|
35
|
+
// Apply browser-specific overrides on top of the base config
|
|
36
|
+
if (browserCfg.redirectUri)
|
|
37
|
+
this.cfg.redirectUri = browserCfg.redirectUri;
|
|
38
|
+
if (browserCfg.audience)
|
|
39
|
+
this.cfg.audience = browserCfg.audience;
|
|
40
|
+
if (browserCfg.scopes) {
|
|
41
|
+
this.cfg.scopes = this.scopes(browserCfg.scopes);
|
|
42
|
+
}
|
|
43
|
+
// Eagerly restore any persisted session from storage
|
|
44
|
+
this.session = this.loadPersistedSession();
|
|
45
|
+
if (this.session) {
|
|
46
|
+
this.scheduleRefresh();
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
// ── Initialise ─────────────────────────────────────────────────────────────
|
|
50
|
+
/**
|
|
51
|
+
* Discover and cache the client configuration from Base IDP.
|
|
52
|
+
* Call this once on app start before using any other method.
|
|
53
|
+
*/
|
|
54
|
+
async init() {
|
|
55
|
+
await this.resolveConfig();
|
|
56
|
+
if (!this.session) {
|
|
57
|
+
this.session = this.loadPersistedSession();
|
|
58
|
+
if (this.session)
|
|
59
|
+
this.scheduleRefresh();
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
// ── Login ──────────────────────────────────────────────────────────────────
|
|
63
|
+
/**
|
|
64
|
+
* Start the PKCE authorization code flow.
|
|
65
|
+
*
|
|
66
|
+
* - Generates a fresh code_verifier + S256 challenge
|
|
67
|
+
* - Stores `{ state, codeVerifier, redirectUri, returnTo }` in sessionStorage
|
|
68
|
+
* - Redirects the browser to Base IDP's authorization endpoint
|
|
69
|
+
*
|
|
70
|
+
* Call `handleRedirectCallback()` on the redirect URI page to complete login.
|
|
71
|
+
*/
|
|
72
|
+
async loginWithRedirect(options = {}) {
|
|
73
|
+
await this.resolveConfig();
|
|
74
|
+
// Confidential clients require a client_secret for code exchange.
|
|
75
|
+
// Secrets must never appear in browser bundles — reject early with a
|
|
76
|
+
// clear developer message instead of failing silently at the token endpoint.
|
|
77
|
+
if (this.cfg.confidential) {
|
|
78
|
+
throw idpError("confidential_client_in_browser", "This client is registered as confidential (requires a client_secret). " +
|
|
79
|
+
"Browser apps must use a public PKCE client. " +
|
|
80
|
+
"Either register a new public client for this app, or route the OAuth callback " +
|
|
81
|
+
"through a backend server that holds the secret and calls exchangeCode() there.");
|
|
82
|
+
}
|
|
83
|
+
const pkce = await generatePKCE();
|
|
84
|
+
const state = options.state ?? generateRandom(32);
|
|
85
|
+
const nonce = options.nonce ?? generateRandom(16);
|
|
86
|
+
const redirectUri = options.redirectUri ?? this.cfg.redirectUri;
|
|
87
|
+
if (!redirectUri) {
|
|
88
|
+
throw idpError("invalid_config", "redirectUri is required — pass it in BrowserBaseIdpConfig or LoginWithRedirectOptions");
|
|
89
|
+
}
|
|
90
|
+
const tx = {
|
|
91
|
+
state,
|
|
92
|
+
codeVerifier: pkce.verifier,
|
|
93
|
+
redirectUri,
|
|
94
|
+
returnTo: options.returnTo,
|
|
95
|
+
nonce,
|
|
96
|
+
createdAt: Date.now(),
|
|
97
|
+
};
|
|
98
|
+
sessionStorage.setItem(`${this.prefix}.txs.${state}`, JSON.stringify(tx));
|
|
99
|
+
const url = this.authorizeUrl({
|
|
100
|
+
state,
|
|
101
|
+
nonce,
|
|
102
|
+
codeChallenge: pkce.challenge,
|
|
103
|
+
codeChallengeMethod: "S256",
|
|
104
|
+
redirectUri,
|
|
105
|
+
scopes: options.scopes ?? this.cfg.scopes,
|
|
106
|
+
additionalParameters: options.additionalParameters,
|
|
107
|
+
});
|
|
108
|
+
window.location.assign(url);
|
|
109
|
+
}
|
|
110
|
+
// ── Callback ───────────────────────────────────────────────────────────────
|
|
111
|
+
/**
|
|
112
|
+
* Handle the OAuth2 redirect callback.
|
|
113
|
+
*
|
|
114
|
+
* Reads `code` and `state` from the URL (or the `url` argument),
|
|
115
|
+
* validates state against the pending transaction in sessionStorage,
|
|
116
|
+
* exchanges the code for tokens, verifies the access token, and
|
|
117
|
+
* stores the resulting session.
|
|
118
|
+
*
|
|
119
|
+
* Returns `{ principal, returnTo }` so the app can navigate to the
|
|
120
|
+
* original destination.
|
|
121
|
+
*/
|
|
122
|
+
async handleRedirectCallback(url) {
|
|
123
|
+
const searchString = url ? new URL(url).search : window.location.search;
|
|
124
|
+
const params = new URLSearchParams(searchString);
|
|
125
|
+
// Surface IDP-level errors (access_denied, etc.) before anything else
|
|
126
|
+
const errorParam = params.get("error");
|
|
127
|
+
if (errorParam) {
|
|
128
|
+
const desc = params.get("error_description") ?? errorParam;
|
|
129
|
+
throw idpError("oauth_callback_error", desc);
|
|
130
|
+
}
|
|
131
|
+
const code = params.get("code");
|
|
132
|
+
const state = params.get("state") ?? "";
|
|
133
|
+
if (!code) {
|
|
134
|
+
throw idpError("missing_code", "no authorization code found in the callback URL");
|
|
135
|
+
}
|
|
136
|
+
// Retrieve and remove the pending PKCE transaction
|
|
137
|
+
const txKey = `${this.prefix}.txs.${state}`;
|
|
138
|
+
const txRaw = sessionStorage.getItem(txKey);
|
|
139
|
+
if (!txRaw) {
|
|
140
|
+
throw idpError("invalid_state", "no pending auth transaction for this state value — possible CSRF or stale redirect");
|
|
141
|
+
}
|
|
142
|
+
const tx = JSON.parse(txRaw);
|
|
143
|
+
sessionStorage.removeItem(txKey);
|
|
144
|
+
// Exchange authorization code for token pair
|
|
145
|
+
const tokens = await this.exchangeCode({
|
|
146
|
+
code,
|
|
147
|
+
codeVerifier: tx.codeVerifier,
|
|
148
|
+
redirectUri: tx.redirectUri,
|
|
149
|
+
});
|
|
150
|
+
// Verify the access token using the browser-safe PASETO verifier
|
|
151
|
+
const principal = await this.verifyTokenBrowser(tokens.access_token);
|
|
152
|
+
const internal = {
|
|
153
|
+
principal,
|
|
154
|
+
accessToken: tokens.access_token,
|
|
155
|
+
refreshToken: tokens.refresh_token,
|
|
156
|
+
expiresAt: Date.now() + tokens.expires_in * 1000,
|
|
157
|
+
refreshExpiresAt: tokens.refresh_token_expires_at,
|
|
158
|
+
};
|
|
159
|
+
this.storeSession(internal);
|
|
160
|
+
return { principal, returnTo: tx.returnTo };
|
|
161
|
+
}
|
|
162
|
+
// ── Session access ─────────────────────────────────────────────────────────
|
|
163
|
+
/**
|
|
164
|
+
* Synchronously return the current session, or null if not authenticated.
|
|
165
|
+
*/
|
|
166
|
+
getSession() {
|
|
167
|
+
if (!this.session)
|
|
168
|
+
return null;
|
|
169
|
+
return {
|
|
170
|
+
principal: this.session.principal,
|
|
171
|
+
accessToken: this.session.accessToken,
|
|
172
|
+
expiresAt: this.session.expiresAt,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Synchronously return the current user (VerifiedPrincipal), or null.
|
|
177
|
+
*/
|
|
178
|
+
getUser() {
|
|
179
|
+
return this.session?.principal ?? null;
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Returns true if there is a session whose access token has not yet expired.
|
|
183
|
+
*/
|
|
184
|
+
isAuthenticated() {
|
|
185
|
+
return this.session !== null && this.session.expiresAt > Date.now();
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Return a live access token, transparently refreshing it if it is within
|
|
189
|
+
* REFRESH_THRESHOLD_MS (60 s) of expiry.
|
|
190
|
+
*
|
|
191
|
+
* Throws `not_authenticated` if there is no session at all.
|
|
192
|
+
* Throws `session_expired` if the refresh also fails.
|
|
193
|
+
*/
|
|
194
|
+
async getAccessToken(options = {}) {
|
|
195
|
+
if (!this.session) {
|
|
196
|
+
throw idpError("not_authenticated", "no active session — call loginWithRedirect() first");
|
|
197
|
+
}
|
|
198
|
+
if (this.session.expiresAt - Date.now() < REFRESH_THRESHOLD_MS) {
|
|
199
|
+
await this.refreshSession();
|
|
200
|
+
}
|
|
201
|
+
if (!this.session) {
|
|
202
|
+
throw idpError("session_expired", "session expired and could not be refreshed automatically");
|
|
203
|
+
}
|
|
204
|
+
if (options.requiredScope && !this.session.principal.scopes.includes(options.requiredScope)) {
|
|
205
|
+
throw idpError("insufficient_scope", `required scope '${options.requiredScope}' is not present in the session`);
|
|
206
|
+
}
|
|
207
|
+
return this.session.accessToken;
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Force a token refresh using the stored refresh token.
|
|
211
|
+
* Updates the in-memory (and optionally persisted) session.
|
|
212
|
+
*/
|
|
213
|
+
async refreshSession() {
|
|
214
|
+
if (!this.session?.refreshToken) {
|
|
215
|
+
throw idpError("not_authenticated", "no refresh token available");
|
|
216
|
+
}
|
|
217
|
+
try {
|
|
218
|
+
const tokens = await this.refresh({ refreshToken: this.session.refreshToken });
|
|
219
|
+
const principal = await this.verifyTokenBrowser(tokens.access_token);
|
|
220
|
+
const internal = {
|
|
221
|
+
principal,
|
|
222
|
+
accessToken: tokens.access_token,
|
|
223
|
+
refreshToken: tokens.refresh_token,
|
|
224
|
+
expiresAt: Date.now() + tokens.expires_in * 1000,
|
|
225
|
+
refreshExpiresAt: tokens.refresh_token_expires_at,
|
|
226
|
+
};
|
|
227
|
+
this.storeSession(internal, "refreshed");
|
|
228
|
+
return this.getSession();
|
|
229
|
+
}
|
|
230
|
+
catch {
|
|
231
|
+
this.clearSession();
|
|
232
|
+
throw idpError("refresh_failed", "token refresh failed — user must sign in again");
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
// ── Logout ─────────────────────────────────────────────────────────────────
|
|
236
|
+
/**
|
|
237
|
+
* Clear the session and optionally redirect to a post-logout URL.
|
|
238
|
+
*/
|
|
239
|
+
logout(options = {}) {
|
|
240
|
+
this.clearSession();
|
|
241
|
+
if (options.returnTo) {
|
|
242
|
+
window.location.assign(options.returnTo);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
// ── Session change events ──────────────────────────────────────────────────
|
|
246
|
+
/**
|
|
247
|
+
* Subscribe to session lifecycle events (`signed_in`, `signed_out`,
|
|
248
|
+
* `refreshed`, `expired`).
|
|
249
|
+
*
|
|
250
|
+
* Returns an unsubscribe function.
|
|
251
|
+
*
|
|
252
|
+
* @example
|
|
253
|
+
* const off = auth.onSessionChange(({ type, session }) => {
|
|
254
|
+
* if (type === "signed_out") router.push("/login");
|
|
255
|
+
* });
|
|
256
|
+
* // Later:
|
|
257
|
+
* off();
|
|
258
|
+
*/
|
|
259
|
+
onSessionChange(listener) {
|
|
260
|
+
this.listeners.add(listener);
|
|
261
|
+
return () => {
|
|
262
|
+
this.listeners.delete(listener);
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
// ── Private helpers ────────────────────────────────────────────────────────
|
|
266
|
+
async verifyTokenBrowser(token) {
|
|
267
|
+
await this.resolveConfig();
|
|
268
|
+
const keySet = await this.publicKeys();
|
|
269
|
+
return verifyPasetoV4PublicBrowser(token, keySet, {
|
|
270
|
+
issuer: this.cfg.issuer,
|
|
271
|
+
audience: this.cfg.audience,
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
storeSession(internal, eventType = "signed_in") {
|
|
275
|
+
this.session = internal;
|
|
276
|
+
if (this.persistSession) {
|
|
277
|
+
try {
|
|
278
|
+
localStorage.setItem(`${this.prefix}.session`, JSON.stringify(internal));
|
|
279
|
+
}
|
|
280
|
+
catch {
|
|
281
|
+
// Storage quota exceeded or private-browsing restriction — fail silently
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
this.scheduleRefresh();
|
|
285
|
+
this.emit({ type: eventType, session: this.toPublicSession(internal) });
|
|
286
|
+
}
|
|
287
|
+
clearSession() {
|
|
288
|
+
this.session = null;
|
|
289
|
+
if (this.persistSession) {
|
|
290
|
+
try {
|
|
291
|
+
localStorage.removeItem(`${this.prefix}.session`);
|
|
292
|
+
}
|
|
293
|
+
catch {
|
|
294
|
+
// Ignore
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
if (this.refreshTimer !== null) {
|
|
298
|
+
clearTimeout(this.refreshTimer);
|
|
299
|
+
this.refreshTimer = null;
|
|
300
|
+
}
|
|
301
|
+
this.emit({ type: "signed_out", session: null });
|
|
302
|
+
}
|
|
303
|
+
loadPersistedSession() {
|
|
304
|
+
if (!this.persistSession)
|
|
305
|
+
return null;
|
|
306
|
+
try {
|
|
307
|
+
const raw = localStorage.getItem(`${this.prefix}.session`);
|
|
308
|
+
if (!raw)
|
|
309
|
+
return null;
|
|
310
|
+
const parsed = JSON.parse(raw);
|
|
311
|
+
// Accept even expired sessions so refreshSession() can still run
|
|
312
|
+
if (!parsed.accessToken || !parsed.refreshToken || !parsed.principal)
|
|
313
|
+
return null;
|
|
314
|
+
return parsed;
|
|
315
|
+
}
|
|
316
|
+
catch {
|
|
317
|
+
return null;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
scheduleRefresh() {
|
|
321
|
+
if (this.refreshTimer !== null) {
|
|
322
|
+
clearTimeout(this.refreshTimer);
|
|
323
|
+
this.refreshTimer = null;
|
|
324
|
+
}
|
|
325
|
+
if (!this.session)
|
|
326
|
+
return;
|
|
327
|
+
const msUntilExpiry = this.session.expiresAt - Date.now();
|
|
328
|
+
const delay = Math.max(0, msUntilExpiry - REFRESH_THRESHOLD_MS);
|
|
329
|
+
this.refreshTimer = setTimeout(() => {
|
|
330
|
+
this.refreshSession().catch(() => {
|
|
331
|
+
this.emit({ type: "expired", session: null });
|
|
332
|
+
});
|
|
333
|
+
}, delay);
|
|
334
|
+
}
|
|
335
|
+
toPublicSession(internal) {
|
|
336
|
+
return {
|
|
337
|
+
principal: internal.principal,
|
|
338
|
+
accessToken: internal.accessToken,
|
|
339
|
+
expiresAt: internal.expiresAt,
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
emit(event) {
|
|
343
|
+
for (const listener of this.listeners) {
|
|
344
|
+
try {
|
|
345
|
+
listener(event);
|
|
346
|
+
}
|
|
347
|
+
catch {
|
|
348
|
+
// Never let a subscriber error crash the auth flow
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* Create a browser auth client from config.
|
|
355
|
+
*
|
|
356
|
+
* @example
|
|
357
|
+
* // Vite / Vanilla browser app
|
|
358
|
+
* const auth = createBrowserBaseIdpAuth({
|
|
359
|
+
* key: import.meta.env.VITE_BASE_IDP_KEY,
|
|
360
|
+
* issuer: import.meta.env.VITE_BASE_IDP_ISSUER,
|
|
361
|
+
* persistSession: true,
|
|
362
|
+
* });
|
|
363
|
+
*
|
|
364
|
+
* // On app load
|
|
365
|
+
* await auth.init();
|
|
366
|
+
*
|
|
367
|
+
* // Start login
|
|
368
|
+
* await auth.loginWithRedirect({ returnTo: "/dashboard" });
|
|
369
|
+
*
|
|
370
|
+
* // On the callback page
|
|
371
|
+
* const { principal, returnTo } = await auth.handleRedirectCallback();
|
|
372
|
+
*/
|
|
373
|
+
export function createBrowserBaseIdpAuth(config) {
|
|
374
|
+
return new BrowserBaseIdpClient(config);
|
|
375
|
+
}
|
|
376
|
+
// ── Private utils ──────────────────────────────────────────────────────────
|
|
377
|
+
function generateRandom(bytes) {
|
|
378
|
+
const arr = new Uint8Array(bytes);
|
|
379
|
+
crypto.getRandomValues(arr);
|
|
380
|
+
return Array.from(arr, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
381
|
+
}
|
|
382
|
+
//# sourceMappingURL=browser.js.map
|