Why Auth0 email_verified Was Missing from My Access Token

Published: (February 18, 2026 at 08:35 PM EST)
3 min read
Source: Dev.to

Source: Dev.to

The Problem

A Spring Boot filter was rejecting users even though they were marked Verified in the Auth0 dashboard.

Boolean emailVerified = jwt.getClaimAsBoolean("email_verified");
if (emailVerified == null || !emailVerified) {
    response.setStatus(HttpServletResponse.SC_FORBIDDEN);
    response.getWriter().write("{\"error\":\"Email not verified\"}");
    return;
}

Log output:

User email not verified: sub=auth0|67c6a695657d0f4f7ac8736f

The users were verified. The reason: Auth0 does not include email_verified in access tokens by default—only in ID tokens.

When decoding an access token you might see:

{
  "iss": "https://your-tenant.auth0.com/",
  "sub": "auth0|67c6a695657d0f4f7ac8736f",
  "aud": ["https://your-api/"],
  "iat": 1767421775,
  "exp": 1767508175,
  "scope": "openid profile email"
}

Notice the missing email_verified claim, even though the email scope was requested.

Adding email_verified to the Access Token

Using a Post‑Login Action

  1. In the Auth0 Dashboard go to Actions → Flows → Login.
  2. Add a new Action and paste the following code:
exports.onExecutePostLogin = async (event, api) => {
  // Add email_verified to access token for API validation
  api.accessToken.setCustomClaim('email_verified', event.user.email_verified);
};
  1. Deploy the Action and drag it into the Login flow.

After deployment, the access token will contain:

{
  "email_verified": true,
  "iss": "https://your-tenant.auth0.com/",
  "sub": "auth0|67c6a695657d0f4f7ac8736f",
  ...
}

Why This Works

  • ID tokens are intended for the client (frontend) to convey identity information.
  • Access tokens are meant for the API (backend) to authorize requests.
  • Auth0’s default philosophy is to keep identity‑related claims out of access tokens, but many APIs need them. Adding the claim via an Action is the recommended approach.

Alternative: Query the UserInfo Endpoint

If you cannot use Actions, you can fetch the claim from the /userinfo endpoint when it is missing:

if (emailVerified == null) {
    String userinfoUrl = auth0Issuer + "userinfo";
    // Perform GET with Bearer token
    // Parse JSON response for "email_verified"
}

Note: This adds extra latency to every request, so the Action method is generally preferred.

Social Logins

For social connections (Google, GitHub, etc.) Auth0 automatically sets email_verified: true because the provider has already verified the email. You could skip the check for non‑database connections:

String subject = jwt.getSubject();
boolean isDatabaseConnection = subject != null && subject.startsWith("auth0|");

if (isDatabaseConnection) {
    // Only check email_verified for username/password users
}

Full Action Example (including optional email resend)

exports.onExecutePostLogin = async (event, api) => {
  // Always include email_verified in access token
  api.accessToken.setCustomClaim('email_verified', event.user.email_verified);

  // Auto‑resend verification email for unverified users (rate‑limited)
  if (!event.user.email_verified) {
    const lastSent = event.user.user_metadata?.verification_email_last_sent;
    const now = Date.now();
    const ONE_HOUR = 60 * 60 * 1000;

    if (!lastSent || (now - lastSent) > ONE_HOUR) {
      // Trigger verification email via Management API
      // (see Auth0 docs for Management API setup)

      api.user.setUserMetadata('verification_email_last_sent', now);
    }
  }
};

Takeaways

  • ID token ≠ Access token – they serve different purposes and contain different claims.
  • Use Auth0 Actions to add any custom claim you need to the access token.
  • Verify the actual token contents (e.g., with jwt.io) rather than relying solely on the dashboard UI.
0 views
Back to Blog

Related posts

Read more »