nuxt-auth-utils 0.0.4 → 0.0.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.
package/README.md CHANGED
@@ -7,14 +7,15 @@
7
7
 
8
8
  Minimalist Authentication module for Nuxt exposing Vue composables and server utils.
9
9
 
10
- - [✨  Release Notes](/CHANGELOG.md)
10
+ - [Release Notes](/CHANGELOG.md)
11
+ - [Demo](https://github.com/atinux/nuxt-todos-edge)
11
12
  <!-- - [🏀 Online playground](https://stackblitz.com/github/your-org/nuxt-auth-utils?file=playground%2Fapp.vue) -->
12
13
  <!-- - [📖 &nbsp;Documentation](https://example.com) -->
13
14
 
14
15
  ## Features
15
16
 
16
17
  - Secured & sealed cookies sessions
17
- - OAuth Providers
18
+ - [OAuth Providers](#supported-oauth-providers)
18
19
 
19
20
  ## Requirements
20
21
 
@@ -141,12 +142,21 @@ export default defineNuxtConfig({
141
142
  ```
142
143
 
143
144
  It can also be set using environment variables:
145
+
144
146
  - `NUXT_OAUTH_<PROVIDER>_CLIENT_ID`
145
147
  - `NUXT_OAUTH_<PROVIDER>_CLIENT_SECRET`
146
148
 
147
- Supported providers:
149
+ #### Supported OAuth Providers
150
+
151
+ - Auth0
152
+ - Discord
148
153
  - GitHub
154
+ - Google
149
155
  - Spotify
156
+ - Twitch
157
+ - Battle.net
158
+
159
+ You can add your favorite provider by creating a new file in [src/runtime/server/lib/oauth/](./src/runtime/server/lib/oauth/).
150
160
 
151
161
  ### Example
152
162
 
@@ -154,6 +164,9 @@ Example: `~/server/routes/auth/github.get.ts`
154
164
 
155
165
  ```ts
156
166
  export default oauth.githubEventHandler({
167
+ config: {
168
+ emailRequired: true
169
+ },
157
170
  async onSuccess(event, { user, tokens }) {
158
171
  await setUserSession(event, {
159
172
  user: {
@@ -172,7 +185,6 @@ export default oauth.githubEventHandler({
172
185
 
173
186
  Make sure to set the callback URL in your OAuth app settings as `<your-domain>/auth/github`.
174
187
 
175
-
176
188
  ## Development
177
189
 
178
190
  ```bash
package/dist/module.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "name": "auth-core",
2
+ "name": "auth-utils",
3
3
  "configKey": "auth",
4
- "version": "0.0.4"
4
+ "version": "0.0.6"
5
5
  }
package/dist/module.mjs CHANGED
@@ -4,20 +4,18 @@ import { defu } from 'defu';
4
4
 
5
5
  const module = defineNuxtModule({
6
6
  meta: {
7
- name: "auth-core",
7
+ name: "auth-utils",
8
8
  configKey: "auth"
9
9
  },
10
10
  // Default configuration options of the Nuxt module
11
11
  defaults: {},
12
12
  setup(options, nuxt) {
13
13
  const resolver = createResolver(import.meta.url);
14
- if (!process.env.NUXT_SESSION_PASSWORD) {
14
+ if (!process.env.NUXT_SESSION_PASSWORD && !nuxt.options._prepare) {
15
15
  const randomPassword = sha256(`${Date.now()}${Math.random()}`).slice(0, 32);
16
16
  process.env.NUXT_SESSION_PASSWORD = randomPassword;
17
- if (!nuxt.options._prepare) {
18
- console.warn("No session password set, using a random password, please set NUXT_SESSION_PASSWORD in your .env file with at least 32 chars");
19
- console.log(`NUXT_SESSION_PASSWORD=${randomPassword}`);
20
- }
17
+ console.warn("No session password set, using a random password, please set NUXT_SESSION_PASSWORD in your .env file with at least 32 chars");
18
+ console.log(`NUXT_SESSION_PASSWORD=${randomPassword}`);
21
19
  }
22
20
  nuxt.options.alias["#auth-utils"] = resolver.resolve("./runtime/types/auth-utils-session");
23
21
  addImportsDir(resolver.resolve("./runtime/composables"));
@@ -65,6 +63,27 @@ const module = defineNuxtModule({
65
63
  clientId: "",
66
64
  clientSecret: ""
67
65
  });
66
+ runtimeConfig.oauth.google = defu(runtimeConfig.oauth.google, {
67
+ clientId: "",
68
+ clientSecret: ""
69
+ });
70
+ runtimeConfig.oauth.twitch = defu(runtimeConfig.oauth.twitch, {
71
+ clientId: "",
72
+ clientSecret: ""
73
+ });
74
+ runtimeConfig.oauth.auth0 = defu(runtimeConfig.oauth.auth0, {
75
+ clientId: "",
76
+ clientSecret: "",
77
+ domain: ""
78
+ });
79
+ runtimeConfig.oauth.discord = defu(runtimeConfig.oauth.discord, {
80
+ clientId: "",
81
+ clientSecret: ""
82
+ });
83
+ runtimeConfig.oauth.battledotnet = defu(runtimeConfig.oauth.battledotnet, {
84
+ clientId: "",
85
+ clientSecret: ""
86
+ });
68
87
  }
69
88
  });
70
89
 
@@ -0,0 +1,45 @@
1
+ import type { H3Event, H3Error } from 'h3';
2
+ export interface OAuthAuth0Config {
3
+ /**
4
+ * Auth0 OAuth Client ID
5
+ * @default process.env.NUXT_OAUTH_AUTH0_CLIENT_ID
6
+ */
7
+ clientId?: string;
8
+ /**
9
+ * Auth0 OAuth Client Secret
10
+ * @default process.env.NUXT_OAUTH_AUTH0_CLIENT_SECRET
11
+ */
12
+ clientSecret?: string;
13
+ /**
14
+ * Auth0 OAuth Issuer
15
+ * @default process.env.NUXT_OAUTH_AUTH0_DOMAIN
16
+ */
17
+ domain?: string;
18
+ /**
19
+ * Auth0 OAuth Audience
20
+ * @default process.env.NUXT_OAUTH_AUTH0_AUDIENCE
21
+ */
22
+ audience?: string;
23
+ /**
24
+ * Auth0 OAuth Scope
25
+ * @default []
26
+ * @see https://auth0.com/docs/get-started/apis/scopes/openid-connect-scopes
27
+ * @example ['openid']
28
+ */
29
+ scope?: string[];
30
+ /**
31
+ * Require email from user, adds the ['email'] scope if not present
32
+ * @default false
33
+ */
34
+ emailRequired?: boolean;
35
+ }
36
+ interface OAuthConfig {
37
+ config?: OAuthAuth0Config;
38
+ onSuccess: (event: H3Event, result: {
39
+ user: any;
40
+ tokens: any;
41
+ }) => Promise<void> | void;
42
+ onError?: (event: H3Event, error: H3Error) => Promise<void> | void;
43
+ }
44
+ export declare function auth0EventHandler({ config, onSuccess, onError }: OAuthConfig): import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<void>>;
45
+ export {};
@@ -0,0 +1,78 @@
1
+ import { eventHandler, createError, getQuery, getRequestURL, sendRedirect } from "h3";
2
+ import { withQuery, parsePath } from "ufo";
3
+ import { ofetch } from "ofetch";
4
+ import { defu } from "defu";
5
+ import { useRuntimeConfig } from "#imports";
6
+ export function auth0EventHandler({ config, onSuccess, onError }) {
7
+ return eventHandler(async (event) => {
8
+ config = defu(config, useRuntimeConfig(event).oauth?.auth0);
9
+ const { code } = getQuery(event);
10
+ if (!config.clientId || !config.clientSecret || !config.domain) {
11
+ const error = createError({
12
+ statusCode: 500,
13
+ message: "Missing NUXT_OAUTH_AUTH0_CLIENT_ID or NUXT_OAUTH_AUTH0_CLIENT_SECRET or NUXT_OAUTH_AUTH0_DOMAIN env variables."
14
+ });
15
+ if (!onError)
16
+ throw error;
17
+ return onError(event, error);
18
+ }
19
+ const authorizationURL = `https://${config.domain}/authorize`;
20
+ const tokenURL = `https://${config.domain}/oauth/token`;
21
+ const redirectUrl = getRequestURL(event).href;
22
+ if (!code) {
23
+ config.scope = config.scope || ["openid", "offline_access"];
24
+ if (config.emailRequired && !config.scope.includes("email")) {
25
+ config.scope.push("email");
26
+ }
27
+ return sendRedirect(
28
+ event,
29
+ withQuery(authorizationURL, {
30
+ response_type: "code",
31
+ client_id: config.clientId,
32
+ redirect_uri: redirectUrl,
33
+ scope: config.scope.join(" "),
34
+ audience: config.audience || ""
35
+ })
36
+ );
37
+ }
38
+ const tokens = await ofetch(
39
+ tokenURL,
40
+ {
41
+ method: "POST",
42
+ headers: {
43
+ "Content-Type": "application/json"
44
+ },
45
+ body: {
46
+ grant_type: "authorization_code",
47
+ client_id: config.clientId,
48
+ client_secret: config.clientSecret,
49
+ redirect_uri: parsePath(redirectUrl).pathname,
50
+ code
51
+ }
52
+ }
53
+ ).catch((error) => {
54
+ return { error };
55
+ });
56
+ if (tokens.error) {
57
+ const error = createError({
58
+ statusCode: 401,
59
+ message: `Auth0 login failed: ${tokens.error?.data?.error_description || "Unknown error"}`,
60
+ data: tokens
61
+ });
62
+ if (!onError)
63
+ throw error;
64
+ return onError(event, error);
65
+ }
66
+ const tokenType = tokens.token_type;
67
+ const accessToken = tokens.access_token;
68
+ const user = await ofetch(`https://${config.domain}/userinfo`, {
69
+ headers: {
70
+ Authorization: `${tokenType} ${accessToken}`
71
+ }
72
+ });
73
+ return onSuccess(event, {
74
+ tokens,
75
+ user
76
+ });
77
+ });
78
+ }
@@ -0,0 +1,47 @@
1
+ import type { H3Event, H3Error } from 'h3';
2
+ export interface OAuthBattledotnetConfig {
3
+ /**
4
+ * Battle.net OAuth Client ID
5
+ * @default process.env.NUXT_OAUTH_BATTLEDOTNET_CLIENT_ID
6
+ */
7
+ clientId?: string;
8
+ /**
9
+ * Battle.net OAuth Client Secret
10
+ * @default process.env.NUXT_OAUTH_BATTLEDOTNET_CLIENT_SECRET
11
+ */
12
+ clientSecret?: string;
13
+ /**
14
+ * Battle.net OAuth Scope
15
+ * @default []
16
+ * @see https://develop.battle.net/documentation/guides/using-oauth
17
+ * @example ['openid', 'wow.profile', 'sc2.profile', 'd3.profile']
18
+ */
19
+ scope?: string[];
20
+ /**
21
+ * Battle.net OAuth Region
22
+ * @default EU
23
+ * @see https://develop.battle.net/documentation/guides/using-oauth
24
+ * @example EU (possible values: US, EU, APAC)
25
+ */
26
+ region?: string;
27
+ /**
28
+ * Battle.net OAuth Authorization URL
29
+ * @default 'https://oauth.battle.net/authorize'
30
+ */
31
+ authorizationURL?: string;
32
+ /**
33
+ * Battle.net OAuth Token URL
34
+ * @default 'https://oauth.battle.net/token'
35
+ */
36
+ tokenURL?: string;
37
+ }
38
+ interface OAuthConfig {
39
+ config?: OAuthBattledotnetConfig;
40
+ onSuccess: (event: H3Event, result: {
41
+ user: any;
42
+ tokens: any;
43
+ }) => Promise<void> | void;
44
+ onError?: (event: H3Event, error: H3Error) => Promise<void> | void;
45
+ }
46
+ export declare function battledotnetEventHandler({ config, onSuccess, onError }: OAuthConfig): import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<void>>;
47
+ export {};
@@ -0,0 +1,110 @@
1
+ import { eventHandler, createError, getQuery, getRequestURL, sendRedirect } from "h3";
2
+ import { ofetch } from "ofetch";
3
+ import { withQuery, parsePath } from "ufo";
4
+ import { defu } from "defu";
5
+ import { useRuntimeConfig } from "#imports";
6
+ import { randomUUID } from "crypto";
7
+ export function battledotnetEventHandler({ config, onSuccess, onError }) {
8
+ return eventHandler(async (event) => {
9
+ config = defu(config, useRuntimeConfig(event).oauth?.battledotnet, {
10
+ authorizationURL: "https://oauth.battle.net/authorize",
11
+ tokenURL: "https://oauth.battle.net/token"
12
+ });
13
+ const query = getQuery(event);
14
+ const { code } = query;
15
+ if (query.error) {
16
+ const error = createError({
17
+ statusCode: 401,
18
+ message: `Battle.net login failed: ${query.error || "Unknown error"}`,
19
+ data: query
20
+ });
21
+ if (!onError)
22
+ throw error;
23
+ return onError(event, error);
24
+ }
25
+ if (!config.clientId || !config.clientSecret) {
26
+ const error = createError({
27
+ statusCode: 500,
28
+ message: "Missing NUXT_OAUTH_BATTLEDOTNET_CLIENT_ID or NUXT_OAUTH_BATTLEDOTNET_CLIENT_SECRET env variables."
29
+ });
30
+ if (!onError)
31
+ throw error;
32
+ return onError(event, error);
33
+ }
34
+ if (!code) {
35
+ config.scope = config.scope || ["openid"];
36
+ config.region = config.region || "EU";
37
+ if (config.region === "CN") {
38
+ config.authorizationURL = "https://oauth.battlenet.com.cn/authorize";
39
+ config.tokenURL = "https://oauth.battlenet.com.cn/token";
40
+ }
41
+ const redirectUrl2 = getRequestURL(event).href;
42
+ return sendRedirect(
43
+ event,
44
+ withQuery(config.authorizationURL, {
45
+ client_id: config.clientId,
46
+ redirect_uri: redirectUrl2,
47
+ scope: config.scope.join(" "),
48
+ state: randomUUID(),
49
+ // Todo: handle PKCE flow
50
+ response_type: "code"
51
+ })
52
+ );
53
+ }
54
+ const redirectUrl = getRequestURL(event).href;
55
+ config.scope = config.scope || [];
56
+ if (!config.scope.includes("openid")) {
57
+ config.scope.push("openid");
58
+ }
59
+ const authCode = Buffer.from(`${config.clientId}:${config.clientSecret}`).toString("base64");
60
+ const tokens = await $fetch(
61
+ config.tokenURL,
62
+ {
63
+ method: "POST",
64
+ headers: {
65
+ "Content-Type": "application/x-www-form-urlencoded",
66
+ Authorization: `Basic ${authCode}`
67
+ },
68
+ params: {
69
+ code,
70
+ grant_type: "authorization_code",
71
+ scope: config.scope.join(" "),
72
+ redirect_uri: parsePath(redirectUrl).pathname
73
+ }
74
+ }
75
+ ).catch((error) => {
76
+ return { error };
77
+ });
78
+ if (tokens.error) {
79
+ const error = createError({
80
+ statusCode: 401,
81
+ message: `Battle.net login failed: ${tokens.error || "Unknown error"}`,
82
+ data: tokens
83
+ });
84
+ if (!onError)
85
+ throw error;
86
+ return onError(event, error);
87
+ }
88
+ const accessToken = tokens.access_token;
89
+ const user = await ofetch("https://oauth.battle.net/userinfo", {
90
+ headers: {
91
+ "User-Agent": `Battledotnet-OAuth-${config.clientId}`,
92
+ Authorization: `Bearer ${accessToken}`
93
+ }
94
+ });
95
+ if (!user) {
96
+ const error = createError({
97
+ statusCode: 500,
98
+ message: "Could not get Battle.net user",
99
+ data: tokens
100
+ });
101
+ if (!onError)
102
+ throw error;
103
+ return onError(event, error);
104
+ }
105
+ return onSuccess(event, {
106
+ user,
107
+ tokens
108
+ });
109
+ });
110
+ }
@@ -0,0 +1,51 @@
1
+ import type { H3Event, H3Error } from 'h3';
2
+ export interface OAuthDiscordConfig {
3
+ /**
4
+ * Discord OAuth Client ID
5
+ * @default process.env.NUXT_OAUTH_DISCORD_CLIENT_ID
6
+ */
7
+ clientId?: string;
8
+ /**
9
+ * Discord OAuth Client Secret
10
+ * @default process.env.NUXT_OAUTH_DISCORD_CLIENT_SECRET
11
+ */
12
+ clientSecret?: string;
13
+ /**
14
+ * Discord OAuth Scope
15
+ * @default []
16
+ * @see https://discord.com/developers/docs/topics/oauth2#shared-resources-oauth2-scopes
17
+ * @example ['identify', 'email']
18
+ * Without the identify scope the user will not be returned.
19
+ */
20
+ scope?: string[];
21
+ /**
22
+ * Require email from user, adds the ['email'] scope if not present.
23
+ * @default false
24
+ */
25
+ emailRequired?: boolean;
26
+ /**
27
+ * Require profile from user, adds the ['identify'] scope if not present.
28
+ * @default true
29
+ */
30
+ profileRequired?: boolean;
31
+ /**
32
+ * Discord OAuth Authorization URL
33
+ * @default 'https://discord.com/oauth2/authorize'
34
+ */
35
+ authorizationURL?: string;
36
+ /**
37
+ * Discord OAuth Token URL
38
+ * @default 'https://discord.com/api/oauth2/token'
39
+ */
40
+ tokenURL?: string;
41
+ }
42
+ interface OAuthConfig {
43
+ config?: OAuthDiscordConfig;
44
+ onSuccess: (event: H3Event, result: {
45
+ user: any;
46
+ tokens: any;
47
+ }) => Promise<void> | void;
48
+ onError?: (event: H3Event, error: H3Error) => Promise<void> | void;
49
+ }
50
+ export declare function discordEventHandler({ config, onSuccess, onError }: OAuthConfig): import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<void>>;
51
+ export {};
@@ -0,0 +1,85 @@
1
+ import { eventHandler, createError, getQuery, getRequestURL, sendRedirect } from "h3";
2
+ import { withQuery, parseURL, stringifyParsedURL } from "ufo";
3
+ import { ofetch } from "ofetch";
4
+ import { defu } from "defu";
5
+ import { useRuntimeConfig } from "#imports";
6
+ export function discordEventHandler({ config, onSuccess, onError }) {
7
+ return eventHandler(async (event) => {
8
+ config = defu(config, useRuntimeConfig(event).oauth?.discord, {
9
+ authorizationURL: "https://discord.com/oauth2/authorize",
10
+ tokenURL: "https://discord.com/api/oauth2/token",
11
+ profileRequired: true
12
+ });
13
+ const { code } = getQuery(event);
14
+ if (!config.clientId || !config.clientSecret) {
15
+ const error = createError({
16
+ statusCode: 500,
17
+ message: "Missing NUXT_OAUTH_DISCORD_CLIENT_ID or NUXT_OAUTH_DISCORD_CLIENT_SECRET env variables."
18
+ });
19
+ if (!onError)
20
+ throw error;
21
+ return onError(event, error);
22
+ }
23
+ const redirectUrl = getRequestURL(event).href;
24
+ if (!code) {
25
+ config.scope = config.scope || [];
26
+ if (config.emailRequired && !config.scope.includes("email")) {
27
+ config.scope.push("email");
28
+ }
29
+ if (config.profileRequired && !config.scope.includes("identify")) {
30
+ config.scope.push("identify");
31
+ }
32
+ return sendRedirect(
33
+ event,
34
+ withQuery(config.authorizationURL, {
35
+ response_type: "code",
36
+ client_id: config.clientId,
37
+ redirect_uri: redirectUrl,
38
+ scope: config.scope.join(" ")
39
+ })
40
+ );
41
+ }
42
+ const parsedRedirectUrl = parseURL(redirectUrl);
43
+ parsedRedirectUrl.search = "";
44
+ const tokens = await ofetch(
45
+ config.tokenURL,
46
+ {
47
+ method: "POST",
48
+ headers: {
49
+ "Content-Type": "application/x-www-form-urlencoded"
50
+ },
51
+ body: new URLSearchParams({
52
+ client_id: config.clientId,
53
+ client_secret: config.clientSecret,
54
+ grant_type: "authorization_code",
55
+ redirect_uri: stringifyParsedURL(parsedRedirectUrl),
56
+ code
57
+ }).toString()
58
+ }
59
+ ).catch((error) => {
60
+ return { error };
61
+ });
62
+ if (tokens.error) {
63
+ console.log(tokens);
64
+ const error = createError({
65
+ statusCode: 401,
66
+ message: `Discord login failed: ${tokens.error?.data?.error_description || "Unknown error"}`,
67
+ data: tokens
68
+ });
69
+ if (!onError)
70
+ throw error;
71
+ return onError(event, error);
72
+ }
73
+ const accessToken = tokens.access_token;
74
+ const user = await ofetch("https://discord.com/api/users/@me", {
75
+ headers: {
76
+ "user-agent": "Nuxt Auth Utils",
77
+ Authorization: `Bearer ${accessToken}`
78
+ }
79
+ });
80
+ return onSuccess(event, {
81
+ tokens,
82
+ user
83
+ });
84
+ });
85
+ }
@@ -0,0 +1,45 @@
1
+ import type { H3Event, H3Error } from 'h3';
2
+ export interface OAuthGoogleConfig {
3
+ /**
4
+ * Google OAuth Client ID
5
+ * @default process.env.NUXT_OAUTH_GOOGLE_CLIENT_ID
6
+ */
7
+ clientId?: string;
8
+ /**
9
+ * Google OAuth Client Secret
10
+ * @default process.env.NUXT_OAUTH_GOOGLE_CLIENT_SECRET
11
+ */
12
+ clientSecret?: string;
13
+ /**
14
+ * Google OAuth Scope
15
+ * @default []
16
+ * @see https://developers.google.com/identity/protocols/oauth2/scopes#google-sign-in
17
+ * @example ['email', 'openid', 'profile']
18
+ */
19
+ scope?: string[];
20
+ /**
21
+ * Google OAuth Authorization URL
22
+ * @default 'https://accounts.google.com/o/oauth2/v2/auth'
23
+ */
24
+ authorizationURL?: string;
25
+ /**
26
+ * Google OAuth Token URL
27
+ * @default 'https://oauth2.googleapis.com/token'
28
+ */
29
+ tokenURL?: string;
30
+ /**
31
+ * Redirect URL post authenticating via google
32
+ * @default '/auth/google'
33
+ */
34
+ redirectUrl: '/auth/google';
35
+ }
36
+ interface OAuthConfig {
37
+ config?: OAuthGoogleConfig;
38
+ onSuccess: (event: H3Event, result: {
39
+ user: any;
40
+ tokens: any;
41
+ }) => Promise<void> | void;
42
+ onError?: (event: H3Event, error: H3Error) => Promise<void> | void;
43
+ }
44
+ export declare function googleEventHandler({ config, onSuccess, onError, }: OAuthConfig): import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<void>>;
45
+ export {};
@@ -0,0 +1,82 @@
1
+ import {
2
+ eventHandler,
3
+ createError,
4
+ getQuery,
5
+ getRequestURL,
6
+ sendRedirect
7
+ } from "h3";
8
+ import { withQuery, parsePath } from "ufo";
9
+ import { ofetch } from "ofetch";
10
+ import { defu } from "defu";
11
+ import { useRuntimeConfig } from "#imports";
12
+ export function googleEventHandler({
13
+ config,
14
+ onSuccess,
15
+ onError
16
+ }) {
17
+ return eventHandler(async (event) => {
18
+ config = defu(config, useRuntimeConfig(event).oauth?.google, {
19
+ authorizationURL: "https://accounts.google.com/o/oauth2/v2/auth",
20
+ tokenURL: "https://oauth2.googleapis.com/token"
21
+ });
22
+ const { code } = getQuery(event);
23
+ if (!config.clientId) {
24
+ const error = createError({
25
+ statusCode: 500,
26
+ message: "Missing NUXT_OAUTH_GOOGLE_CLIENT_ID env variables."
27
+ });
28
+ if (!onError)
29
+ throw error;
30
+ return onError(event, error);
31
+ }
32
+ const redirectUrl = getRequestURL(event).href;
33
+ if (!code) {
34
+ config.scope = config.scope || ["email", "profile"];
35
+ return sendRedirect(
36
+ event,
37
+ withQuery(config.authorizationURL, {
38
+ response_type: "code",
39
+ client_id: config.clientId,
40
+ redirect_uri: redirectUrl,
41
+ scope: config.scope.join(" ")
42
+ })
43
+ );
44
+ }
45
+ const body = {
46
+ grant_type: "authorization_code",
47
+ redirect_uri: parsePath(redirectUrl).pathname,
48
+ client_id: config.clientId,
49
+ client_secret: config.clientSecret,
50
+ code
51
+ };
52
+ const tokens = await ofetch(config.tokenURL, {
53
+ method: "POST",
54
+ body
55
+ }).catch((error) => {
56
+ return { error };
57
+ });
58
+ if (tokens.error) {
59
+ const error = createError({
60
+ statusCode: 401,
61
+ message: `Google login failed: ${tokens.error?.data?.error_description || "Unknown error"}`,
62
+ data: tokens
63
+ });
64
+ if (!onError)
65
+ throw error;
66
+ return onError(event, error);
67
+ }
68
+ const accessToken = tokens.access_token;
69
+ const user = await ofetch(
70
+ "https://www.googleapis.com/oauth2/v3/userinfo",
71
+ {
72
+ headers: {
73
+ Authorization: `Bearer ${accessToken}`
74
+ }
75
+ }
76
+ );
77
+ return onSuccess(event, {
78
+ tokens,
79
+ user
80
+ });
81
+ });
82
+ }
@@ -0,0 +1,45 @@
1
+ import type { H3Event, H3Error } from 'h3';
2
+ export interface OAuthTwitchConfig {
3
+ /**
4
+ * Twitch Client ID
5
+ * @default process.env.NUXT_OAUTH_TWITCH_CLIENT_ID
6
+ */
7
+ clientId?: string;
8
+ /**
9
+ * Twitch OAuth Client Secret
10
+ * @default process.env.NUXT_OAUTH_TWITCH_CLIENT_SECRET
11
+ */
12
+ clientSecret?: string;
13
+ /**
14
+ * Twitch OAuth Scope
15
+ * @default []
16
+ * @see https://dev.twitch.tv/docs/authentication/scopes
17
+ * @example ['user:read:email']
18
+ */
19
+ scope?: string[];
20
+ /**
21
+ * Require email from user, adds the ['user:read:email'] scope if not present
22
+ * @default false
23
+ */
24
+ emailRequired?: boolean;
25
+ /**
26
+ * Twitch OAuth Authorization URL
27
+ * @default 'https://id.twitch.tv/oauth2/authorize'
28
+ */
29
+ authorizationURL?: string;
30
+ /**
31
+ * Twitch OAuth Token URL
32
+ * @default 'https://id.twitch.tv/oauth2/token'
33
+ */
34
+ tokenURL?: string;
35
+ }
36
+ interface OAuthConfig {
37
+ config?: OAuthTwitchConfig;
38
+ onSuccess: (event: H3Event, result: {
39
+ user: any;
40
+ tokens: any;
41
+ }) => Promise<void> | void;
42
+ onError?: (event: H3Event, error: H3Error) => Promise<void> | void;
43
+ }
44
+ export declare function twitchEventHandler({ config, onSuccess, onError }: OAuthConfig): import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<void>>;
45
+ export {};
@@ -0,0 +1,89 @@
1
+ import { eventHandler, createError, getQuery, getRequestURL, sendRedirect } from "h3";
2
+ import { withQuery, parsePath } from "ufo";
3
+ import { ofetch } from "ofetch";
4
+ import { defu } from "defu";
5
+ import { useRuntimeConfig } from "#imports";
6
+ export function twitchEventHandler({ config, onSuccess, onError }) {
7
+ return eventHandler(async (event) => {
8
+ config = defu(config, useRuntimeConfig(event).oauth?.twitch, {
9
+ authorizationURL: "https://id.twitch.tv/oauth2/authorize",
10
+ tokenURL: "https://id.twitch.tv/oauth2/token"
11
+ });
12
+ const { code } = getQuery(event);
13
+ if (!config.clientId) {
14
+ const error = createError({
15
+ statusCode: 500,
16
+ message: "Missing NUXT_OAUTH_TWITCH_CLIENT_ID env variables."
17
+ });
18
+ if (!onError)
19
+ throw error;
20
+ return onError(event, error);
21
+ }
22
+ const redirectUrl = getRequestURL(event).href;
23
+ if (!code) {
24
+ config.scope = config.scope || [];
25
+ if (config.emailRequired && !config.scope.includes("user:read:email")) {
26
+ config.scope.push("user:read:email");
27
+ }
28
+ return sendRedirect(
29
+ event,
30
+ withQuery(config.authorizationURL, {
31
+ response_type: "code",
32
+ client_id: config.clientId,
33
+ redirect_uri: redirectUrl,
34
+ scope: config.scope.join("%20")
35
+ })
36
+ );
37
+ }
38
+ const tokens = await ofetch(
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
+ }
52
+ }
53
+ ).catch((error) => {
54
+ return { error };
55
+ });
56
+ 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)
63
+ throw error;
64
+ return onError(event, error);
65
+ }
66
+ const accessToken = tokens.access_token;
67
+ const users = await ofetch("https://api.twitch.tv/helix/users", {
68
+ headers: {
69
+ "Client-ID": config.clientId,
70
+ Authorization: `Bearer ${accessToken}`
71
+ }
72
+ });
73
+ const user = users.data?.[0];
74
+ if (!user) {
75
+ const error = createError({
76
+ statusCode: 500,
77
+ message: "Could not get Twitch user",
78
+ data: tokens
79
+ });
80
+ if (!onError)
81
+ throw error;
82
+ return onError(event, error);
83
+ }
84
+ return onSuccess(event, {
85
+ tokens,
86
+ user
87
+ });
88
+ });
89
+ }
@@ -1,6 +1,16 @@
1
1
  import { githubEventHandler } from '../lib/oauth/github';
2
+ import { googleEventHandler } from '../lib/oauth/google';
2
3
  import { spotifyEventHandler } from '../lib/oauth/spotify';
4
+ import { twitchEventHandler } from '../lib/oauth/twitch';
5
+ import { auth0EventHandler } from '../lib/oauth/auth0';
6
+ import { discordEventHandler } from '../lib/oauth/discord';
7
+ import { battledotnetEventHandler } from '../lib/oauth/battledotnet';
3
8
  export declare const oauth: {
4
9
  githubEventHandler: typeof githubEventHandler;
5
10
  spotifyEventHandler: typeof spotifyEventHandler;
11
+ googleEventHandler: typeof googleEventHandler;
12
+ twitchEventHandler: typeof twitchEventHandler;
13
+ auth0EventHandler: typeof auth0EventHandler;
14
+ discordEventHandler: typeof discordEventHandler;
15
+ battledotnetEventHandler: typeof battledotnetEventHandler;
6
16
  };
@@ -1,6 +1,16 @@
1
1
  import { githubEventHandler } from "../lib/oauth/github.mjs";
2
+ import { googleEventHandler } from "../lib/oauth/google.mjs";
2
3
  import { spotifyEventHandler } from "../lib/oauth/spotify.mjs";
4
+ import { twitchEventHandler } from "../lib/oauth/twitch.mjs";
5
+ import { auth0EventHandler } from "../lib/oauth/auth0.mjs";
6
+ import { discordEventHandler } from "../lib/oauth/discord.mjs";
7
+ import { battledotnetEventHandler } from "../lib/oauth/battledotnet.mjs";
3
8
  export const oauth = {
4
9
  githubEventHandler,
5
- spotifyEventHandler
10
+ spotifyEventHandler,
11
+ googleEventHandler,
12
+ twitchEventHandler,
13
+ auth0EventHandler,
14
+ discordEventHandler,
15
+ battledotnetEventHandler
6
16
  };
package/dist/types.d.mts CHANGED
@@ -1,5 +1,6 @@
1
1
 
2
- import { ModuleOptions } from './module'
2
+ import type { ModuleOptions } from './module'
3
+
3
4
 
4
5
  declare module '@nuxt/schema' {
5
6
  interface NuxtConfig { ['auth']?: Partial<ModuleOptions> }
@@ -12,4 +13,4 @@ declare module 'nuxt/schema' {
12
13
  }
13
14
 
14
15
 
15
- export { ModuleOptions, default } from './module'
16
+ export type { ModuleOptions, default } from './module'
package/dist/types.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
 
2
- import { ModuleOptions } from './module'
2
+ import type { ModuleOptions } from './module'
3
+
3
4
 
4
5
  declare module '@nuxt/schema' {
5
6
  interface NuxtConfig { ['auth']?: Partial<ModuleOptions> }
@@ -12,4 +13,4 @@ declare module 'nuxt/schema' {
12
13
  }
13
14
 
14
15
 
15
- export { ModuleOptions, default } from './module'
16
+ export type { ModuleOptions, default } from './module'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nuxt-auth-utils",
3
- "version": "0.0.4",
3
+ "version": "0.0.6",
4
4
  "description": "Minimalist Auth module for Nuxt with SSR",
5
5
  "repository": "Atinux/nuxt-auth-utils",
6
6
  "license": "MIT",
@@ -32,23 +32,24 @@
32
32
  "dependencies": {
33
33
  "@nuxt/kit": "^3.8.1",
34
34
  "defu": "^6.1.3",
35
+ "ofetch": "^1.3.3",
35
36
  "ohash": "^1.1.3"
36
37
  },
37
38
  "devDependencies": {
38
- "@iconify-json/simple-icons": "^1.1.76",
39
+ "@iconify-json/simple-icons": "^1.1.78",
39
40
  "@nuxt/devtools": "latest",
40
41
  "@nuxt/eslint-config": "^0.2.0",
41
- "@nuxt/module-builder": "^0.5.2",
42
+ "@nuxt/module-builder": "^0.5.4",
42
43
  "@nuxt/schema": "^3.8.1",
43
44
  "@nuxt/test-utils": "^3.8.1",
44
45
  "@nuxt/ui": "^2.10.0",
45
46
  "@nuxt/ui-pro": "^0.4.2",
46
- "@types/node": "^20.8.9",
47
+ "@types/node": "^20.9.0",
47
48
  "changelogen": "^0.5.5",
48
- "eslint": "^8.52.0",
49
+ "eslint": "^8.53.0",
49
50
  "nuxt": "^3.8.1",
50
51
  "typescript": "^5.2.2",
51
- "vitest": "^0.33.0",
52
+ "vitest": "^0.34.6",
52
53
  "vue-tsc": "^1.8.22"
53
54
  }
54
55
  }