@bagelink/auth 1.4.178 → 1.4.180

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/index.mjs CHANGED
@@ -1,12 +1,12 @@
1
1
  import axios from 'axios';
2
2
  import { computed, ref } from 'vue';
3
3
 
4
- function createAxiosInstance(baseURL = "") {
4
+ function createAxiosInstance(baseURL) {
5
5
  return axios.create({
6
- baseURL: baseURL || "",
6
+ baseURL,
7
+ withCredentials: true,
7
8
  headers: {
8
- "Content-Type": "application/json",
9
- "withCredentials": true
9
+ "Content-Type": "application/json"
10
10
  }
11
11
  });
12
12
  }
@@ -43,16 +43,12 @@ class EventEmitter {
43
43
 
44
44
  class AuthApi {
45
45
  api;
46
- constructor(axiosInstance, baseURL = "") {
47
- this.api = axiosInstance || createAxiosInstance(baseURL);
46
+ constructor(baseURL = "") {
47
+ this.api = createAxiosInstance(baseURL);
48
48
  this.setupInterceptors();
49
49
  }
50
50
  setupInterceptors() {
51
51
  this.api.interceptors.request.use((config) => {
52
- const sessionToken = localStorage.getItem("session_token");
53
- if (sessionToken !== null && config.headers) {
54
- config.headers.Authorization = `Bearer ${sessionToken}`;
55
- }
56
52
  const urlParams = new URLSearchParams(window.location.search);
57
53
  const resetToken = urlParams.get("token");
58
54
  if (resetToken !== null && config.headers) {
@@ -74,45 +70,67 @@ class AuthApi {
74
70
  * Register a new account
75
71
  */
76
72
  async register(data) {
77
- const response = await this.api.post("/authentication/register", {
73
+ return this.api.post("/authentication/register", {
78
74
  ...data,
79
75
  email: data.email.toLowerCase()
80
76
  });
81
- if (response.data.session_token) {
82
- localStorage.setItem("session_token", response.data.session_token);
83
- }
84
- return response;
85
77
  }
86
78
  /**
87
79
  * Login with password
88
80
  */
89
81
  async login(email, password) {
90
- const response = await this.api.post("/authentication/login/password", {
82
+ return this.api.post("/authentication/login/password", {
91
83
  email: email.toLowerCase(),
92
84
  password
93
85
  });
94
- if (response.data.session_token) {
95
- localStorage.setItem("session_token", response.data.session_token);
96
- }
97
- return response;
98
86
  }
99
87
  /**
100
88
  * Logout and clear session
101
89
  */
102
90
  async logout() {
103
- const response = await this.api.post("/authentication/logout", {});
104
- localStorage.removeItem("session_token");
105
- return response;
91
+ return this.api.post("/authentication/logout", {});
106
92
  }
107
93
  /**
108
94
  * Refresh current session
109
95
  */
110
96
  async refreshSession() {
111
- const response = await this.api.post("/authentication/refresh", {});
112
- if (response.data.session_token) {
113
- localStorage.setItem("session_token", response.data.session_token);
114
- }
115
- return response;
97
+ return this.api.post("/authentication/refresh", {});
98
+ }
99
+ // ============================================
100
+ // SSO Authentication Methods
101
+ // ============================================
102
+ /**
103
+ * Initiate SSO login flow
104
+ * Returns authorization URL to redirect user to
105
+ */
106
+ async initiateSSO(data) {
107
+ return this.api.post(`/authentication/sso/${data.provider}/initiate`, {
108
+ redirect_uri: data.redirect_uri,
109
+ state: data.state
110
+ });
111
+ }
112
+ /**
113
+ * Complete SSO login after callback from provider
114
+ */
115
+ async ssoCallback(data) {
116
+ return this.api.post(`/authentication/sso/${data.provider}/callback`, {
117
+ code: data.code,
118
+ state: data.state
119
+ });
120
+ }
121
+ /**
122
+ * Link an SSO provider to existing account
123
+ */
124
+ async linkSSOProvider(data) {
125
+ return this.api.post(`/authentication/sso/${data.provider}/link`, {
126
+ code: data.code
127
+ });
128
+ }
129
+ /**
130
+ * Unlink an SSO provider from account
131
+ */
132
+ async unlinkSSOProvider(provider) {
133
+ return this.api.delete(`/authentication/sso/${provider}/unlink`);
116
134
  }
117
135
  // ============================================
118
136
  // Current User (Me) Methods
@@ -133,9 +151,7 @@ class AuthApi {
133
151
  * Delete current user account
134
152
  */
135
153
  async deleteCurrentUser() {
136
- const response = await this.api.delete("/authentication/me");
137
- localStorage.removeItem("session_token");
138
- return response;
154
+ return this.api.delete("/authentication/me");
139
155
  }
140
156
  // ============================================
141
157
  // Account Management (Admin)
@@ -245,6 +261,356 @@ class AuthApi {
245
261
  }
246
262
  }
247
263
 
264
+ let authApiRef = null;
265
+ function setAuthContext(authApi) {
266
+ authApiRef = authApi;
267
+ }
268
+ function getAuthApi() {
269
+ if (!authApiRef) {
270
+ throw new Error("SSO auth context not initialized. Make sure to call useAuth() before using SSO methods.");
271
+ }
272
+ return authApiRef;
273
+ }
274
+ class SSOError extends Error {
275
+ constructor(message, code) {
276
+ super(message);
277
+ this.code = code;
278
+ this.name = "SSOError";
279
+ }
280
+ }
281
+ class PopupBlockedError extends SSOError {
282
+ constructor() {
283
+ super("Popup was blocked. Please allow popups for this site.", "POPUP_BLOCKED");
284
+ this.name = "PopupBlockedError";
285
+ }
286
+ }
287
+ class PopupClosedError extends SSOError {
288
+ constructor() {
289
+ super("Popup was closed by user", "POPUP_CLOSED");
290
+ this.name = "PopupClosedError";
291
+ }
292
+ }
293
+ class PopupTimeoutError extends SSOError {
294
+ constructor() {
295
+ super("Popup authentication timed out", "POPUP_TIMEOUT");
296
+ this.name = "PopupTimeoutError";
297
+ }
298
+ }
299
+ class StateMismatchError extends SSOError {
300
+ constructor() {
301
+ super("State mismatch - possible CSRF attack", "STATE_MISMATCH");
302
+ this.name = "StateMismatchError";
303
+ }
304
+ }
305
+ function generateState() {
306
+ const array = new Uint8Array(32);
307
+ crypto.getRandomValues(array);
308
+ return Array.from(array, (byte) => byte.toString(16).padStart(2, "0")).join("");
309
+ }
310
+ function openPopup(url, width = 500, height = 600) {
311
+ const left = window.screenX + (window.outerWidth - width) / 2;
312
+ const top = window.screenY + (window.outerHeight - height) / 2;
313
+ return window.open(
314
+ url,
315
+ "oauth-popup",
316
+ `width=${width},height=${height},left=${left},top=${top},toolbar=no,menubar=no,location=no,status=no`
317
+ );
318
+ }
319
+ function waitForPopupCallback(popup, provider, timeoutMs = 9e4) {
320
+ return new Promise((resolve, reject) => {
321
+ let done = false;
322
+ const finish = (fn) => {
323
+ if (!done) {
324
+ done = true;
325
+ fn();
326
+ }
327
+ };
328
+ const timer = setTimeout(() => {
329
+ finish(() => {
330
+ reject(new PopupTimeoutError());
331
+ });
332
+ }, timeoutMs);
333
+ function onMessage(ev) {
334
+ try {
335
+ if (ev.origin !== window.location.origin) return;
336
+ const data = ev.data || {};
337
+ if (data.type !== "auth:complete" || data.provider !== provider) return;
338
+ cleanup();
339
+ if (data.error) {
340
+ reject(new SSOError(data.error, "OAUTH_ERROR"));
341
+ } else if (data.code) {
342
+ resolve({ code: data.code, state: data.state });
343
+ }
344
+ } catch {
345
+ }
346
+ }
347
+ const pollInterval = setInterval(() => {
348
+ try {
349
+ if (popup.closed) {
350
+ cleanup();
351
+ reject(new PopupClosedError());
352
+ return;
353
+ }
354
+ const url = new URL(popup.location.href);
355
+ if (url.origin === window.location.origin) {
356
+ const code = url.searchParams.get("code");
357
+ const state = url.searchParams.get("state") ?? void 0;
358
+ const error = url.searchParams.get("error");
359
+ if (code || error) {
360
+ cleanup();
361
+ try {
362
+ popup.close();
363
+ } catch {
364
+ }
365
+ if (error) {
366
+ reject(new SSOError(error, "OAUTH_ERROR"));
367
+ } else if (code) {
368
+ resolve({ code, state });
369
+ }
370
+ }
371
+ }
372
+ } catch {
373
+ }
374
+ }, 150);
375
+ function cleanup() {
376
+ finish(() => {
377
+ clearInterval(pollInterval);
378
+ clearTimeout(timer);
379
+ window.removeEventListener("message", onMessage);
380
+ try {
381
+ popup.close();
382
+ } catch {
383
+ }
384
+ });
385
+ }
386
+ window.addEventListener("message", onMessage);
387
+ });
388
+ }
389
+ function createSSOProvider(config) {
390
+ const getDefaultRedirectUri = () => {
391
+ if (typeof window !== "undefined") {
392
+ return `${window.location.origin}/auth/callback`;
393
+ }
394
+ return `/auth/callback`;
395
+ };
396
+ const getStateKey = () => `oauth_state:${config.id}`;
397
+ return {
398
+ ...config,
399
+ async redirect(options = {}) {
400
+ const auth = getAuthApi();
401
+ const redirectUri = options.redirectUri ?? getDefaultRedirectUri();
402
+ const state = options.state ?? generateState();
403
+ if (typeof sessionStorage !== "undefined") {
404
+ sessionStorage.setItem(getStateKey(), state);
405
+ sessionStorage.setItem(`oauth_provider:${state}`, config.id);
406
+ }
407
+ const authUrl = await auth.initiateSSO({
408
+ provider: config.id,
409
+ redirect_uri: redirectUri,
410
+ state,
411
+ scopes: options.scopes ?? config.defaultScopes,
412
+ params: options.params
413
+ });
414
+ window.location.href = authUrl;
415
+ },
416
+ async popup(options = {}) {
417
+ const auth = getAuthApi();
418
+ const redirectUri = options.redirectUri ?? getDefaultRedirectUri();
419
+ const state = options.state ?? generateState();
420
+ const timeout = options.popupTimeout ?? 9e4;
421
+ if (typeof sessionStorage !== "undefined") {
422
+ sessionStorage.setItem(getStateKey(), state);
423
+ sessionStorage.setItem(`oauth_provider:${state}`, config.id);
424
+ }
425
+ const authUrl = await auth.initiateSSO({
426
+ provider: config.id,
427
+ redirect_uri: redirectUri,
428
+ state,
429
+ scopes: options.scopes ?? config.defaultScopes,
430
+ params: options.params
431
+ });
432
+ const { width = 500, height = 600 } = options.popupDimensions ?? {};
433
+ const popupWindow = openPopup(authUrl, width, height);
434
+ if (!popupWindow) {
435
+ throw new PopupBlockedError();
436
+ }
437
+ const result = await waitForPopupCallback(popupWindow, config.id, timeout);
438
+ return auth.loginWithSSO({
439
+ provider: config.id,
440
+ code: result.code,
441
+ state: result.state
442
+ });
443
+ },
444
+ async callback(code, state) {
445
+ const auth = getAuthApi();
446
+ if (typeof sessionStorage !== "undefined" && state) {
447
+ const storedState = sessionStorage.getItem(getStateKey());
448
+ sessionStorage.removeItem(getStateKey());
449
+ sessionStorage.removeItem(`oauth_provider:${state}`);
450
+ if (storedState && storedState !== state) {
451
+ throw new StateMismatchError();
452
+ }
453
+ }
454
+ return auth.loginWithSSO({
455
+ provider: config.id,
456
+ code,
457
+ state
458
+ });
459
+ },
460
+ async link(code) {
461
+ const auth = getAuthApi();
462
+ await auth.linkSSOProvider({
463
+ provider: config.id,
464
+ code
465
+ });
466
+ },
467
+ async unlink() {
468
+ const auth = getAuthApi();
469
+ await auth.unlinkSSOProvider(config.id);
470
+ },
471
+ async getAuthUrl(options = {}) {
472
+ const auth = getAuthApi();
473
+ const redirectUri = options.redirectUri ?? getDefaultRedirectUri();
474
+ const state = options.state ?? generateState();
475
+ return auth.initiateSSO({
476
+ provider: config.id,
477
+ redirect_uri: redirectUri,
478
+ state,
479
+ scopes: options.scopes ?? config.defaultScopes,
480
+ params: options.params
481
+ });
482
+ },
483
+ supportsPopup: true
484
+ // Default, can be overridden per provider
485
+ };
486
+ }
487
+ const sso = {
488
+ /**
489
+ * Google OAuth Provider
490
+ * https://developers.google.com/identity/protocols/oauth2
491
+ */
492
+ google: createSSOProvider({
493
+ id: "google",
494
+ name: "Google",
495
+ color: "#4285F4",
496
+ icon: "google",
497
+ defaultScopes: ["openid", "email", "profile"],
498
+ metadata: {
499
+ authDomain: "accounts.google.com",
500
+ buttonText: "Continue with Google"
501
+ }
502
+ }),
503
+ /**
504
+ * Microsoft OAuth Provider (Azure AD / Microsoft Entra ID)
505
+ * https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow
506
+ */
507
+ microsoft: createSSOProvider({
508
+ id: "microsoft",
509
+ name: "Microsoft",
510
+ color: "#00A4EF",
511
+ icon: "microsoft",
512
+ defaultScopes: ["openid", "email", "profile", "User.Read"],
513
+ metadata: {
514
+ authDomain: "login.microsoftonline.com",
515
+ buttonText: "Continue with Microsoft"
516
+ }
517
+ }),
518
+ /**
519
+ * GitHub OAuth Provider
520
+ * https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/authorizing-oauth-apps
521
+ */
522
+ github: createSSOProvider({
523
+ id: "github",
524
+ name: "GitHub",
525
+ color: "#24292E",
526
+ icon: "github",
527
+ defaultScopes: ["read:user", "user:email"],
528
+ metadata: {
529
+ authDomain: "github.com",
530
+ buttonText: "Continue with GitHub"
531
+ }
532
+ }),
533
+ /**
534
+ * Okta OAuth Provider
535
+ * https://developer.okta.com/docs/guides/implement-grant-type/authcode/main/
536
+ */
537
+ okta: createSSOProvider({
538
+ id: "okta",
539
+ name: "Okta",
540
+ color: "#007DC1",
541
+ icon: "okta",
542
+ defaultScopes: ["openid", "email", "profile"],
543
+ metadata: {
544
+ buttonText: "Continue with Okta"
545
+ }
546
+ }),
547
+ /**
548
+ * Apple Sign In Provider
549
+ * https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_rest_api
550
+ * Note: Apple works best with redirect flow on web
551
+ */
552
+ apple: {
553
+ ...createSSOProvider({
554
+ id: "apple",
555
+ name: "Apple",
556
+ color: "#000000",
557
+ icon: "apple",
558
+ defaultScopes: ["name", "email"],
559
+ metadata: {
560
+ authDomain: "appleid.apple.com",
561
+ buttonText: "Continue with Apple"
562
+ }
563
+ }),
564
+ supportsPopup: false,
565
+ // Apple prefers redirect on web
566
+ // Override popup to use redirect for better UX
567
+ async popup(options) {
568
+ return this.redirect(options);
569
+ }
570
+ },
571
+ /**
572
+ * Facebook OAuth Provider
573
+ * https://developers.facebook.com/docs/facebook-login/guides/advanced/manual-flow
574
+ */
575
+ facebook: createSSOProvider({
576
+ id: "facebook",
577
+ name: "Facebook",
578
+ color: "#1877F2",
579
+ icon: "facebook",
580
+ defaultScopes: ["email", "public_profile"],
581
+ metadata: {
582
+ authDomain: "www.facebook.com",
583
+ buttonText: "Continue with Facebook"
584
+ }
585
+ })
586
+ };
587
+ const ssoProviders = Object.values(sso);
588
+ function getSSOProvider(provider) {
589
+ return sso[provider];
590
+ }
591
+ function getAllSSOProviders() {
592
+ return ssoProviders;
593
+ }
594
+ function isSupportedProvider(provider) {
595
+ return provider in sso;
596
+ }
597
+ async function handleOAuthCallback() {
598
+ if (typeof window === "undefined") {
599
+ return null;
600
+ }
601
+ const urlParams = new URLSearchParams(window.location.search);
602
+ const code = urlParams.get("code");
603
+ const state = urlParams.get("state");
604
+ if (!code || !state) {
605
+ return null;
606
+ }
607
+ const provider = sessionStorage.getItem(`oauth_provider:${state}`);
608
+ if (!provider || !isSupportedProvider(provider)) {
609
+ throw new Error("Unable to determine OAuth provider. State may have expired.");
610
+ }
611
+ return sso[provider].callback(code, state);
612
+ }
613
+
248
614
  var AuthState = /* @__PURE__ */ ((AuthState2) => {
249
615
  AuthState2["LOGIN"] = "login";
250
616
  AuthState2["LOGOUT"] = "logout";
@@ -285,7 +651,7 @@ function accountToUser(account) {
285
651
  metadata: account.entity.metadata
286
652
  };
287
653
  }
288
- const emailMethod = account.authentication_methods?.find(
654
+ const emailMethod = account.authentication_methods.find(
289
655
  (m) => m.type === "password" || m.type === "email_token"
290
656
  );
291
657
  return {
@@ -304,11 +670,10 @@ let authApi = null;
304
670
  let eventEmitter = null;
305
671
  const accountInfo = ref(null);
306
672
  function initAuth({
307
- axios,
308
673
  baseURL
309
674
  }) {
310
675
  if (authApi === null) {
311
- authApi = new AuthApi(axios, baseURL);
676
+ authApi = new AuthApi(baseURL);
312
677
  }
313
678
  if (eventEmitter === null) {
314
679
  eventEmitter = new EventEmitter();
@@ -344,6 +709,29 @@ function useAuth() {
344
709
  }
345
710
  const api = authApi;
346
711
  const emitter = eventEmitter;
712
+ const authMethods = {
713
+ initiateSSO: async (params) => {
714
+ const { data } = await api.initiateSSO(params);
715
+ return data.authorization_url;
716
+ },
717
+ loginWithSSO: async (params) => {
718
+ const { data } = await api.ssoCallback(params);
719
+ if (data.success === true && data.requires_verification !== true) {
720
+ await checkAuth();
721
+ }
722
+ emitter.emit(AuthState.LOGIN);
723
+ return data;
724
+ },
725
+ linkSSOProvider: async (params) => {
726
+ await api.linkSSOProvider(params);
727
+ await checkAuth();
728
+ },
729
+ unlinkSSOProvider: async (provider) => {
730
+ await api.unlinkSSOProvider(provider);
731
+ await checkAuth();
732
+ }
733
+ };
734
+ setAuthContext(authMethods);
347
735
  const user = computed(() => accountToUser(accountInfo.value));
348
736
  const getFullName = () => {
349
737
  return user.value?.name ?? "";
@@ -482,11 +870,33 @@ function useAuth() {
482
870
  }
483
871
  await api.revokeAllSessions(id);
484
872
  }
873
+ async function initiateSSO(params) {
874
+ const { data } = await api.initiateSSO(params);
875
+ return data.authorization_url;
876
+ }
877
+ async function loginWithSSO(params) {
878
+ const { data } = await api.ssoCallback(params);
879
+ if (data.success === true && data.requires_verification !== true) {
880
+ await checkAuth();
881
+ }
882
+ emitter.emit(AuthState.LOGIN);
883
+ return data;
884
+ }
885
+ async function linkSSOProvider(params) {
886
+ await api.linkSSOProvider(params);
887
+ await checkAuth();
888
+ }
889
+ async function unlinkSSOProvider(provider) {
890
+ await api.unlinkSSOProvider(provider);
891
+ await checkAuth();
892
+ }
485
893
  return {
486
894
  // Primary State (use this!)
487
895
  user,
488
896
  // Full account info (for advanced use cases)
489
897
  accountInfo,
898
+ // SSO Providers (ready to use!)
899
+ sso,
490
900
  // Getters
491
901
  getFullName,
492
902
  getIsLoggedIn,
@@ -501,6 +911,11 @@ function useAuth() {
501
911
  signup,
502
912
  checkAuth,
503
913
  refreshSession,
914
+ // SSO Authentication (lower-level - prefer using sso.google.redirect())
915
+ initiateSSO,
916
+ loginWithSSO,
917
+ linkSSOProvider,
918
+ unlinkSSOProvider,
504
919
  // Profile Actions
505
920
  updateProfile,
506
921
  deleteCurrentUser,
@@ -523,4 +938,4 @@ function useAuth() {
523
938
  };
524
939
  }
525
940
 
526
- export { AuthApi, AuthState, accountToUser, initAuth, useAuth };
941
+ export { AuthApi, AuthState, PopupBlockedError, PopupClosedError, PopupTimeoutError, SSOError, StateMismatchError, accountToUser, getAllSSOProviders, getSSOProvider, handleOAuthCallback, initAuth, isSupportedProvider, setAuthContext, sso, ssoProviders, useAuth };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@bagelink/auth",
3
3
  "type": "module",
4
- "version": "1.4.178",
4
+ "version": "1.4.180",
5
5
  "description": "Bagelink auth package",
6
6
  "author": {
7
7
  "name": "Bagel Studio",