Skip to main content
The Custom Issuer Guard is the on-chain contract that lets NEAR Auth accept JWTs from your own OpenID Connect (OIDC) issuer instead of Auth0. It verifies the RS256 signature, checks the iss and fatxn claims, and — unlike the Auth0 Guard — performs no audience (aud) check. Its RSA public keys are not owner-managed; they are pulled from a DAO-governed Attestation contract, so keys can rotate dynamically without redeploying the guard.
This is an advanced protocol topic. If you are building on the default Auth0 happy path, you do not need this guard — see How it works instead. Use the Custom Issuer Guard only when you run your own custom issuer service.
The Rust source lives in the Peersyst/fast-auth repo at contracts/jwt-guards/custom-issuer-guard, and the shared verification logic it inherits lives in contracts/jwt-guards/base-jwt-guard.

Where it fits

Like every guard, the Custom Issuer Guard implements the shared JwtGuard trait and is reached through the router:
1

An app calls FastAuth.sign

The dApp submits FastAuth.sign(guard_id, verify_payload, sign_payload, algorithm) on the NEAR Auth contract, where guard_id resolves to your custom-issuer guard and verify_payload is the JWT re-issued by your custom issuer.
2

The router routes to this guard

The JWT Guard Router looks up the guard account registered for that guard_id and forwards the verification request to this contract.
3

The guard verifies the JWT

The Custom Issuer Guard checks the RS256 signature against its stored keys, then validates the issuer and the fatxn binding. On success it returns the token subject.
4

MPC signs at the derived path

If verification passes, NEAR Auth asks the MPC network to sign at the identity’s deterministic path, and a relayer can submit the resulting transaction.

What it verifies

Verification runs through the shared internal_verify flow inherited from base-jwt-guard, plus the custom-claims check implemented by this guard. A token is accepted only when all of the following hold:
CheckRule
SizeThe JWT must not exceed 7 KB (7168 bytes).
SignatureThe RS256 signature must verify against at least one stored RSA public key, using PKCS#1 v1.5 padding over the header.payload segment.
fatxnThe fatxn claim (the encoded transaction / delegate-action payload) must byte-for-byte equal the sign_payload passed to sign.
expThe token must not be expired (exp > current block time in seconds).
nbfThe token must already be valid (nbf ≤ current block time), when present.
issThe iss claim must exactly match the issuer argument supplied by the caller.
Unlike the Auth0 Guard, the Custom Issuer Guard does not check the aud (audience) claim. Trust in the token comes from the signature plus the iss match, so the RSA keys held by this guard must correspond only to issuers you control. Anyone who can mint a validly signed token for the configured issuer can pass verification.
The custom-claims portion is deliberately minimal — it parses only the fatxn field and compares it to the sign payload:
custom-issuer-guard/src/lib.rs
/// Custom claims for a NEAR Auth custom-issuer JWT.
pub struct CustomClaims {
    pub fatxn: Vec<u8>,
}

// Inside verify_custom_claims:
if claims.fatxn != sign_payload {
    return (false, "Transaction payload mismatch".to_string());
}
(true, "".to_string())
The iss, exp, and nbf checks (and the RS256 verification itself) are handled by the shared base-jwt-guard logic, so this guard only has to bind the token to the exact transaction being signed.

Methods

verify

The public entry point the router calls during a signing request.
pub fn verify(
    &self,
    issuer: String,
    jwt: String,
    sign_payload: Vec<u8>,
    predecessor: AccountId,
) -> (bool, String)
It delegates to the shared internal_verify and returns a tuple of (verified, message). On success, verified is true and the second element carries the token’s sub (subject) claim; on failure it is false with a human-readable reason such as "Invalid issuer", "Token expired", or "Transaction payload mismatch".

set_public_keys

Refreshes the guard’s RSA keys by pulling the current key set from the configured Attestation contract. This is the mechanism that enables dynamic key rotation — no redeploy required.
pub fn set_public_keys(&mut self) -> Promise
It issues a cross-contract call to get_public_keys() on the attestation contract, then in the on_set_public_keys_callback it validates every returned key with assert_valid_public_key and stores them. If the attestation call fails, the callback panics so no partial or invalid key set is ever persisted.
Because keys are sourced from attestation, anyone can call set_public_keys — it only mirrors what the DAO-governed attestation contract already publishes. Governance of which keys are trusted lives in the attestation contract, not here.

set_attestation_contract and get_attestation_contract

Point the guard at a different attestation contract, or read the one currently configured.
#[access_control_any(roles(Role::DAO))]
pub fn set_attestation_contract(&mut self, attestation_contract: AccountId)

pub fn get_attestation_contract(&self) -> AccountId
Changing the attestation source is a privileged operation restricted to the DAO role; reading it is open.

get_public_keys

The JwtGuard trait accessor that returns the RSA public keys the guard currently trusts.
fn get_public_keys(&self) -> Vec<JwtPublicKey>
Each JwtPublicKey is the RSA modulus (n) and exponent (e) as byte vectors — 2048-bit PKCS#1 v1.5 keys.

Roles and access control

The guard uses the near-plugins access-control and upgradable framework. Four roles govern administration and staged upgrades:
RoleResponsibility
DAOSuper-administrative role. Can repoint the attestation contract and holds every upgrade permission below.
CodeStagerCan stage new contract code ahead of a deployment.
CodeDeployerCan deploy staged code updates.
DurationManagerCan initialize and update the upgrade timelock (duration) settings.
Upgrades are staged and time-locked: the code_stagers, code_deployers, and the duration initializer/updater/applier permissions are each granted to the relevant role and to DAO, so the DAO can always act as a backstop. Roles are seeded at initialization from a RolesConfig of super_admins, admins, and grantees, alongside the initial public_keys.
#[init]
pub fn init(config: CustomIssuerGuardConfig, attestation_contract: AccountId) -> Self
CustomIssuerGuardConfig carries the starting public_keys and the roles configuration; the attestation contract account is passed separately and can later be changed by the DAO.

How it differs from the Auth0 Guard

Audience check

Auth0 Guard requires aud to contain its own account id. The Custom Issuer Guard performs no audience check.

Key management

Auth0 Guard keys are owner-managed. Custom Issuer Guard keys are pulled from a DAO-governed attestation contract and rotate dynamically.

Issuer

Auth0 Guard trusts Auth0’s issuer. This guard trusts whatever iss you supply, backed by keys you attest to.

Governance

Auth0 Guard is single-owner. This guard is role-based (DAO and upgrade roles) with staged, time-locked upgrades.
Everything else — the RS256 signature check, the 7 KB size limit, the fatxn-to-sign_payload binding, and the exp/nbf/iss validation — is shared with the other guards through base-jwt-guard.

Next steps

Custom Issuer

Run your own OIDC issuer service that mints the JWTs this guard verifies.

Attestation

The DAO-governed contract that publishes the RSA keys the guard fetches.

JWT Guard Router

How signing requests are routed to the right guard.

Auth0 Guard

The default guard, and the audience-checking counterpart to this one.