> ## 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.

# Verify ID Token

OTPless issues **ID tokens** as [JWTs](https://jwt.io/) 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

### Header

```json theme={null}
{
  "kid": "pk0183",
  "typ": "JWT",
  "alg": "RS256"
}
```

### Payload

```json theme={null}
{
  "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](https://en.wikipedia.org/wiki/E.164)  | `"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

1. **Signature**

   * Verify using RS256 with OTPless JWKS (`https://otpless.com/.well-known/jwks`).

2. **Issuer (`iss`)**

   * Must equal `https://otpless.com`.

3. **Audience (`aud`)**

   * Must equal `PXXXXG1XXXX1NXXYAO`.

4. **Time claims**

   * `exp` > current time (allow ±60s skew).
   * `iat` and `auth_time` optional checks.

5. **App-specific checks**

   * Ensure `phone_number_verified: true` before granting sensitive access.
   * Use `sub` as the stable user identifier.

6. **Algorithm hardening**

   * Accept only `RS256`.
   * Reject tokens with `alg: none` or unexpected algorithms.

***

## Code Examples

### Node.js (Express) — `jose`

```bash theme={null}
npm i jose
```

```ts theme={null}
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

```xml theme={null}
<dependency>
  <groupId>com.nimbusds</groupId>
  <artifactId>nimbus-jose-jwt</artifactId>
  <version>9.37.3</version>
</dependency>
```

```java theme={null}
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`

```bash theme={null}
pip install authlib httpx
```

```py theme={null}
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)

```bash theme={null}
go get github.com/golang-jwt/jwt/v5 github.com/lestrrat-go/jwx/v2/jwk
```

```go theme={null}
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.
