nuxt-auth-utils 0.5.6 → 0.5.8

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.
Files changed (30) hide show
  1. package/README.md +3 -0
  2. package/dist/module.json +1 -1
  3. package/dist/module.mjs +18 -1
  4. package/dist/runtime/server/lib/oauth/atlassian.d.ts +66 -0
  5. package/dist/runtime/server/lib/oauth/atlassian.js +99 -0
  6. package/dist/runtime/server/lib/oauth/cognito.d.ts +0 -5
  7. package/dist/runtime/server/lib/oauth/cognito.js +14 -6
  8. package/dist/runtime/server/lib/oauth/discord.js +3 -0
  9. package/dist/runtime/server/lib/oauth/dropbox.js +2 -2
  10. package/dist/runtime/server/lib/oauth/facebook.js +11 -5
  11. package/dist/runtime/server/lib/oauth/github.js +9 -3
  12. package/dist/runtime/server/lib/oauth/gitlab.d.ts +5 -0
  13. package/dist/runtime/server/lib/oauth/gitlab.js +6 -4
  14. package/dist/runtime/server/lib/oauth/google.js +2 -2
  15. package/dist/runtime/server/lib/oauth/hubspot.d.ts +26 -0
  16. package/dist/runtime/server/lib/oauth/hubspot.js +57 -0
  17. package/dist/runtime/server/lib/oauth/instagram.js +11 -5
  18. package/dist/runtime/server/lib/oauth/keycloak.d.ts +9 -2
  19. package/dist/runtime/server/lib/oauth/keycloak.js +2 -1
  20. package/dist/runtime/server/lib/oauth/line.d.ts +45 -0
  21. package/dist/runtime/server/lib/oauth/line.js +75 -0
  22. package/dist/runtime/server/lib/oauth/paypal.js +2 -2
  23. package/dist/runtime/server/lib/oauth/seznam.js +2 -2
  24. package/dist/runtime/server/lib/oauth/twitch.js +2 -2
  25. package/dist/runtime/server/lib/oauth/x.js +2 -2
  26. package/dist/runtime/server/lib/webauthn/authenticate.d.ts +0 -5
  27. package/dist/runtime/server/lib/webauthn/register.d.ts +0 -8
  28. package/dist/runtime/server/plugins/oauth.d.ts +1 -1
  29. package/dist/runtime/types/oauth-config.d.ts +1 -1
  30. package/package.json +18 -17
package/README.md CHANGED
@@ -204,6 +204,7 @@ It can also be set using environment variables:
204
204
 
205
205
  #### Supported OAuth Providers
206
206
 
207
+ - Atlassian
207
208
  - Auth0
208
209
  - Authentik
209
210
  - AWS Cognito
@@ -214,8 +215,10 @@ It can also be set using environment variables:
214
215
  - GitHub
215
216
  - GitLab
216
217
  - Google
218
+ - Hubspot
217
219
  - Instagram
218
220
  - Keycloak
221
+ - Line
219
222
  - Linear
220
223
  - LinkedIn
221
224
  - Microsoft
package/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "auth-utils",
3
3
  "configKey": "auth",
4
- "version": "0.5.6",
4
+ "version": "0.5.8",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "0.8.4",
7
7
  "unbuild": "2.0.0"
