AuthJet
← Docs

How to Verify access tokens on your backend

Your frontend sends the user's access token to your API asAuthorization: Bearer <token>. Your API verifies it locally against our public keys — no shared secret and no call to AuthJet per request.

You need three values

JWKS URL
https://api.authjet.dev/.well-known/jwks.json
Issuer
https://api.authjet.dev
Audience — your account id
Shown in the dashboard under Account Settings → Account details → Account ID, with a copy button.

What to check

Any standard JWT library does all of this when given the right options — pick your language below.

Code samples

Each sample verifies a token and returns its claims. Replace<your-account-id> with your account id.

Language
Install
npm install jose
auth.ts
import { createRemoteJWKSet, jwtVerify } from 'jose' // npm install jose

const JWKS = createRemoteJWKSet(new URL('https://api.authjet.dev/.well-known/jwks.json')) // caches keys
const ISSUER = 'https://api.authjet.dev'
const AUDIENCE = '<your-account-id>'

// Resolves to the token's claims, or throws.
export async function verify(token: string) {
  const { payload } = await jwtVerify(token, JWKS, {
    algorithms: ['RS256'],
    issuer: ISSUER,
    audience: AUDIENCE,
  })
  if (payload.type !== 'access') throw new Error('not an access token')
  return payload
}
Install
pip install "PyJWT[crypto]"
auth.py
import jwt  # pip install "PyJWT[crypto]"

JWKS = jwt.PyJWKClient("https://api.authjet.dev/.well-known/jwks.json")  # caches keys
ISSUER = "https://api.authjet.dev"
AUDIENCE = "<your-account-id>"


def verify(token: str) -> dict:
    """Return the token's claims, or raise jwt.PyJWTError."""
    signing_key = JWKS.get_signing_key_from_jwt(token)
    claims = jwt.decode(token, signing_key.key, algorithms=["RS256"], issuer=ISSUER, audience=AUDIENCE)
    if claims.get("type") != "access":
        raise jwt.InvalidTokenError("not an access token")
    return claims
pom.xml
<dependency>
  <groupId>com.nimbusds</groupId>
  <artifactId>nimbus-jose-jwt</artifactId>
  <version>10.3</version>
</dependency>
AuthJetVerifier.java
// Maven: com.nimbusds:nimbus-jose-jwt
import com.nimbusds.jose.JWSAlgorithm;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.jwk.source.JWKSourceBuilder;
import com.nimbusds.jose.proc.JWSVerificationKeySelector;
import com.nimbusds.jose.proc.SecurityContext;
import com.nimbusds.jwt.JWTClaimsSet;
import com.nimbusds.jwt.proc.DefaultJWTClaimsVerifier;
import com.nimbusds.jwt.proc.DefaultJWTProcessor;
import java.net.URL;
import java.util.Set;

public class AuthJetVerifier {
    private static final String JWKS_URL = "https://api.authjet.dev/.well-known/jwks.json";
    private static final String ISSUER = "https://api.authjet.dev";
    private static final String AUDIENCE = "<your-account-id>";

    private final DefaultJWTProcessor<SecurityContext> processor = new DefaultJWTProcessor<>();

    public AuthJetVerifier() throws Exception {
        // Caches keys and refetches on an unknown kid.
        JWKSource<SecurityContext> keys = JWKSourceBuilder.create(new URL(JWKS_URL)).build();
        processor.setJWSKeySelector(new JWSVerificationKeySelector<>(JWSAlgorithm.RS256, keys));
        processor.setJWTClaimsSetVerifier(new DefaultJWTClaimsVerifier<>(
                AUDIENCE,
                new JWTClaimsSet.Builder().issuer(ISSUER).claim("type", "access").build(),
                Set.of("sub", "exp", "tenant_id")));
    }

    /** Returns the token's claims, or throws. */
    public JWTClaimsSet verify(String token) throws Exception {
        return processor.process(token, null);
    }
}

Using Spring Security? NimbusJwtDecoder.withJwkSetUri(...) plus an issuer and audience validator does the same.

Install
dotnet add package Microsoft.IdentityModel.JsonWebTokens
AuthJetVerifier.cs
// dotnet add package Microsoft.IdentityModel.JsonWebTokens
using Microsoft.IdentityModel.JsonWebTokens;
using Microsoft.IdentityModel.Tokens;

public class AuthJetVerifier
{
    const string JwksUrl = "https://api.authjet.dev/.well-known/jwks.json";
    const string Issuer = "https://api.authjet.dev";
    const string Audience = "<your-account-id>";

    static readonly HttpClient Http = new();
    readonly JsonWebTokenHandler _handler = new();
    JsonWebKeySet? _keys;
    DateTime _fetchedAt;

    // Returns the token's claims, or throws SecurityTokenException.
    public async Task<IDictionary<string, object>> VerifyAsync(string token)
    {
        var result = await ValidateAsync(token, refresh: false);
        if (result.Exception is SecurityTokenSignatureKeyNotFoundException)
            result = await ValidateAsync(token, refresh: true); // key rotated: refetch once
        if (!result.IsValid) throw result.Exception;
        if (result.Claims["type"] as string != "access")
            throw new SecurityTokenException("not an access token");
        return result.Claims;
    }

