---
title: "Verify user tokens on your server"
description: "Check the token your app sends to your own server: the ES256 signature against Abrova's public keys (JWKS at /.well-known/jwks.json), the issuer, the audience, the expiry and your project. The model of Firebase's verifyIdToken, with any standard JWT library in Node.js, PHP, Python or Go."
product: auth
language: en
url: https://abrova.ir/en/docs/auth/verify-tokens
updated: 2026-11-14
---

# Verify user tokens on your server

Check the token your app sends to your own server: the ES256 signature against Abrova's public keys (JWKS at /.well-known/jwks.json), the issuer, the audience, the expiry and your project. The model of Firebase's verifyIdToken, with any standard JWT library in Node.js, PHP, Python or Go.

> **When you need this**
>
> Your app calls **your own API**, for example to place an order. Your server must know who is calling. The app sends the signed-in user's token, and your server checks it with Abrova's public keys. No call to Abrova per request, no shared secret: the same model as Firebase's `verifyIdToken`.

## How it works

Every Abrova user token is a JWT signed with **ES256**. Abrova publishes the public keys that check the signature as a standard **JWKS** (RFC 7517):

```text
https://auth.abrova.ir/.well-known/jwks.json
```

Only public keys are there. The private key never leaves Abrova, so nobody can make a token, but anyone can check one. Each key has an id (`kid`), and each token names the `kid` that signed it. When Abrova changes its key, the new key is published before any token uses it and the old one stays published for at least a day after, so a verifier that keeps the keys for 10 minutes never misses one.

The keys are served from Iran, so your server can check tokens when the international internet is cut.

## Step 1: Send the token from the app

Read the current token in the app and send it in the `Authorization` header:

| Platform | Read the token |
| --- | --- |
| `Flutter` | await auth.getToken() |
| `iOS` | try await auth.getAccessToken() |
| `Android` | auth.getAccessToken() |
| `Web, React Native` | await auth.getToken() |

```http
GET /api/orders
Authorization: Bearer eyJhbGciOiJFUzI1NiIs...
```

The SDK renews the token before it expires, so read it for each request rather than keeping a copy. Always call your API over HTTPS.

## Step 2: Verify it on your server

> **Check the project, not only the signature**
>
> One key signs the tokens of **every** Abrova project. A token with a good signature can still belong to another app's user. Always check that `pid` is **your** project id (Console › Project settings › General).

### Node.js: the Admin SDK

