nuxt-auth-utils 0.5.16 → 0.5.17
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 +3 -0
- package/dist/module.json +1 -1
- package/dist/module.mjs +19 -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/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/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
|
@@ -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
|
|
@@ -227,10 +228,12 @@ It can also be set using environment variables:
|
|
|
227
228
|
- Google
|
|
228
229
|
- Hubspot
|
|
229
230
|
- Instagram
|
|
231
|
+
- Kick
|
|
230
232
|
- Keycloak
|
|
231
233
|
- Line
|
|
232
234
|
- Linear
|
|
233
235
|
- LinkedIn
|
|
236
|
+
- LiveChat
|
|
234
237
|
- Microsoft
|
|
235
238
|
- PayPal
|
|
236
239
|
- Polar
|
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,15 @@ 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
|
+
});
|
|
318
337
|
}
|
|
319
338
|
});
|
|
320
339
|
|
|
@@ -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}`
|
|
@@ -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
|
+
}
|
|
@@ -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' | (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.17",
|
|
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",
|