{"record":{"id":"bcb8033224470f6f","repo":"SonarSource/sonarqube","slug":"tokens-expiring-after-s-are-not-allowed-please-u","errorCode":null,"errorMessage":"Tokens expiring after %s are not allowed. Please use an expiration date.","messagePattern":"Tokens expiring after (.+?) are not allowed\\. Please use an 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":96,"sourceCode":"  void validateExpirationDate(@Nullable LocalDate expirationDate) {\n    MaxTokenLifetimeOption maxTokenLifetime = getMaxTokenLifetimeOption();\n    if (expirationDate != null) {\n      validateMinExpirationDate(expirationDate);\n      validateMaxExpirationDate(maxTokenLifetime, expirationDate);\n    } else {\n      validateMaxExpirationDate(maxTokenLifetime);\n    }\n  }\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    }","sourceCodeStart":78,"sourceCodeEnd":114,"githubUrl":"https://github.com/SonarSource/sonarqube/blob/184c821202192afc1c599fc912d0889b69fffa53/server/sonar-webserver-webapi/src/main/java/org/sonar/server/usertoken/ws/GenerateActionValidation.java#L78-L114","documentation":"GenerateActionValidation throws this IllegalArgumentException when the server-wide max token lifetime policy is set in 'no expiration allowed' mode (validateMaxExpirationDate): any token generation request is rejected outright, with the message naming the effective cutoff date. The policy forbids non-expiring tokens, so callers must supply an expiration date within the allowed window.","triggerScenarios":"POST api/user_tokens/generate (including with an expiration_date) when sonar.auth.token.max-allowed-lifetime is configured so that getDays() is present and validateMaxExpirationDate is reached — i.e. the server policy prohibits the requested lifetime entirely.","commonSituations":"Enterprise security policy enabled a hard maximum token lifetime; automation still generating long-lived or no-expiration tokens; scripts written before the lifetime policy feature was introduced; server hardened after an audit.","solutions":["Always pass an expiration_date on or before the stated cutoff (date printed in the message).","Shorten token lifetime in automation: generate tokens for the minimum needed duration and rotate them.","If generation must be unexpiring for service accounts, request a policy exception and adjust sonar.auth.token.max-allowed-lifetime (server admin action).","Migrate CI to alternative credentials (e.g. short-lived tokens refreshed by pipeline) that comply with the policy."],"exampleFix":"// before: no expiration under a max-lifetime policy\ncurl -su \"$TOKEN:\" -X POST 'https://sonar/api/user_tokens/generate?name=ci'\n// after: bounded expiration\nEXP=$(date -u -d '+30 days' +%F)\ncurl -su \"$TOKEN:\" -X POST \"https://sonar/api/user_tokens/generate?name=ci&expiration_date=$EXP\"","handlingStrategy":"validation","validationCode":"function withinMaxLifetime(expiryIso, maxDays) {\n  const cutoff = new Date(Date.now() + maxDays * 86400000).toISOString().slice(0, 10);\n  return expiryIso != null && expiryIso <= cutoff;\n}\nif (!expirationDate) throw new Error('policy requires an expiration_date; non-expiring tokens are not allowed');","typeGuard":"function hasAllowedExpiration(e) {\n  return typeof e === 'string' && /^\\d{4}-\\d{2}-\\d{2}$/.test(e);\n}","tryCatchPattern":"try {\n  await generateToken(name, exp);\n} catch (e) {\n  if (e.status === 400 && /Tokens expiring after .* are not allowed/.test(e.message)) {\n    const cutoff = e.message.match(/after (\\d{4}-\\d{2}-\\d{2})/)?.[1];\n    return generateToken(name, cutoff); // clamp to policy boundary\n  }\n  throw e;\n}","preventionTips":["Always send expiration_date under a max-lifetime policy","Read the policy value (sonar.auth.token.max-allowed-lifetime) and clamp dates in scripts","Rotate short-lived tokens instead of minting long-lived ones","Re-test token automation after security policy changes"],"tags":["sonarqube","rest-api","token-expiration","policy"],"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"}