TryMellon
Navigation

Reading custom claims (backend)

Decode and read tryMellon custom claims from session JWTs in Node, Go, and Python.

Reading custom claims

Custom claims are nested under the namespace https://trymellon.dev/claims in the session JWT. The namespace prevents collisions with reserved OIDC claims and signals “vendor-defined” to any standards-aware library.

For the SDK side (defining schema, passing claims at login) see SDK custom claims.

JWT structure

After offline verification, the payload looks like:

{
  "iss": "https://api.trymellonauth.com",
  "sub": "user_…",
  "aud": "client_…",
  "iat": 1717000000,
  "exp": 1717003600,
  "https://trymellon.dev/claims": {
    "role": "admin",
    "company_id": 42
  }
}

Node.js (jose)

import { createRemoteJWKSet, jwtVerify } from 'jose';

const JWKS = createRemoteJWKSet(
  new URL('https://api.trymellonauth.com/.well-known/jwks.json'),
);

const TM_CLAIMS = 'https://trymellon.dev/claims' as const;

interface AppClaims {
  role?: 'admin' | 'member';
  company_id?: number;
  wallet_address?: string;
}

export async function readClaims(token: string): Promise<AppClaims> {
  const { payload } = await jwtVerify(token, JWKS, {
    issuer: 'https://api.trymellonauth.com',
  });
  return (payload[TM_CLAIMS] ?? {}) as AppClaims;
}

Go (lestrrat-go/jwx)

const TMClaimsKey = "https://trymellon.dev/claims"

type AppClaims struct {
    Role          string `json:"role,omitempty"`
    CompanyID     int    `json:"company_id,omitempty"`
    WalletAddress string `json:"wallet_address,omitempty"`
}

func ReadClaims(token jwt.Token) (AppClaims, error) {
    raw, ok := token.Get(TMClaimsKey)
    if !ok {
        return AppClaims{}, nil
    }
    bytes, err := json.Marshal(raw)
    if err != nil {
        return AppClaims{}, err
    }
    var c AppClaims
    return c, json.Unmarshal(bytes, &c)
}

Python (python-jose)

TM_CLAIMS = "https://trymellon.dev/claims"

def read_claims(decoded_payload: dict) -> dict:
    return decoded_payload.get(TM_CLAIMS, {})

# Usage
payload = jwt.decode(token, jwks(), algorithms=["RS256"], audience=AUD)
claims = read_claims(payload)
role = claims.get("role")

Defensive reads

The schema you configured in the dashboard is enforced at issuance. By the time a token reaches your backend the claims are already shape-validated — but treat them as untrusted input anyway:

const role = claims.role === 'admin' ? 'admin' : 'member';

This avoids cascading failures if you later change the schema and old tokens still carry the previous shape.