nuxt-auth-utils 0.5.15 → 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 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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "auth-utils",
3
3
  "configKey": "auth",
4
- "version": "0.5.15",
4
+ "version": "0.5.17",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "0.8.4",
7
7
  "unbuild": "2.0.0"
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 tokens = await verifyJwt(accessTokenResult.id_token, {
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 (!tokens) {
70
- return handleAccessTokenErrorResponse(event, "apple", tokens, onError);
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}`
@@ -35,6 +35,7 @@ export function defineOAuthKeycloakEventHandler({
35
35
  return sendRedirect(
36
36
  event,
37
37
  withQuery(authorizationURL, {
38
+ ...query,
38
39
  client_id: config.clientId,
39
40
  redirect_uri: redirectURL,
40
41
  scope: config.scope.join(" "),
@@ -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
- claimed_id: "https://steamcommunity.com/openid/id/",
29
- identity: "https://steamcommunity.com/openid/id/"
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 idRegex = /^https?:\/\/steamcommunity\.com\/openid\/id\/(\d+)$/;
32
- const steamIdCheck = idRegex.exec(query["openid.claimed_id"]);
33
- if (query["openid.op_endpoint"] !== config.authorizationURL || !steamIdCheck || query["openid.ns"] !== openIdCheck.ns || !query["openid.claimed_id"]?.startsWith(openIdCheck.claimed_id) || !query["openid.identity"]?.startsWith(openIdCheck.identity)) {
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 { H3Event } from 'h3';
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
+ }
@@ -0,0 +1,5 @@
1
+ import type { WebAuthnAuthenticateEventHandlerOptions, WebAuthnCredential } from '#auth-utils';
2
+ export declare function defineWebAuthnAuthenticateEventHandler<T extends WebAuthnCredential>({ storeChallenge, getChallenge, getCredential, allowCredentials, getOptions, onSuccess, onError, }: WebAuthnAuthenticateEventHandlerOptions<T>): import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<void | import("@simplewebauthn/server").VerifiedAuthenticationResponse | {
3
+ requestOptions: import("@simplewebauthn/types").PublicKeyCredentialRequestOptionsJSON;
4
+ attemptId: string;
5
+ }>>;
@@ -0,0 +1,8 @@
1
+ import type { H3Event } from 'h3';
2
+ import type { WebAuthnUser, WebAuthnRegisterEventHandlerOptions } from '#auth-utils';
3
+ import type { ValidateUserFunction } from '~/src/runtime/types/webauthn';
4
+ export declare function defineWebAuthnRegisterEventHandler<T extends WebAuthnUser>({ storeChallenge, getChallenge, getOptions, validateUser, excludeCredentials, onSuccess, onError, }: WebAuthnRegisterEventHandlerOptions<T>): import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<void | import("@simplewebauthn/server").VerifiedRegistrationResponse | {
5
+ creationOptions: import("@simplewebauthn/types").PublicKeyCredentialCreationOptionsJSON;
6
+ attemptId: string;
7
+ }>>;
8
+ export declare function validateUserData<T>(userBody: WebAuthnUser, event: H3Event, fn: ValidateUserFunction<T>): Promise<T>;
@@ -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.15",
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",