{"record":{"id":"5abe0b443cafb697","repo":"apereo/cas","slug":"token-has-expired-s-and-is-after-s","errorCode":null,"errorMessage":"Token has expired: %s and is after %s","messagePattern":"Token has expired: (.+?) and is after (.+?)","errorType":"exception","errorClass":"AuthenticationException","httpStatus":null,"severity":"error","filePath":"support/cas-server-support-heimdall/src/main/java/org/apereo/cas/heimdall/engine/DefaultAuthorizationPrincipalParser.java","lineNumber":137,"sourceCode":"            return Optional.empty();\n        }\n    }\n\n    protected JWTClaimsSet buildClaimSetFromAuthentication(final String token) throws Throwable {\n        val usernamePass = Splitter.on(':').splitToList(EncodingUtils.decodeBase64ToString(token));\n        val credential = new UsernamePasswordCredential(usernamePass.getFirst(), usernamePass.getLast());\n        val authResultBuilder = authenticationSystemSupport.handleInitialAuthenticationTransaction(null, credential);\n        val authentication = authenticationSystemSupport.finalizeAllAuthenticationTransactions(authResultBuilder, null);\n        val claimsMap = buildClaimsFromAuthentication(authentication.getAuthentication());\n        return JWTClaimsSet.parse(claimsMap);\n    }\n\n    protected JWTClaimsSet validateClaims(final JWTClaimsSet claimsSet) {\n        val maxClockSkew = Beans.newDuration(casProperties.getAuthn().getOidc().getCore().getSkew()).toSeconds();\n        val now = new Date();\n        val exp = claimsSet.getExpirationTime();\n        if (exp != null && !DateUtils.isAfter(exp, now, maxClockSkew)) {\n            throw new AuthenticationException(\"Token has expired: %s and is after %s\".formatted(exp, now));\n        }\n        val nbf = claimsSet.getNotBeforeTime();\n        if (nbf != null && !DateUtils.isBefore(nbf, now, maxClockSkew)) {\n            throw new AuthenticationException(\"Token cannot be used before %s and now is %s\".formatted(nbf, now));\n        }\n        return claimsSet;\n    }\n\n    private Optional<JWTClaimsSet> getJwtClaimsSetFromAccessToken(final String token) {\n        try {\n            val ticket = ticketRegistry.getTicket(token, OAuth20AccessToken.class);\n            FunctionUtils.throwIf(ticket == null || ticket.isExpired(),\n                () -> new AuthenticationException(\"Token %s is not found or has expired\".formatted(token)));\n            val claimsMap = buildClaimsFromAuthentication(ticket.getAuthentication());\n            claimsMap.putAll(ticket.getClaims());\n            claimsMap.put(OAuth20Constants.SCOPE, ticket.getScopes());\n            claimsMap.put(OAuth20Constants.TOKEN, token);\n            return Optional.of(JWTClaimsSet.parse(claimsMap));","sourceCodeStart":119,"sourceCodeEnd":155,"githubUrl":"https://github.com/apereo/cas/blob/e7288fc434b4f4505b8452e1a57e8fb3111bb863/support/cas-server-support-heimdall/src/main/java/org/apereo/cas/heimdall/engine/DefaultAuthorizationPrincipalParser.java#L119-L155","documentation":"During JWT claim validation, the parser compares the token's exp claim to the current time allowing a configurable clock skew (cas.authn.oidc.core.skew). If the expiration time is not after 'now' within the skew, it throws AuthenticationException 'Token has expired: <exp> and is after <now>'.","triggerScenarios":"validateClaims (called from parseAuthorizationHeader) receives a JWTClaimsSet whose expirationTime is non-null and has passed (minus allowed clock skew).","commonSituations":"Client cached a token beyond its lifetime; long-running service replaying an old assertion; severe clock drift between issuer and CAS server; token issued for a short-lived flow (e.g. OIDC access token) reused later.","solutions":["Obtain a fresh token and retry the request","Increase the allowed clock skew via cas.authn.oidc.core.skew if servers' clocks differ mildly","Synchronize clocks with NTP on both issuer and CAS hosts","Implement token refresh in the client before expiry instead of reusing expired tokens"],"exampleFix":"// before\ncas.authn.oidc.core.skew=PT0S\n// after\ncas.authn.oidc.core.skew=PT2M","handlingStrategy":"validation","validationCode":"Date exp = claimsSet.getExpirationTime();\nif (exp != null && exp.before(new Date())) {\n    throw new IllegalStateException(\"Token expired, refresh before calling\");\n}","typeGuard":null,"tryCatchPattern":"try {\n    return parser.claims(token);\n} catch (AuthenticationException e) {\n    // trigger token refresh flow and retry once\n}","preventionTips":["Refresh tokens proactively before expiry","Synchronize clocks with NTP","Configure a small positive skew on the CAS side"],"tags":["jwt","token-expired","claims-validation","clock-skew"],"backgroundTag":"jwt-token-expired","analyzedSha":"e7288fc434b4f4505b8452e1a57e8fb3111bb863","analyzedAt":"2026-09-08T15:39:16.015Z","contentChangedAt":"2026-09-08T15:39:16.015Z","schemaVersion":2},"datasetVersion":"2026-09-15T23:17:13.987Z"}