@kevisual/auth 1.0.5-alpha.1 → 2.0.0
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/bun.config.ts +20 -0
- package/package.json +18 -50
- package/readme.md +45 -18
- package/src/auth.ts +123 -0
- package/src/generate.ts +39 -0
- package/src/index.ts +1 -6
- package/src/jwks/common.ts +8 -0
- package/src/jwks/create.ts +23 -0
- package/src/jwks/get.ts +22 -0
- package/src/router.ts +2 -0
- package/test/create.ts +38 -0
- package/dist/create-token.d.ts +0 -18
- package/dist/create-token.js +0 -6339
- package/dist/index.d.ts +0 -49
- package/dist/index.js +0 -7506
- package/dist/proxy.d.ts +0 -41
- package/dist/proxy.js +0 -98
- package/dist/salt.d.ts +0 -22
- package/dist/salt.js +0 -1130
- package/src/create-token.ts +0 -29
- package/src/proxy.ts +0 -115
- package/src/route.ts +0 -44
- package/src/salt.ts +0 -32
package/bun.config.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { resolvePath } from '@kevisual/use-config';
|
|
2
|
+
import { execSync } from 'node:child_process';
|
|
3
|
+
|
|
4
|
+
const entry = 'src/index.ts';
|
|
5
|
+
const naming = 'app';
|
|
6
|
+
const external = ['pm2'];
|
|
7
|
+
await Bun.build({
|
|
8
|
+
target: 'browser',
|
|
9
|
+
format: 'esm',
|
|
10
|
+
entrypoints: [resolvePath(entry, { meta: import.meta })],
|
|
11
|
+
outdir: resolvePath('./dist', { meta: import.meta }),
|
|
12
|
+
naming: {
|
|
13
|
+
entry: `${naming}.js`,
|
|
14
|
+
},
|
|
15
|
+
external,
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
const cmd ='dts -i src/index.ts -o app.d.ts'
|
|
19
|
+
execSync(cmd, { stdio: 'inherit' });
|
|
20
|
+
console.log('Build completed.');
|
package/package.json
CHANGED
|
@@ -1,61 +1,29 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kevisual/auth",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "",
|
|
5
|
-
"main": "dist/index.js",
|
|
6
|
-
"type": "module",
|
|
7
|
-
"typings": "dist/index.d.js",
|
|
8
|
-
"private": false,
|
|
9
5
|
"scripts": {
|
|
10
|
-
"build": "
|
|
11
|
-
"clean": "rimraf dist"
|
|
12
|
-
},
|
|
13
|
-
"files": [
|
|
14
|
-
"src",
|
|
15
|
-
"dist"
|
|
16
|
-
],
|
|
17
|
-
"keywords": [
|
|
18
|
-
"router",
|
|
19
|
-
"auth"
|
|
20
|
-
],
|
|
21
|
-
"author": "",
|
|
22
|
-
"license": "ISC",
|
|
23
|
-
"peerDependencies": {
|
|
24
|
-
"@kevisual/router": ">=0.0.4"
|
|
6
|
+
"build": "bun run bun.config.ts"
|
|
25
7
|
},
|
|
8
|
+
"keywords": [],
|
|
9
|
+
"author": "abearxiong <xiongxiao@xiongxiao.me> (https://www.xiongxiao.me)",
|
|
10
|
+
"license": "MIT",
|
|
11
|
+
"packageManager": "pnpm@10.26.0",
|
|
12
|
+
"type": "module",
|
|
13
|
+
"dependencies": {},
|
|
26
14
|
"devDependencies": {
|
|
27
|
-
"@
|
|
28
|
-
"@
|
|
29
|
-
"@
|
|
30
|
-
"@
|
|
31
|
-
"@types/
|
|
32
|
-
"
|
|
33
|
-
"
|
|
34
|
-
"
|
|
35
|
-
"rollup-plugin-dts": "^6.1.1",
|
|
36
|
-
"tslib": "^2.8.1"
|
|
37
|
-
},
|
|
38
|
-
"repository": {
|
|
39
|
-
"type": "git",
|
|
40
|
-
"url": "git+https://github.com/abearxiong/kevisual-router.git"
|
|
15
|
+
"@kevisual/types": "^0.0.12",
|
|
16
|
+
"@kevisual/use-config": "^1.0.28",
|
|
17
|
+
"@kevisual/query": "^0.0.38",
|
|
18
|
+
"@kevisual/router": "^0.0.60",
|
|
19
|
+
"@types/bun": "^1.3.6",
|
|
20
|
+
"@types/node": "^25.0.10",
|
|
21
|
+
"es-toolkit": "^1.44.0",
|
|
22
|
+
"jose": "^6.1.3"
|
|
41
23
|
},
|
|
42
24
|
"exports": {
|
|
43
|
-
".":
|
|
44
|
-
|
|
45
|
-
"require": "./dist/index.js"
|
|
46
|
-
},
|
|
47
|
-
"./token": {
|
|
48
|
-
"import": "./dist/create-token.js",
|
|
49
|
-
"require": "./dist/create-token.js"
|
|
50
|
-
},
|
|
51
|
-
"./salt": {
|
|
52
|
-
"import": "./dist/create-token.js",
|
|
53
|
-
"require": "./dist/create-token.js"
|
|
54
|
-
},
|
|
55
|
-
"./proxy": {
|
|
56
|
-
"import": "./dist/proxy.js",
|
|
57
|
-
"require": "./dist/proxy.js"
|
|
58
|
-
}
|
|
25
|
+
".": "./dist/app.js",
|
|
26
|
+
"./src/*": "./dist/src/*"
|
|
59
27
|
},
|
|
60
28
|
"publishConfig": {
|
|
61
29
|
"access": "public"
|
package/readme.md
CHANGED
|
@@ -1,26 +1,53 @@
|
|
|
1
|
-
|
|
1
|
+
## JWT Configuration
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
### Convex auth.config.ts
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
issuer: https://convex.kevisual.cn
|
|
6
|
+
applicationID: convex-app
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
}
|
|
8
|
+
issuer必须与JWT中的iss字段匹配,applicationID必须与aud字段匹配。
|
|
9
|
+
|
|
10
|
+
```ts
|
|
11
|
+
import { AuthConfig } from 'convex/server';
|
|
12
|
+
|
|
13
|
+
export default {
|
|
14
|
+
providers: [
|
|
15
|
+
{
|
|
16
|
+
type: 'customJwt',
|
|
17
|
+
applicationID: 'convex-app',
|
|
18
|
+
issuer: 'https://convex.kevisual.cn',
|
|
19
|
+
jwks: 'https://api-convex.kevisual.cn/root/convex/jwks.json',
|
|
20
|
+
algorithm: 'RS256',
|
|
21
|
+
},
|
|
22
|
+
],
|
|
23
|
+
};
|
|
11
24
|
```
|
|
12
25
|
|
|
13
|
-
|
|
26
|
+
### Payload 例子
|
|
27
|
+
|
|
28
|
+
header必须包含kid字段以匹配jwks中的密钥ID。
|
|
14
29
|
|
|
15
30
|
```ts
|
|
16
|
-
import
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
31
|
+
import * as jose from "jose";
|
|
32
|
+
// 加载测试私钥
|
|
33
|
+
const keys = JSON.parse(await Bun.file("./jwt/privateKey.json").text());
|
|
34
|
+
const privateKey = await jose.importJWK(keys, "RS256");
|
|
35
|
+
|
|
36
|
+
// 生成 RS256 JWT
|
|
37
|
+
const payload = {
|
|
38
|
+
iss: "https://convex.kevisual.cn",
|
|
39
|
+
sub: "user:8fa2be73c2229e85",
|
|
40
|
+
aud: "convex-app",
|
|
41
|
+
exp: Math.floor(Date.now() / 1000) + 3600,
|
|
42
|
+
name: "Test User AA",
|
|
43
|
+
email: "test@example.com",
|
|
44
|
+
};
|
|
45
|
+
const token = await new jose.SignJWT(payload)
|
|
46
|
+
.setProtectedHeader({
|
|
47
|
+
"alg": "RS256",
|
|
48
|
+
"typ": "JWT",
|
|
49
|
+
"kid": "kid-key-1"
|
|
50
|
+
})
|
|
51
|
+
.setIssuedAt()
|
|
52
|
+
.sign(privateKey);
|
|
26
53
|
```
|
package/src/auth.ts
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import * as jose from 'jose';
|
|
2
|
+
|
|
3
|
+
/***
|
|
4
|
+
* 验证 JWT
|
|
5
|
+
* @param token JWT 字符串
|
|
6
|
+
* @param publicKey 公钥,可以是 CryptoKey、PEM 字符串或 JWK/JWKS 对象
|
|
7
|
+
* @returns 解码后的有效载荷
|
|
8
|
+
* @throws 如果验证失败或令牌无效,则抛出错误
|
|
9
|
+
*/
|
|
10
|
+
export async function verifyJWT(token: string, publicKey: jose.CryptoKey | string | jose.JWK) {
|
|
11
|
+
try {
|
|
12
|
+
let key: any;
|
|
13
|
+
if (typeof publicKey === 'string') {
|
|
14
|
+
key = await jose.importSPKI(publicKey, 'RS256')
|
|
15
|
+
} else if (typeof publicKey === 'object' && publicKey !== null && 'keys' in publicKey) {
|
|
16
|
+
// JWKS 格式
|
|
17
|
+
key = jose.createLocalJWKSet(publicKey as jose.JSONWebKeySet);
|
|
18
|
+
} else {
|
|
19
|
+
key = publicKey;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const { payload } = await jose.jwtVerify(token, key, {
|
|
23
|
+
algorithms: ['RS256'],
|
|
24
|
+
});
|
|
25
|
+
return payload;
|
|
26
|
+
} catch (error) {
|
|
27
|
+
if (error instanceof jose.errors.JWTExpired) {
|
|
28
|
+
console.error('JWT has expired:');
|
|
29
|
+
throw new Error('Token has expired');
|
|
30
|
+
}
|
|
31
|
+
console.error('JWT verification failed:', error);
|
|
32
|
+
throw new Error('Invalid token');
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
export type JWTPayload<T = {}> = {
|
|
36
|
+
/**
|
|
37
|
+
* 会设置默认值: "https://convex.kevisual.cn"
|
|
38
|
+
*/
|
|
39
|
+
iss?: string,
|
|
40
|
+
/**
|
|
41
|
+
* "user:8fa2be73c2229e85"
|
|
42
|
+
* "ip:192.168.1.1"
|
|
43
|
+
*/
|
|
44
|
+
sub: string,
|
|
45
|
+
/**
|
|
46
|
+
* 会设置默认值:"convex-app"
|
|
47
|
+
*/
|
|
48
|
+
aud?: string,
|
|
49
|
+
/**
|
|
50
|
+
* 会设置默认值:当前时间 + 2 小时
|
|
51
|
+
*/
|
|
52
|
+
exp?: number,
|
|
53
|
+
/**
|
|
54
|
+
* 会设置默认值:当前时间
|
|
55
|
+
*/
|
|
56
|
+
iat?: number,
|
|
57
|
+
/**
|
|
58
|
+
* 其他自定义字段
|
|
59
|
+
*/
|
|
60
|
+
name?: string,
|
|
61
|
+
/**
|
|
62
|
+
* email
|
|
63
|
+
*/
|
|
64
|
+
email?: string
|
|
65
|
+
} & T;
|
|
66
|
+
|
|
67
|
+
/***
|
|
68
|
+
* 签发 JWT
|
|
69
|
+
* @param payload 有效载荷
|
|
70
|
+
* @param privateKey 私钥,可以是 CryptoKey、PEM 字符串或 JWK 对象
|
|
71
|
+
* @returns 签发的 JWT 字符串
|
|
72
|
+
*/
|
|
73
|
+
export async function signJWT(payload: JWTPayload, privateKey: jose.CryptoKey | string | jose.JWK): Promise<string> {
|
|
74
|
+
const expirationTime = Math.floor(Date.now() / 1000) + (2 * 60 * 60); // 2 hour from now
|
|
75
|
+
if (!payload.exp) {
|
|
76
|
+
payload.exp = expirationTime;
|
|
77
|
+
}
|
|
78
|
+
let cryptoKey: jose.CryptoKey;
|
|
79
|
+
if (typeof privateKey === 'string') {
|
|
80
|
+
cryptoKey = await jose.importPKCS8(privateKey, "RS256") as jose.CryptoKey;
|
|
81
|
+
} else if (typeof privateKey === 'object' && privateKey !== null && 'kty' in privateKey) {
|
|
82
|
+
cryptoKey = await jose.importJWK(privateKey, "RS256") as jose.CryptoKey;
|
|
83
|
+
} else {
|
|
84
|
+
cryptoKey = privateKey as jose.CryptoKey;
|
|
85
|
+
}
|
|
86
|
+
const iss = payload.iss || "https://convex.kevisual.cn";
|
|
87
|
+
if (!payload.iss) {
|
|
88
|
+
payload.iss = iss;
|
|
89
|
+
}
|
|
90
|
+
if (!payload.iat) {
|
|
91
|
+
payload.iat = Math.floor(Date.now() / 1000);
|
|
92
|
+
}
|
|
93
|
+
if (!payload.aud) {
|
|
94
|
+
payload.aud = "convex-app";
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const token = await new jose.SignJWT(payload)
|
|
98
|
+
.setProtectedHeader({
|
|
99
|
+
"alg": "RS256",
|
|
100
|
+
"typ": "JWT",
|
|
101
|
+
"kid": "kid-key-1"
|
|
102
|
+
})
|
|
103
|
+
.setIssuedAt()
|
|
104
|
+
.setExpirationTime(payload.exp)
|
|
105
|
+
.setIssuer(iss)
|
|
106
|
+
.setSubject(payload.sub || "")
|
|
107
|
+
.sign(cryptoKey);
|
|
108
|
+
return token;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* 单独解码 JWT,不验证签名
|
|
113
|
+
* @param token
|
|
114
|
+
* @returns
|
|
115
|
+
*/
|
|
116
|
+
export const decodeJWT = (token: string): JWTPayload => {
|
|
117
|
+
try {
|
|
118
|
+
const decoded = jose.decodeJwt(token);
|
|
119
|
+
return decoded as JWTPayload;
|
|
120
|
+
} catch (error) {
|
|
121
|
+
throw new Error('Invalid token');
|
|
122
|
+
}
|
|
123
|
+
}
|
package/src/generate.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import * as jose from 'jose';
|
|
2
|
+
|
|
3
|
+
async function generateKeyPair() {
|
|
4
|
+
const { privateKey, publicKey } = await jose.generateKeyPair('RS256', {
|
|
5
|
+
modulusLength: 2048,
|
|
6
|
+
extractable: true,
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
return { privateKey, publicKey };
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async function createJWKS(publicKey: CryptoKey, kid?: string) {
|
|
13
|
+
const jwk = await jose.exportJWK(publicKey);
|
|
14
|
+
// 添加 kid 字段
|
|
15
|
+
jwk.kid = kid || 'kid-key-1';
|
|
16
|
+
const jwks = {
|
|
17
|
+
keys: [jwk]
|
|
18
|
+
};
|
|
19
|
+
return jwks;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
type GenerateOpts = {
|
|
23
|
+
kid?: string;
|
|
24
|
+
}
|
|
25
|
+
export const generate = async (opts: GenerateOpts = {}) => {
|
|
26
|
+
const { privateKey, publicKey } = await generateKeyPair();
|
|
27
|
+
const jwks = await createJWKS(publicKey, opts.kid);
|
|
28
|
+
|
|
29
|
+
// 将私钥和 JWKS 保存到文件
|
|
30
|
+
const privateJWK = await jose.exportJWK(privateKey);
|
|
31
|
+
const privatePEM = await jose.exportPKCS8(privateKey);
|
|
32
|
+
const publicPEM = await jose.exportSPKI(publicKey);
|
|
33
|
+
return {
|
|
34
|
+
jwks,
|
|
35
|
+
privateJWK,
|
|
36
|
+
privatePEM,
|
|
37
|
+
publicPEM
|
|
38
|
+
}
|
|
39
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import { createToken, checkToken } from './create-token.ts';
|
|
3
|
-
|
|
4
|
-
export { getRandomSalt, cryptPwd, checkPwd, createToken, checkToken };
|
|
5
|
-
|
|
6
|
-
export { createAuthRoute } from './route.ts';
|
|
1
|
+
export * from './auth.ts';
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
const dir = path.join(process.cwd(), 'jwt');
|
|
3
|
+
|
|
4
|
+
export const JWKS_PATH = path.join(dir, 'jwks.json');
|
|
5
|
+
export const PRIVATE_JWK_PATH = path.join(dir, 'privateKey.json');
|
|
6
|
+
|
|
7
|
+
export const PRIVATE_KEY_PATH = path.join(dir, 'privateKey.txt');
|
|
8
|
+
export const PUBLIC_KEY_PATH = path.join(dir, 'publicKey.txt');
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
|
|
2
|
+
import fs from 'node:fs'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import * as common from './common.ts';
|
|
5
|
+
import { generate } from '../generate.ts'
|
|
6
|
+
|
|
7
|
+
async function main() {
|
|
8
|
+
const { jwks, privateJWK, privatePEM, publicPEM } = await generate();
|
|
9
|
+
const dir = path.join(process.cwd(), 'jwt');
|
|
10
|
+
if (!fs.existsSync(dir)) {
|
|
11
|
+
fs.mkdirSync(dir);
|
|
12
|
+
}
|
|
13
|
+
fs.writeFileSync(common.PUBLIC_KEY_PATH, publicPEM);
|
|
14
|
+
|
|
15
|
+
fs.writeFileSync(common.PRIVATE_KEY_PATH, privatePEM);
|
|
16
|
+
|
|
17
|
+
fs.writeFileSync(common.PRIVATE_JWK_PATH, JSON.stringify(privateJWK, null, 2));
|
|
18
|
+
fs.writeFileSync(common.JWKS_PATH, JSON.stringify(jwks, null, 2));
|
|
19
|
+
|
|
20
|
+
console.log('Private key and JWKS have been saved to files.');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
main().catch(console.error);
|
package/src/jwks/get.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
|
|
2
|
+
import fs from 'node:fs'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import * as common from './common.ts';
|
|
5
|
+
|
|
6
|
+
export const getValues = () => {
|
|
7
|
+
if (!fs.existsSync(common.JWKS_PATH)) {
|
|
8
|
+
throw new Error('JWKS file does not exist. Please create it first.');
|
|
9
|
+
}
|
|
10
|
+
const jwksData = fs.readFileSync(common.JWKS_PATH, 'utf-8');
|
|
11
|
+
const privateJWKData = fs.readFileSync(common.PRIVATE_JWK_PATH, 'utf-8');
|
|
12
|
+
const publicKeyPEM = fs.readFileSync(common.PUBLIC_KEY_PATH, 'utf-8');
|
|
13
|
+
const privateKeyPEM = fs.readFileSync(common.PRIVATE_KEY_PATH, 'utf-8');
|
|
14
|
+
|
|
15
|
+
return {
|
|
16
|
+
jwks: JSON.parse(jwksData),
|
|
17
|
+
privateJWK: JSON.parse(privateJWKData),
|
|
18
|
+
publicKeyPEM,
|
|
19
|
+
privateKeyPEM
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
package/src/router.ts
ADDED
package/test/create.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import * as jose from 'jose';
|
|
2
|
+
import { signJWT, decodeJWT, verifyJWT } from '../src/auth.ts';
|
|
3
|
+
import { getValues } from '../src/jwks/get.ts';
|
|
4
|
+
|
|
5
|
+
const payload = {
|
|
6
|
+
iss: "https://convex.kevisual.cn",
|
|
7
|
+
sub: "user:123456",
|
|
8
|
+
aud: "convex-app",
|
|
9
|
+
name: "John Doe",
|
|
10
|
+
email: "john.doe@example.com",
|
|
11
|
+
// exp: Math.floor(Date.now() / 1000) - (2 * 60 * 60) // 2 hours from now
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const createToken = async () => {
|
|
15
|
+
const { privateJWK, privateKeyPEM } = getValues();
|
|
16
|
+
const token = await signJWT(payload, privateKeyPEM);
|
|
17
|
+
console.log('Generated JWT:', token);
|
|
18
|
+
|
|
19
|
+
// console.log('expited at:', new Date(payload.exp * 1000).toLocaleString());
|
|
20
|
+
return token;
|
|
21
|
+
}
|
|
22
|
+
const token = await createToken()
|
|
23
|
+
|
|
24
|
+
const decode = decodeJWT(token)
|
|
25
|
+
|
|
26
|
+
console.log('Decoded JWT:', decode);
|
|
27
|
+
|
|
28
|
+
const verify = async () => {
|
|
29
|
+
const { publicKeyPEM, privateJWK, jwks } = getValues();
|
|
30
|
+
try {
|
|
31
|
+
const verifiedPayload = await verifyJWT(token, publicKeyPEM);
|
|
32
|
+
console.log('Verified JWT payload:', verifiedPayload);
|
|
33
|
+
} catch (error) {
|
|
34
|
+
console.error('Verification failed:', error);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
await verify();
|
package/dist/create-token.d.ts
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import jwt from 'jsonwebtoken';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
*
|
|
5
|
-
* @param user
|
|
6
|
-
* @param secret
|
|
7
|
-
*
|
|
8
|
-
* @param expiresIn default 7d expressed in seconds or a string describing a time span [zeit/ms](https://github.com/zeit/ms.js). Eg: 60, "2 days", "10h", "7d"
|
|
9
|
-
* @returns
|
|
10
|
-
*/
|
|
11
|
-
declare const createToken: (user: {
|
|
12
|
-
id: string;
|
|
13
|
-
username: string;
|
|
14
|
-
[key: string]: any;
|
|
15
|
-
}, secret: string, expiresIn?: string) => Promise<string>;
|
|
16
|
-
declare const checkToken: (token: string, secret: string) => Promise<jwt.Jwt>;
|
|
17
|
-
|
|
18
|
-
export { checkToken, createToken };
|