nuxt-auth-utils 0.3.4 → 0.3.6

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 (34) hide show
  1. package/README.md +2 -1
  2. package/dist/module.d.mts +1 -1
  3. package/dist/module.d.ts +1 -1
  4. package/dist/module.json +2 -2
  5. package/dist/module.mjs +5 -0
  6. package/dist/runtime/app/plugins/session.client.js +3 -1
  7. package/dist/runtime/server/lib/oauth/auth0.js +18 -34
  8. package/dist/runtime/server/lib/oauth/battledotnet.js +23 -37
  9. package/dist/runtime/server/lib/oauth/cognito.d.ts +5 -0
  10. package/dist/runtime/server/lib/oauth/cognito.js +21 -30
  11. package/dist/runtime/server/lib/oauth/discord.js +15 -36
  12. package/dist/runtime/server/lib/oauth/facebook.js +8 -24
  13. package/dist/runtime/server/lib/oauth/github.js +14 -26
  14. package/dist/runtime/server/lib/oauth/google.js +16 -36
  15. package/dist/runtime/server/lib/oauth/keycloak.js +14 -37
  16. package/dist/runtime/server/lib/oauth/linkedin.d.ts +2 -11
  17. package/dist/runtime/server/lib/oauth/linkedin.js +15 -36
  18. package/dist/runtime/server/lib/oauth/microsoft.d.ts +2 -11
  19. package/dist/runtime/server/lib/oauth/microsoft.js +16 -35
  20. package/dist/runtime/server/lib/oauth/paypal.js +17 -35
  21. package/dist/runtime/server/lib/oauth/spotify.js +17 -34
  22. package/dist/runtime/server/lib/oauth/steam.js +2 -6
  23. package/dist/runtime/server/lib/oauth/tiktok.d.ts +45 -0
  24. package/dist/runtime/server/lib/oauth/tiktok.js +84 -0
  25. package/dist/runtime/server/lib/oauth/twitch.js +16 -35
  26. package/dist/runtime/server/lib/oauth/x.d.ts +1 -6
  27. package/dist/runtime/server/lib/oauth/x.js +19 -60
  28. package/dist/runtime/server/lib/oauth/xsuaa.js +15 -34
  29. package/dist/runtime/server/lib/oauth/yandex.js +14 -36
  30. package/dist/runtime/server/lib/utils.d.ts +36 -0
  31. package/dist/runtime/server/lib/utils.js +46 -0
  32. package/dist/runtime/types/index.d.ts +1 -1
  33. package/dist/runtime/types/oauth-config.d.ts +3 -1
  34. package/package.json +12 -11
@@ -1,13 +1,8 @@
1
- import {
2
- eventHandler,
3
- createError,
4
- getQuery,
5
- getRequestURL,
6
- sendRedirect
7
- } from "h3";
8
- import { withQuery, parsePath } from "ufo";
1
+ import { eventHandler, getQuery, sendRedirect } from "h3";
2
+ import { withQuery } from "ufo";
9
3
  import { defu } from "defu";
