Protect credentials with confidential tokens ensuring integrity and privacy.
- Installation
- Quick Start
- Methods Overview
- Options
- Basic Usage
- API Reference
- How This Works
- Token Format
- Environment & Compatibility
- Tips & Best Practices
- Troubleshooting
- Testing
- License
deno add jsr:@neabyte/secure-tokenOnly secret, version, and expireIn are required. See Options for full config.
Tip
Use try/catch for sign and decode; they throw on failure. verify returns true/false and does not throw.
import JWT from '@neabyte/secure-token'
// create instance with secret, version, expireIn
const jwt = new JWT({
secret: 'super-secret',
version: '1.0.0',
expireIn: '1h'
})
// issue token
const token = await jwt.sign({ userId: '123', role: 'admin' })
// check without reading payload
const isValid = await jwt.verify(token)
// decrypt and get payload
const payload = await jwt.decode(token)| Method | Input | Returns | Description |
|---|---|---|---|
jwt.sign(data) |
JSON-serializable | Promise<string> |
Encrypt payload, return Base64 token. |
jwt.verify(token) |
token string | Promise<boolean> |
Validate structure, expiry, version, decrypt; returns true/false (does not throw). |
jwt.decode(token) |
token string | Promise<unknown> |
Decrypt and return original payload data. |
JWTOptions for the constructor:
| Option | Type | Required | Description |
|---|---|---|---|
secret |
string |
yes | Secret used to derive the encryption key. |
version |
string |
yes | Application token version (bound in AAD). |
expireIn |
string |
yes | Duration, e.g. '30m', '7d', '500ms' (max 1y). |
algorithm |
'aes-128-gcm' | 'aes-256-gcm' |
no | Default: 'aes-128-gcm'. |
issuer |
string |
no | Issuer bound in AAD. Default: 'secure-token'. |
cipher |
Cipher |
no | Custom encrypt/decrypt; default uses AES-GCM. |
const options = {
secret: 'super-secret',
version: '1.0.0',
expireIn: '1h',
algorithm: 'aes-128-gcm', // optional
issuer: 'secure-token', // optional
cipher: undefined // optional; pluggable cipher
}const payload = { userId: 123, role: 'admin' }
const token = await jwt.sign(payload)const isValid = await jwt.verify(token)const payload = await jwt.decode(token)decode throws on invalid, expired, or wrong-version tokens. All decode failures use the same message 'Invalid token' (no information leakage).
try {
const payload = await jwt.decode(token)
// use payload
} catch (err) {
const msg = err instanceof Error ? err.message : ''
if (msg === 'Invalid token') {
// invalid, expired, wrong version, or wrong secret — re-login or refresh
}
}Pass algorithm: 'aes-256-gcm' for a 32-byte key. Use a secret long enough (e.g. 32+ chars or a 32-byte base64 value from a secure generator).
const jwt = new JWT({
secret: 'your-secret-at-least-32-chars-long!!!!!!!!',
version: '1.0.0',
expireIn: '1h',
algorithm: 'aes-256-gcm'
})-
You can plug in a custom encrypt/decrypt implementation via the
Cipherinterface (see exported typeCipher). It must exposeencrypt(plaintext, secret, keySizeBytes, issuer, version)anddecrypt(token, secret, keySizeBytes, issuer, version), both returning a Promise. -
Contract (isolation): The implementation must use
secret(for key derivation or lookup) so tokens are isolated per secret, and must bindissuerandversion(e.g. as AAD) so tokens cannot be used across issuer/version. Ignoring them breaks isolation. -
Return shape:
encryptmust return{ encrypted, iv, tag }as hex strings;iv24 hex chars (12 bytes),tag32 hex chars (16 bytes) for compatibility with the default path.
import JWT, { type Cipher, type TokenEncrypted } from '@neabyte/secure-token'
const myCipher: Cipher = {
async encrypt(plaintext, secret, keySizeBytes, issuer, version) {
// use secret, issuer, version; return { encrypted, iv, tag } hex (iv 24, tag 32 chars)
return { encrypted: '...', iv: '...', tag: '...' }
},
async decrypt(token: TokenEncrypted, secret, keySizeBytes, issuer, version) {
// use same secret, issuer, version; return plaintext string
return '...'
}
}
const jwt = new JWT({
secret: 'secret',
version: '1.0.0',
expireIn: '1h',
cipher: myCipher
})See Custom Cipher for a full contract-compliant example.
Payload is not limited to objects. You can sign strings, numbers, booleans, or arrays; decode returns the same value. Only null and undefined are rejected.
await jwt.sign('user-123') // string
await jwt.sign(42) // number
await jwt.sign(true) // boolean
await jwt.sign(['a', 'b', 'c']) // array
await jwt.sign({ a: 1, b: 2 }) // object
const token = await jwt.sign(['a', 1, false])
const data = await jwt.decode(token) // data === ['a', 1, false]Use when creating a JWT instance with secret, version, and expiration.
options<JWTOptions>:secret,version,expireIn(required);algorithm?,issuer?,cipher?.- Returns:
<JWT>. - Validates options and parses
expireInto milliseconds.
Use when you need to issue a token from arbitrary JSON-serializable data.
data<unknown>: Payload to embed (object, array, string, number, boolean).- Returns:
<Promise<string>>Base64 token. - Encrypts with AES-GCM, AAD
issuer-version; throws on invalid input.
Use when you only need to know if the token is valid (no payload).
token<string>: Token string fromsign.- Returns:
<Promise<boolean>>—trueif valid,falseif invalid or expired. Does not throw. - Validates structure, expiration, version, and decryptability.
Use when you need the decrypted payload from the token.
token<string>: Token string fromsign.- Returns:
<Promise<unknown>>Originaldata. - Decrypts and validates; throws on invalid, expired, or version mismatch.
Important
sign and decode throw on failure (invalid input, invalid format, version mismatch, expired, decryption error). verify does not throw; it returns false when the token is invalid. Use try/catch for sign and decode.
try {
const token = await jwt.sign({ userId: '123' })
const isValid = await jwt.verify(token) // false if invalid; does not throw
if (!isValid) {
// token invalid or expired
}
} catch (error) {
// sign failed (e.g. null/undefined payload)
}- Sign: Validate input → build payload
{ data, iat, exp, version }→ encrypt with AES-GCM (AAD:issuer-version) → assembleTokenData→ returnbtoa(JSON.stringify(TokenData)). - Decode: Validate string →
atob→ parseTokenData→ checkexpandversion→ decrypt with AAD → validate payload → returnpayload.data. - Verify: Same as decode; returns
trueon success,falseon failure.
- AES-GCM provides confidentiality and integrity (auth tag).
- AAD binds tokens to
issuerandversion. - Expiration enforced; max 1 year.
- Outer and inner
{ iat, exp, version }must match.
Base64-encoded JSON:
{
"encrypted": "<hex>",
"iv": "<hex>",
"tag": "<hex>",
"exp": 1735689600,
"iat": 1735686000,
"version": "1.0.0"
}Inner payload { data, iat, exp, version } is JSON-encoded then AES-GCM encrypted to produce the hex fields.
Note
Requires WebCrypto (crypto.subtle). Supported in Deno (version in badge). No Node polyfills; use in Deno without extra flags.
Start with Error Handling and Verify vs Decode for integration; then pick by use case below.
- Salt Secrets With a Strong Random Generator
- Token Reissue During Rotation Window
- Refresh Token Pattern (Rolling Expiration)
- Multi-Issuer Setup (Microservices)
- Custom Cipher — KMS, key from env, or different algorithm.
- ExpireIn in Practice — Access vs refresh vs API key; max 1y and NTP.
- Versioning Schema — Breaking changes and migration window.
- Error Handling — Map error messages to refresh, upgrade, or 401.
- Serverless and Env-Based Secrets — One JWT instance per process; Deno Deploy, Fly, Cloud Run.
- Typed Payload — Interface, cast or type guard after decode, handler usage.
- Verify vs Decode — When to use verify (middleware) vs decode (handler); snippets.
Important
- Use strong, unique secrets per environment.
- Keep secrets out of source control; use secret managers.
- Ensure time sync (NTP) on all nodes; expiration depends on accurate clocks.
- Keep payload small (ids and minimal claims); token size grows with payload.
| Issue | Cause / fix |
|---|---|
| Invalid time format | expireIn must be digits + unit: ms, s, m, h, d, M, y (e.g. 1h, 30m). |
| Invalid token (decode throws) | Token invalid, expired, wrong version, or wrong secret; re-login or refresh. |
| Wrong issuer/secret/algorithm | Changing any of these invalidates existing tokens. |
deno task testContributions welcome. Open a Pull Request.
This project is licensed under the MIT license. See the LICENSE file for details.