nuxt-auth-utils 0.5.10 → 0.5.12

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 CHANGED
@@ -62,7 +62,7 @@ Nuxt Auth Utils automatically adds some plugins to fetch the current user sessio
62
62
 
63
63
  ```vue
64
64
  <script setup>
65
- const { loggedIn, user, session, fetch, clear } = useUserSession()
65
+ const { loggedIn, user, session, fetch, clear, openInPopup } = useUserSession()
66
66
  </script>
67
67
 
68
68
  <template>
@@ -74,6 +74,8 @@ const { loggedIn, user, session, fetch, clear } = useUserSession()
74
74
  <div v-else>
75
75
  <h1>Not logged in</h1>
76
76
  <a href="/auth/github">Login with GitHub</a>
77
+ <!-- or open the OAuth route in a popup -->
78
+ <button @click="openInPopup('/auth/github')">Login with GitHub</button>
77
79
  </div>
78
80
  </template>
79
81
  ```
@@ -106,6 +108,10 @@ interface UserSessionComposable {
106
108
  * Clear the user session and remove the session cookie.
107
109
  */
108
110
  clear: () => Promise<void>
111
+ /**
112
+ * Open the OAuth route in a popup that auto-closes when successful.
113
+ */
114
+ openInPopup: (route: string, size?: { width?: number, height?: number }) => void
109
115
  }
110
116
  ```
111
117
 
@@ -211,11 +217,13 @@ It can also be set using environment variables:
211
217
  - Authentik
212
218
  - AWS Cognito
213
219
  - Battle.net
220
+ - Bluesky (AT Protocol)
214
221
  - Discord
215
222
  - Dropbox
216
223
  - Facebook
217
224
  - GitHub
218
225
  - GitLab
226
+ - Gitea
219
227
  - Google
220
228
  - Hubspot
221
229
  - Instagram
@@ -297,6 +305,28 @@ export default defineNuxtConfig({
297
305
  })
298
306
  ```
299
307
 
308
+ ### AT Protocol
309
+
310
+ Social networks that rely on AT Protocol (e.g., Bluesky) slightly differ from a regular OAuth flow.
311
+
312
+ To enable OAuth with AT Protocol, you need to:
313
+
314
+ 1. Install the peer dependencies:
315
+
316
+ ```bash
317
+ npx nypm i @atproto/oauth-client-node @atproto/api
318
+ ```
319
+
320
+ 2. Enable it in your `nuxt.config.ts`
321
+
322
+ ```ts
323
+ export default defineNuxtConfig({
324
+ auth: {
325
+ atproto: true
326
+ }
327
+ })
328
+ ```
329
+
300
330
  ### WebAuthn (passkey)
301
331
 
302
332
  WebAuthn (Web Authentication) is a web standard that enhances security by replacing passwords with passkeys using public key cryptography. Users can authenticate with biometric data (like fingerprints or facial recognition) or physical devices (like USB keys), reducing the risk of phishing and password breaches. This approach offers a more secure and user-friendly authentication method, supported by major browsers and platforms.
