Phonelink

Server Verification

Verify Phonelink JWT tokens on your server to get the authenticated phone number.

Overview

Every Phonelink integration — whether web, React, or Expo — requires server-side token verification. The client never verifies tokens; it passes the raw JWT and nonce to your server, where validate performs cryptographic validation.

Install

npm install phonelink

The server module uses the jose library for JWT verification, which is included as a dependency.

Basic usage

import { validate } from "phonelink/validate";

const payload = await validate(token, nonce, "your-client-id");

console.log(payload.phone_e164); // "+14155551234"
console.log(payload.verified);   // true

What validate checks

The function performs five validation steps in order. If any step fails, it throws an error.

StepCheckError
1JWT signature via JWKSJWSSignatureVerificationFailed
2Token issuer is https://phone.linkJWTClaimValidationFailed
3Audience matches your client IDJWTClaimValidationFailed
4Nonce matches the expected value"Nonce mismatch"
5verified claim is true"Phone number not verified"

Token expiry is also enforced automatically by the JWT library.

Parameters

ParameterTypeDescription
tokenstringThe JWT returned from the client verification flow
expectedNoncestringThe nonce returned alongside the token from the client
expectedAudstringYour Phonelink client ID (must match the token's aud claim)

Return value

On success, returns a PhonelinkPayload object:

PropertyTypeDescription
phone_e164stringVerified phone number in E.164 format (e.g. "+14155551234")
verifiedbooleanAlways true (fails if not)
methodstringVerification method used
providerstringVerification provider
noncestringThe nonce used for this verification
substringSubject identifier
issstringIssuer ("https://phone.link")
audstringAudience (your client ID)
iatnumberIssued-at timestamp (unix seconds)
expnumberExpiry timestamp (unix seconds)
jtistringUnique token identifier

Error handling

Always wrap validate in a try/catch. The function throws for any validation failure:

import { validate } from "phonelink/validate";

try {
  const payload = await validate(token, nonce, "your-client-id");
  // Success — use payload.phone_e164
} catch (error) {
  if (error instanceof Error) {
    switch (error.message) {
      case "Nonce mismatch":
        // The nonce from the client doesn't match the token's nonce.
        // This could indicate a replay attack or a bug in nonce handling.
        break;
      case "Phone number not verified":
        // The token was issued but phone verification wasn't completed.
        break;
      default:
        // JWT validation failed (invalid signature, expired, wrong issuer/audience).
        break;
    }
  }
}

JWKS caching

The JWKS (JSON Web Key Set) is fetched from https://phone.link/.well-known/jwks.json and cached automatically by the jose library. You don't need to manage key rotation or caching yourself.

Nonce lifecycle

The nonce prevents replay attacks. Here's its lifecycle:

  1. Generated — The client creates a random 32-byte hex string
  2. Stored — The client stores it (in sessionStorage for web, or in memory for Expo)
  3. Sent to Phonelink — Included in the auth URL as a query parameter
  4. Embedded in JWT — Phonelink includes the nonce in the signed token
  5. Returned to client — The client retrieves the stored nonce
  6. Validated on server — Your server checks that the token's nonce matches the client's nonce
  7. Discarded — The nonce is never reused

Always forward the nonce from the client and validate it on the server. Never skip nonce validation.

On this page