{"record":{"id":"33a740dccd9c10ae","repo":"SonarSource/sonarqube","slug":"tokens-expiring-after-s-are-not-allowed-please-u-33a740","errorCode":null,"errorMessage":"Tokens expiring after %s are not allowed. Please use a valid expiration date.","messagePattern":"Tokens expiring after (.+?) are not allowed\\. Please use a valid expiration date\\.","errorType":"validation","errorClass":"IllegalArgumentException","httpStatus":400,"severity":"error","filePath":"server/sonar-webserver-webapi/src/main/java/org/sonar/server/usertoken/ws/GenerateActionValidation.java","lineNumber":104,"sourceCode":"  }\n\n  static void validateMaxExpirationDate(MaxTokenLifetimeOption maxTokenLifetime, LocalDate expirationDate) {\n    maxTokenLifetime.getDays()\n      .ifPresent(days -> compareExpirationDateToMaxAllowedLifetime(expirationDate, LocalDate.now(ZoneOffset.UTC).plusDays(days)));\n  }\n\n  static void validateMaxExpirationDate(MaxTokenLifetimeOption maxTokenLifetime) {\n    maxTokenLifetime.getDays()\n      .ifPresent(days -> {\n        throw new IllegalArgumentException(\n          String.format(\"Tokens expiring after %s are not allowed. Please use an expiration date.\",\n            LocalDate.now(ZoneOffset.UTC).plusDays(days).format(DateTimeFormatter.ISO_DATE)));\n      });\n  }\n\n  static void compareExpirationDateToMaxAllowedLifetime(LocalDate expirationDate, LocalDate maxExpirationDate) {\n    if (expirationDate.isAfter(maxExpirationDate)) {\n      throw new IllegalArgumentException(\n        String.format(\"Tokens expiring after %s are not allowed. Please use a valid expiration date.\",\n          maxExpirationDate.format(DateTimeFormatter.ISO_DATE)));\n    }\n  }\n\n  static void validateMinExpirationDate(LocalDate localDate) {\n    if (localDate.isBefore(LocalDate.now(ZoneOffset.UTC).plusDays(1))) {\n      throw new IllegalArgumentException(\n        String.format(\"The minimum value for parameter %s is %s.\", PARAM_EXPIRATION_DATE, LocalDate.now(ZoneOffset.UTC).plusDays(1).format(DateTimeFormatter.ISO_DATE)));\n    }\n  }\n\n  static void validateParametersCombination(UserTokenSupport userTokenSupport, DbSession dbSession, Request request, TokenType tokenType) {\n    if (PROJECT_ANALYSIS_TOKEN.equals(tokenType)) {\n      validateProjectAnalysisParameters(userTokenSupport, dbSession, request);\n    } else if (GLOBAL_ANALYSIS_TOKEN.equals(tokenType)) {\n      validateGlobalAnalysisParameters(userTokenSupport, request);\n    }","sourceCodeStart":86,"sourceCodeEnd":122,"githubUrl":"https://github.com/SonarSource/sonarqube/blob/184c821202192afc1c599fc912d0889b69fffa53/server/sonar-webserver-webapi/src/main/java/org/sonar/server/usertoken/ws/GenerateActionValidation.java#L86-L122","documentation":"compareExpirationDateToMaxAllowedLifetime throws this IllegalArgumentException when the requested token expiration_date is strictly after the maximum allowed by the server's max token lifetime setting. The message includes the latest permitted date (ISO format), making it clear which window is acceptable.","triggerScenarios":"POST api/user_tokens/generate with expiration_date later than LocalDate.now(UTC) + maxLifetimeDays, e.g. requesting 1 year when policy allows 90 days.","commonSituations":"Scripts hardcoding '+1 year' expirations; policy tightened from unlimited to a capped lifetime while automation kept old values; server migration to an org with stricter token policies; off-by-boundary requests (day after cutoff).","solutions":["Cap the expiration at the date shown in the message (maxExpirationDate); compute it as today UTC plus the configured max days.","Set expiration_date to that boundary or earlier, and add token rotation to automation.","If the requested duration is a genuine business need, have a server admin raise sonar.auth.token.max-allowed-lifetime.","Dynamically read/adapt: compute expiry in the script from the allowed lifetime instead of hardcoding dates."],"exampleFix":"// before: hardcoded one-year expiry\ncurl -su \"$TOKEN:\" -X POST 'https://sonar/api/user_tokens/generate?name=ci&expiration_date=2027-09-09'\n// after: clamp to policy (e.g. 90 days)\nEXP=$(date -u -d '+90 days' +%F)\ncurl -su \"$TOKEN:\" -X POST \"https://sonar/api/user_tokens/generate?name=ci&expiration_date=$EXP\"","handlingStrategy":"validation","validationCode":"function clampToPolicy(expiryIso, maxDays) {\n  const max = new Date(Date.now() + maxDays * 86400000).toISOString().slice(0, 10);\n  return expiryIso > max ? max : expiryIso;\n}\nconst safeExp = clampToPolicy(requestedExp, maxLifetimeDays);","typeGuard":"function isWithinWindow(e, maxDays) {\n  if (typeof e !== 'string' || !/^\\d{4}-\\d{2}-\\d{2}$/.test(e)) return false;\n  return e <= new Date(Date.now() + maxDays * 86400000).toISOString().slice(0, 10);\n}","tryCatchPattern":"try {\n  await generateToken(name, exp);\n} catch (e) {\n  if (e.status === 400 && /use a valid expiration date/.test(e.message)) {\n    const max = e.message.match(/after (\\d{4}-\\d{2}-\\d{2})/)?.[1];\n    return generateToken(name, max); // retry at the boundary\n  }\n  throw e;\n}","preventionTips":["Compute expirations relative to today UTC, never hardcode future dates","Clamp requested expiry to now + maxLifetimeDays before calling","Re-validate token durations after any policy change or server migration","Include rotation jobs so clamped lifetimes stay sufficient"],"tags":["sonarqube","rest-api","token-expiration","validation"],"backgroundTag":"value-out-of-range","analyzedSha":"184c821202192afc1c599fc912d0889b69fffa53","analyzedAt":"2026-09-09T12:23:51.573Z","contentChangedAt":"2026-09-09T12:23:51.573Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}