nuxt-auth-utils 0.3.7 → 0.3.9
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 +32 -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/index.d.ts +1 -1
- package/dist/runtime/types/oauth-config.d.ts +1 -1
- package/dist/runtime/types/session.d.ts +13 -0
- package/package.json +15 -15
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
|
|
@@ -148,11 +156,18 @@ declare module '#auth-utils' {
|
|
|
148
156
|
interface UserSession {
|
|
149
157
|
// Add your own fields
|
|
150
158
|
}
|
|
159
|
+
|
|
160
|
+
interface SecureSessionData {
|
|
161
|
+
// Add your own fields
|
|
162
|
+
}
|
|
151
163
|
}
|
|
152
164
|
|
|
153
165
|
export {}
|
|
154
166
|
```
|
|
155
167
|
|
|
168
|
+
> [!IMPORTANT]
|
|
169
|
+
> Since we encrypt and store session data in cookies, we're constrained by the 4096-byte cookie size limit. Store only essential information.
|
|
170
|
+
|
|
156
171
|
### OAuth Event Handlers
|
|
157
172
|
|
|
158
173
|
All handlers can be auto-imported and used in your server routes or API routes.
|
|
@@ -192,7 +207,9 @@ It can also be set using environment variables:
|
|
|
192
207
|
- Discord
|
|
193
208
|
- Facebook
|
|
194
209
|
- GitHub
|
|
210
|
+
- GitLab
|
|
195
211
|
- Google
|
|
212
|
+
- Instagram
|
|
196
213
|
- Keycloak
|
|
197
214
|
- LinkedIn
|
|
198
215
|
- Microsoft
|
|
@@ -201,6 +218,7 @@ It can also be set using environment variables:
|
|
|
201
218
|
- Steam
|
|
202
219
|
- TikTok
|
|
203
220
|
- Twitch
|
|
221
|
+
- VK
|
|
204
222
|
- X (Twitter)
|
|
205
223
|
- XSUAA
|
|
206
224
|
- Yandex
|
|
@@ -350,6 +368,14 @@ Our defaults are:
|
|
|
350
368
|
}
|
|
351
369
|
```
|
|
352
370
|
|
|
371
|
+
You can also overwrite the session config by passing it as 3rd argument of the `setUserSession` and `replaceUserSession` functions:
|
|
372
|
+
|
|
373
|
+
```ts
|
|
374
|
+
await setUserSession(event, { ... } , {
|
|
375
|
+
maxAge: 60 * 60 * 24 * 7 // 1 week
|
|
376
|
+
})
|
|
377
|
+
```
|
|
378
|
+
|
|
353
379
|
Checkout the [`SessionConfig`](https://github.com/unjs/h3/blob/c04c458810e34eb15c1647e1369e7d7ef19f567d/src/utils/session.ts#L20) for all options.
|
|
354
380
|
|
|
355
381
|
## 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,2 +1,2 @@
|
|
|
1
|
-
export type { User, UserSession, UserSessionRequired, UserSessionComposable } from './session.js';
|
|
1
|
+
export type { User, UserSession, UserSessionRequired, UserSessionComposable, SecureSessionData } from './session.js';
|
|
2
2
|
export type { OAuthConfig, OAuthProvider, OnError } from './oauth-config.js';
|
|
@@ -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;
|
|
@@ -1,8 +1,21 @@
|
|
|
1
1
|
import type { ComputedRef, Ref } from 'vue';
|
|
2
2
|
export interface User {
|
|
3
3
|
}
|
|
4
|
+
export interface SecureSessionData {
|
|
5
|
+
}
|
|
4
6
|
export interface UserSession {
|
|
7
|
+
/**
|
|
8
|
+
* User session data, available on client and server
|
|
9
|
+
*/
|
|
5
10
|
user?: User;
|
|
11
|
+
/**
|
|
12
|
+
* Private session data, only available on server/ code
|
|
13
|
+
*/
|
|
14
|
+
secure?: SecureSessionData;
|
|
15
|
+
/**
|
|
16
|
+
* Extra session data, available on client and server
|
|
17
|
+
*/
|
|
18
|
+
[key: string]: unknown;
|
|
6
19
|
}
|
|
7
20
|
export interface UserSessionRequired extends UserSession {
|
|
8
21
|
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.9",
|
|
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",
|
|
@@ -32,29 +32,29 @@
|
|
|
32
32
|
"test:watch": "vitest watch"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@nuxt/kit": "^3.13.
|
|
35
|
+
"@nuxt/kit": "^3.13.2",
|
|
36
36
|
"defu": "^6.1.4",
|
|
37
37
|
"hookable": "^5.5.3",
|
|
38
38
|
"ofetch": "^1.3.4",
|
|
39
|
-
"ohash": "^1.1.
|
|
39
|
+
"ohash": "^1.1.4",
|
|
40
40
|
"pathe": "^1.1.2",
|
|
41
41
|
"scule": "^1.3.0",
|
|
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.3",
|
|
46
46
|
"@nuxt/devtools": "latest",
|
|
47
|
-
"@nuxt/eslint-config": "^0.5.
|
|
48
|
-
"@nuxt/module-builder": "^0.8.
|
|
49
|
-
"@nuxt/schema": "^3.13.
|
|
50
|
-
"@nuxt/test-utils": "^3.14.
|
|
51
|
-
"@nuxt/ui": "^2.18.
|
|
52
|
-
"@nuxt/ui-pro": "^1.4.
|
|
53
|
-
"changelogen": "^0.5.
|
|
47
|
+
"@nuxt/eslint-config": "^0.5.7",
|
|
48
|
+
"@nuxt/module-builder": "^0.8.4",
|
|
49
|
+
"@nuxt/schema": "^3.13.2",
|
|
50
|
+
"@nuxt/test-utils": "^3.14.2",
|
|
51
|
+
"@nuxt/ui": "^2.18.5",
|
|
52
|
+
"@nuxt/ui-pro": "^1.4.2",
|
|
53
|
+
"changelogen": "^0.5.7",
|
|
54
54
|
"eslint": "^9.10.0",
|
|
55
|
-
"nuxt": "^3.13.
|
|
56
|
-
"typescript": "^5.
|
|
57
|
-
"vitest": "^2.
|
|
55
|
+
"nuxt": "^3.13.2",
|
|
56
|
+
"typescript": "^5.6.2",
|
|
57
|
+
"vitest": "^2.1.1",
|
|
58
58
|
"vue-tsc": "^2.1.6"
|
|
59
59
|
}
|
|
60
60
|
}
|