tokenValidationContext(TokenValidationContextHolder tokenValidationContextHolder){
- if(tokenValidationContextHolder == null){
- throw new IllegalStateException("{} cannot be null, check your configuration.");
- }
- return Optional.ofNullable(tokenValidationContextHolder.getTokenValidationContext());
- }
-}
diff --git a/token-validation-core/src/main/java/no/nav/security/token/support/core/validation/ConfigurableJwtTokenValidator.java b/token-validation-core/src/main/java/no/nav/security/token/support/core/validation/ConfigurableJwtTokenValidator.java
deleted file mode 100644
index d8936d19..00000000
--- a/token-validation-core/src/main/java/no/nav/security/token/support/core/validation/ConfigurableJwtTokenValidator.java
+++ /dev/null
@@ -1,86 +0,0 @@
-package no.nav.security.token.support.core.validation;
-
-import com.nimbusds.jose.JWSAlgorithm;
-import com.nimbusds.jose.jwk.source.RemoteJWKSet;
-import com.nimbusds.jose.proc.JWSVerificationKeySelector;
-import com.nimbusds.jose.proc.SecurityContext;
-import com.nimbusds.jwt.JWT;
-import com.nimbusds.jwt.JWTClaimsSet;
-import com.nimbusds.jwt.JWTParser;
-import com.nimbusds.jwt.proc.DefaultJWTClaimsVerifier;
-import com.nimbusds.jwt.proc.DefaultJWTProcessor;
-import com.nimbusds.jwt.proc.JWTClaimsSetVerifier;
-import no.nav.security.token.support.core.exceptions.JwtTokenValidatorException;
-
-import java.util.HashSet;
-import java.util.List;
-import java.util.Optional;
-
-/**
- * Configurable JwtTokenValidator. Allows for optional claims, does not validate audience.
- *
- * @deprecated Use {@link DefaultConfigurableJwtValidator} instead.
- */
-@Deprecated(since = "3.1.3", forRemoval = true)
-public class ConfigurableJwtTokenValidator implements JwtTokenValidator {
-
- private final String issuer;
- private final RemoteJWKSet remoteJWKSet;
- private final List defaultRequiredClaims = List.of("sub", "aud", "iss", "iat", "exp", "nbf");
- private final List requiredClaims;
-
- public ConfigurableJwtTokenValidator(String issuer, List optionalClaims, RemoteJWKSet remoteJWKSet) {
- this.issuer = issuer;
- this.remoteJWKSet = remoteJWKSet;
- this.requiredClaims = removeOptionalClaims(defaultRequiredClaims, Optional.ofNullable(optionalClaims).orElse(List.of()));
- }
-
- @Override
- public void assertValidToken(String tokenString) throws JwtTokenValidatorException {
- verify(issuer, tokenString,
- new JWSVerificationKeySelector<>(
- JWSAlgorithm.RS256,
- remoteJWKSet
- )
- );
- }
-
- private void verify(String issuer, String tokenString, JWSVerificationKeySelector keySelector) {
- verify(
- tokenString,
- new DefaultJWTClaimsVerifier<>(
- new JWTClaimsSet.Builder()
- .issuer(issuer)
- .build(),
- new HashSet<>(requiredClaims)
- ),
- keySelector
- );
- }
-
- private void verify(String tokenString, JWTClaimsSetVerifier jwtClaimsSetVerifier, JWSVerificationKeySelector keySelector) {
- try {
- var jwtProcessor = new DefaultJWTProcessor<>();
- jwtProcessor.setJWSKeySelector(keySelector);
- jwtProcessor.setJWTClaimsSetVerifier(jwtClaimsSetVerifier);
- var token = parse(tokenString);
- jwtProcessor.process(token, null);
- } catch (Throwable t) {
- throw new JwtTokenValidatorException("Token validation failed: " + t.getMessage(), t);
- }
- }
-
- private static List removeOptionalClaims(List first, List second) {
- return first.stream()
- .filter(c -> !second.contains(c))
- .toList();
- }
-
- private JWT parse(String tokenString) {
- try {
- return JWTParser.parse(tokenString);
- } catch (Throwable t) {
- throw new JwtTokenValidatorException("Token verification failed: " + t.getMessage(), t);
- }
- }
-}
diff --git a/token-validation-core/src/main/java/no/nav/security/token/support/core/validation/DefaultConfigurableJwtValidator.java b/token-validation-core/src/main/java/no/nav/security/token/support/core/validation/DefaultConfigurableJwtValidator.java
deleted file mode 100644
index 271eee83..00000000
--- a/token-validation-core/src/main/java/no/nav/security/token/support/core/validation/DefaultConfigurableJwtValidator.java
+++ /dev/null
@@ -1,133 +0,0 @@
-package no.nav.security.token.support.core.validation;
-
-import com.nimbusds.jose.JWSAlgorithm;
-import com.nimbusds.jose.jwk.source.JWKSource;
-import com.nimbusds.jose.proc.JWSVerificationKeySelector;
-import com.nimbusds.jose.proc.SecurityContext;
-import com.nimbusds.jwt.JWTClaimNames;
-import com.nimbusds.jwt.JWTClaimsSet;
-import com.nimbusds.jwt.proc.ConfigurableJWTProcessor;
-import com.nimbusds.jwt.proc.DefaultJWTProcessor;
-import no.nav.security.token.support.core.exceptions.JwtTokenValidatorException;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Optional;
-import java.util.Set;
-import java.util.stream.Collectors;
-
-/**
- * The default configurable JwtTokenValidator.
- * Configures sane defaults and delegates verification to {@link DefaultJwtClaimsVerifier}:
- *
- * The following set of claims are required by default and mustbe present in the JWTs:
- *
- * - iss - Issuer
- * - sub - Subject
- * - aud - Audience
- * - exp - Expiration Time
- * - iat - Issued At
- *
- *
- * Otherwise, the following checks are in place:
- *
- * - The issuer ("iss") claim value must match exactly with the specified accepted issuer value.
- * - At least one of the values in audience ("aud") claim must match one of the specified accepted audiences.
- * - Time validity checks are performed on the issued at ("iat"), expiration ("exp") and not-before ("nbf") claims if and only if they are present.
- *
- *
- * Note: the not-before ("nbf") claim is not a required claim. Conversely, the expiration ("exp") claim is a default required claim.
- *
- * Specifying optional claims will remove any matching claims from the default set of required claims.
- *
- * Audience validation is only skipped if the claim is explicitly configured as an optional claim, and the list of accepted audiences is empty / not configured.
- *
- *
If the audience claim is explicitly configured as an optional claim and the list of accepted audience is non-empty, the following rules apply:
- *
- * - If the audience claim is present (non-empty) in the JWT, it will be matched against the list of accepted audiences.
- * - If the audience claim is not present, the audience match and existence checks are skipped - since it is an optional claim.
- *
- *
- * An empty list of accepted audiences alone does not remove the audience ("aud") claim from the default set of required claims; the claim must explicitly be specified as optional.
- */
-public class DefaultConfigurableJwtValidator implements JwtTokenValidator {
- private static final List DEFAULT_REQUIRED_CLAIMS = List.of(
- JWTClaimNames.AUDIENCE,
- JWTClaimNames.EXPIRATION_TIME,
- JWTClaimNames.ISSUED_AT,
- JWTClaimNames.ISSUER,
- JWTClaimNames.SUBJECT
- );
- private static final Set PROHIBITED_CLAIMS = Collections.emptySet();
- private final JWKSource jwkSource;
- private final ConfigurableJWTProcessor jwtProcessor;
-
- public DefaultConfigurableJwtValidator(String issuer, List acceptedAudiences, JWKSource jwkSource) {
- this(issuer, acceptedAudiences, null, jwkSource);
- }
-
- public DefaultConfigurableJwtValidator(String issuer, List acceptedAudiences, List optionalClaims, JWKSource jwkSource) {
- acceptedAudiences = Optional.ofNullable(acceptedAudiences).orElse(List.of());
- optionalClaims = Optional.ofNullable(optionalClaims).orElse(List.of());
-
- var requiredClaims = difference(DEFAULT_REQUIRED_CLAIMS, optionalClaims);
- var exactMatchClaims = new JWTClaimsSet.Builder()
- .issuer(issuer)
- .build();
- var keySelector = new JWSVerificationKeySelector<>(
- JWSAlgorithm.RS256,
- jwkSource
- );
- var claimsVerifier = new DefaultJwtClaimsVerifier<>(
- acceptedAudiences(acceptedAudiences, optionalClaims),
- exactMatchClaims,
- requiredClaims,
- PROHIBITED_CLAIMS
- );
-
- var processor = new DefaultJWTProcessor<>();
- processor.setJWSKeySelector(keySelector);
- processor.setJWTClaimsSetVerifier(claimsVerifier);
-
- this.jwkSource = jwkSource;
- this.jwtProcessor = processor;
- }
-
- @Override
- public void assertValidToken(String tokenString) throws JwtTokenValidatorException {
- try {
- jwtProcessor.process(tokenString, null);
- } catch (Throwable t) {
- throw new JwtTokenValidatorException("Token validation failed: " + t.getMessage(), t);
- }
- }
-
- private static Set acceptedAudiences(List acceptedAudiences, List optionalClaims) {
- if (!optionalClaims.contains(JWTClaimNames.AUDIENCE)) {
- return new HashSet<>(acceptedAudiences);
- }
-
- if (acceptedAudiences.isEmpty()) {
- // Must be null to effectively skip all audience existence and matching checks
- return null;
- }
-
- // Otherwise, add null to instruct DefaultJwtClaimsVerifier to validate against audience if present in the JWT,
- // but don't require existence of the claim for all JWTs.
- var acceptedAudiencesCopy = new ArrayList<>(acceptedAudiences);
- acceptedAudiencesCopy.add(null);
- return new HashSet<>(acceptedAudiencesCopy);
- }
-
- private static Set difference(List first, List second) {
- return first.stream()
- .filter(c -> !second.contains(c))
- .collect(Collectors.toUnmodifiableSet());
- }
-
- protected JWKSource getJwkSource() {
- return this.jwkSource;
- }
-}
diff --git a/token-validation-core/src/main/java/no/nav/security/token/support/core/validation/DefaultJwtClaimsVerifier.java b/token-validation-core/src/main/java/no/nav/security/token/support/core/validation/DefaultJwtClaimsVerifier.java
deleted file mode 100644
index 155cc0b1..00000000
--- a/token-validation-core/src/main/java/no/nav/security/token/support/core/validation/DefaultJwtClaimsVerifier.java
+++ /dev/null
@@ -1,38 +0,0 @@
-package no.nav.security.token.support.core.validation;
-
-import com.nimbusds.jose.proc.SecurityContext;
-import com.nimbusds.jwt.JWTClaimsSet;
-import com.nimbusds.jwt.proc.BadJWTException;
-import com.nimbusds.jwt.proc.DefaultJWTClaimsVerifier;
-import com.nimbusds.jwt.util.DateUtils;
-import com.nimbusds.openid.connect.sdk.validators.BadJWTExceptions;
-
-import java.util.Date;
-import java.util.Set;
-
-/**
- * Extends {@link com.nimbusds.jwt.proc.DefaultJWTClaimsVerifier} with a time check for the issued at ("iat") claim.
- * The claim is only checked if it exists in the given claim set.
- */
-public class DefaultJwtClaimsVerifier extends DefaultJWTClaimsVerifier {
-
- public DefaultJwtClaimsVerifier(final Set acceptedAudience,
- final JWTClaimsSet exactMatchClaims,
- final Set requiredClaims,
- final Set prohibitedClaims) {
- super(acceptedAudience, exactMatchClaims, requiredClaims, prohibitedClaims);
- }
-
- @Override
- public void verify(final JWTClaimsSet claimsSet, final C context) throws BadJWTException {
- super.verify(claimsSet, context);
-
- Date iat = claimsSet.getIssueTime();
- if (iat != null) {
- Date now = new Date();
- if (!iat.equals(now) && !DateUtils.isBefore(iat, now, super.getMaxClockSkew())) {
- throw BadJWTExceptions.IAT_CLAIM_AHEAD_EXCEPTION;
- }
- }
- }
-}
diff --git a/token-validation-core/src/main/java/no/nav/security/token/support/core/validation/DefaultJwtTokenValidator.java b/token-validation-core/src/main/java/no/nav/security/token/support/core/validation/DefaultJwtTokenValidator.java
deleted file mode 100644
index 143d9557..00000000
--- a/token-validation-core/src/main/java/no/nav/security/token/support/core/validation/DefaultJwtTokenValidator.java
+++ /dev/null
@@ -1,103 +0,0 @@
-package no.nav.security.token.support.core.validation;
-
-import com.nimbusds.jose.JWSAlgorithm;
-import com.nimbusds.jose.jwk.source.RemoteJWKSet;
-import com.nimbusds.jose.proc.JWSVerificationKeySelector;
-import com.nimbusds.jose.proc.SecurityContext;
-import com.nimbusds.jwt.JWT;
-import com.nimbusds.jwt.JWTParser;
-import com.nimbusds.oauth2.sdk.id.ClientID;
-import com.nimbusds.oauth2.sdk.id.Issuer;
-import com.nimbusds.openid.connect.sdk.Nonce;
-import com.nimbusds.openid.connect.sdk.validators.IDTokenValidator;
-import no.nav.security.token.support.core.exceptions.JwtTokenValidatorException;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.text.ParseException;
-import java.util.Date;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
-/**
- * "Default" JwtTokenValidator. JWT must have claims that fulfill the OpenID Connect id_token requirements.
- *
- * @deprecated Use {@link DefaultConfigurableJwtValidator} instead.
- */
-@Deprecated(since = "3.1.3", forRemoval = true)
-public class DefaultJwtTokenValidator implements JwtTokenValidator {
- private static final Logger LOG = LoggerFactory.getLogger(DefaultJwtTokenValidator.class);
- private static final JWSAlgorithm JWS_ALG = JWSAlgorithm.RS256;
- private final Map audienceValidatorMap;
- private final RemoteJWKSet remoteJWKSet;
-
- public DefaultJwtTokenValidator(
- String issuer,
- List acceptedAudience,
- RemoteJWKSet remoteJWKSet
- ) {
- this.remoteJWKSet = remoteJWKSet;
- this.audienceValidatorMap = initializeMap(issuer, acceptedAudience);
- }
-
- @Override
- public void assertValidToken(String tokenString) throws JwtTokenValidatorException {
- assertValidToken(tokenString, null);
- }
-
- public void assertValidToken(String tokenString, String expectedNonce) throws JwtTokenValidatorException {
- JWT token = null;
- try {
- token = JWTParser.parse(tokenString);
- get(token).validate(token, expectedNonce != null ? new Nonce(expectedNonce) : null);
- } catch (NoSuchMethodError e) {
- String msg = "Dependant method not found. Ensure that nimbus-jose-jwt and/or oauth2-oidc-sdk has versions >= 9.x (e.g. Spring Boot >= 2.5.0)";
- LOG.error(msg, e);
- throw new JwtTokenValidatorException(msg, e);
- } catch (Throwable t) {
- throw new JwtTokenValidatorException("Token validation failed", expiryDate(token), t);
- }
- }
-
- protected IDTokenValidator get(JWT jwt) throws ParseException, JwtTokenValidatorException {
- List tokenAud = jwt.getJWTClaimsSet().getAudience();
- for (String aud : tokenAud) {
- if (audienceValidatorMap.containsKey(aud)) {
- return audienceValidatorMap.get(aud);
- }
- }
- LOG.warn("Could not find validator for token audience {} among {}", tokenAud, audienceValidatorMap);
- throw new JwtTokenValidatorException(
- "Could not find appropriate validator to validate token. check your config.");
- }
-
- protected IDTokenValidator createValidator(String issuer, String clientId) {
- Issuer iss = new Issuer(issuer);
- ClientID clientID = new ClientID(clientId);
- JWSVerificationKeySelector jwsKeySelector = new JWSVerificationKeySelector<>(
- JWS_ALG,
- remoteJWKSet
- );
- return new IDTokenValidator(iss, clientID, jwsKeySelector, null);
- }
-
- private static Date expiryDate(JWT token) {
- try {
- return token != null ? token.getJWTClaimsSet().getExpirationTime() : null;
- } catch (ParseException e) {
- return null;
- }
- }
-
- private Map initializeMap(String issuer, List acceptedAudience) {
- if (acceptedAudience == null || acceptedAudience.isEmpty()) {
- throw new IllegalArgumentException("Accepted audience cannot be null or empty in validator config.");
- }
- Map map = new HashMap<>();
- for (String aud : acceptedAudience) {
- map.put(aud, createValidator(issuer, aud));
- }
- return map;
- }
-}
diff --git a/token-validation-core/src/main/java/no/nav/security/token/support/core/validation/JwtTokenAnnotationHandler.java b/token-validation-core/src/main/java/no/nav/security/token/support/core/validation/JwtTokenAnnotationHandler.java
deleted file mode 100755
index 8f05cb56..00000000
--- a/token-validation-core/src/main/java/no/nav/security/token/support/core/validation/JwtTokenAnnotationHandler.java
+++ /dev/null
@@ -1,153 +0,0 @@
-package no.nav.security.token.support.core.validation;
-
-import no.nav.security.token.support.core.api.Protected;
-import no.nav.security.token.support.core.api.ProtectedWithClaims;
-import no.nav.security.token.support.core.api.RequiredIssuers;
-import no.nav.security.token.support.core.api.Unprotected;
-import no.nav.security.token.support.core.context.TokenValidationContextHolder;
-import no.nav.security.token.support.core.exceptions.AnnotationRequiredException;
-import no.nav.security.token.support.core.exceptions.JwtTokenInvalidClaimException;
-import no.nav.security.token.support.core.exceptions.JwtTokenMissingException;
-import no.nav.security.token.support.core.jwt.JwtToken;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.lang.annotation.Annotation;
-import java.lang.reflect.Method;
-import java.util.Arrays;
-import java.util.List;
-import java.util.Objects;
-import java.util.Optional;
-
-import static no.nav.security.token.support.core.utils.Cluster.*;
-import static no.nav.security.token.support.core.utils.JwtTokenUtil.contextHasValidToken;
-import static no.nav.security.token.support.core.utils.JwtTokenUtil.getJwtToken;
-
-public class JwtTokenAnnotationHandler {
-
- private static final List> SUPPORTED_ANNOTATIONS = List.of(RequiredIssuers.class, ProtectedWithClaims.class,
- Protected.class, Unprotected.class);
- protected static final Logger LOG = LoggerFactory.getLogger(JwtTokenAnnotationHandler.class);
- private final TokenValidationContextHolder tokenValidationContextHolder;
-
- public JwtTokenAnnotationHandler(TokenValidationContextHolder tokenValidationContextHolder) {
- this.tokenValidationContextHolder = tokenValidationContextHolder;
- }
-
- public boolean assertValidAnnotation(Method m) throws AnnotationRequiredException {
- return Optional.ofNullable(getAnnotation(m, SUPPORTED_ANNOTATIONS))
- .map(this::assertValidAnnotation)
- .orElseThrow(() -> new AnnotationRequiredException(m));
- }
-
- private boolean assertValidAnnotation(Annotation a) {
- if (a instanceof Unprotected) {
- LOG.debug("annotation is of type={}, no token validation performed.", Unprotected.class.getSimpleName());
- return true;
- }
- if (a instanceof RequiredIssuers r) {
- return handleRequiredIssuers(r);
- }
- if (a instanceof ProtectedWithClaims p) {
- return handleProtectedWithClaims(p);
- }
- if (a instanceof Protected) {
- return handleProtected();
- }
- LOG.debug("Annotation is unknown, type={}, no token validation performed. but possible bug so throw exception", a.annotationType());
- return false;
- }
-
- private boolean handleProtected() {
- LOG.debug("Annotation is of type={}, check if context has valid token.", Protected.class.getSimpleName());
- if (contextHasValidToken(tokenValidationContextHolder)) {
- return true;
- }
- throw new JwtTokenMissingException();
- }
-
- private boolean handleProtectedWithClaims(ProtectedWithClaims a) {
- if (!isProd() && Arrays.stream(a.excludedClusters()).toList().contains(currentCluster())) {
- LOG.info("Excluding current cluster {} from validation", currentCluster());
- return true;
- }
- LOG.debug("Annotation is of type={}, do token validation and claim checking.", ProtectedWithClaims.class.getSimpleName());
- var jwtToken = getJwtToken(a.issuer(), tokenValidationContextHolder);
- if (jwtToken.isEmpty()) {
- throw new JwtTokenMissingException();
- }
-
- if (!handleProtectedWithClaimsAnnotation(a, jwtToken.get())) {
- throw new JwtTokenInvalidClaimException(a);
- }
- return true;
- }
-
- private boolean handleRequiredIssuers(RequiredIssuers a) {
- boolean hasToken = false;
- for (var sub : a.value()) {
- var jwtToken = getJwtToken(sub.issuer(), tokenValidationContextHolder);
- if (jwtToken.isEmpty()) {
- continue;
- }
- if (handleProtectedWithClaimsAnnotation(sub, jwtToken.get())) {
- return true;
- }
- hasToken = true;
- }
- if (!hasToken) {
- throw new JwtTokenMissingException(a);
- }
- throw new JwtTokenInvalidClaimException(a);
- }
-
- protected Annotation getAnnotation(Method method, List> types) {
- return Optional.ofNullable(findAnnotation(types, method.getAnnotations()))
- .orElseGet(() -> findAnnotation(types, method.getDeclaringClass().getAnnotations()));
- }
-
- private static Annotation findAnnotation(List> types, Annotation... annotations) {
- return Arrays.stream(annotations)
- .filter(a -> types.contains(a.annotationType()))
- .findFirst()
- .orElse(null);
- }
-
- protected boolean handleProtectedWithClaimsAnnotation(ProtectedWithClaims a, JwtToken jwtToken) {
- return handleProtectedWithClaims(a.issuer(), a.claimMap(), a.combineWithOr(), jwtToken);
- }
-
- protected boolean handleProtectedWithClaims(String issuer, String[] requiredClaims, boolean combineWithOr, JwtToken jwtToken) {
- if (Objects.nonNull(issuer) && !issuer.isEmpty()) {
- return containsRequiredClaims(jwtToken, combineWithOr, requiredClaims);
- }
- return true;
- }
-
- protected boolean containsRequiredClaims(JwtToken jwtToken, boolean combineWithOr, String... claims) {
- LOG.debug("choose matching logic based on combineWithOr={}",combineWithOr);
- return combineWithOr ? containsAnyClaim(jwtToken, claims)
- : containsAllClaims(jwtToken, claims);
- }
-
- private boolean containsAllClaims(JwtToken jwtToken, String... claims) {
- if (claims != null && claims.length > 0) {
- return Arrays.stream(claims)
- .map(claimUnparsed -> claimUnparsed.split("="))
- .filter(pair -> pair.length == 2)
- .allMatch(pair -> jwtToken.containsClaim(pair[0].trim(), pair[1].trim()));
- }
- return true;
- }
-
- private boolean containsAnyClaim(JwtToken jwtToken, String... claims) {
- if (claims != null && claims.length > 0) {
- return Arrays.stream(claims)
- .map(claimUnparsed -> claimUnparsed.split("="))
- .filter(pair -> pair.length == 2)
- .anyMatch(pair -> jwtToken.containsClaim(pair[0].trim(), pair[1].trim()));
- }
- LOG.debug("no claims listed, so claim checking is ok.");
- return true;
- }
-}
\ No newline at end of file
diff --git a/token-validation-core/src/main/java/no/nav/security/token/support/core/validation/JwtTokenRetriever.java b/token-validation-core/src/main/java/no/nav/security/token/support/core/validation/JwtTokenRetriever.java
deleted file mode 100644
index 1a1e5899..00000000
--- a/token-validation-core/src/main/java/no/nav/security/token/support/core/validation/JwtTokenRetriever.java
+++ /dev/null
@@ -1,79 +0,0 @@
-package no.nav.security.token.support.core.validation;
-
-import no.nav.security.token.support.core.configuration.IssuerConfiguration;
-import no.nav.security.token.support.core.configuration.MultiIssuerConfiguration;
-import no.nav.security.token.support.core.http.HttpRequest;
-import no.nav.security.token.support.core.jwt.JwtToken;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.util.Arrays;
-import java.util.List;
-import java.util.Optional;
-import java.util.stream.Stream;
-public class JwtTokenRetriever {
-
- private JwtTokenRetriever() {
-
- }
-
- private static final Logger LOG = LoggerFactory.getLogger(JwtTokenRetriever.class);
- private static final String BEARER = "Bearer";
-
- static List retrieveUnvalidatedTokens(MultiIssuerConfiguration config, HttpRequest request) {
- return Stream.concat(
- getTokensFromHeader(config, request).stream(),
- getTokensFromCookies(config, request).stream())
- .toList();
- }
-
- private static List getTokensFromHeader(MultiIssuerConfiguration config, HttpRequest request) {
- try {
- LOG.debug("Checking authorization header for tokens using config " + config);
-
- var issuers = config.getIssuers();
- Optional issuer = issuers.values().stream().filter(it -> request.getHeader(it.getHeaderName()) != null).findFirst();
-
- if (issuer.isPresent()) {
- var authorization = request.getHeader(issuer.get().getHeaderName());
- String[] headerValues = authorization.split(",");
- return extractBearerTokens(headerValues)
- .stream()
- .map(JwtToken::new)
- .filter(jwtToken -> config.getIssuer(jwtToken.getIssuer()).isPresent())
- .toList();
- }
- LOG.debug("No tokens found in authorization header");
- } catch (Exception e) {
- LOG.warn("Received exception when attempting to extract and parse token from Authorization header", e);
- }
- return List.of();
- }
-
- private static List getTokensFromCookies(MultiIssuerConfiguration config, HttpRequest request) {
- try {
- List cookies = request.getCookies() != null ? Arrays.asList(request.getCookies()) : List.of();
- return cookies.stream()
- .filter(nameValue -> containsCookieName(config, nameValue.getName()))
- .map(nameValue -> new JwtToken(nameValue.getValue()))
- .toList();
- } catch (Exception e) {
- LOG.warn("received exception when attempting to extract and parse token from cookie", e);
- return List.of();
- }
- }
-
- private static boolean containsCookieName(MultiIssuerConfiguration configuration, String cookieName) {
- return configuration.getIssuers().values().stream()
- .anyMatch(issuerConfiguration -> cookieName.equalsIgnoreCase(issuerConfiguration.getCookieName()));
- }
-
- private static List extractBearerTokens(String... headerValues) {
- return Arrays.stream(headerValues)
- .map(s -> s.split(" "))
- .filter(pair -> pair.length == 2)
- .filter(pair -> pair[0].trim().equalsIgnoreCase(BEARER))
- .map(pair -> pair[1].trim())
- .toList();
- }
-}
\ No newline at end of file
diff --git a/token-validation-core/src/main/java/no/nav/security/token/support/core/validation/JwtTokenValidationHandler.java b/token-validation-core/src/main/java/no/nav/security/token/support/core/validation/JwtTokenValidationHandler.java
deleted file mode 100644
index 2f3a1e7b..00000000
--- a/token-validation-core/src/main/java/no/nav/security/token/support/core/validation/JwtTokenValidationHandler.java
+++ /dev/null
@@ -1,86 +0,0 @@
-package no.nav.security.token.support.core.validation;
-
-import no.nav.security.token.support.core.configuration.IssuerConfiguration;
-import no.nav.security.token.support.core.configuration.MultiIssuerConfiguration;
-import no.nav.security.token.support.core.context.TokenValidationContext;
-import no.nav.security.token.support.core.exceptions.IssuerConfigurationException;
-import no.nav.security.token.support.core.exceptions.JwtTokenValidatorException;
-import no.nav.security.token.support.core.http.HttpRequest;
-import no.nav.security.token.support.core.jwt.JwtToken;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.util.AbstractMap;
-import java.util.Map;
-import java.util.Optional;
-import java.util.stream.Collectors;
-
-public class JwtTokenValidationHandler {
-
- private static final Logger LOG = LoggerFactory.getLogger(JwtTokenValidationHandler.class);
- private final MultiIssuerConfiguration config;
-
- public JwtTokenValidationHandler(MultiIssuerConfiguration config) {
- this.config = config;
- }
-
- public TokenValidationContext getValidatedTokens(HttpRequest request) {
-
- var tokensOnRequest = JwtTokenRetriever.retrieveUnvalidatedTokens(config, request);
-
- Map validatedTokens = tokensOnRequest.stream()
- .map(this::validate)
- .filter(Optional::isPresent)
- .map(Optional::get)
- .collect(Collectors.toConcurrentMap(
- Map.Entry::getKey,
- Map.Entry::getValue));
-
- LOG.debug("Found {} tokens on request, number of validated tokens is {}", tokensOnRequest.size(), validatedTokens.size());
- if (validatedTokens.isEmpty() && !tokensOnRequest.isEmpty()) {
- LOG.debug("Found {} unvalidated token(s) with issuer(s) {} on request, is this a configuration error?", tokensOnRequest.size(),
- tokensOnRequest.stream().map(JwtToken::getIssuer).toList());
- }
- return new TokenValidationContext(validatedTokens);
- }
-
- private Optional> validate(JwtToken jwtToken) {
- try {
- LOG.debug("Check if token with issuer={} is present in config", jwtToken.getIssuer());
- if (config.getIssuer(jwtToken.getIssuer()).isPresent()) {
- var issuerShortName = issuerConfiguration(jwtToken.getIssuer()).getName();
- LOG.debug("Found token from trusted issuer={} with shortName={} in request", jwtToken.getIssuer(), issuerShortName);
-
- long start = System.currentTimeMillis();
- tokenValidator(jwtToken).assertValidToken(jwtToken.getTokenAsString());
- long end = System.currentTimeMillis();
-
- LOG.debug("Validated token from issuer[{}] in {} ms", jwtToken.getIssuer(), (end - start));
- return Optional.of(entry(issuerShortName, jwtToken));
- }
- LOG.debug("Token is from an unknown issuer={}, skipping validation.", jwtToken.getIssuer());
- return Optional.empty();
-
- } catch (JwtTokenValidatorException e) {
- LOG.info(
- "Found invalid token for issuer [{}, expires at {}], message:{} ",
- jwtToken.getIssuer(),
- e.getExpiryDate(),
- e.getMessage());
- return Optional.empty();
- }
- }
-
- private JwtTokenValidator tokenValidator(JwtToken jwtToken) {
- return issuerConfiguration(jwtToken.getIssuer()).getTokenValidator();
- }
-
- private IssuerConfiguration issuerConfiguration(String issuer) {
- return config.getIssuer(issuer)
- .orElseThrow(() -> new IssuerConfigurationException(String.format("Could not find IssuerConfiguration for issuer=%s", issuer)));
- }
-
- private static Map.Entry entry(T key, U value) {
- return new AbstractMap.SimpleImmutableEntry<>(key, value);
- }
-}
diff --git a/token-validation-core/src/main/java/no/nav/security/token/support/core/validation/JwtTokenValidator.java b/token-validation-core/src/main/java/no/nav/security/token/support/core/validation/JwtTokenValidator.java
deleted file mode 100644
index 355a07d1..00000000
--- a/token-validation-core/src/main/java/no/nav/security/token/support/core/validation/JwtTokenValidator.java
+++ /dev/null
@@ -1,8 +0,0 @@
-package no.nav.security.token.support.core.validation;
-
-import no.nav.security.token.support.core.exceptions.JwtTokenValidatorException;
-
-public interface JwtTokenValidator {
-
- void assertValidToken(String tokenString) throws JwtTokenValidatorException;
-}
diff --git a/token-validation-core/src/main/java/no/nav/security/token/support/core/validation/JwtTokenValidatorFactory.java b/token-validation-core/src/main/java/no/nav/security/token/support/core/validation/JwtTokenValidatorFactory.java
deleted file mode 100644
index 3bdae010..00000000
--- a/token-validation-core/src/main/java/no/nav/security/token/support/core/validation/JwtTokenValidatorFactory.java
+++ /dev/null
@@ -1,69 +0,0 @@
-package no.nav.security.token.support.core.validation;
-
-import com.nimbusds.jose.jwk.source.JWKSource;
-import com.nimbusds.jose.jwk.source.JWKSourceBuilder;
-import com.nimbusds.jose.proc.SecurityContext;
-import com.nimbusds.jose.util.ResourceRetriever;
-import com.nimbusds.oauth2.sdk.as.AuthorizationServerMetadata;
-import no.nav.security.token.support.core.configuration.IssuerProperties;
-import no.nav.security.token.support.core.exceptions.MetaDataNotAvailableException;
-
-import java.net.MalformedURLException;
-import java.net.URL;
-
-public class JwtTokenValidatorFactory {
-
- private JwtTokenValidatorFactory() {
-
- }
-
- public static JwtTokenValidator tokenValidator(
- IssuerProperties issuerProperties,
- AuthorizationServerMetadata metadata,
- ResourceRetriever resourceRetriever
- ) {
- return tokenValidator(issuerProperties, metadata, jwkSource(
- issuerProperties,
- getJWKsUrl(metadata),
- resourceRetriever
- ));
- }
-
- public static JwtTokenValidator tokenValidator(
- IssuerProperties issuerProperties,
- AuthorizationServerMetadata metadata,
- JWKSource remoteJWKSet
- ) {
- return new DefaultConfigurableJwtValidator(
- metadata.getIssuer().getValue(),
- issuerProperties.getAcceptedAudience(),
- issuerProperties.getValidation().getOptionalClaims(),
- remoteJWKSet
- );
- }
-
- private static JWKSource jwkSource(
- IssuerProperties issuerProperties,
- URL jwksUrl,
- ResourceRetriever resourceRetriever
- ) {
- var jwkSource = JWKSourceBuilder.create(jwksUrl, resourceRetriever);
-
- if (issuerProperties.getJwksCache().isConfigured()) {
- jwkSource.cache(
- issuerProperties.getJwksCache().getLifespanMillis(),
- issuerProperties.getJwksCache().getRefreshTimeMillis()
- );
- }
-
- return jwkSource.build();
- }
-
- private static URL getJWKsUrl(AuthorizationServerMetadata metaData) {
- try {
- return metaData.getJWKSetURI().toURL();
- } catch (MalformedURLException e) {
- throw new MetaDataNotAvailableException(e);
- }
- }
-}
diff --git a/token-validation-core/src/main/kotlin/no/nav/security/token/support/core/JwtTokenConstants.kt b/token-validation-core/src/main/kotlin/no/nav/security/token/support/core/JwtTokenConstants.kt
new file mode 100644
index 00000000..d278597a
--- /dev/null
+++ b/token-validation-core/src/main/kotlin/no/nav/security/token/support/core/JwtTokenConstants.kt
@@ -0,0 +1,9 @@
+package no.nav.security.token.support.core
+
+object JwtTokenConstants {
+ const val AUTHORIZATION_HEADER = "Authorization"
+ const val EXPIRY_THRESHOLD_ENV_PROPERTY = "no.nav.security.jwt.expirythreshold"
+ const val TOKEN_VALIDATION_FILTER_ORDER_PROPERTY = "no.nav.security.jwt.tokenvalidationfilter.order"
+ const val TOKEN_EXPIRES_SOON_HEADER = "x-token-expires-soon"
+ const val BEARER_TOKEN_DONT_PROPAGATE_ENV_PROPERTY = "no.nav.security.jwt.dont-propagate-bearertoken"
+}
\ No newline at end of file
diff --git a/token-validation-core/src/main/kotlin/no/nav/security/token/support/core/api/Protected.kt b/token-validation-core/src/main/kotlin/no/nav/security/token/support/core/api/Protected.kt
new file mode 100644
index 00000000..867425f5
--- /dev/null
+++ b/token-validation-core/src/main/kotlin/no/nav/security/token/support/core/api/Protected.kt
@@ -0,0 +1,12 @@
+package no.nav.security.token.support.core.api
+
+import kotlin.annotation.AnnotationRetention.RUNTIME
+import kotlin.annotation.AnnotationTarget.CLASS
+import kotlin.annotation.AnnotationTarget.FUNCTION
+import kotlin.annotation.AnnotationTarget.PROPERTY_GETTER
+import kotlin.annotation.AnnotationTarget.PROPERTY_SETTER
+
+@Retention(RUNTIME)
+@MustBeDocumented
+@Target(FUNCTION, PROPERTY_GETTER, PROPERTY_SETTER, CLASS)
+annotation class Protected
\ No newline at end of file
diff --git a/token-validation-core/src/main/kotlin/no/nav/security/token/support/core/api/ProtectedWithClaims.kt b/token-validation-core/src/main/kotlin/no/nav/security/token/support/core/api/ProtectedWithClaims.kt
new file mode 100644
index 00000000..11c1ba92
--- /dev/null
+++ b/token-validation-core/src/main/kotlin/no/nav/security/token/support/core/api/ProtectedWithClaims.kt
@@ -0,0 +1,29 @@
+package no.nav.security.token.support.core.api
+
+import kotlin.annotation.AnnotationRetention.RUNTIME
+import kotlin.annotation.AnnotationTarget.CLASS
+import kotlin.annotation.AnnotationTarget.FUNCTION
+import kotlin.annotation.AnnotationTarget.PROPERTY_GETTER
+import kotlin.annotation.AnnotationTarget.PROPERTY_SETTER
+import no.nav.security.token.support.core.utils.Cluster
+
+@Retention(RUNTIME)
+@Target(CLASS, FUNCTION, PROPERTY_GETTER, PROPERTY_SETTER)
+@Protected
+@MustBeDocumented
+annotation class ProtectedWithClaims(val issuer : String,
+ /**
+ * Required claims in token in key=value format.
+ * If the value is an asterisk (*), it checks that the required key is present.
+ * @return array containing claims as key=value
+ */
+ val claimMap : Array = [], val excludedClusters : Array = [],
+ /**
+ * How to check for the presence of claims,
+ * default is false which will require all claims in the list
+ * to be present in token. If set to true, any claim in the list
+ * will suffice.
+ *
+ * @return boolean
+ */
+ val combineWithOr : Boolean = false)
\ No newline at end of file
diff --git a/token-validation-core/src/main/kotlin/no/nav/security/token/support/core/api/RequiredIssuers.kt b/token-validation-core/src/main/kotlin/no/nav/security/token/support/core/api/RequiredIssuers.kt
new file mode 100644
index 00000000..f7dc8fcb
--- /dev/null
+++ b/token-validation-core/src/main/kotlin/no/nav/security/token/support/core/api/RequiredIssuers.kt
@@ -0,0 +1,7 @@
+package no.nav.security.token.support.core.api
+
+import kotlin.annotation.AnnotationRetention.RUNTIME
+
+@Retention(RUNTIME)
+@MustBeDocumented
+annotation class RequiredIssuers(vararg val value : ProtectedWithClaims)
\ No newline at end of file
diff --git a/token-validation-core/src/main/kotlin/no/nav/security/token/support/core/api/Unprotected.kt b/token-validation-core/src/main/kotlin/no/nav/security/token/support/core/api/Unprotected.kt
new file mode 100644
index 00000000..edfc5e23
--- /dev/null
+++ b/token-validation-core/src/main/kotlin/no/nav/security/token/support/core/api/Unprotected.kt
@@ -0,0 +1,12 @@
+package no.nav.security.token.support.core.api
+
+import kotlin.annotation.AnnotationRetention.RUNTIME
+import kotlin.annotation.AnnotationTarget.CLASS
+import kotlin.annotation.AnnotationTarget.FUNCTION
+import kotlin.annotation.AnnotationTarget.PROPERTY_GETTER
+import kotlin.annotation.AnnotationTarget.PROPERTY_SETTER
+
+@Retention(RUNTIME)
+@MustBeDocumented
+@Target(FUNCTION, PROPERTY_GETTER, PROPERTY_SETTER, CLASS)
+annotation class Unprotected
\ No newline at end of file
diff --git a/token-validation-core/src/main/kotlin/no/nav/security/token/support/core/configuration/IssuerConfiguration.kt b/token-validation-core/src/main/kotlin/no/nav/security/token/support/core/configuration/IssuerConfiguration.kt
new file mode 100644
index 00000000..59412894
--- /dev/null
+++ b/token-validation-core/src/main/kotlin/no/nav/security/token/support/core/configuration/IssuerConfiguration.kt
@@ -0,0 +1,34 @@
+package no.nav.security.token.support.core.configuration
+
+import com.nimbusds.jose.util.ResourceRetriever
+import com.nimbusds.oauth2.sdk.`as`.AuthorizationServerMetadata
+import java.net.URL
+import no.nav.security.token.support.core.exceptions.MetaDataNotAvailableException
+import no.nav.security.token.support.core.validation.JwtTokenValidator
+import no.nav.security.token.support.core.validation.JwtTokenValidatorFactory.tokenValidator
+
+open class IssuerConfiguration(val name : String, properties : IssuerProperties, val resourceRetriever : ResourceRetriever = ProxyAwareResourceRetriever()) {
+
+ val metadata : AuthorizationServerMetadata
+ val acceptedAudience = properties.acceptedAudience
+ val cookieName = properties.cookieName
+ val headerName = properties.headerName
+ val tokenValidator : JwtTokenValidator
+
+ init {
+ metadata = providerMetadata(resourceRetriever, properties.discoveryUrl)
+ tokenValidator = tokenValidator(properties, metadata, resourceRetriever)
+ }
+
+ override fun toString() = ("${javaClass.simpleName} [name=$name, metaData=$metadata, acceptedAudience=$acceptedAudience, cookieName=$cookieName, headerName=$headerName, tokenValidator=$tokenValidator, resourceRetriever=$resourceRetriever]")
+
+ companion object {
+
+ private fun providerMetadata(retriever : ResourceRetriever, url : URL) =
+ runCatching {
+ AuthorizationServerMetadata.parse(retriever.retrieveResource(url).content)
+ }.getOrElse {
+ throw MetaDataNotAvailableException("Make sure you are not using proxying in GCP", url, it)
+ }
+ }
+}
\ No newline at end of file
diff --git a/token-validation-core/src/main/kotlin/no/nav/security/token/support/core/configuration/IssuerProperties.kt b/token-validation-core/src/main/kotlin/no/nav/security/token/support/core/configuration/IssuerProperties.kt
new file mode 100644
index 00000000..ae4d41e0
--- /dev/null
+++ b/token-validation-core/src/main/kotlin/no/nav/security/token/support/core/configuration/IssuerProperties.kt
@@ -0,0 +1,76 @@
+package no.nav.security.token.support.core.configuration
+
+import jakarta.validation.Validation
+import java.net.URL
+import java.util.Objects
+import java.util.concurrent.TimeUnit.MINUTES
+import org.slf4j.Logger
+import org.slf4j.LoggerFactory
+import no.nav.security.token.support.core.JwtTokenConstants.AUTHORIZATION_HEADER
+import no.nav.security.token.support.core.configuration.IssuerProperties.JwksCache.Companion.EMPTY_CACHE
+import no.nav.security.token.support.core.configuration.IssuerProperties.Validation.Companion.EMPTY
+
+class IssuerProperties @JvmOverloads constructor(val discoveryUrl : URL,
+ val acceptedAudience : List = listOf(),
+ val cookieName : String? = null,
+ val headerName : String = AUTHORIZATION_HEADER,
+ val validation : Validation = EMPTY,
+ val jwksCache : JwksCache = EMPTY_CACHE,
+ val proxyUrl: URL? = null,
+ val usePlaintextForHttps: Boolean = false) {
+
+ private val LOG : Logger = LoggerFactory.getLogger(IssuerProperties::class.java)
+
+ init {
+ cookieName?.let { LOG.warn("Cookie-support will be discontinued in future versions, please consider changing your configuration now") }
+ }
+
+ override fun toString() = "IssuerProperties(discoveryUrl=$discoveryUrl, acceptedAudience=$acceptedAudience, cookieName=$cookieName, headerName=$headerName, proxyUrl=$proxyUrl, usePlaintextForHttps=$usePlaintextForHttps, validation=$validation, jwksCache=$jwksCache)"
+
+ class Validation(val optionalClaims : List = emptyList()) {
+
+ val isConfigured = optionalClaims.isNotEmpty()
+
+ override fun equals(other : Any?) : Boolean {
+ if (this === other) return true
+ if (other == null || javaClass != other.javaClass) return false
+ val that = other as Validation
+ return optionalClaims == that.optionalClaims
+ }
+
+ override fun hashCode() = Objects.hash(optionalClaims)
+
+ override fun toString() = "IssuerProperties.Validation(optionalClaims=$optionalClaims)"
+
+ companion object {
+
+ @JvmField
+ val EMPTY : Validation = Validation(emptyList())
+ }
+ }
+
+ class JwksCache(val lifespan : Long?, val refreshTime : Long?) {
+
+ val isConfigured = lifespan != null && refreshTime != null
+
+ val lifespanMillis = MINUTES.toMillis(lifespan!!)
+
+ val refreshTimeMillis = MINUTES.toMillis(refreshTime!!)
+
+ override fun equals(other : Any?) : Boolean {
+ if (this === other) return true
+ if (other == null || javaClass != other.javaClass) return false
+ val jwksCache = other as JwksCache
+ return lifespan == jwksCache.lifespan && refreshTime == jwksCache.refreshTime
+ }
+
+ override fun hashCode() = Objects.hash(lifespan, refreshTime)
+
+ override fun toString() = "${javaClass.simpleName} [lifespan=$lifespan,refreshTime=$refreshTime]"
+
+ companion object {
+
+ @JvmField val EMPTY_CACHE = JwksCache(15, 5)
+ }
+ }
+}
\ No newline at end of file
diff --git a/token-validation-core/src/main/kotlin/no/nav/security/token/support/core/configuration/MultiIssuerConfiguration.kt b/token-validation-core/src/main/kotlin/no/nav/security/token/support/core/configuration/MultiIssuerConfiguration.kt
new file mode 100644
index 00000000..d727d438
--- /dev/null
+++ b/token-validation-core/src/main/kotlin/no/nav/security/token/support/core/configuration/MultiIssuerConfiguration.kt
@@ -0,0 +1,35 @@
+package no.nav.security.token.support.core.configuration
+
+import com.nimbusds.jose.util.ResourceRetriever
+import java.util.Optional
+import kotlin.DeprecationLevel.WARNING
+
+class MultiIssuerConfiguration @JvmOverloads constructor(private val properties : Map, val retriever : ResourceRetriever = ProxyAwareResourceRetriever()) {
+
+ val issuerShortNames = ArrayList()
+
+ val issuers : MutableMap = HashMap()
+
+ init {
+ loadIssuerConfigurations()
+ }
+ @Deprecated(message ="Use of Optional not necessary",ReplaceWith("getIssuers.get()"), WARNING)
+ fun getIssuer(name : String) = Optional.ofNullable(issuers[name])
+
+ private fun loadIssuerConfigurations() =
+ properties.forEach { (shortName, p) ->
+ createIssuerConfiguration(shortName, p).run {
+ issuerShortNames.add(shortName)
+ issuers[shortName] = this
+ issuers["${metadata.issuer}"] = this
+ }
+ }
+
+ private fun createIssuerConfiguration(shortName : String, p : IssuerProperties) =
+ if (p.usePlaintextForHttps || p.proxyUrl != null) {
+ IssuerConfiguration(shortName, p, ProxyAwareResourceRetriever(p.proxyUrl, p.usePlaintextForHttps))
+ }
+ else IssuerConfiguration(shortName, p, retriever)
+
+ override fun toString() = ("${javaClass.simpleName} [issuerShortNames=$issuerShortNames, resourceRetriever=$retriever, issuers=$issuers, issuerPropertiesMap=$properties]")
+}
\ No newline at end of file
diff --git a/token-validation-core/src/main/kotlin/no/nav/security/token/support/core/configuration/ProxyAwareResourceRetriever.kt b/token-validation-core/src/main/kotlin/no/nav/security/token/support/core/configuration/ProxyAwareResourceRetriever.kt
new file mode 100644
index 00000000..fbf4f4a4
--- /dev/null
+++ b/token-validation-core/src/main/kotlin/no/nav/security/token/support/core/configuration/ProxyAwareResourceRetriever.kt
@@ -0,0 +1,76 @@
+package no.nav.security.token.support.core.configuration
+
+import com.nimbusds.jose.util.DefaultResourceRetriever
+import java.io.IOException
+import java.net.HttpURLConnection
+import java.net.InetSocketAddress
+import java.net.Proxy
+import java.net.Proxy.NO_PROXY
+import java.net.Proxy.Type.DIRECT
+import java.net.Proxy.Type.HTTP
+import java.net.URI
+import java.net.URISyntaxException
+import java.net.URL
+import org.slf4j.Logger
+import org.slf4j.LoggerFactory
+
+open class ProxyAwareResourceRetriever(proxyUrl : URL?, private val usePlainTextForHttps : Boolean, connectTimeout : Int, readTimeout : Int, sizeLimit : Int) : DefaultResourceRetriever(connectTimeout, readTimeout, sizeLimit) {
+
+ @JvmOverloads
+ constructor(proxyUrl : URL? = null, usePlainTextForHttps : Boolean = false) : this(proxyUrl, usePlainTextForHttps, DEFAULT_HTTP_CONNECT_TIMEOUT, DEFAULT_HTTP_READ_TIMEOUT, DEFAULT_HTTP_SIZE_LIMIT)
+
+ init {
+ super.setProxy(proxyFrom(proxyUrl))
+ }
+
+
+ fun urlWithPlainTextForHttps(url : URL) : URL {
+ try {
+ if (!url.toURI().scheme.equals("https")) {
+ return url
+ }
+ val port = if (url.port > 0) url.port else 443
+ val newUrl = ("http://" + url.host + ":" + port + url.path
+ + (if (url.query != null && url.query.isNotEmpty()) "?" + url.query else ""))
+ LOG.debug("using plaintext connection for https url, new url is {}", newUrl)
+ return URI.create(newUrl).toURL()
+ }
+ catch (e : URISyntaxException) {
+ throw IOException(e)
+ }
+ }
+
+ override fun openHTTPConnection(url : URL) : HttpURLConnection {
+ val urlToOpen = if (usePlainTextForHttps) urlWithPlainTextForHttps(url) else url
+ if (shouldProxy(url)) {
+ LOG.trace("Connecting to {} via proxy {}", urlToOpen, proxy)
+ return urlToOpen.openConnection(proxy) as HttpURLConnection
+ }
+ LOG.trace("Connecting to {} without proxy", urlToOpen)
+ return urlToOpen.openConnection() as HttpURLConnection
+ }
+
+ fun shouldProxy(url : URL) = proxy.type() != DIRECT && !isNoProxy(url)
+ private fun proxyFrom(uri : URL?) = uri?.let { Proxy(HTTP, InetSocketAddress(it.host, it.port)) } ?: NO_PROXY
+ private fun isNoProxy(url: URL): Boolean {
+ val noProxy = System.getenv("NO_PROXY")
+ val isNoProxy = noProxy?.split(",")
+ ?.any("$url"::contains) ?: false
+
+ if (noProxy != null && isNoProxy) {
+ LOG.trace("Not using proxy for {} since it is covered by the NO_PROXY setting {}", url, noProxy)
+ } else {
+ LOG.trace("Using proxy for {} since it is not covered by the NO_PROXY setting {}", url, noProxy)
+ }
+
+ return isNoProxy
+ }
+
+ companion object {
+ private val LOG : Logger = LoggerFactory.getLogger(ProxyAwareResourceRetriever::class.java)
+ const val DEFAULT_HTTP_CONNECT_TIMEOUT : Int = 21050
+ const val DEFAULT_HTTP_READ_TIMEOUT : Int = 30000
+ const val DEFAULT_HTTP_SIZE_LIMIT : Int = 50 * 1024
+
+ }
+}
\ No newline at end of file
diff --git a/token-validation-core/src/main/kotlin/no/nav/security/token/support/core/context/TokenValidationContext.kt b/token-validation-core/src/main/kotlin/no/nav/security/token/support/core/context/TokenValidationContext.kt
new file mode 100644
index 00000000..b63b823a
--- /dev/null
+++ b/token-validation-core/src/main/kotlin/no/nav/security/token/support/core/context/TokenValidationContext.kt
@@ -0,0 +1,30 @@
+package no.nav.security.token.support.core.context
+
+import java.util.Optional
+import no.nav.security.token.support.core.jwt.JwtToken
+
+class TokenValidationContext(private val validatedTokens : Map) {
+
+ fun getJwtTokenAsOptional(issuerName : String) = jwtToken(issuerName)?.let { Optional.of(it) } ?: Optional.empty()
+
+ val firstValidToken get() = validatedTokens.values.firstOrNull()
+ fun getJwtToken(issuerName : String) = jwtToken(issuerName)
+
+ fun getClaims(issuerName : String) = jwtToken(issuerName)?.jwtTokenClaims ?: throw IllegalArgumentException("No token found for issuer $issuerName")
+
+ val anyValidClaims get() =
+ validatedTokens.values
+ .map(JwtToken::jwtTokenClaims)
+ .firstOrNull()
+
+
+ fun hasValidToken() = validatedTokens.isNotEmpty()
+
+ fun hasTokenFor(issuerName : String) = getJwtToken(issuerName) != null
+ val issuers get() = validatedTokens.keys.toList()
+
+ private fun jwtToken(issuerName: String) = validatedTokens[issuerName]
+
+ override fun toString() = "TokenValidationContext{issuers=${validatedTokens.keys}}"
+
+}
\ No newline at end of file
diff --git a/token-validation-core/src/main/kotlin/no/nav/security/token/support/core/context/TokenValidationContextHolder.kt b/token-validation-core/src/main/kotlin/no/nav/security/token/support/core/context/TokenValidationContextHolder.kt
new file mode 100644
index 00000000..454bb855
--- /dev/null
+++ b/token-validation-core/src/main/kotlin/no/nav/security/token/support/core/context/TokenValidationContextHolder.kt
@@ -0,0 +1,8 @@
+package no.nav.security.token.support.core.context
+
+interface TokenValidationContextHolder {
+
+ fun getTokenValidationContext() : TokenValidationContext
+
+ fun setTokenValidationContext(tokenValidationContext: TokenValidationContext?)
+}
\ No newline at end of file
diff --git a/token-validation-core/src/main/kotlin/no/nav/security/token/support/core/exceptions/AnnotationRequiredException.kt b/token-validation-core/src/main/kotlin/no/nav/security/token/support/core/exceptions/AnnotationRequiredException.kt
new file mode 100644
index 00000000..c443d372
--- /dev/null
+++ b/token-validation-core/src/main/kotlin/no/nav/security/token/support/core/exceptions/AnnotationRequiredException.kt
@@ -0,0 +1,7 @@
+package no.nav.security.token.support.core.exceptions
+
+import java.lang.reflect.Method
+import no.nav.security.token.support.core.validation.JwtTokenAnnotationHandler.Companion.SUPPORTED_ANNOTATIONS
+class AnnotationRequiredException(message : String) : RuntimeException(message) {
+ constructor(method : Method) : this("Server misconfigured - controller/method [${method.declaringClass.name}.${method.name}] not annotated with any of $SUPPORTED_ANNOTATIONS or added to ignore list")
+}
\ No newline at end of file
diff --git a/token-validation-core/src/main/kotlin/no/nav/security/token/support/core/exceptions/IssuerConfigurationException.kt b/token-validation-core/src/main/kotlin/no/nav/security/token/support/core/exceptions/IssuerConfigurationException.kt
new file mode 100644
index 00000000..ff91b5fc
--- /dev/null
+++ b/token-validation-core/src/main/kotlin/no/nav/security/token/support/core/exceptions/IssuerConfigurationException.kt
@@ -0,0 +1,3 @@
+package no.nav.security.token.support.core.exceptions
+
+class IssuerConfigurationException @JvmOverloads constructor(message : String, cause : Throwable? = null) : RuntimeException(message, cause)
\ No newline at end of file
diff --git a/token-validation-core/src/main/kotlin/no/nav/security/token/support/core/exceptions/JwtTokenInvalidClaimException.kt b/token-validation-core/src/main/kotlin/no/nav/security/token/support/core/exceptions/JwtTokenInvalidClaimException.kt
new file mode 100644
index 00000000..4a110cd3
--- /dev/null
+++ b/token-validation-core/src/main/kotlin/no/nav/security/token/support/core/exceptions/JwtTokenInvalidClaimException.kt
@@ -0,0 +1,14 @@
+package no.nav.security.token.support.core.exceptions
+
+import no.nav.security.token.support.core.api.ProtectedWithClaims
+import no.nav.security.token.support.core.api.RequiredIssuers
+
+class JwtTokenInvalidClaimException(message : String) : RuntimeException(message) {
+ constructor(ann : RequiredIssuers) : this("Required claims not present in token for any of ${issuersAndClaims(ann)}")
+
+ constructor(ann : ProtectedWithClaims) : this("Required claims not present in token. ${listOf(*ann.claimMap)}")
+
+ companion object {
+ private fun issuersAndClaims(ann: RequiredIssuers) = ann.value.associate { it.issuer to it.claimMap }
+ }
+}
\ No newline at end of file
diff --git a/token-validation-core/src/main/kotlin/no/nav/security/token/support/core/exceptions/JwtTokenMissingException.kt b/token-validation-core/src/main/kotlin/no/nav/security/token/support/core/exceptions/JwtTokenMissingException.kt
new file mode 100644
index 00000000..47ac0126
--- /dev/null
+++ b/token-validation-core/src/main/kotlin/no/nav/security/token/support/core/exceptions/JwtTokenMissingException.kt
@@ -0,0 +1,7 @@
+package no.nav.security.token.support.core.exceptions
+
+import no.nav.security.token.support.core.api.RequiredIssuers
+
+class JwtTokenMissingException @JvmOverloads constructor(message : String? = "No valid token found in validation context") : RuntimeException(message) {
+ constructor(ann : RequiredIssuers) : this("No valid token found in validation context for any of the issuers ${ann.value.map { it.issuer }}")
+}
\ No newline at end of file
diff --git a/token-validation-core/src/main/kotlin/no/nav/security/token/support/core/exceptions/JwtTokenValidatorException.kt b/token-validation-core/src/main/kotlin/no/nav/security/token/support/core/exceptions/JwtTokenValidatorException.kt
new file mode 100644
index 00000000..ffe01680
--- /dev/null
+++ b/token-validation-core/src/main/kotlin/no/nav/security/token/support/core/exceptions/JwtTokenValidatorException.kt
@@ -0,0 +1,7 @@
+package no.nav.security.token.support.core.exceptions
+
+import java.util.Date
+
+class JwtTokenValidatorException @JvmOverloads constructor(msg : String? = null, val expiryDate : Date? = null, cause : Throwable? = null) : RuntimeException(msg, cause) {
+ constructor(msg : String, cause : Throwable) : this(msg, null,cause)
+}
\ No newline at end of file
diff --git a/token-validation-core/src/main/kotlin/no/nav/security/token/support/core/exceptions/MetaDataNotAvailableException.kt b/token-validation-core/src/main/kotlin/no/nav/security/token/support/core/exceptions/MetaDataNotAvailableException.kt
new file mode 100644
index 00000000..852cfec7
--- /dev/null
+++ b/token-validation-core/src/main/kotlin/no/nav/security/token/support/core/exceptions/MetaDataNotAvailableException.kt
@@ -0,0 +1,5 @@
+package no.nav.security.token.support.core.exceptions
+
+import java.net.URL
+
+class MetaDataNotAvailableException(msg : String, url : URL, e : Throwable) : RuntimeException("Could not retrieve metadata from $url. $msg", e)
\ No newline at end of file
diff --git a/token-validation-core/src/main/kotlin/no/nav/security/token/support/core/http/HttpRequest.kt b/token-validation-core/src/main/kotlin/no/nav/security/token/support/core/http/HttpRequest.kt
new file mode 100644
index 00000000..cdf38a96
--- /dev/null
+++ b/token-validation-core/src/main/kotlin/no/nav/security/token/support/core/http/HttpRequest.kt
@@ -0,0 +1,14 @@
+package no.nav.security.token.support.core.http
+
+/***
+ * Abstraction interface for an HTTP request to avoid dependencies on specific implementations such as HttpServletRequest etc.
+ */
+interface HttpRequest {
+ fun getHeader(headerName: String): String?
+ fun getCookies(): Array?
+
+ interface NameValue {
+ fun getName(): String
+ fun getValue(): String
+ }
+}
\ No newline at end of file
diff --git a/token-validation-core/src/main/kotlin/no/nav/security/token/support/core/jwt/JwtToken.kt b/token-validation-core/src/main/kotlin/no/nav/security/token/support/core/jwt/JwtToken.kt
new file mode 100644
index 00000000..4e782674
--- /dev/null
+++ b/token-validation-core/src/main/kotlin/no/nav/security/token/support/core/jwt/JwtToken.kt
@@ -0,0 +1,27 @@
+package no.nav.security.token.support.core.jwt
+
+import com.nimbusds.jwt.JWT
+import com.nimbusds.jwt.JWTParser
+import com.nimbusds.jwt.SignedJWT
+import kotlin.DeprecationLevel.WARNING
+
+open class JwtToken(val encodedToken : String, protected val jwt : JWT, val jwtTokenClaims : JwtTokenClaims) {
+ constructor(encodedToken : String) : this(encodedToken, JWTParser.parse(encodedToken), JwtTokenClaims(JWTParser.parse(encodedToken).jwtClaimsSet))
+
+ val jwtClaimsSet = jwt.jwtClaimsSet
+
+ val subject = jwtTokenClaims.subject
+
+ val issuer = jwtTokenClaims.issuer
+
+ @Deprecated("Use getEncodedToken instead", ReplaceWith("getEncodedToken()"), WARNING)
+ val tokenAsString = encodedToken
+
+ fun asBearer() = "Bearer $encodedToken"
+
+ fun containsClaim(name : String, value : String) = jwtTokenClaims.containsClaim(name, value)
+
+ companion object {
+ fun SignedJWT.asBearer() = "Bearer ${serialize()}"
+ }
+}
\ No newline at end of file
diff --git a/token-validation-core/src/main/kotlin/no/nav/security/token/support/core/jwt/JwtTokenClaims.kt b/token-validation-core/src/main/kotlin/no/nav/security/token/support/core/jwt/JwtTokenClaims.kt
new file mode 100644
index 00000000..b325ff0b
--- /dev/null
+++ b/token-validation-core/src/main/kotlin/no/nav/security/token/support/core/jwt/JwtTokenClaims.kt
@@ -0,0 +1,23 @@
+package no.nav.security.token.support.core.jwt
+
+import com.nimbusds.jwt.JWTClaimsSet
+
+class JwtTokenClaims(private val claimSet : JWTClaimsSet) {
+
+ val issuer = claimSet.issuer
+ val expirationTime = claimSet.expirationTime
+ val subject = claimSet.subject
+ val allClaims = claimSet.claims
+
+
+ fun get(name : String) = claimSet.getClaim(name)
+ fun getStringClaim(name : String) = runCatching { claimSet.getStringClaim(name) }.getOrElse { throw RuntimeException(it) }
+ fun getAsList(name : String) = runCatching { claimSet.getStringListClaim(name) }.getOrElse { throw RuntimeException(it) }
+
+ fun containsClaim(name: String?, value: String) =
+ when (val claim = claimSet.getClaim(name)) {
+ is String -> value == "*" || claim == value
+ is Collection<*> -> value == "*" || value in claim
+ else -> false
+ }
+}
\ No newline at end of file
diff --git a/token-validation-core/src/main/kotlin/no/nav/security/token/support/core/utils/Cluster.kt b/token-validation-core/src/main/kotlin/no/nav/security/token/support/core/utils/Cluster.kt
new file mode 100644
index 00000000..5f5cf83a
--- /dev/null
+++ b/token-validation-core/src/main/kotlin/no/nav/security/token/support/core/utils/Cluster.kt
@@ -0,0 +1,24 @@
+package no.nav.security.token.support.core.utils
+
+import no.nav.security.token.support.core.utils.EnvUtil.NAIS_CLUSTER_NAME
+
+enum class Cluster(private val navn : String) {
+ TEST(EnvUtil.TEST),
+ LOCAL(EnvUtil.LOCAL),
+ DEV_SBS(EnvUtil.DEV_SBS),
+ DEV_FSS(EnvUtil.DEV_FSS),
+ DEV_GCP(EnvUtil.DEV_GCP),
+ PROD_GCP(EnvUtil.PROD_GCP),
+ PROD_FSS(EnvUtil.PROD_FSS),
+ PROD_SBS(EnvUtil.PROD_SBS);
+
+ companion object {
+
+ @JvmStatic
+ fun currentCluster() = entries.firstOrNull { it.navn == cluster() } ?: LOCAL
+
+ @JvmStatic
+ val isProd = cluster() in listOf(EnvUtil.PROD_GCP, EnvUtil.PROD_FSS)
+ private fun cluster() = System.getenv(NAIS_CLUSTER_NAME)
+ }
+}
\ No newline at end of file
diff --git a/token-validation-core/src/main/kotlin/no/nav/security/token/support/core/utils/EnvUtil.kt b/token-validation-core/src/main/kotlin/no/nav/security/token/support/core/utils/EnvUtil.kt
new file mode 100644
index 00000000..7eeebd0b
--- /dev/null
+++ b/token-validation-core/src/main/kotlin/no/nav/security/token/support/core/utils/EnvUtil.kt
@@ -0,0 +1,31 @@
+package no.nav.security.token.support.core.utils
+
+object EnvUtil {
+
+ const val FSS = "fss"
+ const val SBS = "sbs"
+ const val LOCAL = "local"
+ const val GCP = "gcp"
+ const val TEST = "test"
+ const val DEV = "dev"
+ const val PROD = "prod"
+
+ @JvmField
+ val DEV_GCP = "$DEV-$GCP"
+
+ @JvmField
+ val PROD_GCP = "$PROD-$GCP"
+
+ @JvmField
+ val PROD_SBS = "$PROD-$SBS"
+
+ @JvmField
+ val DEV_SBS = "$DEV-$SBS"
+
+ @JvmField
+ val PROD_FSS = "$PROD-$FSS"
+
+ @JvmField
+ val DEV_FSS = "$DEV-$FSS"
+ const val NAIS_CLUSTER_NAME = "NAIS_CLUSTER_NAME"
+}
\ No newline at end of file
diff --git a/token-validation-core/src/main/kotlin/no/nav/security/token/support/core/utils/JwtTokenUtil.kt b/token-validation-core/src/main/kotlin/no/nav/security/token/support/core/utils/JwtTokenUtil.kt
new file mode 100644
index 00000000..8f46fadb
--- /dev/null
+++ b/token-validation-core/src/main/kotlin/no/nav/security/token/support/core/utils/JwtTokenUtil.kt
@@ -0,0 +1,13 @@
+package no.nav.security.token.support.core.utils
+
+import no.nav.security.token.support.core.context.TokenValidationContextHolder
+
+object JwtTokenUtil {
+
+ @JvmStatic
+ fun contextHasValidToken(holder : TokenValidationContextHolder?) = context(holder).hasValidToken()
+ @JvmStatic
+ fun getJwtToken(issuer : String, holder : TokenValidationContextHolder?) = context(holder).getJwtTokenAsOptional(issuer)
+
+ private fun context(holder : TokenValidationContextHolder?) = holder?.getTokenValidationContext() ?: throw IllegalStateException("TokenValidationContextHolder is null")
+}
\ No newline at end of file
diff --git a/token-validation-core/src/main/kotlin/no/nav/security/token/support/core/validation/DefaultConfigurableJwtValidator.kt b/token-validation-core/src/main/kotlin/no/nav/security/token/support/core/validation/DefaultConfigurableJwtValidator.kt
new file mode 100644
index 00000000..8cdd377d
--- /dev/null
+++ b/token-validation-core/src/main/kotlin/no/nav/security/token/support/core/validation/DefaultConfigurableJwtValidator.kt
@@ -0,0 +1,87 @@
+package no.nav.security.token.support.core.validation
+
+import com.nimbusds.jose.JWSAlgorithm.RS256
+import com.nimbusds.jose.jwk.source.JWKSource
+import com.nimbusds.jose.proc.JWSVerificationKeySelector
+import com.nimbusds.jose.proc.SecurityContext
+import com.nimbusds.jwt.JWTClaimNames.AUDIENCE
+import com.nimbusds.jwt.JWTClaimNames.EXPIRATION_TIME
+import com.nimbusds.jwt.JWTClaimNames.ISSUED_AT
+import com.nimbusds.jwt.JWTClaimNames.ISSUER
+import com.nimbusds.jwt.JWTClaimNames.SUBJECT
+import com.nimbusds.jwt.JWTClaimsSet.Builder
+import com.nimbusds.jwt.proc.DefaultJWTProcessor
+import no.nav.security.token.support.core.exceptions.JwtTokenValidatorException
+
+/**
+ * The default configurable JwtTokenValidator.
+ * Configures sane defaults and delegates verification to [DefaultJwtClaimsVerifier]:
+ *
+ *
+ * The following set of claims are required by default and *must*be present in the JWTs:
+ *
+ * * iss - Issuer
+ * * sub - Subject
+ * * aud - Audience
+ * * exp - Expiration Time
+ * * iat - Issued At
+ *
+ *
+ *
+ * Otherwise, the following checks are in place:
+ *
+ * * The issuer ("iss") claim value must match exactly with the specified accepted issuer value.
+ * * *At least one* of the values in audience ("aud") claim must match one of the specified accepted audiences.
+ * * Time validity checks are performed on the issued at ("iat"), expiration ("exp") and not-before ("nbf") claims if and only if they are present.
+ *
+ *
+ *
+ * Note: the not-before ("nbf") claim is *not* a required claim. Conversely, the expiration ("exp") claim *is* a default required claim.
+ *
+ *
+ * Specifying optional claims will *remove* any matching claims from the default set of required claims.
+ *
+ *
+ * Audience validation is only skipped if the claim is explicitly configured as an optional claim, and the list of accepted audiences is empty / not configured.
+ *
+ *
+ * If the audience claim is explicitly configured as an optional claim and the list of accepted audience is non-empty, the following rules apply:
+ *
+ * * If the audience claim is present (non-empty) in the JWT, it will be matched against the list of accepted audiences.
+ * * If the audience claim is not present, the audience match and existence checks are skipped - since it is an optional claim.
+ *
+ *
+ *
+ * An *empty* list of accepted audiences alone does *not* remove the audience ("aud") claim from the default set of required claims; the claim must explicitly be specified as optional.
+ */
+class DefaultConfigurableJwtValidator(issuer : String, acceptedAudiences : List, optionalClaims : List, val jwkSource : JWKSource) : JwtTokenValidator {
+
+ private val jwtProcessor = DefaultJWTProcessor().apply {
+ jwsKeySelector = JWSVerificationKeySelector(RS256, jwkSource)
+ setJWTClaimsSetVerifier(DefaultJwtClaimsVerifier(acceptedAudiences(acceptedAudiences, optionalClaims),
+ Builder().issuer(issuer).build(), difference(DEFAULT_REQUIRED_CLAIMS, optionalClaims),
+ PROHIBITED_CLAIMS))
+ }
+
+ override fun assertValidToken(tokenString : String) {
+ runCatching {
+ jwtProcessor.process(tokenString, null)
+ }.getOrElse {
+ throw JwtTokenValidatorException("Token validation failed: ${it.message}", cause = it)
+ }
+ }
+
+ companion object {
+
+ private val DEFAULT_REQUIRED_CLAIMS = listOf(AUDIENCE, EXPIRATION_TIME, ISSUED_AT, ISSUER, SUBJECT)
+ private val PROHIBITED_CLAIMS = emptySet()
+ private fun acceptedAudiences(acceptedAudiences: List, optionalClaims: List) =
+ when {
+ AUDIENCE !in optionalClaims -> acceptedAudiences.toSet()
+ acceptedAudiences.isEmpty() -> null
+ else -> acceptedAudiences.plus(null as String?).toSet()
+ }
+
+ private fun difference(first: List, second: List) = first.asSequence().filterNot { it in second }.toSet()
+ }
+}
\ No newline at end of file
diff --git a/token-validation-core/src/main/kotlin/no/nav/security/token/support/core/validation/DefaultJwtClaimsVerifier.kt b/token-validation-core/src/main/kotlin/no/nav/security/token/support/core/validation/DefaultJwtClaimsVerifier.kt
new file mode 100644
index 00000000..77cdfb9d
--- /dev/null
+++ b/token-validation-core/src/main/kotlin/no/nav/security/token/support/core/validation/DefaultJwtClaimsVerifier.kt
@@ -0,0 +1,24 @@
+package no.nav.security.token.support.core.validation
+
+import com.nimbusds.jose.proc.SecurityContext
+import com.nimbusds.jwt.JWTClaimsSet
+import com.nimbusds.jwt.proc.DefaultJWTClaimsVerifier
+import com.nimbusds.jwt.util.DateUtils.isBefore
+import com.nimbusds.openid.connect.sdk.validators.BadJWTExceptions.IAT_CLAIM_AHEAD_EXCEPTION
+import java.util.Date
+
+/**
+ * Extends [com.nimbusds.jwt.proc.DefaultJWTClaimsVerifier] with a time check for the issued at ("iat") claim.
+ * The claim is only checked if it exists in the given claim set.
+ */
+class DefaultJwtClaimsVerifier(acceptedAudience : Set