@@ -704,26 +734,26 @@ Checkout the [`SessionConfig`](https://github.com/unjs/h3/blob/c04c458810e34eb15
704
734
 
705
735
  ```bash
706
736
  # Install dependencies
707
- npm install
737
+ pnpm install
708
738
 
709
739
  # Generate type stubs
710
- npm run dev:prepare
740
+ pnpm run dev:prepare
711
741
 
712
742
  # Develop with the playground
713
- npm run dev
743
+ pnpm run dev
714
744
 
715
745
  # Build the playground
716
- npm run dev:build
746
+ pnpm run dev:build
717
747
 
718
748
  # Run ESLint
719
- npm run lint
749
+ pnpm run lint
720
750
 
721
751
  # Run Vitest
722
- npm run test
723
- npm run test:watch
752
+ pnpm run test
753
+ pnpm run test:watch
724
754
 
725
755
  # Release new version
726
- npm run release
756
+ pnpm run release
727
757
  ```
728
758
 
729
759
  <!-- Badges -->
package/dist/module.d.mts CHANGED
@@ -8,6 +8,11 @@ interface ModuleOptions {
8
8
  * @default false
9
9
  */
10
10
  webAuthn?: boolean;
11
+ /**
12
+ * Enable atproto OAuth (Bluesky, etc.)
13
+ * @default false
14
+ */
15
+ atproto?: boolean;
11
16
  /**
12
17
  * Hash options used for password hashing
13
18
  */
package/dist/module.d.ts CHANGED
@@ -8,6 +8,11 @@ interface ModuleOptions {
8
8
  * @default false
9
9
  */
10
10
  webAuthn?: boolean;
11
+ /**
12
+ * Enable atproto OAuth (Bluesky, etc.)
13
+ * @default false
14
+ */
15
+ atproto?: boolean;
11
16
  /**
12
17
  * Hash options used for password hashing
13
18
  */
package/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "auth-utils",
3
3
  "configKey": "auth",
4
- "version": "0.5.10",
4
+ "version": "0.5.12",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "0.8.4",
7
7
  "unbuild": "2.0.0"
package/dist/module.mjs CHANGED
@@ -4,6 +4,26 @@ import { join } from 'pathe';
4
4
  import { defu } from 'defu';
5
5
  import { randomUUID } from 'uncrypto';
6
6
 
7
+ const atprotoProviders = ["bluesky"];
8
+ const atprotoProviderDefaultClientMetadata = {
9
+ clientMetadataFilename: "",
10
+ clientName: "",
11
+ clientUri: "",
12
+ logoUri: "",
13
+ policyUri: "",
14
+ tosUri: "",
15
+ scope: ["atproto"],
16
+ grantTypes: ["authorization_code"],
17
+ responseTypes: ["code"],
18
+ applicationType: "web",
19
+ redirectUris: [],
20
+ dpopBoundAccessTokens: true,
21
+ tokenEndpointAuthMethod: "none"
22
+ };
23
+ function getClientMetadataFilename(provider, config) {
24
+ return config?.clientMetadataFilename || provider + "/client-metadata.json";
25
+ }
26
+
7
27
  const module = defineNuxtModule({
8
28
  meta: {
9
29
  name: "auth-utils",
@@ -12,6 +32,7 @@ const module = defineNuxtModule({
12
32
  // Default configuration options of the Nuxt module
13
33
  defaults: {
14
34
  webAuthn: false,
35
+ atproto: false,
15
36
  hash: {
16
37
  scrypt: {}
17
38
  }
@@ -97,6 +118,12 @@ const module = defineNuxtModule({
97
118
  authenticate: {}
98
119
  });
99
120
  runtimeConfig.oauth = defu(runtimeConfig.oauth, {});
121
+ runtimeConfig.oauth.gitea = defu(runtimeConfig.oauth.gitea, {
122
+ clientId: "",
123
+ clientSecret: "",
124
+ redirectURL: "",
125
+ baseURL: ""
126
+ });
100
127
  runtimeConfig.oauth.github = defu(runtimeConfig.oauth.github, {
101
128
  clientId: "",
102
129
  clientSecret: "",
@@ -157,6 +184,32 @@ const module = defineNuxtModule({
157
184
  clientSecret: "",
158
185
  redirectURL: ""
159
186
  });
187
+ for (const provider of atprotoProviders) {
188
+ runtimeConfig.oauth[provider] = defu(runtimeConfig.oauth[provider], atprotoProviderDefaultClientMetadata);
189
+ }
190
+ if (options.atproto) {
191
+ const missingDeps = [];
192
+ const peerDeps = ["@atproto/oauth-client-node", "@atproto/api"];
193
+ for (const pkg of peerDeps) {
194
+ await import(pkg).catch(() => {
195
+ missingDeps.push(pkg);
196
+ });
197
+ }
198
+ if (missingDeps.length > 0) {
199
+ logger.withTag("nuxt-auth-utils").error(`Missing dependencies for \`atproto\`, please install with:
200
+
201
+ \`npx nypm i ${missingDeps.join(" ")}\``);
202
+ process.exit(1);
203
+ }
204
+ for (const provider of atprotoProviders) {
205
+ addServerHandler({
206
+ handler: resolver.resolve("./runtime/server/routes/atproto/client-metadata.json.get.ts"),
207
+ route: "/" + getClientMetadataFilename(provider, runtimeConfig.oauth[provider]),
208
+ method: "get"
209
+ });
210
+ }
211
+ addServerImportsDir(resolver.resolve("./runtime/server/lib/atproto"));
212
+ }
160
213
  runtimeConfig.oauth.keycloak = defu(runtimeConfig.oauth.keycloak, {
161
214
  clientId: "",
162
215
  clientSecret: "",
@@ -28,12 +28,32 @@ export function useUserSession() {
28
28
  authReadyState.value = true;
29
29
  }
30
30
  };
31
+ const popupListener = (e) => {
32
+ if (e.key === "temp-nuxt-auth-utils-popup") {
33
+ fetch();
34
+ window.removeEventListener("storage", popupListener);
35
+ }
36
+ };
37
+ const openInPopup = (route, size = {}) => {
38
+ localStorage.setItem("temp-nuxt-auth-utils-popup", "true");
39
+ const width = size.width ?? 960;
40
+ const height = size.height ?? 600;
41
+ const top = (window.top?.outerHeight ?? 0) / 2 + (window.top?.screenY ?? 0) - height / 2;
42
+ const left = (window.top?.outerWidth ?? 0) / 2 + (window.top?.screenX ?? 0) - width / 2;
43
+ window.open(
44
+ route,
45
+ "nuxt-auth-utils-popup",
46
+ `width=${width}, height=${height}, top=${top}, left=${left}, toolbar=no, location=no, directories=no, status=no, menubar=no, scrollbars=no, resizable=no, copyhistory=no`
47
+ );
48
+ window.addEventListener("storage", popupListener);
49
+ };
31
50
  return {
32
51
  ready: computed(() => authReadyState.value),
33
52
  loggedIn: computed(() => Boolean(sessionState.value.user)),
34
53
  user: computed(() => sessionState.value.user || null),
35
54
  session: sessionState,
36
55
  fetch,
56
+ openInPopup,
37
57
  clear
38
58
  };
39
59
  }
@@ -7,4 +7,9 @@ export default defineNuxtPlugin(async (nuxtApp) => {
7
7
  await useUserSession().fetch();
8
8
  });
9
9
  }
10
+ if (localStorage.getItem("temp-nuxt-auth-utils-popup")) {
11
+ localStorage.removeItem("temp-nuxt-auth-utils-popup");
12
+ const error = useError();
13
+ if (!error.value) window.close();
14
+ }
10
15
  });
@@ -1,5 +1,5 @@
1
1
  declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<{
2
- [key: string]: unknown;
2
+ id: string;
3
3
  user?: import("../../types").User;
4
4
  }>>;
5
5
  export default _default;
@@ -0,0 +1,39 @@
1
+ import type { H3Event } from 'h3';
2
+ import type { NodeSavedSession, NodeSavedSessionStore, NodeSavedState, NodeSavedStateStore } from '@atproto/oauth-client-node';
3
+ import type { AppBskyActorDefs } from '@atproto/api';
4
+ import type { OAuthConfig } from '#auth-utils';
5
+ export interface OAuthBlueskyConfig {
6
+ /**
7
+ * Redirect URL to use for this authorization flow. It should only consist of the path, as the hostname must always match the client id's hostname.
8
+ * @default process.env.NUXT_OAUTH_BLUESKY_REDIRECT_URL
9
+ * @example '/auth/bluesky'
10
+ */
11
+ redirectUrl?: string;
12
+ /**
13
+ * Bluesky OAuth Scope. The `atproto` scope is required and will be added if not present.
14
+ * @default ['atproto']
15
+ * @see https://atproto.com/specs/oauth#authorization-scopes
16
+ * @example ['atproto', 'transition:generic']
17
+ */
18
+ scope?: string[];
19
+ }
20
+ type BlueSkyUser = AppBskyActorDefs.ProfileViewDetailed | Pick<AppBskyActorDefs.ProfileView, 'did'>;
21
+ type BlueSkyTokens = NodeSavedSession['tokenSet'];
22
+ export declare function defineOAuthBlueskyEventHandler({ config, onSuccess, onError }: OAuthConfig<OAuthBlueskyConfig, BlueSkyUser, BlueSkyTokens>): import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<void>>;
23
+ export declare class StateStore implements NodeSavedStateStore {
24
+ private event;
25
+ private readonly stateKey;
26
+ constructor(event: H3Event);
27
+ get(): Promise<NodeSavedState | undefined>;
28
+ set(key: string, val: NodeSavedState): Promise<void>;
29
+ del(): Promise<void>;
30
+ }
31
+ export declare class SessionStore implements NodeSavedSessionStore {
32
+ private event;
33
+ private readonly sessionKey;
34
+ constructor(event: H3Event);
35
+ get(): Promise<NodeSavedSession | undefined>;
36
+ set(key: string, val: NodeSavedSession): Promise<void>;
37
+ del(): Promise<void>;
38
+ }
39
+ export {};
@@ -0,0 +1,101 @@
1
+ import { createError, eventHandler, getQuery, sendRedirect, getCookie, setCookie, deleteCookie } from "h3";
2
+ import { NodeOAuthClient, OAuthCallbackError, OAuthResolverError, OAuthResponseError } from "@atproto/oauth-client-node";
3
+ import { Agent } from "@atproto/api";
4
+ import { getAtprotoClientMetadata } from "../../utils/atproto.js";
5
+ export function defineOAuthBlueskyEventHandler({ config, onSuccess, onError }) {
6
+ return eventHandler(async (event) => {
7
+ const clientMetadata = getAtprotoClientMetadata(event, "bluesky", config);
8
+ const scopes = clientMetadata.scope?.split(" ") ?? [];
9
+ const sessionStore = new SessionStore(event);
10
+ const stateStore = new StateStore(event);
11
+ const client = new NodeOAuthClient({
12
+ stateStore,
13
+ sessionStore,
14
+ clientMetadata
15
+ });
16
+ const query = getQuery(event);
17
+ if (!query.code) {
18
+ try {
19
+ const handle = query.handle?.toString();
20
+ if (!handle) throw createError({
21
+ statusCode: 400,
22
+ message: "Query parameter `handle` empty or missing. Please provide a valid Bluesky handle."
23
+ });
24
+ const url = await client.authorize(handle, { scope: clientMetadata.scope });
25
+ return sendRedirect(event, url.toString());
26
+ } catch (err) {
27
+ const error = (() => {
28
+ switch (true) {
29
+ case err instanceof OAuthResponseError:
30
+ return createError({
31
+ statusCode: 500,
32
+ message: `Bluesky login failed: ${err.errorDescription || "Unknown error"}`,
33
+ data: err.payload
34
+ });
35
+ case err instanceof OAuthResolverError:
36
+ return createError({
37
+ statusCode: 400,
38
+ message: `Bluesky login failed: ${err.message || "Unknown error"}`
39
+ });
40
+ default:
41
+ throw err;
42
+ }
43
+ })();
44
+ if (!onError) throw error;
45
+ return onError(event, error);
46
+ }
47
+ }
48
+ try {
49
+ const { session } = await client.callback(new URLSearchParams(query));
50
+ const sessionInfo = await sessionStore.get();
51
+ const profile = scopes.includes("transition:generic") ? (await new Agent(session).getProfile({ actor: session.did })).data : null;
52
+ sessionStore.del();
53
+ return onSuccess(event, {
54
+ user: profile ?? { did: session.did },
55
+ tokens: sessionInfo.tokenSet
56
+ });
57
+ } catch (err) {
58
+ if (!(err instanceof OAuthCallbackError)) throw err;
59
+ const error = createError({
60
+ statusCode: 500,
61
+ message: `Bluesky login failed: ${err.message || "Unknown error"}`
62
+ });
63
+ if (!onError) throw error;
64
+ return onError(event, error);
65
+ }
66
+ });
67
+ }
68
+ export class StateStore {
69
+ constructor(event) {
70
+ this.event = event;
71
+ }
72
+ stateKey = "oauth:bluesky:stat";
73
+ async get() {
74
+ const result = getCookie(this.event, this.stateKey);
75
+ if (!result) return;
76
+ return JSON.parse(atob(result));
77
+ }
78
+ async set(key, val) {
79
+ setCookie(this.event, this.stateKey, btoa(JSON.stringify(val)));
80
+ }
81
+ async del() {
82
+ deleteCookie(this.event, this.stateKey);
83
+ }
84
+ }
85
+ export class SessionStore {
86
+ constructor(event) {
87
+ this.event = event;
88
+ }
89
+ sessionKey = "oauth:bluesky:session";
90
+ async get() {
91
+ const result = getCookie(this.event, this.sessionKey);
92
+ if (!result) return;
93
+ return JSON.parse(atob(result));
94
+ }
95
+ async set(key, val) {
96
+ setCookie(this.event, this.sessionKey, btoa(JSON.stringify(val)));
97
+ }
98
+ async del() {
99
+ deleteCookie(this.event, this.sessionKey);
100
+ }
101
+ }
@@ -0,0 +1,51 @@
1
+ import type { OAuthConfig } from '#auth-utils';
2
+ export interface OAuthGiteaConfig {
3
+ /**
4
+ * Gitea OAuth Client ID
5
+ * @default process.env.NUXT_OAUTH_GITEA_CLIENT_ID
6
+ */
7
+ clientId?: string;
8
+ /**
9
+ * Gitea OAuth Client Secret
10
+ * @default process.env.NUXT_OAUTH_GITEA_CLIENT_SECRET
11
+ */
12
+ clientSecret?: string;
13
+ /**
14
+ * Gitea OAuth Scope
15
+ * @default ['read:user']
16
+ * @see https://docs.gitea.io/en-us/oauth2-provider/
17
+ * @example ['read:user']
18
+ */
19
+ scope?: string[];
20
+ /**
21
+ * Require email from user, adds the ['email'] scope if not present
22
+ * @default false
23
+ */
24
+ emailRequired?: boolean;
25
+ /**
26
+ * Gitea OAuth Authorization URL
27
+ * @default '/login/oauth/authorize'
28
+ */
29
+ authorizationURL?: string;
30
+ /**
31
+ * Gitea OAuth Token URL
32
+ * @default '/login/oauth/access_token'
33
+ */
34
+ tokenURL?: string;
35
+ /**
36
+ * Extra authorization parameters to provide to the authorization URL
37
+ * @see https://docs.gitea.io/en-us/oauth2-provider/
38
+ */
39
+ authorizationParams?: Record<string, string>;
40
+ /**
41
+ * Redirect URL to to allow overriding for situations like prod failing to determine public hostname
42
+ * @default process.env.NUXT_OAUTH_GITEA_REDIRECT_URL
43
+ */
44
+ redirectURL?: string;
45
+ /**
46
+ * URL of your Gitea instance
47
+ * @default 'https://gitea.com'
48
+ */
49
+ baseURL?: string;
50
+ }
51
+ export declare function defineOAuthGiteaEventHandler({ config, onSuccess, onError, }: OAuthConfig<OAuthGiteaConfig>): import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<void>>;
@@ -0,0 +1,85 @@
1
+ import { eventHandler, getQuery, sendRedirect } from "h3";
2
+ import { withQuery } from "ufo";
3
+ import { defu } from "defu";
4
+ import {
5
+ handleMissingConfiguration,
6
+ handleAccessTokenErrorResponse,
7
+ getOAuthRedirectURL,
8
+ requestAccessToken
9
+ } from "../utils.js";
10
+ import { useRuntimeConfig, createError } from "#imports";
11
+ export function defineOAuthGiteaEventHandler({
12
+ config,
13
+ onSuccess,
14
+ onError
15
+ }) {
16
+ return eventHandler(async (event) => {
17
+ const runtimeConfig = useRuntimeConfig(event).oauth?.gitea;
18
+ const baseURL = config?.baseURL ?? runtimeConfig.baseURL ?? "https://gitea.com";
19
+ config = defu(config, runtimeConfig, {
20
+ authorizationURL: `${baseURL}/login/oauth/authorize`,
21
+ tokenURL: `${baseURL}/login/oauth/access_token`,
22
+ authorizationParams: {}
23
+ });
24
+ const query = getQuery(event);
25
+ if (query.error) {
26
+ const error = createError({
27
+ statusCode: 401,
28
+ message: `Gitea login failed: ${query.error || "Unknown error"}`,
29
+ data: query
30
+ });
31
+ if (!onError) throw error;
32
+ return onError(event, error);
33
+ }
34
+ if (!config.clientId || !config.clientSecret) {
35
+ return handleMissingConfiguration(
36
+ event,
37
+ "gitea",
38
+ ["clientId", "clientSecret"],
39
+ onError
40
+ );
41
+ }
42
+ const redirectURL = config.redirectURL || getOAuthRedirectURL(event);
43
+ if (!query.code) {
44
+ config.scope = config.scope || [];
45
+ if (!config.scope.length) {
46
+ config.scope.push("read:user");
47
+ }
48
+ if (config.emailRequired && !config.scope.includes("email")) {
49
+ config.scope.push("email");
50
+ }
51
+ return sendRedirect(
52
+ event,
53
+ withQuery(config.authorizationURL, {
54
+ response_type: "code",
55
+ client_id: config.clientId,
56
+ redirect_uri: redirectURL,
57
+ scope: config.scope.join(" "),
58
+ ...config.authorizationParams
59
+ })
60
+ );
61
+ }
62
+ const tokens = await requestAccessToken(config.tokenURL, {
63
+ body: {
64
+ grant_type: "authorization_code",
65
+ client_id: config.clientId,
66
+ client_secret: config.clientSecret,
67
+ redirect_uri: redirectURL,
68
+ code: query.code
69
+ }
70
+ });
71
+ if (tokens.error) {
72
+ return handleAccessTokenErrorResponse(event, "gitea", tokens, onError);
73
+ }
74
+ const accessToken = tokens.access_token;
75
+ const user = await $fetch(`${baseURL}/api/v1/user`, {
76
+ headers: {
77
+ Authorization: `token ${accessToken}`
78
+ }
79
+ });
80
+ return onSuccess(event, {
81
+ user,
82
+ tokens
83
+ });
84
+ });
85
+ }
@@ -76,6 +76,7 @@ export function defineOAuthGitHubEventHandler({ config, onSuccess, onError }) {
76
76
  return onError(event, error);
77
77
  }
78
78
  user.email = primaryEmail.email;
79
+ user.email_verified = primaryEmail.verified;
79
80
  }
80
81
  return onSuccess(event, {
81
82
  user,
@@ -16,14 +16,15 @@ export function defineOAuthMicrosoftEventHandler({ config, onSuccess, onError })
16
16
  const tokenURL = config.tokenURL || `https://login.microsoftonline.com/${config.tenant}/oauth2/v2.0/token`;
17
17
  const redirectURL = config.redirectURL || getOAuthRedirectURL(event);
18
18
  if (!query.code) {
19
- const scope = config.scope && config.scope.length > 0 ? config.scope : ["User.Read"];
19
+ config.scope = config.scope && config.scope.length > 0 ? config.scope : ["User.Read"];
20
+ config.scope = [...new Set(config.scope)];
20
21
  return sendRedirect(
21
22
  event,
22
23
  withQuery(authorizationURL, {
23
24
  client_id: config.clientId,
24
25
  response_type: "code",
25
26
  redirect_uri: redirectURL,
26
- scope: scope.join(" "),
27
+ scope: config.scope.join(" "),
27
28
  ...config.authorizationParams
28
29
  })
29
30
  );
@@ -0,0 +1,129 @@
1
+ declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, {
2
+ redirect_uris: [string, ...string[]];
3
+ scope?: string | undefined;
4
+ response_types?: ["code" | "none" | "token" | "code id_token token" | "code id_token" | "code token" | "id_token token" | "id_token", ...("code" | "none" | "token" | "code id_token token" | "code id_token" | "code token" | "id_token token" | "id_token")[]] | undefined;
5
+ grant_types?: ["authorization_code" | "implicit" | "refresh_token" | "password" | "client_credentials" | "urn:ietf:params:oauth:grant-type:jwt-bearer" | "urn:ietf:params:oauth:grant-type:saml2-bearer", ...("authorization_code" | "implicit" | "refresh_token" | "password" | "client_credentials" | "urn:ietf:params:oauth:grant-type:jwt-bearer" | "urn:ietf:params:oauth:grant-type:saml2-bearer")[]] | undefined;
6
+ token_endpoint_auth_method?: "client_secret_basic" | "client_secret_jwt" | "client_secret_post" | "none" | "private_key_jwt" | "self_signed_tls_client_auth" | "tls_client_auth" | undefined;
7
+ token_endpoint_auth_signing_alg?: string | undefined;
8
+ userinfo_signed_response_alg?: string | undefined;
9
+ userinfo_encrypted_response_alg?: string | undefined;
10
+ jwks_uri?: string | undefined;
11
+ jwks?: {
12
+ keys: ({
13
+ kty: "RSA";
14
+ n: string;
15
+ e: string;
16
+ alg?: "RS256" | "RS384" | "RS512" | "PS256" | "PS384" | "PS512" | undefined;
17
+ kid?: string | undefined;
18
+ ext?: boolean | undefined;
19
+ use?: "sig" | "enc" | undefined;
20
+ key_ops?: ("sign" | "verify" | "encrypt" | "decrypt" | "wrapKey" | "unwrapKey" | "deriveKey" | "deriveBits")[] | undefined;
21
+ x5c?: string[] | undefined;
22
+ x5t?: string | undefined;
23
+ "x5t#S256"?: string | undefined;
24
+ x5u?: string | undefined;
25
+ d?: string | undefined;
26
+ p?: string | undefined;
27
+ q?: string | undefined;
28
+ dp?: string | undefined;
29
+ dq?: string | undefined;
30
+ qi?: string | undefined;
31
+ oth?: [{
32
+ d?: string | undefined;
33
+ r?: string | undefined;
34
+ t?: string | undefined;
35
+ }, ...{
36
+ d?: string | undefined;
37
+ r?: string | undefined;
38
+ t?: string | undefined;
39
+ }[]] | undefined;
40
+ } | {
41
+ kty: "EC";
42
+ crv: "P-256" | "P-384" | "P-521";
43
+ x: string;
44
+ y: string;
45
+ alg?: "ES256" | "ES384" | "ES512" | undefined;
46
+ kid?: string | undefined;
47
+ ext?: boolean | undefined;
48
+ use?: "sig" | "enc" | undefined;
49
+ key_ops?: ("sign" | "verify" | "encrypt" | "decrypt" | "wrapKey" | "unwrapKey" | "deriveKey" | "deriveBits")[] | undefined;
50
+ x5c?: string[] | undefined;
51
+ x5t?: string | undefined;
52
+ "x5t#S256"?: string | undefined;
53
+ x5u?: string | undefined;
54
+ d?: string | undefined;
55
+ } | {
56
+ kty: "EC";
57
+ crv: "secp256k1";
58
+ x: string;
59
+ y: string;
60
+ alg?: "ES256K" | undefined;
61
+ kid?: string | undefined;
62
+ ext?: boolean | undefined;
63
+ use?: "sig" | "enc" | undefined;
64
+ key_ops?: ("sign" | "verify" | "encrypt" | "decrypt" | "wrapKey" | "unwrapKey" | "deriveKey" | "deriveBits")[] | undefined;
65
+ x5c?: string[] | undefined;
66
+ x5t?: string | undefined;
67
+ "x5t#S256"?: string | undefined;
68
+ x5u?: string | undefined;
69
+ d?: string | undefined;
70
+ } | {
71
+ kty: "OKP";
72
+ crv: "Ed25519" | "Ed448";
73
+ x: string;
74
+ alg?: "EdDSA" | undefined;
75
+ kid?: string | undefined;
76
+ ext?: boolean | undefined;
77
+ use?: "sig" | "enc" | undefined;
78
+ key_ops?: ("sign" | "verify" | "encrypt" | "decrypt" | "wrapKey" | "unwrapKey" | "deriveKey" | "deriveBits")[] | undefined;
79
+ x5c?: string[] | undefined;
80
+ x5t?: string | undefined;
81
+ "x5t#S256"?: string | undefined;
82
+ x5u?: string | undefined;
83
+ d?: string | undefined;
84
+ } | {
85
+ kty: "oct";
86
+ k: string;
87
+ alg?: "HS256" | "HS384" | "HS512" | undefined;
88
+ kid?: string | undefined;
89
+ ext?: boolean | undefined;
90
+ use?: "sig" | "enc" | undefined;
91
+ key_ops?: ("sign" | "verify" | "encrypt" | "decrypt" | "wrapKey" | "unwrapKey" | "deriveKey" | "deriveBits")[] | undefined;
92
+ x5c?: string[] | undefined;
93
+ x5t?: string | undefined;
94
+ "x5t#S256"?: string | undefined;
95
+ x5u?: string | undefined;
96
+ } | {
97
+ kty: string;
98
+ alg?: string | undefined;
99
+ kid?: string | undefined;
100
+ ext?: boolean | undefined;
101
+ use?: "sig" | "enc" | undefined;
102
+ key_ops?: ("sign" | "verify" | "encrypt" | "decrypt" | "wrapKey" | "unwrapKey" | "deriveKey" | "deriveBits")[] | undefined;
103
+ x5c?: string[] | undefined;
104
+ x5t?: string | undefined;
105
+ "x5t#S256"?: string | undefined;
106
+ x5u?: string | undefined;
107
+ })[];
108
+ } | undefined;
109
+ application_type?: "web" | "native" | undefined;
110
+ subject_type?: "public" | "pairwise" | undefined;
111
+ request_object_signing_alg?: string | undefined;
112
+ id_token_signed_response_alg?: string | undefined;
113
+ authorization_signed_response_alg?: string | undefined;
114
+ authorization_encrypted_response_enc?: "A128CBC-HS256" | undefined;
115
+ authorization_encrypted_response_alg?: string | undefined;
116
+ client_id?: string | undefined;
117
+ client_name?: string | undefined;
118
+ client_uri?: string | undefined;
119
+ policy_uri?: string | undefined;
120
+ tos_uri?: string | undefined;
121
+ logo_uri?: string | undefined;
122
+ default_max_age?: number | undefined;
123
+ require_auth_time?: boolean | undefined;
124
+ contacts?: string[] | undefined;
125
+ tls_client_certificate_bound_access_tokens?: boolean | undefined;
126
+ dpop_bound_access_tokens?: boolean | undefined;
127
+ authorization_details_types?: string[] | undefined;
128
+ }>;
129
+ export default _default;
@@ -0,0 +1,18 @@
1
+ import { defineEventHandler, createError } from "h3";
2
+ import { getAtprotoClientMetadata } from "../../utils/atproto.js";
3
+ import { atprotoProviders, getClientMetadataFilename } from "../../../../utils/atproto";
4
+ import { useRuntimeConfig } from "#imports";
5
+ export default defineEventHandler((event) => {
6
+ const path = event.path.slice(1);
7
+ const runtimeConfig = useRuntimeConfig(event);
8
+ for (const provider of atprotoProviders) {
9
+ const config = runtimeConfig.oauth[provider];
10
+ if (getClientMetadataFilename(provider, config) === path) {
11
+ return getAtprotoClientMetadata(event, provider);
12
+ }
13
+ }
14
+ throw createError({
15
+ statusCode: 404,
16
+ message: "Provider not found"
17
+ });
18
+ });
@@ -0,0 +1,5 @@
1
+ import type { H3Event } from 'h3';
2
+ import type { OAuthClientMetadataInput } from '@atproto/oauth-client-node';
3
+ import type { OAuthBlueskyConfig } from '../lib/oauth/bluesky.js';
4
+ import type { ATProtoProvider, OAuthConfig } from '#auth-utils';
5
+ export declare function getAtprotoClientMetadata(event: H3Event, provider: ATProtoProvider, config?: OAuthConfig<OAuthBlueskyConfig>['config']): OAuthClientMetadataInput;
@@ -0,0 +1,35 @@
1
+ import { getOAuthRedirectURL } from "../lib/utils.js";
2
+ import { getClientMetadataFilename } from "../../../utils/atproto";
3
+ import { getRequestURL, useRuntimeConfig } from "#imports";
4
+ export function getAtprotoClientMetadata(event, provider, config) {
5
+ const providerRuntimeConfig = useRuntimeConfig(event).oauth[provider];
6
+ const scopes = [.../* @__PURE__ */ new Set(["atproto", ...config?.scope ?? [], ...providerRuntimeConfig.scope ?? []])];
7
+ const scope = scopes.join(" ");
8
+ const grantTypes = [.../* @__PURE__ */ new Set(["authorization_code", ...providerRuntimeConfig.grantTypes ?? []])];
9
+ const requestURL = getRequestURL(event);
10
+ const baseUrl = `${requestURL.protocol}//${requestURL.host}`;
11
+ const redirectURL = new URL(
12
+ config?.redirectUrl && baseUrl + config.redirectUrl || providerRuntimeConfig.redirectUris[0] && baseUrl + providerRuntimeConfig.redirectUris[0] || getOAuthRedirectURL(event)
13
+ );
14
+ const dev = import.meta.dev;
15
+ if (dev && redirectURL.hostname === "localhost") {
16
+ redirectURL.hostname = "127.0.0.1";
17
+ }
18
+ const redirectUris = (providerRuntimeConfig.redirectUris.length ? providerRuntimeConfig.redirectUris : [requestURL.pathname]).map((uri) => new URL(`${redirectURL.protocol}//${redirectURL.host}${uri}`).toString());
19
+ const clientId = dev ? `http://localhost?redirect_uri=${encodeURIComponent(redirectURL.toString())}&scope=${encodeURIComponent(scope)}` : `${baseUrl}/${getClientMetadataFilename("bluesky", providerRuntimeConfig)}`;
20
+ const clientMetadata = {
21
+ client_name: providerRuntimeConfig.clientName || void 0,
22
+ client_uri: providerRuntimeConfig.clientUri || void 0,
23
+ logo_uri: providerRuntimeConfig.logoUri || void 0,
24
+ policy_uri: providerRuntimeConfig.policyUri || void 0,
25
+ tos_uri: providerRuntimeConfig.tosUri || void 0,
26
+ client_id: clientId,
27
+ redirect_uris: redirectUris,
28
+ scope,
29
+ grant_types: grantTypes,
30
+ application_type: providerRuntimeConfig.applicationType,
31
+ token_endpoint_auth_method: providerRuntimeConfig.tokenEndpointAuthMethod,
32
+ dpop_bound_access_tokens: true
33
+ };
34
+ return clientMetadata;
35
+ }
@@ -20,7 +20,11 @@ export declare const sessionHooks: import("hookable").Hookable<SessionHooks, imp
20
20
  * @param event The Request (h3) event
21
21
  * @returns The user session
22
22
  */
23
- export declare function getUserSession(event: UseSessionEvent): Promise<UserSession>;
23
+ export declare function getUserSession(event: UseSessionEvent): Promise<{
24
+ id: string;
25
+ user?: import("#auth-utils").User;
26
+ secure?: import("#auth-utils").SecureSessionData;
27
+ }>;
24
28
  /**
25
29
  * Set a user session
26
30
  * @param event The Request (h3) event
@@ -4,7 +4,11 @@ import { createHooks } from "hookable";
4
4
  import { useRuntimeConfig } from "#imports";
5
5
  export const sessionHooks = createHooks();
6
6
  export async function getUserSession(event) {
7
- return (await _useSession(event)).data;
7
+ const session = await _useSession(event);
8
+ return {
9
+ id: session.id,
10
+ ...session.data
11
+ };
8
12
  }
9
13
  export async function setUserSession(event, data, config) {
10
14
  const session = await _useSession(event, config);
@@ -0,0 +1,62 @@
1
+ import type { OAuthClientMetadata } from '@atproto/oauth-client-node';
2
+ export interface AtprotoProviderClientMetadata {
3
+ /**
4
+ * The name of the client metadata file. This is used to construct the client ID.
5
+ * @example 'client-metadata.json'
6
+ * @example 'bluesky/client-metadata.json'
7
+ */
8
+ clientMetadataFilename: string;
9
+ /**
10
+ * The human-readable name of the client.
11
+ */
12
+ clientName?: OAuthClientMetadata['client_name'];
13
+ /**
14
+ * Not to be confused with client_id, this is a homepage URL for the client. If provided, the client_uri must have the same hostname as client_id.
15
+ */
16
+ clientUri?: OAuthClientMetadata['client_uri'];
17
+ /**
18
+ * The client's logo URL.
19
+ */
20
+ logoUri?: OAuthClientMetadata['logo_uri'];
21
+ /**
22
+ * URL to human-readable terms of service (ToS) for the client. Only https: URIs are allowed.
23
+ */
24
+ tosUri?: OAuthClientMetadata['tos_uri'];
25
+ /**
26
+ * URL to human-readable privacy policy for the client. Only https: URIs are allowed.
27
+ */
28
+ policyUri?: OAuthClientMetadata['policy_uri'];
29
+ /**
30
+ * Must be one of web or native, with web as the default if not specified. Note that this is field specified by OpenID/OIDC, which we are borrowing. Used by the Authorization Server to enforce the relevant "best current practices".
31
+ * @default 'web'
32
+ */
33
+ applicationType?: OAuthClientMetadata['application_type'];
34
+ /**
35
+ * `authorization_code` must always be included and will be added if missing. `refresh_token` is optional, but must be included if the client will make token refresh requests.
36
+ * @default ['authorization_code']
37
+ */
38
+ grantTypes?: OAuthClientMetadata['grant_types'];
39
+ /**
40
+ * All scope values which might be requested by this client are declared here. The atproto scope is required, and will be added if missing.
41
+ * @default ['atproto']
42
+ */
43
+ scope?: NonNullable<OAuthClientMetadata['scope']>[];
44
+ /**
45
+ * `code` must always be included and will be added if missing.
46
+ */
47
+ responseTypes?: OAuthClientMetadata['response_types'];
48
+ /**
49
+ * At least one redirect URI is required. The URL origin must match that of the `clientId`, so declare only the path.
50
+ * @example ['/auth/callback']
51
+ */
52
+ redirectUris: OAuthClientMetadata['redirect_uris'];
53
+ /**
54
+ * The token endpoint authentication method. `none` is the default, and the only supported value at this time.
55
+ * @default 'none'
56
+ */
57
+ tokenEndpointAuthMethod?: OAuthClientMetadata['token_endpoint_auth_method'];
58
+ /**
59
+ * DPoP is mandatory for all clients, so this must be present and true.
60
+ */
61
+ dpopBoundAccessTokens?: OAuthClientMetadata['dpop_bound_access_tokens'];
62
+ }
File without changes
@@ -1,3 +1,3 @@
1
1
  export type { User, UserSession, UserSessionRequired, UserSessionComposable, SecureSessionData } from './session.js';
2
- export type { OAuthConfig, OAuthProvider, OnError } from './oauth-config.js';
2
+ export type { OAuthConfig, OAuthProvider, ATProtoProvider, OnError } from './oauth-config.js';
3
3
  export type { WebAuthnCredential, WebAuthnRegisterEventHandlerOptions, WebAuthnAuthenticateEventHandlerOptions, WebAuthnComposable, WebAuthnUser, } from './webauthn.js';
@@ -1,5 +1,6 @@
1
1
  import type { H3Event, H3Error } from 'h3';
2
- export type OAuthProvider = 'atlassian' | 'auth0' | 'authentik' | 'battledotnet' | 'cognito' | 'discord' | 'dropbox' | 'facebook' | 'github' | 'gitlab' | 'google' | 'hubspot' | 'instagram' | 'keycloak' | 'line' | 'linear' | 'linkedin' | 'microsoft' | 'paypal' | 'polar' | 'spotify' | 'seznam' | 'steam' | 'strava' | 'tiktok' | 'twitch' | 'vk' | 'workos' | 'x' | 'xsuaa' | 'yandex' | 'zitadel' | 'apple' | (string & {});
2
+ export type ATProtoProvider = 'bluesky';
3
+ export type OAuthProvider = ATProtoProvider | 'atlassian' | 'auth0' | 'authentik' | 'battledotnet' | 'cognito' | 'discord' | 'dropbox' | 'facebook' | 'gitea' | 'github' | 'gitlab' | 'google' | 'hubspot' | 'instagram' | 'keycloak' | 'line' | 'linear' | 'linkedin' | 'microsoft' | 'paypal' | 'polar' | 'spotify' | 'seznam' | 'steam' | 'strava' | 'tiktok' | 'twitch' | 'vk' | 'workos' | 'x' | 'xsuaa' | 'yandex' | 'zitadel' | 'apple' | (string & {});
3
4
  export type OnError = (event: H3Event, error: H3Error) => Promise<void> | void;
4
5
  export interface OAuthConfig<TConfig, TUser = any, TTokens = any> {
5
6
  config?: TConfig;
@@ -4,6 +4,10 @@ export interface User {
4
4
  export interface SecureSessionData {
5
5
  }
6
6
  export interface UserSession {
7
+ /**
8
+ * Session ID
9
+ */
10
+ id: string;
7
11
  /**
8
12
  * User session data, available on client and server
9
13
  */
@@ -45,4 +49,11 @@ export interface UserSessionComposable {
45
49
  * Clear the user session and remove the session cookie.
46
50
  */
47
51
  clear: () => Promise<void>;
52
+ /**
53
+ * Open the OAuth route in a popup that auto-closes when successful.
54
+ */
55
+ openInPopup: (route: string, size?: {
56
+ width?: number;
57
+ height?: number;
58
+ }) => void;
48
59
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nuxt-auth-utils",
3
- "version": "0.5.10",
3
+ "version": "0.5.12",
4
4
  "description": "Add Authentication to Nuxt applications with secured & sealed cookies sessions.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -36,7 +36,7 @@
36
36
  },
37
37
  "dependencies": {
38
38
  "@adonisjs/hash": "^9.0.5",
39
- "@nuxt/kit": "^3.15.2",
39
+ "@nuxt/kit": "^3.15.4",
40
40
  "defu": "^6.1.4",
41
41
  "h3": "^1.14.0",
42
42
  "hookable": "^5.5.3",
@@ -49,6 +49,8 @@
49
49
  "uncrypto": "^0.1.3"
50
50
  },
51
51
  "peerDependencies": {
52
+ "@atproto/api": "^0.13.15",
53
+ "@atproto/oauth-client-node": "^0.2.0",
52
54
  "@simplewebauthn/browser": "^11.0.0",
53
55
  "@simplewebauthn/server": "^11.0.0"
54
56
  },
@@ -58,23 +60,29 @@
58
60
  },
59
61
  "@simplewebauthn/server": {
60
62
  "optional": true
63
+ },
64
+ "@atproto/oauth-client-node": {
65
+ "optional": true
66
+ },
67
+ "@atproto/api": {
68
+ "optional": true
61
69
  }
62
70
  },
63
71
  "devDependencies": {
64
- "@iconify-json/simple-icons": "^1.2.21",
65
- "@nuxt/devtools": "latest",
66
- "@nuxt/eslint-config": "^0.7.5",
72
+ "@iconify-json/simple-icons": "^1.2.23",
73
+ "@nuxt/devtools": "2.0.0-beta.7",
74
+ "@nuxt/eslint-config": "^0.7.6",
67
75
  "@nuxt/module-builder": "^0.8.4",
68
- "@nuxt/schema": "^3.15.2",
76
+ "@nuxt/schema": "^3.15.4",
69
77
  "@nuxt/test-utils": "^3.15.4",
70
78
  "@nuxt/ui": "^2.21.0",
71
79
  "@nuxt/ui-pro": "^1.7.0",
72
80
  "@simplewebauthn/types": "^12.0.0",
73
81
  "changelogen": "^0.5.7",
74
- "eslint": "^9.18.0",
75
- "nuxt": "^3.15.2",
82
+ "eslint": "^9.19.0",
83
+ "nuxt": "^3.15.4",
76
84
  "typescript": "5.6.3",
77
- "vitest": "^3.0.3",
85
+ "vitest": "^3.0.5",
78
86
  "vue-tsc": "^2.2.0"
79
87
  }
80
88
  }