@zephkelly/nuxt-authkit 0.0.1
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 +170 -0
- package/dist/module.d.mts +6 -0
- package/dist/module.json +9 -0
- package/dist/module.mjs +80 -0
- package/dist/runtime/composables/useAuthkitToken.d.ts +25 -0
- package/dist/runtime/composables/useAuthkitToken.js +31 -0
- package/dist/runtime/helpers/request-middleware.d.ts +22 -0
- package/dist/runtime/helpers/request-middleware.js +82 -0
- package/dist/runtime/index.d.ts +8 -0
- package/dist/runtime/index.js +2 -0
- package/dist/runtime/plugins/auth-fetch.client.d.ts +13 -0
- package/dist/runtime/plugins/auth-fetch.client.js +91 -0
- package/dist/runtime/plugins/internal/request-target.d.ts +16 -0
- package/dist/runtime/plugins/internal/request-target.js +17 -0
- package/dist/runtime/roles/has-role.d.ts +21 -0
- package/dist/runtime/roles/has-role.js +51 -0
- package/dist/runtime/server/api/_auth/refresh-handler.d.ts +6 -0
- package/dist/runtime/server/api/_auth/refresh-handler.js +44 -0
- package/dist/runtime/server/api/_auth/refresh.post.d.ts +6 -0
- package/dist/runtime/server/api/_auth/refresh.post.js +5 -0
- package/dist/runtime/server/tsconfig.json +3 -0
- package/dist/runtime/service/index.d.ts +119 -0
- package/dist/runtime/service/index.js +170 -0
- package/dist/runtime/service/new-index.d.ts +24 -0
- package/dist/runtime/service/new-index.js +0 -0
- package/dist/runtime/service/types/async-service.d.ts +59 -0
- package/dist/runtime/service/types/async-service.js +0 -0
- package/dist/runtime/service/types/sync-service.d.ts +28 -0
- package/dist/runtime/service/types/sync-service.js +0 -0
- package/dist/runtime/service/utils/getJwtAsyncService.d.ts +11 -0
- package/dist/runtime/service/utils/getJwtAsyncService.js +16 -0
- package/dist/runtime/storage/nitro.d.ts +61 -0
- package/dist/runtime/storage/nitro.js +124 -0
- package/dist/runtime/storage/types.d.ts +27 -0
- package/dist/runtime/storage/types.js +0 -0
- package/dist/runtime/strategy/base.d.ts +40 -0
- package/dist/runtime/strategy/base.js +88 -0
- package/dist/runtime/strategy/jwt/callback.d.ts +60 -0
- package/dist/runtime/strategy/jwt/callback.js +0 -0
- package/dist/runtime/strategy/jwt/index.d.ts +137 -0
- package/dist/runtime/strategy/jwt/index.js +748 -0
- package/dist/runtime/strategy/jwt/jwt-crypto.d.ts +122 -0
- package/dist/runtime/strategy/jwt/jwt-crypto.js +298 -0
- package/dist/runtime/strategy/jwt/types.d.ts +68 -0
- package/dist/runtime/strategy/jwt/types.js +0 -0
- package/dist/runtime/strategy/types.d.ts +68 -0
- package/dist/runtime/strategy/types.js +0 -0
- package/dist/runtime/types/2fac.d.ts +32 -0
- package/dist/runtime/types/2fac.js +0 -0
- package/dist/runtime/types/authkit-types.d.ts +6 -0
- package/dist/runtime/types/authkit-types.js +0 -0
- package/dist/runtime/types/callbacks.d.ts +133 -0
- package/dist/runtime/types/callbacks.js +0 -0
- package/dist/runtime/types/cookie.d.ts +47 -0
- package/dist/runtime/types/cookie.js +0 -0
- package/dist/runtime/types/expand.d.ts +3 -0
- package/dist/runtime/types/expand.js +0 -0
- package/dist/runtime/types/index.d.ts +52 -0
- package/dist/runtime/types/index.js +0 -0
- package/dist/runtime/types/module-options.d.ts +33 -0
- package/dist/runtime/types/module-options.js +0 -0
- package/dist/runtime/types/runtime-config.d.ts +2 -0
- package/dist/runtime/types/runtime-config.js +47 -0
- package/dist/runtime/types/token.d.ts +42 -0
- package/dist/runtime/types/token.js +12 -0
- package/dist/runtime/utils/context-helpers.d.ts +4 -0
- package/dist/runtime/utils/context-helpers.js +10 -0
- package/dist/runtime/utils/get-module-options.d.ts +2 -0
- package/dist/runtime/utils/get-module-options.js +18 -0
- package/dist/runtime/utils/password.d.ts +23 -0
- package/dist/runtime/utils/password.js +18 -0
- package/dist/runtime/utils/uuid.d.ts +187 -0
- package/dist/runtime/utils/uuid.js +345 -0
- package/dist/types.d.mts +7 -0
- package/package.json +65 -0
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import type { TokenPayload } from '../../types/token.js';
|
|
2
|
+
export interface JWTCryptoConfig {
|
|
3
|
+
/**
|
|
4
|
+
* RSA private key in PEM format for signing tokens
|
|
5
|
+
*/
|
|
6
|
+
privateKey?: string;
|
|
7
|
+
/**
|
|
8
|
+
* RSA public key in PEM format for verifying tokens
|
|
9
|
+
*/
|
|
10
|
+
publicKey?: string;
|
|
11
|
+
/**
|
|
12
|
+
* Algorithm to use for signing (default: RS256)
|
|
13
|
+
*/
|
|
14
|
+
algorithm?: 'RS256' | 'RS384' | 'RS512';
|
|
15
|
+
/**
|
|
16
|
+
* Reject tokens longer than this before doing any parsing or signature work.
|
|
17
|
+
* @default 16384
|
|
18
|
+
*/
|
|
19
|
+
maxTokenLength?: number;
|
|
20
|
+
}
|
|
21
|
+
export interface JWTHeader {
|
|
22
|
+
alg: string;
|
|
23
|
+
typ: string;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Why a token was rejected. Callers map these to a stable, non-leaking error
|
|
27
|
+
* taxonomy — a malformed or badly-signed token is a hard failure, never a
|
|
28
|
+
* "recoverable" one that a client should retry.
|
|
29
|
+
*/
|
|
30
|
+
export type JWTCryptoErrorCode = 'TOKEN_MALFORMED' | 'TOKEN_SIGNATURE_INVALID' | 'TOKEN_ALGORITHM_MISMATCH' | 'TOKEN_TOO_LARGE' | 'KEY_UNAVAILABLE';
|
|
31
|
+
/**
|
|
32
|
+
* A token-level failure carrying a machine-readable code.
|
|
33
|
+
*
|
|
34
|
+
* The `message` is for server logs only — it distinguishes malformed-from-
|
|
35
|
+
* bad-signature and must never be relayed to a client, or it becomes an oracle.
|
|
36
|
+
*/
|
|
37
|
+
export declare class JWTCryptoError extends Error {
|
|
38
|
+
readonly code: JWTCryptoErrorCode;
|
|
39
|
+
constructor(code: JWTCryptoErrorCode, message: string);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Secure JWT token handler using RSA cryptography
|
|
43
|
+
* Supports RS256, RS384, and RS512 algorithms
|
|
44
|
+
*/
|
|
45
|
+
export declare class JWTCrypto {
|
|
46
|
+
private privateKey?;
|
|
47
|
+
private publicKey?;
|
|
48
|
+
private algorithm;
|
|
49
|
+
private maxTokenLength;
|
|
50
|
+
constructor(config: JWTCryptoConfig);
|
|
51
|
+
/**
|
|
52
|
+
* Sign and encode a JWT token
|
|
53
|
+
* @throws Error if private key is not configured
|
|
54
|
+
*/
|
|
55
|
+
sign(payload: TokenPayload): string;
|
|
56
|
+
/**
|
|
57
|
+
* Verify and decode a JWT token
|
|
58
|
+
* @throws Error if token is invalid or public key is not configured
|
|
59
|
+
*/
|
|
60
|
+
verify(token: string): TokenPayload;
|
|
61
|
+
/**
|
|
62
|
+
* Decode a token WITHOUT verifying its signature.
|
|
63
|
+
*
|
|
64
|
+
* The returned claims are attacker-controlled and must never be used for an
|
|
65
|
+
* authentication or authorization decision. Use {@link verify} for anything
|
|
66
|
+
* that matters. Kept only for diagnostics.
|
|
67
|
+
*
|
|
68
|
+
* @internal
|
|
69
|
+
* @deprecated Use {@link verify}. This performs no signature check.
|
|
70
|
+
*/
|
|
71
|
+
unsafeDecodeNoVerify(token: string): TokenPayload | null;
|
|
72
|
+
/**
|
|
73
|
+
* Decode token without verification (use with caution!)
|
|
74
|
+
* Useful for extracting claims before verification or when signature verification is not required
|
|
75
|
+
*
|
|
76
|
+
* @internal
|
|
77
|
+
* @deprecated Use {@link verify}. This performs no signature check, so its
|
|
78
|
+
* output is attacker-controlled.
|
|
79
|
+
*/
|
|
80
|
+
decode(token: string): TokenPayload | null;
|
|
81
|
+
private createSignature;
|
|
82
|
+
private verifySignature;
|
|
83
|
+
private getHashAlgorithm;
|
|
84
|
+
private parsePrivateKey;
|
|
85
|
+
private parsePublicKey;
|
|
86
|
+
/**
|
|
87
|
+
* The configured algorithm (RS*) names RSA. An EC or Ed25519 PEM would
|
|
88
|
+
* otherwise be accepted here and used under an "RS256" label.
|
|
89
|
+
*/
|
|
90
|
+
private assertRsaKey;
|
|
91
|
+
/**
|
|
92
|
+
* Encode string to base64url
|
|
93
|
+
*/
|
|
94
|
+
private base64UrlEncode;
|
|
95
|
+
/**
|
|
96
|
+
* Decode base64url string to UTF-8 string
|
|
97
|
+
*/
|
|
98
|
+
private base64UrlDecode;
|
|
99
|
+
/**
|
|
100
|
+
* Convert binary buffer to base64url (for signatures)
|
|
101
|
+
*/
|
|
102
|
+
private bufferToBase64Url;
|
|
103
|
+
/**
|
|
104
|
+
* Convert base64url to binary buffer (for signatures)
|
|
105
|
+
*/
|
|
106
|
+
private base64UrlToBuffer;
|
|
107
|
+
/**
|
|
108
|
+
* Generate a new RSA key pair for development/testing
|
|
109
|
+
* In production, use properly generated and stored keys
|
|
110
|
+
*
|
|
111
|
+
* Defaults to 3072 bits: NIST SP 800-57 rates 2048-bit RSA as adequate only
|
|
112
|
+
* through ~2030, which is inside the lifetime of a key minted today.
|
|
113
|
+
*/
|
|
114
|
+
static generateKeyPair(modulusLength?: 2048 | 3072 | 4096): {
|
|
115
|
+
privateKey: string;
|
|
116
|
+
publicKey: string;
|
|
117
|
+
};
|
|
118
|
+
/**
|
|
119
|
+
* Validate that a key pair matches (public key can verify tokens signed by private key)
|
|
120
|
+
*/
|
|
121
|
+
static validateKeyPair(privateKey: string, publicKey: string): boolean;
|
|
122
|
+
}
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
import { createSign, createVerify, generateKeyPairSync, createPrivateKey, createPublicKey } from "node:crypto";
|
|
2
|
+
export class JWTCryptoError extends Error {
|
|
3
|
+
code;
|
|
4
|
+
constructor(code, message) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.name = "JWTCryptoError";
|
|
7
|
+
this.code = code;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
const SUPPORTED_ALGORITHMS = ["RS256", "RS384", "RS512"];
|
|
11
|
+
const DEFAULT_MAX_TOKEN_LENGTH = 16384;
|
|
12
|
+
export class JWTCrypto {
|
|
13
|
+
privateKey;
|
|
14
|
+
publicKey;
|
|
15
|
+
algorithm;
|
|
16
|
+
maxTokenLength;
|
|
17
|
+
constructor(config) {
|
|
18
|
+
this.algorithm = config.algorithm || "RS256";
|
|
19
|
+
this.maxTokenLength = config.maxTokenLength ?? DEFAULT_MAX_TOKEN_LENGTH;
|
|
20
|
+
if (!SUPPORTED_ALGORITHMS.includes(this.algorithm)) {
|
|
21
|
+
throw new Error(
|
|
22
|
+
`Unsupported JWT algorithm "${this.algorithm}". Supported: ${SUPPORTED_ALGORITHMS.join(", ")}`
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
if (config.privateKey) {
|
|
26
|
+
try {
|
|
27
|
+
this.privateKey = this.parsePrivateKey(config.privateKey);
|
|
28
|
+
} catch (error) {
|
|
29
|
+
throw new Error(`Failed to parse private key: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
if (config.publicKey) {
|
|
33
|
+
try {
|
|
34
|
+
this.publicKey = this.parsePublicKey(config.publicKey);
|
|
35
|
+
} catch (error) {
|
|
36
|
+
throw new Error(`Failed to parse public key: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (!this.privateKey && !this.publicKey) {
|
|
40
|
+
throw new Error("At least one of privateKey or publicKey must be provided");
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Sign and encode a JWT token
|
|
45
|
+
* @throws Error if private key is not configured
|
|
46
|
+
*/
|
|
47
|
+
sign(payload) {
|
|
48
|
+
if (!this.privateKey) {
|
|
49
|
+
throw new Error("Private key is required for signing tokens");
|
|
50
|
+
}
|
|
51
|
+
const header = {
|
|
52
|
+
alg: this.algorithm,
|
|
53
|
+
typ: "JWT"
|
|
54
|
+
};
|
|
55
|
+
const encodedHeader = this.base64UrlEncode(JSON.stringify(header));
|
|
56
|
+
const encodedPayload = this.base64UrlEncode(JSON.stringify(payload));
|
|
57
|
+
const signatureInput = `${encodedHeader}.${encodedPayload}`;
|
|
58
|
+
const signature = this.createSignature(signatureInput);
|
|
59
|
+
return `${signatureInput}.${signature}`;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Verify and decode a JWT token
|
|
63
|
+
* @throws Error if token is invalid or public key is not configured
|
|
64
|
+
*/
|
|
65
|
+
verify(token) {
|
|
66
|
+
if (!this.publicKey) {
|
|
67
|
+
throw new JWTCryptoError("KEY_UNAVAILABLE", "Public key is required for verifying tokens");
|
|
68
|
+
}
|
|
69
|
+
if (typeof token !== "string" || token.length === 0) {
|
|
70
|
+
throw new JWTCryptoError("TOKEN_MALFORMED", "Invalid token format: not a string");
|
|
71
|
+
}
|
|
72
|
+
if (token.length > this.maxTokenLength) {
|
|
73
|
+
throw new JWTCryptoError(
|
|
74
|
+
"TOKEN_TOO_LARGE",
|
|
75
|
+
`Token exceeds maximum length of ${this.maxTokenLength} characters`
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
const parts = token.split(".");
|
|
79
|
+
if (parts.length !== 3) {
|
|
80
|
+
throw new JWTCryptoError("TOKEN_MALFORMED", "Invalid token format: expected 3 parts");
|
|
81
|
+
}
|
|
82
|
+
const [encodedHeader, encodedPayload, signature] = parts;
|
|
83
|
+
if (!encodedHeader || !encodedPayload || !signature) {
|
|
84
|
+
throw new JWTCryptoError("TOKEN_MALFORMED", "Invalid token format: missing parts");
|
|
85
|
+
}
|
|
86
|
+
const signatureInput = `${encodedHeader}.${encodedPayload}`;
|
|
87
|
+
const isValid = this.verifySignature(signatureInput, signature);
|
|
88
|
+
if (!isValid) {
|
|
89
|
+
throw new JWTCryptoError("TOKEN_SIGNATURE_INVALID", "Invalid token signature");
|
|
90
|
+
}
|
|
91
|
+
let header;
|
|
92
|
+
try {
|
|
93
|
+
header = JSON.parse(this.base64UrlDecode(encodedHeader));
|
|
94
|
+
} catch {
|
|
95
|
+
throw new JWTCryptoError("TOKEN_MALFORMED", "Invalid token header");
|
|
96
|
+
}
|
|
97
|
+
if (header.alg !== this.algorithm) {
|
|
98
|
+
throw new JWTCryptoError(
|
|
99
|
+
"TOKEN_ALGORITHM_MISMATCH",
|
|
100
|
+
`Algorithm mismatch: expected ${this.algorithm}, got ${header.alg}`
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
if (header.typ !== "JWT") {
|
|
104
|
+
throw new JWTCryptoError(
|
|
105
|
+
"TOKEN_MALFORMED",
|
|
106
|
+
`Invalid JOSE header typ: expected "JWT", got ${JSON.stringify(header.typ)}`
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
let payload;
|
|
110
|
+
try {
|
|
111
|
+
payload = JSON.parse(this.base64UrlDecode(encodedPayload));
|
|
112
|
+
} catch {
|
|
113
|
+
throw new JWTCryptoError("TOKEN_MALFORMED", "Invalid token payload");
|
|
114
|
+
}
|
|
115
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
|
|
116
|
+
throw new JWTCryptoError("TOKEN_MALFORMED", "Invalid token payload: not an object");
|
|
117
|
+
}
|
|
118
|
+
return payload;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Decode a token WITHOUT verifying its signature.
|
|
122
|
+
*
|
|
123
|
+
* The returned claims are attacker-controlled and must never be used for an
|
|
124
|
+
* authentication or authorization decision. Use {@link verify} for anything
|
|
125
|
+
* that matters. Kept only for diagnostics.
|
|
126
|
+
*
|
|
127
|
+
* @internal
|
|
128
|
+
* @deprecated Use {@link verify}. This performs no signature check.
|
|
129
|
+
*/
|
|
130
|
+
unsafeDecodeNoVerify(token) {
|
|
131
|
+
return this.decode(token);
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Decode token without verification (use with caution!)
|
|
135
|
+
* Useful for extracting claims before verification or when signature verification is not required
|
|
136
|
+
*
|
|
137
|
+
* @internal
|
|
138
|
+
* @deprecated Use {@link verify}. This performs no signature check, so its
|
|
139
|
+
* output is attacker-controlled.
|
|
140
|
+
*/
|
|
141
|
+
decode(token) {
|
|
142
|
+
try {
|
|
143
|
+
const parts = token.split(".");
|
|
144
|
+
if (parts.length !== 3) {
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
if (!parts[0] || !parts[1]) {
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
const payload = this.base64UrlDecode(parts[1]);
|
|
151
|
+
return JSON.parse(payload);
|
|
152
|
+
} catch {
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
createSignature(data) {
|
|
157
|
+
if (!this.privateKey) {
|
|
158
|
+
throw new Error("Private key is required for signing");
|
|
159
|
+
}
|
|
160
|
+
const hashAlgorithm = this.getHashAlgorithm();
|
|
161
|
+
const sign = createSign(hashAlgorithm);
|
|
162
|
+
sign.update(data);
|
|
163
|
+
sign.end();
|
|
164
|
+
const signature = sign.sign(this.privateKey);
|
|
165
|
+
return this.bufferToBase64Url(signature);
|
|
166
|
+
}
|
|
167
|
+
verifySignature(data, signature) {
|
|
168
|
+
if (!this.publicKey) {
|
|
169
|
+
throw new Error("Public key is required for verification");
|
|
170
|
+
}
|
|
171
|
+
try {
|
|
172
|
+
const hashAlgorithm = this.getHashAlgorithm();
|
|
173
|
+
const verify = createVerify(hashAlgorithm);
|
|
174
|
+
verify.update(data);
|
|
175
|
+
verify.end();
|
|
176
|
+
const signatureBuffer = this.base64UrlToBuffer(signature);
|
|
177
|
+
return verify.verify(this.publicKey, signatureBuffer);
|
|
178
|
+
} catch {
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
getHashAlgorithm() {
|
|
183
|
+
switch (this.algorithm) {
|
|
184
|
+
case "RS256":
|
|
185
|
+
return "RSA-SHA256";
|
|
186
|
+
case "RS384":
|
|
187
|
+
return "RSA-SHA384";
|
|
188
|
+
case "RS512":
|
|
189
|
+
return "RSA-SHA512";
|
|
190
|
+
default:
|
|
191
|
+
throw new Error(`Unsupported algorithm: ${this.algorithm}`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
parsePrivateKey(keyData) {
|
|
195
|
+
const key = createPrivateKey({
|
|
196
|
+
key: keyData,
|
|
197
|
+
format: "pem"
|
|
198
|
+
});
|
|
199
|
+
this.assertRsaKey(key, "private");
|
|
200
|
+
return key;
|
|
201
|
+
}
|
|
202
|
+
parsePublicKey(keyData) {
|
|
203
|
+
const key = createPublicKey({
|
|
204
|
+
key: keyData,
|
|
205
|
+
format: "pem"
|
|
206
|
+
});
|
|
207
|
+
this.assertRsaKey(key, "public");
|
|
208
|
+
return key;
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* The configured algorithm (RS*) names RSA. An EC or Ed25519 PEM would
|
|
212
|
+
* otherwise be accepted here and used under an "RS256" label.
|
|
213
|
+
*/
|
|
214
|
+
assertRsaKey(key, kind) {
|
|
215
|
+
if (key.asymmetricKeyType !== "rsa" && key.asymmetricKeyType !== "rsa-pss") {
|
|
216
|
+
throw new Error(
|
|
217
|
+
`Expected an RSA ${kind} key for ${this.algorithm}, got "${key.asymmetricKeyType}"`
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Encode string to base64url
|
|
223
|
+
*/
|
|
224
|
+
base64UrlEncode(str) {
|
|
225
|
+
const base64 = Buffer.from(str, "utf-8").toString("base64");
|
|
226
|
+
return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Decode base64url string to UTF-8 string
|
|
230
|
+
*/
|
|
231
|
+
base64UrlDecode(str) {
|
|
232
|
+
let base64 = str.replace(/-/g, "+").replace(/_/g, "/");
|
|
233
|
+
const pad = base64.length % 4;
|
|
234
|
+
if (pad) {
|
|
235
|
+
base64 += "=".repeat(4 - pad);
|
|
236
|
+
}
|
|
237
|
+
return Buffer.from(base64, "base64").toString("utf-8");
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Convert binary buffer to base64url (for signatures)
|
|
241
|
+
*/
|
|
242
|
+
bufferToBase64Url(buffer) {
|
|
243
|
+
return buffer.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Convert base64url to binary buffer (for signatures)
|
|
247
|
+
*/
|
|
248
|
+
base64UrlToBuffer(str) {
|
|
249
|
+
let base64 = str.replace(/-/g, "+").replace(/_/g, "/");
|
|
250
|
+
const pad = base64.length % 4;
|
|
251
|
+
if (pad) {
|
|
252
|
+
base64 += "=".repeat(4 - pad);
|
|
253
|
+
}
|
|
254
|
+
return Buffer.from(base64, "base64");
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Generate a new RSA key pair for development/testing
|
|
258
|
+
* In production, use properly generated and stored keys
|
|
259
|
+
*
|
|
260
|
+
* Defaults to 3072 bits: NIST SP 800-57 rates 2048-bit RSA as adequate only
|
|
261
|
+
* through ~2030, which is inside the lifetime of a key minted today.
|
|
262
|
+
*/
|
|
263
|
+
static generateKeyPair(modulusLength = 3072) {
|
|
264
|
+
const { publicKey, privateKey } = generateKeyPairSync("rsa", {
|
|
265
|
+
modulusLength,
|
|
266
|
+
publicKeyEncoding: {
|
|
267
|
+
type: "spki",
|
|
268
|
+
format: "pem"
|
|
269
|
+
},
|
|
270
|
+
privateKeyEncoding: {
|
|
271
|
+
type: "pkcs8",
|
|
272
|
+
format: "pem"
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
return {
|
|
276
|
+
privateKey,
|
|
277
|
+
publicKey
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Validate that a key pair matches (public key can verify tokens signed by private key)
|
|
282
|
+
*/
|
|
283
|
+
static validateKeyPair(privateKey, publicKey) {
|
|
284
|
+
try {
|
|
285
|
+
const crypto = new JWTCrypto({ privateKey, publicKey });
|
|
286
|
+
const testPayload = {
|
|
287
|
+
sub: "test",
|
|
288
|
+
iat: Math.floor(Date.now() / 1e3),
|
|
289
|
+
exp: Math.floor(Date.now() / 1e3) + 60
|
|
290
|
+
};
|
|
291
|
+
const token = crypto.sign(testPayload);
|
|
292
|
+
const verified = crypto.verify(token);
|
|
293
|
+
return verified.sub === testPayload.sub;
|
|
294
|
+
} catch {
|
|
295
|
+
return false;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { NuxtAuthkitCookieOptions } from '../../types/cookie.js';
|
|
2
|
+
/**
|
|
3
|
+
* JWT Strategy Configuration
|
|
4
|
+
*/
|
|
5
|
+
export interface JwtStrategyConfig {
|
|
6
|
+
name: string;
|
|
7
|
+
tokens: {
|
|
8
|
+
access: {
|
|
9
|
+
expiresIn: number;
|
|
10
|
+
};
|
|
11
|
+
refresh?: {
|
|
12
|
+
expiresIn: number;
|
|
13
|
+
storageKey?: string;
|
|
14
|
+
cookie?: NuxtAuthkitCookieOptions;
|
|
15
|
+
/**
|
|
16
|
+
* Rotate the refresh token on every successful refresh: a new
|
|
17
|
+
* refresh token is issued, stored via `onStoreRefreshToken`, and
|
|
18
|
+
* the presented one is invalidated via `onRemoveRefreshToken`.
|
|
19
|
+
*
|
|
20
|
+
* Disabling this means a leaked refresh token stays valid for its
|
|
21
|
+
* full lifetime and cannot be contained server-side.
|
|
22
|
+
*
|
|
23
|
+
* @default true
|
|
24
|
+
*/
|
|
25
|
+
rotate?: boolean;
|
|
26
|
+
};
|
|
27
|
+
};
|
|
28
|
+
autoRefresh?: {
|
|
29
|
+
enabled: boolean;
|
|
30
|
+
beforeExpiry: number;
|
|
31
|
+
};
|
|
32
|
+
jwt?: {
|
|
33
|
+
issuer?: string;
|
|
34
|
+
audience?: string | string[];
|
|
35
|
+
/**
|
|
36
|
+
* Static claims added to every access token.
|
|
37
|
+
*
|
|
38
|
+
* Reserved claims (`sub`, `exp`, `iat`, `nbf`, `iss`, `aud`, `jti`,
|
|
39
|
+
* `roles`, `typ`, `family`) are rejected at strategy construction —
|
|
40
|
+
* they are computed per request and must not be overridden.
|
|
41
|
+
*/
|
|
42
|
+
claims?: Record<string, unknown>;
|
|
43
|
+
algorithm?: 'RS256' | 'RS384' | 'RS512';
|
|
44
|
+
privateKey: string;
|
|
45
|
+
publicKey: string;
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
export interface JWTServerRevokeOptions {
|
|
49
|
+
userId?: string;
|
|
50
|
+
exceptCredentialId?: string;
|
|
51
|
+
expiredOnly?: boolean;
|
|
52
|
+
refreshTokenId?: string;
|
|
53
|
+
}
|
|
54
|
+
export interface JWTVerifyOptions {
|
|
55
|
+
accessToken?: string;
|
|
56
|
+
ignoreExpiration?: boolean;
|
|
57
|
+
}
|
|
58
|
+
export interface JWTRefreshOptions {
|
|
59
|
+
/**
|
|
60
|
+
* Override `tokens.refresh.rotate` for this call.
|
|
61
|
+
*/
|
|
62
|
+
rotate?: boolean;
|
|
63
|
+
/**
|
|
64
|
+
* @deprecated Use `rotate`. Rotation is now on by default; this flag is
|
|
65
|
+
* honoured only for backwards compatibility with 0.0.x callers.
|
|
66
|
+
*/
|
|
67
|
+
refreshToken?: boolean;
|
|
68
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { H3Event } from "h3";
|
|
2
|
+
import type { VerificationType } from "../types/2fac.js";
|
|
3
|
+
import type { AuthResult, VerifyAuthResult } from "../types/index.js";
|
|
4
|
+
import type { AuthStorage } from "../storage/types.js";
|
|
5
|
+
import type { AuthkitCredentials } from "../types/authkit-types.js";
|
|
6
|
+
export interface StrategyConfig {
|
|
7
|
+
/** Strategy name */
|
|
8
|
+
name: string;
|
|
9
|
+
/** Enable 2FA support */
|
|
10
|
+
twoFactor?: {
|
|
11
|
+
enabled: boolean;
|
|
12
|
+
methods: VerificationType[];
|
|
13
|
+
/** Grace period before requiring 2FA (in seconds) */
|
|
14
|
+
gracePeriod?: number;
|
|
15
|
+
};
|
|
16
|
+
/** Token configuration (strategy-specific) */
|
|
17
|
+
tokens?: unknown;
|
|
18
|
+
/** Storage configuration */
|
|
19
|
+
storage?: AuthStorage;
|
|
20
|
+
/** Custom options */
|
|
21
|
+
options?: Record<string, unknown>;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Base interface that all authentication strategies must implement
|
|
25
|
+
*/
|
|
26
|
+
export interface AuthStrategyGeneral<TCredentials = AuthkitCredentials> {
|
|
27
|
+
/** Strategy name (e.g., 'jwt', 'session', 'oauth') */
|
|
28
|
+
readonly name: string;
|
|
29
|
+
/** Configuration */
|
|
30
|
+
readonly config: StrategyConfig;
|
|
31
|
+
/**
|
|
32
|
+
* Authenticate user with credentials
|
|
33
|
+
* Returns tokens and user data on success
|
|
34
|
+
*/
|
|
35
|
+
authenticate(credentials: TCredentials, event?: H3Event, options?: any): Promise<AuthResult>;
|
|
36
|
+
/**
|
|
37
|
+
* Verify a token and return payload
|
|
38
|
+
* Used for protected routes
|
|
39
|
+
*/
|
|
40
|
+
verify(event?: H3Event, options?: any): VerifyAuthResult;
|
|
41
|
+
/**
|
|
42
|
+
* Verify a bare token with no request context — WebSocket peers, workers,
|
|
43
|
+
* signed asset URLs. Optional: not every strategy has a context-free token.
|
|
44
|
+
*/
|
|
45
|
+
verifyToken?(token: string): VerifyAuthResult;
|
|
46
|
+
/**
|
|
47
|
+
* Mint a session for a subject the app has already identified by other means
|
|
48
|
+
* (OAuth callback, accepted invite, impersonation), bypassing credentials.
|
|
49
|
+
* Optional: strategies without a token pair to hand out need not implement it.
|
|
50
|
+
*/
|
|
51
|
+
issueTokens?(userId: string, event?: H3Event, options?: any): Promise<AuthResult>;
|
|
52
|
+
/**
|
|
53
|
+
* Refresh tokens using refresh token
|
|
54
|
+
*/
|
|
55
|
+
refresh(event?: H3Event, options?: any): Promise<AuthResult>;
|
|
56
|
+
/**
|
|
57
|
+
* Revoke/invalidate tokens
|
|
58
|
+
* Used during logout
|
|
59
|
+
*/
|
|
60
|
+
revoke(event?: H3Event): Promise<void>;
|
|
61
|
+
/**
|
|
62
|
+
* Server-initiated revocation (admin force-logout, scheduled cleanup).
|
|
63
|
+
* Implementations must fail loudly if they cannot actually revoke anything.
|
|
64
|
+
*/
|
|
65
|
+
revokeServer?(options?: any, event?: H3Event): Promise<void>;
|
|
66
|
+
}
|
|
67
|
+
export interface IAuthStrategy<TCredentials = AuthkitCredentials> extends AuthStrategyGeneral<TCredentials> {
|
|
68
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export type VerificationType = 'totp' | 'sms' | 'email' | 'backup_code' | 'webauthn' | 'push';
|
|
2
|
+
export interface TwoFactorChallenge {
|
|
3
|
+
/** Unique challenge ID */
|
|
4
|
+
id: string;
|
|
5
|
+
/** Type of 2FA */
|
|
6
|
+
type: VerificationType;
|
|
7
|
+
/** When the challenge expires */
|
|
8
|
+
expiresAt: number;
|
|
9
|
+
/** Additional data needed for the challenge */
|
|
10
|
+
data?: {
|
|
11
|
+
/** For TOTP: QR code data URL (during setup) */
|
|
12
|
+
qrCode?: string;
|
|
13
|
+
/** For TOTP: Secret key (during setup) */
|
|
14
|
+
secret?: string;
|
|
15
|
+
/** For SMS/Email: Masked destination */
|
|
16
|
+
destination?: string;
|
|
17
|
+
/** For WebAuthn: Challenge data */
|
|
18
|
+
challenge?: unknown;
|
|
19
|
+
};
|
|
20
|
+
/** Available methods if multiple 2FA methods enabled */
|
|
21
|
+
availableMethods?: VerificationType[];
|
|
22
|
+
}
|
|
23
|
+
export interface TwoFactorVerification {
|
|
24
|
+
/** The challenge ID */
|
|
25
|
+
challengeId: string;
|
|
26
|
+
/** The verification code/response */
|
|
27
|
+
code: string;
|
|
28
|
+
/** Type of verification being provided */
|
|
29
|
+
type: VerificationType;
|
|
30
|
+
/** Whether to remember this device */
|
|
31
|
+
trustDevice?: boolean;
|
|
32
|
+
}
|
|
File without changes
|
|
File without changes
|