nuxt-auth-utils 0.5.27 → 0.5.28

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
@@ -239,6 +239,7 @@ It can also be set using environment variables:
239
239
  - OIDC / OpenID Connect (Generic)
240
240
  - Okta
241
241
  - Ory
242
+ - osu!
242
243
  - PayPal
243
244
  - Polar
244
245
  - Salesforce
@@ -449,6 +450,8 @@ export default defineWebAuthnRegisterEventHandler({
449
450
  },
450
451
  })
451
452
  ```
453
+ > [!TIP]
454
+ > If you want to plug in and use the [Community sourced list of AAGUIDS](https://github.com/passkeydeveloper/passkey-authenticator-aaguids) to decorate your display of added passkeys, you can access the the ID via `credential.aaguid` inside `onSuccess`
452
455
 
453
456
  ```ts
454
457
  // server/api/webauthn/authenticate.post.ts
package/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "auth-utils",
3
3
  "configKey": "auth",
4
- "version": "0.5.27",
4
+ "version": "0.5.28",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "3.6.1"
package/dist/module.mjs CHANGED
@@ -400,6 +400,12 @@ const module$1 = defineNuxtModule({
400
400
  redirectURL: "",
401
401
  scope: []
402
402
  });
403
+ runtimeConfig.oauth.osu = defu(runtimeConfig.oauth.osu, {
404
+ clientId: "",
405
+ clientSecret: "",
406
+ redirectURL: "",
407
+ scope: []
408
+ });
403
409
  }
404
410
  });
405
411
 
@@ -35,4 +35,21 @@ export interface OAuthKickConfig {
35
35
  */
36
36
  redirectURL?: string;
37
37
  }