With [@abrova/auth-admin](https://abrova.ir/docs/auth/admin-sdk) 0.2.0 or later, one call checks everything on this page:

```ts
import { initAdmin } from '@abrova/auth-admin';

const auth = initAdmin(); // reads ABROVA_PROJECT_ID, ABROVA_API_KEY, ABROVA_SERVER_SECRET

app.use(async (req, res, next) => {
  const token = req.headers.authorization?.replace(/^Bearer /, '');
  try {
    req.user = await auth.verifyIdToken(token);
    next();
  } catch {
    res.status(401).json({ error: 'Sign in again' });
  }
});

// req.user.uid, req.user.role (a custom claim), req.user.amr (how they signed in)
```

For sensitive actions, such as a payment or deleting data, also check that the user was not disabled or signed out everywhere since the token was issued. This costs one request to Abrova Auth:

```ts
const user = await auth.verifyIdToken(token, true); // checkRevoked
```

| Error code | Meaning |
| --- | --- |
| `INVALID_ID_TOKEN` | Not a valid token: a bad signature, another project, a wrong issuer or audience. The message says which. |
| `ID_TOKEN_EXPIRED` | The token expired. The app gets a fresh one on its next call. |
| `ID_TOKEN_REVOKED` | With checkRevoked: the user was signed out everywhere after this token was issued. |
| `USER_DISABLED` | With checkRevoked: the user is disabled in the Console. |
| `NETWORK` | The public keys could not be read. Keys already read keep working. |

### Any language: a standard JWT library

Any JWT library that reads a JWKS works. Make all of these checks:

| Check | Value |
| --- | --- |
| `Algorithm` | ES256 only. Refuse every other alg, "none" included. |
| `Key` | From https://auth.abrova.ir/.well-known/jwks.json by the token's kid. Never a key or key address (jwk, jku, x5u) from the token itself. |
| `iss` | abrova |
| `aud` | abrova_user |
| `exp` | In the future. Allow at most 30 seconds of clock difference. |
| `pid` | Your project id. |
| `sub` | The user id: ru_... for an Abrova Auth user, or the id your server gave when it asked for the token. |

**Node.js** with [jose](https://github.com/panva/jose):

```ts
import { createRemoteJWKSet, jwtVerify } from 'jose';

const JWKS = createRemoteJWKSet(new URL('https://auth.abrova.ir/.well-known/jwks.json'));

export async function verifyUser(token: string) {
  const { payload } = await jwtVerify(token, JWKS, {
    algorithms: ['ES256'],
    issuer: 'abrova',
    audience: 'abrova_user',
    clockTolerance: 30,
  });
  if (payload.pid !== process.env.ABROVA_PROJECT_ID) throw new Error('Token of another project');
  return payload; // payload.sub is the user id
}
```

**PHP (Laravel)** with [firebase/php-jwt](https://github.com/firebase/php-jwt) 6 or later:

```php
use Firebase\JWT\JWK;
use Firebase\JWT\JWT;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http;

function verifyUser(string $token): object
{
    $jwks = Cache::remember('abrova_jwks', 600, fn () =>
        Http::timeout(5)->get('https://auth.abrova.ir/.well-known/jwks.json')->throw()->json());

    JWT::$leeway = 30;
    $claims = JWT::decode($token, JWK::parseKeySet($jwks, 'ES256'));

    if ($claims->iss !== 'abrova' || $claims->aud !== 'abrova_user' || $claims->pid !== env('ABROVA_PROJECT_ID')) {
        throw new UnexpectedValueException('Not a token of this project');
    }
    return $claims; // $claims->sub is the user id
}
```

`JWT::decode` checks the signature with the key the token's `kid` names, and the expiry. When Abrova adds a new key, a token with an unknown `kid` fails until the cache renews (at most 10 minutes); clear the cache key and retry once when that happens.

**Python** with [PyJWT](https://pyjwt.readthedocs.io) (`pip install "pyjwt[crypto]"`):

```python
import os
import jwt

jwks = jwt.PyJWKClient("https://auth.abrova.ir/.well-known/jwks.json", lifespan=600)

def verify_user(token: str) -> dict:
    key = jwks.get_signing_key_from_jwt(token).key
    claims = jwt.decode(token, key, algorithms=["ES256"], issuer="abrova", audience="abrova_user", leeway=30)
    if claims.get("pid") != os.environ["ABROVA_PROJECT_ID"]:
        raise jwt.InvalidTokenError("Token of another project")
    return claims  # claims["sub"] is the user id
```

**Go** with [golang-jwt](https://github.com/golang-jwt/jwt) v5 and [keyfunc](https://github.com/MicahParks/keyfunc) v3:

```go
var jwks keyfunc.Keyfunc

func init() {
	var err error
	jwks, err = keyfunc.NewDefault([]string{"https://auth.abrova.ir/.well-known/jwks.json"})
	if err != nil {
		log.Fatal(err)
	}
}

func verifyUser(token string) (jwt.MapClaims, error) {
	claims := jwt.MapClaims{}
	_, err := jwt.ParseWithClaims(token, claims, jwks.Keyfunc,
		jwt.WithValidMethods([]string{"ES256"}),
		jwt.WithIssuer("abrova"),
		jwt.WithAudience("abrova_user"),
		jwt.WithExpirationRequired(),
		jwt.WithLeeway(30*time.Second))
	if err != nil {
		return nil, err
	}
	if claims["pid"] != os.Getenv("ABROVA_PROJECT_ID") {
		return nil, errors.New("token of another project")
	}
	return claims, nil // claims["sub"] is the user id
}
```

## Step 3: Use the claims

| Claim | What it is |
| --- | --- |
| `sub` | The user id. |
| `pid` | The project the token is for. |
| `iss, aud` | abrova and abrova_user. |
| `iat, exp` | When the token was issued and when it expires (seconds). A token lives one hour. |
| `auth_time` | When the user signed in. |
| `amr` | How the user signed in, for example ["google"]. Abrova Auth tokens only. |
| `Your custom claims` | At the top level, for example role or plan. See Custom claims. |

Decide access on your server from `sub` and your [custom claims](https://abrova.ir/docs/auth/custom-claims), never from anything else the app sends.

## Good to know

- **Revocation.** A token stays valid until it expires, at most one hour, even after the user signs out everywhere. This is the same as in Firebase. Where that matters, check revocation (`checkRevoked` in the Admin SDK, or the user's `tokensValidAfterTime` from `getUser`).
- **Tokens your server asked for** (`POST /users/token` with the server secret) are checked the same way. Their `sub` is the id you gave.
- **Never verify in the app.** The app reads its own claims with `getTokenResult()` to choose screens. Only your server's check can be trusted.
- **Don't log tokens.** Anyone who holds a token can act as the user until it expires.
