☰ Docs menu · Verify tokens on your server
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.
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):
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() |
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#
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 0.2.0 or later, one call checks everything on this page:
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:
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:
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 6 or later:
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 (pip install "pyjwt[crypto]"):
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 idGo with golang-jwt v5 and keyfunc v3:
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, 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 (
checkRevokedin the Admin SDK, or the user'stokensValidAfterTimefromgetUser). - Tokens your server asked for (
POST /users/tokenwith the server secret) are checked the same way. Theirsubis 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.
Last updated: 14 Nov 2026