Encoding

Strict base64url and base64 codecs with the JOSE rules fixed in, plus the byte, text and randomness helpers that go with them.

import { base64UrlParse, base64UrlStringify } from "unjwt/utils";

base64UrlStringify('{"alg":"HS256","typ":"JWT"}'); // "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"
base64UrlParse("aGVsbG8"); // "hello"

Every segment of a JWT is base64url text. These helpers are the codecs from unsecure with the alphabet and padding rules pinned, so you cannot call them with a policy that disagrees with the one verify() and decrypt() apply.

#Signatures

function base64UrlStringify(data: string | BytesSource): string;
function base64UrlParse(
  input: string | Uint8Array,
  options?: { returnAs?: "string" | "uint8array" | "bytes"; loose?: boolean },
): string | Uint8Array<ArrayBuffer>;

function base64Stringify(data: string | BytesSource): string;
function base64Parse(
  input: string | Uint8Array,
  options?: { returnAs?: "string" | "uint8array" | "bytes"; loose?: boolean },
): string | Uint8Array<ArrayBuffer>;

BytesSource is an ArrayBuffer or any ArrayBufferView. A string passed to a *Stringify function is UTF-8 encoded first.

#Options

OptionTypeDefaultWhat it does
returnAs"string" | "uint8array" | "bytes"mirrors the input"string" decodes the bytes as UTF-8. "uint8array" and its alias "bytes" return the raw bytes.
loosebooleanfalseDrop characters the alphabet does not carry instead of throwing. For application data only.

#What unjwt adds to the unsecure codecs

FunctionAlphabetPadding
base64UrlStringifyURL-safe -_never written
base64UrlParseURL-safe -_ onlyany = is rejected
base64Stringifystandard +/always written
base64Parsestandard +/ onlythe = run is required

The URL-safe pair follows RFC 7515 §2: no padding, no whitespace, no line breaks. The standard pair follows RFC 4648 §4 and is the one PEM bodies use.

#Return shape

You passedYou get back
a stringa UTF-8 string, which must be valid UTF-8
a Uint8Array of the encoded texta Uint8Array<ArrayBuffer>
anything, plus returnAsexactly what returnAs names
import {
  base64Parse,
  base64Stringify,
  base64UrlParse,
  base64UrlStringify,
  textEncoder,
} from "unjwt/utils";

base64UrlStringify("hello"); // "aGVsbG8"
base64UrlStringify(new Uint8Array([255, 255])); // "__8"
base64Stringify("hello"); // "aGVsbG8="
base64Stringify(new Uint8Array([255, 255])); // "//8="

base64UrlParse("aGVsbG8", { returnAs: "bytes" }); // Uint8Array(5) [104, 101, 108, 108, 111]
base64UrlParse(textEncoder.encode("aGVsbG8")); // Uint8Array(5) [104, 101, 108, 108, 111]
base64UrlParse(textEncoder.encode("aGVsbG8"), { returnAs: "string" }); // "hello"
base64Parse("aGVsbG8="); // "hello"

When the decoded bytes are not text, ask for bytes. Decoding "__8" to a string throws, because 0xFF 0xFF is not UTF-8.

base64UrlParse("__8", { returnAs: "bytes" }); // Uint8Array(2) [255, 255]
base64UrlParse("__8");
// UnsecureError MALFORMED: Base64.parse: decoded bytes are not valid UTF-8.

#Strict by default

parse accepts only the canonical encoding of a byte string. Two different texts that decode to the same bytes are how signature checks get bypassed, so anything else is refused.

base64UrlParse("aGVsbG8=");
// UnsecureError MALFORMED: Base64.parse: unexpected "=" padding.
base64UrlParse("a+Vs");
// UnsecureError MALFORMED: Base64.parse: invalid base64 character "+" at index 1.
base64UrlParse("aGVs bG8");
// UnsecureError MALFORMED: Base64.parse: invalid base64 character " " at index 4.
base64UrlParse("aGVsbG9");
// UnsecureError MALFORMED: Base64.parse: the last base64 symbol sets bits past the final byte.
base64Parse("aGVsbG8");
// UnsecureError MALFORMED: Base64.parse: expected 1 "=" padding characters, found 0.
base64Parse("__8=");
// UnsecureError MALFORMED: Base64.parse: invalid base64 character "_" at index 0.

{ loose: true } drops whitespace, = and any character outside the alphabet. Use it for values a person pasted. verify() and decrypt() have no such option, and token material should never go through it.

base64UrlParse("aGVs bG8=", { loose: true }); // "hello"

#Bytes and text

concatUint8Arrays(...arrays) joins byte arrays into one new buffer. maybeArray(value) wraps a value in an array unless it already is one. textEncoder and textDecoder are shared module-level instances from unsecure/utils, so you do not allocate one per call.

import {
  base64UrlStringify,
  concatUint8Arrays,
  maybeArray,
  textDecoder,
  textEncoder,
} from "unjwt/utils";

const signingInput = concatUint8Arrays(
  textEncoder.encode(base64UrlStringify('{"alg":"HS256"}')),
  textEncoder.encode("."),
  textEncoder.encode(base64UrlStringify("hi")),
);
textDecoder.decode(signingInput); // "eyJhbGciOiJIUzI1NiJ9.aGk"

concatUint8Arrays(new Uint8Array([1, 2]), new Uint8Array([3])); // Uint8Array(3) [1, 2, 3]
maybeArray("api"); // ["api"]
maybeArray(["api", "web"]); // ["api", "web"]

#secureRandomBytes()

secureRandomBytes(length) returns length bytes from the platform CSPRNG. It is the unsecure/random function, re-exported unchanged. Use it for salts, IVs and raw secrets, never Math.random().

import { base64UrlStringify, secureRandomBytes } from "unjwt/utils";

const salt = secureRandomBytes(16); // Uint8Array(16)
base64UrlStringify(salt); // "HZVurVz1r5er…"

secureRandomBytes(-1);
// UnsecureError OUT_OF_RANGE: secureRandomBytes: length must be an integer between 0 and 2147483647, got -1.

#Errors

These helpers come from unsecure and throw its UnsecureError directly, not a JWTError.

Causecode
Text that is not the canonical encoding, or a decoded string that is not UTF-8MALFORMED
A value that is neither text nor bytes, such as nullINVALID_TYPE
A secureRandomBytes length that is negative, fractional or too largeOUT_OF_RANGE

Check with isUnsecureError imported from unjwt. unjwt bundles its own copy of unsecure, so an UnsecureError class imported from your own unsecure dependency is a different class and instanceof against it never matches.

#See also

  • unsecure codecs: the full strict and loose contract, plus hex and base32.
  • Type guards: isUnsecureError and isJWTError.
  • Errors: how a decode failure inside verify() surfaces as a JWTError with the UnsecureError as cause.