package/dist/module.mjs CHANGED
@@ -102,7 +102,8 @@ const module = defineNuxtModule({
102
102
  runtimeConfig.oauth.gitlab = defu(runtimeConfig.oauth.gitlab, {
103
103
  clientId: "",
104
104
  clientSecret: "",
105
- redirectURL: ""
105
+ redirectURL: "",
106
+ baseURL: "https://gitlab.com"
106
107
  });
107
108
  runtimeConfig.oauth.spotify = defu(runtimeConfig.oauth.spotify, {
108
109
  clientId: "",
@@ -157,6 +158,7 @@ const module = defineNuxtModule({
157
158
  clientId: "",
158
159
  clientSecret: "",
159
160
  serverUrl: "",
161
+ serverUrlInternal: "",
160
162
  realm: "",
161
163
  redirectURL: ""
162
164
  });
@@ -254,6 +256,21 @@ const module = defineNuxtModule({
254
256
  clientSecret: "",
255
257
  redirectURL: ""
256
258
  });
259
+ runtimeConfig.oauth.hubspot = defu(runtimeConfig.oauth.hubspot, {
260
+ clientId: "",
261
+ clientSecret: "",
262
+ redirectURL: ""
263
+ });
264
+ runtimeConfig.oauth.line = defu(runtimeConfig.oauth.line, {
265
+ clientId: "",
266
+ clientSecret: "",
267
+ redirectURL: ""
268
+ });
269
+ runtimeConfig.oauth.atlassian = defu(runtimeConfig.oauth.atlassian, {
270
+ clientId: "",
271
+ clientSecret: "",
272
+ redirectURL: ""
273
+ });
257
274
  }
258
275
  });
259
276
 
@@ -0,0 +1,66 @@
1
+ import type { OAuthConfig } from '#auth-utils';
2
+ /**
3
+ * @see https://developer.atlassian.com/cloud/jira/platform/oauth-2-3lo-apps
4
+ */
5
+ export interface OAuthAtlassianConfig {
6
+ /**
7
+ * Atlassian OAuth Client ID
8
+ * @default process.env.NUXT_OAUTH_ATLASSIAN_CLIENT_ID
9
+ * @see https://developer.atlassian.com/console/myapps
10
+ */
11
+ clientId?: string;
12
+ /**
13
+ * Atlassian OAuth Client Secret
14
+ * @default process.env.NUXT_OAUTH_ATLASSIAN_CLIENT_SECRET
15
+ * @see https://developer.atlassian.com/console/myapps
16
+ */
17
+ clientSecret?: string;
18
+ /**
19
+ * Redirect URL to allow overriding for situations like prod failing to determine public hostname
20
+ * @default process.env.NUXT_OAUTH_ATLASSIAN_REDIRECT_URL or current URL
21
+ * @see https://developer.atlassian.com/console/myapps
22
+ */
23
+ redirectURL?: string;
24
+ /**
25
+ * Atlassian OAuth Scope
26
+ * @default ['read:me', 'read:account']
27
+ * @see [Jira scopes](https://developer.atlassian.com/cloud/jira/platform/scopes-for-oauth-2-3LO-and-forge-apps) | [Confluence scopes](https://developer.atlassian.com/cloud/confluence/scopes-for-oauth-2-3LO-and-forge-apps)
28
+ *
29
+ * @example
30
+ * User identity API: ['read:me', 'read:account']
31
+ * Confluence API: ['read:confluence-user']
32
+ * BRIE API: ['read:account:brie]
33
+ * Jira platform REST API: ['read:jira-user']
34
+ * Personal data reporting API: ['report:personal-data']
35
+ */
36
+ scope?: string[];
37
+ /**
38
+ * Atlassian OAuth Audience URL
39
+ * @default 'https://api.atlassian.com'
40
+ */
41
+ audienceURL?: string;
42
+ /**
43
+ * Atlassian OAuth Authorization URL
44
+ * @default 'https://auth.atlassian.com/authorize'
45
+ */
46
+ authorizationURL?: string;
47
+ /**
48
+ * Atlassian OAuth Token URL
49
+ * @default 'https://auth.atlassian.com/oauth/token'
50
+ */
51
+ tokenURL?: string;
52
+ /**
53
+ * Require email from user, adds the ['read:me'] scope if not present
54
+ * @default false
55
+ */
56
+ emailHasToBeVerified?: boolean;
57
+ /**
58
+ * Extra authorization parameters to provide to the authorization URL
59
+ * @default {}
60
+ */
61
+ authorizationParams?: Record<string, string>;
62
+ }
63
+ /**
64
+ * Atlassian User identity, Confluence, BRIE, Jira platform, Atlassian Personal data reporting
65
+ */
66
+ export declare function defineOAuthAtlassianEventHandler({ config, onSuccess, onError, }: OAuthConfig<OAuthAtlassianConfig>): import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<void>>;
@@ -0,0 +1,99 @@
1
+ import { eventHandler, getQuery, sendRedirect } from "h3";
2
+ import { withQuery } from "ufo";
3
+ import { defu } from "defu";
4
+ import { randomUUID } from "uncrypto";
5
+ import { handleMissingConfiguration, handleAccessTokenErrorResponse, getOAuthRedirectURL, requestAccessToken } from "../utils.js";
6
+ import { useRuntimeConfig, createError } from "#imports";
7
+ export function defineOAuthAtlassianEventHandler({
8
+ config,
9
+ onSuccess,
10
+ onError
11
+ }) {
12
+ return eventHandler(async (event) => {
13
+ config = defu(config, useRuntimeConfig().oauth?.atlassian, {
14
+ authorizationURL: "https://auth.atlassian.com/authorize",
15
+ tokenURL: "https://auth.atlassian.com/oauth/token",
16
+ audienceURL: "https://api.atlassian.com",
17
+ scope: ["read:me", "read:account"],
18
+ authorizationParams: {}
19
+ });
20
+ if (!config.clientId || !config.clientSecret) {
21
+ return handleMissingConfiguration(event, "atlassian", ["clientId", "clientSecret"], onError);
22
+ }
23
+ if (config.scope?.length === 0) {
24
+ config.scope = ["read:me"];
25
+ }
26
+ if (config.emailHasToBeVerified && !config.scope?.includes("read:me")) {
27
+ config.scope?.push("read:me");
28
+ }
29
+ const query = getQuery(event);
30
+ const redirectURL = config.redirectURL || getOAuthRedirectURL(event);
31
+ if (!query.code) {
32
+ return sendRedirect(
33
+ event,
34
+ withQuery(config.authorizationURL, {
35
+ audience: config.audienceURL,
36
+ client_id: config.clientId,
37
+ scope: config.scope?.join(" "),
38
+ redirect_uri: redirectURL,
39
+ state: randomUUID(),
40
+ response_type: "code",
41
+ prompt: "consent",
42
+ ...config.authorizationParams
43
+ })
44
+ );
45
+ }
46
+ if (query.error) {
47
+ const error = createError({
48
+ statusCode: 401,
49
+ message: `Atlassian login failed: ${query.error || "Unknown error"}`,
50
+ data: query
51
+ });
52
+ if (!onError) throw error;
53
+ return onError(event, error);
54
+ }
55
+ const tokens = await requestAccessToken(config.tokenURL, {
56
+ headers: {
57
+ "Content-Type": "application/json"
58
+ },
59
+ body: {
60
+ grant_type: "authorization_code",
61
+ client_id: config.clientId,
62
+ client_secret: config.clientSecret,
63
+ code: query.code,
64
+ redirect_uri: redirectURL
65
+ }
66
+ });
67
+ if (tokens.error || !tokens.access_token) {
68
+ return handleAccessTokenErrorResponse(event, "atlassian", tokens, onError);
69
+ }
70
+ const user = await $fetch("https://api.atlassian.com/me", {
71
+ headers: {
72
+ "Authorization": `Bearer ${tokens.access_token}`,
73
+ "Content-Type": "application/json"
74
+ }
75
+ });
76
+ if (user.account_status === "inactive") {
77
+ const error = createError({
78
+ statusCode: 403,
79
+ statusMessage: "Atlassian account is inactive",
80
+ data: { accountStatus: user.account_status }
81
+ });
82
+ if (!onError) throw error;
83
+ return onError(event, error);
84
+ }
85
+ if (!user.email_verified) {
86
+ const error = createError({
87
+ statusCode: 400,
88
+ statusMessage: "Email address is not verified",
89
+ data: { email: user.email }
90
+ });
91
+ if (!onError) throw error;
92
+ return onError(event, error);
93
+ }
94
+ return onSuccess(event, {
95
+ user,
96
+ tokens
97
+ });
98
+ });
99
+ }
@@ -35,10 +35,5 @@ export interface OAuthCognitoConfig {
35
35
  * @default process.env.NUXT_OAUTH_COGNITO_REDIRECT_URL or current URL
36
36
  */
37
37
  redirectURL?: string;
38
- /**
39
- * AWS Cognito App Custom Domain – some pool configurations require this
40
- * @default ''
41
- */
42
- domain?: string;
43
38
  }
44
39
  export declare function defineOAuthCognitoEventHandler({ config, onSuccess, onError }: OAuthConfig<OAuthCognitoConfig>): import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<void>>;
@@ -1,7 +1,8 @@
1
+ import { defu } from "defu";
1
2
  import { eventHandler, getQuery, sendRedirect } from "h3";
3
+ import { discovery } from "openid-client";
2
4
  import { withQuery } from "ufo";
3
- import { defu } from "defu";
4
- import { handleMissingConfiguration, handleAccessTokenErrorResponse, getOAuthRedirectURL, requestAccessToken } from "../utils.js";
5
+ import { getOAuthRedirectURL, handleAccessTokenErrorResponse, handleMissingConfiguration, requestAccessToken } from "../utils.js";
5
6
  import { useRuntimeConfig } from "#imports";
6
7
  export function defineOAuthCognitoEventHandler({ config, onSuccess, onError }) {
7
8
  return eventHandler(async (event) => {
@@ -11,9 +12,16 @@ export function defineOAuthCognitoEventHandler({ config, onSuccess, onError }) {
11
12
  if (!config.clientId || !config.clientSecret || !config.userPoolId || !config.region) {
12
13
  return handleMissingConfiguration(event, "cognito", ["clientId", "clientSecret", "userPoolId", "region"], onError);
13
14
  }
14
- const urlBase = config?.domain || `${config.userPoolId}.auth.${config.region}.amazoncognito.com`;
15
- const authorizationURL = `https://${urlBase}/oauth2/authorize`;
16
- const tokenURL = `https://${urlBase}/oauth2/token`;
15
+ const congitoDiscoveryUrl = new URL(`https://cognito-idp.${config.region}.amazonaws.com/${config.userPoolId}/.well-known/openid-configuration`);
16
+ const issuer = await discovery(congitoDiscoveryUrl, config.clientId, config.clientSecret);
17
+ const {
18
+ authorization_endpoint: authorizationURL,
19
+ token_endpoint: tokenURL,
20
+ userinfo_endpoint: userinfoURL,
21
+ // TODO: implement logout
22
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
23
+ end_session_endpoint: logoutURL
24
+ } = issuer.serverMetadata();
17
25
  const query = getQuery(event);
18
26
  const redirectURL = config.redirectURL || getOAuthRedirectURL(event);
19
27
  if (!query.code) {
@@ -46,7 +54,7 @@ export function defineOAuthCognitoEventHandler({ config, onSuccess, onError }) {
46
54
  }
47
55
  const tokenType = tokens.token_type;
48
56
  const accessToken = tokens.access_token;
49
- const user = await $fetch(`https://${urlBase}/oauth2/userInfo`, {
57
+ const user = await $fetch(userinfoURL, {
50
58
  headers: {
51
59
  Authorization: `${tokenType} ${accessToken}`
52
60
  }
@@ -15,6 +15,9 @@ export function defineOAuthDiscordEventHandler({ config, onSuccess, onError }) {
15
15
  if (!config.clientId || !config.clientSecret) {
16
16
  return handleMissingConfiguration(event, "discord", ["clientId", "clientSecret"], onError);
17
17
  }
18
+ if (query.error) {
19
+ return handleAccessTokenErrorResponse(event, "discord", query, onError);
20
+ }
18
21
  const redirectURL = config.redirectURL || getOAuthRedirectURL(event);
19
22
  if (!query.code) {
20
23
  config.scope = config.scope || [];
@@ -11,8 +11,8 @@ export function defineOAuthDropboxEventHandler({ config, onSuccess, onError }) {
11
11
  authorizationParams: {}
12
12
  });
13
13
  const query = getQuery(event);
14
- if (!config.clientId) {
15
- return handleMissingConfiguration(event, "dropbox", ["clientId"], onError);
14
+ if (!config.clientId || !config.clientSecret) {
15
+ return handleMissingConfiguration(event, "dropbox", ["clientId", "clientSecret"], onError);
16
16
  }
17
17
  const redirectURL = config.redirectURL || getOAuthRedirectURL(event);
18
18
  if (!query.code) {
@@ -1,8 +1,8 @@
1
- import { eventHandler, getQuery, sendRedirect } from "h3";
1
+ import { eventHandler, getQuery, sendRedirect, createError } from "h3";
2
2
  import { withQuery } from "ufo";
3
3
  import { defu } from "defu";
4
4
  import { handleMissingConfiguration, handleAccessTokenErrorResponse, getOAuthRedirectURL, requestAccessToken } from "../utils.js";
5
- import { useRuntimeConfig, createError } from "#imports";
5
+ import { useRuntimeConfig } from "#imports";
6
6
  export function defineOAuthFacebookEventHandler({
7
7
  config,
8
8
  onSuccess,
@@ -24,8 +24,8 @@ export function defineOAuthFacebookEventHandler({
24
24
  if (!onError) throw error;
25
25
  return onError(event, error);
26
26
  }
27
- if (!config.clientId) {
28
- return handleMissingConfiguration(event, "facebook", ["clientId"], onError);
27
+ if (!config.clientId || !config.clientSecret) {
28
+ return handleMissingConfiguration(event, "facebook", ["clientId", "clientSecret"], onError);
29
29
  }
30
30
  const redirectURL = config.redirectURL || getOAuthRedirectURL(event);
31
31
  if (!query.code) {
@@ -58,7 +58,13 @@ export function defineOAuthFacebookEventHandler({
58
58
  `https://graph.facebook.com/v19.0/me?fields=${fields}&access_token=${accessToken}`
59
59
  );
60
60
  if (!user) {
61
- throw new Error("Facebook login failed: no user found");
61
+ const error = createError({
62
+ statusCode: 500,
63
+ message: "Could not get Facebook user",
64
+ data: tokens
65
+ });
66
+ if (!onError) throw error;
67
+ return onError(event, error);
62
68
  }
63
69
  return onSuccess(event, {
64
70
  user,
@@ -1,8 +1,8 @@
1
- import { eventHandler, getQuery, sendRedirect } from "h3";
1
+ import { eventHandler, getQuery, sendRedirect, createError } from "h3";
2
2
  import { withQuery } from "ufo";
3
3
  import { defu } from "defu";
4
4
  import { handleMissingConfiguration, handleAccessTokenErrorResponse, getOAuthRedirectURL, requestAccessToken } from "../utils.js";
5
- import { useRuntimeConfig, createError } from "#imports";
5
+ import { useRuntimeConfig } from "#imports";
6
6
  export function defineOAuthGitHubEventHandler({ config, onSuccess, onError }) {
7
7
  return eventHandler(async (event) => {
8
8
  config = defu(config, useRuntimeConfig(event).oauth?.github, {
@@ -67,7 +67,13 @@ export function defineOAuthGitHubEventHandler({ config, onSuccess, onError }) {
67
67
  });
68
68
  const primaryEmail = emails.find((email) => email.primary);
69
69
  if (!primaryEmail) {
70
- throw new Error("GitHub login failed: no user email found");
70
+ const error = createError({
71
+ statusCode: 500,
72
+ message: "Could not get GitHub user email",
73
+ data: tokens
74
+ });
75
+ if (!onError) throw error;
76
+ return onError(event, error);
71
77
  }
72
78
  user.email = primaryEmail.email;
73
79
  }
@@ -43,5 +43,10 @@ export interface OAuthGitLabConfig {
43
43
  * @default process.env.NUXT_OAUTH_GITLAB_REDIRECT_URL
44
44
  */
45
45
  redirectURL?: string;
46
+ /**
47
+ * URL of your instance, if you are self-hosting
48
+ * @default https://gitlab.com
49
+ */
50
+ baseURL?: string;
46
51
  }
47
52
  export declare function defineOAuthGitLabEventHandler({ config, onSuccess, onError, }: OAuthConfig<OAuthGitLabConfig>): import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<void>>;
@@ -14,9 +14,11 @@ export function defineOAuthGitLabEventHandler({
14
14
  onError
15
15
  }) {
16
16
  return eventHandler(async (event) => {
17
- config = defu(config, useRuntimeConfig(event).oauth?.gitlab, {
18
- authorizationURL: "https://gitlab.com/oauth/authorize",
19
- tokenURL: "https://gitlab.com/oauth/token",
17
+ const runtimeConfig = useRuntimeConfig(event).oauth?.gitlab;
18
+ const baseURL = config?.baseURL ?? runtimeConfig.baseURL ?? "https://gitlab.com";
19
+ config = defu(config, runtimeConfig, {
20
+ authorizationURL: `${baseURL}/oauth/authorize`,
21
+ tokenURL: `${baseURL}/oauth/token`,
20
22
  authorizationParams: {}
21
23
  });
22
24
  const query = getQuery(event);
@@ -70,7 +72,7 @@ export function defineOAuthGitLabEventHandler({
70
72
  return handleAccessTokenErrorResponse(event, "gitlab", tokens, onError);
71
73
  }
72
74
  const accessToken = tokens.access_token;
73
- const user = await $fetch("https://gitlab.com/api/v4/user", {
75
+ const user = await $fetch(`${baseURL}/api/v4/user`, {
74
76
  headers: {
75
77
  Authorization: `Bearer ${accessToken}`
76
78
  }
@@ -16,8 +16,8 @@ export function defineOAuthGoogleEventHandler({
16
16
  authorizationParams: {}
17
17
  });
18
18
  const query = getQuery(event);
19
- if (!config.clientId) {
20
- return handleMissingConfiguration(event, "google", ["clientId"], onError);
19
+ if (!config.clientId || !config.clientSecret) {
20
+ return handleMissingConfiguration(event, "google", ["clientId", "clientSecret"], onError);
21
21
  }
22
22
  const redirectURL = config.redirectURL || getOAuthRedirectURL(event);
23
23
  if (!query.code) {
@@ -0,0 +1,26 @@
1
+ import type { OAuthConfig } from '#auth-utils';
2
+ export interface OAuthHubspotConfig {
3
+ /**
4
+ * Hubspot OAuth Client ID
5
+ * @default process.env.NUXT_OAUTH_HUBSPOT_CLIENT_ID
6
+ */
7
+ clientId?: string;
8
+ /**
9
+ * Hubspot OAuth Client Secret
10
+ * @default process.env.NUXT_OAUTH_HUBSPOT_CLIENT_SECRET
11
+ */
12
+ clientSecret?: string;
13
+ /**
14
+ * Hubspot OAuth Redirect URL
15
+ * @default process.env.NUXT_OAUTH_HUBSPOT_REDIRECT_URL
16
+ */
17
+ redirectURL?: string;
18
+ /**
19
+ * Hubspot OAuth Scope
20
+ * @default ['oauth']
21
+ * @see https://developers.hubspot.com/beta-docs/guides/apps/authentication/scopes
22
+ * @example ['accounting', 'automation', 'actions']
23
+ */
24
+ scope?: string[];
25
+ }
26
+ export declare function defineOAuthHubspotEventHandler({ config, onSuccess, onError }: OAuthConfig<OAuthHubspotConfig>): import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<void>>;
@@ -0,0 +1,57 @@
1
+ import { eventHandler, getQuery, sendRedirect } from "h3";
2
+ import { withQuery } from "ufo";
3
+ import defu from "defu";
4
+ import {
5
+ getOAuthRedirectURL,
6
+ handleAccessTokenErrorResponse,
7
+ handleMissingConfiguration,
8
+ requestAccessToken
9
+ } from "../utils.js";
10
+ import { useRuntimeConfig } from "#imports";
11
+ export function defineOAuthHubspotEventHandler({ config, onSuccess, onError }) {
12
+ return eventHandler(async (event) => {
13
+ config = defu(config, useRuntimeConfig(event).oauth?.hubspot);
14
+ if (!config.clientId || !config.clientSecret || !config.redirectURL) {
15
+ return handleMissingConfiguration(event, "hubspot", ["clientId", "clientSecret", "redirectURL"], onError);
16
+ }
17
+ const query = getQuery(event);
18
+ const redirectURL = config.redirectURL || getOAuthRedirectURL(event);
19
+ if (query.error) {
20
+ return handleAccessTokenErrorResponse(event, "hubspot", query, onError);
21
+ }
22
+ if (!query.code) {
23
+ return sendRedirect(
24
+ event,
25
+ withQuery("https://app.hubspot.com/oauth/authorize", {
26
+ client_id: config.clientId,
27
+ redirect_uri: redirectURL,
28
+ scope: config.scope?.join(" ") || "oauth"
29
+ })
30
+ );
31
+ }
32
+ const tokens = await requestAccessToken(
33
+ "https://api.hubapi.com/oauth/v1/token",
34
+ {
35
+ body: {
36
+ client_id: config.clientId,
37
+ client_secret: config.clientSecret,
38
+ code: query.code,
39
+ redirect_uri: redirectURL,
40
+ grant_type: "authorization_code"
41
+ }
42
+ }
43
+ );
44
+ if (tokens.error) {
45
+ return handleAccessTokenErrorResponse(event, "hubspot", tokens, onError);
46
+ }
47
+ const info = await $fetch("https://api.hubapi.com/oauth/v1/access-tokens/" + tokens.access_token);
48
+ return onSuccess(event, {
49
+ user: {
50
+ id: info.user_id,
51
+ email: info.user,
52
+ domain: info.hub_domain
53
+ },
54
+ tokens
55
+ });
56
+ });
57
+ }
@@ -1,8 +1,8 @@
1
- import { eventHandler, getQuery, sendRedirect } from "h3";
1
+ import { eventHandler, getQuery, sendRedirect, createError } from "h3";
2
2
  import { withQuery } from "ufo";
3
3
  import { defu } from "defu";
4
4
  import { handleMissingConfiguration, handleAccessTokenErrorResponse, getOAuthRedirectURL, requestAccessToken } from "../utils.js";
5
- import { useRuntimeConfig, createError } from "#imports";
5
+ import { useRuntimeConfig } from "#imports";
6
6
  export function defineOAuthInstagramEventHandler({
7
7
  config,
8
8
  onSuccess,
@@ -29,8 +29,8 @@ export function defineOAuthInstagramEventHandler({
29
29
  if (!onError) throw error;
30
30
  return onError(event, error);
31
31
  }
32
- if (!config.clientId) {
33
- return handleMissingConfiguration(event, "instagram", ["clientId"], onError);
32
+ if (!config.clientId || !config.clientSecret) {
33
+ return handleMissingConfiguration(event, "instagram", ["clientId", "clientSecret"], onError);
34
34
  }
35
35
  const redirectURL = config.redirectURL || getOAuthRedirectURL(event);
36
36
  if (!query.code) {
@@ -64,7 +64,13 @@ export function defineOAuthInstagramEventHandler({
64
64
  `https://graph.instagram.com/v21.0/me?fields=${fields}&access_token=${accessToken}`
65
65
  );
66
66
  if (!user) {
67
- throw new Error("Instagram login failed: no user found");
67
+ const error = createError({
68
+ statusCode: 500,
69
+ message: "Could not get Instagram user",
70
+ data: tokens
71
+ });
72
+ if (!onError) throw error;
73
+ return onError(event, error);
68
74
  }
69
75
  return onSuccess(event, {
70
76
  user,
@@ -12,10 +12,17 @@ export interface OAuthKeycloakConfig {
12
12
  clientSecret?: string;
13
13
  /**
14
14
  * Keycloak OAuth Server URL
15
- * @example http://192.168.1.10:8080/auth
15
+ * @example http://192.168.1.10:8080
16
16
  * @default process.env.NUXT_OAUTH_KEYCLOAK_SERVER_URL
17
17
  */
18
18
  serverUrl?: string;
19
+ /**
20
+ * Optional Keycloak OAuth Server URL to use internally, e.g. if Nuxt connects to a Docker hostname while the browser
21
+ * redirect goes to localhost
22
+ * @example http://keycloak:8080
23
+ * @default process.env.NUXT_OAUTH_KEYCLOAK_SERVER_URL_INTERNAL
24
+ */
25
+ serverUrlInternal?: string;
19
26
  /**
20
27
  * Keycloak OAuth Realm
21
28
  * @default process.env.NUXT_OAUTH_KEYCLOAK_REALM
@@ -33,7 +40,7 @@ export interface OAuthKeycloakConfig {
33
40
  */
34
41
  authorizationParams?: Record<string, string>;
35
42
  /**
36
- * Redirect URL to to allow overriding for situations like prod failing to determine public hostname
43
+ * Redirect URL to allow overriding for situations like prod failing to determine public hostname
37
44
  * @default process.env.NUXT_OAUTH_KEYCLOAK_REDIRECT_URL or current URL
38
45
  */
39
46
  redirectURL?: string;
@@ -26,8 +26,9 @@ export function defineOAuthKeycloakEventHandler({
26
26
  return handleMissingConfiguration(event, "keycloak", ["clientId", "clientSecret", "serverUrl", "realm"], onError);
27
27
  }
28
28
  const realmURL = `${config.serverUrl}/realms/${config.realm}`;
29
+ const realmURLInternal = `${config.serverUrlInternal || config.serverUrl}/realms/${config.realm}`;
29
30
  const authorizationURL = `${realmURL}/protocol/openid-connect/auth`;
30
- const tokenURL = `${realmURL}/protocol/openid-connect/token`;
31
+ const tokenURL = `${realmURLInternal}/protocol/openid-connect/token`;
31
32
  const redirectURL = config.redirectURL || getOAuthRedirectURL(event);
32
33
  if (!query.code) {
33
34
  config.scope = config.scope || ["openid"];
@@ -0,0 +1,45 @@
1
+ import type { OAuthConfig } from '#auth-utils';
2
+ export interface OAuthLineConfig {
3
+ /**
4
+ * Line OAuth Client ID
5
+ * @default process.env.NUXT_OAUTH_LINE_CLIENT_ID
6
+ */
7
+ clientId?: string;
8
+ /**
9
+ * Line OAuth Client Secret
10
+ * @default process.env.NUXT_OAUTH_LINE_CLIENT_SECRET
11
+ */
12
+ clientSecret?: string;
13
+ /**
14
+ * Line OAuth Scope
15
+ * @default ['profile', 'openid']
16
+ * @see https://developers.line.biz/en/docs/line-login/integrate-line-login/
17
+ */
18
+ scope?: string[];
19
+ /**
20
+ * Line OAuth Authorization URL
21
+ * @default 'https://access.line.me/oauth2/v2.1/authorize'
22
+ */
23
+ authorizationURL?: string;
24
+ /**
25
+ * Line OAuth Token URL
26
+ * @default 'https://api.line.me/oauth2/v2.1/token'
27
+ */
28
+ tokenURL?: string;
29
+ /**
30
+ * Line OAuth User Info URL
31
+ * @default 'https://api.line.me/v2/profile'
32
+ */
33
+ userURL?: string;
34
+ /**
35
+ * Extra authorization parameters to provide to the authorization URL
36
+ * @example { bot_prompt: 'normal' }
37
+ */
38
+ authorizationParams?: Record<string, string>;
39
+ /**
40
+ * Redirect URL to to allow overriding for situations like prod failing to determine public hostname
41
+ * @default process.env.NUXT_OAUTH_LINE_REDIRECT_URL or current URL
42
+ */
43
+ redirectURL?: string;
44
+ }
45
+ export declare function defineOAuthLineEventHandler({ config, onSuccess, onError, }: OAuthConfig<OAuthLineConfig>): import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<void>>;
@@ -0,0 +1,75 @@
1
+ import { eventHandler, getQuery, sendRedirect, createError } from "h3";
2
+ import { withQuery } from "ufo";
3
+ import { defu } from "defu";
4
+ import { handleMissingConfiguration, handleAccessTokenErrorResponse, getOAuthRedirectURL, requestAccessToken } from "../utils.js";
5
+ import { useRuntimeConfig } from "#imports";
6
+ export function defineOAuthLineEventHandler({
7
+ config,
8
+ onSuccess,
9
+ onError
10
+ }) {
11
+ return eventHandler(async (event) => {
12
+ config = defu(config, useRuntimeConfig(event).oauth?.line, {
13
+ authorizationURL: "https://access.line.me/oauth2/v2.1/authorize",
14
+ tokenURL: "https://api.line.me/oauth2/v2.1/token",
15
+ userURL: "https://api.line.me/v2/profile",
16
+ authorizationParams: {}
17
+ });
18
+ const query = getQuery(event);
19
+ if (query.error) {
20
+ return onError(
21
+ event,
22
+ new Error(`Line login failed: ${query.error || "Unknown error"}`)
23
+ );
24
+ }
25
+ if (!config.clientId || !config.clientSecret) {
26
+ return handleMissingConfiguration(event, "line", ["clientId", "clientSecret"], onError);
27
+ }
28
+ const redirectURL = config.redirectURL || getOAuthRedirectURL(event);
29
+ if (!query.code) {
30
+ config.scope = config.scope || ["profile", "openid"];
31
+ return sendRedirect(
32
+ event,
33
+ withQuery(config.authorizationURL, {
34
+ response_type: "code",
35
+ client_id: config.clientId,
36
+ redirect_uri: redirectURL,
37
+ scope: config.scope.join(" "),
38
+ state: query.state || "",
39
+ ...config.authorizationParams
40
+ })
41
+ );
42
+ }
43
+ const tokens = await requestAccessToken(config.tokenURL, {
44
+ body: {
45
+ grant_type: "authorization_code",
46
+ code: query.code,
47
+ client_id: config.clientId,
48
+ client_secret: config.clientSecret,
49
+ redirect_uri: redirectURL
50
+ }
51
+ });
52
+ if (tokens.error) {
53
+ return handleAccessTokenErrorResponse(event, "line", tokens, onError);
54
+ }
55
+ const accessToken = tokens.access_token;
56
+ const user = await $fetch(config.userURL, {
57
+ headers: {
58
+ Authorization: `Bearer ${accessToken}`
59
+ }
60
+ });
61
+ if (!user) {
62
+ const error = createError({
63
+ statusCode: 500,
64
+ message: "Could not get Line user",
65
+ data: tokens
66
+ });
67
+ if (!onError) throw error;
68
+ return onError(event, error);
69
+ }
70
+ return onSuccess(event, {
71
+ tokens,
72
+ user
73
+ });
74
+ });
75
+ }
@@ -12,8 +12,8 @@ export function defineOAuthPaypalEventHandler({ config, onSuccess, onError }) {
12
12
  authorizationParams: {}
13
13
  });
14
14
  const query = getQuery(event);
15
- if (!config.clientId) {
16
- return handleMissingConfiguration(event, "paypal", ["clientId"], onError);
15
+ if (!config.clientId || !config.clientSecret) {
16
+ return handleMissingConfiguration(event, "paypal", ["clientId", "clientSecret"], onError);
17
17
  }
18
18
  let paypalAPI = "api-m.paypal.com";
19
19
  if (config.sandbox) {
@@ -11,8 +11,8 @@ export function defineOAuthSeznamEventHandler({ config, onSuccess, onError }) {
11
11
  userURL: "https://login.szn.cz/api/v1/user"
12
12
  });
13
13
  const query = getQuery(event);
14
- if (!config.clientId) {
15
- return handleMissingConfiguration(event, "seznam", ["clientId"], onError);
14
+ if (!config.clientId || !config.clientSecret) {
15
+ return handleMissingConfiguration(event, "seznam", ["clientId", "clientSecret"], onError);
16
16
  }
17
17
  const redirectURL = config.redirectURL || getOAuthRedirectURL(event);
18
18
  if (!query.code) {
@@ -11,8 +11,8 @@ export function defineOAuthTwitchEventHandler({ config, onSuccess, onError }) {
11
11
  authorizationParams: {}
12
12
  });
13
13
  const query = getQuery(event);
14
- if (!config.clientId) {
15
- return handleMissingConfiguration(event, "twitch", ["clientId"], onError);
14
+ if (!config.clientId || !config.clientSecret) {
15
+ return handleMissingConfiguration(event, "twitch", ["clientId", "clientSecret"], onError);
16
16
  }
17
17
  const redirectURL = config.redirectURL || getOAuthRedirectURL(event);
18
18
  if (!query.code) {
@@ -25,8 +25,8 @@ export function defineOAuthXEventHandler({
25
25
  }
26
26
  });
27
27
  const query = getQuery(event);
28
- if (!config.clientId) {
29
- return handleMissingConfiguration(event, "x", ["clientId"], onError);
28
+ if (!config.clientId || !config.clientSecret) {
29
+ return handleMissingConfiguration(event, "x", ["clientId", "clientSecret"], onError);
30
30
  }
31
31
  const redirectURL = config.redirectURL || getOAuthRedirectURL(event);
32
32
  if (!query.code) {
@@ -1,5 +0,0 @@
1
- import type { WebAuthnAuthenticateEventHandlerOptions, WebAuthnCredential } from '#auth-utils';
2
- export declare function defineWebAuthnAuthenticateEventHandler<T extends WebAuthnCredential>({ storeChallenge, getChallenge, getCredential, allowCredentials, getOptions, onSuccess, onError, }: WebAuthnAuthenticateEventHandlerOptions<T>): import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<void | import("@simplewebauthn/server").VerifiedAuthenticationResponse | {
3
- requestOptions: import("@simplewebauthn/types").PublicKeyCredentialRequestOptionsJSON;
4
- attemptId: string;
5
- }>>;
@@ -1,8 +0,0 @@
1
- import type { H3Event } from 'h3';
2
- import type { WebAuthnUser, WebAuthnRegisterEventHandlerOptions } from '#auth-utils';
3
- import type { ValidateUserFunction } from '~/src/runtime/types/webauthn';
4
- export declare function defineWebAuthnRegisterEventHandler<T extends WebAuthnUser>({ storeChallenge, getChallenge, getOptions, validateUser, excludeCredentials, onSuccess, onError, }: WebAuthnRegisterEventHandlerOptions<T>): import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<void | import("@simplewebauthn/server").VerifiedRegistrationResponse | {
5
- creationOptions: import("@simplewebauthn/types").PublicKeyCredentialCreationOptionsJSON;
6
- attemptId: string;
7
- }>>;
8
- export declare function validateUserData<T>(userBody: WebAuthnUser, event: H3Event, fn: ValidateUserFunction<T>): Promise<T>;
@@ -1,2 +1,2 @@
1
- declare const _default: import("nitropack").NitroAppPlugin;
1
+ declare const _default: import("nitropack/types").NitroAppPlugin;
2
2
  export default _default;
@@ -1,5 +1,5 @@
1
1
  import type { H3Event, H3Error } from 'h3';
2
- export type OAuthProvider = 'auth0' | 'authentik' | 'battledotnet' | 'cognito' | 'discord' | 'dropbox' | 'facebook' | 'github' | 'gitlab' | 'google' | 'instagram' | 'keycloak' | 'linear' | 'linkedin' | 'microsoft' | 'paypal' | 'polar' | 'spotify' | 'seznam' | 'steam' | 'strava' | 'tiktok' | 'twitch' | 'vk' | 'workos' | 'x' | 'xsuaa' | 'yandex' | 'zitadel' | (string & {});
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' | (string & {});
3
3
  export type OnError = (event: H3Event, error: H3Error) => Promise<void> | void;
4
4
  export interface OAuthConfig<TConfig, TUser = any, TTokens = any> {
5
5
  config?: TConfig;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nuxt-auth-utils",
3
- "version": "0.5.6",
3
+ "version": "0.5.8",
4
4
  "description": "Add Authentication to Nuxt applications with secured & sealed cookies sessions.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -8,7 +8,7 @@
8
8
  },
9
9
  "license": "MIT",
10
10
  "type": "module",
11
- "packageManager": "pnpm@9.13.2",
11
+ "packageManager": "pnpm@9.15.4",
12
12
  "exports": {
13
13
  ".": {
14
14
  "types": "./dist/types.d.ts",
@@ -36,13 +36,14 @@
36
36
  },
37
37
  "dependencies": {
38
38
  "@adonisjs/hash": "^9.0.5",
39
- "@nuxt/kit": "^3.14.159",
39
+ "@nuxt/kit": "^3.15.2",
40
40
  "defu": "^6.1.4",
41
- "h3": "^1.13.0",
41
+ "h3": "^1.13.1",
42
42
  "hookable": "^5.5.3",
43
43
  "ofetch": "^1.4.1",
44
44
  "ohash": "^1.1.4",
45
- "pathe": "^1.1.2",
45
+ "openid-client": "^6.1.7",
46
+ "pathe": "^2.0.2",
46
47
  "scule": "^1.3.0",
47
48
  "uncrypto": "^0.1.3"
48
49
  },
@@ -59,20 +60,20 @@
59
60
  }
60
61
  },
61
62
  "devDependencies": {
62
- "@iconify-json/simple-icons": "^1.2.11",
63
+ "@iconify-json/simple-icons": "^1.2.21",
63
64
  "@nuxt/devtools": "latest",
64
- "@nuxt/eslint-config": "^0.6.1",
65
+ "@nuxt/eslint-config": "^0.7.5",
65
66
  "@nuxt/module-builder": "^0.8.4",
66
- "@nuxt/schema": "^3.14.159",
67
- "@nuxt/test-utils": "^3.14.4",
68
- "@nuxt/ui": "^2.19.2",
69
- "@nuxt/ui-pro": "^1.5.0",
70
- "@simplewebauthn/types": "^11.0.0",
67
+ "@nuxt/schema": "^3.15.2",
68
+ "@nuxt/test-utils": "^3.15.4",
69
+ "@nuxt/ui": "^2.21.0",
70
+ "@nuxt/ui-pro": "^1.7.0",
71
+ "@simplewebauthn/types": "^12.0.0",
71
72
  "changelogen": "^0.5.7",
72
- "eslint": "^9.14.0",
73
- "nuxt": "^3.14.159",
74
- "typescript": "^5.6.3",
75
- "vitest": "^2.1.5",
76
- "vue-tsc": "^2.1.10"
73
+ "eslint": "^9.18.0",
74
+ "nuxt": "^3.15.2",
75
+ "typescript": "5.6.3",
76
+ "vitest": "^3.0.3",
77
+ "vue-tsc": "^2.2.0"
77
78
  }
78
79
  }