Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

History

6 Commits

Repository files navigation

Secure Token

DenoJSRCILicense

Protect credentials with confidential tokens ensuring integrity and privacy.

Table of Contents

Installation

deno add jsr:@neabyte/secure-token

Note

Requires Deno. The package is published on JSR.

Quick Start

Only 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)

Methods Overview

MethodInputReturnsDescription
jwt.sign(data)JSON-serializablePromise<string>Encrypt payload, return Base64 token.
jwt.verify(token)token stringPromise<boolean>Validate structure, expiry, version, decrypt; returns true/false (does not throw).
jwt.decode(token)token stringPromise<unknown>Decrypt and return original payload data.

Options

JWTOptions for the constructor:

OptionTypeRequiredDescription
secretstringyesSecret used to derive the encryption key.
versionstringyesApplication token version (bound in AAD).
expireInstringyesDuration, e.g. '30m', '7d', '500ms' (max 1y).
algorithm'aes-128-gcm' | 'aes-256-gcm'noDefault: 'aes-128-gcm'.
issuerstringnoIssuer bound in AAD. Default: 'secure-token'.
cipherCiphernoCustom 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}

Basic Usage

Sign a payload

constpayload={userId: 123,role: 'admin'}consttoken=awaitjwt.sign(payload)

Verify a token

constisValid=awaitjwt.verify(token)

Decode a 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}}

Using AES-256-GCM

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'})

Custom cipher

  • You can plug in a custom encrypt/decrypt implementation via the Cipher interface (see exported type Cipher). It must expose encrypt(plaintext, secret, keySizeBytes, issuer, version) and decrypt(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 bind issuer and version (e.g. as AAD) so tokens cannot be used across issuer/version. Ignoring them breaks isolation.

  • Return shape:encrypt must return { encrypted, iv, tag } as hex strings; iv 24 hex chars (12 bytes), tag 32 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.

Sign non-object data

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]

API Reference

constructor(options)

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 expireIn to milliseconds.

jwt.sign(data)

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.

jwt.verify(token)

Use when you only need to know if the token is valid (no payload).

  • token<string>: Token string from sign.
  • Returns: <Promise<boolean>>true if valid, false if invalid or expired. Does not throw.
  • Validates structure, expiration, version, and decryptability.

jwt.decode(token)

Use when you need the decrypted payload from the token.

  • token<string>: Token string from sign.
  • Returns: <Promise<unknown>> Original data.
  • 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)}

How This Works

End-to-end flow

  • Sign: Validate input → build payload { data, iat, exp, version } → encrypt with AES-GCM (AAD: issuer-version) → assemble TokenData → return btoa(JSON.stringify(TokenData)).
  • Decode: Validate string → atob → parse TokenData → check exp and version → decrypt with AAD → validate payload → return payload.data.
  • Verify: Same as decode; returns true on success, false on failure.

Security properties

  • AES-GCM provides confidentiality and integrity (auth tag).
  • AAD binds tokens to issuer and version.
  • Expiration enforced; max 1 year.
  • Outer and inner { iat, exp, version } must match.

Token Format

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.

Environment & Compatibility

Note

Requires WebCrypto (crypto.subtle). Supported in Deno (version in badge). No Node polyfills; use in Deno without extra flags.

Tips & Best Practices

Start with Error Handling and Verify vs Decode for integration; then pick by use case below.

Security Considerations

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.

Troubleshooting

IssueCause / fix
Invalid time formatexpireIn 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/algorithmChanging any of these invalidates existing tokens.

Testing

deno task test

Contributing

Contributions welcome. Open a Pull Request.

License

This project is licensed under the MIT license. See the LICENSE file for details.