@logi-auth/browser 0.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/README.md +120 -0
- package/dist/index.d.ts +82 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +223 -0
- package/dist/index.js.map +1 -0
- package/dist/pkce.d.ts +4 -0
- package/dist/pkce.d.ts.map +1 -0
- package/dist/pkce.js +27 -0
- package/dist/pkce.js.map +1 -0
- package/dist/storage.d.ts +19 -0
- package/dist/storage.d.ts.map +1 -0
- package/dist/storage.js +48 -0
- package/dist/storage.js.map +1 -0
- package/package.json +57 -0
package/README.md
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# @logi-auth/browser
|
|
2
|
+
|
|
3
|
+
Browser SDK for **logi (1pass)** — OAuth 2.0 + OIDC PKCE for SPAs. Zero dependencies, ~3 KB minified.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npm install @logi-auth/browser
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## Quickstart
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import { LogiAuth } from "@logi-auth/browser";
|
|
13
|
+
|
|
14
|
+
const auth = new LogiAuth({
|
|
15
|
+
clientId: "logi_xxx",
|
|
16
|
+
redirectUri: window.location.origin + "/auth/callback",
|
|
17
|
+
// scopes: ["openid", "profile:basic", "email"], // default
|
|
18
|
+
// issuer: "https://api.1pass.dev", // default
|
|
19
|
+
});
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
### Sign in (page A — wherever the login button lives)
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
loginButton.addEventListener("click", () => {
|
|
26
|
+
auth.signIn({ returnTo: location.pathname });
|
|
27
|
+
// → redirects to https://api.1pass.dev/oauth/authorize
|
|
28
|
+
});
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### Handle callback (page B — `/auth/callback`)
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
if (auth.hasPendingCallback()) {
|
|
35
|
+
try {
|
|
36
|
+
const tokens = await auth.handleCallback();
|
|
37
|
+
// tokens.accessToken — Bearer token for your API
|
|
38
|
+
// tokens.refreshToken — store securely (preferably HttpOnly cookie via your backend)
|
|
39
|
+
// tokens.idToken — OIDC identity (decode for UI hints only)
|
|
40
|
+
// tokens.returnTo — what you passed to signIn({ returnTo })
|
|
41
|
+
// tokens.expiresAt — ms epoch
|
|
42
|
+
location.replace(tokens.returnTo ?? "/");
|
|
43
|
+
} catch (err) {
|
|
44
|
+
if (err instanceof LogiAuthError) {
|
|
45
|
+
console.error(err.code, err.message, err.details);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Refresh
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
const fresh = await auth.refresh(savedRefreshToken);
|
|
55
|
+
// fresh.refreshToken is the rotated token — persist the new value.
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Read the ID token (UI hints only)
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
const claims = auth.parseIdToken<{ sub: string; email?: string }>(tokens.idToken!);
|
|
62
|
+
// ⚠️ No signature verification. Don't make authorization decisions client-side.
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Why this SDK
|
|
66
|
+
|
|
67
|
+
Browser PKCE flow is small but easy to get wrong:
|
|
68
|
+
- Generating the `code_verifier` + SHA-256 `code_challenge`
|
|
69
|
+
- Persisting `verifier` + `state` across the IdP redirect
|
|
70
|
+
- Validating returned `state` to defeat CSRF
|
|
71
|
+
- Distinguishing `error=` callbacks from missing-`code` cases
|
|
72
|
+
- Cleaning up `sessionStorage` on every exit path (success or failure)
|
|
73
|
+
|
|
74
|
+
This SDK does all of that in ~250 LOC, zero deps, ESM-only.
|
|
75
|
+
|
|
76
|
+
## Design
|
|
77
|
+
|
|
78
|
+
- **Zero dependencies.** Uses `crypto.subtle` and `fetch` directly.
|
|
79
|
+
- **Public client.** Never sends `client_secret`. Token endpoint must accept `none` auth (logi PKCE clients do).
|
|
80
|
+
- **No signature verification.** ID token claims are decoded for UI only; your backend is the trust root and re-verifies via `/.well-known/jwks.json`.
|
|
81
|
+
- **sessionStorage by default.** Pending handoff is wiped on tab close. Override via `storage:` option.
|
|
82
|
+
- **TTL on pending handoff.** Stale handoffs (default 10 min) are rejected with `expired_handoff`.
|
|
83
|
+
|
|
84
|
+
## Requirements
|
|
85
|
+
|
|
86
|
+
- **Secure context.** `crypto.subtle` is undefined on plain `http://` (except `http://localhost`). Serve your SPA over HTTPS.
|
|
87
|
+
- **Modern browsers.** Chromium 92+, Safari 15.4+, Firefox 90+ (anything with `crypto.subtle.digest("SHA-256")` and `fetch`).
|
|
88
|
+
- **Node ≥ 18** if you import this from a server-side test harness or SSR layer.
|
|
89
|
+
|
|
90
|
+
## Errors
|
|
91
|
+
|
|
92
|
+
`LogiAuthError` with one of:
|
|
93
|
+
|
|
94
|
+
- `storage_unavailable` — `signIn()` couldn't persist the PKCE handoff (Safari ITP, iOS private browsing, corp policy). Thrown **before** redirecting to the IdP so the user doesn't waste a round-trip.
|
|
95
|
+
- `no_pending_handoff` — `handleCallback()` called without a prior `signIn()` in this tab
|
|
96
|
+
- `state_mismatch` — returned `state` ≠ persisted (CSRF attempt or stale callback)
|
|
97
|
+
- `missing_code` — callback URL had no `code` parameter
|
|
98
|
+
- `authorization_server_error` — IdP returned `?error=...`
|
|
99
|
+
- `token_exchange_failed` — `/oauth/token` POST failed (HTTP status + truncated body in `details`)
|
|
100
|
+
- `network_error` — `fetch` rejected (offline, DNS, CORS, TLS)
|
|
101
|
+
- `expired_handoff` — pending older than `pendingTtlMs`
|
|
102
|
+
|
|
103
|
+
> **`details.body` may include server payloads.** We truncate to 2 KB but logging it to Sentry/Datadog without scrubbing could leak tokens that the IdP echoed in a 4xx response.
|
|
104
|
+
|
|
105
|
+
## Limitations (v0.1.0)
|
|
106
|
+
|
|
107
|
+
- **Multi-tab race.** Concurrent `signIn()` calls in multiple tabs share `sessionStorage` per origin, so only the most-recent handoff completes; the older tab's `handleCallback()` will fail with `state_mismatch`. State-keyed storage is on the v0.2.0 roadmap.
|
|
108
|
+
- **No automatic refresh.** Call `auth.refresh(savedRefreshToken)` yourself before `expiresAt`. A token-manager wrapper (`@logi-auth/react`) is planned.
|
|
109
|
+
|
|
110
|
+
## Server side
|
|
111
|
+
|
|
112
|
+
This SDK only handles the browser. Your backend should:
|
|
113
|
+
1. Validate `accessToken` against `/.well-known/jwks.json` on every protected request
|
|
114
|
+
2. Store `refreshToken` server-side (HttpOnly cookie) — don't keep it in `localStorage`
|
|
115
|
+
|
|
116
|
+
For Node.js servers, use a generic OIDC library pointed at `https://api.1pass.dev/.well-known/openid-configuration`. logi advertises a full discovery document so `oauth4webapi`, `openid-client`, `next-auth`, `auth.js` all auto-configure.
|
|
117
|
+
|
|
118
|
+
## License
|
|
119
|
+
|
|
120
|
+
MIT © Seunghan Kim
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { type StorageBackend } from "./storage.js";
|
|
2
|
+
export interface LogiAuthOptions {
|
|
3
|
+
/** OAuth client_id from logi developer portal. Public PKCE client. */
|
|
4
|
+
clientId: string;
|
|
5
|
+
/** Where the IdP returns the user. Must be one of the registered redirect_uris. */
|
|
6
|
+
redirectUri: string;
|
|
7
|
+
/** Default scopes; override per-call via signIn({ scopes }). */
|
|
8
|
+
scopes?: string[];
|
|
9
|
+
/** Issuer URL. Defaults to https://api.1pass.dev. */
|
|
10
|
+
issuer?: string;
|
|
11
|
+
/** Override storage backend (default: sessionStorage). */
|
|
12
|
+
storage?: StorageBackend;
|
|
13
|
+
/** Maximum age of a pending handoff in ms (default: 10 minutes). */
|
|
14
|
+
pendingTtlMs?: number;
|
|
15
|
+
}
|
|
16
|
+
export interface SignInRequest {
|
|
17
|
+
/** Override default scopes. */
|
|
18
|
+
scopes?: string[];
|
|
19
|
+
/** Caller-supplied passthrough — restored in handleCallback().returnTo. */
|
|
20
|
+
returnTo?: string;
|
|
21
|
+
/** OIDC `prompt` parameter (e.g. "login" or "consent"). */
|
|
22
|
+
prompt?: "none" | "login" | "consent" | "select_account";
|
|
23
|
+
}
|
|
24
|
+
export interface TokenResponse {
|
|
25
|
+
accessToken: string;
|
|
26
|
+
idToken?: string;
|
|
27
|
+
refreshToken?: string;
|
|
28
|
+
tokenType: string;
|
|
29
|
+
expiresAt?: number;
|
|
30
|
+
scope?: string;
|
|
31
|
+
}
|
|
32
|
+
export interface CallbackResult extends TokenResponse {
|
|
33
|
+
/** The `returnTo` value passed to signIn(), if any. */
|
|
34
|
+
returnTo?: string;
|
|
35
|
+
}
|
|
36
|
+
export type LogiAuthErrorCode = "no_pending_handoff" | "state_mismatch" | "missing_code" | "authorization_server_error" | "token_exchange_failed" | "expired_handoff" | "storage_unavailable" | "network_error";
|
|
37
|
+
export declare class LogiAuthError extends Error {
|
|
38
|
+
readonly code: LogiAuthErrorCode;
|
|
39
|
+
readonly details?: unknown | undefined;
|
|
40
|
+
constructor(code: LogiAuthErrorCode, message: string, details?: unknown | undefined);
|
|
41
|
+
}
|
|
42
|
+
export declare class LogiAuth {
|
|
43
|
+
readonly issuer: string;
|
|
44
|
+
readonly clientId: string;
|
|
45
|
+
readonly redirectUri: string;
|
|
46
|
+
readonly defaultScopes: string[];
|
|
47
|
+
private readonly storage;
|
|
48
|
+
private readonly pendingTtlMs;
|
|
49
|
+
constructor(opts: LogiAuthOptions);
|
|
50
|
+
/**
|
|
51
|
+
* Build the authorize URL and navigate the browser to it. Persists the PKCE
|
|
52
|
+
* verifier + state to sessionStorage so handleCallback() can complete the
|
|
53
|
+
* exchange after the IdP redirects back.
|
|
54
|
+
*/
|
|
55
|
+
signIn(req?: SignInRequest): Promise<void>;
|
|
56
|
+
/**
|
|
57
|
+
* Read the current page URL for ?code & ?state, validate against the
|
|
58
|
+
* persisted handoff, and exchange the code for tokens. Call this from your
|
|
59
|
+
* `/auth/callback` route.
|
|
60
|
+
*
|
|
61
|
+
* Pass an explicit URL if you've already routed past the callback (rare).
|
|
62
|
+
*/
|
|
63
|
+
handleCallback(callbackUrl?: string | URL): Promise<CallbackResult>;
|
|
64
|
+
/**
|
|
65
|
+
* Exchange a refresh_token for a fresh access_token. Public clients should
|
|
66
|
+
* persist the rotated refresh_token returned in `refreshToken`.
|
|
67
|
+
*/
|
|
68
|
+
refresh(refreshToken: string): Promise<TokenResponse>;
|
|
69
|
+
/**
|
|
70
|
+
* Decode an ID token's payload (no signature verification). Use only for
|
|
71
|
+
* UI hints (e.g. show user's email). Real authorization decisions must be
|
|
72
|
+
* made server-side after re-verifying via JWKS.
|
|
73
|
+
*/
|
|
74
|
+
parseIdToken<T = Record<string, unknown>>(idToken: string): T;
|
|
75
|
+
/**
|
|
76
|
+
* True when sessionStorage has a pending sign-in (i.e. user just returned
|
|
77
|
+
* from the IdP). Use to gate calling handleCallback() in shared components.
|
|
78
|
+
*/
|
|
79
|
+
hasPendingCallback(): boolean;
|
|
80
|
+
}
|
|
81
|
+
export type { StorageBackend, PendingHandoff } from "./storage.js";
|
|
82
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAmBA,OAAO,EACL,KAAK,cAAc,EAKpB,MAAM,cAAc,CAAC;AAEtB,MAAM,WAAW,eAAe;IAC9B,sEAAsE;IACtE,QAAQ,EAAE,MAAM,CAAC;IACjB,mFAAmF;IACnF,WAAW,EAAE,MAAM,CAAC;IACpB,gEAAgE;IAChE,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,qDAAqD;IACrD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,0DAA0D;IAC1D,OAAO,CAAC,EAAE,cAAc,CAAC;IACzB,oEAAoE;IACpE,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,aAAa;IAC5B,+BAA+B;IAC/B,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,2EAA2E;IAC3E,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,2DAA2D;IAC3D,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,GAAG,SAAS,GAAG,gBAAgB,CAAC;CAC1D;AAED,MAAM,WAAW,aAAa;IAC5B,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,cAAe,SAAQ,aAAa;IACnD,uDAAuD;IACvD,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,MAAM,iBAAiB,GACzB,oBAAoB,GACpB,gBAAgB,GAChB,cAAc,GACd,4BAA4B,GAC5B,uBAAuB,GACvB,iBAAiB,GACjB,qBAAqB,GACrB,eAAe,CAAC;AAEpB,qBAAa,aAAc,SAAQ,KAAK;aAEpB,IAAI,EAAE,iBAAiB;aAEvB,OAAO,CAAC,EAAE,OAAO;gBAFjB,IAAI,EAAE,iBAAiB,EACvC,OAAO,EAAE,MAAM,EACC,OAAO,CAAC,EAAE,OAAO,YAAA;CAKpC;AAED,qBAAa,QAAQ;IACnB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,aAAa,EAAE,MAAM,EAAE,CAAC;IACjC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAiB;IACzC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAS;gBAE1B,IAAI,EAAE,eAAe;IAWjC;;;;OAIG;IACG,MAAM,CAAC,GAAG,GAAE,aAAkB,GAAG,OAAO,CAAC,IAAI,CAAC;IA0CpD;;;;;;OAMG;IACG,cAAc,CAAC,WAAW,CAAC,EAAE,MAAM,GAAG,GAAG,GAAG,OAAO,CAAC,cAAc,CAAC;IAqGzE;;;OAGG;IACG,OAAO,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC;IA2C3D;;;;OAIG;IACH,YAAY,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,OAAO,EAAE,MAAM,GAAG,CAAC;IAa7D;;;OAGG;IACH,kBAAkB,IAAI,OAAO;CAG9B;AAED,YAAY,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
// @logi-auth/browser — OAuth 2.0 + OIDC PKCE client for SPAs.
|
|
2
|
+
//
|
|
3
|
+
// Usage:
|
|
4
|
+
// const auth = new LogiAuth({
|
|
5
|
+
// clientId: 'logi_xxx',
|
|
6
|
+
// redirectUri: window.location.origin + '/auth/callback',
|
|
7
|
+
// });
|
|
8
|
+
//
|
|
9
|
+
// // Page A — kick off
|
|
10
|
+
// await auth.signIn();
|
|
11
|
+
//
|
|
12
|
+
// // Page B (callback) — finish
|
|
13
|
+
// const tokens = await auth.handleCallback();
|
|
14
|
+
//
|
|
15
|
+
// // Server validates id_token via /.well-known/jwks.json — this SDK does
|
|
16
|
+
// // NOT verify signatures (browsers can't keep the public-key cache safe
|
|
17
|
+
// // and the RP backend should be the trust root anyway).
|
|
18
|
+
import { generateCodeVerifier, deriveCodeChallenge, generateState } from "./pkce.js";
|
|
19
|
+
import { sessionStorageBackend, savePending, loadPending, clearPending, } from "./storage.js";
|
|
20
|
+
export class LogiAuthError extends Error {
|
|
21
|
+
code;
|
|
22
|
+
details;
|
|
23
|
+
constructor(code, message, details) {
|
|
24
|
+
super(message);
|
|
25
|
+
this.code = code;
|
|
26
|
+
this.details = details;
|
|
27
|
+
this.name = "LogiAuthError";
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
export class LogiAuth {
|
|
31
|
+
issuer;
|
|
32
|
+
clientId;
|
|
33
|
+
redirectUri;
|
|
34
|
+
defaultScopes;
|
|
35
|
+
storage;
|
|
36
|
+
pendingTtlMs;
|
|
37
|
+
constructor(opts) {
|
|
38
|
+
if (!opts.clientId)
|
|
39
|
+
throw new Error("LogiAuth: clientId is required");
|
|
40
|
+
if (!opts.redirectUri)
|
|
41
|
+
throw new Error("LogiAuth: redirectUri is required");
|
|
42
|
+
this.clientId = opts.clientId;
|
|
43
|
+
this.redirectUri = opts.redirectUri;
|
|
44
|
+
this.issuer = (opts.issuer ?? "https://api.1pass.dev").replace(/\/+$/, "");
|
|
45
|
+
this.defaultScopes = opts.scopes ?? ["openid", "profile:basic", "email"];
|
|
46
|
+
this.storage = opts.storage ?? sessionStorageBackend;
|
|
47
|
+
this.pendingTtlMs = opts.pendingTtlMs ?? 10 * 60 * 1000;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Build the authorize URL and navigate the browser to it. Persists the PKCE
|
|
51
|
+
* verifier + state to sessionStorage so handleCallback() can complete the
|
|
52
|
+
* exchange after the IdP redirects back.
|
|
53
|
+
*/
|
|
54
|
+
async signIn(req = {}) {
|
|
55
|
+
const verifier = generateCodeVerifier();
|
|
56
|
+
const challenge = await deriveCodeChallenge(verifier);
|
|
57
|
+
const state = generateState();
|
|
58
|
+
const scopes = (req.scopes ?? this.defaultScopes).join(" ");
|
|
59
|
+
// Persist BEFORE navigating. If sessionStorage is disabled (Safari ITP,
|
|
60
|
+
// iOS private browsing, corp policy), throw a typed error instead of
|
|
61
|
+
// redirecting the user into a flow that can't complete (codex P2
|
|
62
|
+
// 2026-05-15).
|
|
63
|
+
try {
|
|
64
|
+
savePending({
|
|
65
|
+
state,
|
|
66
|
+
verifier,
|
|
67
|
+
redirectUri: this.redirectUri,
|
|
68
|
+
returnTo: req.returnTo,
|
|
69
|
+
startedAt: Date.now(),
|
|
70
|
+
}, this.storage);
|
|
71
|
+
}
|
|
72
|
+
catch (cause) {
|
|
73
|
+
throw new LogiAuthError("storage_unavailable", "Could not persist PKCE handoff to sessionStorage (private browsing, ITP, or corp policy).", cause);
|
|
74
|
+
}
|
|
75
|
+
const url = new URL(`${this.issuer}/oauth/authorize`);
|
|
76
|
+
url.searchParams.set("response_type", "code");
|
|
77
|
+
url.searchParams.set("client_id", this.clientId);
|
|
78
|
+
url.searchParams.set("redirect_uri", this.redirectUri);
|
|
79
|
+
url.searchParams.set("scope", scopes);
|
|
80
|
+
url.searchParams.set("state", state);
|
|
81
|
+
url.searchParams.set("code_challenge", challenge);
|
|
82
|
+
url.searchParams.set("code_challenge_method", "S256");
|
|
83
|
+
if (req.prompt)
|
|
84
|
+
url.searchParams.set("prompt", req.prompt);
|
|
85
|
+
window.location.assign(url.toString());
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Read the current page URL for ?code & ?state, validate against the
|
|
89
|
+
* persisted handoff, and exchange the code for tokens. Call this from your
|
|
90
|
+
* `/auth/callback` route.
|
|
91
|
+
*
|
|
92
|
+
* Pass an explicit URL if you've already routed past the callback (rare).
|
|
93
|
+
*/
|
|
94
|
+
async handleCallback(callbackUrl) {
|
|
95
|
+
const url = new URL(callbackUrl ?? (typeof window !== "undefined" ? window.location.href : "http://localhost/"));
|
|
96
|
+
const params = url.searchParams;
|
|
97
|
+
const pending = loadPending(this.storage);
|
|
98
|
+
if (!pending) {
|
|
99
|
+
throw new LogiAuthError("no_pending_handoff", "No pending sign-in handoff in sessionStorage. Did you call signIn() in this tab?");
|
|
100
|
+
}
|
|
101
|
+
if (Date.now() - pending.startedAt > this.pendingTtlMs) {
|
|
102
|
+
clearPending(this.storage);
|
|
103
|
+
throw new LogiAuthError("expired_handoff", `Pending handoff older than ${this.pendingTtlMs}ms — the user took too long. Restart sign-in.`);
|
|
104
|
+
}
|
|
105
|
+
const errParam = params.get("error");
|
|
106
|
+
if (errParam) {
|
|
107
|
+
clearPending(this.storage);
|
|
108
|
+
throw new LogiAuthError("authorization_server_error", `Authorization server returned error: ${errParam}`, { error: errParam, errorDescription: params.get("error_description") });
|
|
109
|
+
}
|
|
110
|
+
const returnedState = params.get("state");
|
|
111
|
+
if (returnedState !== pending.state) {
|
|
112
|
+
clearPending(this.storage);
|
|
113
|
+
throw new LogiAuthError("state_mismatch", "state parameter mismatch — possible CSRF attempt or stale callback.");
|
|
114
|
+
}
|
|
115
|
+
const code = params.get("code");
|
|
116
|
+
if (!code) {
|
|
117
|
+
clearPending(this.storage);
|
|
118
|
+
throw new LogiAuthError("missing_code", "Callback URL had no `code` parameter.");
|
|
119
|
+
}
|
|
120
|
+
// PKCE token exchange. Public client → no client_secret.
|
|
121
|
+
let tokenResp;
|
|
122
|
+
try {
|
|
123
|
+
tokenResp = await fetch(`${this.issuer}/oauth/token`, {
|
|
124
|
+
method: "POST",
|
|
125
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
126
|
+
body: new URLSearchParams({
|
|
127
|
+
grant_type: "authorization_code",
|
|
128
|
+
code,
|
|
129
|
+
redirect_uri: pending.redirectUri,
|
|
130
|
+
client_id: this.clientId,
|
|
131
|
+
code_verifier: pending.verifier,
|
|
132
|
+
}).toString(),
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
catch (cause) {
|
|
136
|
+
clearPending(this.storage);
|
|
137
|
+
throw new LogiAuthError("network_error", "Network error during token exchange (offline, DNS, CORS, or TLS failure).", cause);
|
|
138
|
+
}
|
|
139
|
+
clearPending(this.storage);
|
|
140
|
+
if (!tokenResp.ok) {
|
|
141
|
+
// Truncate body to 2 KB — IdPs occasionally echo request params on
|
|
142
|
+
// 4xx, and consumers shouldn't blindly log multi-MB payloads to Sentry.
|
|
143
|
+
const rawBody = await tokenResp.text();
|
|
144
|
+
const body = rawBody.length > 2048 ? rawBody.slice(0, 2048) + "…[truncated]" : rawBody;
|
|
145
|
+
throw new LogiAuthError("token_exchange_failed", `Token exchange failed: HTTP ${tokenResp.status}`, { status: tokenResp.status, body });
|
|
146
|
+
}
|
|
147
|
+
const tokens = await tokenResp.json();
|
|
148
|
+
return {
|
|
149
|
+
accessToken: tokens.access_token,
|
|
150
|
+
idToken: tokens.id_token,
|
|
151
|
+
refreshToken: tokens.refresh_token,
|
|
152
|
+
tokenType: tokens.token_type ?? "Bearer",
|
|
153
|
+
expiresAt: tokens.expires_in
|
|
154
|
+
? Date.now() + Number(tokens.expires_in) * 1000
|
|
155
|
+
: undefined,
|
|
156
|
+
scope: tokens.scope,
|
|
157
|
+
returnTo: pending.returnTo,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Exchange a refresh_token for a fresh access_token. Public clients should
|
|
162
|
+
* persist the rotated refresh_token returned in `refreshToken`.
|
|
163
|
+
*/
|
|
164
|
+
async refresh(refreshToken) {
|
|
165
|
+
let resp;
|
|
166
|
+
try {
|
|
167
|
+
resp = await fetch(`${this.issuer}/oauth/token`, {
|
|
168
|
+
method: "POST",
|
|
169
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
170
|
+
body: new URLSearchParams({
|
|
171
|
+
grant_type: "refresh_token",
|
|
172
|
+
refresh_token: refreshToken,
|
|
173
|
+
client_id: this.clientId,
|
|
174
|
+
}).toString(),
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
catch (cause) {
|
|
178
|
+
throw new LogiAuthError("network_error", "Network error during refresh (offline, DNS, CORS, or TLS failure).", cause);
|
|
179
|
+
}
|
|
180
|
+
if (!resp.ok) {
|
|
181
|
+
const rawBody = await resp.text();
|
|
182
|
+
const body = rawBody.length > 2048 ? rawBody.slice(0, 2048) + "…[truncated]" : rawBody;
|
|
183
|
+
throw new LogiAuthError("token_exchange_failed", `Refresh failed: HTTP ${resp.status}`, { status: resp.status, body });
|
|
184
|
+
}
|
|
185
|
+
const tokens = await resp.json();
|
|
186
|
+
return {
|
|
187
|
+
accessToken: tokens.access_token,
|
|
188
|
+
idToken: tokens.id_token,
|
|
189
|
+
refreshToken: tokens.refresh_token,
|
|
190
|
+
tokenType: tokens.token_type ?? "Bearer",
|
|
191
|
+
expiresAt: tokens.expires_in
|
|
192
|
+
? Date.now() + Number(tokens.expires_in) * 1000
|
|
193
|
+
: undefined,
|
|
194
|
+
scope: tokens.scope,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Decode an ID token's payload (no signature verification). Use only for
|
|
199
|
+
* UI hints (e.g. show user's email). Real authorization decisions must be
|
|
200
|
+
* made server-side after re-verifying via JWKS.
|
|
201
|
+
*/
|
|
202
|
+
parseIdToken(idToken) {
|
|
203
|
+
const [, payload] = idToken.split(".");
|
|
204
|
+
if (!payload)
|
|
205
|
+
throw new Error("Invalid id_token: missing payload");
|
|
206
|
+
const b64 = payload.replace(/-/g, "+").replace(/_/g, "/");
|
|
207
|
+
const padded = b64.padEnd(b64.length + ((4 - (b64.length % 4)) % 4), "=");
|
|
208
|
+
// Decode as UTF-8, not Latin-1 — Korean / Japanese / emoji claim values
|
|
209
|
+
// round-trip correctly. atob() returns a binary string interpreted as
|
|
210
|
+
// UTF-16 by JSON.parse, which mojibakes any non-ASCII (codex P2 fix).
|
|
211
|
+
const bytes = Uint8Array.from(atob(padded), (c) => c.charCodeAt(0));
|
|
212
|
+
const json = new TextDecoder("utf-8").decode(bytes);
|
|
213
|
+
return JSON.parse(json);
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* True when sessionStorage has a pending sign-in (i.e. user just returned
|
|
217
|
+
* from the IdP). Use to gate calling handleCallback() in shared components.
|
|
218
|
+
*/
|
|
219
|
+
hasPendingCallback() {
|
|
220
|
+
return loadPending(this.storage) !== null;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/pkce.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pkce.d.ts","sourceRoot":"","sources":["../src/pkce.ts"],"names":[],"mappings":"AAWA,wBAAgB,oBAAoB,CAAC,UAAU,SAAK,GAAG,MAAM,CAI5D;AAED,wBAAsB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAI3E;AAED,wBAAgB,aAAa,IAAI,MAAM,CAKtC"}
|
package/dist/pkce.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// PKCE helpers (RFC 7636) — browser-only, uses crypto.subtle.
|
|
2
|
+
//
|
|
3
|
+
// `code_verifier`: 43–128 char URL-safe random string.
|
|
4
|
+
// `code_challenge`: BASE64URL(SHA256(verifier)), 43 chars unpadded.
|
|
5
|
+
function base64url(bytes) {
|
|
6
|
+
let bin = "";
|
|
7
|
+
for (const b of bytes)
|
|
8
|
+
bin += String.fromCharCode(b);
|
|
9
|
+
return btoa(bin).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
10
|
+
}
|
|
11
|
+
export function generateCodeVerifier(byteLength = 48) {
|
|
12
|
+
const bytes = new Uint8Array(byteLength);
|
|
13
|
+
crypto.getRandomValues(bytes);
|
|
14
|
+
return base64url(bytes);
|
|
15
|
+
}
|
|
16
|
+
export async function deriveCodeChallenge(verifier) {
|
|
17
|
+
const data = new TextEncoder().encode(verifier);
|
|
18
|
+
const digest = await crypto.subtle.digest("SHA-256", data);
|
|
19
|
+
return base64url(new Uint8Array(digest));
|
|
20
|
+
}
|
|
21
|
+
export function generateState() {
|
|
22
|
+
// Random opaque value to defeat CSRF on the callback. 16 bytes → 22 chars.
|
|
23
|
+
const bytes = new Uint8Array(16);
|
|
24
|
+
crypto.getRandomValues(bytes);
|
|
25
|
+
return base64url(bytes);
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=pkce.js.map
|
package/dist/pkce.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pkce.js","sourceRoot":"","sources":["../src/pkce.ts"],"names":[],"mappings":"AAAA,8DAA8D;AAC9D,EAAE;AACF,uDAAuD;AACvD,oEAAoE;AAEpE,SAAS,SAAS,CAAC,KAAiB;IAClC,IAAI,GAAG,GAAG,EAAE,CAAC;IACb,KAAK,MAAM,CAAC,IAAI,KAAK;QAAE,GAAG,IAAI,MAAM,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;IACrD,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;AAC9E,CAAC;AAED,MAAM,UAAU,oBAAoB,CAAC,UAAU,GAAG,EAAE;IAClD,MAAM,KAAK,GAAG,IAAI,UAAU,CAAC,UAAU,CAAC,CAAC;IACzC,MAAM,CAAC,eAAe,CAAC,KAAK,CAAC,CAAC;IAC9B,OAAO,SAAS,CAAC,KAAK,CAAC,CAAC;AAC1B,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,mBAAmB,CAAC,QAAgB;IACxD,MAAM,IAAI,GAAG,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;IAChD,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,SAAS,EAAE,IAAI,CAAC,CAAC;IAC3D,OAAO,SAAS,CAAC,IAAI,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC;AAC3C,CAAC;AAED,MAAM,UAAU,aAAa;IAC3B,2EAA2E;IAC3E,MAAM,KAAK,GAAG,IAAI,UAAU,CAAC,EAAE,CAAC,CAAC;IACjC,MAAM,CAAC,eAAe,CAAC,KAAK,CAAC,CAAC;IAC9B,OAAO,SAAS,CAAC,KAAK,CAAC,CAAC;AAC1B,CAAC"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export interface PendingHandoff {
|
|
2
|
+
state: string;
|
|
3
|
+
verifier: string;
|
|
4
|
+
redirectUri: string;
|
|
5
|
+
/** Optional caller-supplied passthrough (e.g. UI route to restore). */
|
|
6
|
+
returnTo?: string;
|
|
7
|
+
/** ms epoch — used to expire stale handoffs. */
|
|
8
|
+
startedAt: number;
|
|
9
|
+
}
|
|
10
|
+
export interface StorageBackend {
|
|
11
|
+
get(key: string): string | null;
|
|
12
|
+
set(key: string, value: string): void;
|
|
13
|
+
remove(key: string): void;
|
|
14
|
+
}
|
|
15
|
+
export declare const sessionStorageBackend: StorageBackend;
|
|
16
|
+
export declare function savePending(p: PendingHandoff, backend: StorageBackend): void;
|
|
17
|
+
export declare function loadPending(backend: StorageBackend): PendingHandoff | null;
|
|
18
|
+
export declare function clearPending(backend: StorageBackend): void;
|
|
19
|
+
//# sourceMappingURL=storage.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"storage.d.ts","sourceRoot":"","sources":["../src/storage.ts"],"names":[],"mappings":"AAMA,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,uEAAuE;IACvE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,gDAAgD;IAChD,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,cAAc;IAC7B,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;IAChC,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACtC,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B;AAED,eAAO,MAAM,qBAAqB,EAAE,cAuBnC,CAAC;AAEF,wBAAgB,WAAW,CAAC,CAAC,EAAE,cAAc,EAAE,OAAO,EAAE,cAAc,GAAG,IAAI,CAE5E;AAED,wBAAgB,WAAW,CAAC,OAAO,EAAE,cAAc,GAAG,cAAc,GAAG,IAAI,CAQ1E;AAED,wBAAgB,YAAY,CAAC,OAAO,EAAE,cAAc,GAAG,IAAI,CAE1D"}
|
package/dist/storage.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// Per-handoff storage for the PKCE verifier + CSRF state. Kept in
|
|
2
|
+
// sessionStorage so it survives the IdP redirect round-trip but is wiped on
|
|
3
|
+
// tab close.
|
|
4
|
+
const KEY = "logi-auth.pending";
|
|
5
|
+
export const sessionStorageBackend = {
|
|
6
|
+
get(key) {
|
|
7
|
+
try {
|
|
8
|
+
return sessionStorage.getItem(key);
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
set(key, value) {
|
|
15
|
+
// Re-throw on quota / disabled storage so signIn() can refuse to
|
|
16
|
+
// navigate to the IdP. Silent failure (codex P2 2026-05-15) lets the
|
|
17
|
+
// user complete the IdP round-trip and only fail at handleCallback()
|
|
18
|
+
// with a misleading no_pending_handoff. Real-world hits: Safari ITP,
|
|
19
|
+
// iOS private browsing, corporate policies disabling sessionStorage.
|
|
20
|
+
sessionStorage.setItem(key, value);
|
|
21
|
+
},
|
|
22
|
+
remove(key) {
|
|
23
|
+
try {
|
|
24
|
+
sessionStorage.removeItem(key);
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
// ignore
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
export function savePending(p, backend) {
|
|
32
|
+
backend.set(KEY, JSON.stringify(p));
|
|
33
|
+
}
|
|
34
|
+
export function loadPending(backend) {
|
|
35
|
+
const raw = backend.get(KEY);
|
|
36
|
+
if (!raw)
|
|
37
|
+
return null;
|
|
38
|
+
try {
|
|
39
|
+
return JSON.parse(raw);
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
export function clearPending(backend) {
|
|
46
|
+
backend.remove(KEY);
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=storage.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"storage.js","sourceRoot":"","sources":["../src/storage.ts"],"names":[],"mappings":"AAAA,kEAAkE;AAClE,4EAA4E;AAC5E,aAAa;AAEb,MAAM,GAAG,GAAG,mBAAmB,CAAC;AAkBhC,MAAM,CAAC,MAAM,qBAAqB,GAAmB;IACnD,GAAG,CAAC,GAAG;QACL,IAAI,CAAC;YACH,OAAO,cAAc,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QACrC,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,IAAI,CAAC;QACd,CAAC;IACH,CAAC;IACD,GAAG,CAAC,GAAG,EAAE,KAAK;QACZ,iEAAiE;QACjE,qEAAqE;QACrE,qEAAqE;QACrE,qEAAqE;QACrE,qEAAqE;QACrE,cAAc,CAAC,OAAO,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;IACrC,CAAC;IACD,MAAM,CAAC,GAAG;QACR,IAAI,CAAC;YACH,cAAc,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC;QACjC,CAAC;QAAC,MAAM,CAAC;YACP,SAAS;QACX,CAAC;IACH,CAAC;CACF,CAAC;AAEF,MAAM,UAAU,WAAW,CAAC,CAAiB,EAAE,OAAuB;IACpE,OAAO,CAAC,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC;AACtC,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,OAAuB;IACjD,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IAC7B,IAAI,CAAC,GAAG;QAAE,OAAO,IAAI,CAAC;IACtB,IAAI,CAAC;QACH,OAAO,IAAI,CAAC,KAAK,CAAC,GAAG,CAAmB,CAAC;IAC3C,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,OAAuB;IAClD,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;AACtB,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@logi-auth/browser",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Browser SDK for logi (1pass) — OAuth 2.0 + OIDC PKCE for SPAs. Zero dependencies.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"default": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist",
|
|
16
|
+
"README.md",
|
|
17
|
+
"LICENSE"
|
|
18
|
+
],
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "tsc -p tsconfig.json",
|
|
21
|
+
"test": "vitest run",
|
|
22
|
+
"test:watch": "vitest",
|
|
23
|
+
"prepublishOnly": "npm run build && npm run test"
|
|
24
|
+
},
|
|
25
|
+
"keywords": [
|
|
26
|
+
"logi",
|
|
27
|
+
"1pass",
|
|
28
|
+
"oauth",
|
|
29
|
+
"oidc",
|
|
30
|
+
"pkce",
|
|
31
|
+
"spa",
|
|
32
|
+
"auth",
|
|
33
|
+
"openid"
|
|
34
|
+
],
|
|
35
|
+
"author": "Seunghan Kim (https://github.com/seunghan91)",
|
|
36
|
+
"license": "MIT",
|
|
37
|
+
"repository": {
|
|
38
|
+
"type": "git",
|
|
39
|
+
"url": "git+https://github.com/seunghan91/logi.git",
|
|
40
|
+
"directory": "Packages/npm/browser"
|
|
41
|
+
},
|
|
42
|
+
"homepage": "https://docs.1pass.dev",
|
|
43
|
+
"bugs": {
|
|
44
|
+
"url": "https://github.com/seunghan91/logi/issues"
|
|
45
|
+
},
|
|
46
|
+
"publishConfig": {
|
|
47
|
+
"access": "public"
|
|
48
|
+
},
|
|
49
|
+
"engines": {
|
|
50
|
+
"node": ">=18"
|
|
51
|
+
},
|
|
52
|
+
"devDependencies": {
|
|
53
|
+
"typescript": "^5.6.0",
|
|
54
|
+
"vitest": "^2.1.0"
|
|
55
|
+
},
|
|
56
|
+
"sideEffects": false
|
|
57
|
+
}
|