nuxt-auth-utils 0.5.31 → 0.5.33
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 +55 -4
- package/dist/module.json +1 -1
- package/dist/module.mjs +19 -1
- package/dist/runtime/app/composables/session.js +8 -3
- package/dist/runtime/app/plugins/session.client.js +8 -4
- package/dist/runtime/server/lib/oauth/apple.js +4 -1
- package/dist/runtime/server/lib/oauth/atlassian.js +1 -1
- package/dist/runtime/server/lib/oauth/auth0.js +1 -1
- package/dist/runtime/server/lib/oauth/authentik.js +1 -1
- package/dist/runtime/server/lib/oauth/azureb2c.js +1 -1
- package/dist/runtime/server/lib/oauth/battledotnet.js +1 -1
- package/dist/runtime/server/lib/oauth/box.js +1 -1
- package/dist/runtime/server/lib/oauth/cloudflare.d.ts +86 -0
- package/dist/runtime/server/lib/oauth/cloudflare.js +72 -0
- package/dist/runtime/server/lib/oauth/cognito.js +1 -1
- package/dist/runtime/server/lib/oauth/discord.js +1 -1
- package/dist/runtime/server/lib/oauth/dropbox.js +1 -1
- package/dist/runtime/server/lib/oauth/facebook.d.ts +13 -2
- package/dist/runtime/server/lib/oauth/facebook.js +16 -4
- package/dist/runtime/server/lib/oauth/gitea.js +1 -1
- package/dist/runtime/server/lib/oauth/github.js +1 -1
- package/dist/runtime/server/lib/oauth/gitlab.js +1 -1
- package/dist/runtime/server/lib/oauth/google.js +1 -1
- package/dist/runtime/server/lib/oauth/heroku.js +1 -1
- package/dist/runtime/server/lib/oauth/hubspot.js +1 -1
- package/dist/runtime/server/lib/oauth/instagram.js +1 -1
- package/dist/runtime/server/lib/oauth/keycloak.js +1 -1
- package/dist/runtime/server/lib/oauth/kick.js +1 -1
- package/dist/runtime/server/lib/oauth/line.js +1 -1
- package/dist/runtime/server/lib/oauth/linear.js +1 -1
- package/dist/runtime/server/lib/oauth/linkedin.js +1 -1
- package/dist/runtime/server/lib/oauth/livechat.js +1 -1
- package/dist/runtime/server/lib/oauth/mastodon.d.ts +62 -0
- package/dist/runtime/server/lib/oauth/mastodon.js +178 -0
- package/dist/runtime/server/lib/oauth/microsoft.js +1 -1
- package/dist/runtime/server/lib/oauth/oidc.js +5 -4
- package/dist/runtime/server/lib/oauth/okta.js +1 -1
- package/dist/runtime/server/lib/oauth/ory.js +1 -1
- package/dist/runtime/server/lib/oauth/osu.js +1 -1
- package/dist/runtime/server/lib/oauth/paypal.js +1 -1
- package/dist/runtime/server/lib/oauth/polar.js +1 -1
- package/dist/runtime/server/lib/oauth/riotgames.js +1 -1
- package/dist/runtime/server/lib/oauth/roblox.js +1 -1
- package/dist/runtime/server/lib/oauth/salesforce.d.ts +8 -3
- package/dist/runtime/server/lib/oauth/salesforce.js +9 -5
- package/dist/runtime/server/lib/oauth/seznam.js +1 -1
- package/dist/runtime/server/lib/oauth/shopifyCustomer.js +2 -2
- package/dist/runtime/server/lib/oauth/slack.js +1 -1
- package/dist/runtime/server/lib/oauth/spotify.js +1 -1
- package/dist/runtime/server/lib/oauth/steam.js +1 -1
- package/dist/runtime/server/lib/oauth/strava.js +1 -1
- package/dist/runtime/server/lib/oauth/tiktok.js +1 -1
- package/dist/runtime/server/lib/oauth/twitch.js +1 -1
- package/dist/runtime/server/lib/oauth/vk.js +1 -1
- package/dist/runtime/server/lib/oauth/wechat.d.ts +67 -0
- package/dist/runtime/server/lib/oauth/wechat.js +107 -0
- package/dist/runtime/server/lib/oauth/workos.js +1 -1
- package/dist/runtime/server/lib/oauth/x.js +1 -1
- package/dist/runtime/server/lib/oauth/xsuaa.js +1 -1
- package/dist/runtime/server/lib/oauth/yandex.js +1 -1
- package/dist/runtime/server/lib/oauth/zitadel.js +1 -1
- package/dist/runtime/server/lib/utils.d.ts +1 -0
- package/dist/runtime/server/lib/utils.js +3 -3
- package/dist/runtime/server/utils/session.js +6 -1
- package/dist/runtime/types/oauth-config.d.ts +1 -1
- package/package.json +12 -16
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
Add Authentication to Nuxt applications with secured & sealed cookies sessions.
|
|
9
9
|
|
|
10
|
-
- [Release Notes](/
|
|
10
|
+
- [Release Notes](https://github.com/atinux/nuxt-auth-utils/releases)
|
|
11
11
|
- [Demo with OAuth](https://github.com/atinux/atidone)
|
|
12
12
|
- [Demo with Passkeys](https://github.com/atinux/todo-passkeys)
|
|
13
13
|
<!-- - [🏀 Online playground](https://stackblitz.com/github/your-org/nuxt-auth-utils?file=playground%2Fapp.vue) -->
|
|
@@ -220,6 +220,7 @@ It can also be set using environment variables:
|
|
|
220
220
|
- Battle.net
|
|
221
221
|
- Bluesky (AT Protocol)
|
|
222
222
|
- Box.com
|
|
223
|
+
- Cloudflare
|
|
223
224
|
- Discord
|
|
224
225
|
- Dropbox
|
|
225
226
|
- Facebook
|
|
@@ -236,6 +237,7 @@ It can also be set using environment variables:
|
|
|
236
237
|
- Linear
|
|
237
238
|
- LinkedIn
|
|
238
239
|
- LiveChat
|
|
240
|
+
- Mastodon
|
|
239
241
|
- Microsoft
|
|
240
242
|
- OIDC / OpenID Connect (Generic)
|
|
241
243
|
- Okta
|
|
@@ -254,6 +256,7 @@ It can also be set using environment variables:
|
|
|
254
256
|
- TikTok
|
|
255
257
|
- Twitch
|
|
256
258
|
- VK
|
|
259
|
+
- WeChat
|
|
257
260
|
- WorkOS
|
|
258
261
|
- X (Twitter)
|
|
259
262
|
- XSUAA
|
|
@@ -291,6 +294,34 @@ Make sure to set the callback URL in your OAuth app settings as `<your-domain>/a
|
|
|
291
294
|
|
|
292
295
|
If the redirect URL mismatch in production, this means that the module cannot guess the right redirect URL. You can set the `NUXT_OAUTH_<PROVIDER>_REDIRECT_URL` env variable to overwrite the default one.
|
|
293
296
|
|
|
297
|
+
#### Mastodon
|
|
298
|
+
|
|
299
|
+
Mastodon does not have a single shared OAuth server: every account lives on its own instance (e.g. `mastodon.social`, `h4.io`), and there is no portable identity across instances the way there is with AT Protocol/Bluesky. Because of this, the provider needs to know which instance to talk to before it can start the flow, and it registers an application against that instance on first use via `POST /api/v1/apps` (Mastodon's own API, not the RFC 7591 Dynamic Client Registration spec it superficially resembles).
|
|
300
|
+
|
|
301
|
+
```ts
|
|
302
|
+
// server/routes/auth/mastodon.get.ts
|
|
303
|
+
export default defineOAuthMastodonEventHandler({
|
|
304
|
+
async onSuccess(event, { user }) {
|
|
305
|
+
await setUserSession(event, {
|
|
306
|
+
user: {
|
|
307
|
+
mastodon: user.acct,
|
|
308
|
+
},
|
|
309
|
+
})
|
|
310
|
+
return sendRedirect(event, '/')
|
|
311
|
+
},
|
|
312
|
+
})
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
The instance can be provided either as a static `instance` config/`NUXT_OAUTH_MASTODON_INSTANCE` env variable, or per-request as an `instance` query parameter, e.g. `/auth/mastodon?instance=mastodon.social`:
|
|
316
|
+
|
|
317
|
+
```ts
|
|
318
|
+
navigateTo({ path: '/auth/mastodon', query: { instance: 'mastodon.social' } }, { external: true })
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
A full handle (`@user@mastodon.social`) is also accepted and only the instance part is used, since that's all that is required to start the OAuth flow.
|
|
322
|
+
|
|
323
|
+
Registered apps are cached in memory per instance and redirect URL for the lifetime of the server process, so it does not re-register on every login. The callback doesn't depend on this cache, so after a restart or on another worker, the next login only registers a new application.
|
|
324
|
+
|
|
294
325
|
### Password Hashing
|
|
295
326
|
|
|
296
327
|
Nuxt Auth Utils provides password hashing utilities like `hashPassword` and `verifyPassword` to hash and verify passwords by using [scrypt](https://en.wikipedia.org/wiki/Scrypt) as it is supported in many JS runtime.
|
|
@@ -778,6 +809,27 @@ await setUserSession(event, { ... } , {
|
|
|
778
809
|
|
|
779
810
|
Checkout the [`SessionConfig`](https://github.com/unjs/h3/blob/c04c458810e34eb15c1647e1369e7d7ef19f567d/src/utils/session.ts#L20) for all options.
|
|
780
811
|
|
|
812
|
+
## Shared session between microservices and subdomains
|
|
813
|
+
|
|
814
|
+
To share a session between multiple applications, use the same `NUXT_SESSION_PASSWORD` and session `name` in each of them.
|
|
815
|
+
|
|
816
|
+
If the applications run on different subdomains, also set the session cookie `domain` to the parent domain in each `nuxt.config.ts`:
|
|
817
|
+
|
|
818
|
+
```ts
|
|
819
|
+
export default defineNuxtConfig({
|
|
820
|
+
modules: ['nuxt-auth-utils'],
|
|
821
|
+
runtimeConfig: {
|
|
822
|
+
session: {
|
|
823
|
+
cookie: {
|
|
824
|
+
domain: '.example.com'
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
})
|
|
829
|
+
```
|
|
830
|
+
|
|
831
|
+
The browser then sends the session cookie to every subdomain of `example.com`, and each application can decrypt it.
|
|
832
|
+
|
|
781
833
|
## More
|
|
782
834
|
|
|
783
835
|
- [nuxt-authorization](https://github.com/barbapapazes/nuxt-authorization): Authorization module for managing permissions inside a Nuxt app, compatible with `nuxt-auth-utils`
|
|
@@ -803,11 +855,10 @@ pnpm run lint
|
|
|
803
855
|
# Run Vitest
|
|
804
856
|
pnpm run test
|
|
805
857
|
pnpm run test:watch
|
|
806
|
-
|
|
807
|
-
# Release new version
|
|
808
|
-
pnpm run release
|
|
809
858
|
```
|
|
810
859
|
|
|
860
|
+
Releases are automated with [uppt](https://github.com/danielroe/uppt): every push to `main` opens or updates a draft release PR from conventional commits. Merging it tags the version, creates the GitHub release and stages the npm publish, which is then approved on npmjs.com.
|
|
861
|
+
|
|
811
862
|
<!-- Badges -->
|
|
812
863
|
[npm-version-src]: https://img.shields.io/npm/v/nuxt-auth-utils/latest.svg?style=flat&colorA=020420&colorB=00DC82
|
|
813
864
|
[npm-version-href]: https://npmjs.com/package/nuxt-auth-utils
|
package/dist/module.json
CHANGED
package/dist/module.mjs
CHANGED
|
@@ -191,6 +191,11 @@ const module$1 = defineNuxtModule({
|
|
|
191
191
|
clientSecret: "",
|
|
192
192
|
redirectURL: ""
|
|
193
193
|
});
|
|
194
|
+
runtimeConfig.oauth.cloudflare = defu(runtimeConfig.oauth.cloudflare, {
|
|
195
|
+
clientId: "",
|
|
196
|
+
clientSecret: "",
|
|
197
|
+
redirectURL: ""
|
|
198
|
+
});
|
|
194
199
|
for (const provider of atprotoProviders) {
|
|
195
200
|
runtimeConfig.oauth[provider] = defu(runtimeConfig.oauth[provider], atprotoProviderDefaultClientMetadata);
|
|
196
201
|
}
|
|
@@ -235,6 +240,11 @@ const module$1 = defineNuxtModule({
|
|
|
235
240
|
clientSecret: "",
|
|
236
241
|
redirectURL: ""
|
|
237
242
|
});
|
|
243
|
+
runtimeConfig.oauth.mastodon = defu(runtimeConfig.oauth.mastodon, {
|
|
244
|
+
instance: "",
|
|
245
|
+
clientName: "",
|
|
246
|
+
redirectURL: ""
|
|
247
|
+
});
|
|
238
248
|
runtimeConfig.oauth.cognito = defu(runtimeConfig.oauth.cognito, {
|
|
239
249
|
clientId: "",
|
|
240
250
|
clientSecret: "",
|
|
@@ -245,7 +255,8 @@ const module$1 = defineNuxtModule({
|
|
|
245
255
|
runtimeConfig.oauth.facebook = defu(runtimeConfig.oauth.facebook, {
|
|
246
256
|
clientId: "",
|
|
247
257
|
clientSecret: "",
|
|
248
|
-
redirectURL: ""
|
|
258
|
+
redirectURL: "",
|
|
259
|
+
apiVersion: "v25.0"
|
|
249
260
|
});
|
|
250
261
|
runtimeConfig.oauth.instagram = defu(runtimeConfig.oauth.instagram, {
|
|
251
262
|
clientId: "",
|
|
@@ -329,6 +340,12 @@ const module$1 = defineNuxtModule({
|
|
|
329
340
|
clientSecret: "",
|
|
330
341
|
redirectURL: ""
|
|
331
342
|
});
|
|
343
|
+
runtimeConfig.oauth.wechat = defu(runtimeConfig.oauth.wechat, {
|
|
344
|
+
appId: "",
|
|
345
|
+
appSecret: "",
|
|
346
|
+
redirectURL: "",
|
|
347
|
+
scope: []
|
|
348
|
+
});
|
|
332
349
|
runtimeConfig.oauth.atlassian = defu(runtimeConfig.oauth.atlassian, {
|
|
333
350
|
clientId: "",
|
|
334
351
|
clientSecret: "",
|
|
@@ -355,6 +372,7 @@ const module$1 = defineNuxtModule({
|
|
|
355
372
|
clientSecret: "",
|
|
356
373
|
redirectURL: "",
|
|
357
374
|
baseURL: "",
|
|
375
|
+
userURL: "",
|
|
358
376
|
scope: ""
|
|
359
377
|
});
|
|
360
378
|
runtimeConfig.oauth.slack = defu(runtimeConfig.oauth.slack, {
|
|
@@ -4,8 +4,9 @@ export function useUserSession() {
|
|
|
4
4
|
const serverEvent = import.meta.server ? useRequestEvent() : null;
|
|
5
5
|
const sessionState = useState("nuxt-session", () => null);
|
|
6
6
|
const authReadyState = useState("nuxt-auth-ready", () => false);
|
|
7
|
+
const requestFetch = useRequestFetch();
|
|
7
8
|
const clear = async () => {
|
|
8
|
-
await
|
|
9
|
+
await requestFetch("/api/_auth/session", {
|
|
9
10
|
method: "DELETE",
|
|
10
11
|
onResponse({ response: { headers } }) {
|
|
11
12
|
if (import.meta.server && serverEvent) {
|
|
@@ -18,7 +19,7 @@ export function useUserSession() {
|
|
|
18
19
|
sessionState.value = null;
|
|
19
20
|
};
|
|
20
21
|
const fetch = async () => {
|
|
21
|
-
sessionState.value = await
|
|
22
|
+
sessionState.value = await requestFetch("/api/_auth/session", {
|
|
22
23
|
headers: {
|
|
23
24
|
accept: "application/json"
|
|
24
25
|
},
|
|
@@ -35,7 +36,11 @@ export function useUserSession() {
|
|
|
35
36
|
}
|
|
36
37
|
};
|
|
37
38
|
const openInPopup = (route, size = {}) => {
|
|
38
|
-
|
|
39
|
+
try {
|
|
40
|
+
localStorage.setItem("temp-nuxt-auth-utils-popup", "true");
|
|
41
|
+
} catch (error) {
|
|
42
|
+
console.error("[nuxt-auth-utils] Could not use localStorage during popup flow initialization.", error);
|
|
43
|
+
}
|
|
39
44
|
const width = size.width ?? 960;
|
|
40
45
|
const height = size.height ?? 600;
|
|
41
46
|
const top = (window.top?.outerHeight ?? 0) / 2 + (window.top?.screenY ?? 0) - height / 2;
|
|
@@ -7,9 +7,13 @@ export default defineNuxtPlugin(async (nuxtApp) => {
|
|
|
7
7
|
await useUserSession().fetch();
|
|
8
8
|
});
|
|
9
9
|
}
|
|
10
|
-
|
|
11
|
-
localStorage.
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+
try {
|
|
11
|
+
if (localStorage.getItem("temp-nuxt-auth-utils-popup")) {
|
|
12
|
+
localStorage.removeItem("temp-nuxt-auth-utils-popup");
|
|
13
|
+
const error = useError();
|
|
14
|
+
if (!error.value) window.close();
|
|
15
|
+
}
|
|
16
|
+
} catch (error) {
|
|
17
|
+
console.error("[nuxt-auth-utils] Could not use localStorage during popup flow last step. Popup won't be closed automatically.", error);
|
|
14
18
|
}
|
|
15
19
|
});
|
|
@@ -44,7 +44,7 @@ export function defineOAuthAppleEventHandler({
|
|
|
44
44
|
})
|
|
45
45
|
);
|
|
46
46
|
}
|
|
47
|
-
if (state !== storedState) {
|
|
47
|
+
if (!storedState || state !== storedState) {
|
|
48
48
|
return handleInvalidState(event, "apple", onError);
|
|
49
49
|
}
|
|
50
50
|
try {
|
|
@@ -71,6 +71,9 @@ export function defineOAuthAppleEventHandler({
|
|
|
71
71
|
redirect_uri: config.redirectURL
|
|
72
72
|
}
|
|
73
73
|
});
|
|
74
|
+
if (accessTokenResult.error) {
|
|
75
|
+
return handleAccessTokenErrorResponse(event, "apple", accessTokenResult, onError);
|
|
76
|
+
}
|
|
74
77
|
const payload = await verifyJwt(accessTokenResult.id_token, {
|
|
75
78
|
publicJwkUrl: "https://appleid.apple.com/auth/keys",
|
|
76
79
|
audience: config.clientId,
|
|
@@ -52,7 +52,7 @@ export function defineOAuthAtlassianEventHandler({
|
|
|
52
52
|
if (!onError) throw error;
|
|
53
53
|
return onError(event, error);
|
|
54
54
|
}
|
|
55
|
-
if (query.state !== state) {
|
|
55
|
+
if (!state || query.state !== state) {
|
|
56
56
|
return handleInvalidState(event, "atlassian", onError);
|
|
57
57
|
}
|
|
58
58
|
const tokens = await requestAccessToken(config.tokenURL, {
|
|
@@ -47,7 +47,7 @@ export function defineOAuthAuth0EventHandler({ config, onSuccess, onError }) {
|
|
|
47
47
|
})
|
|
48
48
|
);
|
|
49
49
|
}
|
|
50
|
-
if (query.state !== state) {
|
|
50
|
+
if (!state || query.state !== state) {
|
|
51
51
|
return handleInvalidState(event, "auth0", onError);
|
|
52
52
|
}
|
|
53
53
|
const tokens = await requestAccessToken(tokenURL, {
|
|
@@ -35,7 +35,7 @@ export function defineOAuthAuthentikEventHandler({ config, onSuccess, onError })
|
|
|
35
35
|
})
|
|
36
36
|
);
|
|
37
37
|
}
|
|
38
|
-
if (query.state !== state) {
|
|
38
|
+
if (!state || query.state !== state) {
|
|
39
39
|
return handleInvalidState(event, "authentik", onError);
|
|
40
40
|
}
|
|
41
41
|
const tokens = await requestAccessToken(tokenURL, {
|
|
@@ -34,7 +34,7 @@ export function defineOAuthAzureB2CEventHandler({ config, onSuccess, onError })
|
|
|
34
34
|
})
|
|
35
35
|
);
|
|
36
36
|
}
|
|
37
|
-
if (query.state !== state) {
|
|
37
|
+
if (!state || query.state !== state) {
|
|
38
38
|
return handleInvalidState(event, "azureb2c", onError);
|
|
39
39
|
}
|
|
40
40
|
const tokens = await requestAccessToken(tokenURL, {
|
|
@@ -49,7 +49,7 @@ export function defineOAuthBattledotnetEventHandler({ config, onSuccess, onError
|
|
|
49
49
|
})
|
|
50
50
|
);
|
|
51
51
|
}
|
|
52
|
-
if (query.state !== state) {
|
|
52
|
+
if (!state || query.state !== state) {
|
|
53
53
|
return handleInvalidState(event, "battledotnet", onError);
|
|
54
54
|
}
|
|
55
55
|
config.scope = config.scope || [];
|
|
@@ -41,7 +41,7 @@ export function defineOAuthBoxEventHandler({ config, onSuccess, onError }) {
|
|
|
41
41
|
})
|
|
42
42
|
);
|
|
43
43
|
}
|
|
44
|
-
if (query.state !== state) {
|
|
44
|
+
if (!state || query.state !== state) {
|
|
45
45
|
return handleInvalidState(event, "box", onError);
|
|
46
46
|
}
|
|
47
47
|
const tokens = await requestAccessToken(config.tokenURL, {
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import type { OAuthConfig } from '#auth-utils';
|
|
2
|
+
/**
|
|
3
|
+
* Cloudflare OAuth provider — Authorization Code flow (the only flow Cloudflare supports).
|
|
4
|
+
*
|
|
5
|
+
* References:
|
|
6
|
+
* - Endpoints: https://developers.cloudflare.com/fundamentals/oauth/integrate-with-cloudflare/
|
|
7
|
+
* - Flow/PKCE: https://developers.cloudflare.com/fundamentals/oauth/create-an-oauth-client/#supported-oauth-flows
|
|
8
|
+
* - Scopes: https://developers.cloudflare.com/fundamentals/oauth/create-an-oauth-client/#select-scopes
|
|
9
|
+
*
|
|
10
|
+
* Scopes are dot-notation API-permission names (e.g. `workers-platform.read`) fetched from
|
|
11
|
+
* `GET /client/v4/oauth/scopes` — `openid` is NOT a Cloudflare scope. PKCE (S256) is required
|
|
12
|
+
* for public clients (auth method `none`) and optional for confidential clients (client secret).
|
|
13
|
+
*/
|
|
14
|
+
export interface OAuthCloudflareConfig {
|
|
15
|
+
/**
|
|
16
|
+
* Cloudflare OAuth Client ID
|
|
17
|
+
* @default process.env.NUXT_OAUTH_CLOUDFLARE_CLIENT_ID
|
|
18
|
+
*/
|
|
19
|
+
clientId?: string;
|
|
20
|
+
/**
|
|
21
|
+
* Cloudflare OAuth Client Secret. Required for confidential clients
|
|
22
|
+
* (`client_secret_post`/`client_secret_basic`); omit for public PKCE-only
|
|
23
|
+
* clients (`token_endpoint_auth_method=none`).
|
|
24
|
+
* @default process.env.NUXT_OAUTH_CLOUDFLARE_CLIENT_SECRET
|
|
25
|
+
*/
|
|
26
|
+
clientSecret?: string;
|
|
27
|
+
/**
|
|
28
|
+
* Cloudflare OAuth scopes — dot-notation API token permission IDs from
|
|
29
|
+
* `GET /client/v4/oauth/scopes`, matching the scopes the client was registered with.
|
|
30
|
+
* Cloudflare requires at least one; there is no default (`openid` is not a Cloudflare scope).
|
|
31
|
+
* @see https://developers.cloudflare.com/fundamentals/oauth/create-an-oauth-client/#select-scopes
|
|
32
|
+
* @example ['workers-platform.read']
|
|
33
|
+
*/
|
|
34
|
+
scope?: string[];
|
|
35
|
+
/**
|
|
36
|
+
* Cloudflare OAuth Authorization URL
|
|
37
|
+
* @default 'https://dash.cloudflare.com/oauth2/auth'
|
|
38
|
+
*/
|
|
39
|
+
authorizationURL?: string;
|
|
40
|
+
/**
|
|
41
|
+
* Cloudflare OAuth Token URL
|
|
42
|
+
* @default 'https://dash.cloudflare.com/oauth2/token'
|
|
43
|
+
*/
|
|
44
|
+
tokenURL?: string;
|
|
45
|
+
/**
|
|
46
|
+
* Cloudflare OIDC userinfo URL. Returns standard claims (`sub`, `iss`, `aud`, …);
|
|
47
|
+
* use `GET /client/v4/user` for email/name.
|
|
48
|
+
* @default 'https://dash.cloudflare.com/oauth2/userinfo'
|
|
49
|
+
*/
|
|
50
|
+
userURL?: string;
|
|
51
|
+
/**
|
|
52
|
+
* Extra authorization parameters to provide to the authorization URL
|
|
53
|
+
*/
|
|
54
|
+
authorizationParams?: Record<string, string>;
|
|
55
|
+
/**
|
|
56
|
+
* Redirect URL to allow overriding for situations like prod failing to determine public hostname
|
|
57
|
+
* @default process.env.NUXT_OAUTH_CLOUDFLARE_REDIRECT_URL or current URL
|
|
58
|
+
*/
|
|
59
|
+
redirectURL?: string;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Claims returned by Cloudflare's `/oauth2/userinfo` endpoint. `sub` is the stable
|
|
63
|
+
* Cloudflare user ID; the rest are standard OIDC token claims. There is no email/name
|
|
64
|
+
* here — for richer profile data call `GET /client/v4/user` with the access token.
|
|
65
|
+
*/
|
|
66
|
+
export interface CloudflareUser {
|
|
67
|
+
/** Stable Cloudflare user ID. */
|
|
68
|
+
sub: string;
|
|
69
|
+
aud?: string[];
|
|
70
|
+
iss?: string;
|
|
71
|
+
iat?: number;
|
|
72
|
+
auth_time?: number;
|
|
73
|
+
rat?: number;
|
|
74
|
+
}
|
|
75
|
+
export interface CloudflareTokens {
|
|
76
|
+
access_token: string;
|
|
77
|
+
token_type: string;
|
|
78
|
+
expires_in: number;
|
|
79
|
+
/** Present when scope includes `offline_access` */
|
|
80
|
+
refresh_token?: string;
|
|
81
|
+
scope: string;
|
|
82
|
+
}
|
|
83
|
+
export declare function defineOAuthCloudflareEventHandler({ config, onSuccess, onError }: OAuthConfig<OAuthCloudflareConfig, {
|
|
84
|
+
user: CloudflareUser;
|
|
85
|
+
tokens: CloudflareTokens;
|
|
86
|
+
}>): import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<any>>;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { eventHandler, getQuery, sendRedirect, createError } from "h3";
|
|
2
|
+
import { withQuery } from "ufo";
|
|
3
|
+
import { defu } from "defu";
|
|
4
|
+
import { getOAuthRedirectURL, handleAccessTokenErrorResponse, handleInvalidState, handleMissingConfiguration, handlePkceVerifier, handleState, requestAccessToken } from "../utils.js";
|
|
5
|
+
import { useRuntimeConfig } from "#imports";
|
|
6
|
+
export function defineOAuthCloudflareEventHandler({ config, onSuccess, onError }) {
|
|
7
|
+
return eventHandler(async (event) => {
|
|
8
|
+
config = defu(config, useRuntimeConfig(event).oauth?.cloudflare, {
|
|
9
|
+
authorizationURL: "https://dash.cloudflare.com/oauth2/auth",
|
|
10
|
+
tokenURL: "https://dash.cloudflare.com/oauth2/token",
|
|
11
|
+
userURL: "https://dash.cloudflare.com/oauth2/userinfo",
|
|
12
|
+
authorizationParams: {}
|
|
13
|
+
});
|
|
14
|
+
const query = getQuery(event);
|
|
15
|
+
if (query.error) {
|
|
16
|
+
const error = createError({
|
|
17
|
+
statusCode: 401,
|
|
18
|
+
message: `Cloudflare login failed: ${query.error_description || query.error}`,
|
|
19
|
+
data: query
|
|
20
|
+
});
|
|
21
|
+
if (!onError) throw error;
|
|
22
|
+
return onError(event, error);
|
|
23
|
+
}
|
|
24
|
+
if (!config.clientId) {
|
|
25
|
+
return handleMissingConfiguration(event, "cloudflare", ["clientId"], onError);
|
|
26
|
+
}
|
|
27
|
+
const redirectURL = config.redirectURL || getOAuthRedirectURL(event);
|
|
28
|
+
const state = await handleState(event);
|
|
29
|
+
const pkce = await handlePkceVerifier(event);
|
|
30
|
+
if (!query.code) {
|
|
31
|
+
return sendRedirect(
|
|
32
|
+
event,
|
|
33
|
+
withQuery(config.authorizationURL, {
|
|
34
|
+
response_type: "code",
|
|
35
|
+
client_id: config.clientId,
|
|
36
|
+
redirect_uri: redirectURL,
|
|
37
|
+
scope: config.scope?.length ? config.scope.join(" ") : void 0,
|
|
38
|
+
...config.authorizationParams,
|
|
39
|
+
code_challenge: pkce.code_challenge,
|
|
40
|
+
code_challenge_method: pkce.code_challenge_method,
|
|
41
|
+
state
|
|
42
|
+
})
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
if (!state || query.state !== state) {
|
|
46
|
+
return handleInvalidState(event, "cloudflare", onError);
|
|
47
|
+
}
|
|
48
|
+
const tokens = await requestAccessToken(config.tokenURL, {
|
|
49
|
+
body: {
|
|
50
|
+
grant_type: "authorization_code",
|
|
51
|
+
client_id: config.clientId,
|
|
52
|
+
// Public (PKCE-only) clients authenticate with `none` and send no secret.
|
|
53
|
+
...config.clientSecret ? { client_secret: config.clientSecret } : {},
|
|
54
|
+
redirect_uri: redirectURL,
|
|
55
|
+
code: query.code,
|
|
56
|
+
code_verifier: pkce.code_verifier
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
if (tokens.error) {
|
|
60
|
+
return handleAccessTokenErrorResponse(event, "cloudflare", tokens, onError);
|
|
61
|
+
}
|
|
62
|
+
const user = await $fetch(config.userURL, {
|
|
63
|
+
headers: {
|
|
64
|
+
Authorization: `Bearer ${tokens.access_token}`
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
return onSuccess(event, {
|
|
68
|
+
user,
|
|
69
|
+
tokens
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
}
|
|
@@ -39,7 +39,7 @@ export function defineOAuthCognitoEventHandler({ config, onSuccess, onError }) {
|
|
|
39
39
|
})
|
|
40
40
|
);
|
|
41
41
|
}
|
|
42
|
-
if (query.state !== state) {
|
|
42
|
+
if (!state || query.state !== state) {
|
|
43
43
|
return handleInvalidState(event, "cognito", onError);
|
|
44
44
|
}
|
|
45
45
|
const tokens = await requestAccessToken(
|
|
@@ -40,7 +40,7 @@ export function defineOAuthDiscordEventHandler({ config, onSuccess, onError }) {
|
|
|
40
40
|
})
|
|
41
41
|
);
|
|
42
42
|
}
|
|
43
|
-
if (query.state !== state) {
|
|
43
|
+
if (!state || query.state !== state) {
|
|
44
44
|
return handleInvalidState(event, "discord", onError);
|
|
45
45
|
}
|
|
46
46
|
const tokens = await requestAccessToken(config.tokenURL, {
|
|
@@ -36,7 +36,7 @@ export function defineOAuthDropboxEventHandler({ config, onSuccess, onError }) {
|
|
|
36
36
|
})
|
|
37
37
|
);
|
|
38
38
|
}
|
|
39
|
-
if (query.state !== state) {
|
|
39
|
+
if (!state || query.state !== state) {
|
|
40
40
|
return handleInvalidState(event, "dropbox", onError);
|
|
41
41
|
}
|
|
42
42
|
const tokens = await requestAccessToken(config.tokenURL, {
|
|
@@ -10,6 +10,12 @@ export interface OAuthFacebookConfig {
|
|
|
10
10
|
* @default process.env.NUXT_OAUTH_FACEBOOK_CLIENT_SECRET
|
|
11
11
|
*/
|
|
12
12
|
clientSecret?: string;
|
|
13
|
+
/**
|
|
14
|
+
* Facebook Graph API version used for default OAuth and /me URLs
|
|
15
|
+
* @default 'v25.0'
|
|
16
|
+
* @see https://developers.facebook.com/docs/graph-api/changelog/versions/
|
|
17
|
+
*/
|
|
18
|
+
apiVersion?: string;
|
|
13
19
|
/**
|
|
14
20
|
* Facebook OAuth Scope
|
|
15
21
|
* @default []
|
|
@@ -26,14 +32,19 @@ export interface OAuthFacebookConfig {
|
|
|
26
32
|
fields?: string[];
|
|
27
33
|
/**
|
|
28
34
|
* Facebook OAuth Authorization URL
|
|
29
|
-
* @default 'https://www.facebook.com/
|
|
35
|
+
* @default 'https://www.facebook.com/${apiVersion}/dialog/oauth'
|
|
30
36
|
*/
|
|
31
37
|
authorizationURL?: string;
|
|
32
38
|
/**
|
|
33
39
|
* Facebook OAuth Token URL
|
|
34
|
-
* @default 'https://graph.facebook.com/
|
|
40
|
+
* @default 'https://graph.facebook.com/${apiVersion}/oauth/access_token'
|
|
35
41
|
*/
|
|
36
42
|
tokenURL?: string;
|
|
43
|
+
/**
|
|
44
|
+
* Facebook Graph API user profile URL
|
|
45
|
+
* @default 'https://graph.facebook.com/${apiVersion}/me'
|
|
46
|
+
*/
|
|
47
|
+
userURL?: string;
|
|
37
48
|
/**
|
|
38
49
|
* Extra authorization parameters to provide to the authorization URL
|
|
39
50
|
* @see https://developers.facebook.com/docs/facebook-login/guides/advanced/manual-flow/
|
|
@@ -1,6 +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 { subtle } from "uncrypto";
|
|
4
5
|
import { handleMissingConfiguration, handleAccessTokenErrorResponse, getOAuthRedirectURL, handleInvalidState, handleState, requestAccessToken } from "../utils.js";
|
|
5
6
|
import { useRuntimeConfig } from "#imports";
|
|
6
7
|
export function defineOAuthFacebookEventHandler({
|
|
@@ -9,9 +10,12 @@ export function defineOAuthFacebookEventHandler({
|
|
|
9
10
|
onError
|
|
10
11
|
}) {
|
|
11
12
|
return eventHandler(async (event) => {
|
|
13
|
+
const apiVersion = config?.apiVersion || useRuntimeConfig(event).oauth?.facebook?.apiVersion || "v25.0";
|
|
12
14
|
config = defu(config, useRuntimeConfig(event).oauth?.facebook, {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
+
apiVersion,
|
|
16
|
+
authorizationURL: `https://www.facebook.com/${apiVersion}/dialog/oauth`,
|
|
17
|
+
tokenURL: `https://graph.facebook.com/${apiVersion}/oauth/access_token`,
|
|
18
|
+
userURL: `https://graph.facebook.com/${apiVersion}/me`,
|
|
15
19
|
authorizationParams: {}
|
|
16
20
|
});
|
|
17
21
|
const query = getQuery(event);
|
|
@@ -42,7 +46,7 @@ export function defineOAuthFacebookEventHandler({
|
|
|
42
46
|
})
|
|
43
47
|
);
|
|
44
48
|
}
|
|
45
|
-
if (query.state !== state) {
|
|
49
|
+
if (!state || query.state !== state) {
|
|
46
50
|
return handleInvalidState(event, "facebook", onError);
|
|
47
51
|
}
|
|
48
52
|
const tokens = await requestAccessToken(config.tokenURL, {
|
|
@@ -60,8 +64,16 @@ export function defineOAuthFacebookEventHandler({
|
|
|
60
64
|
const accessToken = tokens.access_token;
|
|
61
65
|
config.fields = [...new Set(config.fields || ["id", "name"])];
|
|
62
66
|
const fields = config.fields.join(",");
|
|
67
|
+
const encoder = new TextEncoder();
|
|
68
|
+
const key = await subtle.importKey("raw", encoder.encode(config.clientSecret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
|
|
69
|
+
const signature = await subtle.sign("HMAC", key, encoder.encode(accessToken));
|
|
70
|
+
const appsecretProof = Array.from(new Uint8Array(signature), (b) => b.toString(16).padStart(2, "0")).join("");
|
|
63
71
|
const user = await $fetch(
|
|
64
|
-
|
|
72
|
+
withQuery(config.userURL, {
|
|
73
|
+
fields,
|
|
74
|
+
access_token: accessToken,
|
|
75
|
+
appsecret_proof: appsecretProof
|
|
76
|
+
})
|
|
65
77
|
);
|
|
66
78
|
if (!user) {
|
|
67
79
|
const error = createError({
|
|
@@ -42,7 +42,7 @@ export function defineOAuthGitHubEventHandler({ config, onSuccess, onError }) {
|
|
|
42
42
|
})
|
|
43
43
|
);
|
|
44
44
|
}
|
|
45
|
-
if (query.state !== state) {
|
|
45
|
+
if (!state || query.state !== state) {
|
|
46
46
|
return handleInvalidState(event, "github", onError);
|
|
47
47
|
}
|
|
48
48
|
const tokens = await requestAccessToken(config.tokenURL, {
|
|
@@ -63,7 +63,7 @@ export function defineOAuthGitLabEventHandler({
|
|
|
63
63
|
})
|
|
64
64
|
);
|
|
65
65
|
}
|
|
66
|
-
if (query.state !== state) {
|
|
66
|
+
if (!state || query.state !== state) {
|
|
67
67
|
return handleInvalidState(event, "gitlab", onError);
|
|
68
68
|
}
|
|
69
69
|
const tokens = await requestAccessToken(config.tokenURL, {
|
|
@@ -35,7 +35,7 @@ export function defineOAuthGoogleEventHandler({
|
|
|
35
35
|
})
|
|
36
36
|
);
|
|
37
37
|
}
|
|
38
|
-
if (query.state !== state) {
|
|
38
|
+
if (!state || query.state !== state) {
|
|
39
39
|
return handleInvalidState(event, "google", onError);
|
|
40
40
|
}
|
|
41
41
|
const tokens = await requestAccessToken(config.tokenURL, {
|
|
@@ -45,7 +45,7 @@ export function defineOAuthHerokuEventHandler({
|
|
|
45
45
|
})
|
|
46
46
|
);
|
|
47
47
|
}
|
|
48
|
-
if (query.state !== state) {
|
|
48
|
+
if (!state || query.state !== state) {
|
|
49
49
|
return handleInvalidState(event, "heroku", onError);
|
|
50
50
|
}
|
|
51
51
|
const tokens = await requestAccessToken(config.tokenURL, {
|
|
@@ -33,7 +33,7 @@ export function defineOAuthHubspotEventHandler({ config, onSuccess, onError }) {
|
|
|
33
33
|
})
|
|
34
34
|
);
|
|
35
35
|
}
|
|
36
|
-
if (query.state !== state) {
|
|
36
|
+
if (!state || query.state !== state) {
|
|
37
37
|
return handleInvalidState(event, "hubspot", onError);
|
|
38
38
|
}
|
|
39
39
|
const tokens = await requestAccessToken(
|
|
@@ -47,7 +47,7 @@ export function defineOAuthInstagramEventHandler({
|
|
|
47
47
|
})
|
|
48
48
|
);
|
|
49
49
|
}
|
|
50
|
-
if (query.state !== state) {
|
|
50
|
+
if (!state || query.state !== state) {
|
|
51
51
|
return handleInvalidState(event, "instagram", onError);
|
|
52
52
|
}
|
|
53
53
|
const tokens = await requestAccessToken(config.tokenURL, {
|