38
- export declare function defineOAuthKickEventHandler({ config, onSuccess, onError }: OAuthConfig<OAuthKickConfig>): import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<any>>;
38
+ interface KickUser {
39
+ email: string;
40
+ name: string;
41
+ profile_picture: string;
42
+ user_id: string;
43
+ }
44
+ interface KickTokens {
45
+ access_token: string;
46
+ expires_in: number;
47
+ refresh_token: string;
48
+ scope: string;
49
+ token_type: string;
50
+ }
51
+ export declare function defineOAuthKickEventHandler({ config, onSuccess, onError }: OAuthConfig<OAuthKickConfig, {
52
+ user: KickUser;
53
+ tokens: KickTokens;
54
+ }>): import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<any>>;
55
+ export {};
@@ -8,7 +8,6 @@ export interface OAuthOidcConfig {
8
8
  clientId?: string;
9
9
  /**
10
10
  * OAuth Client secret.
11
- * If unset, PKCE will be used where no client secret is needed.
12
11
  *
13
12
  * @default process.env.NUXT_OAUTH_OIDC_CLIENT_SECRET
14
13
  */
@@ -2,7 +2,7 @@ import { useRuntimeConfig } from "#imports";
2
2
  import { defu } from "defu";
3
3
  import { createError, eventHandler, getQuery, sendRedirect } from "h3";
4
4
  import { withQuery } from "ufo";
5
- import { getOAuthRedirectURL, handleAccessTokenErrorResponse, handleInvalidState, handleMissingConfiguration, handlePkceVerifier, handleState, requestAccessToken } from "../utils.js";
5
+ import { getOAuthRedirectURL, handleAccessTokenErrorResponse, handleInvalidState, handleMissingConfiguration, handlePkceVerifier, handleState, requestAccessToken, handleNonce, parseJwt } from "../utils.js";
6
6
  export function defineOAuthOidcEventHandler({ config, onSuccess, onError }) {
7
7
  return eventHandler(async (event) => {
8
8
  config = defu(config, useRuntimeConfig(event).oauth.oidc, {
@@ -24,7 +24,8 @@ export function defineOAuthOidcEventHandler({ config, onSuccess, onError }) {
24
24
  const oidcConfig = typeof config.openidConfig === "string" ? await $fetch(config.openidConfig) : config.openidConfig;
25
25
  const redirectURL = config.redirectURL || getOAuthRedirectURL(event);
26
26
  const state = await handleState(event);
27
- const verifier = !config.clientSecret ? await handlePkceVerifier(event) : void 0;
27
+ const verifier = await handlePkceVerifier(event);
28
+ const nonce = handleNonce(event);
28
29
  if (!query.code) {
29
30
  config.scope = config.scope || [];
30
31
  const authQuery = {
@@ -32,13 +33,12 @@ export function defineOAuthOidcEventHandler({ config, onSuccess, onError }) {
32
33
  redirect_uri: redirectURL,
33
34
  scope: config.scope.join(" "),
34
35
  state,
36
+ nonce,
35
37
  response_type: "code",
36
38
  ...config.params?.authorization_endpoint
37
39
  };
38
- if (verifier) {
39
- authQuery.code_challenge = verifier.code_challenge;
40
- authQuery.code_challenge_method = verifier.code_challenge_method;
41
- }
40
+ authQuery.code_challenge = verifier.code_challenge;
41
+ authQuery.code_challenge_method = verifier.code_challenge_method;
42
42
  return sendRedirect(
43
43
  event,
44
44
  withQuery(oidcConfig.authorization_endpoint, authQuery)
@@ -53,17 +53,26 @@ export function defineOAuthOidcEventHandler({ config, onSuccess, onError }) {
53
53
  client_secret: config.clientSecret,
54
54
  redirect_uri: redirectURL,
55
55
  code: query.code,
56
+ code_verifier: verifier.code_verifier,
56
57
  ...config.params?.token_endpoint
57
58
  };
58
- if (verifier) {
59
- tokenQuery.code_verifier = verifier.code_verifier;
60
- }
61
59
  const tokens = await requestAccessToken(oidcConfig.token_endpoint, {
62
60
  body: tokenQuery
63
61
  });
64
62
  if (tokens.error) {
65
63
  return handleAccessTokenErrorResponse(event, "oidc", tokens, onError);
66
64
  }
65
+ if (tokens.id_token) {
66
+ const claims = parseJwt(tokens.id_token);
67
+ if (claims.nonce !== nonce) {
68
+ const error = createError({
69
+ statusCode: 401,
70
+ message: "OIDC login failed: nonce mismatch"
71
+ });
72
+ if (!onError) throw error;
73
+ return onError(event, error);
74
+ }
75
+ }
67
76
  let user = {};
68
77
  if (oidcConfig.userinfo_endpoint) {
69
78
  user = await $fetch(oidcConfig.userinfo_endpoint, {
@@ -0,0 +1,42 @@
1
+ import type { OAuthConfig } from '#auth-utils';
2
+ export interface OAuthOsuConfig {
3
+ /**
4
+ * osu! OAuth Client ID
5
+ * @default process.env.NUXT_OAUTH_OSU_CLIENT_ID
6
+ */
7
+ clientId?: string;
8
+ /**
9
+ * osu! OAuth Client Secret
10
+ * @default process.env.NUXT_OAUTH_OSU_CLIENT_SECRET
11
+ */
12
+ clientSecret?: string;
13
+ /**
14
+ * osu! OAuth Scope
15
+ *
16
+ * The identify scope is always implicitly provided.
17
+ * @default []
18
+ * @see https://osu.ppy.sh/docs/#scopes
19
+ */
20
+ scope?: string[];
21
+ /**
22
+ * osu! OAuth Authorization URL
23
+ * @default 'https://osu.ppy.sh/oauth/authorize'
24
+ */
25
+ authorizationURL?: string;
26
+ /**
27
+ * osu! OAuth Token URL
28
+ * @default 'https://osu.ppy.sh/oauth/token'
29
+ */
30
+ tokenURL?: string;
31
+ /**
32
+ * Extra authorization parameters to provide to the authorization URL
33
+ * @see 'https://osu.ppy.sh/docs/#authorization-code-grant'
34
+ */
35
+ authorizationParams?: Record<string, string>;
36
+ /**
37
+ * Redirect URL to to allow overriding for situations like prod failing to determine public hostname
38
+ * @default process.env.NUXT_OAUTH_OSU_REDIRECT_URL or current URL
39
+ */
40
+ redirectURL?: string;
41
+ }
42
+ export declare function defineOAuthOsuEventHandler({ config, onSuccess, onError }: OAuthConfig<OAuthOsuConfig>): import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<any>>;
@@ -0,0 +1,63 @@
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 } from "#imports";
6
+ export function defineOAuthOsuEventHandler({ config, onSuccess, onError }) {
7
+ return eventHandler(async (event) => {
8
+ config = defu(config, useRuntimeConfig(event).oauth?.osu, {
9
+ authorizationURL: "https://osu.ppy.sh/oauth/authorize",
10
+ tokenURL: "https://osu.ppy.sh/oauth/token",
11
+ authorizationParams: {}
12
+ });
13
+ const query = getQuery(event);
14
+ if (!config.clientId || !config.clientSecret) {
15
+ return handleMissingConfiguration(event, "osu", ["clientId", "clientSecret"], onError);
16
+ }
17
+ if (query.error) {
18
+ return handleAccessTokenErrorResponse(event, "osu", query, onError);
19
+ }
20
+ const redirectURL = config.redirectURL || getOAuthRedirectURL(event);
21
+ const state = await handleState(event);
22
+ if (!query.code) {
23
+ config.scope = config.scope || [];
24
+ return sendRedirect(
25
+ event,
26
+ withQuery(config.authorizationURL, {
27
+ response_type: "code",
28
+ client_id: config.clientId,
29
+ redirect_uri: redirectURL,
30
+ scope: config.scope.join(" "),
31
+ state,
32
+ ...config.authorizationParams
33
+ })
34
+ );
35
+ }
36
+ if (query.state !== state) {
37
+ return handleInvalidState(event, "osu", onError);
38
+ }
39
+ const tokens = await requestAccessToken(config.tokenURL, {
40
+ body: {
41
+ client_id: config.clientId,
42
+ client_secret: config.clientSecret,
43
+ grant_type: "authorization_code",
44
+ redirect_uri: redirectURL,
45
+ code: query.code
46
+ }
47
+ });
48
+ if (tokens.error) {
49
+ return handleAccessTokenErrorResponse(event, "osu", tokens, onError);
50
+ }
51
+ const accessToken = tokens.access_token;
52
+ const user = await $fetch("https://osu.ppy.sh/api/v2/me", {
53
+ headers: {
54
+ "user-agent": "Nuxt Auth Utils",
55
+ "Authorization": `Bearer ${accessToken}`
56
+ }
57
+ });
58
+ return onSuccess(event, {
59
+ tokens,
60
+ user
61
+ });
62
+ });
63
+ }
@@ -44,4 +44,28 @@ export interface OAuthTwitchConfig {
44
44
  */
45
45
  redirectURL?: string;
46
46
  }
47
- export declare function defineOAuthTwitchEventHandler({ config, onSuccess, onError }: OAuthConfig<OAuthTwitchConfig>): import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<any>>;
47
+ interface TwitchUser {
48
+ id: string;
49
+ login: string;
50
+ display_name: string;
51
+ type: string;
52
+ broadcaster_type: string;
53
+ description: string;
54
+ profile_image_url: string;
55
+ offline_image_url: string;
56
+ view_count: number;
57
+ email?: string;
58
+ created_at: string;
59
+ }
60
+ interface TwitchTokens {
61
+ access_token: string;
62
+ expires_in: number;
63
+ refresh_token: string;
64
+ scope: string[];
65
+ token_type: string;
66
+ }
67
+ export declare function defineOAuthTwitchEventHandler({ config, onSuccess, onError }: OAuthConfig<OAuthTwitchConfig, {
68
+ user: TwitchUser;
69
+ tokens: TwitchTokens;
70
+ }>): import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<any>>;
71
+ export {};
@@ -1,4 +1,5 @@
1
1
  import { type H3Event } from 'h3';
2
+ import * as jose from 'jose';
2
3
  import type { OAuthProvider, OnError } from '#auth-utils';
3
4
  export declare function getOAuthRedirectURL(event: H3Event): string;
4
5
  /**
@@ -70,4 +71,6 @@ export declare function handlePkceVerifier(event: H3Event): Promise<{
70
71
  code_challenge_method?: undefined;
71
72
  }>;
72
73
  export declare function handleState(event: H3Event): Promise<string | undefined>;
74
+ export declare function parseJwt(token: string): jose.JWTPayload;
75
+ export declare function handleNonce(event: H3Event): string | undefined;
73
76
  export {};
@@ -121,3 +121,23 @@ export async function handleState(event) {
121
121
  });
122
122
  return state;
123
123
  }
124
+ export function parseJwt(token) {
125
+ return jose.decodeJwt(token);
126
+ }
127
+ export function handleNonce(event) {
128
+ const query = getQuery(event);
129
+ if (query.code) {
130
+ const nonce2 = getCookie(event, "nuxt-auth-nonce");
131
+ deleteCookie(event, "nuxt-auth-nonce");
132
+ return nonce2;
133
+ }
134
+ const nonce = encodeBase64Url(getRandomBytes(8));
135
+ setCookie(event, "nuxt-auth-nonce", nonce, {
136
+ httpOnly: true,
137
+ secure: !isDevelopment,
138
+ sameSite: "lax",
139
+ maxAge: OAUTH_COOKIE_MAX_AGE,
140
+ path: "/"
141
+ });
142
+ return nonce;
143
+ }
@@ -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' | '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' | 'roblox' | 'okta' | 'ory' | 'shopifyCustomer' | 'oicd' | (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' | 'roblox' | 'okta' | 'ory' | 'shopifyCustomer' | 'oidc' | 'osu' | (string & {});
4
4
  export type OnError = (event: H3Event, error: H3Error) => Promise<void> | void;
5
5
  export interface OAuthConfig<TConfig, TResult = {
6
6
  user: any;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nuxt-auth-utils",
3
- "version": "0.5.27",
3
+ "version": "0.5.28",
4
4
  "description": "Add Authentication to Nuxt applications with secured & sealed cookies sessions.",
5
5
  "repository": {
6
6
  "type": "git",