@ericminassian/auth 0.1.0 → 0.3.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.
@@ -38,10 +38,28 @@ interface SignInOptions {
38
38
  interface AuthClient {
39
39
  /** Build a PKCE+state transaction and navigate to the authorize endpoint. */
40
40
  signInWithRedirect(options?: SignInOptions): Promise<void>;
41
+ /**
42
+ * Attempt silent SSO via a hidden iframe (`prompt=none`). Resolves to the
43
+ * resulting state — authenticated if an IdP session already existed,
44
+ * otherwise unchanged. Never rejects on `login_required`.
45
+ *
46
+ * Same-site only: the IdP session cookie is not sent to a cross-site iframe,
47
+ * so this is a no-op when the app and issuer aren't on the same site (e.g.
48
+ * `localhost` against a hosted issuer).
49
+ */
50
+ signInSilently(): Promise<AuthState>;
41
51
  /** Complete the redirect: exchange the code for tokens. Returns the saved returnTo. */
42
52
  handleRedirectCallback(url?: string): Promise<{
43
53
  returnTo: string | undefined;
44
54
  }>;
55
+ /**
56
+ * Call from the redirect callback page. Inside a silent-auth iframe it relays
57
+ * the result to the opener and resolves `null` (the page should do nothing
58
+ * else). At top level it completes the code exchange and returns `returnTo`.
59
+ */
60
+ handleCallback(): Promise<{
61
+ returnTo: string | undefined;
62
+ } | null>;
45
63
  /** A valid access token, refreshing if necessary. Throws `login_required` if not signed in. */
46
64
  getAccessToken(options?: {
47
65
  forceRefresh?: boolean;
@@ -1,3 +1,3 @@
1
1
  import { i as User, n as AuthErrorCode, r as DEFAULT_ISSUER, t as AuthError } from "../index-CiPxwNnj.js";
2
- import { a as createAuthClient, i as SignInOptions, n as AuthClientOptions, o as TokenStorage, r as AuthState, t as AuthClient } from "../auth-client-DZWvgw3X.js";
2
+ import { a as createAuthClient, i as SignInOptions, n as AuthClientOptions, o as TokenStorage, r as AuthState, t as AuthClient } from "../auth-client-DSXlCl42.js";
3
3
  export { type AuthClient, type AuthClientOptions, AuthError, type AuthErrorCode, type AuthState, DEFAULT_ISSUER, type SignInOptions, type TokenStorage, type User, createAuthClient };
@@ -52,6 +52,8 @@ const TX_KEY = "ema_auth_tx";
52
52
  const RT_KEY = "ema_auth_rt";
53
53
  const ID_KEY = "ema_auth_id";
54
54
  const EXPIRY_SKEW_SECONDS = 30;
55
+ const SILENT_MESSAGE_SOURCE = "ema_auth_silent";
56
+ const SILENT_TIMEOUT_MS = 8e3;
55
57
  function createAuthClient(options) {
56
58
  const issuer = (options.issuer ?? DEFAULT_ISSUER).replace(/\/$/, "");
57
59
  const scope = options.scope ?? "openid email offline_access";
@@ -99,46 +101,110 @@ function createAuthClient(options) {
99
101
  });
100
102
  }
101
103
  }
104
+ async function buildAuthorizeUrl(extra, returnTo) {
105
+ const { authorization_endpoint } = await getDiscovery();
106
+ const pkce = await createPkcePair();
107
+ const tx = {
108
+ verifier: pkce.verifier,
109
+ state: createState()
110
+ };
111
+ if (returnTo !== void 0) tx.returnTo = returnTo;
112
+ storage.set(TX_KEY, JSON.stringify(tx));
113
+ const url = new URL(authorization_endpoint);
114
+ url.search = new URLSearchParams({
115
+ response_type: "code",
116
+ client_id: options.clientId,
117
+ redirect_uri: options.redirectUri,
118
+ scope,
119
+ state: tx.state,
120
+ code_challenge: pkce.challenge,
121
+ code_challenge_method: "S256",
122
+ ...extra
123
+ }).toString();
124
+ return url.toString();
125
+ }
126
+ async function completeCallback(url) {
127
+ const params = new URL(url ?? currentUrl()).searchParams;
128
+ const raw = storage.get(TX_KEY);
129
+ storage.remove(TX_KEY);
130
+ if (!raw) throw new AuthError("state_mismatch", "no authorization transaction in progress");
131
+ const tx = JSON.parse(raw);
132
+ if (params.get("error")) throw new AuthError("invalid_grant", params.get("error_description") ?? params.get("error") ?? "authorization failed");
133
+ if (params.get("state") !== tx.state) throw new AuthError("state_mismatch", "state parameter mismatch");
134
+ const code = params.get("code");
135
+ if (!code) throw new AuthError("invalid_grant", "missing authorization code");
136
+ await exchange({
137
+ grant_type: "authorization_code",
138
+ code,
139
+ redirect_uri: options.redirectUri,
140
+ client_id: options.clientId,
141
+ code_verifier: tx.verifier
142
+ });
143
+ return { returnTo: tx.returnTo };
144
+ }
145
+ function runSilentFrame(authorizeUrl) {
146
+ return new Promise((resolve) => {
147
+ const iframe = document.createElement("iframe");
148
+ iframe.style.display = "none";
149
+ iframe.setAttribute("aria-hidden", "true");
150
+ let settled = false;
151
+ const finish = (result) => {
152
+ if (settled) return;
153
+ settled = true;
154
+ window.removeEventListener("message", onMessage);
155
+ clearTimeout(timer);
156
+ iframe.remove();
157
+ resolve(result);
158
+ };
159
+ const onMessage = (event) => {
160
+ if (event.origin !== window.location.origin) return;
161
+ const data = event.data;
162
+ if (!data || data.source !== SILENT_MESSAGE_SOURCE) return;
163
+ finish(typeof data.search === "string" ? data.search : "");
164
+ };
165
+ const timer = setTimeout(() => finish(void 0), SILENT_TIMEOUT_MS);
166
+ window.addEventListener("message", onMessage);
167
+ iframe.src = authorizeUrl;
168
+ document.body.appendChild(iframe);
169
+ });
170
+ }
102
171
  return {
103
172
  async signInWithRedirect(signInOptions) {
104
- const { authorization_endpoint } = await getDiscovery();
105
- const pkce = await createPkcePair();
106
- const tx = {
107
- verifier: pkce.verifier,
108
- state: createState(),
109
- returnTo: signInOptions?.returnTo ?? currentUrl()
110
- };
111
- storage.set(TX_KEY, JSON.stringify(tx));
112
- const url = new URL(authorization_endpoint);
113
- url.search = new URLSearchParams({
114
- response_type: "code",
115
- client_id: options.clientId,
116
- redirect_uri: options.redirectUri,
117
- scope,
118
- state: tx.state,
119
- code_challenge: pkce.challenge,
120
- code_challenge_method: "S256"
121
- }).toString();
122
- redirect(url.toString());
173
+ redirect(await buildAuthorizeUrl({}, signInOptions?.returnTo ?? currentUrl()));
174
+ },
175
+ async signInSilently() {
176
+ if (state.status === "authenticated") return state;
177
+ if (typeof window === "undefined" || typeof document === "undefined") return state;
178
+ let authorizeUrl;
179
+ try {
180
+ authorizeUrl = await buildAuthorizeUrl({ prompt: "none" });
181
+ } catch {
182
+ return state;
183
+ }
184
+ const search = await runSilentFrame(authorizeUrl);
185
+ if (search === void 0) {
186
+ storage.remove(TX_KEY);
187
+ return state;
188
+ }
189
+ const callbackUrl = new URL(options.redirectUri);
190
+ callbackUrl.search = search;
191
+ try {
192
+ await completeCallback(callbackUrl.toString());
193
+ } catch {}
194
+ return state;
123
195
  },
124
196
  async handleRedirectCallback(url) {
125
- const params = new URL(url ?? currentUrl()).searchParams;
126
- const raw = storage.get(TX_KEY);
127
- storage.remove(TX_KEY);
128
- if (!raw) throw new AuthError("state_mismatch", "no authorization transaction in progress");
129
- const tx = JSON.parse(raw);
130
- if (params.get("error")) throw new AuthError("invalid_grant", params.get("error_description") ?? params.get("error") ?? "authorization failed");
131
- if (params.get("state") !== tx.state) throw new AuthError("state_mismatch", "state parameter mismatch");
132
- const code = params.get("code");
133
- if (!code) throw new AuthError("invalid_grant", "missing authorization code");
134
- await exchange({
135
- grant_type: "authorization_code",
136
- code,
137
- redirect_uri: options.redirectUri,
138
- client_id: options.clientId,
139
- code_verifier: tx.verifier
140
- });
141
- return { returnTo: tx.returnTo };
197
+ return completeCallback(url);
198
+ },
199
+ async handleCallback() {
200
+ if (isFramed()) {
201
+ window.parent.postMessage({
202
+ source: SILENT_MESSAGE_SOURCE,
203
+ search: window.location.search
204
+ }, window.location.origin);
205
+ return null;
206
+ }
207
+ return completeCallback();
142
208
  },
143
209
  async getAccessToken(getOptions) {
144
210
  if (!getOptions?.forceRefresh && cachedToken && cachedToken.expiresAt > Date.now()) return cachedToken.accessToken;
@@ -226,6 +292,14 @@ async function fetchJson(url) {
226
292
  function currentUrl() {
227
293
  return typeof location !== "undefined" ? location.href : "";
228
294
  }
295
+ function isFramed() {
296
+ if (typeof window === "undefined") return false;
297
+ try {
298
+ return window.self !== window.top;
299
+ } catch {
300
+ return true;
301
+ }
302
+ }
229
303
  function redirect(url) {
230
304
  if (typeof location !== "undefined") location.assign(url);
231
305
  }
@@ -1,5 +1,5 @@
1
1
  import { i as User } from "../index-CiPxwNnj.js";
2
- import { i as SignInOptions, r as AuthState, t as AuthClient } from "../auth-client-DZWvgw3X.js";
2
+ import { i as SignInOptions, r as AuthState, t as AuthClient } from "../auth-client-DSXlCl42.js";
3
3
  import { ReactNode } from "react";
4
4
 
5
5
  //#region src/react/context.d.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ericminassian/auth",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "TypeScript SDK for auth.ericminassian.com — OIDC client, React bindings, and server-side JWT verification.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -42,12 +42,6 @@
42
42
  "default": "./dist/server/express.js"
43
43
  }
44
44
  },
45
- "scripts": {
46
- "build": "tsdown",
47
- "typecheck": "tsc --noEmit",
48
- "generate": "openapi-typescript ../../openapi/openapi.json -o src/generated/api.d.ts",
49
- "test": "vitest run"
50
- },
51
45
  "dependencies": {
52
46
  "jose": "^6"
53
47
  },
@@ -80,5 +74,11 @@
80
74
  },
81
75
  "publishConfig": {
82
76
  "access": "public"
77
+ },
78
+ "scripts": {
79
+ "build": "tsdown",
80
+ "typecheck": "tsc --noEmit",
81
+ "generate": "openapi-typescript ../../openapi/openapi.json -o src/generated/api.d.ts",
82
+ "test": "vitest run"
83
83
  }
84
- }
84
+ }