Abrova
☰ Docs menu · Verify tokens on your server

Verify user tokens on your server

View as Markdown

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:

PlatformRead the token
Flutterawait auth.getToken()
iOStry await auth.getAccessToken()
Androidauth.getAccessToken()
Web, React Nativeawait 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 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 codeMeaning
INVALID_ID_TOKENNot a valid token: a bad signature, another project, a wrong issuer or audience. The message says which.
ID_TOKEN_EXPIREDThe token expired. The app gets a fresh one on its next call.
ID_TOKEN_REVOKEDWith checkRevoked: the user was signed out everywhere after this token was issued.
USER_DISABLEDWith checkRevoked: the user is disabled in the Console.
NETWORKThe 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:

CheckValue
AlgorithmES256 only. Refuse every other alg, "none" included.
KeyFrom 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.
issabrova
audabrova_user
expIn the future. Allow at most 30 seconds of clock difference.
pidYour project id.
subThe user id: ru_... for an Abrova Auth user, or the id your server gave when it asked for the token.

Node.js with 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 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 (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 v5 and 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#

ClaimWhat it is
subThe user id.
pidThe project the token is for.
iss, audabrova and abrova_user.
iat, expWhen the token was issued and when it expires (seconds). A token lives one hour.
auth_timeWhen the user signed in.
amrHow the user signed in, for example ["google"]. Abrova Auth tokens only.
Your custom claimsAt 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 (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.
Was this page helpful?
Report a problem

Last updated: 14 Nov 2026