nuxt-auth-utils 0.5.16 → 0.5.18
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 +7 -1
- package/dist/module.json +1 -1
- package/dist/module.mjs +38 -0
- package/dist/runtime/server/lib/oauth/apple.js +4 -4
- package/dist/runtime/server/lib/oauth/azureb2c.d.ts +54 -0
- package/dist/runtime/server/lib/oauth/azureb2c.js +79 -0
- package/dist/runtime/server/lib/oauth/facebook.js +2 -2
- package/dist/runtime/server/lib/oauth/github.d.ts +5 -0
- package/dist/runtime/server/lib/oauth/github.js +3 -2
- package/dist/runtime/server/lib/oauth/heroku.d.ts +41 -0
- package/dist/runtime/server/lib/oauth/heroku.js +75 -0
- package/dist/runtime/server/lib/oauth/kick.d.ts +38 -0
- package/dist/runtime/server/lib/oauth/kick.js +71 -0
- package/dist/runtime/server/lib/oauth/livechat.d.ts +74 -0
- package/dist/runtime/server/lib/oauth/livechat.js +74 -0
- package/dist/runtime/server/lib/oauth/salesforce.d.ts +46 -0
- package/dist/runtime/server/lib/oauth/salesforce.js +74 -0
- package/dist/runtime/server/lib/oauth/slack.d.ts +41 -0
- package/dist/runtime/server/lib/oauth/slack.js +74 -0
- package/dist/runtime/server/lib/oauth/steam.js +31 -6
- package/dist/runtime/server/lib/oauth/tiktok.js +2 -2
- package/dist/runtime/server/lib/utils.d.ts +12 -1
- package/dist/runtime/server/lib/utils.js +44 -0
- package/dist/runtime/types/oauth-config.d.ts +1 -1
- package/package.json +1 -2
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@ Add Authentication to Nuxt applications with secured & sealed cookies sessions.
|
|
|
16
16
|
## Features
|
|
17
17
|
|
|
18
18
|
- [Hybrid Rendering](#hybrid-rendering) support (SSR / CSR / SWR / Prerendering)
|
|
19
|
-
- [
|
|
19
|
+
- [40+ OAuth Providers](#supported-oauth-providers)
|
|
20
20
|
- [Password Hashing](#password-hashing)
|
|
21
21
|
- [WebAuthn (passkey)](#webauthn-passkey)
|
|
22
22
|
- [`useUserSession()` Vue composable](#vue-composable)
|
|
@@ -216,6 +216,7 @@ It can also be set using environment variables:
|
|
|
216
216
|
- Auth0
|
|
217
217
|
- Authentik
|
|
218
218
|
- AWS Cognito
|
|
219
|
+
- Azure B2C
|
|
219
220
|
- Battle.net
|
|
220
221
|
- Bluesky (AT Protocol)
|
|
221
222
|
- Discord
|
|
@@ -225,16 +226,21 @@ It can also be set using environment variables:
|
|
|
225
226
|
- GitLab
|
|
226
227
|
- Gitea
|
|
227
228
|
- Google
|
|
229
|
+
- Heroku
|
|
228
230
|
- Hubspot
|
|
229
231
|
- Instagram
|
|
232
|
+
- Kick
|
|
230
233
|
- Keycloak
|
|
231
234
|
- Line
|
|
232
235
|
- Linear
|
|
233
236
|
- LinkedIn
|
|
237
|
+
- LiveChat
|
|
234
238
|
- Microsoft
|
|
235
239
|
- PayPal
|
|
236
240
|
- Polar
|
|
241
|
+
- Salesforce
|
|
237
242
|
- Seznam
|
|
243
|
+
- Slack
|
|
238
244
|
- Spotify
|
|
239
245
|
- Steam
|
|
240
246
|
- Strava
|
package/dist/module.json
CHANGED
package/dist/module.mjs
CHANGED
|
@@ -155,6 +155,16 @@ const module = defineNuxtModule({
|
|
|
155
155
|
userURL: "",
|
|
156
156
|
redirectURL: ""
|
|
157
157
|
});
|
|
158
|
+
runtimeConfig.oauth.azureb2c = defu(runtimeConfig.oauth.azureb2c, {
|
|
159
|
+
clientId: "",
|
|
160
|
+
policy: "",
|
|
161
|
+
tenant: "",
|
|
162
|
+
scope: [],
|
|
163
|
+
authorizationURL: "",
|
|
164
|
+
tokenURL: "",
|
|
165
|
+
userURL: "",
|
|
166
|
+
redirectURL: ""
|
|
167
|
+
});
|
|
158
168
|
runtimeConfig.oauth.discord = defu(runtimeConfig.oauth.discord, {
|
|
159
169
|
clientId: "",
|
|
160
170
|
clientSecret: "",
|
|
@@ -315,6 +325,34 @@ const module = defineNuxtModule({
|
|
|
315
325
|
redirectURL: "",
|
|
316
326
|
clientId: ""
|
|
317
327
|
});
|
|
328
|
+
runtimeConfig.oauth.kick = defu(runtimeConfig.oauth.kick, {
|
|
329
|
+
clientId: "",
|
|
330
|
+
clientSecret: "",
|
|
331
|
+
redirectURL: ""
|
|
332
|
+
});
|
|
333
|
+
runtimeConfig.oauth.livechat = defu(runtimeConfig.oauth.livechat, {
|
|
334
|
+
clientId: "",
|
|
335
|
+
clientSecret: ""
|
|
336
|
+
});
|
|
337
|
+
runtimeConfig.oauth.salesforce = defu(runtimeConfig.oauth.salesforce, {
|
|
338
|
+
clientId: "",
|
|
339
|
+
clientSecret: "",
|
|
340
|
+
redirectURL: "",
|
|
341
|
+
baseURL: "",
|
|
342
|
+
scope: ""
|
|
343
|
+
});
|
|
344
|
+
runtimeConfig.oauth.slack = defu(runtimeConfig.oauth.slack, {
|
|
345
|
+
clientId: "",
|
|
346
|
+
clientSecret: "",
|
|
347
|
+
redirectURL: "",
|
|
348
|
+
scope: ""
|
|
349
|
+
});
|
|
350
|
+
runtimeConfig.oauth.heroku = defu(runtimeConfig.oauth.heroku, {
|
|
351
|
+
clientId: "",
|
|
352
|
+
clientSecret: "",
|
|
353
|
+
redirectURL: "",
|
|
354
|
+
scope: ""
|
|
355
|
+
});
|
|
318
356
|
}
|
|
319
357
|
});
|
|
320
358
|
|
|
@@ -61,15 +61,15 @@ export function defineOAuthAppleEventHandler({
|
|
|
61
61
|
redirect_uri: config.redirectURL
|
|
62
62
|
}
|
|
63
63
|
});
|
|
64
|
-
const
|
|
64
|
+
const payload = await verifyJwt(accessTokenResult.id_token, {
|
|
65
65
|
publicJwkUrl: "https://appleid.apple.com/auth/keys",
|
|
66
66
|
audience: config.clientId,
|
|
67
67
|
issuer: "https://appleid.apple.com"
|
|
68
68
|
});
|
|
69
|
-
if (!
|
|
70
|
-
return handleAccessTokenErrorResponse(event, "apple",
|
|
69
|
+
if (!payload) {
|
|
70
|
+
return handleAccessTokenErrorResponse(event, "apple", payload, onError);
|
|
71
71
|
}
|
|
72
|
-
return onSuccess(event, { user, tokens });
|
|
72
|
+
return onSuccess(event, { user, payload, tokens: accessTokenResult });
|
|
73
73
|
} catch (error) {
|
|
74
74
|
return handleAccessTokenErrorResponse(event, "apple", error, onError);
|
|
75
75
|
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { OAuthConfig } from '#auth-utils';
|
|
2
|
+
export interface OAuthAzureB2CConfig {
|
|
3
|
+
/**
|
|
4
|
+
* Azure OAuth Client ID
|
|
5
|
+
* @default process.env.NUXT_OAUTH_AZUREB2C_CLIENT_ID
|
|
6
|
+
*/
|
|
7
|
+
clientId?: string;
|
|
8
|
+
/**
|
|
9
|
+
* Azure OAuth Policy
|
|
10
|
+
* @default process.env.NUXT_OAUTH_AZUREB2C_POLICY
|
|
11
|
+
*/
|
|
12
|
+
policy?: string;
|
|
13
|
+
/**
|
|
14
|
+
* Azure OAuth Tenant ID
|
|
15
|
+
* @default process.env.NUXT_OAUTH_AZUREB2C_TENANT
|
|
16
|
+
*/
|
|
17
|
+
tenant?: string;
|
|
18
|
+
/**
|
|
19
|
+
* Azure OAuth Scope
|
|
20
|
+
* @default ['offline_access']
|
|
21
|
+
* @see https://learn.microsoft.com/en-us/azure/active-directory-b2c/access-tokens#scopes
|
|
22
|
+
*/
|
|
23
|
+
scope?: string[];
|
|
24
|
+
/**
|
|
25
|
+
* Azure OAuth Authorization URL
|
|
26
|
+
* @default 'https://${tenant}.onmicrosoft.com/${policy}/oauth2/v2.0/token'
|
|
27
|
+
* @see https://learn.microsoft.com/en-us/azure/active-directory-b2c/openid-connect
|
|
28
|
+
*/
|
|
29
|
+
authorizationURL?: string;
|
|
30
|
+
/**
|
|
31
|
+
* Azure OAuth Token URL
|
|
32
|
+
* @default 'https://${tenant}.onmicrosoft.com/${policy}/oauth2/v2.0/token'
|
|
33
|
+
* @see https://learn.microsoft.com/en-us/azure/active-directory-b2c/openid-connect
|
|
34
|
+
*/
|
|
35
|
+
tokenURL?: string;
|
|
36
|
+
/**
|
|
37
|
+
* Azure OAuth User URL
|
|
38
|
+
* @default 'https://graph.microsoft.com/v1.0/me'
|
|
39
|
+
* @see https://docs.microsoft.com/en-us/graph/api/user-get?view=graph-rest-1.0&tabs=http
|
|
40
|
+
*/
|
|
41
|
+
userURL?: string;
|
|
42
|
+
/**
|
|
43
|
+
* Extra authorization parameters to provide to the authorization URL
|
|
44
|
+
* @see https://learn.microsoft.com/en-us/azure/active-directory-b2c/authorization-code-flow
|
|
45
|
+
*/
|
|
46
|
+
authorizationParams?: Record<string, string>;
|
|
47
|
+
/**
|
|
48
|
+
* Redirect URL to prevent in prod prevent redirect_uri mismatch http to https
|
|
49
|
+
* @default process.env.NUXT_OAUTH_AZUREB2C_REDIRECT_URL
|
|
50
|
+
* @see https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow
|
|
51
|
+
*/
|
|
52
|
+
redirectURL?: string;
|
|
53
|
+
}
|
|
54
|
+
export declare function defineOAuthAzureB2CEventHandler({ config, onSuccess, onError }: OAuthConfig<OAuthAzureB2CConfig>): import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<void>>;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { eventHandler, getQuery, sendRedirect } from "h3";
|
|
2
|
+
import { withQuery } from "ufo";
|
|
3
|
+
import { defu } from "defu";
|
|
4
|
+
import { handleMissingConfiguration, handleAccessTokenErrorResponse, getOAuthRedirectURL, requestAccessToken, handlePkceVerifier, handleState, handleInvalidState } from "../utils.js";
|
|
5
|
+
import { useRuntimeConfig, createError } from "#imports";
|
|
6
|
+
export function defineOAuthAzureB2CEventHandler({ config, onSuccess, onError }) {
|
|
7
|
+
return eventHandler(async (event) => {
|
|
8
|
+
config = defu(config, useRuntimeConfig(event).oauth?.azureb2c, {
|
|
9
|
+
authorizationParams: {}
|
|
10
|
+
});
|
|
11
|
+
const query = getQuery(event);
|
|
12
|
+
if (!config.clientId || !config.policy || !config.tenant) {
|
|
13
|
+
return handleMissingConfiguration(event, "azureb2c", ["clientId", "policy", "tenant"], onError);
|
|
14
|
+
}
|
|
15
|
+
const authorizationURL = config.authorizationURL || `https://${config.tenant}.b2clogin.com/${config.tenant}.onmicrosoft.com/${config.policy}/oauth2/v2.0/authorize`;
|
|
16
|
+
const tokenURL = config.tokenURL || `https://${config.tenant}.b2clogin.com/${config.tenant}.onmicrosoft.com/${config.policy}/oauth2/v2.0/token`;
|
|
17
|
+
const redirectURL = config.redirectURL || getOAuthRedirectURL(event);
|
|
18
|
+
config.scope = config.scope && config.scope.length > 0 ? config.scope : ["openid"];
|
|
19
|
+
config.scope = [...new Set(config.scope)];
|
|
20
|
+
const verifier = await handlePkceVerifier(event);
|
|
21
|
+
const state = await handleState(event);
|
|
22
|
+
if (!query.code) {
|
|
23
|
+
return sendRedirect(
|
|
24
|
+
event,
|
|
25
|
+
withQuery(authorizationURL, {
|
|
26
|
+
client_id: config.clientId,
|
|
27
|
+
response_type: "code",
|
|
28
|
+
redirect_uri: redirectURL,
|
|
29
|
+
scope: config.scope.join(" "),
|
|
30
|
+
state,
|
|
31
|
+
code_challenge: verifier.code_challenge,
|
|
32
|
+
code_challenge_method: verifier.code_challenge_method,
|
|
33
|
+
...config.authorizationParams
|
|
34
|
+
})
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
if (query.state !== state) {
|
|
38
|
+
handleInvalidState(event, "azureb2c", onError);
|
|
39
|
+
}
|
|
40
|
+
console.info("code verifier", verifier.code_verifier);
|
|
41
|
+
const tokens = await requestAccessToken(tokenURL, {
|
|
42
|
+
body: {
|
|
43
|
+
grant_type: "authorization_code",
|
|
44
|
+
client_id: config.clientId,
|
|
45
|
+
scope: config.scope.join(" "),
|
|
46
|
+
code: query.code,
|
|
47
|
+
redirect_uri: redirectURL,
|
|
48
|
+
response_type: "code",
|
|
49
|
+
code_verifier: verifier.code_verifier
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
if (tokens.error) {
|
|
53
|
+
return handleAccessTokenErrorResponse(event, "azureb2c", tokens, onError);
|
|
54
|
+
}
|
|
55
|
+
const tokenType = tokens.token_type;
|
|
56
|
+
const accessToken = tokens.access_token;
|
|
57
|
+
const userURL = config.userURL || "https://graph.microsoft.com/v1.0/me";
|
|
58
|
+
const user = await $fetch(userURL, {
|
|
59
|
+
headers: {
|
|
60
|
+
Authorization: `${tokenType} ${accessToken}`
|
|
61
|
+
}
|
|
62
|
+
}).catch((error) => {
|
|
63
|
+
return { error };
|
|
64
|
+
});
|
|
65
|
+
if (user.error) {
|
|
66
|
+
const error = createError({
|
|
67
|
+
statusCode: 401,
|
|
68
|
+
message: `azureb2c login failed: ${user.error || "Unknown error"}`,
|
|
69
|
+
data: user
|
|
70
|
+
});
|
|
71
|
+
if (!onError) throw error;
|
|
72
|
+
return onError(event, error);
|
|
73
|
+
}
|
|
74
|
+
return onSuccess(event, {
|
|
75
|
+
tokens,
|
|
76
|
+
user
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
}
|
|
@@ -29,7 +29,7 @@ export function defineOAuthFacebookEventHandler({
|
|
|
29
29
|
}
|
|
30
30
|
const redirectURL = config.redirectURL || getOAuthRedirectURL(event);
|
|
31
31
|
if (!query.code) {
|
|
32
|
-
config.scope = config.scope
|
|
32
|
+
config.scope = [...new Set(config.scope)];
|
|
33
33
|
return sendRedirect(
|
|
34
34
|
event,
|
|
35
35
|
withQuery(config.authorizationURL, {
|
|
@@ -52,7 +52,7 @@ export function defineOAuthFacebookEventHandler({
|
|
|
52
52
|
return handleAccessTokenErrorResponse(event, "facebook", tokens, onError);
|
|
53
53
|
}
|
|
54
54
|
const accessToken = tokens.access_token;
|
|
55
|
-
config.fields = config.fields || ["id", "name"];
|
|
55
|
+
config.fields = [...new Set(config.fields || ["id", "name"])];
|
|
56
56
|
const fields = config.fields.join(",");
|
|
57
57
|
const user = await $fetch(
|
|
58
58
|
`https://graph.facebook.com/v19.0/me?fields=${fields}&access_token=${accessToken}`
|
|
@@ -32,6 +32,11 @@ export interface OAuthGitHubConfig {
|
|
|
32
32
|
* @default 'https://github.com/login/oauth/access_token'
|
|
33
33
|
*/
|
|
34
34
|
tokenURL?: string;
|
|
35
|
+
/**
|
|
36
|
+
* GitHub API URL
|
|
37
|
+
* @default 'https://api.github.com'
|
|
38
|
+
*/
|
|
39
|
+
apiURL?: string;
|
|
35
40
|
/**
|
|
36
41
|
* Extra authorization parameters to provide to the authorization URL
|
|
37
42
|
* @see https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/authorizing-oauth-apps#1-request-a-users-github-identity
|
|
@@ -8,6 +8,7 @@ export function defineOAuthGitHubEventHandler({ config, onSuccess, onError }) {
|
|
|
8
8
|
config = defu(config, useRuntimeConfig(event).oauth?.github, {
|
|
9
9
|
authorizationURL: "https://github.com/login/oauth/authorize",
|
|
10
10
|
tokenURL: "https://github.com/login/oauth/access_token",
|
|
11
|
+
apiURL: "https://api.github.com",
|
|
11
12
|
authorizationParams: {}
|
|
12
13
|
});
|
|
13
14
|
const query = getQuery(event);
|
|
@@ -52,14 +53,14 @@ export function defineOAuthGitHubEventHandler({ config, onSuccess, onError }) {
|
|
|
52
53
|
return handleAccessTokenErrorResponse(event, "github", tokens, onError);
|
|
53
54
|
}
|
|
54
55
|
const accessToken = tokens.access_token;
|
|
55
|
-
const user = await $fetch(
|
|
56
|
+
const user = await $fetch(`${config.apiURL}/user`, {
|
|
56
57
|
headers: {
|
|
57
58
|
"User-Agent": `Github-OAuth-${config.clientId}`,
|
|
58
59
|
"Authorization": `token ${accessToken}`
|
|
59
60
|
}
|
|
60
61
|
});
|
|
61
62
|
if (!user.email && config.emailRequired) {
|
|
62
|
-
const emails = await $fetch(
|
|
63
|
+
const emails = await $fetch(`${config.apiURL}/user/emails`, {
|
|
63
64
|
headers: {
|
|
64
65
|
"User-Agent": `Github-OAuth-${config.clientId}`,
|
|
65
66
|
"Authorization": `token ${accessToken}`
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { OAuthConfig } from '#auth-utils';
|
|
2
|
+
export interface OAuthHerokuConfig {
|
|
3
|
+
/**
|
|
4
|
+
* Heroku OAuth Client ID
|
|
5
|
+
* @default process.env.NUXT_OAUTH_HEROKU_CLIENT_ID
|
|
6
|
+
*/
|
|
7
|
+
clientId?: string;
|
|
8
|
+
/**
|
|
9
|
+
* Heroku OAuth Client Secret
|
|
10
|
+
* @default process.env.NUXT_OAUTH_HEROKU_CLIENT_SECRET
|
|
11
|
+
*/
|
|
12
|
+
clientSecret?: string;
|
|
13
|
+
/**
|
|
14
|
+
* Heroku OAuth Scope
|
|
15
|
+
* @default ['identity']
|
|
16
|
+
* @see https://devcenter.heroku.com/articles/oauth#scopes
|
|
17
|
+
* @example ['identity']
|
|
18
|
+
*/
|
|
19
|
+
scope?: string[];
|
|
20
|
+
/**
|
|
21
|
+
* Heroku OAuth Authorization URL
|
|
22
|
+
* @default 'https://id.heroku.com/oauth/authorize'
|
|
23
|
+
*/
|
|
24
|
+
authorizationURL?: string;
|
|
25
|
+
/**
|
|
26
|
+
* Heroku OAuth Authorization URL
|
|
27
|
+
* @default 'https://id.heroku.com/oauth/token'
|
|
28
|
+
*/
|
|
29
|
+
tokenURL?: string;
|
|
30
|
+
/**
|
|
31
|
+
* Extra authorization parameters to provide to the authorization URL
|
|
32
|
+
* @default {}
|
|
33
|
+
*/
|
|
34
|
+
authorizationParams?: Record<string, string>;
|
|
35
|
+
/**
|
|
36
|
+
* Redirect URL to allow overriding for situations like prod failing to determine public hostname
|
|
37
|
+
* @default process.env.NUXT_OAUTH_HEROKU_REDIRECT_URL or current URL
|
|
38
|
+
*/
|
|
39
|
+
redirectURL?: string;
|
|
40
|
+
}
|
|
41
|
+
export declare function defineOAuthHerokuEventHandler({ config, onSuccess, onError, }: OAuthConfig<OAuthHerokuConfig>): import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<void>>;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { eventHandler, getQuery, sendRedirect } from "h3";
|
|
2
|
+
import { withQuery } from "ufo";
|
|
3
|
+
import { defu } from "defu";
|
|
4
|
+
import { handleMissingConfiguration, handleAccessTokenErrorResponse, getOAuthRedirectURL, requestAccessToken, handleState, handleInvalidState } from "../utils.js";
|
|
5
|
+
import { useRuntimeConfig, createError } from "#imports";
|
|
6
|
+
export function defineOAuthHerokuEventHandler({
|
|
7
|
+
config,
|
|
8
|
+
onSuccess,
|
|
9
|
+
onError
|
|
10
|
+
}) {
|
|
11
|
+
return eventHandler(async (event) => {
|
|
12
|
+
const runtimeConfig = useRuntimeConfig(event).oauth?.heroku;
|
|
13
|
+
const baseURL = "https://id.heroku.com";
|
|
14
|
+
config = defu(config, runtimeConfig, {
|
|
15
|
+
authorizationURL: `${baseURL}/oauth/authorize`,
|
|
16
|
+
tokenURL: `${baseURL}/oauth/token`,
|
|
17
|
+
authorizationParams: {}
|
|
18
|
+
});
|
|
19
|
+
const query = getQuery(event);
|
|
20
|
+
if (query.error) {
|
|
21
|
+
const error = createError({
|
|
22
|
+
statusCode: 401,
|
|
23
|
+
message: `Heroku login failed: ${query.error || "Unknown error"}`,
|
|
24
|
+
data: query
|
|
25
|
+
});
|
|
26
|
+
if (!onError) throw error;
|
|
27
|
+
return onError(event, error);
|
|
28
|
+
}
|
|
29
|
+
if (!config.clientId || !config.clientSecret) {
|
|
30
|
+
return handleMissingConfiguration(event, "heroku", ["clientId", "clientSecret"], onError);
|
|
31
|
+
}
|
|
32
|
+
const redirectURL = config.redirectURL || getOAuthRedirectURL(event);
|
|
33
|
+
const state = await handleState(event);
|
|
34
|
+
if (!query.code) {
|
|
35
|
+
config.scope = config.scope || ["identity"];
|
|
36
|
+
return sendRedirect(
|
|
37
|
+
event,
|
|
38
|
+
withQuery(config.authorizationURL, {
|
|
39
|
+
response_type: "code",
|
|
40
|
+
client_id: config.clientId,
|
|
41
|
+
redirect_uri: redirectURL,
|
|
42
|
+
scope: config.scope.join(" "),
|
|
43
|
+
state,
|
|
44
|
+
...config.authorizationParams
|
|
45
|
+
})
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
if (query.state !== state) {
|
|
49
|
+
handleInvalidState(event, "heroku", onError);
|
|
50
|
+
}
|
|
51
|
+
const tokens = await requestAccessToken(config.tokenURL, {
|
|
52
|
+
body: {
|
|
53
|
+
grant_type: "authorization_code",
|
|
54
|
+
client_id: config.clientId,
|
|
55
|
+
client_secret: config.clientSecret,
|
|
56
|
+
redirect_uri: redirectURL,
|
|
57
|
+
code: query.code
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
if (tokens.error) {
|
|
61
|
+
return handleAccessTokenErrorResponse(event, "heroku", tokens, onError);
|
|
62
|
+
}
|
|
63
|
+
const accessToken = tokens.access_token;
|
|
64
|
+
const user = await $fetch(`https://api.heroku.com/account`, {
|
|
65
|
+
headers: {
|
|
66
|
+
Accept: "application/vnd.heroku+json; version=3",
|
|
67
|
+
Authorization: `Bearer ${accessToken}`
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
return onSuccess(event, {
|
|
71
|
+
user,
|
|
72
|
+
tokens
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { OAuthConfig } from '#auth-utils';
|
|
2
|
+
export interface OAuthKickConfig {
|
|
3
|
+
/**
|
|
4
|
+
* Kick Client ID
|
|
5
|
+
* @default process.env.NUXT_OAUTH_KICK_CLIENT_ID
|
|
6
|
+
*/
|
|
7
|
+
clientId?: string;
|
|
8
|
+
/**
|
|
9
|
+
* Kick OAuth Client Secret
|
|
10
|
+
* @default process.env.NUXT_OAUTH_KICK_CLIENT_SECRET
|
|
11
|
+
*/
|
|
12
|
+
clientSecret?: string;
|
|
13
|
+
/**
|
|
14
|
+
* Kick OAuth Scope
|
|
15
|
+
* @default []
|
|
16
|
+
* @see https://docs.kick.com/getting-started/scopes
|
|
17
|
+
* @example ['channel:read']
|
|
18
|
+
*/
|
|
19
|
+
scope?: string[];
|
|
20
|
+
/**
|
|
21
|
+
* Kick OAuth Authorization URL
|
|
22
|
+
* @see https://docs.kick.com/getting-started/generating-tokens-oauth2-flow#authorization-endpoint
|
|
23
|
+
* @default 'https://id.kick.com/oauth/authorize'
|
|
24
|
+
*/
|
|
25
|
+
authorizationURL?: string;
|
|
26
|
+
/**
|
|
27
|
+
* Kick OAuth Token URL
|
|
28
|
+
* @see https://docs.kick.com/getting-started/generating-tokens-oauth2-flow#token-endpoint
|
|
29
|
+
* @default 'https://id.kick.com/oauth/token'
|
|
30
|
+
*/
|
|
31
|
+
tokenURL?: string;
|
|
32
|
+
/**
|
|
33
|
+
* Redirect URL to to allow overriding for situations like prod failing to determine public hostname
|
|
34
|
+
* @default process.env.NUXT_OAUTH_KICK_REDIRECT_URL or current URL
|
|
35
|
+
*/
|
|
36
|
+
redirectURL?: string;
|
|
37
|
+
}
|
|
38
|
+
export declare function defineOAuthKickEventHandler({ config, onSuccess, onError }: OAuthConfig<OAuthKickConfig>): import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<void>>;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { eventHandler, getQuery, sendRedirect } from "h3";
|
|
2
|
+
import { withQuery } from "ufo";
|
|
3
|
+
import { defu } from "defu";
|
|
4
|
+
import { randomUUID } from "uncrypto";
|
|
5
|
+
import { handleAccessTokenErrorResponse, handleMissingConfiguration, getOAuthRedirectURL, requestAccessToken, handlePkceVerifier } from "../utils.js";
|
|
6
|
+
import { useRuntimeConfig, createError } from "#imports";
|
|
7
|
+
export function defineOAuthKickEventHandler({ config, onSuccess, onError }) {
|
|
8
|
+
return eventHandler(async (event) => {
|
|
9
|
+
config = defu(config, useRuntimeConfig(event).oauth?.kick, {
|
|
10
|
+
authorizationURL: "https://id.kick.com/oauth/authorize",
|
|
11
|
+
tokenURL: "https://id.kick.com/oauth/token"
|
|
12
|
+
});
|
|
13
|
+
const query = getQuery(event);
|
|
14
|
+
if (!config.clientId || !config.clientSecret) {
|
|
15
|
+
return handleMissingConfiguration(event, "kick", ["clientId", "clientSecret"], onError);
|
|
16
|
+
}
|
|
17
|
+
const verifier = await handlePkceVerifier(event);
|
|
18
|
+
const redirectURL = config.redirectURL || getOAuthRedirectURL(event);
|
|
19
|
+
if (!query.code) {
|
|
20
|
+
config.scope = config.scope || [];
|
|
21
|
+
if (!config.scope.includes("user:read"))
|
|
22
|
+
config.scope.push("user:read");
|
|
23
|
+
return sendRedirect(
|
|
24
|
+
event,
|
|
25
|
+
withQuery(config.authorizationURL, {
|
|
26
|
+
response_type: "code",
|
|
27
|
+
client_id: config.clientId,
|
|
28
|
+
redirect_uri: redirectURL,
|
|
29
|
+
scope: config.scope.join(" "),
|
|
30
|
+
state: randomUUID(),
|
|
31
|
+
code_challenge: verifier.code_challenge,
|
|
32
|
+
code_challenge_method: verifier.code_challenge_method
|
|
33
|
+
})
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
const tokens = await requestAccessToken(config.tokenURL, {
|
|
37
|
+
body: {
|
|
38
|
+
grant_type: "authorization_code",
|
|
39
|
+
redirect_uri: redirectURL,
|
|
40
|
+
client_id: config.clientId,
|
|
41
|
+
client_secret: config.clientSecret,
|
|
42
|
+
code: query.code,
|
|
43
|
+
code_verifier: verifier.code_verifier
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
if (tokens.error) {
|
|
47
|
+
return handleAccessTokenErrorResponse(event, "kick", tokens, onError);
|
|
48
|
+
}
|
|
49
|
+
const accessToken = tokens.access_token;
|
|
50
|
+
const { data } = await $fetch("https://api.kick.com/public/v1/users", {
|
|
51
|
+
headers: {
|
|
52
|
+
Authorization: `Bearer ${accessToken}`,
|
|
53
|
+
Accept: "application/json"
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
if (!data || !data.length) {
|
|
57
|
+
const error = createError({
|
|
58
|
+
statusCode: 500,
|
|
59
|
+
message: "Could not get Kick user",
|
|
60
|
+
data: tokens
|
|
61
|
+
});
|
|
62
|
+
if (!onError) throw error;
|
|
63
|
+
return onError(event, error);
|
|
64
|
+
}
|
|
65
|
+
const user = data[0];
|
|
66
|
+
return onSuccess(event, {
|
|
67
|
+
tokens,
|
|
68
|
+
user
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { OAuthConfig } from '#auth-utils';
|
|
2
|
+
export interface LiveChatTokens {
|
|
3
|
+
access_token: string;
|
|
4
|
+
account_id: string;
|
|
5
|
+
expires_in: number;
|
|
6
|
+
organization_id: string;
|
|
7
|
+
refresh_token: string;
|
|
8
|
+
scope: string;
|
|
9
|
+
token_type: string;
|
|
10
|
+
}
|
|
11
|
+
export interface LiveChatUser {
|
|
12
|
+
account_id: string;
|
|
13
|
+
name: string;
|
|
14
|
+
email: string;
|
|
15
|
+
email_verified: boolean;
|
|
16
|
+
default_product: string | null;
|
|
17
|
+
default_organization_id: string | null;
|
|
18
|
+
avatar_url: string | null;
|
|
19
|
+
time_zone: string;
|
|
20
|
+
roles?: {
|
|
21
|
+
role_id: string;
|
|
22
|
+
product: string;
|
|
23
|
+
role: string;
|
|
24
|
+
type: string;
|
|
25
|
+
predefined: boolean;
|
|
26
|
+
}[];
|
|
27
|
+
updated_at: string;
|
|
28
|
+
created_at: string;
|
|
29
|
+
properties?: Record<string, unknown>;
|
|
30
|
+
}
|
|
31
|
+
export interface LiveChatConfig {
|
|
32
|
+
/**
|
|
33
|
+
* LiveChat OAuth Client ID
|
|
34
|
+
* @default process.env.NUXT_LIVECHAT_CLIENT_ID
|
|
35
|
+
*/
|
|
36
|
+
clientId?: string;
|
|
37
|
+
/**
|
|
38
|
+
* LiveChat OAuth Client Secret
|
|
39
|
+
* @default process.env.NUXT_LIVECHAT_CLIENT_SECRET
|
|
40
|
+
*/
|
|
41
|
+
clientSecret?: string;
|
|
42
|
+
/**
|
|
43
|
+
* Redirect URL to to allow overriding for situations like prod failing to determine public hostname
|
|
44
|
+
* @default process.env.NUXT_OAUTH_LIVECHAT_REDIRECT_URL or current URL
|
|
45
|
+
*/
|
|
46
|
+
redirectURL?: string;
|
|
47
|
+
/**
|
|
48
|
+
* LiveChat OAuth Authorization URL
|
|
49
|
+
* @default 'https://accounts.livechat.com
|
|
50
|
+
*/
|
|
51
|
+
authorizationURL?: string;
|
|
52
|
+
/**
|
|
53
|
+
* LiveChat OAuth Token URL
|
|
54
|
+
* @default 'https://accounts.livechat.com/v2/token
|
|
55
|
+
*/
|
|
56
|
+
tokenURL?: string;
|
|
57
|
+
/**
|
|
58
|
+
* LiveChat User URL
|
|
59
|
+
* @default 'https://accounts.livechat.com/v2/accounts/me
|
|
60
|
+
*/
|
|
61
|
+
userURL?: string;
|
|
62
|
+
/**
|
|
63
|
+
* LiveChat OAuth Scope. accounts--my:ro is always applied to get user profile.
|
|
64
|
+
* @default ['accounts--my:ro']
|
|
65
|
+
* @example ['accounts--my:ro', 'chats--my:ro']
|
|
66
|
+
*/
|
|
67
|
+
scope?: string[];
|
|
68
|
+
/**
|
|
69
|
+
* Extra authorization parameters to provide to the authorization URL
|
|
70
|
+
* @see https://platform.text.com/docs/authorization/authorization-in-practice
|
|
71
|
+
*/
|
|
72
|
+
authorizationParams?: Record<string, string>;
|
|
73
|
+
}
|
|
74
|
+
export declare function defineOAuthLiveChatEventHandler({ config, onSuccess, onError, }: OAuthConfig<LiveChatConfig>): import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<void>>;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { eventHandler, getQuery, sendRedirect } from "h3";
|
|
2
|
+
import { defu } from "defu";
|
|
3
|
+
import { withQuery } from "ufo";
|
|
4
|
+
import { randomUUID } from "uncrypto";
|
|
5
|
+
import {
|
|
6
|
+
getOAuthRedirectURL,
|
|
7
|
+
handleAccessTokenErrorResponse,
|
|
8
|
+
handleMissingConfiguration,
|
|
9
|
+
requestAccessToken
|
|
10
|
+
} from "../utils.js";
|
|
11
|
+
import { useRuntimeConfig } from "#imports";
|
|
12
|
+
export function defineOAuthLiveChatEventHandler({
|
|
13
|
+
config,
|
|
14
|
+
onSuccess,
|
|
15
|
+
onError
|
|
16
|
+
}) {
|
|
17
|
+
return eventHandler(async (event) => {
|
|
18
|
+
config = defu(config, useRuntimeConfig(event).oauth?.livechat, {
|
|
19
|
+
authorizationURL: "https://accounts.livechat.com",
|
|
20
|
+
tokenURL: "https://accounts.livechat.com/v2/token",
|
|
21
|
+
userURL: "https://accounts.livechat.com/v2/accounts/me",
|
|
22
|
+
scope: [],
|
|
23
|
+
authorizationParams: {
|
|
24
|
+
state: randomUUID()
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
if (!config.clientId || !config.clientSecret) {
|
|
28
|
+
return handleMissingConfiguration(
|
|
29
|
+
event,
|
|
30
|
+
"livechat",
|
|
31
|
+
["clientId", "clientSecret"],
|
|
32
|
+
onError
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
const query = getQuery(event);
|
|
36
|
+
const redirectURL = config.redirectURL || getOAuthRedirectURL(event);
|
|
37
|
+
const scope = [.../* @__PURE__ */ new Set([...config.scope, "accounts--my:ro"])].join(" ");
|
|
38
|
+
if (!query.code) {
|
|
39
|
+
return sendRedirect(
|
|
40
|
+
event,
|
|
41
|
+
withQuery(config.authorizationURL, {
|
|
42
|
+
client_id: config.clientId,
|
|
43
|
+
redirect_uri: redirectURL,
|
|
44
|
+
response_type: "code",
|
|
45
|
+
scope,
|
|
46
|
+
...config.authorizationParams
|
|
47
|
+
})
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
const tokens = await requestAccessToken(config.tokenURL, {
|
|
51
|
+
params: {
|
|
52
|
+
grant_type: "authorization_code",
|
|
53
|
+
code: query.code,
|
|
54
|
+
client_id: config.clientId,
|
|
55
|
+
client_secret: config.clientSecret,
|
|
56
|
+
redirect_uri: redirectURL
|
|
57
|
+
}
|
|
58
|
+
}).catch((error) => {
|
|
59
|
+
return { error };
|
|
60
|
+
});
|
|
61
|
+
if (tokens.error) {
|
|
62
|
+
return handleAccessTokenErrorResponse(event, "livechat", tokens, onError);
|
|
63
|
+
}
|
|
64
|
+
const user = await $fetch(config.userURL, {
|
|
65
|
+
headers: {
|
|
66
|
+
Authorization: `Bearer ${tokens.access_token}`
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
return onSuccess(event, {
|
|
70
|
+
tokens,
|
|
71
|
+
user
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { OAuthConfig } from '#auth-utils';
|
|
2
|
+
export interface OAuthSalesforceConfig {
|
|
3
|
+
/**
|
|
4
|
+
* Salesforce OAuth Client ID
|
|
5
|
+
* @default process.env.NUXT_OAUTH_SALESFORCE_CLIENT_ID
|
|
6
|
+
*/
|
|
7
|
+
clientId?: string;
|
|
8
|
+
/**
|
|
9
|
+
* Salesforce OAuth Client Secret
|
|
10
|
+
* @default process.env.NUXT_OAUTH_SALESFORCE_CLIENT_SECRET
|
|
11
|
+
*/
|
|
12
|
+
clientSecret?: string;
|
|
13
|
+
/**
|
|
14
|
+
* Salesforce OAuth Scope
|
|
15
|
+
* @default ['id']
|
|
16
|
+
* @see https://help.salesforce.com/s/articleView?id=xcloud.remoteaccess_oauth_tokens_scopes.htm
|
|
17
|
+
* @example ['id']
|
|
18
|
+
*/
|
|
19
|
+
scope?: string[];
|
|
20
|
+
/**
|
|
21
|
+
* Salesforce MyDomain URL
|
|
22
|
+
* @default 'https://login.salesforce.com'
|
|
23
|
+
*/
|
|
24
|
+
baseURL?: string;
|
|
25
|
+
/**
|
|
26
|
+
* Salesforce OAuth Authorization URL
|
|
27
|
+
* @default 'https://login.salesforce.com/services/oauth2/authorize'
|
|
28
|
+
*/
|
|
29
|
+
authorizationURL?: string;
|
|
30
|
+
/**
|
|
31
|
+
* Salesforce OAuth Authorization URL
|
|
32
|
+
* @default 'https://login.salesforce.com/services/oauth2/token'
|
|
33
|
+
*/
|
|
34
|
+
tokenURL?: string;
|
|
35
|
+
/**
|
|
36
|
+
* Extra authorization parameters to provide to the authorization URL
|
|
37
|
+
* @default {}
|
|
38
|
+
*/
|
|
39
|
+
authorizationParams?: Record<string, string>;
|
|
40
|
+
/**
|
|
41
|
+
* Redirect URL to allow overriding for situations like prod failing to determine public hostname
|
|
42
|
+
* @default process.env.NUXT_OAUTH_SALESFORCE_REDIRECT_URL or current URL
|
|
43
|
+
*/
|
|
44
|
+
redirectURL?: string;
|
|
45
|
+
}
|
|
46
|
+
export declare function defineOAuthSalesforceEventHandler({ config, onSuccess, onError, }: OAuthConfig<OAuthSalesforceConfig>): import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<void>>;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { eventHandler, getQuery, sendRedirect } from "h3";
|
|
2
|
+
import { withQuery } from "ufo";
|
|
3
|
+
import { defu } from "defu";
|
|
4
|
+
import { handleMissingConfiguration, handleAccessTokenErrorResponse, getOAuthRedirectURL, requestAccessToken, handleState, handleInvalidState } from "../utils.js";
|
|
5
|
+
import { useRuntimeConfig, createError } from "#imports";
|
|
6
|
+
export function defineOAuthSalesforceEventHandler({
|
|
7
|
+
config,
|
|
8
|
+
onSuccess,
|
|
9
|
+
onError
|
|
10
|
+
}) {
|
|
11
|
+
return eventHandler(async (event) => {
|
|
12
|
+
const runtimeConfig = useRuntimeConfig(event).oauth?.salesforce;
|
|
13
|
+
const baseURL = config?.baseURL || "https://login.salesforce.com";
|
|
14
|
+
config = defu(config, runtimeConfig, {
|
|
15
|
+
authorizationURL: `${baseURL}/services/oauth2/authorize`,
|
|
16
|
+
tokenURL: `${baseURL}/services/oauth2/token`,
|
|
17
|
+
authorizationParams: {}
|
|
18
|
+
});
|
|
19
|
+
const query = getQuery(event);
|
|
20
|
+
if (query.error) {
|
|
21
|
+
const error = createError({
|
|
22
|
+
statusCode: 401,
|
|
23
|
+
message: `Salesforce login failed: ${query.error || "Unknown error"}`,
|
|
24
|
+
data: query
|
|
25
|
+
});
|
|
26
|
+
if (!onError) throw error;
|
|
27
|
+
return onError(event, error);
|
|
28
|
+
}
|
|
29
|
+
if (!config.clientId || !config.clientSecret) {
|
|
30
|
+
return handleMissingConfiguration(event, "salesforce", ["clientId", "clientSecret"], onError);
|
|
31
|
+
}
|
|
32
|
+
const redirectURL = config.redirectURL || getOAuthRedirectURL(event);
|
|
33
|
+
const state = await handleState(event);
|
|
34
|
+
if (!query.code) {
|
|
35
|
+
config.scope = config.scope || ["id"];
|
|
36
|
+
return sendRedirect(
|
|
37
|
+
event,
|
|
38
|
+
withQuery(config.authorizationURL, {
|
|
39
|
+
response_type: "code",
|
|
40
|
+
client_id: config.clientId,
|
|
41
|
+
redirect_uri: redirectURL,
|
|
42
|
+
scope: config.scope.join(" "),
|
|
43
|
+
state,
|
|
44
|
+
...config.authorizationParams
|
|
45
|
+
})
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
if (query.state !== state) {
|
|
49
|
+
handleInvalidState(event, "salesforce", onError);
|
|
50
|
+
}
|
|
51
|
+
const tokens = await requestAccessToken(config.tokenURL, {
|
|
52
|
+
body: {
|
|
53
|
+
grant_type: "authorization_code",
|
|
54
|
+
client_id: config.clientId,
|
|
55
|
+
client_secret: config.clientSecret,
|
|
56
|
+
redirect_uri: redirectURL,
|
|
57
|
+
code: query.code
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
if (tokens.error) {
|
|
61
|
+
return handleAccessTokenErrorResponse(event, "salesforce", tokens, onError);
|
|
62
|
+
}
|
|
63
|
+
const accessToken = tokens.access_token;
|
|
64
|
+
const user = await $fetch(`${baseURL}/services/oauth2/userinfo`, {
|
|
65
|
+
headers: {
|
|
66
|
+
Authorization: `Bearer ${accessToken}`
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
return onSuccess(event, {
|
|
70
|
+
user,
|
|
71
|
+
tokens
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { OAuthConfig } from '#auth-utils';
|
|
2
|
+
export interface OAuthSlackConfig {
|
|
3
|
+
/**
|
|
4
|
+
* Slack OAuth Client ID
|
|
5
|
+
* @default process.env.NUXT_OAUTH_SLACK_CLIENT_ID
|
|
6
|
+
*/
|
|
7
|
+
clientId?: string;
|
|
8
|
+
/**
|
|
9
|
+
* Slack OAuth Client Secret
|
|
10
|
+
* @default process.env.NUXT_OAUTH_SLACK_CLIENT_SECRET
|
|
11
|
+
*/
|
|
12
|
+
clientSecret?: string;
|
|
13
|
+
/**
|
|
14
|
+
* Slack OAuth Scope
|
|
15
|
+
* @default ['openid', 'email', 'profile']
|
|
16
|
+
* @see https://api.slack.com/scopes?filter=user
|
|
17
|
+
* @example ['openid']
|
|
18
|
+
*/
|
|
19
|
+
scope?: string[];
|
|
20
|
+
/**
|
|
21
|
+
* Slack OAuth Authorization URL
|
|
22
|
+
* @default 'https://slack.com/openid/connect/authorize'
|
|
23
|
+
*/
|
|
24
|
+
authorizationURL?: string;
|
|
25
|
+
/**
|
|
26
|
+
* Slack OAuth Authorization URL
|
|
27
|
+
* @default 'https://slack.com/api/openid.connect.token'
|
|
28
|
+
*/
|
|
29
|
+
tokenURL?: string;
|
|
30
|
+
/**
|
|
31
|
+
* Extra authorization parameters to provide to the authorization URL
|
|
32
|
+
* @default {}
|
|
33
|
+
*/
|
|
34
|
+
authorizationParams?: Record<string, string>;
|
|
35
|
+
/**
|
|
36
|
+
* Redirect URL to allow overriding for situations like prod failing to determine public hostname
|
|
37
|
+
* @default process.env.NUXT_OAUTH_SLACK_REDIRECT_URL or current URL
|
|
38
|
+
*/
|
|
39
|
+
redirectURL?: string;
|
|
40
|
+
}
|
|
41
|
+
export declare function defineOAuthSlackEventHandler({ config, onSuccess, onError, }: OAuthConfig<OAuthSlackConfig>): import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<void>>;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { eventHandler, getQuery, sendRedirect } from "h3";
|
|
2
|
+
import { withQuery } from "ufo";
|
|
3
|
+
import { defu } from "defu";
|
|
4
|
+
import { handleMissingConfiguration, handleAccessTokenErrorResponse, getOAuthRedirectURL, requestAccessToken, handleState, handleInvalidState } from "../utils.js";
|
|
5
|
+
import { useRuntimeConfig, createError } from "#imports";
|
|
6
|
+
export function defineOAuthSlackEventHandler({
|
|
7
|
+
config,
|
|
8
|
+
onSuccess,
|
|
9
|
+
onError
|
|
10
|
+
}) {
|
|
11
|
+
return eventHandler(async (event) => {
|
|
12
|
+
const runtimeConfig = useRuntimeConfig(event).oauth?.slack;
|
|
13
|
+
const baseURL = "https://slack.com";
|
|
14
|
+
config = defu(config, runtimeConfig, {
|
|
15
|
+
authorizationURL: `${baseURL}/openid/connect/authorize`,
|
|
16
|
+
tokenURL: `${baseURL}/api/openid.connect.token`,
|
|
17
|
+
authorizationParams: {}
|
|
18
|
+
});
|
|
19
|
+
const query = getQuery(event);
|
|
20
|
+
if (query.error) {
|
|
21
|
+
const error = createError({
|
|
22
|
+
statusCode: 401,
|
|
23
|
+
message: `Slack login failed: ${query.error || "Unknown error"}`,
|
|
24
|
+
data: query
|
|
25
|
+
});
|
|
26
|
+
if (!onError) throw error;
|
|
27
|
+
return onError(event, error);
|
|
28
|
+
}
|
|
29
|
+
if (!config.clientId || !config.clientSecret) {
|
|
30
|
+
return handleMissingConfiguration(event, "slack", ["clientId", "clientSecret"], onError);
|
|
31
|
+
}
|
|
32
|
+
const redirectURL = config.redirectURL || getOAuthRedirectURL(event);
|
|
33
|
+
const state = await handleState(event);
|
|
34
|
+
if (!query.code) {
|
|
35
|
+
config.scope = config.scope || ["openid", "email", "profile"];
|
|
36
|
+
return sendRedirect(
|
|
37
|
+
event,
|
|
38
|
+
withQuery(config.authorizationURL, {
|
|
39
|
+
response_type: "code",
|
|
40
|
+
client_id: config.clientId,
|
|
41
|
+
redirect_uri: redirectURL,
|
|
42
|
+
scope: config.scope.join(" "),
|
|
43
|
+
state,
|
|
44
|
+
...config.authorizationParams
|
|
45
|
+
})
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
if (query.state !== state) {
|
|
49
|
+
handleInvalidState(event, "slack", onError);
|
|
50
|
+
}
|
|
51
|
+
const tokens = await requestAccessToken(config.tokenURL, {
|
|
52
|
+
body: {
|
|
53
|
+
grant_type: "authorization_code",
|
|
54
|
+
client_id: config.clientId,
|
|
55
|
+
client_secret: config.clientSecret,
|
|
56
|
+
redirect_uri: redirectURL,
|
|
57
|
+
code: query.code
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
if (tokens.error) {
|
|
61
|
+
return handleAccessTokenErrorResponse(event, "slack", tokens, onError);
|
|
62
|
+
}
|
|
63
|
+
const accessToken = tokens.access_token;
|
|
64
|
+
const user = await $fetch(`${baseURL}/api/openid.connect.userInfo`, {
|
|
65
|
+
headers: {
|
|
66
|
+
Authorization: `Bearer ${accessToken}`
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
return onSuccess(event, {
|
|
70
|
+
user,
|
|
71
|
+
tokens
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
}
|
|
@@ -12,25 +12,48 @@ export function defineOAuthSteamEventHandler({ config, onSuccess, onError }) {
|
|
|
12
12
|
if (!config.apiKey) {
|
|
13
13
|
return handleMissingConfiguration(event, "steam", ["apiKey"], onError);
|
|
14
14
|
}
|
|
15
|
+
const url = getRequestURL(event);
|
|
15
16
|
if (!query["openid.claimed_id"]) {
|
|
16
17
|
const redirectURL = config.redirectURL || getRequestURL(event).href;
|
|
17
18
|
const steamOpenIdParams = {
|
|
18
19
|
"openid.ns": "http://specs.openid.net/auth/2.0",
|
|
19
20
|
"openid.mode": "checkid_setup",
|
|
20
21
|
"openid.return_to": redirectURL,
|
|
22
|
+
"openid.realm": `${url.protocol}//${url.hostname}`,
|
|
21
23
|
"openid.identity": "http://specs.openid.net/auth/2.0/identifier_select",
|
|
22
24
|
"openid.claimed_id": "http://specs.openid.net/auth/2.0/identifier_select"
|
|
23
25
|
};
|
|
24
26
|
return sendRedirect(event, withQuery(config.authorizationURL, steamOpenIdParams));
|
|
25
27
|
}
|
|
28
|
+
if (!query["openid.signed"] || !query["openid.sig"]) {
|
|
29
|
+
const error = createError({
|
|
30
|
+
statusCode: 400,
|
|
31
|
+
message: "Steam login failed: Incomplete query."
|
|
32
|
+
});
|
|
33
|
+
if (!onError) throw error;
|
|
34
|
+
return onError(event, error);
|
|
35
|
+
}
|
|
26
36
|
const openIdCheck = {
|
|
27
|
-
ns: "http://specs.openid.net/auth/2.0",
|
|
28
|
-
|
|
29
|
-
|
|
37
|
+
"openid.ns": "http://specs.openid.net/auth/2.0",
|
|
38
|
+
"openid.mode": "check_authentication",
|
|
39
|
+
"openid.signed": query["openid.signed"],
|
|
40
|
+
"openid.sig": query["openid.sig"]
|
|
30
41
|
};
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
42
|
+
for (const signed of query["openid.signed"].split(",")) {
|
|
43
|
+
if (!query[`openid.${signed}`]) {
|
|
44
|
+
const error = createError({
|
|
45
|
+
statusCode: 400,
|
|
46
|
+
message: "Steam login failed: Incomplete query."
|
|
47
|
+
});
|
|
48
|
+
if (!onError) throw error;
|
|
49
|
+
return onError(event, error);
|
|
50
|
+
}
|
|
51
|
+
openIdCheck[`openid.${signed}`] = query[`openid.${signed}`];
|
|
52
|
+
}
|
|
53
|
+
const auth_validation = await $fetch(withQuery(config?.authorizationURL, openIdCheck));
|
|
54
|
+
const validRegex = /is_valid:true/;
|
|
55
|
+
const valid = validRegex.test(auth_validation);
|
|
56
|
+
if (!valid) {
|
|
34
57
|
const error = createError({
|
|
35
58
|
statusCode: 401,
|
|
36
59
|
message: "Steam login failed: Claimed identity is invalid."
|
|
@@ -38,6 +61,8 @@ export function defineOAuthSteamEventHandler({ config, onSuccess, onError }) {
|
|
|
38
61
|
if (!onError) throw error;
|
|
39
62
|
return onError(event, error);
|
|
40
63
|
}
|
|
64
|
+
const idRegex = /^https?:\/\/steamcommunity\.com\/openid\/id\/(\d+)$/;
|
|
65
|
+
const steamIdCheck = idRegex.exec(query["openid.claimed_id"]);
|
|
41
66
|
const steamId = steamIdCheck[1];
|
|
42
67
|
const user = await $fetch(withQuery("https://api.steampowered.com/ISteamUser/GetPlayerSummaries/v2/", {
|
|
43
68
|
key: config.apiKey,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
1
2
|
import { eventHandler, getQuery, sendRedirect } from "h3";
|
|
2
3
|
import { withQuery } from "ufo";
|
|
3
4
|
import { defu } from "defu";
|
|
4
|
-
import { sha256 } from "ohash";
|
|
5
5
|
import { handleAccessTokenErrorResponse, handleMissingConfiguration, getOAuthRedirectURL, requestAccessToken } from "../utils.js";
|
|
6
6
|
import { useRuntimeConfig, createError } from "#imports";
|
|
7
7
|
export function defineOAuthTikTokEventHandler({ config, onSuccess, onError }) {
|
|
@@ -31,7 +31,7 @@ export function defineOAuthTikTokEventHandler({ config, onSuccess, onError }) {
|
|
|
31
31
|
scope: config.scope.join(","),
|
|
32
32
|
...config.sandbox ? {
|
|
33
33
|
code_verifier: codeVerifier,
|
|
34
|
-
code_challenge: sha256(codeVerifier),
|
|
34
|
+
code_challenge: crypto.createHash("sha256").update(codeVerifier).digest("hex"),
|
|
35
35
|
code_challenge_method: "S256"
|
|
36
36
|
} : {}
|
|
37
37
|
})
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type H3Event } from 'h3';
|
|
2
2
|
import type { OAuthProvider, OnError } from '#auth-utils';
|
|
3
3
|
export declare function getOAuthRedirectURL(event: H3Event): string;
|
|
4
4
|
/**
|
|
@@ -34,6 +34,7 @@ export declare function requestAccessToken(url: string, options: RequestAccessTo
|
|
|
34
34
|
*/
|
|
35
35
|
export declare function handleAccessTokenErrorResponse(event: H3Event, oauthProvider: OAuthProvider, oauthError: any, onError?: OnError): void | Promise<void>;
|
|
36
36
|
export declare function handleMissingConfiguration(event: H3Event, provider: OAuthProvider, missingKeys: string[], onError?: OnError): void | Promise<void>;
|
|
37
|
+
export declare function handleInvalidState(event: H3Event, provider: OAuthProvider, onError?: OnError): void | Promise<void>;
|
|
37
38
|
/**
|
|
38
39
|
* JWT signing using jose
|
|
39
40
|
*
|
|
@@ -59,4 +60,14 @@ interface JWTVerifyOptions {
|
|
|
59
60
|
issuer: string;
|
|
60
61
|
}
|
|
61
62
|
export declare function verifyJwt<T>(token: string, options: JWTVerifyOptions): Promise<T>;
|
|
63
|
+
export declare function handlePkceVerifier(event: H3Event): Promise<{
|
|
64
|
+
code_verifier: string;
|
|
65
|
+
code_challenge?: undefined;
|
|
66
|
+
code_challenge_method?: undefined;
|
|
67
|
+
} | {
|
|
68
|
+
code_verifier: string;
|
|
69
|
+
code_challenge: string;
|
|
70
|
+
code_challenge_method: string;
|
|
71
|
+
}>;
|
|
72
|
+
export declare function handleState(event: H3Event): Promise<string>;
|
|
62
73
|
export {};
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { deleteCookie, getCookie, setCookie } from "h3";
|
|
1
2
|
import { getRequestURL } from "h3";
|
|
2
3
|
import { FetchError } from "ofetch";
|
|
3
4
|
import { snakeCase, upperFirst } from "scule";
|
|
4
5
|
import * as jose from "jose";
|
|
6
|
+
import { subtle, getRandomValues } from "uncrypto";
|
|
5
7
|
import { createError } from "#imports";
|
|
6
8
|
export function getOAuthRedirectURL(event) {
|
|
7
9
|
const requestURL = getRequestURL(event);
|
|
@@ -45,6 +47,15 @@ export function handleMissingConfiguration(event, provider, missingKeys, onError
|
|
|
45
47
|
if (!onError) throw error;
|
|
46
48
|
return onError(event, error);
|
|
47
49
|
}
|
|
50
|
+
export function handleInvalidState(event, provider, onError) {
|
|
51
|
+
const message = `${upperFirst(provider)} login failed: state mismatch`;
|
|
52
|
+
const error = createError({
|
|
53
|
+
statusCode: 500,
|
|
54
|
+
message
|
|
55
|
+
});
|
|
56
|
+
if (!onError) throw error;
|
|
57
|
+
return onError(event, error);
|
|
58
|
+
}
|
|
48
59
|
export async function signJwt(payload, options) {
|
|
49
60
|
const now = Math.floor(Date.now() / 1e3);
|
|
50
61
|
const privateKey = await jose.importPKCS8(
|
|
@@ -61,3 +72,36 @@ export async function verifyJwt(token, options) {
|
|
|
61
72
|
});
|
|
62
73
|
return payload;
|
|
63
74
|
}
|
|
75
|
+
function encodeBase64Url(input) {
|
|
76
|
+
return btoa(String.fromCharCode.apply(null, input)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
|
|
77
|
+
}
|
|
78
|
+
function getRandomBytes(size = 32) {
|
|
79
|
+
return getRandomValues(new Uint8Array(size));
|
|
80
|
+
}
|
|
81
|
+
export async function handlePkceVerifier(event) {
|
|
82
|
+
let verifier = getCookie(event, "nuxt-auth-pkce");
|
|
83
|
+
if (verifier) {
|
|
84
|
+
deleteCookie(event, "nuxt-auth-pkce");
|
|
85
|
+
return { code_verifier: verifier };
|
|
86
|
+
}
|
|
87
|
+
verifier = encodeBase64Url(getRandomBytes());
|
|
88
|
+
setCookie(event, "nuxt-auth-pkce", verifier);
|
|
89
|
+
const encodedPkce = new TextEncoder().encode(verifier);
|
|
90
|
+
const pkceHash = await subtle.digest("SHA-256", encodedPkce);
|
|
91
|
+
const pkce = encodeBase64Url(new Uint8Array(pkceHash));
|
|
92
|
+
return {
|
|
93
|
+
code_verifier: verifier,
|
|
94
|
+
code_challenge: pkce,
|
|
95
|
+
code_challenge_method: "S256"
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
export async function handleState(event) {
|
|
99
|
+
let state = getCookie(event, "nuxt-auth-state");
|
|
100
|
+
if (state) {
|
|
101
|
+
deleteCookie(event, "nuxt-auth-state");
|
|
102
|
+
return state;
|
|
103
|
+
}
|
|
104
|
+
state = encodeBase64Url(getRandomBytes(8));
|
|
105
|
+
setCookie(event, "nuxt-auth-state", state);
|
|
106
|
+
return state;
|
|
107
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { H3Event, H3Error } from 'h3';
|
|
2
2
|
export type ATProtoProvider = 'bluesky';
|
|
3
|
-
export type OAuthProvider = ATProtoProvider | 'atlassian' | 'auth0' | 'authentik' | 'battledotnet' | 'cognito' | 'discord' | 'dropbox' | 'facebook' | 'gitea' | 'github' | 'gitlab' | 'google' | 'hubspot' | 'instagram' | 'keycloak' | 'line' | 'linear' | 'linkedin' | 'microsoft' | 'paypal' | 'polar' | 'spotify' | 'seznam' | 'steam' | 'strava' | 'tiktok' | 'twitch' | 'vk' | 'workos' | 'x' | 'xsuaa' | 'yandex' | 'zitadel' | 'apple' | (string & {});
|
|
3
|
+
export type OAuthProvider = ATProtoProvider | 'atlassian' | 'auth0' | 'authentik' | 'azureb2c' | 'battledotnet' | 'cognito' | 'discord' | 'dropbox' | 'facebook' | 'gitea' | 'github' | 'gitlab' | 'google' | 'hubspot' | 'instagram' | 'kick' | 'keycloak' | 'line' | 'linear' | 'linkedin' | 'microsoft' | 'paypal' | 'polar' | 'spotify' | 'seznam' | 'steam' | 'strava' | 'tiktok' | 'twitch' | 'vk' | 'workos' | 'x' | 'xsuaa' | 'yandex' | 'zitadel' | 'apple' | 'livechat' | 'salesforce' | 'slack' | 'heroku' | (string & {});
|
|
4
4
|
export type OnError = (event: H3Event, error: H3Error) => Promise<void> | void;
|
|
5
5
|
export interface OAuthConfig<TConfig, TUser = any, TTokens = any> {
|
|
6
6
|
config?: TConfig;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nuxt-auth-utils",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.18",
|
|
4
4
|
"description": "Add Authentication to Nuxt applications with secured & sealed cookies sessions.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -42,7 +42,6 @@
|
|
|
42
42
|
"hookable": "^5.5.3",
|
|
43
43
|
"jose": "^5.9.6",
|
|
44
44
|
"ofetch": "^1.4.1",
|
|
45
|
-
"ohash": "^1.1.4",
|
|
46
45
|
"openid-client": "^6.1.7",
|
|
47
46
|
"pathe": "^2.0.2",
|
|
48
47
|
"scule": "^1.3.0",
|