    async Task<TokenValidationResult> ValidateAsync(string token, bool refresh)
    {
        if (_keys is null || (refresh && DateTime.UtcNow - _fetchedAt > TimeSpan.FromMinutes(1)))
        {
            _keys = new JsonWebKeySet(await Http.GetStringAsync(JwksUrl));
            _fetchedAt = DateTime.UtcNow;
        }
        return await _handler.ValidateTokenAsync(token, new TokenValidationParameters
        {
            ValidIssuer = Issuer,
            ValidAudience = Audience,
            ValidAlgorithms = new[] { SecurityAlgorithms.RsaSha256 },
            IssuerSigningKeys = _keys.GetSigningKeys(),
        });
    }
}

ASP.NET Core's AddJwtBearer(Authority = ...) expects an OpenID discovery document, which AuthJet doesn't serve — verify with the JWKS as shown here.

Install
go get github.com/golang-jwt/jwt/v5 github.com/MicahParks/keyfunc/v3
auth.go
package auth

// go get github.com/golang-jwt/jwt/v5 github.com/MicahParks/keyfunc/v3

import (
	"context"
	"errors"

	"github.com/MicahParks/keyfunc/v3"
	"github.com/golang-jwt/jwt/v5"
)

const (
	jwksURL  = "https://api.authjet.dev/.well-known/jwks.json"
	issuer   = "https://api.authjet.dev"
	audience = "<your-account-id>"
)

var jwks keyfunc.Keyfunc

func init() {
	// Fetches the JWKS, then refreshes it in the background and on an unknown kid.
	var err error
	jwks, err = keyfunc.NewDefaultCtx(context.Background(), []string{jwksURL})
	if err != nil {
		panic(err)
	}
}

// Verify returns the token's claims, or an error.
func Verify(token string) (jwt.MapClaims, error) {
	claims := jwt.MapClaims{}
	_, err := jwt.ParseWithClaims(token, claims, jwks.Keyfunc,
		jwt.WithValidMethods([]string{"RS256"}),
		jwt.WithIssuer(issuer),
		jwt.WithAudience(audience),
		jwt.WithExpirationRequired(),
	)
	if err != nil {
		return nil, err
	}
	if claims["type"] != "access" {
		return nil, errors.New("not an access token")
	}
	return claims, nil
}
Install
composer require firebase/php-jwt
auth.php
<?php
// composer require firebase/php-jwt
use Firebase\JWT\JWK;
use Firebase\JWT\JWT;

const JWKS_URL = 'https://api.authjet.dev/.well-known/jwks.json';
const ISSUER = 'https://api.authjet.dev';
const AUDIENCE = '<your-account-id>';

/** Returns the token's claims, or throws. Cache the JWKS (e.g. APCu) in production. */
function verify(string $token): object
{
    $jwks = json_decode(file_get_contents(JWKS_URL), true);
    $claims = JWT::decode($token, JWK::parseKeySet($jwks, 'RS256')); // checks signature + exp

    // php-jwt doesn't check iss/aud itself.
    if (($claims->iss ?? null) !== ISSUER) throw new UnexpectedValueException('wrong issuer');
    if (($claims->aud ?? null) !== AUDIENCE) throw new UnexpectedValueException('wrong audience');
    if (($claims->type ?? null) !== 'access') throw new UnexpectedValueException('not an access token');
    return $claims;
}

firebase/php-jwt checks the signature and expiry but not iss or aud, so the sample checks those itself — don't drop them.

Install
gem install jwt
auth.rb
# gem install jwt
require "jwt"
require "net/http"
require "json"

JWKS_URL = "https://api.authjet.dev/.well-known/jwks.json"
ISSUER = "https://api.authjet.dev"
AUDIENCE = "<your-account-id>"

# Caches the JWKS; refetches (at most once a minute) when a token has an unknown kid.
JWKS_LOADER = lambda do |options|
  if options[:kid_not_found] && @jwks_fetched_at.to_i < Time.now.to_i - 60
    @jwks = nil
  end
  @jwks ||= begin
    @jwks_fetched_at = Time.now
    JWT::JWK::Set.new(JSON.parse(Net::HTTP.get(URI(JWKS_URL))))
  end
end

# Returns the token's claims, or raises JWT::DecodeError.
def verify(token)
  claims, = JWT.decode(token, nil, true,
                       algorithms: ["RS256"], jwks: JWKS_LOADER,
                       iss: ISSUER, verify_iss: true,
                       aud: AUDIENCE, verify_aud: true)
  raise JWT::DecodeError, "not an access token" unless claims["type"] == "access"
  claims
end

Claims

subUser id. The literal "platform" during an AuthJet support session with no specific user.
tenant_idThe tenant the user signed in to — scope your own data by this.
audYour account id.
emailThe user's email, when known.
app_role / app_permissionsThe app role and permissions you defined for this user.
platform_impersonationtrue when AuthJet support staff are acting in your tenant. Log it; decide whether your API allows it.
exp / iat / jtiExpiry, issued-at, unique token id.

Key rotation

We rotate signing keys periodically. The JWKS always lists the active key and, for a while after a rotation, the previous one. Both libraries above refetch the JWKS when they see an unknownkid, so rotations need nothing from you. Don't hard-code a key — always load it from the JWKS URL.