Type guards

Functions that answer "what is this value?" about keys, key sets, headers and errors, and narrow its TypeScript type when the answer is yes.

import { isPublicJWK } from "unjwt/utils";

const shareable = jwks.keys.filter(isPublicJWK);

A JWK is a plain JSON object, so at runtime nothing tells a public key from a private one except the fields it carries. Each guard checks those fields and returns a boolean. In TypeScript it also narrows the value, so code after the check sees the precise key type.

The examples on this page use these keys:

import { generateJWK, generateKey } from "unjwt/jwk";

const hs = await generateJWK("HS256"); // { kty: "oct", k: "cSc6O7FW…", alg: "HS256", … }
const { privateKey: esPrivate, publicKey: esPublic } = await generateJWK("ES256");
const cryptoKey = await generateKey("HS256"); // CryptoKey
const pair = await generateKey("ES256"); // CryptoKeyPair

#At a glance

GuardNarrows toTrue when
isJWK(value)JWKan object whose kty is oct, RSA, EC or OKP
isJWKSet(value)JWKSetan object with a keys array
isSymmetricJWK(value)JWK_octa JWK with kty: "oct" and a string k
isAsymmetricJWK(value)Exclude<JWK, JWK_oct>a JWK whose kty is RSA, EC or OKP
isPrivateJWK(value)JWK_Privatean RSA, EC or OKP JWK with a string d
isPublicJWK(value)JWK_Publican RSA, EC or OKP JWK with its public fields and no d
isCryptoKey(value)CryptoKeya Web Crypto key, from any realm
isCryptoKeyPair(value)CryptoKeyPairan object whose publicKey and privateKey are both CryptoKeys
assertCryptoKey(value)asserts value is CryptoKeythrows ERR_JWK_INVALID otherwise
isJWTContent(header)boolean, no narrowingtyp names JWT, or cty names JSON
isJWTError(error, code?)JWTErroran unjwt error, optionally with that code
isUnsecureError(error, code?)UnsecureErroran error from the bundled unsecure primitives

#isJWK

Recognises the four key types this library models. A JWK with any other kty is refused, so the narrowed type is always one unjwt can use.

isJWK(esPrivate); // true
isJWK({ kty: "foo" }); // false
isJWK("secret"); // false

#isJWKSet

Checks the shape { keys: [...] } and nothing more. It does not look inside the array, so filter the entries with isJWK before you trust them.

isJWKSet({ keys: [esPublic] }); // true
isJWKSet({ keys: [] }); // true
isJWKSet(esPublic); // false

#isSymmetricJWK

True for oct keys that carry k. Their k is the shared secret, so a symmetric JWK is always secret.

isSymmetricJWK(hs); // true
isSymmetricJWK(esPrivate); // false
isSymmetricJWK({ kty: "oct" }); // false

#isAsymmetricJWK

True for an RSA, EC or OKP JWK, public or private. Anything that is not a JWK, and every oct key, is false.

isAsymmetricJWK(esPublic); // true
isAsymmetricJWK(esPrivate); // true
isAsymmetricJWK(hs); // false
isAsymmetricJWK("x"); // false

#isPrivateJWK

True for an RSA, EC or OKP key with a d member. It returns false for every oct key, even though an oct key is secret.

isPrivateJWK(esPrivate); // true
isPrivateJWK(esPublic); // false
isPrivateJWK(hs); // false

Warning

!isPrivateJWK(key) is no "safe to publish" check, because it lets every oct key through. Use isPublicJWK to decide what goes into a JWKS endpoint.

#isPublicJWK

The "safe to publish" check. True only for an asymmetric key that has no d and carries the public fields its type needs: x and y for EC, x for OKP, n and e for RSA.

isPublicJWK(esPublic); // true
isPublicJWK(esPrivate); // false
isPublicJWK(hs); // false
isPublicJWK({ kty: "EC", crv: "P-256", x: "abc" }); // false, no y

#isCryptoKey and assertCryptoKey

isCryptoKey reads Symbol.toStringTag, so it also recognises a CryptoKey created in another realm, such as a worker or an iframe. assertCryptoKey throws instead of returning false.

isCryptoKey(cryptoKey); // true
isCryptoKey(hs); // false

assertCryptoKey(cryptoKey); // passes
assertCryptoKey(hs);
// JWTError ERR_JWK_INVALID: CryptoKey instance expected

#isCryptoKeyPair

True when both halves are present and both are CryptoKeys.

isCryptoKeyPair(pair); // true
isCryptoKeyPair({ publicKey: pair.publicKey }); // false

#isJWTContent

Reports what a header claims the content is: true when typ contains jwt (case-insensitive) or cty is json, application/json or ends in +json. A non-string typ or cty returns false.

isJWTContent({ typ: "JWT" }); // true
isJWTContent({ typ: "at+jwt" }); // true
isJWTContent({ cty: "application/vnd.example+json" }); // true
isJWTContent({ typ: "text", cty: "text/plain" }); // false
isJWTContent(undefined); // false

Note

typ and cty are chosen by whoever made the token. verify() and decrypt() never read them to decide how to parse a payload; you declare that with the payload option. Treat isJWTContent as information, not as a security check.

#isJWTError

Narrows to JWTError. With a code, it also checks the code, and a misspelled code fails to type-check. For ERR_JWT_EXPIRED it also types cause as { jti?, iat?, exp? }. It is exported from unjwt, unjwt/jws, unjwt/jwe and unjwt/jwk, not from unjwt/utils.

import { isJWTError, verify } from "unjwt/jws";

const error = await verify("not.a.token", hs).catch((e) => e);
// JWTError ERR_JWS_INVALID: Invalid JWS: Protected header could not be decoded.

isJWTError(error); // true
isJWTError(error, "ERR_JWS_INVALID"); // true
isJWTError(error, "ERR_JWT_EXPIRED"); // false
isJWTError(new Error("x")); // false

#isUnsecureError

Narrows to the UnsecureError class that unjwt bundles. The encoding helpers throw it directly, and a JWTError from a token that did not decode carries it as cause. Import the guard from unjwt: a class from your own unsecure install is a different class.

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

isUnsecureError(error.cause, "MALFORMED"); // true, the error from above
isUnsecureError(error); // false, it is a JWTError

try {
  base64UrlParse("a=");
} catch (e) {
  isUnsecureError(e, "MALFORMED"); // true
}

#See also