floci-io/floci · error · InvalidTokenException

InvalidIdentityToken

InvalidIdentityToken

Error message

The web identity token is not a well-formed JWT

What it means

Thrown by WebIdentityTokenVerifier.verify when the web identity token supplied to STS AssumeRoleWithWebIdentity does not split into exactly three dot-separated JWT segments (header.payload.signature). split is called with limit -1 so trailing empty segments count: an unsigned 'header.payload.' token is treated as three parts and moves on to the algorithm check instead. The error surfaces as STS InvalidIdentityToken.

Source

Thrown at src/main/java/io/github/hectorvent/floci/core/common/WebIdentityTokenVerifier.java:78

    public Optional<String> peekIssuer(String token) {
        return parseClaims(token).map(claims -> claims.path("iss").asText(null))
                .filter(iss -> iss != null && !iss.isBlank());
    }

    /**
     * Fully verifies {@code token}: RS256 signature against {@code publicKey}, {@code iss} equal to
     * {@code expectedIssuer}, {@code aud} containing {@code requiredAudience}, and {@code exp}/
     * {@code nbf} within {@link #CLOCK_SKEW_SECONDS}.
     *
     * <p>Claim comparisons are exact and case-sensitive, matching AWS treatment of OIDC claims.
     */
    public WebIdentityToken verify(String token, RSAPublicKey publicKey, String expectedIssuer,
                                   String requiredAudience) throws InvalidTokenException {
        // Limit -1 keeps trailing empty segments, so an unsigned "header.payload." token is seen as
        // three parts and rejected by the algorithm check below rather than as malformed.
        String[] parts = token == null ? new String[0] : token.split("\\.", -1);
        if (parts.length != 3) {
            throw new InvalidTokenException("The web identity token is not a well-formed JWT");
        }

        JsonNode header = decodeJson(parts[0])
                .orElseThrow(() -> new InvalidTokenException("The web identity token header is not valid JSON"));
        String alg = header.path("alg").asText("");
        if (!"RS256".equals(alg)) {
            throw new InvalidTokenException("Unsupported web identity token algorithm: "
                    + (alg.isEmpty() ? "none" : alg));
        }

        if (!signatureValid(parts[0] + "." + parts[1], parts[2], publicKey)) {
            throw new InvalidTokenException("The web identity token signature is invalid");
        }

        JsonNode claims = decodeJson(parts[1])
                .orElseThrow(() -> new InvalidTokenException("The web identity token payload is not valid JSON"));

        String issuer = claims.path("iss").asText(null);

View on GitHub (pinned to 62ff490619)

Solutions

  1. Check the token has exactly two '.' characters and three non-empty base64url segments before calling AssumeRoleWithWebIdentity
  2. Make sure you pass the OIDC provider's signed JWT (usually the id_token), not an opaque access token
  3. Re-fetch a fresh token from the identity provider and retry

Example fix

// before
String token = oidcClient.getAccessToken(); // opaque, not a JWT
sts.assumeRoleWithWebIdentity(...);

// after
String token = oidcClient.getIdToken(); // signed RS256 JWT
if (token.split("\\.", -1).length != 3) throw new IllegalArgumentException("not a JWT");
sts.assumeRoleWithWebIdentity(...);
Defensive patterns

Strategy: validation

Validate before calling

boolean isWellFormedJwt(String token) {
    if (token == null) return false;
    String[] parts = token.split("\\.", -1);
    if (parts.length != 3) return false;
    return !parts[0].isEmpty() && !parts[1].isEmpty() && !parts[2].isEmpty();
}

Try / catch

try {
    sts.assumeRoleWithWebIdentity(req);
} catch (InvalidIdentityTokenException e) {
    // malformed token: re-authenticate, do not retry the same token
}

Prevention

When it happens

Trigger: Passing a null, empty, or truncated token; passing only two segments ('header.payload'); passing a token with four dots (double separator); or passing an opaque access token that is not a JWT at all in the WebIdentityToken parameter.

Common situations: Confusing the OIDC ID token with an opaque provider access token; copy-paste truncation of long JWTs; environment variables stripping characters; building the token by concatenation and omitting the signature segment.


AI-assisted analysis of floci-io/floci@62ff490619 (2026-08-14). Data as JSON: /api/errors/fae46ba71fc2786d. Report an issue: GitHub.