@ericminassian/auth 0.2.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;
|
package/dist/client/index.d.ts
CHANGED
|
@@ -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-
|
|
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 };
|
package/dist/client/index.js
CHANGED
|
@@ -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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
if (
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
}
|
package/dist/react/index.d.ts
CHANGED
|
@@ -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-
|
|
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