nuxt-auth-utils 0.3.7 → 0.3.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.
- package/README.md +28 -6
- package/dist/module.json +2 -2
- package/dist/module.mjs +18 -1
- package/dist/runtime/app/plugins/session.client.d.ts +2 -0
- package/dist/runtime/app/plugins/session.server.d.ts +2 -0
- package/dist/runtime/server/api/session.get.d.ts +4 -1
- package/dist/runtime/server/api/session.get.js +2 -1
- package/dist/runtime/server/lib/oauth/gitlab.d.ts +47 -0
- package/dist/runtime/server/lib/oauth/gitlab.js +83 -0
- package/dist/runtime/server/lib/oauth/instagram.d.ts +48 -0
- package/dist/runtime/server/lib/oauth/instagram.js +70 -0
- package/dist/runtime/server/lib/oauth/vk.d.ts +46 -0
- package/dist/runtime/server/lib/oauth/vk.js +87 -0
- package/dist/runtime/server/lib/oauth/x.js +22 -18
- package/dist/runtime/server/lib/oauth/yandex.js +12 -2
- package/dist/runtime/server/plugins/oauth.js +1 -1
- package/dist/runtime/server/utils/session.d.ts +3 -3
- package/dist/runtime/server/utils/session.js +7 -6
- package/dist/runtime/types/oauth-config.d.ts +1 -1
- package/dist/runtime/types/session.d.ts +11 -0
- package/package.json +8 -8
package/README.md
CHANGED
|
@@ -15,9 +15,9 @@ Add Authentication to Nuxt applications with secured & sealed cookies sessions.
|
|
|
15
15
|
## Features
|
|
16
16
|
|
|
17
17
|
- [Hybrid Rendering](#hybrid-rendering) support (SSR / CSR / SWR / Prerendering)
|
|
18
|
-
- [
|
|
19
|
-
- [Vue composable](#vue-composable)
|
|
20
|
-
- [
|
|
18
|
+
- [20+ OAuth Providers](#supported-oauth-providers)
|
|
19
|
+
- [`useUserSession()` Vue composable](#vue-composable)
|
|
20
|
+
- [Tree-shakable server utils](#server-utils)
|
|
21
21
|
- [`<AuthState>` component](#authstate-component)
|
|
22
22
|
- [Extendable with hooks](#extend-session)
|
|
23
23
|
|
|
@@ -105,6 +105,9 @@ interface UserSessionComposable {
|
|
|
105
105
|
}
|
|
106
106
|
```
|
|
107
107
|
|
|
108
|
+
> [!IMPORTANT]
|
|
109
|
+
> Nuxt Auth Utils uses the `/api/_auth/session` route for session management. Ensure your API route middleware doesn't interfere with this path.
|
|
110
|
+
|
|
108
111
|
## Server Utils
|
|
109
112
|
|
|
110
113
|
The following helpers are auto-imported in your `server/` directory.
|
|
@@ -114,13 +117,18 @@ The following helpers are auto-imported in your `server/` directory.
|
|
|
114
117
|
```ts
|
|
115
118
|
// Set a user session, note that this data is encrypted in the cookie but can be decrypted with an API call
|
|
116
119
|
// Only store the data that allow you to recognize a user, but do not store sensitive data
|
|
117
|
-
// Merges new data with existing data using defu
|
|
120
|
+
// Merges new data with existing data using unjs/defu library
|
|
118
121
|
await setUserSession(event, {
|
|
122
|
+
// User data
|
|
119
123
|
user: {
|
|
120
|
-
|
|
124
|
+
login: 'atinux'
|
|
125
|
+
},
|
|
126
|
+
// Private data accessible on server/ routes
|
|
127
|
+
secure: {
|
|
128
|
+
apiToken: '1234567890'
|
|
121
129
|
},
|
|
130
|
+
// Any extra fields for the session data
|
|
122
131
|
loggedInAt: new Date()
|
|
123
|
-
// Any extra fields
|
|
124
132
|
})
|
|
125
133
|
|
|
126
134
|
// Replace a user session. Same behaviour as setUserSession, except it does not merge data with existing data
|
|
@@ -153,6 +161,9 @@ declare module '#auth-utils' {
|
|
|
153
161
|
export {}
|
|
154
162
|
```
|
|
155
163
|
|
|
164
|
+
> [!IMPORTANT]
|
|
165
|
+
> Since we encrypt and store session data in cookies, we're constrained by the 4096-byte cookie size limit. Store only essential information.
|
|
166
|
+
|
|
156
167
|
### OAuth Event Handlers
|
|
157
168
|
|
|
158
169
|
All handlers can be auto-imported and used in your server routes or API routes.
|
|
@@ -192,7 +203,9 @@ It can also be set using environment variables:
|
|
|
192
203
|
- Discord
|
|
193
204
|
- Facebook
|
|
194
205
|
- GitHub
|
|
206
|
+
- GitLab
|
|
195
207
|
- Google
|
|
208
|
+
- Instagram
|
|
196
209
|
- Keycloak
|
|
197
210
|
- LinkedIn
|
|
198
211
|
- Microsoft
|
|
@@ -201,6 +214,7 @@ It can also be set using environment variables:
|
|
|
201
214
|
- Steam
|
|
202
215
|
- TikTok
|
|
203
216
|
- Twitch
|
|
217
|
+
- VK
|
|
204
218
|
- X (Twitter)
|
|
205
219
|
- XSUAA
|
|
206
220
|
- Yandex
|
|
@@ -350,6 +364,14 @@ Our defaults are:
|
|
|
350
364
|
}
|
|
351
365
|
```
|
|
352
366
|
|
|
367
|
+
You can also overwrite the session config by passing it as 3rd argument of the `setUserSession` and `replaceUserSession` functions:
|
|
368
|
+
|
|
369
|
+
```ts
|
|
370
|
+
await setUserSession(event, { ... } , {
|
|
371
|
+
maxAge: 60 * 60 * 24 * 7 // 1 week
|
|
372
|
+
})
|
|
373
|
+
```
|
|
374
|
+
|
|
353
375
|
Checkout the [`SessionConfig`](https://github.com/unjs/h3/blob/c04c458810e34eb15c1647e1369e7d7ef19f567d/src/utils/session.ts#L20) for all options.
|
|
354
376
|
|
|
355
377
|
## More
|
package/dist/module.json
CHANGED
package/dist/module.mjs
CHANGED
|
@@ -13,7 +13,9 @@ const module = defineNuxtModule({
|
|
|
13
13
|
defaults: {},
|
|
14
14
|
async setup(options, nuxt) {
|
|
15
15
|
const resolver = createResolver(import.meta.url);
|
|
16
|
-
nuxt.options.alias["#auth-utils"] = resolver.resolve(
|
|
16
|
+
nuxt.options.alias["#auth-utils"] = resolver.resolve(
|
|
17
|
+
"./runtime/types/index"
|
|
18
|
+
);
|
|
17
19
|
addComponentsDir({ path: resolver.resolve("./runtime/app/components") });
|
|
18
20
|
addImportsDir(resolver.resolve("./runtime/app/composables"));
|
|
19
21
|
addPlugin(resolver.resolve("./runtime/app/plugins/session.server"));
|
|
@@ -58,6 +60,11 @@ const module = defineNuxtModule({
|
|
|
58
60
|
clientSecret: "",
|
|
59
61
|
redirectURL: ""
|
|
60
62
|
});
|
|
63
|
+
runtimeConfig.oauth.gitlab = defu(runtimeConfig.oauth.gitlab, {
|
|
64
|
+
clientId: "",
|
|
65
|
+
clientSecret: "",
|
|
66
|
+
redirectURL: ""
|
|
67
|
+
});
|
|
61
68
|
runtimeConfig.oauth.spotify = defu(runtimeConfig.oauth.spotify, {
|
|
62
69
|
clientId: "",
|
|
63
70
|
clientSecret: "",
|
|
@@ -124,6 +131,11 @@ const module = defineNuxtModule({
|
|
|
124
131
|
clientSecret: "",
|
|
125
132
|
redirectURL: ""
|
|
126
133
|
});
|
|
134
|
+
runtimeConfig.oauth.instagram = defu(runtimeConfig.oauth.instagram, {
|
|
135
|
+
clientId: "",
|
|
136
|
+
clientSecret: "",
|
|
137
|
+
redirectURL: ""
|
|
138
|
+
});
|
|
127
139
|
runtimeConfig.oauth.paypal = defu(runtimeConfig.oauth.paypal, {
|
|
128
140
|
clientId: "",
|
|
129
141
|
clientSecret: "",
|
|
@@ -144,6 +156,11 @@ const module = defineNuxtModule({
|
|
|
144
156
|
domain: "",
|
|
145
157
|
redirectURL: ""
|
|
146
158
|
});
|
|
159
|
+
runtimeConfig.oauth.vk = defu(runtimeConfig.oauth.vk, {
|
|
160
|
+
clientId: "",
|
|
161
|
+
clientSecret: "",
|
|
162
|
+
redirectURL: ""
|
|
163
|
+
});
|
|
147
164
|
runtimeConfig.oauth.yandex = defu(runtimeConfig.oauth.yandex, {
|
|
148
165
|
clientId: "",
|
|
149
166
|
clientSecret: "",
|
|
@@ -1,2 +1,5 @@
|
|
|
1
|
-
declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<
|
|
1
|
+
declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<{
|
|
2
|
+
[key: string]: unknown;
|
|
3
|
+
user?: import("#auth-utils").User;
|
|
4
|
+
}>>;
|
|
2
5
|
export default _default;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { OAuthConfig } from '#auth-utils';
|
|
2
|
+
export interface OAuthGitLabConfig {
|
|
3
|
+
/**
|
|
4
|
+
* GitLab OAuth Client ID
|
|
5
|
+
* @default process.env.NUXT_OAUTH_GITLAB_CLIENT_ID
|
|
6
|
+
*/
|
|
7
|
+
clientId?: string;
|
|
8
|
+
/**
|
|
9
|
+
* GitLab OAuth Client Secret
|
|
10
|
+
* @default process.env.NUXT_OAUTH_GITLAB_CLIENT_SECRET
|
|
11
|
+
*/
|
|
12
|
+
clientSecret?: string;
|
|
13
|
+
/**
|
|
14
|
+
* GitLab OAuth Scope
|
|
15
|
+
* @default ['read_user']
|
|
16
|
+
* @see https://docs.gitlab.com/ee/integration/oauth_provider.html#view-all-authorized-applications
|
|
17
|
+
* @example ['read_user']
|
|
18
|
+
*/
|
|
19
|
+
scope?: string[];
|
|
20
|
+
/**
|
|
21
|
+
* Require email from user, adds the ['email'] scope if not present
|
|
22
|
+
* @default false
|
|
23
|
+
*/
|
|
24
|
+
emailRequired?: boolean;
|
|
25
|
+
/**
|
|
26
|
+
* GitLab OAuth Authorization URL
|
|
27
|
+
* @default 'https://gitlab.com/oauth/authorize'
|
|
28
|
+
*/
|
|
29
|
+
authorizationURL?: string;
|
|
30
|
+
/**
|
|
31
|
+
* GitLab OAuth Token URL
|
|
32
|
+
* @default 'https://gitlab.com/oauth/token'
|
|
33
|
+
*/
|
|
34
|
+
tokenURL?: string;
|
|
35
|
+
/**
|
|
36
|
+
* Extra authorization parameters to provide to the authorization URL
|
|
37
|
+
* @see https://docs.gitlab.com/ee/integration/oauth_provider.html#view-all-authorized-applications
|
|
38
|
+
* @example { allow_signup: 'true' }
|
|
39
|
+
*/
|
|
40
|
+
authorizationParams?: Record<string, string>;
|
|
41
|
+
/**
|
|
42
|
+
* Redirect URL to to allow overriding for situations like prod failing to determine public hostname
|
|
43
|
+
* @default process.env.NUXT_OAUTH_GITLAB_REDIRECT_URL
|
|
44
|
+
*/
|
|
45
|
+
redirectURL?: string;
|
|
46
|
+
}
|
|
47
|
+
export declare function oauthGitLabEventHandler({ config, onSuccess, onError, }: OAuthConfig<OAuthGitLabConfig>): import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<void>>;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { eventHandler, getQuery, sendRedirect } from "h3";
|
|
2
|
+
import { withQuery } from "ufo";
|
|
3
|
+
import { defu } from "defu";
|
|
4
|
+
import {
|
|
5
|
+
handleMissingConfiguration,
|
|
6
|
+
handleAccessTokenErrorResponse,
|
|
7
|
+
getOAuthRedirectURL,
|
|
8
|
+
requestAccessToken
|
|
9
|
+
} from "../utils.js";
|
|
10
|
+
import { useRuntimeConfig, createError } from "#imports";
|
|
11
|
+
export function oauthGitLabEventHandler({
|
|
12
|
+
config,
|
|
13
|
+
onSuccess,
|
|
14
|
+
onError
|
|
15
|
+
}) {
|
|
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",
|
|
20
|
+
authorizationParams: {}
|
|
21
|
+
});
|
|
22
|
+
const query = getQuery(event);
|
|
23
|
+
if (query.error) {
|
|
24
|
+
const error = createError({
|
|
25
|
+
statusCode: 401,
|
|
26
|
+
message: `GitLab login failed: ${query.error || "Unknown error"}`,
|
|
27
|
+
data: query
|
|
28
|
+
});
|
|
29
|
+
if (!onError) throw error;
|
|
30
|
+
return onError(event, error);
|
|
31
|
+
}
|
|
32
|
+
if (!config.clientId || !config.clientSecret) {
|
|
33
|
+
return handleMissingConfiguration(
|
|
34
|
+
event,
|
|
35
|
+
"gitlab",
|
|
36
|
+
["clientId", "clientSecret"],
|
|
37
|
+
onError
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
const redirectURL = config.redirectURL || getOAuthRedirectURL(event);
|
|
41
|
+
if (!query.code) {
|
|
42
|
+
config.scope = config.scope || [];
|
|
43
|
+
if (!config.scope.length) {
|
|
44
|
+
config.scope.push("read_user");
|
|
45
|
+
}
|
|
46
|
+
if (config.emailRequired && !config.scope.includes("email")) {
|
|
47
|
+
config.scope.push("email");
|
|
48
|
+
}
|
|
49
|
+
return sendRedirect(
|
|
50
|
+
event,
|
|
51
|
+
withQuery(config.authorizationURL, {
|
|
52
|
+
response_type: "code",
|
|
53
|
+
client_id: config.clientId,
|
|
54
|
+
redirect_uri: redirectURL,
|
|
55
|
+
scope: config.scope.join(" "),
|
|
56
|
+
...config.authorizationParams
|
|
57
|
+
})
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
const tokens = await requestAccessToken(config.tokenURL, {
|
|
61
|
+
body: {
|
|
62
|
+
grant_type: "authorization_code",
|
|
63
|
+
client_id: config.clientId,
|
|
64
|
+
client_secret: config.clientSecret,
|
|
65
|
+
redirect_uri: redirectURL,
|
|
66
|
+
code: query.code
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
if (tokens.error) {
|
|
70
|
+
return handleAccessTokenErrorResponse(event, "gitlab", tokens, onError);
|
|
71
|
+
}
|
|
72
|
+
const accessToken = tokens.access_token;
|
|
73
|
+
const user = await $fetch("https://gitlab.com/api/v4/user", {
|
|
74
|
+
headers: {
|
|
75
|
+
Authorization: `Bearer ${accessToken}`
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
return onSuccess(event, {
|
|
79
|
+
user,
|
|
80
|
+
tokens
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { OAuthConfig } from '#auth-utils';
|
|
2
|
+
export interface OAuthInstagramConfig {
|
|
3
|
+
/**
|
|
4
|
+
* Instagram OAuth Client ID
|
|
5
|
+
* @default process.env.NUXT_OAUTH_INSTAGRAM_CLIENT_ID
|
|
6
|
+
*/
|
|
7
|
+
clientId?: string;
|
|
8
|
+
/**
|
|
9
|
+
* Instagram OAuth Client Secret
|
|
10
|
+
* @default process.env.NUXT_OAUTH_INSTAGRAM_CLIENT_SECRET
|
|
11
|
+
*/
|
|
12
|
+
clientSecret?: string;
|
|
13
|
+
/**
|
|
14
|
+
* Instagram OAuth Scope
|
|
15
|
+
* @default [ 'user_profile' ]
|
|
16
|
+
* @see https://developers.facebook.com/docs/instagram-basic-display-api/overview#permissions
|
|
17
|
+
* @example [ 'user_profile', 'user_media' ],
|
|
18
|
+
*/
|
|
19
|
+
scope?: string[];
|
|
20
|
+
/**
|
|
21
|
+
* Instagram OAuth User Fields
|
|
22
|
+
* @default [ 'id', 'username'],
|
|
23
|
+
* @see https://developers.facebook.com/docs/instagram-basic-display-api/reference/user#fields
|
|
24
|
+
* @example [ 'id', 'username', 'account_type', 'media_count' ],
|
|
25
|
+
*/
|
|
26
|
+
fields?: string[];
|
|
27
|
+
/**
|
|
28
|
+
* Instagram OAuth Authorization URL
|
|
29
|
+
* @default 'https://api.instagram.com/oauth/authorize'
|
|
30
|
+
*/
|
|
31
|
+
authorizationURL?: string;
|
|
32
|
+
/**
|
|
33
|
+
* Instagram OAuth Token URL
|
|
34
|
+
* @default 'https://api.instagram.com/oauth/access_token'
|
|
35
|
+
*/
|
|
36
|
+
tokenURL?: string;
|
|
37
|
+
/**
|
|
38
|
+
* Extra authorization parameters to provide to the authorization URL
|
|
39
|
+
* @see https://developers.facebook.com/docs/facebook-login/guides/advanced/manual-flow/
|
|
40
|
+
*/
|
|
41
|
+
authorizationParams?: Record<string, string>;
|
|
42
|
+
/**
|
|
43
|
+
* Redirect URL to to allow overriding for situations like prod failing to determine public hostname
|
|
44
|
+
* @default process.env.NUXT_OAUTH_INSTAGRAM_REDIRECT_URL or current URL
|
|
45
|
+
*/
|
|
46
|
+
redirectURL?: string;
|
|
47
|
+
}
|
|
48
|
+
export declare function oauthInstagramEventHandler({ config, onSuccess, onError, }: OAuthConfig<OAuthInstagramConfig>): import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<void>>;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { eventHandler, getQuery, sendRedirect } from "h3";
|
|
2
|
+
import { withQuery } from "ufo";
|
|
3
|
+
import { defu } from "defu";
|
|
4
|
+
import { handleMissingConfiguration, handleAccessTokenErrorResponse, getOAuthRedirectURL, requestAccessToken } from "../utils.js";
|
|
5
|
+
import { useRuntimeConfig, createError } from "#imports";
|
|
6
|
+
export function oauthInstagramEventHandler({
|
|
7
|
+
config,
|
|
8
|
+
onSuccess,
|
|
9
|
+
onError
|
|
10
|
+
}) {
|
|
11
|
+
return eventHandler(async (event) => {
|
|
12
|
+
config = defu(config, useRuntimeConfig(event).oauth?.instagram, {
|
|
13
|
+
scope: ["user_profile"],
|
|
14
|
+
authorizationURL: "https://api.instagram.com/oauth/authorize",
|
|
15
|
+
tokenURL: "https://api.instagram.com/oauth/access_token",
|
|
16
|
+
authorizationParams: {}
|
|
17
|
+
});
|
|
18
|
+
const query = getQuery(event);
|
|
19
|
+
if (query.error) {
|
|
20
|
+
const error = createError({
|
|
21
|
+
statusCode: 401,
|
|
22
|
+
message: `Instagram login failed: ${query.error || "Unknown error"}`,
|
|
23
|
+
data: query
|
|
24
|
+
});
|
|
25
|
+
if (!onError) throw error;
|
|
26
|
+
return onError(event, error);
|
|
27
|
+
}
|
|
28
|
+
if (!config.clientId) {
|
|
29
|
+
return handleMissingConfiguration(event, "instagram", ["clientId"], onError);
|
|
30
|
+
}
|
|
31
|
+
const redirectURL = config.redirectURL || getOAuthRedirectURL(event);
|
|
32
|
+
if (!query.code) {
|
|
33
|
+
config.scope = config.scope || [];
|
|
34
|
+
return sendRedirect(
|
|
35
|
+
event,
|
|
36
|
+
withQuery(config.authorizationURL, {
|
|
37
|
+
client_id: config.clientId,
|
|
38
|
+
redirect_uri: redirectURL,
|
|
39
|
+
scope: config.scope.join(" "),
|
|
40
|
+
response_type: "code"
|
|
41
|
+
})
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
const tokens = await requestAccessToken(config.tokenURL, {
|
|
45
|
+
body: {
|
|
46
|
+
client_id: config.clientId,
|
|
47
|
+
client_secret: config.clientSecret,
|
|
48
|
+
grant_type: "authorization_code",
|
|
49
|
+
redirect_uri: redirectURL,
|
|
50
|
+
code: query.code
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
if (tokens.error) {
|
|
54
|
+
return handleAccessTokenErrorResponse(event, "instagram", tokens, onError);
|
|
55
|
+
}
|
|
56
|
+
const accessToken = tokens.access_token;
|
|
57
|
+
config.fields = config.fields || ["id", "username"];
|
|
58
|
+
const fields = config.fields.join(",");
|
|
59
|
+
const user = await $fetch(
|
|
60
|
+
`https://graph.instagram.com/v20.0/me?fields=${fields}&access_token=${accessToken}`
|
|
61
|
+
);
|
|
62
|
+
if (!user) {
|
|
63
|
+
throw new Error("Instagram login failed: no user found");
|
|
64
|
+
}
|
|
65
|
+
return onSuccess(event, {
|
|
66
|
+
user,
|
|
67
|
+
tokens
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { OAuthConfig } from '#auth-utils';
|
|
2
|
+
export interface OAuthVKConfig {
|
|
3
|
+
/**
|
|
4
|
+
* VK OAuth Client ID
|
|
5
|
+
* @default process.env.NUXT_OAUTH_VK_CLIENT_ID
|
|
6
|
+
*/
|
|
7
|
+
clientId?: string;
|
|
8
|
+
/**
|
|
9
|
+
* VK OAuth Client Secret
|
|
10
|
+
* @default process.env.NUXT_OAUTH_VK_CLIENT_SECRET
|
|
11
|
+
*/
|
|
12
|
+
clientSecret?: string;
|
|
13
|
+
/**
|
|
14
|
+
* VK OAuth Scope
|
|
15
|
+
* @default []
|
|
16
|
+
* @see https://id.vk.com/about/business/go/docs/en/vkid/latest/vk-id/connection/api-integration/api-description#App-access-to-user-data
|
|
17
|
+
* @example ["email", "phone"]
|
|
18
|
+
*/
|
|
19
|
+
scope?: string[];
|
|
20
|
+
/**
|
|
21
|
+
* Require email from user, adds the ['login:email'] scope if not present
|
|
22
|
+
* @default false
|
|
23
|
+
*/
|
|
24
|
+
emailRequired?: boolean;
|
|
25
|
+
/**
|
|
26
|
+
* VK OAuth Authorization URL
|
|
27
|
+
* @default 'https://id.vk.com/authorize'
|
|
28
|
+
*/
|
|
29
|
+
authorizationURL?: string;
|
|
30
|
+
/**
|
|
31
|
+
* VK OAuth Token URL
|
|
32
|
+
* @default 'https://id.vk.com/oauth2/auth'
|
|
33
|
+
*/
|
|
34
|
+
tokenURL?: string;
|
|
35
|
+
/**
|
|
36
|
+
* VK OAuth User URL
|
|
37
|
+
* @default 'https://id.vk.com/oauth2/user_info'
|
|
38
|
+
*/
|
|
39
|
+
userURL?: string;
|
|
40
|
+
/**
|
|
41
|
+
* Redirect URL to to allow overriding for situations like prod failing to determine public hostname
|
|
42
|
+
* @default process.env.NUXT_OAUTH_VK_REDIRECT_URL or current URL
|
|
43
|
+
*/
|
|
44
|
+
redirectURL?: string;
|
|
45
|
+
}
|
|
46
|
+
export declare function oauthVKEventHandler({ config, onSuccess, onError, }: OAuthConfig<OAuthVKConfig>): import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<void>>;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import { eventHandler, getQuery, sendRedirect } from "h3";
|
|
3
|
+
import { withQuery } from "ufo";
|
|
4
|
+
import { defu } from "defu";
|
|
5
|
+
import {
|
|
6
|
+
handleMissingConfiguration,
|
|
7
|
+
handleAccessTokenErrorResponse,
|
|
8
|
+
getOAuthRedirectURL,
|
|
9
|
+
requestAccessToken
|
|
10
|
+
} from "../utils.js";
|
|
11
|
+
import { useRuntimeConfig, createError } from "#imports";
|
|
12
|
+
export function oauthVKEventHandler({
|
|
13
|
+
config,
|
|
14
|
+
onSuccess,
|
|
15
|
+
onError
|
|
16
|
+
}) {
|
|
17
|
+
return eventHandler(async (event) => {
|
|
18
|
+
config = defu(config, useRuntimeConfig(event).oauth?.vk, {
|
|
19
|
+
authorizationURL: "https://id.vk.com/authorize",
|
|
20
|
+
tokenURL: "https://id.vk.com/oauth2/auth",
|
|
21
|
+
userURL: "https://id.vk.com/oauth2/user_info"
|
|
22
|
+
});
|
|
23
|
+
const query = getQuery(event);
|
|
24
|
+
if (!config.clientId || !config.clientSecret) {
|
|
25
|
+
return handleMissingConfiguration(
|
|
26
|
+
event,
|
|
27
|
+
"vk",
|
|
28
|
+
["clientId", "clientSecret"],
|
|
29
|
+
onError
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
const codeVerifier = "verify";
|
|
33
|
+
const redirectURL = config.redirectURL || getOAuthRedirectURL(event);
|
|
34
|
+
if (!query.code) {
|
|
35
|
+
config.scope = config.scope || [];
|
|
36
|
+
if (config.emailRequired && !config.scope.includes("email")) {
|
|
37
|
+
config.scope.push("email");
|
|
38
|
+
}
|
|
39
|
+
return sendRedirect(
|
|
40
|
+
event,
|
|
41
|
+
withQuery(config.authorizationURL, {
|
|
42
|
+
response_type: "code",
|
|
43
|
+
client_id: config.clientId,
|
|
44
|
+
code_challenge: crypto.createHash("sha256").update(codeVerifier).digest("base64url"),
|
|
45
|
+
code_challenge_method: "s256",
|
|
46
|
+
state: crypto.randomUUID(),
|
|
47
|
+
redirect_uri: redirectURL,
|
|
48
|
+
scope: config.scope.join(" ")
|
|
49
|
+
})
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
const tokens = await requestAccessToken(config.tokenURL, {
|
|
53
|
+
body: {
|
|
54
|
+
grant_type: "authorization_code",
|
|
55
|
+
code: query.code,
|
|
56
|
+
code_verifier: codeVerifier,
|
|
57
|
+
client_id: config.clientId,
|
|
58
|
+
device_id: query.device_id,
|
|
59
|
+
redirect_uri: redirectURL
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
if (tokens.error) {
|
|
63
|
+
return handleAccessTokenErrorResponse(event, "vk", tokens, onError);
|
|
64
|
+
}
|
|
65
|
+
const accessToken = tokens.access_token;
|
|
66
|
+
const user = await $fetch(config.userURL, {
|
|
67
|
+
method: "POST",
|
|
68
|
+
body: {
|
|
69
|
+
access_token: accessToken,
|
|
70
|
+
client_id: config.clientId
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
if (!user) {
|
|
74
|
+
const error = createError({
|
|
75
|
+
statusCode: 500,
|
|
76
|
+
message: "Could not get VK user",
|
|
77
|
+
data: tokens
|
|
78
|
+
});
|
|
79
|
+
if (!onError) throw error;
|
|
80
|
+
return onError(event, error);
|
|
81
|
+
}
|
|
82
|
+
return onSuccess(event, {
|
|
83
|
+
tokens,
|
|
84
|
+
user
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
}
|
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
import {
|
|
2
|
-
eventHandler,
|
|
3
|
-
getQuery,
|
|
4
|
-
sendRedirect
|
|
5
|
-
} from "h3";
|
|
1
|
+
import { eventHandler, getQuery, sendRedirect } from "h3";
|
|
6
2
|
import { withQuery } from "ufo";
|
|
7
3
|
import { defu } from "defu";
|
|
8
4
|
import { randomUUID } from "uncrypto";
|
|
9
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
handleMissingConfiguration,
|
|
7
|
+
handleAccessTokenErrorResponse,
|
|
8
|
+
getOAuthRedirectURL,
|
|
9
|
+
requestAccessToken
|
|
10
|
+
} from "../utils.js";
|
|
10
11
|
import { useRuntimeConfig } from "#imports";
|
|
11
12
|
export function oauthXEventHandler({
|
|
12
13
|
config,
|
|
@@ -29,7 +30,11 @@ export function oauthXEventHandler({
|
|
|
29
30
|
}
|
|
30
31
|
const redirectURL = config.redirectURL || getOAuthRedirectURL(event);
|
|
31
32
|
if (!query.code) {
|
|
32
|
-
config.scope = config.scope || [
|
|
33
|
+
config.scope = config.scope || [
|
|
34
|
+
"tweet.read",
|
|
35
|
+
"users.read",
|
|
36
|
+
"offline.access"
|
|
37
|
+
];
|
|
33
38
|
return sendRedirect(
|
|
34
39
|
event,
|
|
35
40
|
withQuery(config.authorizationURL, {
|
|
@@ -44,7 +49,9 @@ export function oauthXEventHandler({
|
|
|
44
49
|
}
|
|
45
50
|
const tokens = await requestAccessToken(config.tokenURL, {
|
|
46
51
|
headers: {
|
|
47
|
-
Authorization: `Basic ${Buffer.from(
|
|
52
|
+
Authorization: `Basic ${Buffer.from(
|
|
53
|
+
`${config.clientId}:${config.clientSecret}`
|
|
54
|
+
).toString("base64")}`
|
|
48
55
|
},
|
|
49
56
|
params: {
|
|
50
57
|
grant_type: "authorization_code",
|
|
@@ -57,17 +64,14 @@ export function oauthXEventHandler({
|
|
|
57
64
|
return handleAccessTokenErrorResponse(event, "x", tokens, onError);
|
|
58
65
|
}
|
|
59
66
|
const accessToken = tokens.access_token;
|
|
60
|
-
const user = await $fetch(
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
query: {
|
|
67
|
-
"user.fields": "description,id,name,profile_image_url,username,verified,verified_type"
|
|
68
|
-
}
|
|
67
|
+
const user = await $fetch(config.userURL, {
|
|
68
|
+
headers: {
|
|
69
|
+
Authorization: `Bearer ${accessToken}`
|
|
70
|
+
},
|
|
71
|
+
query: {
|
|
72
|
+
"user.fields": "description,id,name,profile_image_url,username,verified,verified_type"
|
|
69
73
|
}
|
|
70
|
-
).catch((error) => {
|
|
74
|
+
}).catch((error) => {
|
|
71
75
|
return error;
|
|
72
76
|
});
|
|
73
77
|
return onSuccess(event, {
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import { eventHandler, getQuery, sendRedirect } from "h3";
|
|
2
2
|
import { withQuery } from "ufo";
|
|
3
3
|
import { defu } from "defu";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
handleMissingConfiguration,
|
|
6
|
+
handleAccessTokenErrorResponse,
|
|
7
|
+
getOAuthRedirectURL,
|
|
8
|
+
requestAccessToken
|
|
9
|
+
} from "../utils.js";
|
|
5
10
|
import { useRuntimeConfig } from "#imports";
|
|
6
11
|
export function oauthYandexEventHandler({
|
|
7
12
|
config,
|
|
@@ -16,7 +21,12 @@ export function oauthYandexEventHandler({
|
|
|
16
21
|
});
|
|
17
22
|
const query = getQuery(event);
|
|
18
23
|
if (!config.clientId || !config.clientSecret) {
|
|
19
|
-
return handleMissingConfiguration(
|
|
24
|
+
return handleMissingConfiguration(
|
|
25
|
+
event,
|
|
26
|
+
"yandex",
|
|
27
|
+
["clientId", "clientSecret"],
|
|
28
|
+
onError
|
|
29
|
+
);
|
|
20
30
|
}
|
|
21
31
|
const redirectURL = config.redirectURL || getOAuthRedirectURL(event);
|
|
22
32
|
if (!query.code) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { defineNitroPlugin } from "nitropack/runtime";
|
|
2
2
|
export default defineNitroPlugin((nitroApp) => {
|
|
3
|
-
if (process.env.NUXT_OAUTH_FACEBOOK_CLIENT_ID && process.env.NUXT_OAUTH_FACEBOOK_CLIENT_SECRET) {
|
|
3
|
+
if (process.env.NUXT_OAUTH_FACEBOOK_CLIENT_ID && process.env.NUXT_OAUTH_FACEBOOK_CLIENT_SECRET || process.env.NUXT_OAUTH_INSTAGRAM_CLIENT_ID && process.env.NUXT_OAUTH_INSTAGRAM_CLIENT_SECRET) {
|
|
4
4
|
nitroApp.hooks.hook("render:html", (html) => {
|
|
5
5
|
html.head.unshift(`
|
|
6
6
|
<script>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { H3Event } from 'h3';
|
|
1
|
+
import type { H3Event, SessionConfig } from 'h3';
|
|
2
2
|
import type { UserSession, UserSessionRequired } from '#auth-utils';
|
|
3
3
|
export interface SessionHooks {
|
|
4
4
|
/**
|
|
@@ -25,13 +25,13 @@ export declare function getUserSession(event: H3Event): Promise<UserSession>;
|
|
|
25
25
|
* @param data User session data, please only store public information since it can be decoded with API calls
|
|
26
26
|
* @see https://github.com/atinux/nuxt-auth-utils
|
|
27
27
|
*/
|
|
28
|
-
export declare function setUserSession(event: H3Event, data: UserSession): Promise<UserSession>;
|
|
28
|
+
export declare function setUserSession(event: H3Event, data: UserSession, config?: Partial<SessionConfig>): Promise<UserSession>;
|
|
29
29
|
/**
|
|
30
30
|
* Replace a user session
|
|
31
31
|
* @param event The Request (h3) event
|
|
32
32
|
* @param data User session data, please only store public information since it can be decoded with API calls
|
|
33
33
|
*/
|
|
34
|
-
export declare function replaceUserSession(event: H3Event, data: UserSession): Promise<UserSession>;
|
|
34
|
+
export declare function replaceUserSession(event: H3Event, data: UserSession, config?: Partial<SessionConfig>): Promise<UserSession>;
|
|
35
35
|
/**
|
|
36
36
|
* Clear the user session and removing the session cookie
|
|
37
37
|
* @param event The Request (h3) event
|
|
@@ -6,13 +6,13 @@ export const sessionHooks = createHooks();
|
|
|
6
6
|
export async function getUserSession(event) {
|
|
7
7
|
return (await _useSession(event)).data;
|
|
8
8
|
}
|
|
9
|
-
export async function setUserSession(event, data) {
|
|
10
|
-
const session = await _useSession(event);
|
|
9
|
+
export async function setUserSession(event, data, config) {
|
|
10
|
+
const session = await _useSession(event, config);
|
|
11
11
|
await session.update(defu(data, session.data));
|
|
12
12
|
return session.data;
|
|
13
13
|
}
|
|
14
|
-
export async function replaceUserSession(event, data) {
|
|
15
|
-
const session = await _useSession(event);
|
|
14
|
+
export async function replaceUserSession(event, data, config) {
|
|
15
|
+
const session = await _useSession(event, config);
|
|
16
16
|
await session.clear();
|
|
17
17
|
await session.update(data);
|
|
18
18
|
return session.data;
|
|
@@ -34,11 +34,12 @@ export async function requireUserSession(event, opts = {}) {
|
|
|
34
34
|
return userSession;
|
|
35
35
|
}
|
|
36
36
|
let sessionConfig;
|
|
37
|
-
function _useSession(event) {
|
|
37
|
+
function _useSession(event, config = {}) {
|
|
38
38
|
if (!sessionConfig) {
|
|
39
39
|
const runtimeConfig = useRuntimeConfig(event);
|
|
40
40
|
const envSessionPassword = `${runtimeConfig.nitro?.envPrefix || "NUXT_"}SESSION_PASSWORD`;
|
|
41
41
|
sessionConfig = defu({ password: process.env[envSessionPassword] }, runtimeConfig.session);
|
|
42
42
|
}
|
|
43
|
-
|
|
43
|
+
const finalConfig = defu(config, sessionConfig);
|
|
44
|
+
return useSession(event, finalConfig);
|
|
44
45
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { H3Event, H3Error } from 'h3';
|
|
2
|
-
export type OAuthProvider = 'auth0' | 'battledotnet' | 'cognito' | 'discord' | 'facebook' | 'github' | 'google' | 'keycloak' | 'linkedin' | 'microsoft' | 'paypal' | 'spotify' | 'steam' | 'tiktok' | 'twitch' | 'x' | 'xsuaa' | 'yandex' | (string & {});
|
|
2
|
+
export type OAuthProvider = 'auth0' | 'battledotnet' | 'cognito' | 'discord' | 'facebook' | 'github' | 'gitlab' | 'google' | 'instagram' | 'keycloak' | 'linkedin' | 'microsoft' | 'paypal' | 'spotify' | 'steam' | 'tiktok' | 'twitch' | 'vk' | 'x' | 'xsuaa' | 'yandex' | (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;
|
|
@@ -2,7 +2,18 @@ import type { ComputedRef, Ref } from 'vue';
|
|
|
2
2
|
export interface User {
|
|
3
3
|
}
|
|
4
4
|
export interface UserSession {
|
|
5
|
+
/**
|
|
6
|
+
* User session data, available on client and server
|
|
7
|
+
*/
|
|
5
8
|
user?: User;
|
|
9
|
+
/**
|
|
10
|
+
* Private session data, only available on server/ code
|
|
11
|
+
*/
|
|
12
|
+
secure?: Record<string, unknown>;
|
|
13
|
+
/**
|
|
14
|
+
* Extra session data, available on client and server
|
|
15
|
+
*/
|
|
16
|
+
[key: string]: unknown;
|
|
6
17
|
}
|
|
7
18
|
export interface UserSessionRequired extends UserSession {
|
|
8
19
|
user: User;
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nuxt-auth-utils",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.8",
|
|
4
4
|
"description": "Minimalist Auth module for Nuxt with SSR",
|
|
5
5
|
"repository": "Atinux/nuxt-auth-utils",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"type": "module",
|
|
8
|
-
"packageManager": "pnpm@9.
|
|
8
|
+
"packageManager": "pnpm@9.10.0",
|
|
9
9
|
"exports": {
|
|
10
10
|
".": {
|
|
11
11
|
"types": "./dist/types.d.ts",
|
|
@@ -42,18 +42,18 @@
|
|
|
42
42
|
"uncrypto": "^0.1.3"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
|
-
"@iconify-json/simple-icons": "^1.2.
|
|
45
|
+
"@iconify-json/simple-icons": "^1.2.2",
|
|
46
46
|
"@nuxt/devtools": "latest",
|
|
47
|
-
"@nuxt/eslint-config": "^0.5.
|
|
48
|
-
"@nuxt/module-builder": "^0.8.
|
|
47
|
+
"@nuxt/eslint-config": "^0.5.7",
|
|
48
|
+
"@nuxt/module-builder": "^0.8.4",
|
|
49
49
|
"@nuxt/schema": "^3.13.1",
|
|
50
|
-
"@nuxt/test-utils": "^3.14.
|
|
50
|
+
"@nuxt/test-utils": "^3.14.2",
|
|
51
51
|
"@nuxt/ui": "^2.18.4",
|
|
52
|
-
"@nuxt/ui-pro": "^1.4.
|
|
52
|
+
"@nuxt/ui-pro": "^1.4.2",
|
|
53
53
|
"changelogen": "^0.5.5",
|
|
54
54
|
"eslint": "^9.10.0",
|
|
55
55
|
"nuxt": "^3.13.1",
|
|
56
|
-
"typescript": "^5.
|
|
56
|
+
"typescript": "^5.6.2",
|
|
57
57
|
"vitest": "^2.0.5",
|
|
58
58
|
"vue-tsc": "^2.1.6"
|
|
59
59
|
}
|