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.

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.

Appendix: 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)