Documentation Index
Fetch the complete documentation index at: https://otpless.com/docs/llms.txt
Use this file to discover all available pages before exploring further.
OTPless issues ID tokens as JWTs signed with the RS256 algorithm.
Your backend must verify the signature and claims before trusting the token.
JWKS (Public Keys)
To verify signatures, fetch OTPless public keys from:
- Recommended (secure):
https://otpless.com/.well-known/jwks
These contain the RSA public keys used for verifying RS256 signatures.
You must select the key where kid matches the JWT header.
Example JWT ID Token
{
"kid": "pk0183",
"typ": "JWT",
"alg": "RS256"
}
Payload
{
"sub": "MO-1xx13cc0bf5341xxxxx6da2xxx43xxx",
"aud": "PXXXXG1XXXX1NXXYAO",
"country_code": "+91",
"auth_time": "1758641886",
"iss": "https://otpless.com",
"national_phone_number": "9999999999",
"phone_number_verified": true,
"phone_number": "919999999999",
"exp": 1758622386,
"iat": 1758622086,
"token": "xxxx4e11xxx95f1xxxxxa5xxxc38xxd54"
}
Claim Reference
| Claim | Type | Description | Example |
|---|
sub | string | Subject identifier — unique ID for the user at OTPless level | "MO-1xxxcc0bf..." |
aud | string | Audience — must match your APP_ID | "XXXX1PMXXXXx1X9XXXX" |
country_code | string | Country dialing code in international format | "+91" |
auth_time | number | Epoch timestamp when authentication happened | 1758641886 |
iss | string | Issuer — must always equal https://otpless.com | "https://otpless.com" |
national_phone_number | string | Phone number without country code | "9999999999" |
phone_number_verified | boolean | Indicates whether phone number was successfully verified | true |
phone_number | string | Full phone number in E.164 format | "919999999999" |
exp | number | Expiration time (epoch seconds) — reject if token expired | 1758622386 |
iat | number | Issued-at time (epoch seconds) | 1758622086 |
token | string | Opaque reference token generated by OTPless for one time validate for s2s | "8xexxxxx54..." |
Verification Checklist
-
Signature
- Verify using RS256 with OTPless JWKS (
https://otpless.com/.well-known/jwks).
-
Issuer (
iss)
- Must equal
https://otpless.com.
-
Audience (
aud)
- Must equal
PXXXXG1XXXX1NXXYAO.
-
Time claims
exp > current time (allow ±60s skew).
iat and auth_time optional checks.
-
App-specific checks
- Ensure
phone_number_verified: true before granting sensitive access.
- Use
sub as the stable user identifier.
-
Algorithm hardening
- Accept only
RS256.
- Reject tokens with
alg: none or unexpected algorithms.
Code Examples
Node.js (Express) — jose
import { createRemoteJWKSet, jwtVerify } from 'jose'
const JWKS_URL = 'https://otpless.com/.well-known/jwks'
const ISSUER = 'https://otpless.com'
const AUDIENCE = 'PXXXXG1XXXX1NXXYAO'
const jwks = createRemoteJWKSet(new URL(JWKS_URL))
export async function verifyOtplessIdToken(idToken: string) {
const { payload, protectedHeader } = await jwtVerify(idToken, jwks, {
issuer: ISSUER,
audience: AUDIENCE,
algorithms: ['RS256'],
clockTolerance: 60,
})
return { header: protectedHeader, claims: payload }
}
Java (Spring Boot) — Nimbus JOSE + JWT
<dependency>
<groupId>com.nimbusds</groupId>
<artifactId>nimbus-jose-jwt</artifactId>
<version>9.37.3</version>
</dependency>
public class JwtVerifier {
private final ConfigurableJWTProcessor<SecurityContext> processor;
public JwtVerifier() throws Exception {
processor = new DefaultJWTProcessor<>();
var jwks = new RemoteJWKSet<SecurityContext>(new URL("https://otpless.com/.well-known/jwks"));
var selector = new JWSVerificationKeySelector<SecurityContext>(JWSAlgorithm.RS256, jwks);
processor.setJWSKeySelector(selector);
processor.setJWTClaimsSetVerifier((claims, context) -> {
if (!"https://otpless.com".equals(claims.getIssuer()))
throw new BadJWTException("Invalid issuer");
if (!claims.getAudience().contains("PXXXXG1XXXX1NXXYAO"))
throw new BadJWTException("Invalid audience");
});
}
public JWTClaimsSet verify(String token) throws Exception {
return processor.process(token, null);
}
}
Python (Flask/FastAPI) — authlib
pip install authlib httpx
import time, httpx
from authlib.jose import jwt, JsonWebKey
JWKS_URL = "https://otpless.com/.well-known/jwks"
ISSUER = "https://otpless.com"
AUDIENCE = "PXXXXG1XXXX1NXXYAO"
async def fetch_jwks():
async with httpx.AsyncClient(timeout=5) as client:
return (await client.get(JWKS_URL)).json()
async def verify_otpless_id_token(token: str, leeway: int = 60):
jwks = await fetch_jwks()
header = jwt.get_unverified_header(token)
kid = header.get("kid")
jwk = next((k for k in jwks["keys"] if k["kid"] == kid), None)
if not jwk:
raise ValueError("Signing key not found")
key = JsonWebKey.import_key(jwk)
claims = jwt.decode(token, key, claims_options={
"iss": {"essential": True, "values": [ISSUER]},
"aud": {"essential": True, "values": [AUDIENCE]},
"exp": {"essential": True},
}, leeway=leeway)
claims.validate()
return claims
Go (net/http + jwx + golang-jwt)
go get github.com/golang-jwt/jwt/v5 github.com/lestrrat-go/jwx/v2/jwk
import (
"context"
"errors"
"time"
"github.com/golang-jwt/jwt/v5"
"github.com/lestrrat-go/jwx/v2/jwk"
)
const (
jwksURL = "https://otpless.com/.well-known/jwks"
issuer = "https://otpless.com"
audience = "PXXXXG1XXXX1NXXYAO"
)
func fetchKeySet() (jwk.Set, error) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
return jwk.Fetch(ctx, jwksURL)
}
func verifyOtplessIDToken(tokenStr string) (*jwt.RegisteredClaims, error) {
set, err := fetchKeySet()
if err != nil {
return nil, err
}
keyFunc := func(token *jwt.Token) (interface{}, error) {
if token.Method.Alg() != jwt.SigningMethodRS256.Alg() {
return nil, errors.New("unexpected signing method")
}
kid, ok := token.Header["kid"].(string)
if !ok {
return nil, errors.New("no kid in token header")
}
key, found := set.LookupKeyID(kid)
if !found {
return nil, errors.New("kid not found in JWKS")
}
var pubKey interface{}
err := key.Raw(&pubKey)
return pubKey, err
}
claims := &jwt.RegisteredClaims{}
token, err := jwt.ParseWithClaims(
tokenStr,
claims,
keyFunc,
jwt.WithAudience(audience),
jwt.WithIssuer(issuer),
jwt.WithLeeway(60*time.Second),
jwt.WithRequiredClaim("exp"), // require 'exp' claim
)
if err != nil {
return nil, err
}
if !token.Valid {
return nil, errors.New("invalid token")
}
return claims, nil
}
Best Practices
- Cache JWKS (5–15 minutes). Refresh when a
kid is not found.
- Always use HTTPS JWKS endpoint in production for security.
- Strictly check
iss and aud.
- Allow a small clock skew (30–60s).
- Reject unexpected algorithms (only
RS256).
- Use
phone_number_verified before sensitive operations.
- Never trust a JWT by simply decoding — always verify signature.