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.
importJWTfrom'@neabyte/secure-token'// create instance with secret, version, expireInconstjwt=newJWT({secret: 'super-secret',version: '1.0.0',expireIn: '1h'})// issue tokenconsttoken=awaitjwt.sign({userId: '123',role: 'admin'})// check without reading payloadconstisValid=awaitjwt.verify(token)// decrypt and get payloadconstpayload=awaitjwt.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. |
constoptions={secret: 'super-secret',version: '1.0.0',expireIn: '1h',algorithm: 'aes-128-gcm',// optionalissuer: 'secure-token',// optionalcipher: undefined// optional; pluggable cipher}constpayload={userId: 123,role: 'admin'}consttoken=awaitjwt.sign(payload)constisValid=awaitjwt.verify(token)constpayload=awaitjwt.decode(token)decode throws on invalid, expired, or wrong-version tokens. All decode failures use the same message 'Invalid token' (no information leakage).
try{constpayload=awaitjwt.decode(token)// use payload}catch(err){constmsg=errinstanceofError ? 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).
constjwt=newJWT({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.
importJWT,{typeCipher,typeTokenEncrypted}from'@neabyte/secure-token'constmyCipher: Cipher={asyncencrypt(plaintext,secret,keySizeBytes,issuer,version){// use secret, issuer, version; return { encrypted, iv, tag } hex (iv 24, tag 32 chars)return{encrypted: '...',iv: '...',tag: '...'}},asyncdecrypt(token: TokenEncrypted,secret,keySizeBytes,issuer,version){// use same secret, issuer, version; return plaintext stringreturn'...'}}constjwt=newJWT({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.
awaitjwt.sign('user-123')// stringawaitjwt.sign(42)// numberawaitjwt.sign(true)// booleanawaitjwt.sign(['a','b','c'])// arrayawaitjwt.sign({a: 1,b: 2})// objectconsttoken=awaitjwt.sign(['a',1,false])constdata=awaitjwt.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{consttoken=awaitjwt.sign({userId: '123'})constisValid=awaitjwt.verify(token)// false if invalid; does not throwif(!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.