10
- import { useRuntimeConfig } from "#imports";
4
+ import { handleMissingConfiguration, handleAccessTokenErrorResponse, getOAuthRedirectURL, requestAccessToken } from "../utils.js";
5
+ import { useRuntimeConfig, createError } from "#imports";
11
6
  export function oauthKeycloakEventHandler({
12
7
  config,
13
8
  onSuccess,
@@ -18,7 +13,6 @@ export function oauthKeycloakEventHandler({
18
13
  authorizationParams: {}
19
14
  });
20
15
  const query = getQuery(event);
21
- const { code } = query;
22
16
  if (query.error) {
23
17
  const error = createError({
24
18
  statusCode: 401,
@@ -29,18 +23,13 @@ export function oauthKeycloakEventHandler({
29
23
  return onError(event, error);
30
24
  }
31
25
  if (!config.clientId || !config.clientSecret || !config.serverUrl || !config.realm) {
32
- const error = createError({
33
- statusCode: 500,
34
- message: "Missing NUXT_OAUTH_KEYCLOAK_CLIENT_ID or NUXT_OAUTH_KEYCLOAK_CLIENT_SECRET or NUXT_OAUTH_KEYCLOAK_SERVER_URL or NUXT_OAUTH_KEYCLOAK_REALM env variables."
35
- });
36
- if (!onError) throw error;
37
- return onError(event, error);
26
+ return handleMissingConfiguration(event, "keycloak", ["clientId", "clientSecret", "serverUrl", "realm"], onError);
38
27
  }
39
28
  const realmURL = `${config.serverUrl}/realms/${config.realm}`;
40
29
  const authorizationURL = `${realmURL}/protocol/openid-connect/auth`;
41
30
  const tokenURL = `${realmURL}/protocol/openid-connect/token`;
42
- const redirectURL = config.redirectURL || getRequestURL(event).href;
43
- if (!code) {
31
+ const redirectURL = config.redirectURL || getOAuthRedirectURL(event);
32
+ if (!query.code) {
44
33
  config.scope = config.scope || ["openid"];
45
34
  return sendRedirect(
46
35
  event,
@@ -57,29 +46,17 @@ export function oauthKeycloakEventHandler({
57
46
  if (!config.scope.includes("openid")) {
58
47
  config.scope.push("openid");
59
48
  }
60
- const tokens = await $fetch(tokenURL, {
61
- method: "POST",
62
- headers: {
63
- "Content-Type": "application/x-www-form-urlencoded"
64
- },
65
- body: new URLSearchParams({
49
+ const tokens = await requestAccessToken(tokenURL, {
50
+ body: {
51
+ grant_type: "authorization_code",
66
52
  client_id: config.clientId,
67
53
  client_secret: config.clientSecret,
68
- grant_type: "authorization_code",
69
- redirect_uri: parsePath(redirectURL).pathname,
70
- code
71
- }).toString()
72
- }).catch((error) => {
73
- return { error };
54
+ redirect_uri: redirectURL,
55
+ code: query.code
56
+ }
74
57
  });
75
58
  if (tokens.error) {
76
- const error = createError({
77
- statusCode: 401,
78
- message: `Keycloak login failed: ${tokens.error?.data?.error_description || "Unknown error"}`,
79
- data: tokens
80
- });
81
- if (!onError) throw error;
82
- return onError(event, error);
59
+ return handleAccessTokenErrorResponse(event, "keycloak", tokens, onError);
83
60
  }
84
61
  const accessToken = tokens.access_token;
85
62
  const user = await $fetch(
@@ -1,4 +1,4 @@
1
- import type { H3Event, H3Error } from 'h3';
1
+ import type { OAuthConfig } from '#auth-utils';
2
2
  export interface OAuthLinkedInConfig {
3
3
  /**
4
4
  * LinkedIn OAuth Client ID
@@ -42,13 +42,4 @@ export interface OAuthLinkedInConfig {
42
42
  */
43
43
  redirectURL?: string;
44
44
  }
45
- interface OAuthConfig {
46
- config?: OAuthLinkedInConfig;
47
- onSuccess: (event: H3Event, result: {
48
- user: any;
49
- tokens: any;
50
- }) => Promise<void> | void;
51
- onError?: (event: H3Event, error: H3Error) => Promise<void> | void;
52
- }
53
- export declare function oauthLinkedInEventHandler({ config, onSuccess, onError }: OAuthConfig): import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<void>>;
54
- export {};
45
+ export declare function oauthLinkedInEventHandler({ config, onSuccess, onError }: OAuthConfig<OAuthLinkedInConfig>): import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<void>>;
@@ -1,6 +1,7 @@
1
- import { eventHandler, createError, getQuery, getRequestURL, sendRedirect } from "h3";
2
- import { withQuery, parseURL, stringifyParsedURL } from "ufo";
1
+ import { eventHandler, getQuery, sendRedirect } from "h3";
2
+ import { withQuery } from "ufo";
3
3
  import { defu } from "defu";
4
+ import { handleMissingConfiguration, handleAccessTokenErrorResponse, getOAuthRedirectURL, requestAccessToken } from "../utils.js";
4
5
  import { useRuntimeConfig } from "#imports";
5
6
  export function oauthLinkedInEventHandler({ config, onSuccess, onError }) {
6
7
  return eventHandler(async (event) => {
@@ -9,17 +10,12 @@ export function oauthLinkedInEventHandler({ config, onSuccess, onError }) {
9
10
  tokenURL: "https://www.linkedin.com/oauth/v2/accessToken",
10
11
  authorizationParams: {}
11
12
  });
12
- const { code } = getQuery(event);
13
+ const query = getQuery(event);
13
14
  if (!config.clientId || !config.clientSecret) {
14
- const error = createError({
15
- statusCode: 500,
16
- message: "Missing NUXT_OAUTH_LINKEDIN_CLIENT_ID or NUXT_OAUTH_LINKEDIN_CLIENT_SECRET env variables."
17
- });
18
- if (!onError) throw error;
19
- return onError(event, error);
15
+ return handleMissingConfiguration(event, "linkedin", ["clientId", "clientSecret"], onError);
20
16
  }
21
- const redirectURL = config.redirectURL || getRequestURL(event).href;
22
- if (!code) {
17
+ const redirectURL = config.redirectURL || getOAuthRedirectURL(event);
18
+ if (!query.code) {
23
19
  config.scope = config.scope || [];
24
20
  if (!config.scope.length) {
25
21
  config.scope.push("profile", "openid", "email");
@@ -38,34 +34,17 @@ export function oauthLinkedInEventHandler({ config, onSuccess, onError }) {
38
34
  })
39
35
  );
40
36
  }
41
- const parsedRedirectUrl = parseURL(redirectURL);
42
- parsedRedirectUrl.search = "";
43
- const tokens = await $fetch(
44
- config.tokenURL,
45
- {
46
- method: "POST",
47
- headers: {
48
- "Content-Type": "application/x-www-form-urlencoded"
49
- },
50
- body: new URLSearchParams({
51
- grant_type: "authorization_code",
52
- code,
53
- client_id: config.clientId,
54
- client_secret: config.clientSecret,
55
- redirect_uri: stringifyParsedURL(parsedRedirectUrl)
56
- }).toString()
37
+ const tokens = await requestAccessToken(config.tokenURL, {
38
+ body: {
39
+ grant_type: "authorization_code",
40
+ code: query.code,
41
+ client_id: config.clientId,
42
+ client_secret: config.clientSecret,
43
+ redirect_uri: redirectURL
57
44
  }
58
- ).catch((error) => {
59
- return { error };
60
45
  });
61
46
  if (tokens.error) {
62
- const error = createError({
63
- statusCode: 401,
64
- message: `LinkedIn login failed: ${tokens.error?.data?.error_description || "Unknown error"}`,
65
- data: tokens
66
- });
67
- if (!onError) throw error;
68
- return onError(event, error);
47
+ return handleAccessTokenErrorResponse(event, "linkedin", tokens, onError);
69
48
  }
70
49
  const accessToken = tokens.access_token;
71
50
  const user = await $fetch("https://api.linkedin.com/v2/userinfo", {
@@ -1,4 +1,4 @@
1
- import type { H3Event, H3Error } from 'h3';
1
+ import type { OAuthConfig } from '#auth-utils';
2
2
  export interface OAuthMicrosoftConfig {
3
3
  /**
4
4
  * Microsoft OAuth Client ID
@@ -51,13 +51,4 @@ export interface OAuthMicrosoftConfig {
51
51
  */
52
52
  redirectURL?: string;
53
53
  }
54
- interface OAuthConfig {
55
- config?: OAuthMicrosoftConfig;
56
- onSuccess: (event: H3Event, result: {
57
- user: any;
58
- tokens: any;
59
- }) => Promise<void> | void;
60
- onError?: (event: H3Event, error: H3Error) => Promise<void> | void;
61
- }
62
- export declare function oauthMicrosoftEventHandler({ config, onSuccess, onError }: OAuthConfig): import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<void>>;
63
- export {};
54
+ export declare function oauthMicrosoftEventHandler({ config, onSuccess, onError }: OAuthConfig<OAuthMicrosoftConfig>): import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<void>>;
@@ -1,25 +1,21 @@
1
- import { eventHandler, createError, getQuery, getRequestURL, sendRedirect } from "h3";
2
- import { withQuery, parsePath } from "ufo";
1
+ import { eventHandler, getQuery, sendRedirect } from "h3";
2
+ import { withQuery } from "ufo";
3
3
  import { defu } from "defu";
4
- import { useRuntimeConfig } from "#imports";
4
+ import { handleMissingConfiguration, handleAccessTokenErrorResponse, getOAuthRedirectURL, requestAccessToken } from "../utils.js";
5
+ import { useRuntimeConfig, createError } from "#imports";
5
6
  export function oauthMicrosoftEventHandler({ config, onSuccess, onError }) {
6
7
  return eventHandler(async (event) => {
7
8
  config = defu(config, useRuntimeConfig(event).oauth?.microsoft, {
8
9
  authorizationParams: {}
9
10
  });
10
- const { code } = getQuery(event);
11
+ const query = getQuery(event);
11
12
  if (!config.clientId || !config.clientSecret || !config.tenant) {
12
- const error = createError({
13
- statusCode: 500,
14
- message: "Missing NUXT_OAUTH_MICROSOFT_CLIENT_ID or NUXT_OAUTH_MICROSOFT_CLIENT_SECRET or NUXT_OAUTH_MICROSOFT_TENANT env variables."
15
- });
16
- if (!onError) throw error;
17
- return onError(event, error);
13
+ return handleMissingConfiguration(event, "microsoft", ["clientId", "clientSecret", "tenant"], onError);
18
14
  }
19
15
  const authorizationURL = config.authorizationURL || `https://login.microsoftonline.com/${config.tenant}/oauth2/v2.0/authorize`;
20
16
  const tokenURL = config.tokenURL || `https://login.microsoftonline.com/${config.tenant}/oauth2/v2.0/token`;
21
- const redirectURL = config.redirectURL || getRequestURL(event).href;
22
- if (!code) {
17
+ const redirectURL = config.redirectURL || getOAuthRedirectURL(event);
18
+ if (!query.code) {
23
19
  const scope = config.scope && config.scope.length > 0 ? config.scope : ["User.Read"];
24
20
  return sendRedirect(
25
21
  event,
@@ -32,32 +28,17 @@ export function oauthMicrosoftEventHandler({ config, onSuccess, onError }) {
32
28
  })
33
29
  );
34
30
  }
35
- const data = new URLSearchParams();
36
- data.append("grant_type", "authorization_code");
37
- data.append("client_id", config.clientId);
38
- data.append("client_secret", config.clientSecret);
39
- data.append("redirect_uri", parsePath(redirectURL).pathname);
40
- data.append("code", String(code));
41
- const tokens = await $fetch(
42
- tokenURL,
43
- {
44
- method: "POST",
45
- headers: {
46
- "Content-Type": "application/x-www-form-urlencoded"
47
- },
48
- body: data
31
+ const tokens = await requestAccessToken(tokenURL, {
32
+ body: {
33
+ grant_type: "authorization_code",
34
+ code: query.code,
35
+ client_id: config.clientId,
36
+ client_secret: config.clientSecret,
37
+ redirect_uri: redirectURL
49
38
  }
50
- ).catch((error) => {
51
- return { error };
52
39
  });
53
40
  if (tokens.error) {
54
- const error = createError({
55
- statusCode: 401,
56
- message: `Microsoft login failed: ${tokens.error?.data?.error_description || "Unknown error"}`,
57
- data: tokens
58
- });
59
- if (!onError) throw error;
60
- return onError(event, error);
41
+ return handleAccessTokenErrorResponse(event, "microsoft", tokens, onError);
61
42
  }
62
43
  const tokenType = tokens.token_type;
63
44
  const accessToken = tokens.access_token;
@@ -1,7 +1,8 @@
1
- import { eventHandler, createError, getQuery, getRequestURL, sendRedirect } from "h3";
2
- import { withQuery, parsePath } from "ufo";
1
+ import { eventHandler, getQuery, sendRedirect } from "h3";
2
+ import { withQuery } from "ufo";
3
3
  import { defu } from "defu";
4
- import { useRuntimeConfig } from "#imports";
4
+ import { handleMissingConfiguration, handleAccessTokenErrorResponse, getOAuthRedirectURL, requestAccessToken } from "../utils.js";
5
+ import { useRuntimeConfig, createError } from "#imports";
5
6
  export function oauthPaypalEventHandler({ config, onSuccess, onError }) {
6
7
  return eventHandler(async (event) => {
7
8
  config = defu(config, useRuntimeConfig(event).oauth?.paypal, {
@@ -10,14 +11,9 @@ export function oauthPaypalEventHandler({ config, onSuccess, onError }) {
10
11
  tokenURL: "https://api-m.paypal.com/v1/oauth2/token",
11
12
  authorizationParams: {}
12
13
  });
13
- const { code } = getQuery(event);
14
+ const query = getQuery(event);
14
15
  if (!config.clientId) {
15
- const error = createError({
16
- statusCode: 500,
17
- message: "Missing NUXT_OAUTH_PAYPAL_CLIENT_ID env variables."
18
- });
19
- if (!onError) throw error;
20
- return onError(event, error);
16
+ return handleMissingConfiguration(event, "paypal", ["clientId"], onError);
21
17
  }
22
18
  let paypalAPI = "api-m.paypal.com";
23
19
  if (config.sandbox) {
@@ -25,8 +21,8 @@ export function oauthPaypalEventHandler({ config, onSuccess, onError }) {
25
21
  config.authorizationURL = "https://www.sandbox.paypal.com/signin/authorize";
26
22
  config.tokenURL = `https://${paypalAPI}/v1/oauth2/token`;
27
23
  }
28
- const redirectURL = config.redirectURL || getRequestURL(event).href;
29
- if (!code) {
24
+ const redirectURL = config.redirectURL || getOAuthRedirectURL(event);
25
+ if (!query.code) {
30
26
  config.scope = config.scope || [];
31
27
  if (!config.scope.includes("openid")) {
32
28
  config.scope.push("openid");
@@ -46,32 +42,18 @@ export function oauthPaypalEventHandler({ config, onSuccess, onError }) {
46
42
  })
47
43
  );
48
44
  }
49
- const authCode = Buffer.from(`${config.clientId}:${config.clientSecret}`).toString("base64");
50
- const tokens = await $fetch(
51
- config.tokenURL,
52
- {
53
- method: "POST",
54
- headers: {
55
- "Authorization": `Basic ${authCode}`,
56
- "Content-Type": "application/x-www-form-urlencoded"
57
- },
58
- params: {
59
- grant_type: "authorization_code",
60
- redirect_uri: encodeURIComponent(parsePath(redirectURL).pathname),
61
- code
62
- }
45
+ const tokens = await requestAccessToken(config.tokenURL, {
46
+ headers: {
47
+ Authorization: `Basic ${Buffer.from(`${config.clientId}:${config.clientSecret}`).toString("base64")}`
48
+ },
49
+ params: {
50
+ grant_type: "authorization_code",
51
+ redirect_uri: redirectURL,
52
+ code: query.code
63
53
  }
64
- ).catch((error) => {
65
- return { error };
66
54
  });
67
55
  if (tokens.error) {
68
- const error = createError({
69
- statusCode: 401,
70
- message: `PayPal login failed: ${tokens.error?.data?.error_description || "Unknown error"}`,
71
- data: tokens
72
- });
73
- if (!onError) throw error;
74
- return onError(event, error);
56
+ return handleAccessTokenErrorResponse(event, "paypal", tokens, onError);
75
57
  }
76
58
  const accessToken = tokens.access_token;
77
59
  const users = await $fetch(`https://${paypalAPI}/v1/identity/openidconnect/userinfo`, {
@@ -1,6 +1,7 @@
1
- import { eventHandler, createError, getQuery, getRequestURL, sendRedirect } from "h3";
2
- import { withQuery, parsePath } from "ufo";
1
+ import { eventHandler, getQuery, sendRedirect } from "h3";
2
+ import { withQuery } from "ufo";
3
3
  import { defu } from "defu";
4
+ import { handleMissingConfiguration, handleAccessTokenErrorResponse, getOAuthRedirectURL, requestAccessToken } from "../utils.js";
4
5
  import { useRuntimeConfig } from "#imports";
5
6
  export function oauthSpotifyEventHandler({ config, onSuccess, onError }) {
6
7
  return eventHandler(async (event) => {
@@ -9,17 +10,12 @@ export function oauthSpotifyEventHandler({ config, onSuccess, onError }) {
9
10
  tokenURL: "https://accounts.spotify.com/api/token",
10
11
  authorizationParams: {}
11
12
  });
12
- const { code } = getQuery(event);
13
+ const query = getQuery(event);
13
14
  if (!config.clientId || !config.clientSecret) {
14
- const error = createError({
15
- statusCode: 500,
16
- message: "Missing NUXT_OAUTH_SPOTIFY_CLIENT_ID or NUXT_OAUTH_SPOTIFY_CLIENT_SECRET env variables."
17
- });
18
- if (!onError) throw error;
19
- return onError(event, error);
15
+ return handleMissingConfiguration(event, "spotify", ["clientId", "clientSecret"], onError);
20
16
  }
21
- const redirectURL = config.redirectURL || getRequestURL(event).href;
22
- if (!code) {
17
+ const redirectURL = config.redirectURL || getOAuthRedirectURL(event);
18
+ if (!query.code) {
23
19
  config.scope = config.scope || [];
24
20
  if (config.emailRequired && !config.scope.includes("user-read-email")) {
25
21
  config.scope.push("user-read-email");
@@ -35,32 +31,19 @@ export function oauthSpotifyEventHandler({ config, onSuccess, onError }) {
35
31
  })
36
32
  );
37
33
  }
38
- const authCode = Buffer.from(`${config.clientId}:${config.clientSecret}`).toString("base64");
39
- const tokens = await $fetch(
40
- config.tokenURL,
41
- {
42
- method: "POST",
43
- headers: {
44
- "Authorization": `Basic ${authCode}`,
45
- "Content-Type": "application/x-www-form-urlencoded"
46
- },
47
- params: {
48
- grant_type: "authorization_code",
49
- redirect_uri: parsePath(redirectURL).pathname,
50
- code
51
- }
34
+ const tokens = await requestAccessToken(config.tokenURL, {
35
+ headers: {
36
+ Authorization: `Basic ${Buffer.from(`${config.clientId}:${config.clientSecret}`).toString("base64")}`
37
+ },
38
+ body: {
39
+ client_id: config.clientId,
40
+ grant_type: "authorization_code",
41
+ redirect_uri: redirectURL,
42
+ code: query.code
52
43
  }
53
- ).catch((error) => {
54
- return { error };
55
44
  });
56
45
  if (tokens.error) {
57
- const error = createError({
58
- statusCode: 401,
59
- message: `Spotify login failed: ${tokens.error?.data?.error_description || "Unknown error"}`,
60
- data: tokens
61
- });
62
- if (!onError) throw error;
63
- return onError(event, error);
46
+ return handleAccessTokenErrorResponse(event, "spotify", tokens, onError);
64
47
  }
65
48
  const accessToken = tokens.access_token;
66
49
  const user = await $fetch("https://api.spotify.com/v1/me", {
@@ -1,6 +1,7 @@
1
1
  import { eventHandler, createError, getQuery, getRequestURL, sendRedirect } from "h3";
2
2
  import { withQuery } from "ufo";
3
3
  import { defu } from "defu";
4
+ import { handleMissingConfiguration } from "../utils.js";
4
5
  import { useRuntimeConfig } from "#imports";
5
6
  export function oauthSteamEventHandler({ config, onSuccess, onError }) {
6
7
  return eventHandler(async (event) => {
@@ -9,12 +10,7 @@ export function oauthSteamEventHandler({ config, onSuccess, onError }) {
9
10
  });
10
11
  const query = getQuery(event);
11
12
  if (!config.apiKey) {
12
- const error = createError({
13
- statusCode: 500,
14
- message: "Missing NUXT_OAUTH_STEAM_API_KEY env variable."
15
- });
16
- if (!onError) throw error;
17
- return onError(event, error);
13
+ return handleMissingConfiguration(event, "steam", ["apiKey"], onError);
18
14
  }
19
15
  if (!query["openid.claimed_id"]) {
20
16
  const redirectURL = config.redirectURL || getRequestURL(event).href;
@@ -0,0 +1,45 @@
1
+ import type { OAuthConfig } from '#auth-utils';
2
+ export interface OAuthTikTokConfig {
3
+ /**
4
+ * TikTok Client Key
5
+ * @default process.env.NUXT_OAUTH_TIKTOK_CLIENT_KEY
6
+ */
7
+ clientKey?: string;
8
+ /**
9
+ * TikTok OAuth Client Secret
10
+ * @default process.env.NUXT_OAUTH_TIKTOK_CLIENT_SECRET
11
+ */
12
+ clientSecret?: string;
13
+ /**
14
+ * TikTok OAuth Scope
15
+ * @default []
16
+ * @see https://developers.tiktok.com/doc/tiktok-api-scopes/
17
+ * @example ['user.info.basic']
18
+ */
19
+ scope?: string[];
20
+ /**
21
+ * Use TikTok sandbox environment.
22
+ * If true it will use Login Kit for Desktop, if false it will use Login Kit for Web.
23
+ * This is because Login Kit for Web doesn't support localhost or IP addresses as redirect URIs.
24
+ * @see https://developers.tiktok.com/doc/login-kit-web/
25
+ * @see https://developers.tiktok.com/doc/login-kit-desktop/
26
+ * @default import.meta.dev // true in development, false in production
27
+ */
28
+ sandbox?: boolean;
29
+ /**
30
+ * TikTok OAuth Authorization URL
31
+ * @default 'https://www.tiktok.com/v2/auth/authorize/'
32
+ */
33
+ authorizationURL?: string;
34
+ /**
35
+ * TikTok OAuth Token URL
36
+ * @default 'https://open.tiktokapis.com/v2/oauth/token/'
37
+ */
38
+ tokenURL?: string;
39
+ /**
40
+ * Redirect URL to to allow overriding for situations like prod failing to determine public hostname
41
+ * @default process.env.NUXT_OAUTH_TIKTOK_REDIRECT_URL or current URL
42
+ */
43
+ redirectURL?: string;
44
+ }
45
+ export declare function oauthTikTokEventHandler({ config, onSuccess, onError }: OAuthConfig<OAuthTikTokConfig>): import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<void>>;
@@ -0,0 +1,84 @@
1
+ import { eventHandler, getQuery, sendRedirect } from "h3";
2
+ import { withQuery } from "ufo";
3
+ import { defu } from "defu";
4
+ import { sha256 } from "ohash";
5
+ import { handleAccessTokenErrorResponse, handleMissingConfiguration, getOAuthRedirectURL, requestAccessToken } from "../utils.js";
6
+ import { useRuntimeConfig, createError } from "#imports";
7
+ export function oauthTikTokEventHandler({ config, onSuccess, onError }) {
8
+ return eventHandler(async (event) => {
9
+ config = defu(config, useRuntimeConfig(event).oauth?.tiktok, {
10
+ sandbox: import.meta.dev,
11
+ authorizationURL: "https://www.tiktok.com/v2/auth/authorize/",
12
+ tokenURL: "https://open.tiktokapis.com/v2/oauth/token/"
13
+ });
14
+ const query = getQuery(event);
15
+ if (!config.clientKey || !config.clientSecret) {
16
+ return handleMissingConfiguration(event, "tiktok", ["clientKey", "clientSecret"], onError);
17
+ }
18
+ const codeVerifier = "verify";
19
+ const redirectURL = config.redirectURL || getOAuthRedirectURL(event);
20
+ if (!query.code) {
21
+ config.scope = config.scope || [];
22
+ if (!config.scope.includes("user.info.basic")) {
23
+ config.scope.push("user.info.basic");
24
+ }
25
+ return sendRedirect(
26
+ event,
27
+ withQuery(config.authorizationURL, {
28
+ response_type: "code",
29
+ client_key: config.clientKey,
30
+ redirect_uri: redirectURL,
31
+ scope: config.scope.join(","),
32
+ ...config.sandbox ? {
33
+ code_verifier: codeVerifier,
34
+ code_challenge: sha256(codeVerifier),
35
+ code_challenge_method: "S256"
36
+ } : {}
37
+ })
38
+ );
39
+ }
40
+ const tokens = await requestAccessToken(
41
+ config.tokenURL,
42
+ {
43
+ body: {
44
+ grant_type: "authorization_code",
45
+ redirect_uri: redirectURL,
46
+ client_key: config.clientKey,
47
+ client_secret: config.clientSecret,
48
+ code: query.code,
49
+ ...config.sandbox ? { code_verifier: codeVerifier } : {}
50
+ }
51
+ }
52
+ );
53
+ if (tokens.error) {
54
+ return handleAccessTokenErrorResponse(event, "tiktok", tokens, onError);
55
+ }
56
+ const accessToken = tokens.access_token;
57
+ const userInfoFieldsByScope = {
58
+ "user.info.basic": ["open_id", "union_id", "avatar_url", "avatar_url_100", "avatar_large_url", "display_name"],
59
+ "user.info.profile": ["bio_description", "profile_deep_link", "is_verified", "username"],
60
+ "user.info.stats": ["follower_count", "following_count", "likes_count", "video_count"]
61
+ };
62
+ const userInfo = await $fetch(withQuery("https://open.tiktokapis.com/v2/user/info/", {
63
+ fields: config.scope?.map((scope) => userInfoFieldsByScope[scope]).flat().join(",")
64
+ }), {
65
+ headers: {
66
+ Authorization: `Bearer ${accessToken}`
67
+ }
68
+ });
69
+ const user = userInfo?.data?.user;
70
+ if (!user) {
71
+ const error = createError({
72
+ statusCode: 500,
73
+ message: "Could not get TikTok user",
74
+ data: tokens
75
+ });
76
+ if (!onError) throw error;
77
+ return onError(event, error);
78
+ }
79
+ return onSuccess(event, {
80
+ tokens,
81
+ user
82
+ });
83
+ });
84
+ }
@@ -1,7 +1,8 @@
1
- import { eventHandler, createError, getQuery, getRequestURL, sendRedirect } from "h3";
2
- import { withQuery, parsePath } from "ufo";
1
+ import { eventHandler, getQuery, sendRedirect } from "h3";
2
+ import { withQuery } from "ufo";
3
3
  import { defu } from "defu";
4
- import { useRuntimeConfig } from "#imports";
4
+ import { handleMissingConfiguration, handleAccessTokenErrorResponse, getOAuthRedirectURL, requestAccessToken } from "../utils.js";
5
+ import { useRuntimeConfig, createError } from "#imports";
5
6
  export function oauthTwitchEventHandler({ config, onSuccess, onError }) {
6
7
  return eventHandler(async (event) => {
7
8
  config = defu(config, useRuntimeConfig(event).oauth?.twitch, {
@@ -9,17 +10,12 @@ export function oauthTwitchEventHandler({ config, onSuccess, onError }) {
9
10
  tokenURL: "https://id.twitch.tv/oauth2/token",
10
11
  authorizationParams: {}
11
12
  });
12
- const { code } = getQuery(event);
13
+ const query = getQuery(event);
13
14
  if (!config.clientId) {
14
- const error = createError({
15
- statusCode: 500,
16
- message: "Missing NUXT_OAUTH_TWITCH_CLIENT_ID env variables."
17
- });
18
- if (!onError) throw error;
19
- return onError(event, error);
15
+ return handleMissingConfiguration(event, "twitch", ["clientId"], onError);
20
16
  }
21
- const redirectURL = config.redirectURL || getRequestURL(event).href;
22
- if (!code) {
17
+ const redirectURL = config.redirectURL || getOAuthRedirectURL(event);
18
+ if (!query.code) {
23
19
  config.scope = config.scope || [];
24
20
  if (config.emailRequired && !config.scope.includes("user:read:email")) {
25
21
  config.scope.push("user:read:email");
@@ -35,32 +31,17 @@ export function oauthTwitchEventHandler({ config, onSuccess, onError }) {
35
31
  })
36
32
  );
37
33
  }
38
- const tokens = await $fetch(
39
- config.tokenURL,
40
- {
41
- method: "POST",
42
- headers: {
43
- "Content-Type": "application/x-www-form-urlencoded"
44
- },
45
- params: {
46
- grant_type: "authorization_code",
47
- redirect_uri: parsePath(redirectURL).pathname,
48
- client_id: config.clientId,
49
- client_secret: config.clientSecret,
50
- code
51
- }
34
+ const tokens = await requestAccessToken(config.tokenURL, {
35
+ body: {
36
+ grant_type: "authorization_code",
37
+ redirect_uri: redirectURL,
38
+ client_id: config.clientId,
39
+ client_secret: config.clientSecret,
40
+ code: query.code
52
41
  }
53
- ).catch((error) => {
54
- return { error };
55
42
  });
56
43
  if (tokens.error) {
57
- const error = createError({
58
- statusCode: 401,
59
- message: `Twitch login failed: ${tokens.error?.data?.error_description || "Unknown error"}`,
60
- data: tokens
61
- });
62
- if (!onError) throw error;
63
- return onError(event, error);
44
+ return handleAccessTokenErrorResponse(event, "twitch", tokens, onError);
64
45
  }
65
46
  const accessToken = tokens.access_token;
66
47
  const users = await $fetch("https://api.twitch.tv/helix/users", {