unjwt 0.1.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/LICENSE +21 -0
- package/README.md +70 -0
- package/dist/index.d.mts +78 -0
- package/dist/index.d.ts +78 -0
- package/dist/index.mjs +117 -0
- package/package.json +50 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023-PRESENT Johann Schopplich
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# unjwt
|
|
2
|
+
|
|
3
|
+
> Easy creation and verification of JSON Web Tokens (JWT).
|
|
4
|
+
|
|
5
|
+
You may just use [Jose](https://github.com/panva/jose) or one of the other libraries out there. But I wanted to take my shot at creating a JWT library that is easy to use and has zero dependencies.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- 💨 Zero dependencies
|
|
10
|
+
- 📦 Works in every context supporting the Web Crypto API
|
|
11
|
+
|
|
12
|
+
## Installation
|
|
13
|
+
|
|
14
|
+
Run the following command to add `unjwt` to your project.
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
# pnpm
|
|
18
|
+
pnpm add -D unjwt
|
|
19
|
+
|
|
20
|
+
# npm
|
|
21
|
+
npm install -D unjwt
|
|
22
|
+
|
|
23
|
+
# yarn
|
|
24
|
+
yarn add -D unjwt
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Usage
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
import { signJWT, verifyJWT } from 'unjwt'
|
|
31
|
+
|
|
32
|
+
interface JWTUserClaims {
|
|
33
|
+
email: string
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const secret = 'secret'
|
|
37
|
+
const issuer = 'https://domain.com'
|
|
38
|
+
|
|
39
|
+
// Sign a JWT
|
|
40
|
+
const accessToken = await signJWT<JWTUserClaims>({
|
|
41
|
+
payload: {
|
|
42
|
+
email: 'user@domain.com'
|
|
43
|
+
},
|
|
44
|
+
secret,
|
|
45
|
+
issuer,
|
|
46
|
+
audience: issuer,
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
// Verify a JWT
|
|
50
|
+
try {
|
|
51
|
+
const verifiedAccessToken = await verifyJWT({
|
|
52
|
+
token: accessToken,
|
|
53
|
+
secret,
|
|
54
|
+
issuer,
|
|
55
|
+
audience: issuer
|
|
56
|
+
})
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
// Handle error
|
|
60
|
+
console.error(error)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Decode a JWT – does not verify the signature
|
|
64
|
+
const decodedAccessToken = await decodeJWT<JWTUserClaims>(accessToken)
|
|
65
|
+
console.log(decodedAccessToken.email)
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## License
|
|
69
|
+
|
|
70
|
+
[MIT](./LICENSE) License © 2023-PRESENT [Johann Schopplich](https://github.com/johannschopplich)
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/** Recognized JWT Claims Set members */
|
|
2
|
+
interface JWTRegisteredClaims {
|
|
3
|
+
/**
|
|
4
|
+
* JWT Issuer
|
|
5
|
+
*
|
|
6
|
+
* @see {@link https://www.rfc-editor.org/rfc/rfc7519#section-4.1.1 RFC7519#section-4.1.1}
|
|
7
|
+
*/
|
|
8
|
+
iss?: string;
|
|
9
|
+
/**
|
|
10
|
+
* JWT Subject
|
|
11
|
+
*
|
|
12
|
+
* @see {@link https://www.rfc-editor.org/rfc/rfc7519#section-4.1.2 RFC7519#section-4.1.2}
|
|
13
|
+
*/
|
|
14
|
+
sub?: string;
|
|
15
|
+
/**
|
|
16
|
+
* JWT Audience
|
|
17
|
+
*
|
|
18
|
+
* @see {@link https://www.rfc-editor.org/rfc/rfc7519#section-4.1.3 RFC7519#section-4.1.3}
|
|
19
|
+
*/
|
|
20
|
+
aud?: string | string[];
|
|
21
|
+
/**
|
|
22
|
+
* JWT ID
|
|
23
|
+
*
|
|
24
|
+
* @see {@link https://www.rfc-editor.org/rfc/rfc7519#section-4.1.7 RFC7519#section-4.1.7}
|
|
25
|
+
*/
|
|
26
|
+
jti?: string;
|
|
27
|
+
/**
|
|
28
|
+
* JWT Not Before
|
|
29
|
+
*
|
|
30
|
+
* @see {@link https://www.rfc-editor.org/rfc/rfc7519#section-4.1.5 RFC7519#section-4.1.5}
|
|
31
|
+
*/
|
|
32
|
+
nbf?: number;
|
|
33
|
+
/**
|
|
34
|
+
* JWT Expiration Time
|
|
35
|
+
*
|
|
36
|
+
* @see {@link https://www.rfc-editor.org/rfc/rfc7519#section-4.1.4 RFC7519#section-4.1.4}
|
|
37
|
+
*/
|
|
38
|
+
exp?: number;
|
|
39
|
+
/**
|
|
40
|
+
* JWT Issued At
|
|
41
|
+
*
|
|
42
|
+
* @see {@link https://www.rfc-editor.org/rfc/rfc7519#section-4.1.6 RFC7519#section-4.1.6}
|
|
43
|
+
*/
|
|
44
|
+
iat?: number;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
declare function signJWT<T extends Record<string, any> = Record<string, any>>({ payload, secret, issuer, audience, expires, header, signatureMethod, hashMethod, }: {
|
|
48
|
+
payload: T;
|
|
49
|
+
secret: string;
|
|
50
|
+
issuer: string;
|
|
51
|
+
audience: string;
|
|
52
|
+
/**
|
|
53
|
+
* Expiration time in days
|
|
54
|
+
* @default 30
|
|
55
|
+
*/
|
|
56
|
+
expires?: number;
|
|
57
|
+
/** @default { typ: 'JWT', alg: 'HS256' } */
|
|
58
|
+
header?: Record<string, any>;
|
|
59
|
+
/** @default 'HMAC' */
|
|
60
|
+
signatureMethod?: string;
|
|
61
|
+
/** @default 'SHA-256' */
|
|
62
|
+
hashMethod?: string;
|
|
63
|
+
}): Promise<string>;
|
|
64
|
+
declare function verifyJWT({ token, secret, issuer, audience, leeway, signatureMethod, hashMethod, }: {
|
|
65
|
+
token: string;
|
|
66
|
+
secret: string;
|
|
67
|
+
issuer: string;
|
|
68
|
+
audience: string;
|
|
69
|
+
/** @default 60 */
|
|
70
|
+
leeway?: number;
|
|
71
|
+
/** @default 'HMAC' */
|
|
72
|
+
signatureMethod?: string;
|
|
73
|
+
/** @default 'SHA-256' */
|
|
74
|
+
hashMethod?: string;
|
|
75
|
+
}): Promise<void>;
|
|
76
|
+
declare function decodeJWT<T extends Record<string, any> = Record<string, any>>(token: string): JWTRegisteredClaims & T;
|
|
77
|
+
|
|
78
|
+
export { JWTRegisteredClaims, decodeJWT, signJWT, verifyJWT };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/** Recognized JWT Claims Set members */
|
|
2
|
+
interface JWTRegisteredClaims {
|
|
3
|
+
/**
|
|
4
|
+
* JWT Issuer
|
|
5
|
+
*
|
|
6
|
+
* @see {@link https://www.rfc-editor.org/rfc/rfc7519#section-4.1.1 RFC7519#section-4.1.1}
|
|
7
|
+
*/
|
|
8
|
+
iss?: string;
|
|
9
|
+
/**
|
|
10
|
+
* JWT Subject
|
|
11
|
+
*
|
|
12
|
+
* @see {@link https://www.rfc-editor.org/rfc/rfc7519#section-4.1.2 RFC7519#section-4.1.2}
|
|
13
|
+
*/
|
|
14
|
+
sub?: string;
|
|
15
|
+
/**
|
|
16
|
+
* JWT Audience
|
|
17
|
+
*
|
|
18
|
+
* @see {@link https://www.rfc-editor.org/rfc/rfc7519#section-4.1.3 RFC7519#section-4.1.3}
|
|
19
|
+
*/
|
|
20
|
+
aud?: string | string[];
|
|
21
|
+
/**
|
|
22
|
+
* JWT ID
|
|
23
|
+
*
|
|
24
|
+
* @see {@link https://www.rfc-editor.org/rfc/rfc7519#section-4.1.7 RFC7519#section-4.1.7}
|
|
25
|
+
*/
|
|
26
|
+
jti?: string;
|
|
27
|
+
/**
|
|
28
|
+
* JWT Not Before
|
|
29
|
+
*
|
|
30
|
+
* @see {@link https://www.rfc-editor.org/rfc/rfc7519#section-4.1.5 RFC7519#section-4.1.5}
|
|
31
|
+
*/
|
|
32
|
+
nbf?: number;
|
|
33
|
+
/**
|
|
34
|
+
* JWT Expiration Time
|
|
35
|
+
*
|
|
36
|
+
* @see {@link https://www.rfc-editor.org/rfc/rfc7519#section-4.1.4 RFC7519#section-4.1.4}
|
|
37
|
+
*/
|
|
38
|
+
exp?: number;
|
|
39
|
+
/**
|
|
40
|
+
* JWT Issued At
|
|
41
|
+
*
|
|
42
|
+
* @see {@link https://www.rfc-editor.org/rfc/rfc7519#section-4.1.6 RFC7519#section-4.1.6}
|
|
43
|
+
*/
|
|
44
|
+
iat?: number;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
declare function signJWT<T extends Record<string, any> = Record<string, any>>({ payload, secret, issuer, audience, expires, header, signatureMethod, hashMethod, }: {
|
|
48
|
+
payload: T;
|
|
49
|
+
secret: string;
|
|
50
|
+
issuer: string;
|
|
51
|
+
audience: string;
|
|
52
|
+
/**
|
|
53
|
+
* Expiration time in days
|
|
54
|
+
* @default 30
|
|
55
|
+
*/
|
|
56
|
+
expires?: number;
|
|
57
|
+
/** @default { typ: 'JWT', alg: 'HS256' } */
|
|
58
|
+
header?: Record<string, any>;
|
|
59
|
+
/** @default 'HMAC' */
|
|
60
|
+
signatureMethod?: string;
|
|
61
|
+
/** @default 'SHA-256' */
|
|
62
|
+
hashMethod?: string;
|
|
63
|
+
}): Promise<string>;
|
|
64
|
+
declare function verifyJWT({ token, secret, issuer, audience, leeway, signatureMethod, hashMethod, }: {
|
|
65
|
+
token: string;
|
|
66
|
+
secret: string;
|
|
67
|
+
issuer: string;
|
|
68
|
+
audience: string;
|
|
69
|
+
/** @default 60 */
|
|
70
|
+
leeway?: number;
|
|
71
|
+
/** @default 'HMAC' */
|
|
72
|
+
signatureMethod?: string;
|
|
73
|
+
/** @default 'SHA-256' */
|
|
74
|
+
hashMethod?: string;
|
|
75
|
+
}): Promise<void>;
|
|
76
|
+
declare function decodeJWT<T extends Record<string, any> = Record<string, any>>(token: string): JWTRegisteredClaims & T;
|
|
77
|
+
|
|
78
|
+
export { JWTRegisteredClaims, decodeJWT, signJWT, verifyJWT };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
const DEFAULT_SIGNATURE_METHOD = "HMAC";
|
|
2
|
+
const DEFAULT_HASH_METHODS = "SHA-256";
|
|
3
|
+
const textEncoder = new TextEncoder();
|
|
4
|
+
async function createSignature(key, algorithm, data) {
|
|
5
|
+
const encoded = textEncoder.encode(data);
|
|
6
|
+
const signature = await crypto.subtle.sign(algorithm, key, encoded);
|
|
7
|
+
return encodeToBase64Url(signature);
|
|
8
|
+
}
|
|
9
|
+
async function importKey({
|
|
10
|
+
secret,
|
|
11
|
+
usage = "sign",
|
|
12
|
+
name = DEFAULT_SIGNATURE_METHOD,
|
|
13
|
+
hash = DEFAULT_HASH_METHODS
|
|
14
|
+
}) {
|
|
15
|
+
const encodedKey = textEncoder.encode(secret);
|
|
16
|
+
return await crypto.subtle.importKey(
|
|
17
|
+
"raw",
|
|
18
|
+
encodedKey,
|
|
19
|
+
{ name, hash },
|
|
20
|
+
false,
|
|
21
|
+
[usage]
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
function encodeToBase64Url(input) {
|
|
25
|
+
let data;
|
|
26
|
+
if (typeof input === "string")
|
|
27
|
+
data = textEncoder.encode(input);
|
|
28
|
+
else if (input instanceof ArrayBuffer)
|
|
29
|
+
data = new Uint8Array(input);
|
|
30
|
+
else
|
|
31
|
+
data = textEncoder.encode(JSON.stringify(input));
|
|
32
|
+
const base64 = btoa(String.fromCodePoint(...data));
|
|
33
|
+
return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
34
|
+
}
|
|
35
|
+
function decodeFromBase64Url(base64Url) {
|
|
36
|
+
const base64 = base64Url.replace(/-/g, "+").replace(/_/g, "/");
|
|
37
|
+
return atob(base64);
|
|
38
|
+
}
|
|
39
|
+
function decodeFromBase64UrlToBuffer(base64Url) {
|
|
40
|
+
const bytes = decodeFromBase64Url(base64Url);
|
|
41
|
+
const buffer = Uint8Array.from(bytes, (c) => c.codePointAt(0));
|
|
42
|
+
return buffer.buffer;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function signJWT({
|
|
46
|
+
payload = {},
|
|
47
|
+
secret,
|
|
48
|
+
issuer,
|
|
49
|
+
audience,
|
|
50
|
+
expires = 30,
|
|
51
|
+
header = { typ: "JWT", alg: "HS256" },
|
|
52
|
+
signatureMethod = DEFAULT_SIGNATURE_METHOD,
|
|
53
|
+
hashMethod = DEFAULT_HASH_METHODS
|
|
54
|
+
}) {
|
|
55
|
+
const key = await importKey({
|
|
56
|
+
secret,
|
|
57
|
+
name: signatureMethod,
|
|
58
|
+
hash: hashMethod
|
|
59
|
+
});
|
|
60
|
+
const iat = Math.floor(Date.now() / 1e3);
|
|
61
|
+
const exp = iat + expires * 24 * 60 * 60;
|
|
62
|
+
const jwtPayload = {
|
|
63
|
+
...payload,
|
|
64
|
+
iss: issuer,
|
|
65
|
+
aud: audience,
|
|
66
|
+
iat,
|
|
67
|
+
exp
|
|
68
|
+
};
|
|
69
|
+
const signature = await createSignature(
|
|
70
|
+
key,
|
|
71
|
+
signatureMethod,
|
|
72
|
+
`${encodeToBase64Url(header)}.${encodeToBase64Url(jwtPayload)}`
|
|
73
|
+
);
|
|
74
|
+
return `${encodeToBase64Url(header)}.${encodeToBase64Url(
|
|
75
|
+
jwtPayload
|
|
76
|
+
)}.${signature}`;
|
|
77
|
+
}
|
|
78
|
+
async function verifyJWT({
|
|
79
|
+
token,
|
|
80
|
+
secret,
|
|
81
|
+
issuer,
|
|
82
|
+
audience,
|
|
83
|
+
leeway = 60,
|
|
84
|
+
signatureMethod = DEFAULT_SIGNATURE_METHOD,
|
|
85
|
+
hashMethod = DEFAULT_HASH_METHODS
|
|
86
|
+
}) {
|
|
87
|
+
const [header, payload, signature] = token.split(".");
|
|
88
|
+
const key = await importKey({
|
|
89
|
+
secret,
|
|
90
|
+
usage: "verify",
|
|
91
|
+
name: signatureMethod,
|
|
92
|
+
hash: hashMethod
|
|
93
|
+
});
|
|
94
|
+
const isValid = await crypto.subtle.verify(
|
|
95
|
+
signatureMethod,
|
|
96
|
+
key,
|
|
97
|
+
decodeFromBase64UrlToBuffer(signature),
|
|
98
|
+
textEncoder.encode(`${header}.${payload}`)
|
|
99
|
+
);
|
|
100
|
+
if (!isValid)
|
|
101
|
+
throw new Error("Invalid JWT signature");
|
|
102
|
+
const decodedPayload = JSON.parse(
|
|
103
|
+
decodeFromBase64Url(payload)
|
|
104
|
+
);
|
|
105
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
106
|
+
if (issuer !== decodedPayload.iss || (Array.isArray(decodedPayload.aud) ? !decodedPayload.aud.includes(audience) : audience !== decodedPayload.aud) || decodedPayload.exp && now > decodedPayload.exp + leeway || decodedPayload.nbf && now < decodedPayload.nbf - leeway)
|
|
107
|
+
throw new Error("Invalid JWT claims");
|
|
108
|
+
}
|
|
109
|
+
function decodeJWT(token) {
|
|
110
|
+
const parts = token.split(".");
|
|
111
|
+
if (parts.length !== 3)
|
|
112
|
+
throw new Error("Invalid token format");
|
|
113
|
+
const [, payload] = parts;
|
|
114
|
+
return JSON.parse(decodeFromBase64Url(payload));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export { decodeJWT, signJWT, verifyJWT };
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "unjwt",
|
|
3
|
+
"type": "module",
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"packageManager": "pnpm@8.6.12",
|
|
6
|
+
"description": "Easy creation and verification of JSON Web Tokens (JWT)",
|
|
7
|
+
"author": "Johann Schopplich <pkg@johannschopplich.com>",
|
|
8
|
+
"license": "MIT",
|
|
9
|
+
"homepage": "https://github.com/johannschopplich/unjwt#readme",
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "git+https://github.com/johannschopplich/unjwt.git"
|
|
13
|
+
},
|
|
14
|
+
"bugs": {
|
|
15
|
+
"url": "https://github.com/johannschopplich/unjwt/issues"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"cloudflare",
|
|
19
|
+
"jwt",
|
|
20
|
+
"serverless",
|
|
21
|
+
"token",
|
|
22
|
+
"worker"
|
|
23
|
+
],
|
|
24
|
+
"sideEffects": false,
|
|
25
|
+
"exports": {
|
|
26
|
+
".": {
|
|
27
|
+
"types": "./dist/index.d.ts",
|
|
28
|
+
"import": "./dist/index.mjs"
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"types": "./dist/index.d.ts",
|
|
32
|
+
"files": [
|
|
33
|
+
"dist"
|
|
34
|
+
],
|
|
35
|
+
"scripts": {
|
|
36
|
+
"build": "unbuild",
|
|
37
|
+
"dev": "unbuild --stub",
|
|
38
|
+
"lint": "eslint .",
|
|
39
|
+
"lint:fix": "eslint . --fix",
|
|
40
|
+
"release": "bumpp --commit --push --tag",
|
|
41
|
+
"test:types": "tsc --noEmit"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@antfu/eslint-config": "^0.40.2",
|
|
45
|
+
"bumpp": "^9.1.1",
|
|
46
|
+
"eslint": "^8.47.0",
|
|
47
|
+
"typescript": "^5.1.6",
|
|
48
|
+
"unbuild": "^2.0.0-rc.0"
|
|
49
|
+
}
|
|
50
|
+
}
|