nuxt-auth-utils 0.5.9 → 0.5.11
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 +14 -6
- package/dist/module.json +1 -1
- package/dist/module.mjs +13 -0
- package/dist/runtime/app/composables/session.js +20 -0
- package/dist/runtime/app/plugins/session.client.js +5 -0
- package/dist/runtime/server/lib/oauth/apple.d.ts +73 -0
- package/dist/runtime/server/lib/oauth/apple.js +77 -0
- package/dist/runtime/server/lib/oauth/gitea.d.ts +51 -0
- package/dist/runtime/server/lib/oauth/gitea.js +85 -0
- package/dist/runtime/server/lib/oauth/github.js +1 -0
- package/dist/runtime/server/lib/oauth/microsoft.js +3 -2
- package/dist/runtime/server/lib/utils.d.ts +25 -0
- package/dist/runtime/server/lib/utils.js +17 -0
- package/dist/runtime/types/oauth-config.d.ts +1 -1
- package/dist/runtime/types/session.d.ts +7 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -16,14 +16,14 @@ Add Authentication to Nuxt applications with secured & sealed cookies sessions.
|
|
|
16
16
|
## Features
|
|
17
17
|
|
|
18
18
|
- [Hybrid Rendering](#hybrid-rendering) support (SSR / CSR / SWR / Prerendering)
|
|
19
|
-
- [
|
|
19
|
+
- [30+ OAuth Providers](#supported-oauth-providers)
|
|
20
20
|
- [Password Hashing](#password-hashing)
|
|
21
21
|
- [WebAuthn (passkey)](#webauthn-passkey)
|
|
22
22
|
- [`useUserSession()` Vue composable](#vue-composable)
|
|
23
23
|
- [Tree-shakable server utils](#server-utils)
|
|
24
24
|
- [`<AuthState>` component](#authstate-component)
|
|
25
25
|
- [Extendable with hooks](#extend-session)
|
|
26
|
-
- [WebSocket support](#
|
|
26
|
+
- [WebSocket support](#websocket-support)
|
|
27
27
|
|
|
28
28
|
It has few dependencies (only from [UnJS](https://github.com/unjs)), run on multiple JS environments (Node, Deno, Workers) and is fully typed with TypeScript.
|
|
29
29
|
|
|
@@ -62,7 +62,7 @@ Nuxt Auth Utils automatically adds some plugins to fetch the current user sessio
|
|
|
62
62
|
|
|
63
63
|
```vue
|
|
64
64
|
<script setup>
|
|
65
|
-
const { loggedIn, user, session, fetch, clear } = useUserSession()
|
|
65
|
+
const { loggedIn, user, session, fetch, clear, openInPopup } = useUserSession()
|
|
66
66
|
</script>
|
|
67
67
|
|
|
68
68
|
<template>
|
|
@@ -74,6 +74,8 @@ const { loggedIn, user, session, fetch, clear } = useUserSession()
|
|
|
74
74
|
<div v-else>
|
|
75
75
|
<h1>Not logged in</h1>
|
|
76
76
|
<a href="/auth/github">Login with GitHub</a>
|
|
77
|
+
<!-- or open the OAuth route in a popup -->
|
|
78
|
+
<button @click="openInPopup('/auth/github')">Login with GitHub</button>
|
|
77
79
|
</div>
|
|
78
80
|
</template>
|
|
79
81
|
```
|
|
@@ -106,6 +108,10 @@ interface UserSessionComposable {
|
|
|
106
108
|
* Clear the user session and remove the session cookie.
|
|
107
109
|
*/
|
|
108
110
|
clear: () => Promise<void>
|
|
111
|
+
/**
|
|
112
|
+
* Open the OAuth route in a popup that auto-closes when successful.
|
|
113
|
+
*/
|
|
114
|
+
openInPopup: (route: string, size?: { width?: number, height?: number }) => void
|
|
109
115
|
}
|
|
110
116
|
```
|
|
111
117
|
|
|
@@ -205,6 +211,7 @@ It can also be set using environment variables:
|
|
|
205
211
|
|
|
206
212
|
#### Supported OAuth Providers
|
|
207
213
|
|
|
214
|
+
- Apple
|
|
208
215
|
- Atlassian
|
|
209
216
|
- Auth0
|
|
210
217
|
- Authentik
|
|
@@ -215,6 +222,7 @@ It can also be set using environment variables:
|
|
|
215
222
|
- Facebook
|
|
216
223
|
- GitHub
|
|
217
224
|
- GitLab
|
|
225
|
+
- Gitea
|
|
218
226
|
- Google
|
|
219
227
|
- Hubspot
|
|
220
228
|
- Instagram
|
|
@@ -597,7 +605,7 @@ You can use the `placeholder` slot to show a placeholder on server-side and whil
|
|
|
597
605
|
|
|
598
606
|
If you are caching your routes with `routeRules`, please make sure to use [Nitro](https://github.com/unjs/nitro) >= `2.9.7` to support the client-side fetching of the user session.
|
|
599
607
|
|
|
600
|
-
##
|
|
608
|
+
## WebSocket Support
|
|
601
609
|
|
|
602
610
|
Nuxt Auth Utils is compatible with [Nitro WebSockets](https://nitro.build/guide/websocket).
|
|
603
611
|
|
|
@@ -624,8 +632,8 @@ export default defineWebSocketHandler({
|
|
|
624
632
|
},
|
|
625
633
|
async open(peer) {
|
|
626
634
|
const { user } = await requireUserSession(peer)
|
|
627
|
-
|
|
628
|
-
peer.send(`Hello, ${
|
|
635
|
+
|
|
636
|
+
peer.send(`Hello, ${user.name}!`)
|
|
629
637
|
},
|
|
630
638
|
message(peer, message) {
|
|
631
639
|
peer.send(`Echo: ${message}`)
|
package/dist/module.json
CHANGED
package/dist/module.mjs
CHANGED
|
@@ -97,6 +97,12 @@ const module = defineNuxtModule({
|
|
|
97
97
|
authenticate: {}
|
|
98
98
|
});
|
|
99
99
|
runtimeConfig.oauth = defu(runtimeConfig.oauth, {});
|
|
100
|
+
runtimeConfig.oauth.gitea = defu(runtimeConfig.oauth.gitea, {
|
|
101
|
+
clientId: "",
|
|
102
|
+
clientSecret: "",
|
|
103
|
+
redirectURL: "",
|
|
104
|
+
baseURL: ""
|
|
105
|
+
});
|
|
100
106
|
runtimeConfig.oauth.github = defu(runtimeConfig.oauth.github, {
|
|
101
107
|
clientId: "",
|
|
102
108
|
clientSecret: "",
|
|
@@ -274,6 +280,13 @@ const module = defineNuxtModule({
|
|
|
274
280
|
clientSecret: "",
|
|
275
281
|
redirectURL: ""
|
|
276
282
|
});
|
|
283
|
+
runtimeConfig.oauth.apple = defu(runtimeConfig.oauth.apple, {
|
|
284
|
+
teamId: "",
|
|
285
|
+
keyId: "",
|
|
286
|
+
privateKey: "",
|
|
287
|
+
redirectURL: "",
|
|
288
|
+
clientId: ""
|
|
289
|
+
});
|
|
277
290
|
}
|
|
278
291
|
});
|
|
279
292
|
|
|
@@ -28,12 +28,32 @@ export function useUserSession() {
|
|
|
28
28
|
authReadyState.value = true;
|
|
29
29
|
}
|
|
30
30
|
};
|
|
31
|
+
const popupListener = (e) => {
|
|
32
|
+
if (e.key === "temp-nuxt-auth-utils-popup") {
|
|
33
|
+
fetch();
|
|
34
|
+
window.removeEventListener("storage", popupListener);
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
const openInPopup = (route, size = {}) => {
|
|
38
|
+
localStorage.setItem("temp-nuxt-auth-utils-popup", "true");
|
|
39
|
+
const width = size.width ?? 960;
|
|
40
|
+
const height = size.height ?? 600;
|
|
41
|
+
const top = (window.top?.outerHeight ?? 0) / 2 + (window.top?.screenY ?? 0) - height / 2;
|
|
42
|
+
const left = (window.top?.outerWidth ?? 0) / 2 + (window.top?.screenX ?? 0) - width / 2;
|
|
43
|
+
window.open(
|
|
44
|
+
route,
|
|
45
|
+
"nuxt-auth-utils-popup",
|
|
46
|
+
`width=${width}, height=${height}, top=${top}, left=${left}, toolbar=no, location=no, directories=no, status=no, menubar=no, scrollbars=no, resizable=no, copyhistory=no`
|
|
47
|
+
);
|
|
48
|
+
window.addEventListener("storage", popupListener);
|
|
49
|
+
};
|
|
31
50
|
return {
|
|
32
51
|
ready: computed(() => authReadyState.value),
|
|
33
52
|
loggedIn: computed(() => Boolean(sessionState.value.user)),
|
|
34
53
|
user: computed(() => sessionState.value.user || null),
|
|
35
54
|
session: sessionState,
|
|
36
55
|
fetch,
|
|
56
|
+
openInPopup,
|
|
37
57
|
clear
|
|
38
58
|
};
|
|
39
59
|
}
|
|
@@ -7,4 +7,9 @@ export default defineNuxtPlugin(async (nuxtApp) => {
|
|
|
7
7
|
await useUserSession().fetch();
|
|
8
8
|
});
|
|
9
9
|
}
|
|
10
|
+
if (localStorage.getItem("temp-nuxt-auth-utils-popup")) {
|
|
11
|
+
localStorage.removeItem("temp-nuxt-auth-utils-popup");
|
|
12
|
+
const error = useError();
|
|
13
|
+
if (!error.value) window.close();
|
|
14
|
+
}
|
|
10
15
|
});
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { OAuthConfig } from '#auth-utils';
|
|
2
|
+
export interface OAuthAppleConfig {
|
|
3
|
+
/**
|
|
4
|
+
* Apple OAuth Client ID
|
|
5
|
+
* @default process.env.NUXT_OAUTH_APPLE_CLIENT_ID
|
|
6
|
+
*/
|
|
7
|
+
clientId?: string;
|
|
8
|
+
/**
|
|
9
|
+
* Apple OAuth team ID
|
|
10
|
+
* @default process.env.NUXT_OAUTH_APPLE_TEAM_ID
|
|
11
|
+
*/
|
|
12
|
+
teamId?: string;
|
|
13
|
+
/**
|
|
14
|
+
* Apple OAuth key identifier
|
|
15
|
+
* @default process.env.NUXT_OAUTH_APPLE_KEY_ID
|
|
16
|
+
*/
|
|
17
|
+
keyId?: string;
|
|
18
|
+
/**
|
|
19
|
+
* Apple OAuth Private Key. Linebreaks must be replaced with \n
|
|
20
|
+
* @example '-----BEGIN PRIVATE KEY-----\nMIGTAgEAMBMGByqGSM...\n-----END PRIVATE KEY-----'
|
|
21
|
+
* @default process.env.NUXT_OAUTH_APPLE_PRIVATE_KEY
|
|
22
|
+
*/
|
|
23
|
+
privateKey?: string;
|
|
24
|
+
/**
|
|
25
|
+
* Apple OAuth Scope. Apple wants this to be a string separated by spaces, but for consistency with other providers, we also allow an array of strings.
|
|
26
|
+
* @default ''
|
|
27
|
+
* @see https://developer.apple.com/documentation/sign_in_with_apple/clientconfigi/3230955-scope
|
|
28
|
+
* @example 'name email'
|
|
29
|
+
*/
|
|
30
|
+
scope?: string | string[];
|
|
31
|
+
/**
|
|
32
|
+
* Apple OAuth Authorization URL
|
|
33
|
+
* @default 'https://appleid.apple.com/auth/authorize'
|
|
34
|
+
*/
|
|
35
|
+
authorizationURL?: string;
|
|
36
|
+
/**
|
|
37
|
+
* Extra authorization parameters to provide to the authorization URL
|
|
38
|
+
* @see https://developer.apple.com/documentation/sign_in_with_apple/clientconfigi/3230955-scope
|
|
39
|
+
* @example { usePop: true }
|
|
40
|
+
*/
|
|
41
|
+
authorizationParams?: Record<string, string | boolean>;
|
|
42
|
+
/**
|
|
43
|
+
* Apple OAuth Token URL
|
|
44
|
+
* @default 'https://appleid.apple.com/auth/token'
|
|
45
|
+
*/
|
|
46
|
+
tokenURL?: string;
|
|
47
|
+
/**
|
|
48
|
+
* Redirect URL to to allow overriding for situations like prod failing to determine public hostname
|
|
49
|
+
* @default process.env.NUXT_OAUTH_APPLE_REDIRECT_URL or current URL
|
|
50
|
+
*/
|
|
51
|
+
redirectURL?: string;
|
|
52
|
+
}
|
|
53
|
+
export interface OAuthAppleTokens {
|
|
54
|
+
iss: string;
|
|
55
|
+
aud: string;
|
|
56
|
+
exp: number;
|
|
57
|
+
iat: number;
|
|
58
|
+
sub: string;
|
|
59
|
+
at_hash: string;
|
|
60
|
+
email: string;
|
|
61
|
+
email_verified: boolean;
|
|
62
|
+
is_private_email: boolean;
|
|
63
|
+
auth_time: number;
|
|
64
|
+
nonce_supported: boolean;
|
|
65
|
+
}
|
|
66
|
+
export interface OAuthAppleUser {
|
|
67
|
+
name?: {
|
|
68
|
+
firstName?: string;
|
|
69
|
+
lastName?: string;
|
|
70
|
+
};
|
|
71
|
+
email?: string;
|
|
72
|
+
}
|
|
73
|
+
export declare function defineOAuthAppleEventHandler({ config, onSuccess, onError, }: OAuthConfig<OAuthAppleConfig>): import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<void>>;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { eventHandler, getRequestHeader, readBody, sendRedirect } from "h3";
|
|
2
|
+
import { withQuery } from "ufo";
|
|
3
|
+
import { defu } from "defu";
|
|
4
|
+
import { handleMissingConfiguration, handleAccessTokenErrorResponse, getOAuthRedirectURL, requestAccessToken, signJwt, verifyJwt } from "../utils.js";
|
|
5
|
+
import { useRuntimeConfig } from "#imports";
|
|
6
|
+
export function defineOAuthAppleEventHandler({
|
|
7
|
+
config,
|
|
8
|
+
onSuccess,
|
|
9
|
+
onError
|
|
10
|
+
}) {
|
|
11
|
+
return eventHandler(async (event) => {
|
|
12
|
+
config = defu(config, useRuntimeConfig(event).oauth?.apple, {
|
|
13
|
+
authorizationURL: config?.authorizationURL || "https://appleid.apple.com/auth/authorize",
|
|
14
|
+
authorizationParams: {}
|
|
15
|
+
});
|
|
16
|
+
if (!config.teamId || !config.keyId || !config.privateKey || !config.clientId) {
|
|
17
|
+
return handleMissingConfiguration(event, "apple", ["teamId", "keyId", "privateKey", "clientId"], onError);
|
|
18
|
+
}
|
|
19
|
+
const isPost = getRequestHeader(event, "content-type") === "application/x-www-form-urlencoded";
|
|
20
|
+
let code;
|
|
21
|
+
let user;
|
|
22
|
+
if (isPost) {
|
|
23
|
+
({ code, user } = await readBody(event));
|
|
24
|
+
}
|
|
25
|
+
if (!isPost || !code) {
|
|
26
|
+
const redirectURL = config.redirectURL || getOAuthRedirectURL(event);
|
|
27
|
+
config.scope = Array.isArray(config.scope) ? config.scope.join(" ") : config.scope || "name email";
|
|
28
|
+
return sendRedirect(
|
|
29
|
+
event,
|
|
30
|
+
withQuery(config.authorizationURL, {
|
|
31
|
+
response_type: "code",
|
|
32
|
+
response_mode: "form_post",
|
|
33
|
+
client_id: config.clientId,
|
|
34
|
+
redirect_uri: redirectURL,
|
|
35
|
+
scope: config.scope,
|
|
36
|
+
...config.authorizationParams
|
|
37
|
+
})
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
try {
|
|
41
|
+
const secret = await signJwt(
|
|
42
|
+
{
|
|
43
|
+
iss: config.teamId,
|
|
44
|
+
aud: "https://appleid.apple.com",
|
|
45
|
+
sub: config.clientId
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
privateKey: config.privateKey,
|
|
49
|
+
keyId: config.keyId,
|
|
50
|
+
teamId: config.teamId,
|
|
51
|
+
clientId: config.clientId,
|
|
52
|
+
expiresIn: "5m"
|
|
53
|
+
}
|
|
54
|
+
);
|
|
55
|
+
const accessTokenResult = await requestAccessToken(config.tokenURL || "https://appleid.apple.com/auth/token", {
|
|
56
|
+
params: {
|
|
57
|
+
client_id: config.clientId,
|
|
58
|
+
client_secret: secret,
|
|
59
|
+
code,
|
|
60
|
+
grant_type: "authorization_code",
|
|
61
|
+
redirect_uri: config.redirectURL
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
const tokens = await verifyJwt(accessTokenResult.id_token, {
|
|
65
|
+
publicJwkUrl: "https://appleid.apple.com/auth/keys",
|
|
66
|
+
audience: config.clientId,
|
|
67
|
+
issuer: "https://appleid.apple.com"
|
|
68
|
+
});
|
|
69
|
+
if (!tokens) {
|
|
70
|
+
return handleAccessTokenErrorResponse(event, "apple", tokens, onError);
|
|
71
|
+
}
|
|
72
|
+
return onSuccess(event, { user, tokens });
|
|
73
|
+
} catch (error) {
|
|
74
|
+
return handleAccessTokenErrorResponse(event, "apple", error, onError);
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { OAuthConfig } from '#auth-utils';
|
|
2
|
+
export interface OAuthGiteaConfig {
|
|
3
|
+
/**
|
|
4
|
+
* Gitea OAuth Client ID
|
|
5
|
+
* @default process.env.NUXT_OAUTH_GITEA_CLIENT_ID
|
|
6
|
+
*/
|
|
7
|
+
clientId?: string;
|
|
8
|
+
/**
|
|
9
|
+
* Gitea OAuth Client Secret
|
|
10
|
+
* @default process.env.NUXT_OAUTH_GITEA_CLIENT_SECRET
|
|
11
|
+
*/
|
|
12
|
+
clientSecret?: string;
|
|
13
|
+
/**
|
|
14
|
+
* Gitea OAuth Scope
|
|
15
|
+
* @default ['read:user']
|
|
16
|
+
* @see https://docs.gitea.io/en-us/oauth2-provider/
|
|
17
|
+
* @example ['read:user']
|
|
18
|
+
*/
|
|
19
|
+
scope?: string[];
|
|
20
|
+
/**
|
|
21
|
+
* Require email from user, adds the ['email'] scope if not present
|
|
22
|
+
* @default false
|
|
23
|
+
*/
|
|
24
|
+
emailRequired?: boolean;
|
|
25
|
+
/**
|
|
26
|
+
* Gitea OAuth Authorization URL
|
|
27
|
+
* @default '/login/oauth/authorize'
|
|
28
|
+
*/
|
|
29
|
+
authorizationURL?: string;
|
|
30
|
+
/**
|
|
31
|
+
* Gitea OAuth Token URL
|
|
32
|
+
* @default '/login/oauth/access_token'
|
|
33
|
+
*/
|
|
34
|
+
tokenURL?: string;
|
|
35
|
+
/**
|
|
36
|
+
* Extra authorization parameters to provide to the authorization URL
|
|
37
|
+
* @see https://docs.gitea.io/en-us/oauth2-provider/
|
|
38
|
+
*/
|
|
39
|
+
authorizationParams?: Record<string, string>;
|
|
40
|
+
/**
|
|
41
|
+
* Redirect URL to to allow overriding for situations like prod failing to determine public hostname
|
|
42
|
+
* @default process.env.NUXT_OAUTH_GITEA_REDIRECT_URL
|
|
43
|
+
*/
|
|
44
|
+
redirectURL?: string;
|
|
45
|
+
/**
|
|
46
|
+
* URL of your Gitea instance
|
|
47
|
+
* @default 'https://gitea.com'
|
|
48
|
+
*/
|
|
49
|
+
baseURL?: string;
|
|
50
|
+
}
|
|
51
|
+
export declare function defineOAuthGiteaEventHandler({ config, onSuccess, onError, }: OAuthConfig<OAuthGiteaConfig>): import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<void>>;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { eventHandler, getQuery, sendRedirect } from "h3";
|
|
2
|
+
import { withQuery } from "ufo";
|
|
3
|
+
import { defu } from "defu";
|
|
4
|
+
import {
|
|
5
|
+
handleMissingConfiguration,
|
|
6
|
+
handleAccessTokenErrorResponse,
|
|
7
|
+
getOAuthRedirectURL,
|
|
8
|
+
requestAccessToken
|
|
9
|
+
} from "../utils.js";
|
|
10
|
+
import { useRuntimeConfig, createError } from "#imports";
|
|
11
|
+
export function defineOAuthGiteaEventHandler({
|
|
12
|
+
config,
|
|
13
|
+
onSuccess,
|
|
14
|
+
onError
|
|
15
|
+
}) {
|
|
16
|
+
return eventHandler(async (event) => {
|
|
17
|
+
const runtimeConfig = useRuntimeConfig(event).oauth?.gitea;
|
|
18
|
+
const baseURL = config?.baseURL ?? runtimeConfig.baseURL ?? "https://gitea.com";
|
|
19
|
+
config = defu(config, runtimeConfig, {
|
|
20
|
+
authorizationURL: `${baseURL}/login/oauth/authorize`,
|
|
21
|
+
tokenURL: `${baseURL}/login/oauth/access_token`,
|
|
22
|
+
authorizationParams: {}
|
|
23
|
+
});
|
|
24
|
+
const query = getQuery(event);
|
|
25
|
+
if (query.error) {
|
|
26
|
+
const error = createError({
|
|
27
|
+
statusCode: 401,
|
|
28
|
+
message: `Gitea 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.clientSecret) {
|
|
35
|
+
return handleMissingConfiguration(
|
|
36
|
+
event,
|
|
37
|
+
"gitea",
|
|
38
|
+
["clientId", "clientSecret"],
|
|
39
|
+
onError
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
const redirectURL = config.redirectURL || getOAuthRedirectURL(event);
|
|
43
|
+
if (!query.code) {
|
|
44
|
+
config.scope = config.scope || [];
|
|
45
|
+
if (!config.scope.length) {
|
|
46
|
+
config.scope.push("read:user");
|
|
47
|
+
}
|
|
48
|
+
if (config.emailRequired && !config.scope.includes("email")) {
|
|
49
|
+
config.scope.push("email");
|
|
50
|
+
}
|
|
51
|
+
return sendRedirect(
|
|
52
|
+
event,
|
|
53
|
+
withQuery(config.authorizationURL, {
|
|
54
|
+
response_type: "code",
|
|
55
|
+
client_id: config.clientId,
|
|
56
|
+
redirect_uri: redirectURL,
|
|
57
|
+
scope: config.scope.join(" "),
|
|
58
|
+
...config.authorizationParams
|
|
59
|
+
})
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
const tokens = await requestAccessToken(config.tokenURL, {
|
|
63
|
+
body: {
|
|
64
|
+
grant_type: "authorization_code",
|
|
65
|
+
client_id: config.clientId,
|
|
66
|
+
client_secret: config.clientSecret,
|
|
67
|
+
redirect_uri: redirectURL,
|
|
68
|
+
code: query.code
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
if (tokens.error) {
|
|
72
|
+
return handleAccessTokenErrorResponse(event, "gitea", tokens, onError);
|
|
73
|
+
}
|
|
74
|
+
const accessToken = tokens.access_token;
|
|
75
|
+
const user = await $fetch(`${baseURL}/api/v1/user`, {
|
|
76
|
+
headers: {
|
|
77
|
+
Authorization: `token ${accessToken}`
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
return onSuccess(event, {
|
|
81
|
+
user,
|
|
82
|
+
tokens
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
}
|
|
@@ -16,14 +16,15 @@ export function defineOAuthMicrosoftEventHandler({ config, onSuccess, onError })
|
|
|
16
16
|
const tokenURL = config.tokenURL || `https://login.microsoftonline.com/${config.tenant}/oauth2/v2.0/token`;
|
|
17
17
|
const redirectURL = config.redirectURL || getOAuthRedirectURL(event);
|
|
18
18
|
if (!query.code) {
|
|
19
|
-
|
|
19
|
+
config.scope = config.scope && config.scope.length > 0 ? config.scope : ["User.Read"];
|
|
20
|
+
config.scope = [...new Set(config.scope)];
|
|
20
21
|
return sendRedirect(
|
|
21
22
|
event,
|
|
22
23
|
withQuery(authorizationURL, {
|
|
23
24
|
client_id: config.clientId,
|
|
24
25
|
response_type: "code",
|
|
25
26
|
redirect_uri: redirectURL,
|
|
26
|
-
scope: scope.join(" "),
|
|
27
|
+
scope: config.scope.join(" "),
|
|
27
28
|
...config.authorizationParams
|
|
28
29
|
})
|
|
29
30
|
);
|
|
@@ -34,4 +34,29 @@ 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
|
+
/**
|
|
38
|
+
* JWT signing using jose
|
|
39
|
+
*
|
|
40
|
+
* @see https://github.com/panva/jose
|
|
41
|
+
*/
|
|
42
|
+
interface JWTSignOptions {
|
|
43
|
+
privateKey: string;
|
|
44
|
+
keyId: string;
|
|
45
|
+
teamId?: string;
|
|
46
|
+
clientId?: string;
|
|
47
|
+
algorithm?: 'ES256' | 'RS256';
|
|
48
|
+
expiresIn?: string;
|
|
49
|
+
}
|
|
50
|
+
export declare function signJwt<T extends Record<string, unknown>>(payload: T, options: JWTSignOptions): Promise<string>;
|
|
51
|
+
/**
|
|
52
|
+
* Verify a JWT token using jose - will throw error if invalid
|
|
53
|
+
*
|
|
54
|
+
* @see https://github.com/panva/jose
|
|
55
|
+
*/
|
|
56
|
+
interface JWTVerifyOptions {
|
|
57
|
+
publicJwkUrl: string;
|
|
58
|
+
audience: string;
|
|
59
|
+
issuer: string;
|
|
60
|
+
}
|
|
61
|
+
export declare function verifyJwt<T>(token: string, options: JWTVerifyOptions): Promise<T>;
|
|
37
62
|
export {};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { getRequestURL } from "h3";
|
|
2
2
|
import { FetchError } from "ofetch";
|
|
3
3
|
import { snakeCase, upperFirst } from "scule";
|
|
4
|
+
import * as jose from "jose";
|
|
4
5
|
import { createError } from "#imports";
|
|
5
6
|
export function getOAuthRedirectURL(event) {
|
|
6
7
|
const requestURL = getRequestURL(event);
|
|
@@ -44,3 +45,19 @@ export function handleMissingConfiguration(event, provider, missingKeys, onError
|
|
|
44
45
|
if (!onError) throw error;
|
|
45
46
|
return onError(event, error);
|
|
46
47
|
}
|
|
48
|
+
export async function signJwt(payload, options) {
|
|
49
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
50
|
+
const privateKey = await jose.importPKCS8(
|
|
51
|
+
options.privateKey.replace(/\\n/g, "\n"),
|
|
52
|
+
options.algorithm || "ES256"
|
|
53
|
+
);
|
|
54
|
+
return new jose.SignJWT(payload).setProtectedHeader({ alg: options.algorithm || "ES256", kid: options.keyId }).setIssuedAt(now).setExpirationTime(options.expiresIn || "5m").sign(privateKey);
|
|
55
|
+
}
|
|
56
|
+
export async function verifyJwt(token, options) {
|
|
57
|
+
const JWKS = jose.createRemoteJWKSet(new URL(options.publicJwkUrl));
|
|
58
|
+
const { payload } = await jose.jwtVerify(token, JWKS, {
|
|
59
|
+
audience: options.audience,
|
|
60
|
+
issuer: options.issuer
|
|
61
|
+
});
|
|
62
|
+
return payload;
|
|
63
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { H3Event, H3Error } from 'h3';
|
|
2
|
-
export type OAuthProvider = 'atlassian' | 'auth0' | 'authentik' | 'battledotnet' | 'cognito' | 'discord' | 'dropbox' | 'facebook' | 'github' | 'gitlab' | 'google' | 'hubspot' | 'instagram' | 'keycloak' | 'line' | 'linear' | 'linkedin' | 'microsoft' | 'paypal' | 'polar' | 'spotify' | 'seznam' | 'steam' | 'strava' | 'tiktok' | 'twitch' | 'vk' | 'workos' | 'x' | 'xsuaa' | 'yandex' | 'zitadel' | (string & {});
|
|
2
|
+
export type OAuthProvider = '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
3
|
export type OnError = (event: H3Event, error: H3Error) => Promise<void> | void;
|
|
4
4
|
export interface OAuthConfig<TConfig, TUser = any, TTokens = any> {
|
|
5
5
|
config?: TConfig;
|
|
@@ -45,4 +45,11 @@ export interface UserSessionComposable {
|
|
|
45
45
|
* Clear the user session and remove the session cookie.
|
|
46
46
|
*/
|
|
47
47
|
clear: () => Promise<void>;
|
|
48
|
+
/**
|
|
49
|
+
* Open the OAuth route in a popup that auto-closes when successful.
|
|
50
|
+
*/
|
|
51
|
+
openInPopup: (route: string, size?: {
|
|
52
|
+
width?: number;
|
|
53
|
+
height?: number;
|
|
54
|
+
}) => void;
|
|
48
55
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nuxt-auth-utils",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.11",
|
|
4
4
|
"description": "Add Authentication to Nuxt applications with secured & sealed cookies sessions.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
"defu": "^6.1.4",
|
|
41
41
|
"h3": "^1.14.0",
|
|
42
42
|
"hookable": "^5.5.3",
|
|
43
|
+
"jose": "^5.9.6",
|
|
43
44
|
"ofetch": "^1.4.1",
|
|
44
45
|
"ohash": "^1.1.4",
|
|
45
46
|
"openid-client": "^6.1.7",
|