nuxt-auth-utils 0.5.21 → 0.5.23

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,8 @@ It can also be set using environment variables:
239
239
  - LinkedIn
240
240
  - LiveChat
241
241
  - Microsoft
242
+ - Okta
243
+ - Ory
242
244
  - PayPal
243
245
  - Polar
244
246
  - Salesforce
package/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "auth-utils",
3
3
  "configKey": "auth",
4
- "version": "0.5.21",
4
+ "version": "0.5.23",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "0.8.4",
7
7
  "unbuild": "2.0.0"
package/dist/module.mjs CHANGED
@@ -74,7 +74,7 @@ const module = defineNuxtModule({
74
74
  const envSessionPassword = `${runtimeConfig.nitro?.envPrefix || "NUXT_"}SESSION_PASSWORD`;
75
75
  runtimeConfig.session = defu(runtimeConfig.session, {
76
76
  name: "nuxt-session",
77
- password: process.env[envSessionPassword] || "",
77
+ password: "",
78
78
  cookie: {
79
79
  sameSite: "lax"
80
80
  }
@@ -82,14 +82,14 @@ const module = defineNuxtModule({
82
82
  runtimeConfig.hash = defu(runtimeConfig.hash, {
83
83
  scrypt: options.hash?.scrypt
84
84
  });
85
- if (nuxt.options.dev && !runtimeConfig.session.password) {
86
- runtimeConfig.session.password = randomUUID().replace(/-/g, "");
85
+ if (nuxt.options.dev && !process.env[envSessionPassword]) {
86
+ const password = process.env[envSessionPassword] = randomUUID().replace(/-/g, "");
87
87
  const envPath = join(nuxt.options.rootDir, ".env");
88
88
  const envContent = await readFile(envPath, "utf-8").catch(() => "");
89
89
  if (!envContent.includes(envSessionPassword)) {
90
90
  await writeFile(
91
91
  envPath,
92
- `${envContent ? envContent + "\n" : envContent}${envSessionPassword}=${runtimeConfig.session.password}`,
92
+ `${envContent ? envContent + "\n" : envContent}${envSessionPassword}=${password}`,
93
93
  "utf-8"
94
94
  );
95
95
  }
@@ -353,6 +353,30 @@ const module = defineNuxtModule({
353
353
  redirectURL: "",
354
354
  scope: ""
355
355
  });
356
+ runtimeConfig.oauth.roblox = defu(runtimeConfig.oauth.roblox, {
357
+ clientId: "",
358
+ clientSecret: "",
359
+ redirectURL: "",
360
+ scope: ""
361
+ });
362
+ runtimeConfig.oauth.okta = defu(runtimeConfig.oauth.okta, {
363
+ clientId: "",
364
+ clientSecret: "",
365
+ domain: "",
366
+ audience: "",
367
+ scope: [],
368
+ redirectURL: ""
369
+ });
370
+ runtimeConfig.oauth.ory = defu(runtimeConfig.oauth.ory, {
371
+ clientId: "",
372
+ clientSecret: "",
373
+ sdkURL: "",
374
+ redirectURL: "",
375
+ scope: [],
376
+ authorizationURL: "",
377
+ tokenURL: "",
378
+ userURL: ""
379
+ });
356
380
  }
357
381
  });
358
382
 
@@ -1,7 +1,7 @@
1
1
  import { eventHandler, getQuery, sendRedirect, createError } from "h3";
2
2
  import { withQuery } from "ufo";
3
3
  import { defu } from "defu";
4
- import { handleMissingConfiguration, handleAccessTokenErrorResponse, getOAuthRedirectURL, requestAccessToken } from "../utils.js";
4
+ import { getOAuthRedirectURL, handleAccessTokenErrorResponse, handleInvalidState, handleMissingConfiguration, handleState, requestAccessToken } from "../utils.js";
5
5
  import { useRuntimeConfig } from "#imports";
6
6
  export function defineOAuthGitHubEventHandler({ config, onSuccess, onError }) {
7
7
  return eventHandler(async (event) => {
@@ -25,6 +25,7 @@ export function defineOAuthGitHubEventHandler({ config, onSuccess, onError }) {
25
25
  return handleMissingConfiguration(event, "github", ["clientId", "clientSecret"], onError);
26
26
  }
27
27
  const redirectURL = config.redirectURL || getOAuthRedirectURL(event);
28
+ const state = await handleState(event);
28
29
  if (!query.code) {
29
30
  config.scope = config.scope || [];
30
31
  if (config.emailRequired && !config.scope.includes("user:email")) {
@@ -36,10 +37,14 @@ export function defineOAuthGitHubEventHandler({ config, onSuccess, onError }) {
36
37
  client_id: config.clientId,
37
38
  redirect_uri: redirectURL,
38
39
  scope: config.scope.join(" "),
40
+ state,
39
41
  ...config.authorizationParams
40
42
  })
41
43
  );
42
44
  }
45
+ if (query.state !== state) {
46
+ return handleInvalidState(event, "github", onError);
47
+ }
43
48
  const tokens = await requestAccessToken(config.tokenURL, {
44
49
  body: {
45
50
  grant_type: "authorization_code",
@@ -0,0 +1,88 @@
1
+ import type { H3Event } from 'h3';
2
+ import type { OAuthConfig } from '#auth-utils';
3
+ export interface OpenIdConfig {
4
+ authorization_endpoint: string;
5
+ token_endpoint: string;
6
+ userinfo_endpoint: string;
7
+ end_session_endpoint?: string;
8
+ [key: string]: unknown;
9
+ }
10
+ export interface OAuthConfigExt<TConfig, TResult = {
11
+ user: Record<string, unknown>;
12
+ tokens: Record<string, unknown>;
13
+ openIdConfig: OpenIdConfig;
14
+ }> extends OAuthConfig<TConfig, TResult> {
15
+ config?: TConfig;
16
+ onSuccess: (event: H3Event, result: TResult) => Promise<void> | void;
17
+ onError?: (event: H3Event, error: unknown) => Promise<void> | void;
18
+ }
19
+ export interface OAuthOktaConfig {
20
+ /**
21
+ * Okta OAuth Client ID
22
+ * @default process.env.NUXT_OAUTH_OKTA_CLIENT_ID
23
+ */
24
+ clientId?: string;
25
+ /**
26
+ * Okta OAuth Client Secret
27
+ * @default process.env.NUXT_OAUTH_OKTA_CLIENT_SECRET
28
+ */
29
+ clientSecret?: string;
30
+ /**
31
+ * Okta OAuth Issuer
32
+ * @default process.env.NUXT_OAUTH_OKTA_DOMAIN
33
+ */
34
+ domain?: string;
35
+ /**
36
+ * Okta OAuth Authorization Server
37
+ * @see https://developer.okta.com/docs/guides/customize-authz-server/main/#create-an-authorization-server
38
+ * @default process.env.NUXT_OAUTH_OKTA_AUTHORIZATION_SERVER
39
+ */
40
+ authorizationServer?: string;
41
+ /**
42
+ * Okta OAuth Audience
43
+ * @default process.env.NUXT_OAUTH_OKTA_AUDIENCE
44
+ */
45
+ audience?: string;
46
+ /**
47
+ * Okta OAuth Scope
48
+ * @default []
49
+ * @see https://okta.com/docs/get-started/apis/scopes/openid-connect-scopes
50
+ * @example ['openid']
51
+ */
52
+ scope?: string | string[];
53
+ /**
54
+ * Require email from user, adds the ['email'] scope if not present
55
+ * @default false
56
+ */
57
+ emailRequired?: boolean;
58
+ /**
59
+ * Maximum Authentication Age. If the elapsed time is greater than this value, the OP must attempt to actively re-authenticate the end-user.
60
+ * @default 0
61
+ * @see https://okta.com/docs/authenticate/login/max-age-reauthentication
62
+ */
63
+ maxAge?: number;
64
+ /**
65
+ * Login connection. If no connection is specified, it will redirect to the standard Okta login page and show the Login Widget.
66
+ * @default ''
67
+ * @see https://okta.com/docs/api/authentication#social
68
+ * @example 'github'
69
+ */
70
+ connection?: string;
71
+ /**
72
+ * Extra authorization parameters to provide to the authorization URL
73
+ * @see https://okta.com/docs/api/authentication#social
74
+ * @example { display: 'popup' }
75
+ */
76
+ authorizationParams?: Record<string, string>;
77
+ /**
78
+ * Redirect URL to to allow overriding for situations like prod failing to determine public hostname
79
+ * @default process.env.NUXT_OAUTH_OKTA_REDIRECT_URL or current URL
80
+ */
81
+ redirectURL?: string;
82
+ /**
83
+ * OpenID Configuration cache TTL in milliseconds
84
+ * @default 86400000 (24 hours)
85
+ */
86
+ openIdConfigCacheTTL?: number;
87
+ }
88
+ export declare function defineOAuthOktaEventHandler({ config, onSuccess, onError }: OAuthConfigExt<OAuthOktaConfig>): import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<void>>;
@@ -0,0 +1,171 @@
1
+ import { eventHandler, getQuery, sendRedirect, createError, deleteCookie } from "h3";
2
+ import { withQuery } from "ufo";
3
+ import { defu } from "defu";
4
+ import { handleMissingConfiguration, handleState, handleInvalidState, handleAccessTokenErrorResponse, getOAuthRedirectURL, requestAccessToken } from "../utils.js";
5
+ import { useRuntimeConfig } from "#imports";
6
+ const openIdConfigCache = /* @__PURE__ */ new Map();
7
+ const DEFAULT_CACHE_TTL = 1e3 * 60 * 60 * 24;
8
+ export function defineOAuthOktaEventHandler({ config, onSuccess, onError }) {
9
+ function normalizeScope(scope, emailRequired) {
10
+ let result;
11
+ if (!scope || typeof scope === "string" && scope.trim() === "") {
12
+ result = ["openid", "email", "profile"];
13
+ } else if (typeof scope === "string") {
14
+ result = scope.split(/[,| ]+/).filter(Boolean);
15
+ } else if (Array.isArray(scope) && scope.length > 0) {
16
+ result = scope;
17
+ } else {
18
+ result = ["openid", "email", "profile"];
19
+ }
20
+ result = [...new Set(result)];
21
+ if (emailRequired && !result.includes("email")) {
22
+ result.push("email");
23
+ }
24
+ return result;
25
+ }
26
+ return eventHandler(async (event) => {
27
+ const runtimeConfig = useRuntimeConfig(event);
28
+ config = defu(config, runtimeConfig.oauth?.okta, {
29
+ authorizationParams: {}
30
+ });
31
+ if (!config.clientId || !config.clientSecret || !config.domain || typeof config.domain !== "string" || !config.domain.trim()) {
32
+ return handleMissingConfiguration(event, "okta", ["clientId", "clientSecret", "domain"], onError);
33
+ }
34
+ const query = getQuery(event);
35
+ if (query.error) {
36
+ const error = createError({
37
+ statusCode: 401,
38
+ message: `Okta login failed: ${query.error || "Unknown error"} - ${query.error_description || ""}`,
39
+ data: query
40
+ });
41
+ if (!onError) throw error;
42
+ return onError(event, error);
43
+ }
44
+ config.scope = normalizeScope(config.scope, config.emailRequired);
45
+ const getOpenIdConfig = async (openIdConfigurationUrl2, event2) => {
46
+ const now = Date.now();
47
+ const cacheTTL = config?.openIdConfigCacheTTL || DEFAULT_CACHE_TTL;
48
+ const cached = openIdConfigCache.get(openIdConfigurationUrl2);
49
+ if (cached) {
50
+ if (cached.expiresAt > now) {
51
+ return cached.data;
52
+ } else {
53
+ openIdConfigCache.delete(openIdConfigurationUrl2);
54
+ }
55
+ }
56
+ let openIdConfig2 = null;
57
+ try {
58
+ openIdConfig2 = await $fetch(openIdConfigurationUrl2);
59
+ if (openIdConfig2) {
60
+ openIdConfigCache.set(openIdConfigurationUrl2, {
61
+ data: openIdConfig2,
62
+ expiresAt: now + cacheTTL
63
+ });
64
+ }
65
+ } catch (error) {
66
+ if (config && config.domain) {
67
+ const authz = config.authorizationServer;
68
+ openIdConfig2 = {
69
+ authorization_endpoint: authz ? `https://${config.domain}/oauth2/${authz}/v1/authorize` : `https://${config.domain}/oauth2/v1/authorize`,
70
+ token_endpoint: authz ? `https://${config.domain}/oauth2/${authz}/v1/token` : `https://${config.domain}/oauth2/v1/token`,
71
+ userinfo_endpoint: authz ? `https://${config.domain}/oauth2/${authz}/v1/userinfo` : `https://${config.domain}/oauth2/v1/userinfo`,
72
+ end_session_endpoint: void 0
73
+ };
74
+ openIdConfigCache.set(openIdConfigurationUrl2, {
75
+ data: openIdConfig2,
76
+ expiresAt: now + Math.min(cacheTTL / 24, 1e3 * 60 * 60)
77
+ // 1 hour or 1/24th of configured TTL, whichever is smaller
78
+ });
79
+ } else {
80
+ console.error("Failed to fetch Okta OpenID configuration. Please check your Okta domain and network connectivity:", error);
81
+ const err = createError({
82
+ statusCode: 500,
83
+ message: "Could not get Okta OpenID configuration. Please verify that your Okta domain is correct and reachable, and that the OpenID configuration endpoint is accessible.",
84
+ data: error
85
+ });
86
+ if (onError) await onError(event2, err);
87
+ throw err;
88
+ }
89
+ }
90
+ return openIdConfig2;
91
+ };
92
+ const authServer = config.authorizationServer;
93
+ const openIdConfigurationUrl = authServer ? `https://${config.domain}/oauth2/${authServer}/.well-known/openid-configuration` : `https://${config.domain}/.well-known/openid-configuration`;
94
+ const openIdConfig = await getOpenIdConfig(openIdConfigurationUrl, event);
95
+ const authorizationURL = openIdConfig.authorization_endpoint;
96
+ const tokenURL = openIdConfig.token_endpoint;
97
+ const userInfoUrl = openIdConfig.userinfo_endpoint;
98
+ const redirectURL = config.redirectURL || getOAuthRedirectURL(event);
99
+ const state = await handleState(event);
100
+ if (!query.code) {
101
+ return sendRedirect(
102
+ event,
103
+ withQuery(authorizationURL, {
104
+ response_type: "code",
105
+ client_id: config.clientId,
106
+ redirect_uri: redirectURL,
107
+ scope: config.scope.join(" "),
108
+ audience: config.audience || "",
109
+ max_age: config.maxAge || 0,
110
+ connection: config.connection || "",
111
+ state,
112
+ ...config.authorizationParams
113
+ })
114
+ );
115
+ }
116
+ if (query.state !== state) {
117
+ deleteCookie(event, "nuxt-auth-state");
118
+ return handleInvalidState(event, "okta", onError);
119
+ }
120
+ const tokens = await requestAccessToken(tokenURL, {
121
+ headers: {
122
+ "Content-Type": "application/x-www-form-urlencoded"
123
+ },
124
+ body: {
125
+ response_type: "code",
126
+ grant_type: "authorization_code",
127
+ client_id: config.clientId,
128
+ client_secret: config.clientSecret,
129
+ scope: config.scope?.join(" "),
130
+ redirect_uri: redirectURL,
131
+ code: query.code,
132
+ state: query.state
133
+ }
134
+ });
135
+ if (tokens.error) {
136
+ return handleAccessTokenErrorResponse(event, "okta", tokens, onError);
137
+ }
138
+ if (!tokens.access_token || typeof tokens.access_token !== "string" || !tokens.token_type || typeof tokens.token_type !== "string") {
139
+ const err = createError({
140
+ statusCode: 400,
141
+ message: "Invalid token response from Okta",
142
+ data: tokens
143
+ });
144
+ if (onError) return onError(event, err);
145
+ throw err;
146
+ }
147
+ const tokenType = tokens.token_type;
148
+ const accessToken = tokens.access_token;
149
+ let user;
150
+ try {
151
+ user = await $fetch(userInfoUrl, {
152
+ headers: {
153
+ Authorization: `${tokenType} ${accessToken}`
154
+ }
155
+ });
156
+ } catch (error) {
157
+ const err = createError({
158
+ statusCode: 410,
159
+ message: `Could not get Okta user info - ${error instanceof Error ? error.message : String(error)}`,
160
+ data: tokens
161
+ });
162
+ if (onError) return onError(event, err);
163
+ throw err;
164
+ }
165
+ return onSuccess(event, {
166
+ tokens,
167
+ user,
168
+ openIdConfig
169
+ });
170
+ });
171
+ }
@@ -0,0 +1,51 @@
1
+ import type { OAuthConfig } from '#auth-utils';
2
+ /**
3
+ * Ory OAuth2
4
+ * @see https://www.ory.sh/docs/oauth2-oidc/authorization-code-flow
5
+ */
6
+ export interface OAuthOryConfig {
7
+ /**
8
+ * Ory OAuth Client ID
9
+ * @default process.env.NUXT_OAUTH_ORY_CLIENT_ID
10
+ */
11
+ clientId?: string;
12
+ /**
13
+ * Ory OAuth Client Secret
14
+ * @default process.env.NUXT_OAUTH_ORY_CLIENT_SECRET
15
+ */
16
+ clientSecret?: string;
17
+ /**
18
+ * Ory OAuth SDK URL
19
+ * @default "https://playground.projects.oryapis.com" || process.env.NUXT_OAUTH_ORY_SDK_URL
20
+ */
21
+ sdkURL?: string;
22
+ /**
23
+ * Ory OAuth Scope
24
+ * @default ['openid', 'offline']
25
+ * @see https://www.ory.sh/docs/oauth2-oidc/openid-connect-claims-scope-custom
26
+ * @example ['openid', 'offline', 'email']
27
+ */
28
+ scope?: string[] | string;
29
+ /**
30
+ * Ory OAuth Authorization URL
31
+ * @default '/oauth2/auth'
32
+ */
33
+ authorizationURL?: string;
34
+ /**
35
+ * Ory OAuth Token URL
36
+ * @default '/oauth2/token'
37
+ */
38
+ tokenURL?: string;
39
+ /**
40
+ * Extra authorization parameters to provide to the authorization URL
41
+ * @example { allow_signup: 'true' }
42
+ */
43
+ authorizationParams?: Record<string, string>;
44
+ /**
45
+ * Ory OAuth Userinfo URL
46
+ * @see https://www.ory.sh/docs/oauth2-oidc/userinfo-oidc
47
+ * @default '/userinfo'
48
+ */
49
+ userURL?: string;
50
+ }
51
+ export declare function defineOAuthOryEventHandler({ config, onSuccess, onError }: OAuthConfig<OAuthOryConfig>): import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<void>>;
@@ -0,0 +1,101 @@
1
+ import { eventHandler, getQuery, sendRedirect } from "h3";
2
+ import { withQuery } from "ufo";
3
+ import { defu } from "defu";
4
+ import {
5
+ getOAuthRedirectURL,
6
+ handleAccessTokenErrorResponse,
7
+ handleInvalidState,
8
+ handleMissingConfiguration,
9
+ handlePkceVerifier,
10
+ handleState,
11
+ requestAccessToken
12
+ } from "../utils.js";
13
+ import { createError, useRuntimeConfig } from "#imports";
14
+ export function defineOAuthOryEventHandler({ config, onSuccess, onError }) {
15
+ return eventHandler(async (event) => {
16
+ config = defu(config, useRuntimeConfig(event).oauth?.ory, {
17
+ scope: ["openid", "offline"],
18
+ sdkURL: "https://playground.projects.oryapis.com",
19
+ authorizationURL: "/oauth2/auth",
20
+ tokenURL: "/oauth2/token",
21
+ userURL: "/userinfo",
22
+ authorizationParams: {}
23
+ });
24
+ const query = getQuery(event);
25
+ if (query.error) {
26
+ const error = createError({
27
+ statusCode: 401,
28
+ message: `Ory login failed: ${query.error || "Unknown error"}`,
29
+ data: query
30
+ });
31
+ if (!onError) throw error;
32
+ return onError(event, error);
33
+ }
34
+ if (!config.clientId || !config.sdkURL) {
35
+ return handleMissingConfiguration(event, "ory", ["clientId", "sdkURL"], onError);
36
+ }
37
+ const redirectURL = getOAuthRedirectURL(event);
38
+ if (Array.isArray(config.scope)) {
39
+ config.scope = Array.from(new Set(config.scope)).join(" ");
40
+ }
41
+ const verifier = await handlePkceVerifier(event);
42
+ const state = await handleState(event);
43
+ if (!query.code) {
44
+ const authorizationURL = `${config.sdkURL}${config.authorizationURL}`;
45
+ return sendRedirect(
46
+ event,
47
+ withQuery(authorizationURL, {
48
+ client_id: config.clientId,
49
+ response_type: "code",
50
+ redirect_uri: redirectURL,
51
+ scope: config.scope,
52
+ state,
53
+ code_challenge: verifier.code_challenge,
54
+ code_challenge_method: verifier.code_challenge_method,
55
+ ...config.authorizationParams
56
+ })
57
+ );
58
+ }
59
+ if (query.state !== state) {
60
+ handleInvalidState(event, "ory", onError);
61
+ }
62
+ const tokenURL = `${config.sdkURL}${config.tokenURL}`;
63
+ const tokens = await requestAccessToken(tokenURL, {
64
+ body: {
65
+ grant_type: "authorization_code",
66
+ client_id: config.clientId,
67
+ code: query.code,
68
+ redirect_uri: redirectURL,
69
+ scope: config.scope,
70
+ code_verifier: verifier.code_verifier
71
+ }
72
+ });
73
+ if (tokens.error) {
74
+ return handleAccessTokenErrorResponse(event, "ory", tokens, onError);
75
+ }
76
+ const tokenType = tokens.token_type;
77
+ const accessToken = tokens.access_token;
78
+ const userURL = `${config.sdkURL}${config.userURL}`;
79
+ const user = await $fetch(userURL, {
80
+ headers: {
81
+ "User-Agent": `Ory-${config.clientId}`,
82
+ "Authorization": `${tokenType} ${accessToken}`
83
+ }
84
+ }).catch((error) => {
85
+ return { error };
86
+ });
87
+ if (user.error) {
88
+ const error = createError({
89
+ statusCode: 401,
90
+ message: `Ory userinfo failed: ${user.error || "Unknown error"}`,
91
+ data: user
92
+ });
93
+ if (!onError) throw error;
94
+ return onError(event, error);
95
+ }
96
+ return onSuccess(event, {
97
+ tokens,
98
+ user
99
+ });
100
+ });
101
+ }
@@ -0,0 +1,126 @@
1
+ import type { OAuthConfig } from '#auth-utils';
2
+ export interface OAuthRobloxConfig {
3
+ /**
4
+ * Roblox OAuth Client ID
5
+ * @default process.env.NUXT_OAUTH_ROBLOX_CLIENT_ID
6
+ */
7
+ clientId?: string;
8
+ /**
9
+ * Roblox OAuth Client Secret
10
+ * @default process.env.NUXT_OAUTH_ROBLOX_CLIENT_SECRET
11
+ */
12
+ clientSecret?: string;
13
+ /**
14
+ * Roblox OAuth Scope
15
+ * Some scopes and claims listed are only available to official Roblox apps, e.g. email
16
+ * @default ['openid', 'profile']
17
+ * @see https://apis.roblox.com/oauth/.well-known/openid-configuration
18
+ * @example ['openid', 'profile', 'asset:read', 'universe-messaging-service:publish']
19
+ */
20
+ scope?: string[];
21
+ /**
22
+ * Roblox OAuth Authorization URL
23
+ * @default 'https://apis.roblox.com/oauth/v1/authorize'
24
+ */
25
+ authorizationURL?: string;
26
+ /**
27
+ * Roblox OAuth Token URL
28
+ * @default 'https://apis.roblox.com/oauth/v1/token'
29
+ */
30
+ tokenURL?: string;
31
+ /**
32
+ * Extra authorization parameters to provide to the authorization URL
33
+ * @see https://create.roblox.com/docs/cloud/auth/oauth2-reference#get-v1authorize
34
+ */
35
+ authorizationParams?: Record<string, string>;
36
+ /**
37
+ * Redirect URL to allow overriding for situations like prod failing to determine public hostname
38
+ * @default process.env.NUXT_OAUTH_ROBLOX_REDIRECT_URL or current URL
39
+ */
40
+ redirectURL?: string;
41
+ }
42
+ export interface OAuthRobloxUser {
43
+ /**
44
+ * The resource path of the user
45
+ * @example "users/123"
46
+ */
47
+ path: string;
48
+ /**
49
+ * The timestamp at which the user was created
50
+ * @readonly
51
+ * @example "2023-07-05T12:34:56Z"
52
+ */
53
+ createTime: string;
54
+ /**
55
+ * Unique ID that identifies a user in Roblox
56
+ * @readonly
57
+ * @example "123456"
58
+ */
59
+ id: string;
60
+ /**
61
+ * Unique username for a user in Roblox
62
+ * @example "exampleUser"
63
+ */
64
+ name: string;
65
+ /**
66
+ * Display name for the user
67
+ * @example "userDefinedName"
68
+ */
69
+ displayName: string;
70
+ /**
71
+ * User-defined information about themselves
72
+ * @example "Example User's bio"
73
+ */
74
+ about: string;
75
+ /**
76
+ * Current locale selected by the user as an IETF language code
77
+ * @example "en-US"
78
+ */
79
+ locale: string;
80
+ /**
81
+ * Whether the user is a premium user
82
+ * @readonly
83
+ * @example true
84
+ */
85
+ premium: boolean;
86
+ /**
87
+ * Specifies if the user is identity-verified
88
+ * Verification includes, but isn't limited to, non-VoIP phone numbers or government IDs
89
+ * Available only with the user.advanced:read scope
90
+ * @readonly
91
+ * @example true
92
+ */
93
+ idVerified: boolean;
94
+ /**
95
+ * User's social network profiles and visibility.
96
+ */
97
+ socialNetworkProfiles: {
98
+ /**
99
+ * Facebook profile URI.
100
+ */
101
+ facebook?: string;
102
+ /**
103
+ * Twitter profile URI.
104
+ */
105
+ twitter?: string;
106
+ /**
107
+ * YouTube profile URI.
108
+ */
109
+ youtube?: string;
110
+ /**
111
+ * Twitch profile URI.
112
+ */
113
+ twitch?: string;
114
+ /**
115
+ * Guilded profile URI.
116
+ */
117
+ guilded?: string;
118
+ /**
119
+ * Visibility of the social network profiles.
120
+ * Available only with the user.social:read scope
121
+ * @example "SOCIAL_NETWORK_VISIBILITY_UNSPECIFIED"
122
+ */
123
+ visibility: 'SOCIAL_NETWORK_VISIBILITY_UNSPECIFIED' | 'NO_ONE' | 'FRIENDS' | 'FRIENDS_AND_FOLLOWING' | 'FRIENDS_FOLLOWING_AND_FOLLOWERS' | 'EVERYONE';
124
+ };
125
+ }
126
+ export declare function defineOAuthRobloxEventHandler({ config, onSuccess, onError }: OAuthConfig<OAuthRobloxConfig>): import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<void>>;
@@ -0,0 +1,64 @@
1
+ import { eventHandler, getQuery, sendRedirect } from "h3";
2
+ import { withQuery } from "ufo";
3
+ import { defu } from "defu";
4
+ import { handleMissingConfiguration, handleAccessTokenErrorResponse, getOAuthRedirectURL, requestAccessToken } from "../utils.js";
5
+ import { useRuntimeConfig } from "#imports";
6
+ export function defineOAuthRobloxEventHandler({ config, onSuccess, onError }) {
7
+ return eventHandler(async (event) => {
8
+ config = defu(config, useRuntimeConfig(event).oauth?.roblox, {
9
+ authorizationURL: "https://apis.roblox.com/oauth/v1/authorize",
10
+ tokenURL: "https://apis.roblox.com/oauth/v1/token",
11
+ authorizationParams: {}
12
+ });
13
+ const query = getQuery(event);
14
+ if (!config.clientId || !config.clientSecret) {
15
+ return handleMissingConfiguration(event, "roblox", ["clientId", "clientSecret"], onError);
16
+ }
17
+ if (query.error) {
18
+ return handleAccessTokenErrorResponse(event, "roblox", query, onError);
19
+ }
20
+ const redirectURL = config.redirectURL || getOAuthRedirectURL(event);
21
+ if (!query.code) {
22
+ config.scope = config.scope || [];
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
+ ...config.authorizationParams
31
+ })
32
+ );
33
+ }
34
+ const tokens = await requestAccessToken(config.tokenURL, {
35
+ body: {
36
+ client_id: config.clientId,
37
+ client_secret: config.clientSecret,
38
+ grant_type: "authorization_code",
39
+ redirect_uri: redirectURL,
40
+ code: query.code
41
+ }
42
+ });
43
+ if (tokens.error) {
44
+ return handleAccessTokenErrorResponse(event, "roblox", tokens, onError);
45
+ }
46
+ const accessToken = tokens.access_token;
47
+ const userInfo = await $fetch("https://apis.roblox.com/oauth/userinfo", {
48
+ headers: {
49
+ "user-agent": "Nuxt Auth Utils",
50
+ "Authorization": `Bearer ${accessToken}`
51
+ }
52
+ });
53
+ const user = await $fetch(`https://apis.roblox.com/cloud/v2/users/${userInfo.sub}`, {
54
+ headers: {
55
+ "user-agent": "Nuxt Auth Utils",
56
+ "Authorization": `Bearer ${accessToken}`
57
+ }
58
+ });
59
+ return onSuccess(event, {
60
+ tokens,
61
+ user
62
+ });
63
+ });
64
+ }
@@ -1,7 +1,7 @@
1
1
  import { eventHandler, getQuery, sendRedirect } from "h3";
2
2
  import { withQuery } from "ufo";
3
3
  import { defu } from "defu";
4
- import { handleMissingConfiguration, handleAccessTokenErrorResponse, getOAuthRedirectURL, requestAccessToken } from "../utils.js";
4
+ import { handleMissingConfiguration, handleAccessTokenErrorResponse, getOAuthRedirectURL, requestAccessToken, handleState, handlePkceVerifier, handleInvalidState } from "../utils.js";
5
5
  import { useRuntimeConfig, createError } from "#imports";
6
6
  export function defineOAuthZitadelEventHandler({ config, onSuccess, onError }) {
7
7
  return eventHandler(async (event) => {
@@ -18,12 +18,14 @@ export function defineOAuthZitadelEventHandler({ config, onSuccess, onError }) {
18
18
  if (!onError) throw error;
19
19
  return onError(event, error);
20
20
  }
21
- if (!config.clientId || !config.clientSecret || !config.domain) {
22
- return handleMissingConfiguration(event, "zitadel", ["clientId", "clientSecret", "issuerUrl"], onError);
21
+ if (!config.clientId || !config.domain) {
22
+ return handleMissingConfiguration(event, "zitadel", ["clientId", "domain"], onError);
23
23
  }
24
24
  const authorizationURL = `https://${config.domain}/oauth/v2/authorize`;
25
25
  const tokenURL = `https://${config.domain}/oauth/v2/token`;
26
26
  const redirectURL = config.redirectURL || getOAuthRedirectURL(event);
27
+ const verifier = await handlePkceVerifier(event);
28
+ const state = await handleState(event);
27
29
  if (!query.code) {
28
30
  config.scope = config.scope || ["openid"];
29
31
  return sendRedirect(
@@ -33,22 +35,33 @@ export function defineOAuthZitadelEventHandler({ config, onSuccess, onError }) {
33
35
  client_id: config.clientId,
34
36
  redirect_uri: redirectURL,
35
37
  scope: config.scope.join(" "),
38
+ state,
39
+ code_challenge: verifier.code_challenge,
40
+ code_challenge_method: verifier.code_challenge_method,
36
41
  ...config.authorizationParams
37
42
  })
38
43
  );
39
44
  }
40
- const tokens = await requestAccessToken(tokenURL, {
41
- headers: {
42
- "Authorization": `Basic ${Buffer.from(`${config.clientId}:${config.clientSecret}`).toString("base64")}`,
43
- "Content-Type": "application/x-www-form-urlencoded"
44
- },
45
+ if (query.state !== state) {
46
+ return handleInvalidState(event, "zitadel", onError);
47
+ }
48
+ const request = {
45
49
  body: {
46
50
  grant_type: "authorization_code",
47
51
  client_id: config.clientId,
48
52
  redirect_uri: redirectURL,
49
- code: query.code
53
+ code: query.code,
54
+ code_verifier: verifier.code_verifier
50
55
  }
51
- });
56
+ };
57
+ if (config.clientSecret) {
58
+ const basicAuthorization = Buffer.from(`${config.clientId}:${config.clientSecret}`).toString("base64");
59
+ request.headers = {
60
+ "Authorization": `Basic ${basicAuthorization}`,
61
+ "Content-Type": "application/x-www-form-urlencoded"
62
+ };
63
+ }
64
+ const tokens = await requestAccessToken(tokenURL, request);
52
65
  if (tokens.error) {
53
66
  return handleAccessTokenErrorResponse(event, "zitadel", tokens, onError);
54
67
  }
@@ -14,7 +14,7 @@ export interface RequestAccessTokenBody {
14
14
  client_secret?: string;
15
15
  [key: string]: string | undefined;
16
16
  }
17
- interface RequestAccessTokenOptions {
17
+ export interface RequestAccessTokenOptions {
18
18
  body?: RequestAccessTokenBody;
19
19
  params?: Record<string, string | undefined>;
20
20
  headers?: Record<string, string>;
@@ -49,6 +49,9 @@ function _useSession(event, config = {}) {
49
49
  const runtimeConfig = useRuntimeConfig(isEvent(event) ? event : void 0);
50
50
  const envSessionPassword = `${runtimeConfig.nitro?.envPrefix || "NUXT_"}SESSION_PASSWORD`;
51
51
  sessionConfig = defu({ password: process.env[envSessionPassword] }, runtimeConfig.session);
52
+ if (!sessionConfig.password) {
53
+ console.error(`[nuxt-auth-utils] ${envSessionPassword} environment variable or runtimeConfig.session.password was not set.`);
54
+ }
52
55
  }
53
56
  const finalConfig = defu(config, sessionConfig);
54
57
  return useSession(event, finalConfig);
@@ -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' | (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' | (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.21",
3
+ "version": "0.5.23",
4
4
  "description": "Add Authentication to Nuxt applications with secured & sealed cookies sessions.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -35,15 +35,15 @@
35
35
  "test:watch": "vitest watch"
36
36
  },
37
37
  "dependencies": {
38
- "@adonisjs/hash": "^9.0.5",
39
- "@nuxt/kit": "^3.15.4",
38
+ "@adonisjs/hash": "^9.1.1",
39
+ "@nuxt/kit": "^3.18.1",
40
40
  "defu": "^6.1.4",
41
- "h3": "^1.14.0",
41
+ "h3": "^1.15.4",
42
42
  "hookable": "^5.5.3",
43
- "jose": "^5.9.6",
43
+ "jose": "^5.10.0",
44
44
  "ofetch": "^1.4.1",
45
- "openid-client": "^6.1.7",
46
- "pathe": "^2.0.2",
45
+ "openid-client": "^6.6.2",
46
+ "pathe": "^2.0.3",
47
47
  "scule": "^1.3.0",
48
48
  "uncrypto": "^0.1.3"
49
49
  },
@@ -68,7 +68,7 @@
68
68
  }
69
69
  },
70
70
  "devDependencies": {
71
- "@iconify-json/simple-icons": "^1.2.23",
71
+ "@iconify-json/simple-icons": "^1.2.46",
72
72
  "@nuxt/devtools": "2.0.0-beta.7",
73
73
  "@nuxt/eslint-config": "^0.7.6",
74
74
  "@nuxt/module-builder": "^0.8.4",
@@ -78,10 +78,10 @@
78
78
  "@nuxt/ui-pro": "^1.7.0",
79
79
  "@simplewebauthn/types": "^11.0.0",
80
80
  "changelogen": "^0.5.7",
81
- "eslint": "^9.19.0",
82
- "nuxt": "^3.15.4",
81
+ "eslint": "^9.32.0",
82
+ "nuxt": "^3.18.1",
83
83
  "typescript": "5.6.3",
84
84
  "vitest": "^3.0.5",
85
- "vue-tsc": "^2.2.0"
85
+ "vue-tsc": "^2.2.12"
86
86
  }
87
87
  }