From da714388e20b674d97fb072b35e919b0d26c3a1f Mon Sep 17 00:00:00 2001 From: Jan-Olav Eide Date: Fri, 1 Dec 2023 13:39:07 +0100 Subject: [PATCH 1/5] kotlinify client-core --- token-client-core/pom.xml | 18 +- .../core/ClientAuthenticationProperties.java | 161 ---------- .../support/client/core/ClientProperties.java | 295 ------------------ .../client/core/OAuth2CacheFactory.java | 47 --- .../client/core/OAuth2ClientException.java | 12 - .../support/client/core/OAuth2GrantType.java | 14 - .../client/core/OAuth2ParameterNames.java | 20 -- .../client/core/auth/ClientAssertion.java | 73 ----- .../core/context/JwtBearerTokenResolver.java | 7 - .../client/core/http/OAuth2HttpClient.java | 7 - .../client/core/http/OAuth2HttpHeaders.java | 63 ---- .../client/core/http/OAuth2HttpRequest.java | 108 ------- .../support/client/core/jwk/JwkFactory.java | 100 ------ .../oauth2/AbstractOAuth2GrantRequest.java | 44 --- .../oauth2/AbstractOAuth2TokenClient.java | 109 ------- .../oauth2/ClientCredentialsGrantRequest.java | 11 - .../oauth2/ClientCredentialsTokenClient.java | 17 - .../oauth2/OAuth2AccessTokenResponse.java | 99 ------ .../core/oauth2/OAuth2AccessTokenService.java | 149 --------- .../core/oauth2/OnBehalfOfGrantRequest.java | 34 -- .../core/oauth2/OnBehalfOfTokenClient.java | 27 -- .../core/oauth2/TokenExchangeClient.java | 28 -- .../oauth2/TokenExchangeGrantRequest.java | 35 --- .../core/ClientAuthenticationProperties.kt | 47 +++ .../support/client/core/ClientProperties.kt | 74 +++++ .../support/client/core/OAuth2CacheFactory.kt | 27 ++ .../client/core/OAuth2ClientException.kt | 3 + .../support/client/core/OAuth2GrantType.kt | 14 + .../client/core/OAuth2ParameterNames.kt | 17 + .../client/core/auth/ClientAssertion.kt | 52 +++ .../core/context/JwtBearerTokenResolver.kt | 7 + .../client/core/http/OAuth2HttpClient.kt | 8 + .../client/core/http/OAuth2HttpHeaders.kt | 38 +++ .../client/core/http/OAuth2HttpRequest.kt | 31 ++ .../support/client/core/jwk/JwkFactory.kt | 77 +++++ .../core/oauth2/AbstractOAuth2GrantRequest.kt | 18 ++ .../core/oauth2/AbstractOAuth2TokenClient.kt | 99 ++++++ .../oauth2/ClientCredentialsGrantRequest.kt | 6 + .../oauth2/ClientCredentialsTokenClient.kt | 8 + .../core/oauth2/OAuth2AccessTokenResponse.kt | 7 + .../core/oauth2/OAuth2AccessTokenService.kt | 72 +++++ .../core/oauth2/OnBehalfOfGrantRequest.kt | 19 ++ .../core/oauth2/OnBehalfOfTokenClient.kt | 18 ++ .../client/core/oauth2/TokenExchangeClient.kt | 20 ++ .../core/oauth2/TokenExchangeGrantRequest.kt | 19 ++ .../ClientAuthenticationPropertiesTest.java | 51 --- .../client/core/ClientPropertiesTest.java | 100 ------ .../token/support/client/core/TestUtils.java | 110 ------- .../client/core/auth/ClientAssertionTest.java | 67 ---- .../core/http/OAuth2HttpHeadersTest.java | 24 -- .../core/http/SimpleOAuth2HttpClient.java | 66 ---- .../client/core/jwk/JwkFactoryTest.java | 61 ---- .../ClientCredentialsTokenClientTest.java | 178 ----------- .../oauth2/OAuth2AccessTokenServiceTest.java | 280 ----------------- .../oauth2/OnBehalfOfTokenClientTest.java | 89 ------ .../core/oauth2/TokenExchangeClientTest.java | 112 ------- .../ClientAuthenticationPropertiesTest.kt | 37 +++ .../client/core/ClientPropertiesTest.kt | 99 ++++++ .../token/support/client/core/TestUtils.kt | 110 +++++++ .../client/core/auth/ClientAssertionTest.kt | 54 ++++ .../client/core/http/OAuth2HttpHeadersTest.kt | 23 ++ .../core/http/SimpleOAuth2HttpClient.kt | 74 +++++ .../support/client/core/jwk/JwkFactoryTest.kt | 71 +++++ .../ClientCredentialsTokenClientTest.kt | 185 +++++++++++ .../oauth2/OAuth2AccessTokenServiceTest.kt | 227 ++++++++++++++ .../core/oauth2/OnBehalfOfTokenClientTest.kt | 87 ++++++ .../core/oauth2/TokenExchangeClientTest.kt | 123 ++++++++ .../token/support/ktor/oauth/OAuth2Cache.kt | 22 +- .../token/support/ktor/oauth/OAuth2Client.kt | 4 +- .../spring/oauth2/DefaultOAuth2HttpClient.kt | 4 +- .../oauth2/OAuth2ClientRequestInterceptor.kt | 8 +- .../ClientConfigurationPropertiesTest.kt | 6 +- ...OAuth2AccessTokenServiceIntegrationTest.kt | 74 ++--- .../OAuth2ClientConfigurationWithCacheTest.kt | 8 +- ...uth2ClientConfigurationWithoutCacheTest.kt | 4 +- .../src/test/resources/application-test.yml | 1 + 76 files changed, 1849 insertions(+), 2669 deletions(-) delete mode 100644 token-client-core/src/main/java/no/nav/security/token/support/client/core/ClientAuthenticationProperties.java delete mode 100644 token-client-core/src/main/java/no/nav/security/token/support/client/core/ClientProperties.java delete mode 100644 token-client-core/src/main/java/no/nav/security/token/support/client/core/OAuth2CacheFactory.java delete mode 100644 token-client-core/src/main/java/no/nav/security/token/support/client/core/OAuth2ClientException.java delete mode 100644 token-client-core/src/main/java/no/nav/security/token/support/client/core/OAuth2GrantType.java delete mode 100644 token-client-core/src/main/java/no/nav/security/token/support/client/core/OAuth2ParameterNames.java delete mode 100644 token-client-core/src/main/java/no/nav/security/token/support/client/core/auth/ClientAssertion.java delete mode 100644 token-client-core/src/main/java/no/nav/security/token/support/client/core/context/JwtBearerTokenResolver.java delete mode 100644 token-client-core/src/main/java/no/nav/security/token/support/client/core/http/OAuth2HttpClient.java delete mode 100644 token-client-core/src/main/java/no/nav/security/token/support/client/core/http/OAuth2HttpHeaders.java delete mode 100644 token-client-core/src/main/java/no/nav/security/token/support/client/core/http/OAuth2HttpRequest.java delete mode 100644 token-client-core/src/main/java/no/nav/security/token/support/client/core/jwk/JwkFactory.java delete mode 100644 token-client-core/src/main/java/no/nav/security/token/support/client/core/oauth2/AbstractOAuth2GrantRequest.java delete mode 100644 token-client-core/src/main/java/no/nav/security/token/support/client/core/oauth2/AbstractOAuth2TokenClient.java delete mode 100644 token-client-core/src/main/java/no/nav/security/token/support/client/core/oauth2/ClientCredentialsGrantRequest.java delete mode 100644 token-client-core/src/main/java/no/nav/security/token/support/client/core/oauth2/ClientCredentialsTokenClient.java delete mode 100644 token-client-core/src/main/java/no/nav/security/token/support/client/core/oauth2/OAuth2AccessTokenResponse.java delete mode 100644 token-client-core/src/main/java/no/nav/security/token/support/client/core/oauth2/OAuth2AccessTokenService.java delete mode 100644 token-client-core/src/main/java/no/nav/security/token/support/client/core/oauth2/OnBehalfOfGrantRequest.java delete mode 100644 token-client-core/src/main/java/no/nav/security/token/support/client/core/oauth2/OnBehalfOfTokenClient.java delete mode 100644 token-client-core/src/main/java/no/nav/security/token/support/client/core/oauth2/TokenExchangeClient.java delete mode 100644 token-client-core/src/main/java/no/nav/security/token/support/client/core/oauth2/TokenExchangeGrantRequest.java create mode 100644 token-client-core/src/main/kotlin/no/nav/security/token/support/client/core/ClientAuthenticationProperties.kt create mode 100644 token-client-core/src/main/kotlin/no/nav/security/token/support/client/core/ClientProperties.kt create mode 100644 token-client-core/src/main/kotlin/no/nav/security/token/support/client/core/OAuth2CacheFactory.kt create mode 100644 token-client-core/src/main/kotlin/no/nav/security/token/support/client/core/OAuth2ClientException.kt create mode 100644 token-client-core/src/main/kotlin/no/nav/security/token/support/client/core/OAuth2GrantType.kt create mode 100644 token-client-core/src/main/kotlin/no/nav/security/token/support/client/core/OAuth2ParameterNames.kt create mode 100644 token-client-core/src/main/kotlin/no/nav/security/token/support/client/core/auth/ClientAssertion.kt create mode 100644 token-client-core/src/main/kotlin/no/nav/security/token/support/client/core/context/JwtBearerTokenResolver.kt create mode 100644 token-client-core/src/main/kotlin/no/nav/security/token/support/client/core/http/OAuth2HttpClient.kt create mode 100644 token-client-core/src/main/kotlin/no/nav/security/token/support/client/core/http/OAuth2HttpHeaders.kt create mode 100644 token-client-core/src/main/kotlin/no/nav/security/token/support/client/core/http/OAuth2HttpRequest.kt create mode 100644 token-client-core/src/main/kotlin/no/nav/security/token/support/client/core/jwk/JwkFactory.kt create mode 100644 token-client-core/src/main/kotlin/no/nav/security/token/support/client/core/oauth2/AbstractOAuth2GrantRequest.kt create mode 100644 token-client-core/src/main/kotlin/no/nav/security/token/support/client/core/oauth2/AbstractOAuth2TokenClient.kt create mode 100644 token-client-core/src/main/kotlin/no/nav/security/token/support/client/core/oauth2/ClientCredentialsGrantRequest.kt create mode 100644 token-client-core/src/main/kotlin/no/nav/security/token/support/client/core/oauth2/ClientCredentialsTokenClient.kt create mode 100644 token-client-core/src/main/kotlin/no/nav/security/token/support/client/core/oauth2/OAuth2AccessTokenResponse.kt create mode 100644 token-client-core/src/main/kotlin/no/nav/security/token/support/client/core/oauth2/OAuth2AccessTokenService.kt create mode 100644 token-client-core/src/main/kotlin/no/nav/security/token/support/client/core/oauth2/OnBehalfOfGrantRequest.kt create mode 100644 token-client-core/src/main/kotlin/no/nav/security/token/support/client/core/oauth2/OnBehalfOfTokenClient.kt create mode 100644 token-client-core/src/main/kotlin/no/nav/security/token/support/client/core/oauth2/TokenExchangeClient.kt create mode 100644 token-client-core/src/main/kotlin/no/nav/security/token/support/client/core/oauth2/TokenExchangeGrantRequest.kt delete mode 100644 token-client-core/src/test/java/no/nav/security/token/support/client/core/ClientAuthenticationPropertiesTest.java delete mode 100644 token-client-core/src/test/java/no/nav/security/token/support/client/core/ClientPropertiesTest.java delete mode 100644 token-client-core/src/test/java/no/nav/security/token/support/client/core/TestUtils.java delete mode 100644 token-client-core/src/test/java/no/nav/security/token/support/client/core/auth/ClientAssertionTest.java delete mode 100644 token-client-core/src/test/java/no/nav/security/token/support/client/core/http/OAuth2HttpHeadersTest.java delete mode 100644 token-client-core/src/test/java/no/nav/security/token/support/client/core/http/SimpleOAuth2HttpClient.java delete mode 100644 token-client-core/src/test/java/no/nav/security/token/support/client/core/jwk/JwkFactoryTest.java delete mode 100644 token-client-core/src/test/java/no/nav/security/token/support/client/core/oauth2/ClientCredentialsTokenClientTest.java delete mode 100644 token-client-core/src/test/java/no/nav/security/token/support/client/core/oauth2/OAuth2AccessTokenServiceTest.java delete mode 100644 token-client-core/src/test/java/no/nav/security/token/support/client/core/oauth2/OnBehalfOfTokenClientTest.java delete mode 100644 token-client-core/src/test/java/no/nav/security/token/support/client/core/oauth2/TokenExchangeClientTest.java create mode 100644 token-client-core/src/test/kotlin/no/nav/security/token/support/client/core/ClientAuthenticationPropertiesTest.kt create mode 100644 token-client-core/src/test/kotlin/no/nav/security/token/support/client/core/ClientPropertiesTest.kt create mode 100644 token-client-core/src/test/kotlin/no/nav/security/token/support/client/core/TestUtils.kt create mode 100644 token-client-core/src/test/kotlin/no/nav/security/token/support/client/core/auth/ClientAssertionTest.kt create mode 100644 token-client-core/src/test/kotlin/no/nav/security/token/support/client/core/http/OAuth2HttpHeadersTest.kt create mode 100644 token-client-core/src/test/kotlin/no/nav/security/token/support/client/core/http/SimpleOAuth2HttpClient.kt create mode 100644 token-client-core/src/test/kotlin/no/nav/security/token/support/client/core/jwk/JwkFactoryTest.kt create mode 100644 token-client-core/src/test/kotlin/no/nav/security/token/support/client/core/oauth2/ClientCredentialsTokenClientTest.kt create mode 100644 token-client-core/src/test/kotlin/no/nav/security/token/support/client/core/oauth2/OAuth2AccessTokenServiceTest.kt create mode 100644 token-client-core/src/test/kotlin/no/nav/security/token/support/client/core/oauth2/OnBehalfOfTokenClientTest.kt create mode 100644 token-client-core/src/test/kotlin/no/nav/security/token/support/client/core/oauth2/TokenExchangeClientTest.kt diff --git a/token-client-core/pom.xml b/token-client-core/pom.xml index 0cf7e75f..7e09fec5 100644 --- a/token-client-core/pom.xml +++ b/token-client-core/pom.xml @@ -40,12 +40,25 @@ logback-classic test + + com.fasterxml.jackson.module + jackson-module-kotlin + 2.15.2 + test + + + org.jetbrains.kotlin + kotlin-stdlib + ${kotlin.version} + + ${project.basedir}/src/main/kotlin + ${project.basedir}/src/test/kotlin - org.apache.maven.plugins - maven-compiler-plugin + kotlin-maven-plugin + org.jetbrains.kotlin org.apache.maven.plugins @@ -55,6 +68,7 @@ org.apache.maven.plugins maven-javadoc-plugin + diff --git a/token-client-core/src/main/java/no/nav/security/token/support/client/core/ClientAuthenticationProperties.java b/token-client-core/src/main/java/no/nav/security/token/support/client/core/ClientAuthenticationProperties.java deleted file mode 100644 index 128bc4ec..00000000 --- a/token-client-core/src/main/java/no/nav/security/token/support/client/core/ClientAuthenticationProperties.java +++ /dev/null @@ -1,161 +0,0 @@ -package no.nav.security.token.support.client.core; - -import com.nimbusds.jose.jwk.RSAKey; -import com.nimbusds.oauth2.sdk.auth.ClientAuthenticationMethod; -import jakarta.validation.constraints.NotNull; -import no.nav.security.token.support.client.core.jwk.JwkFactory; - -import java.util.List; -import java.util.Objects; -import java.util.Optional; - -import static com.nimbusds.oauth2.sdk.auth.ClientAuthenticationMethod.*; - -public class ClientAuthenticationProperties { - - private static final List CLIENT_AUTH_METHODS = List.of( - CLIENT_SECRET_BASIC, - CLIENT_SECRET_POST, - PRIVATE_KEY_JWT - ); - - @NotNull - private final String clientId; - private final ClientAuthenticationMethod clientAuthMethod; - private final String clientSecret; - private final String clientJwk; - private final RSAKey clientRsaKey; - - public ClientAuthenticationProperties(@NotNull String clientId, - ClientAuthenticationMethod clientAuthMethod, - String clientSecret, - String clientJwk) { - this.clientId = clientId; - this.clientAuthMethod = getSupported(clientAuthMethod); - this.clientSecret = clientSecret; - this.clientJwk = clientJwk; - this.clientRsaKey = loadKey(clientJwk); - validateAfterPropertiesSet(); - } - - private static RSAKey loadKey(String clientPrivateKey) { - if (clientPrivateKey != null) { - if (clientPrivateKey.startsWith("{")) { - return JwkFactory.fromJson(clientPrivateKey); - } else { - return JwkFactory.fromJsonFile(clientPrivateKey); - } - } - return null; - } - - private static ClientAuthenticationMethod getSupported(ClientAuthenticationMethod clientAuthMethod) { - return clientAuthMethod == null ? - CLIENT_SECRET_BASIC : - Optional.of(clientAuthMethod) - .filter(CLIENT_AUTH_METHODS::contains) - .orElseThrow(() -> new IllegalArgumentException( - String.format("unsupported %s with value %s, must be one of %s", - ClientAuthenticationMethod.class.getSimpleName(), clientAuthMethod, CLIENT_AUTH_METHODS))); - } - - public static ClientAuthenticationPropertiesBuilder builder() { - return new ClientAuthenticationPropertiesBuilder(); - } - - private void validateAfterPropertiesSet() { - Objects.requireNonNull(clientId, "clientId cannot be null"); - if (CLIENT_SECRET_BASIC.equals(this.clientAuthMethod)) { - Objects.requireNonNull(clientSecret, "clientSecret cannot be null"); - } else if (CLIENT_SECRET_POST.equals(this.clientAuthMethod)) { - Objects.requireNonNull(clientSecret, "clientSecret cannot be null"); - } else if (PRIVATE_KEY_JWT.equals(this.clientAuthMethod)) { - Objects.requireNonNull(clientJwk, "clientPrivateKey must be set"); - } - } - - public @NotNull String getClientId() { - return this.clientId; - } - - public ClientAuthenticationMethod getClientAuthMethod() { - return this.clientAuthMethod; - } - - public String getClientSecret() { - return this.clientSecret; - } - - public String getClientJwk() { - return this.clientJwk; - } - - public RSAKey getClientRsaKey() { - return this.clientRsaKey; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - var that = (ClientAuthenticationProperties) o; - return Objects.equals(clientId, that.clientId) - && Objects.equals(clientAuthMethod, that.clientAuthMethod) - && Objects.equals(clientSecret, that.clientSecret) - && Objects.equals(clientJwk, that.clientJwk) - && Objects.equals(clientRsaKey, that.clientRsaKey); - } - - @Override - public int hashCode() { - return Objects.hash(clientId, clientAuthMethod, clientSecret, clientJwk, clientRsaKey); - } - - @Override - public String toString() { - return "ClientAuthenticationProperties(clientId=" + this.getClientId() + ", clientAuthMethod=" + this.getClientAuthMethod() + ", clientSecret=" + this.getClientSecret() + ", clientJwk=" + this.getClientJwk() + ", clientRsaKey=" + this.getClientRsaKey() + ")"; - } - - public ClientAuthenticationPropertiesBuilder toBuilder() { - return new ClientAuthenticationPropertiesBuilder().clientId(this.clientId).clientAuthMethod(this.clientAuthMethod).clientSecret(this.clientSecret).clientJwk(this.clientJwk); - } - - public static class ClientAuthenticationPropertiesBuilder { - private @NotNull String clientId; - private ClientAuthenticationMethod clientAuthMethod; - private String clientSecret; - private String clientJwk; - - ClientAuthenticationPropertiesBuilder() { - } - - public ClientAuthenticationPropertiesBuilder clientId(@NotNull String clientId) { - this.clientId = clientId; - return this; - } - - public ClientAuthenticationPropertiesBuilder clientAuthMethod(ClientAuthenticationMethod clientAuthMethod) { - this.clientAuthMethod = clientAuthMethod; - return this; - } - - public ClientAuthenticationPropertiesBuilder clientSecret(String clientSecret) { - this.clientSecret = clientSecret; - return this; - } - - public ClientAuthenticationPropertiesBuilder clientJwk(String clientJwk) { - this.clientJwk = clientJwk; - return this; - } - - public ClientAuthenticationProperties build() { - return new ClientAuthenticationProperties(clientId, clientAuthMethod, clientSecret, clientJwk); - } - - @Override - public String toString() { - return "ClientAuthenticationProperties.ClientAuthenticationPropertiesBuilder(clientId=" + this.clientId + ", clientAuthMethod=" + this.clientAuthMethod + ", clientSecret=" + this.clientSecret + ", clientJwk=" + this.clientJwk + ")"; - } - } -} diff --git a/token-client-core/src/main/java/no/nav/security/token/support/client/core/ClientProperties.java b/token-client-core/src/main/java/no/nav/security/token/support/client/core/ClientProperties.java deleted file mode 100644 index 50afe1ce..00000000 --- a/token-client-core/src/main/java/no/nav/security/token/support/client/core/ClientProperties.java +++ /dev/null @@ -1,295 +0,0 @@ -package no.nav.security.token.support.client.core; - -import com.nimbusds.jose.util.DefaultResourceRetriever; -import com.nimbusds.jose.util.ResourceRetriever; -import com.nimbusds.oauth2.sdk.ParseException; -import com.nimbusds.oauth2.sdk.as.AuthorizationServerMetadata; -import jakarta.validation.constraints.NotNull; - -import java.io.IOException; -import java.net.URI; -import java.util.Collections; -import java.util.List; -import java.util.Objects; -import java.util.Optional; -import java.util.function.Supplier; - -public class ClientProperties { - - private static final List GRANT_TYPES = List.of( - OAuth2GrantType.JWT_BEARER, - OAuth2GrantType.CLIENT_CREDENTIALS, - OAuth2GrantType.TOKEN_EXCHANGE - ); - - @NotNull - private final URI tokenEndpointUrl; - @NotNull - private final OAuth2GrantType grantType; - private final List scope; - @NotNull - private final ClientAuthenticationProperties authentication; - private final URI resourceUrl; - private final TokenExchangeProperties tokenExchange; - private final URI wellKnownUrl; - private AuthorizationServerMetadata authorizationServerMetadata; - private ResourceRetriever resourceRetriever; - - public ClientProperties(URI tokenEndpointUrl, - URI wellKnownUrl, - @NotNull OAuth2GrantType grantType, - List scope, - @NotNull ClientAuthenticationProperties authentication, - URI resourceUrl, - TokenExchangeProperties tokenExchange - ) { - this.wellKnownUrl = wellKnownUrl; - - if(tokenEndpointUrl != null){ - this.tokenEndpointUrl = tokenEndpointUrl; - } else { - this.resourceRetriever = new DefaultResourceRetriever(); - this.authorizationServerMetadata = retrieveAuthorizationServerMetadata(); - this.tokenEndpointUrl = this.authorizationServerMetadata.getTokenEndpointURI(); - } - this.grantType = getSupported(grantType); - this.scope = Optional.ofNullable(scope).orElse(Collections.emptyList()); - this.authentication = authentication; - this.resourceUrl = resourceUrl; - this.tokenExchange = tokenExchange; - } - - public static ClientPropertiesBuilder builder() { - return new ClientPropertiesBuilder(); - } - - private AuthorizationServerMetadata retrieveAuthorizationServerMetadata(){ - if (wellKnownUrl == null) { - throw new OAuth2ClientException("wellKnownUrl cannot be null, please check your configuration."); - } - try { - return AuthorizationServerMetadata.parse( - resourceRetriever.retrieveResource(wellKnownUrl.toURL()).getContent() - ); - } catch (ParseException | IOException e) { - throw new OAuth2ClientException("received exception when retrieving metadata from url " + wellKnownUrl, e); - } - } - - private static OAuth2GrantType getSupported(OAuth2GrantType oAuth2GrantType) { - return Optional.ofNullable(oAuth2GrantType) - .filter(GRANT_TYPES::contains) - .orElseThrow(unsupported(oAuth2GrantType)); - } - - private static Supplier unsupported(OAuth2GrantType oAuth2GrantType) { - return () -> new IllegalArgumentException( - String.format("unsupported %s with value %s, must be one of %s", - OAuth2GrantType.class.getSimpleName(), oAuth2GrantType, GRANT_TYPES)); - } - - public @NotNull URI getTokenEndpointUrl() { - return this.tokenEndpointUrl; - } - - public @NotNull OAuth2GrantType getGrantType() { - return this.grantType; - } - - public List getScope() { - return this.scope; - } - - public @NotNull ClientAuthenticationProperties getAuthentication() { - return this.authentication; - } - - public URI getResourceUrl() { - return this.resourceUrl; - } - - public TokenExchangeProperties getTokenExchange() { - return this.tokenExchange; - } - - public URI getWellKnownUrl() { - return this.wellKnownUrl; - } - - public AuthorizationServerMetadata getAuthorizationServerMetadata() { - return this.authorizationServerMetadata; - } - - public ResourceRetriever getResourceRetriever() { - return this.resourceRetriever; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - ClientProperties that = (ClientProperties) o; - return Objects.equals(tokenEndpointUrl, that.tokenEndpointUrl) && - Objects.equals(grantType, that.grantType) && - Objects.equals(scope, that.scope) && - Objects.equals(authentication, that.authentication) && - Objects.equals(resourceUrl, that.resourceUrl) && - Objects.equals(tokenExchange, that.tokenExchange) && - Objects.equals(wellKnownUrl, that.wellKnownUrl) && - Objects.equals(authorizationServerMetadata, that.authorizationServerMetadata) && - Objects.equals(resourceRetriever, that.resourceRetriever); - } - - @Override - public int hashCode() { - return Objects.hash(tokenEndpointUrl, grantType, scope, authentication, resourceUrl, tokenExchange, wellKnownUrl, authorizationServerMetadata, resourceRetriever); - } - - @Override - public String toString() { - return "ClientProperties(tokenEndpointUrl=" + this.getTokenEndpointUrl() + ", grantType=" + this.getGrantType() + ", scope=" + this.getScope() + ", authentication=" + this.getAuthentication() + ", resourceUrl=" + this.getResourceUrl() + ", tokenExchange=" + this.getTokenExchange() + ", wellKnownUrl=" + this.getWellKnownUrl() + ", authorizationServerMetadata=" + this.getAuthorizationServerMetadata() + ", resourceRetriever=" + this.getResourceRetriever() + ")"; - } - - public ClientPropertiesBuilder toBuilder() { - return new ClientPropertiesBuilder().tokenEndpointUrl(this.tokenEndpointUrl).wellKnownUrl(this.wellKnownUrl).grantType(this.grantType).scope(this.scope).authentication(this.authentication).resourceUrl(this.resourceUrl).tokenExchange(this.tokenExchange); - } - - public static class TokenExchangeProperties { - - @NotNull - private final String audience; - private final String resource; - - public TokenExchangeProperties(@NotNull String audience, String resource) { - this.audience = audience; - this.resource = resource; - validateAfterPropertiesSet(); - } - - public static TokenExchangePropertiesBuilder builder() { - return new TokenExchangePropertiesBuilder(); - } - - private void validateAfterPropertiesSet() { - Objects.requireNonNull(audience, "audience must be set"); - } - - public String subjectTokenType() { - return "urn:ietf:params:oauth:token-type:jwt"; - } - - public @NotNull String getAudience() { - return this.audience; - } - - public String getResource() { - return this.resource; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - TokenExchangeProperties that = (TokenExchangeProperties) o; - return audience.equals(that.audience) && resource.equals(that.resource); - } - - @Override - public int hashCode() { - return Objects.hash(audience, resource); - } - - @Override - public String toString() { - return "ClientProperties.TokenExchangeProperties(audience=" + this.getAudience() + ", resource=" + this.getResource() + ")"; - } - - public TokenExchangePropertiesBuilder toBuilder() { - return new TokenExchangePropertiesBuilder().audience(this.audience).resource(this.resource); - } - - public static class TokenExchangePropertiesBuilder { - private @NotNull String audience; - private String resource; - - TokenExchangePropertiesBuilder() { - } - - public TokenExchangePropertiesBuilder audience(@NotNull String audience) { - this.audience = audience; - return this; - } - - public TokenExchangePropertiesBuilder resource(String resource) { - this.resource = resource; - return this; - } - - public TokenExchangeProperties build() { - return new TokenExchangeProperties(audience, resource); - } - - @Override - public String toString() { - return "ClientProperties.TokenExchangeProperties.TokenExchangePropertiesBuilder(audience=" + this.audience + ", resource=" + this.resource + ")"; - } - } - } - - public static class ClientPropertiesBuilder { - private URI tokenEndpointUrl; - private URI wellKnownUrl; - private @NotNull OAuth2GrantType grantType; - private List scope; - private @NotNull ClientAuthenticationProperties authentication; - private URI resourceUrl; - private TokenExchangeProperties tokenExchange; - - ClientPropertiesBuilder() { - } - - public ClientPropertiesBuilder tokenEndpointUrl(URI tokenEndpointUrl) { - this.tokenEndpointUrl = tokenEndpointUrl; - return this; - } - - public ClientPropertiesBuilder wellKnownUrl(URI wellKnownUrl) { - this.wellKnownUrl = wellKnownUrl; - return this; - } - - public ClientPropertiesBuilder grantType(@NotNull OAuth2GrantType grantType) { - this.grantType = grantType; - return this; - } - - public ClientPropertiesBuilder scope(List scope) { - this.scope = scope; - return this; - } - - public ClientPropertiesBuilder authentication(@NotNull ClientAuthenticationProperties authentication) { - this.authentication = authentication; - return this; - } - - public ClientPropertiesBuilder resourceUrl(URI resourceUrl) { - this.resourceUrl = resourceUrl; - return this; - } - - public ClientPropertiesBuilder tokenExchange(TokenExchangeProperties tokenExchange) { - this.tokenExchange = tokenExchange; - return this; - } - - public ClientProperties build() { - return new ClientProperties(tokenEndpointUrl, wellKnownUrl, grantType, scope, authentication, resourceUrl, tokenExchange); - } - - @Override - public String toString() { - return "ClientProperties.ClientPropertiesBuilder(tokenEndpointUrl=" + this.tokenEndpointUrl + ", wellKnownUrl=" + this.wellKnownUrl + ", grantType=" + this.grantType + ", scope=" + this.scope + ", authentication=" + this.authentication + ", resourceUrl=" + this.resourceUrl + ", tokenExchange=" + this.tokenExchange + ")"; - } - } -} diff --git a/token-client-core/src/main/java/no/nav/security/token/support/client/core/OAuth2CacheFactory.java b/token-client-core/src/main/java/no/nav/security/token/support/client/core/OAuth2CacheFactory.java deleted file mode 100644 index e635c3a5..00000000 --- a/token-client-core/src/main/java/no/nav/security/token/support/client/core/OAuth2CacheFactory.java +++ /dev/null @@ -1,47 +0,0 @@ -package no.nav.security.token.support.client.core; - -import com.github.benmanes.caffeine.cache.Cache; -import com.github.benmanes.caffeine.cache.Caffeine; -import com.github.benmanes.caffeine.cache.Expiry; -import no.nav.security.token.support.client.core.oauth2.OAuth2AccessTokenResponse; -import org.checkerframework.checker.nullness.qual.NonNull; - -import java.util.concurrent.TimeUnit; - -public class OAuth2CacheFactory { - - private OAuth2CacheFactory() { - - } - public static Cache accessTokenResponseCache(long maximumSize, long skewInSeconds) { - // Evict based on a varying expiration policy - return Caffeine.newBuilder() - .maximumSize(maximumSize) - .expireAfter(evictOnResponseExpiresIn(skewInSeconds)) - .build(); - } - - private static Expiry evictOnResponseExpiresIn(long skewInSeconds) { - return new Expiry<>() { - @Override - public long expireAfterCreate(@NonNull T key, @NonNull OAuth2AccessTokenResponse response, - long currentTime) { - long seconds = response.getExpiresIn() > skewInSeconds ? - response.getExpiresIn() - skewInSeconds : response.getExpiresIn(); - return TimeUnit.SECONDS.toNanos(seconds); - } - - @Override - public long expireAfterUpdate(@NonNull T key, @NonNull OAuth2AccessTokenResponse response, - long currentTime, long currentDuration) { - return currentDuration; - } - - @Override - public long expireAfterRead(@NonNull T key, @NonNull OAuth2AccessTokenResponse response, long currentTime - , long currentDuration) { - return currentDuration; - } - }; - } -} diff --git a/token-client-core/src/main/java/no/nav/security/token/support/client/core/OAuth2ClientException.java b/token-client-core/src/main/java/no/nav/security/token/support/client/core/OAuth2ClientException.java deleted file mode 100644 index a833742d..00000000 --- a/token-client-core/src/main/java/no/nav/security/token/support/client/core/OAuth2ClientException.java +++ /dev/null @@ -1,12 +0,0 @@ -package no.nav.security.token.support.client.core; - -public class OAuth2ClientException extends RuntimeException { - - public OAuth2ClientException(String message) { - this(message,null); - } - - public OAuth2ClientException(String message, Throwable cause) { - super(message, cause); - } -} diff --git a/token-client-core/src/main/java/no/nav/security/token/support/client/core/OAuth2GrantType.java b/token-client-core/src/main/java/no/nav/security/token/support/client/core/OAuth2GrantType.java deleted file mode 100644 index cb15fc16..00000000 --- a/token-client-core/src/main/java/no/nav/security/token/support/client/core/OAuth2GrantType.java +++ /dev/null @@ -1,14 +0,0 @@ -package no.nav.security.token.support.client.core; - -import java.util.Optional; - -public record OAuth2GrantType(String value) { - public static final OAuth2GrantType JWT_BEARER = new OAuth2GrantType("urn:ietf:params:oauth:grant-type:jwt-bearer"); - public static final OAuth2GrantType CLIENT_CREDENTIALS = new OAuth2GrantType("client_credentials"); - public static final OAuth2GrantType TOKEN_EXCHANGE = new OAuth2GrantType("urn:ietf:params:oauth:grant-type:token-exchange"); - - public OAuth2GrantType(String value) { - this.value = Optional.ofNullable(value) - .orElseThrow(() -> new OAuth2ClientException("value for OAuth2GrantType cannot be null")); - } -} diff --git a/token-client-core/src/main/java/no/nav/security/token/support/client/core/OAuth2ParameterNames.java b/token-client-core/src/main/java/no/nav/security/token/support/client/core/OAuth2ParameterNames.java deleted file mode 100644 index 635c298f..00000000 --- a/token-client-core/src/main/java/no/nav/security/token/support/client/core/OAuth2ParameterNames.java +++ /dev/null @@ -1,20 +0,0 @@ -package no.nav.security.token.support.client.core; - -public class OAuth2ParameterNames { - - private OAuth2ParameterNames() { - - } - public static final String GRANT_TYPE = "grant_type"; - public static final String CLIENT_ID = "client_id"; - public static final String CLIENT_SECRET = "client_secret"; - public static final String ASSERTION = "assertion"; - public static final String REQUESTED_TOKEN_USE = "requested_token_use"; - public static final String SCOPE = "scope"; - public static final String CLIENT_ASSERTION_TYPE = "client_assertion_type"; - public static final String CLIENT_ASSERTION = "client_assertion"; - public static final String SUBJECT_TOKEN_TYPE = "subject_token_type"; - public static final String SUBJECT_TOKEN = "subject_token"; - public static final String AUDIENCE = "audience"; - public static final String RESOURCE = "resource"; -} \ No newline at end of file diff --git a/token-client-core/src/main/java/no/nav/security/token/support/client/core/auth/ClientAssertion.java b/token-client-core/src/main/java/no/nav/security/token/support/client/core/auth/ClientAssertion.java deleted file mode 100644 index 89f96014..00000000 --- a/token-client-core/src/main/java/no/nav/security/token/support/client/core/auth/ClientAssertion.java +++ /dev/null @@ -1,73 +0,0 @@ -package no.nav.security.token.support.client.core.auth; - -import com.nimbusds.jose.JOSEException; -import com.nimbusds.jose.JOSEObjectType; -import com.nimbusds.jose.JWSAlgorithm; -import com.nimbusds.jose.JWSHeader; -import com.nimbusds.jose.crypto.RSASSASigner; -import com.nimbusds.jose.jwk.RSAKey; -import com.nimbusds.jwt.JWTClaimsSet; -import com.nimbusds.jwt.SignedJWT; -import jakarta.validation.constraints.NotNull; -import no.nav.security.token.support.client.core.ClientAuthenticationProperties; - -import java.net.URI; -import java.time.Instant; -import java.util.Date; -import java.util.UUID; - -public class ClientAssertion { - - private static final int EXPIRY_IN_SECONDS = 60; - private final URI tokenEndpointUrl; - private final String clientId; - private final RSAKey rsaKey; - private final int expiryInSeconds; - - public ClientAssertion(@NotNull URI tokenEndpointUrl, - @NotNull ClientAuthenticationProperties clientAuthenticationProperties) { - this( - tokenEndpointUrl, - clientAuthenticationProperties.getClientId(), - clientAuthenticationProperties.getClientRsaKey(), - EXPIRY_IN_SECONDS - ); - } - - public ClientAssertion(URI tokenEndpointUrl, String clientId, RSAKey rsaKey, int expiryInSeconds) { - this.tokenEndpointUrl = tokenEndpointUrl; - this.rsaKey = rsaKey; - this.clientId = clientId; - this.expiryInSeconds = expiryInSeconds; - } - - public String assertion() { - var now = Instant.now(); - return createSignedJWT(rsaKey, new JWTClaimsSet.Builder() - .audience(tokenEndpointUrl.toString()) - .expirationTime(Date.from(now.plusSeconds(expiryInSeconds))) - .issuer(clientId) - .subject(clientId) - .claim("jti", UUID.randomUUID().toString()) - .notBeforeTime(Date.from(now)) - .issueTime(Date.from(now)) - .build()).serialize(); - } - - public String assertionType() { - return "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"; - } - - private SignedJWT createSignedJWT(RSAKey rsaJwk, JWTClaimsSet claimsSet) { - try { - var header = new JWSHeader.Builder(JWSAlgorithm.RS256) - .keyID(rsaJwk.getKeyID()) - .type(JOSEObjectType.JWT); - var signedJWT = new SignedJWT(header.build(), claimsSet); - signedJWT.sign(new RSASSASigner(rsaJwk.toPrivateKey())); - return signedJWT; - } catch (JOSEException e) { - throw new RuntimeException(e); - } - } -} \ No newline at end of file diff --git a/token-client-core/src/main/java/no/nav/security/token/support/client/core/context/JwtBearerTokenResolver.java b/token-client-core/src/main/java/no/nav/security/token/support/client/core/context/JwtBearerTokenResolver.java deleted file mode 100644 index 6f730c25..00000000 --- a/token-client-core/src/main/java/no/nav/security/token/support/client/core/context/JwtBearerTokenResolver.java +++ /dev/null @@ -1,7 +0,0 @@ -package no.nav.security.token.support.client.core.context; - -import java.util.Optional; - -public interface JwtBearerTokenResolver { - Optional token(); -} diff --git a/token-client-core/src/main/java/no/nav/security/token/support/client/core/http/OAuth2HttpClient.java b/token-client-core/src/main/java/no/nav/security/token/support/client/core/http/OAuth2HttpClient.java deleted file mode 100644 index 31a59676..00000000 --- a/token-client-core/src/main/java/no/nav/security/token/support/client/core/http/OAuth2HttpClient.java +++ /dev/null @@ -1,7 +0,0 @@ -package no.nav.security.token.support.client.core.http; - -import no.nav.security.token.support.client.core.oauth2.OAuth2AccessTokenResponse; - -public interface OAuth2HttpClient { - OAuth2AccessTokenResponse post(OAuth2HttpRequest oAuth2HttpRequest); -} diff --git a/token-client-core/src/main/java/no/nav/security/token/support/client/core/http/OAuth2HttpHeaders.java b/token-client-core/src/main/java/no/nav/security/token/support/client/core/http/OAuth2HttpHeaders.java deleted file mode 100644 index 0368c006..00000000 --- a/token-client-core/src/main/java/no/nav/security/token/support/client/core/http/OAuth2HttpHeaders.java +++ /dev/null @@ -1,63 +0,0 @@ -package no.nav.security.token.support.client.core.http; - -import java.util.*; - -import static java.lang.String.CASE_INSENSITIVE_ORDER; - -public class OAuth2HttpHeaders { - - private final Map> headers; - - private OAuth2HttpHeaders(final Map> headers) { - this.headers = Optional.ofNullable(headers).orElse(Map.of()); - } - - public static OAuth2HttpHeaders of(Map> headers) { - return new OAuth2HttpHeaders(headers); - } - - public static Builder builder() { - return new Builder(); - } - - public Map> headers() { - return headers; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - var that = (OAuth2HttpHeaders) o; - return Objects.equals(headers, that.headers); - } - - @Override - public int hashCode() { - return Objects.hash(headers); - } - - @Override - public String toString() { - return getClass().getSimpleName() + " [headers=" + headers + "]"; - } - - public static class Builder { - private final TreeMap> headersMap; - - public Builder() { - headersMap = new TreeMap<>(CASE_INSENSITIVE_ORDER); - } - - public Builder header(String name, String value) { - headersMap.computeIfAbsent(name, k -> new ArrayList<>(1)) - .add(value); - return this; - } - - public OAuth2HttpHeaders build() { - return OAuth2HttpHeaders.of(headersMap); - } - } - -} diff --git a/token-client-core/src/main/java/no/nav/security/token/support/client/core/http/OAuth2HttpRequest.java b/token-client-core/src/main/java/no/nav/security/token/support/client/core/http/OAuth2HttpRequest.java deleted file mode 100644 index 63b2c4d3..00000000 --- a/token-client-core/src/main/java/no/nav/security/token/support/client/core/http/OAuth2HttpRequest.java +++ /dev/null @@ -1,108 +0,0 @@ -package no.nav.security.token.support.client.core.http; - -import java.net.URI; -import java.util.ArrayList; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; - -import static java.util.Collections.unmodifiableMap; - -public class OAuth2HttpRequest { - - private final URI tokenEndpointUrl; - private final OAuth2HttpHeaders oAuth2HttpHeaders; - private final Map formParameters; - - OAuth2HttpRequest(URI tokenEndpointUrl, OAuth2HttpHeaders oAuth2HttpHeaders, Map formParameters) { - this.tokenEndpointUrl = tokenEndpointUrl; - this.oAuth2HttpHeaders = oAuth2HttpHeaders; - this.formParameters = formParameters; - } - - public static OAuth2HttpRequestBuilder builder() { - return new OAuth2HttpRequestBuilder(); - } - - public URI getTokenEndpointUrl() { - return tokenEndpointUrl; - } - - public OAuth2HttpHeaders getOAuth2HttpHeaders() { - return oAuth2HttpHeaders; - } - - public Map getFormParameters() { - return formParameters; - } - - public static class OAuth2HttpRequestBuilder { - private URI tokenEndpointUrl; - private OAuth2HttpHeaders oAuth2HttpHeaders; - private List keys; - private List values; - - OAuth2HttpRequestBuilder() { - } - - public OAuth2HttpRequestBuilder tokenEndpointUrl(URI tokenEndpointUrl) { - this.tokenEndpointUrl = tokenEndpointUrl; - return this; - } - - public OAuth2HttpRequestBuilder oAuth2HttpHeaders(OAuth2HttpHeaders oAuth2HttpHeaders) { - this.oAuth2HttpHeaders = oAuth2HttpHeaders; - return this; - } - - public OAuth2HttpRequestBuilder formParameter(String formParameterKey, String formParameterValue) { - if (keys == null) { - keys = new ArrayList<>(); - values = new ArrayList<>(); - } - keys.add(formParameterKey); - values.add(formParameterValue); - return this; - } - - public OAuth2HttpRequestBuilder formParameters(Map formParameters) { - if (keys == null) { - keys = new ArrayList<>(); - values = new ArrayList<>(); - } - formParameters.forEach((key, value) -> { - keys.add(key); - values.add(value); - }); - return this; - } - - public OAuth2HttpRequestBuilder clearFormParameters() { - if (keys != null) { - keys.clear(); - values.clear(); - } - return this; - } - - public OAuth2HttpRequest build() { - switch (keys == null ? 0 : keys.size()) { - case 0: - return new OAuth2HttpRequest(tokenEndpointUrl, oAuth2HttpHeaders, Map.of()); - case 1: - return new OAuth2HttpRequest(tokenEndpointUrl, oAuth2HttpHeaders, Map.of(this.keys.get(0), this.values.get(0))); - default: - var formParameters = new LinkedHashMap(keys.size()); - for (int i = 0; i < this.keys.size(); i++) { - formParameters.put(this.keys.get(i), this.values.get(i)); - } - return new OAuth2HttpRequest(tokenEndpointUrl, oAuth2HttpHeaders, unmodifiableMap(formParameters)); - } - } - - @Override - public String toString() { - return "OAuth2HttpRequest.OAuth2HttpRequestBuilder(tokenEndpointUrl=" + tokenEndpointUrl + ", oAuth2HttpHeaders=" + oAuth2HttpHeaders + ", keys=" + keys + ", values=" + values + ")"; - } - } -} diff --git a/token-client-core/src/main/java/no/nav/security/token/support/client/core/jwk/JwkFactory.java b/token-client-core/src/main/java/no/nav/security/token/support/client/core/jwk/JwkFactory.java deleted file mode 100644 index e4374491..00000000 --- a/token-client-core/src/main/java/no/nav/security/token/support/client/core/jwk/JwkFactory.java +++ /dev/null @@ -1,100 +0,0 @@ -package no.nav.security.token.support.client.core.jwk; - - -import com.nimbusds.jose.jwk.JWKSet; -import com.nimbusds.jose.jwk.RSAKey; -import com.nimbusds.jose.util.Base64URL; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.IOException; -import java.io.InputStream; -import java.nio.file.Files; -import java.nio.file.Path; -import java.security.KeyStore; -import java.security.KeyStoreException; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.security.cert.CertificateEncodingException; -import java.security.cert.CertificateException; -import java.text.ParseException; -import java.util.Optional; - -import static java.nio.charset.StandardCharsets.UTF_8; - -public class JwkFactory { - - private JwkFactory() { - - } - private static final Logger LOG = LoggerFactory.getLogger(JwkFactory.class); - private static final boolean USE_CERTIFICATE_SHA1_THUMBPRINT = true; - - public static RSAKey fromJsonFile(String filePath){ - try { - LOG.debug("attempting to read jwk from path: {}", Path.of(filePath).toAbsolutePath()); - return fromJson(Files.readString(Path.of(filePath), UTF_8)); - } catch (IOException e) { - throw new JwkInvalidException(e); - } - } - - public static RSAKey fromJson(String jsonJwk){ - try { - return RSAKey.parse(jsonJwk); - } catch (ParseException e) { - throw new JwkInvalidException(e); - } - } - - public static RSAKey fromKeyStore(String alias, InputStream keyStoreFile, String password) { - var keyFromKeyStore = (RSAKey) fromKeyStore(keyStoreFile, password).getKeyByKeyId(alias); - return new RSAKey.Builder(keyFromKeyStore) - .keyID(USE_CERTIFICATE_SHA1_THUMBPRINT ? - getX509CertSHA1Thumbprint(keyFromKeyStore) - : keyFromKeyStore.getKeyID()) - .build(); - } - - private static JWKSet fromKeyStore(InputStream keyStoreFile, String password) { - try { - var pwd = Optional.ofNullable(password) - .map(String::toCharArray) - .orElseThrow(() -> new JwkInvalidException("password cannot be null")); - var keyStore = KeyStore.getInstance("JKS"); - keyStore.load(keyStoreFile, pwd); - return JWKSet.load(keyStore, name -> pwd); - } catch (KeyStoreException | IOException | NoSuchAlgorithmException | CertificateException e) { - throw new RuntimeException(e); - } - } - - private static String getX509CertSHA1Thumbprint(RSAKey rsaKey) { - var cert = rsaKey.getParsedX509CertChain().stream() - .findFirst() - .orElse(null); - try { - return cert != null ? createSHA1DigestBase64Url(cert.getEncoded()) : null; - } catch (CertificateEncodingException e) { - throw new RuntimeException(e); - } - } - - private static String createSHA1DigestBase64Url(byte[] bytes) { - try { - return Base64URL.encode(MessageDigest.getInstance("SHA-1").digest(bytes)).toString(); - } catch (NoSuchAlgorithmException e) { - throw new RuntimeException(e); - } - } - - public static class JwkInvalidException extends RuntimeException { - JwkInvalidException(String message) { - super(message); - } - - JwkInvalidException(Throwable cause) { - super(cause); - } - } -} diff --git a/token-client-core/src/main/java/no/nav/security/token/support/client/core/oauth2/AbstractOAuth2GrantRequest.java b/token-client-core/src/main/java/no/nav/security/token/support/client/core/oauth2/AbstractOAuth2GrantRequest.java deleted file mode 100644 index 3c70ee69..00000000 --- a/token-client-core/src/main/java/no/nav/security/token/support/client/core/oauth2/AbstractOAuth2GrantRequest.java +++ /dev/null @@ -1,44 +0,0 @@ -package no.nav.security.token.support.client.core.oauth2; - -import no.nav.security.token.support.client.core.ClientProperties; -import no.nav.security.token.support.client.core.OAuth2GrantType; - -import java.util.Objects; - -abstract class AbstractOAuth2GrantRequest { - - private final OAuth2GrantType oAuth2GrantType; - private final ClientProperties clientProperties; - - protected AbstractOAuth2GrantRequest(OAuth2GrantType oAuth2GrantType, ClientProperties clientProperties) { - this.oAuth2GrantType = oAuth2GrantType; - this.clientProperties = clientProperties; - } - - OAuth2GrantType getGrantType() { - return oAuth2GrantType; - } - - ClientProperties getClientProperties() { - return clientProperties; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - AbstractOAuth2GrantRequest that = (AbstractOAuth2GrantRequest) o; - return Objects.equals(oAuth2GrantType, that.oAuth2GrantType) - && Objects.equals(clientProperties, that.clientProperties); - } - - @Override - public int hashCode() { - return Objects.hash(oAuth2GrantType, clientProperties); - } - - @Override - public String toString() { - return getClass().getSimpleName() + " [oAuth2GrantType=" + oAuth2GrantType + ", clientProperties=" + clientProperties + "]"; - } -} diff --git a/token-client-core/src/main/java/no/nav/security/token/support/client/core/oauth2/AbstractOAuth2TokenClient.java b/token-client-core/src/main/java/no/nav/security/token/support/client/core/oauth2/AbstractOAuth2TokenClient.java deleted file mode 100644 index 0dbbfae8..00000000 --- a/token-client-core/src/main/java/no/nav/security/token/support/client/core/oauth2/AbstractOAuth2TokenClient.java +++ /dev/null @@ -1,109 +0,0 @@ -package no.nav.security.token.support.client.core.oauth2; - -import no.nav.security.token.support.client.core.*; -import no.nav.security.token.support.client.core.auth.ClientAssertion; -import no.nav.security.token.support.client.core.http.OAuth2HttpClient; -import no.nav.security.token.support.client.core.http.OAuth2HttpHeaders; -import no.nav.security.token.support.client.core.http.OAuth2HttpRequest; - -import java.nio.charset.Charset; -import java.nio.charset.CharsetEncoder; -import java.nio.charset.StandardCharsets; -import java.util.*; - -import static com.nimbusds.oauth2.sdk.auth.ClientAuthenticationMethod.*; - -abstract class AbstractOAuth2TokenClient { - - private static final String CONTENT_TYPE_FORM_URL_ENCODED = "application/x-www-form-urlencoded;charset=UTF-8"; - private static final String CONTENT_TYPE_JSON = "application/json;charset=UTF-8"; - private final OAuth2HttpClient oAuth2HttpClient; - - AbstractOAuth2TokenClient(OAuth2HttpClient oAuth2HttpClient) { - this.oAuth2HttpClient = oAuth2HttpClient; - } - - OAuth2AccessTokenResponse getTokenResponse(T grantRequest) { - - var clientProperties = Optional.ofNullable(grantRequest) - .map(AbstractOAuth2GrantRequest::getClientProperties) - .orElseThrow(() -> new OAuth2ClientException("ClientProperties cannot be null")); - - try { - var formParameters = createDefaultFormParameters(grantRequest); - formParameters.putAll(this.formParameters(grantRequest)); - - var oAuth2HttpRequest = OAuth2HttpRequest.builder() - .tokenEndpointUrl(clientProperties.getTokenEndpointUrl()) - .oAuth2HttpHeaders(OAuth2HttpHeaders.of(tokenRequestHeaders(clientProperties))) - .formParameters(formParameters) - .build(); - return oAuth2HttpClient.post(oAuth2HttpRequest); - } catch (Exception e) { - if (!(e instanceof OAuth2ClientException)) { - throw new OAuth2ClientException(String.format("received exception %s when invoking tokenendpoint=%s", - e, grantRequest.getClientProperties().getTokenEndpointUrl()), e); - } - throw e; - } - } - - private Map> tokenRequestHeaders(ClientProperties clientProperties) { - var headers = new HashMap>(); - headers.put("Accept", List.of(CONTENT_TYPE_JSON)); - headers.put("Content-Type", Collections.singletonList(CONTENT_TYPE_FORM_URL_ENCODED)); - var auth = clientProperties.getAuthentication(); - if (CLIENT_SECRET_BASIC.equals(auth.getClientAuthMethod())) { - headers.put("Authorization", - List.of("Basic " + basicAuth(auth.getClientId(), auth.getClientSecret()))); - } - return headers; - } - - Map createDefaultFormParameters(T grantRequest) { - ClientProperties clientProperties = grantRequest.getClientProperties(); - Map formParameters = new LinkedHashMap<>(clientAuthenticationFormParameters(grantRequest)); - formParameters.put(OAuth2ParameterNames.GRANT_TYPE, grantRequest.getGrantType().value()); - if (!clientProperties.getGrantType().equals(OAuth2GrantType.TOKEN_EXCHANGE)) { - formParameters.put(OAuth2ParameterNames.SCOPE, String.join(" ", clientProperties.getScope())); - } - return formParameters; - } - - private Map clientAuthenticationFormParameters(T grantRequest) { - ClientProperties clientProperties = grantRequest.getClientProperties(); - Map formParameters = new LinkedHashMap<>(); - ClientAuthenticationProperties auth = clientProperties.getAuthentication(); - if (CLIENT_SECRET_POST.equals(auth.getClientAuthMethod())) { - formParameters.put(OAuth2ParameterNames.CLIENT_ID, auth.getClientId()); - formParameters.put(OAuth2ParameterNames.CLIENT_SECRET, auth.getClientSecret()); - - } else if (PRIVATE_KEY_JWT.equals(auth.getClientAuthMethod())) { - ClientAssertion clientAssertion = new ClientAssertion(clientProperties.getTokenEndpointUrl(), auth); - - formParameters.put(OAuth2ParameterNames.CLIENT_ID, auth.getClientId()); - formParameters.put(OAuth2ParameterNames.CLIENT_ASSERTION_TYPE, clientAssertion.assertionType()); - formParameters.put(OAuth2ParameterNames.CLIENT_ASSERTION, clientAssertion.assertion()); - } - return formParameters; - } - - private String basicAuth(String username, String password) { - Charset charset = StandardCharsets.UTF_8; - CharsetEncoder encoder = charset.newEncoder(); - if (encoder.canEncode(username) && encoder.canEncode(password)) { - String credentialsString = username + ":" + password; - byte[] encodedBytes = Base64.getEncoder().encode(credentialsString.getBytes(StandardCharsets.UTF_8)); - return new String(encodedBytes, StandardCharsets.UTF_8); - } else { - throw new IllegalArgumentException("Username or password contains characters that cannot be encoded to " + charset.displayName()); - } - } - - protected abstract Map formParameters(T grantRequest); - - @Override - public String toString() { - return getClass().getSimpleName() + " [oAuth2HttpClient=" + oAuth2HttpClient + "]"; - } -} diff --git a/token-client-core/src/main/java/no/nav/security/token/support/client/core/oauth2/ClientCredentialsGrantRequest.java b/token-client-core/src/main/java/no/nav/security/token/support/client/core/oauth2/ClientCredentialsGrantRequest.java deleted file mode 100644 index 936c3a72..00000000 --- a/token-client-core/src/main/java/no/nav/security/token/support/client/core/oauth2/ClientCredentialsGrantRequest.java +++ /dev/null @@ -1,11 +0,0 @@ -package no.nav.security.token.support.client.core.oauth2; - -import no.nav.security.token.support.client.core.ClientProperties; -import no.nav.security.token.support.client.core.OAuth2GrantType; - -public class ClientCredentialsGrantRequest extends AbstractOAuth2GrantRequest { - - public ClientCredentialsGrantRequest(ClientProperties clientProperties) { - super(OAuth2GrantType.CLIENT_CREDENTIALS, clientProperties); - } -} diff --git a/token-client-core/src/main/java/no/nav/security/token/support/client/core/oauth2/ClientCredentialsTokenClient.java b/token-client-core/src/main/java/no/nav/security/token/support/client/core/oauth2/ClientCredentialsTokenClient.java deleted file mode 100644 index 3103d289..00000000 --- a/token-client-core/src/main/java/no/nav/security/token/support/client/core/oauth2/ClientCredentialsTokenClient.java +++ /dev/null @@ -1,17 +0,0 @@ -package no.nav.security.token.support.client.core.oauth2; - -import no.nav.security.token.support.client.core.http.OAuth2HttpClient; - -import java.util.Map; - -public class ClientCredentialsTokenClient extends AbstractOAuth2TokenClient { - - public ClientCredentialsTokenClient(OAuth2HttpClient oAuth2HttpClient) { - super(oAuth2HttpClient); - } - - @Override - protected Map formParameters(ClientCredentialsGrantRequest grantRequest) { - return Map.of(); - } -} diff --git a/token-client-core/src/main/java/no/nav/security/token/support/client/core/oauth2/OAuth2AccessTokenResponse.java b/token-client-core/src/main/java/no/nav/security/token/support/client/core/oauth2/OAuth2AccessTokenResponse.java deleted file mode 100644 index 96e13eda..00000000 --- a/token-client-core/src/main/java/no/nav/security/token/support/client/core/oauth2/OAuth2AccessTokenResponse.java +++ /dev/null @@ -1,99 +0,0 @@ -package no.nav.security.token.support.client.core.oauth2; - -import java.util.HashMap; -import java.util.Map; - -public class OAuth2AccessTokenResponse { - - private String accessToken; - private int expiresAt; - private int expiresIn; - private Map additonalParameters = new HashMap<>(); - - public OAuth2AccessTokenResponse(String accessToken, int expiresAt, int expiresIn, Map additonalParameters) { - this.accessToken = accessToken; - this.expiresAt = expiresAt; - this.expiresIn = expiresIn; - this.additonalParameters = additonalParameters; - } - - public OAuth2AccessTokenResponse() { - } - - public static OAuth2AccessTokenResponseBuilder builder() { - return new OAuth2AccessTokenResponseBuilder(); - } - - //for jackson if it is used for deserialization - void setAccess_token(String access_token) { - this.accessToken = access_token; - } - - void setExpires_at(int expires_at) { - this.expiresAt = expires_at; - } - - void setExpires_in(int expires_in) { - this.expiresIn = expires_in; - } - - public String getAccessToken() { - return this.accessToken; - } - - public int getExpiresAt() { - return this.expiresAt; - } - - public int getExpiresIn() { - return this.expiresIn; - } - - public Map getAdditonalParameters() { - return this.additonalParameters; - } - - @Override - public String toString() { - return "OAuth2AccessTokenResponse(accessToken=" + this.getAccessToken() + ", expiresAt=" + this.getExpiresAt() + ", expiresIn=" + this.getExpiresIn() + ", additonalParameters=" + this.getAdditonalParameters() + ")"; - } - - public static class OAuth2AccessTokenResponseBuilder { - private String accessToken; - private int expiresAt; - private int expiresIn; - private Map additonalParameters; - - OAuth2AccessTokenResponseBuilder() { - } - - public OAuth2AccessTokenResponseBuilder accessToken(String accessToken) { - this.accessToken = accessToken; - return this; - } - - public OAuth2AccessTokenResponseBuilder expiresAt(int expiresAt) { - this.expiresAt = expiresAt; - return this; - } - - public OAuth2AccessTokenResponseBuilder expiresIn(int expiresIn) { - this.expiresIn = expiresIn; - return this; - } - - public OAuth2AccessTokenResponseBuilder additonalParameters(Map additonalParameters) { - this.additonalParameters = additonalParameters; - return this; - } - - public OAuth2AccessTokenResponse build() { - return new OAuth2AccessTokenResponse(accessToken, expiresAt, expiresIn, additonalParameters); - } - - @Override - public String toString() { - return "OAuth2AccessTokenResponse.OAuth2AccessTokenResponseBuilder(accessToken=" + this.accessToken + ", expiresAt=" + this.expiresAt + ", expiresIn=" + this.expiresIn + ", additonalParameters=" + this.additonalParameters + ")"; - } - } -} diff --git a/token-client-core/src/main/java/no/nav/security/token/support/client/core/oauth2/OAuth2AccessTokenService.java b/token-client-core/src/main/java/no/nav/security/token/support/client/core/oauth2/OAuth2AccessTokenService.java deleted file mode 100644 index 1b06ca4d..00000000 --- a/token-client-core/src/main/java/no/nav/security/token/support/client/core/oauth2/OAuth2AccessTokenService.java +++ /dev/null @@ -1,149 +0,0 @@ -package no.nav.security.token.support.client.core.oauth2; - -import com.github.benmanes.caffeine.cache.Cache; -import no.nav.security.token.support.client.core.ClientProperties; -import no.nav.security.token.support.client.core.OAuth2ClientException; -import no.nav.security.token.support.client.core.OAuth2GrantType; -import no.nav.security.token.support.client.core.context.JwtBearerTokenResolver; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.Arrays; -import java.util.List; -import java.util.Optional; -import java.util.function.Function; - -@SuppressWarnings("WeakerAccess") -public class OAuth2AccessTokenService { - - private static final List SUPPORTED_GRANT_TYPES = Arrays.asList( - OAuth2GrantType.JWT_BEARER, - OAuth2GrantType.CLIENT_CREDENTIALS, - OAuth2GrantType.TOKEN_EXCHANGE - ); - private static final Logger log = LoggerFactory.getLogger(OAuth2AccessTokenService.class); - - private Cache clientCredentialsGrantCache; - private Cache onBehalfOfGrantCache; - private Cache exchangeGrantCache; - private final TokenExchangeClient tokenExchangeClient; - private final JwtBearerTokenResolver tokenResolver; - private final OnBehalfOfTokenClient onBehalfOfTokenClient; - private final ClientCredentialsTokenClient clientCredentialsTokenClient; - - public OAuth2AccessTokenService(JwtBearerTokenResolver tokenResolver, - OnBehalfOfTokenClient onBehalfOfTokenClient, - ClientCredentialsTokenClient clientCredentialsTokenClient, - TokenExchangeClient tokenExchangeClient) { - this.tokenResolver = tokenResolver; - this.onBehalfOfTokenClient = onBehalfOfTokenClient; - this.clientCredentialsTokenClient = clientCredentialsTokenClient; - this.tokenExchangeClient = tokenExchangeClient; - } - - private static OAuth2AccessTokenResponse getFromCacheIfEnabled( - T grantRequest, - Cache cache, - Function accessTokenResponseClient - ) { - if (cache != null) { - log.debug("cache is enabled so attempt to get from cache or update cache if not present."); - return cache.get(grantRequest, accessTokenResponseClient); - } else { - log.debug("cache is not set, invoke client directly"); - return accessTokenResponseClient.apply(grantRequest); - } - } - - @SuppressWarnings("unused") - public Cache getClientCredentialsGrantCache() { - return clientCredentialsGrantCache; - } - - public OAuth2AccessTokenResponse getAccessToken(ClientProperties clientProperties) { - if (clientProperties == null) { - throw new OAuth2ClientException("ClientProperties cannot be null"); - } - log.debug("getting access_token for grant={}", clientProperties.getGrantType()); - if (isGrantType(clientProperties, OAuth2GrantType.JWT_BEARER)) { - return executeOnBehalfOf(clientProperties); - } else if (isGrantType(clientProperties, OAuth2GrantType.CLIENT_CREDENTIALS)) { - return executeClientCredentials(clientProperties); - } else if (isGrantType(clientProperties, OAuth2GrantType.TOKEN_EXCHANGE)) { - return executeTokenExchange(clientProperties); - } else { - throw new OAuth2ClientException(String.format("invalid grant-type=%s from OAuth2ClientConfig.OAuth2Client" + - ". grant-type not in supported grant-types (%s)", - clientProperties.getGrantType().value(), SUPPORTED_GRANT_TYPES)); - } - } - - @SuppressWarnings("unused") - public Cache getOnBehalfOfGrantCache() { - return onBehalfOfGrantCache; - } - - public void setOnBehalfOfGrantCache(Cache onBehalfOfGrantCache) { - this.onBehalfOfGrantCache = onBehalfOfGrantCache; - } - - public void setClientCredentialsGrantCache(Cache clientCredentialsGrantCache) { - this.clientCredentialsGrantCache = clientCredentialsGrantCache; - } - - public void setExchangeGrantCache(Cache exchangeGrantCache) { - this.exchangeGrantCache = exchangeGrantCache; - } - - public Cache getExchangeGrantCache() { - return exchangeGrantCache; - } - - private OAuth2AccessTokenResponse executeOnBehalfOf(ClientProperties clientProperties) { - final var grantRequest = onBehalfOfGrantRequest(clientProperties); - return getFromCacheIfEnabled(grantRequest, onBehalfOfGrantCache, onBehalfOfTokenClient::getTokenResponse); - } - - private OAuth2AccessTokenResponse executeTokenExchange(ClientProperties clientProperties) { - final var grantRequest = tokenExchangeGrantRequest(clientProperties); - return getFromCacheIfEnabled(grantRequest, exchangeGrantCache, tokenExchangeClient::getTokenResponse); - } - - private OAuth2AccessTokenResponse executeClientCredentials(ClientProperties clientProperties) { - final var grantRequest = new ClientCredentialsGrantRequest(clientProperties); - return getFromCacheIfEnabled(grantRequest, clientCredentialsGrantCache, - clientCredentialsTokenClient::getTokenResponse); - } - - private boolean isGrantType(ClientProperties clientProperties, - OAuth2GrantType grantType) { - return Optional.ofNullable(clientProperties) - .filter(client -> client.getGrantType().equals(grantType)) - .isPresent(); - } - - private TokenExchangeGrantRequest tokenExchangeGrantRequest(ClientProperties clientProperties) { - return new TokenExchangeGrantRequest(clientProperties, tokenResolver.token() - .orElseThrow(() -> new OAuth2ClientException("no authenticated jwt token found in validation context, " + - "cannot do token exchange"))); - } - - private OnBehalfOfGrantRequest onBehalfOfGrantRequest(ClientProperties clientProperties) { - return new OnBehalfOfGrantRequest(clientProperties, tokenResolver.token() - .orElseThrow(() -> new OAuth2ClientException("no authenticated jwt token found in validation context, " + - "cannot do on-behalf-of"))); - } - - @Override - public String toString() { - return getClass().getSimpleName() + " [" + - " clientCredentialsGrantCache=" + clientCredentialsGrantCache + - ", onBehalfOfGrantCache=" + onBehalfOfGrantCache + - ", tokenExchangeClient=" + tokenExchangeClient + - ", tokenResolver=" + tokenResolver + - ", onBehalfOfTokenClient=" + onBehalfOfTokenClient + - ", clientCredentialsTokenClient=" + clientCredentialsTokenClient + - ", exchangeGrantCache=" + exchangeGrantCache + - "]"; - } -} diff --git a/token-client-core/src/main/java/no/nav/security/token/support/client/core/oauth2/OnBehalfOfGrantRequest.java b/token-client-core/src/main/java/no/nav/security/token/support/client/core/oauth2/OnBehalfOfGrantRequest.java deleted file mode 100644 index 32d2aec3..00000000 --- a/token-client-core/src/main/java/no/nav/security/token/support/client/core/oauth2/OnBehalfOfGrantRequest.java +++ /dev/null @@ -1,34 +0,0 @@ -package no.nav.security.token.support.client.core.oauth2; - -import no.nav.security.token.support.client.core.ClientProperties; - -import java.util.Objects; - -import static no.nav.security.token.support.client.core.OAuth2GrantType.JWT_BEARER; - -public class OnBehalfOfGrantRequest extends AbstractOAuth2GrantRequest { - private final String assertion; - - public OnBehalfOfGrantRequest(ClientProperties clientProperties, String assertion) { - super(JWT_BEARER, clientProperties); - this.assertion = assertion; - } - - String getAssertion() { - return assertion; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - if (!super.equals(o)) return false; - OnBehalfOfGrantRequest that = (OnBehalfOfGrantRequest) o; - return Objects.equals(assertion, that.assertion); - } - - @Override - public int hashCode() { - return Objects.hash(super.hashCode(), assertion); - } -} diff --git a/token-client-core/src/main/java/no/nav/security/token/support/client/core/oauth2/OnBehalfOfTokenClient.java b/token-client-core/src/main/java/no/nav/security/token/support/client/core/oauth2/OnBehalfOfTokenClient.java deleted file mode 100644 index f1981da7..00000000 --- a/token-client-core/src/main/java/no/nav/security/token/support/client/core/oauth2/OnBehalfOfTokenClient.java +++ /dev/null @@ -1,27 +0,0 @@ -package no.nav.security.token.support.client.core.oauth2; - -import no.nav.security.token.support.client.core.http.OAuth2HttpClient; - -import java.util.LinkedHashMap; -import java.util.Map; - -import static no.nav.security.token.support.client.core.OAuth2ParameterNames.ASSERTION; -import static no.nav.security.token.support.client.core.OAuth2ParameterNames.REQUESTED_TOKEN_USE; - -public class OnBehalfOfTokenClient extends AbstractOAuth2TokenClient { - - private static final String REQUESTED_TOKEN_USE_VALUE = "on_behalf_of"; - - public OnBehalfOfTokenClient(OAuth2HttpClient oAuth2HttpClient) { - super(oAuth2HttpClient); - } - - - @Override - protected Map formParameters(OnBehalfOfGrantRequest grantRequest) { - Map formParameters = new LinkedHashMap<>(); - formParameters.put(ASSERTION, grantRequest.getAssertion()); - formParameters.put(REQUESTED_TOKEN_USE, REQUESTED_TOKEN_USE_VALUE); - return formParameters; - } -} diff --git a/token-client-core/src/main/java/no/nav/security/token/support/client/core/oauth2/TokenExchangeClient.java b/token-client-core/src/main/java/no/nav/security/token/support/client/core/oauth2/TokenExchangeClient.java deleted file mode 100644 index 67729fde..00000000 --- a/token-client-core/src/main/java/no/nav/security/token/support/client/core/oauth2/TokenExchangeClient.java +++ /dev/null @@ -1,28 +0,0 @@ -package no.nav.security.token.support.client.core.oauth2; - -import no.nav.security.token.support.client.core.http.OAuth2HttpClient; - -import java.util.LinkedHashMap; -import java.util.Map; - -import static no.nav.security.token.support.client.core.OAuth2ParameterNames.*; - -public class TokenExchangeClient extends AbstractOAuth2TokenClient { - - public TokenExchangeClient(OAuth2HttpClient oAuth2HttpClient) { - super(oAuth2HttpClient); - } - - @Override - protected Map formParameters(TokenExchangeGrantRequest grantRequest) { - Map formParameters = new LinkedHashMap<>(); - var tokenExchangeProperties = grantRequest.getClientProperties().getTokenExchange(); - formParameters.put(SUBJECT_TOKEN_TYPE, tokenExchangeProperties.subjectTokenType()); - formParameters.put(SUBJECT_TOKEN, grantRequest.getSubjectToken()); - formParameters.put(AUDIENCE, tokenExchangeProperties.getAudience()); - if (tokenExchangeProperties.getResource() != null && !tokenExchangeProperties.getResource().isEmpty()) { - formParameters.put(RESOURCE, tokenExchangeProperties.getResource()); - } - return formParameters; - } -} diff --git a/token-client-core/src/main/java/no/nav/security/token/support/client/core/oauth2/TokenExchangeGrantRequest.java b/token-client-core/src/main/java/no/nav/security/token/support/client/core/oauth2/TokenExchangeGrantRequest.java deleted file mode 100644 index 9a5d8207..00000000 --- a/token-client-core/src/main/java/no/nav/security/token/support/client/core/oauth2/TokenExchangeGrantRequest.java +++ /dev/null @@ -1,35 +0,0 @@ -package no.nav.security.token.support.client.core.oauth2; - -import no.nav.security.token.support.client.core.ClientProperties; - -import java.util.Objects; - -import static no.nav.security.token.support.client.core.OAuth2GrantType.TOKEN_EXCHANGE; - -public class TokenExchangeGrantRequest extends AbstractOAuth2GrantRequest { - - private final String subjectToken; - @SuppressWarnings("WeakerAccess") - public TokenExchangeGrantRequest(ClientProperties clientProperties, String subjectToken) { - super(TOKEN_EXCHANGE, clientProperties); - this.subjectToken = subjectToken; - } - - public String getSubjectToken(){ - return this.subjectToken; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - if (!super.equals(o)) return false; - TokenExchangeGrantRequest that = (TokenExchangeGrantRequest) o; - return Objects.equals(subjectToken, that.subjectToken); - } - - @Override - public int hashCode() { - return Objects.hash(super.hashCode(), subjectToken); - } -} diff --git a/token-client-core/src/main/kotlin/no/nav/security/token/support/client/core/ClientAuthenticationProperties.kt b/token-client-core/src/main/kotlin/no/nav/security/token/support/client/core/ClientAuthenticationProperties.kt new file mode 100644 index 00000000..12e58dff --- /dev/null +++ b/token-client-core/src/main/kotlin/no/nav/security/token/support/client/core/ClientAuthenticationProperties.kt @@ -0,0 +1,47 @@ +package no.nav.security.token.support.client.core; + +import com.nimbusds.jose.jwk.RSAKey +import com.nimbusds.oauth2.sdk.auth.ClientAuthenticationMethod +import com.nimbusds.oauth2.sdk.auth.ClientAuthenticationMethod.CLIENT_SECRET_BASIC +import com.nimbusds.oauth2.sdk.auth.ClientAuthenticationMethod.CLIENT_SECRET_POST +import com.nimbusds.oauth2.sdk.auth.ClientAuthenticationMethod.PRIVATE_KEY_JWT +import no.nav.security.token.support.client.core.jwk.JwkFactory.fromJson +import no.nav.security.token.support.client.core.jwk.JwkFactory.fromJsonFile + +class ClientAuthenticationProperties @JvmOverloads constructor(val clientId: String, val clientAuthMethod: ClientAuthenticationMethod,val clientSecret: String?,val clientJwk: String? = null, val clientRsaKey: RSAKey? = loadKey(clientJwk)) { + + init { + require(clientAuthMethod in CLIENT_AUTH_METHODS) { + "Unsupported authentication method $clientAuthMethod, must be one of $CLIENT_AUTH_METHODS" + } + if (clientAuthMethod in listOf(CLIENT_SECRET_BASIC, CLIENT_SECRET_POST)) { + requireNotNull(clientSecret) { "Client secret must be set for authentication method $clientAuthMethod" } + } + if (PRIVATE_KEY_JWT.equals(clientAuthMethod)) { + requireNotNull(clientJwk) { "Client private key must be set for authentication method $clientAuthMethod" } + } + } + + + companion object { + private val CLIENT_AUTH_METHODS = listOf(CLIENT_SECRET_BASIC, CLIENT_SECRET_POST, PRIVATE_KEY_JWT) + + @JvmStatic + fun builder(clientId: String, clientAuthMethod: ClientAuthenticationMethod) = ClientAuthenticationPropertiesBuilder(clientId, clientAuthMethod) + private fun loadKey(clientJwk: String?) = + clientJwk?.let { + if (it.startsWith("{")) { + fromJson(it) + } else { + fromJsonFile(it) + } + } + } + +} + +class ClientAuthenticationPropertiesBuilder @JvmOverloads constructor(private val clientId: String, private val clientAuthMethod: ClientAuthenticationMethod, private var clientSecret: String? = null, private var clientJwk: String? = null) { + fun clientSecret(clientSecret: String)= this.also { it.clientSecret = clientSecret } + fun clientJwk(clientJwk: String)= this.also { it.clientJwk = clientJwk } + fun build() = ClientAuthenticationProperties(clientId, clientAuthMethod, clientSecret, clientJwk); +} \ No newline at end of file diff --git a/token-client-core/src/main/kotlin/no/nav/security/token/support/client/core/ClientProperties.kt b/token-client-core/src/main/kotlin/no/nav/security/token/support/client/core/ClientProperties.kt new file mode 100644 index 00000000..e5e566af --- /dev/null +++ b/token-client-core/src/main/kotlin/no/nav/security/token/support/client/core/ClientProperties.kt @@ -0,0 +1,74 @@ +package no.nav.security.token.support.client.core; + +import com.nimbusds.jose.util.DefaultResourceRetriever; +import com.nimbusds.oauth2.sdk.ParseException +import com.nimbusds.oauth2.sdk.`as`.AuthorizationServerMetadata +import java.io.IOException +import no.nav.security.token.support.client.core.OAuth2GrantType.Companion.CLIENT_CREDENTIALS +import no.nav.security.token.support.client.core.OAuth2GrantType.Companion.JWT_BEARER +import no.nav.security.token.support.client.core.OAuth2GrantType.Companion.TOKEN_EXCHANGE +import java.net.URI +class ClientProperties @JvmOverloads constructor(var tokenEndpointUrl: URI? = null, + private val wellKnownUrl: URI? = null, + val grantType: OAuth2GrantType, + val scope: List = emptyList(), + val authentication: ClientAuthenticationProperties, + val resourceUrl: URI? = null, + val tokenExchange: TokenExchangeProperties? = null) { + + + init { + require(grantType in GRANT_TYPES) { "Unsupported grantType $grantType, must be one of $GRANT_TYPES" } + tokenEndpointUrl = tokenEndpointUrl ?: endpointUrlFromMetadata(wellKnownUrl) + } + + + fun toBuilder() = + ClientPropertiesBuilder(grantType, authentication) + .tokenEndpointUrl(tokenEndpointUrl) + .wellKnownUrl(wellKnownUrl) + .scope(scope) + .resourceUrl(resourceUrl) + .tokenExchange(tokenExchange) + + companion object { + private val GRANT_TYPES = listOf(JWT_BEARER, CLIENT_CREDENTIALS, TOKEN_EXCHANGE) + + @JvmStatic + fun builder(grantType: OAuth2GrantType, authentication: ClientAuthenticationProperties) = ClientPropertiesBuilder(grantType, authentication) + + private fun endpointUrlFromMetadata(wellKnown: URI?) = + runCatching { + wellKnown?.let { AuthorizationServerMetadata.parse(DefaultResourceRetriever().retrieveResource(wellKnown.toURL()).content).tokenEndpointURI } + ?: throw OAuth2ClientException("Well known url cannot be null, please check your configuration") + }.getOrElse { + when(it) { + is ParseException-> throw OAuth2ClientException("Unable to parse response from $wellKnown", it) + is IOException -> throw OAuth2ClientException("Unable to read from $wellKnown", it) + is OAuth2ClientException -> throw it + else -> throw OAuth2ClientException("Unexpected error reading from $wellKnown", it) + } + } + } + + class ClientPropertiesBuilder @JvmOverloads constructor(private val grantType: OAuth2GrantType, val authentication: ClientAuthenticationProperties, + private var tokenEndpointUrl: URI? = null, + private var wellKnownUrl: URI? = null, + private var scope: List = emptyList(), + private var resourceUrl: URI? = null, + private var tokenExchange: TokenExchangeProperties? = null) { + + fun tokenEndpointUrl(endpointURI: URI?) = this.also { it.tokenEndpointUrl = endpointURI } + fun wellKnownUrl(wellKnownURI: URI?) = this.also { it.wellKnownUrl = wellKnownURI } + fun scope(scope: List) = this.also { it.scope = scope} + fun resourceUrl(resourceUrl: URI?) = this.also { it.resourceUrl = resourceUrl } + fun tokenExchange(tokenExchange: TokenExchangeProperties?) = this.also { it.tokenExchange = tokenExchange } + fun build() = ClientProperties(tokenEndpointUrl, wellKnownUrl, grantType, scope, authentication, resourceUrl, tokenExchange) + } + + + class TokenExchangeProperties @JvmOverloads constructor(val audience: String, var resource: String? = null) { + + fun subjectTokenType() = "urn:ietf:params:oauth:token-type:jwt" + } +} \ No newline at end of file diff --git a/token-client-core/src/main/kotlin/no/nav/security/token/support/client/core/OAuth2CacheFactory.kt b/token-client-core/src/main/kotlin/no/nav/security/token/support/client/core/OAuth2CacheFactory.kt new file mode 100644 index 00000000..530933df --- /dev/null +++ b/token-client-core/src/main/kotlin/no/nav/security/token/support/client/core/OAuth2CacheFactory.kt @@ -0,0 +1,27 @@ +package no.nav.security.token.support.client.core + +import com.github.benmanes.caffeine.cache.Caffeine +import com.github.benmanes.caffeine.cache.Expiry +import java.util.concurrent.TimeUnit.SECONDS +import no.nav.security.token.support.client.core.oauth2.OAuth2AccessTokenResponse + +object OAuth2CacheFactory { + + @JvmStatic + fun accessTokenResponseCache(maximumSize : Long, skewInSeconds : Long) = + // Evict based on a varying expiration policy + Caffeine.newBuilder() + .maximumSize(maximumSize) + .expireAfter(evictOnResponseExpiresIn(skewInSeconds)) + .build() + + private fun evictOnResponseExpiresIn(skewInSeconds : Long) : Expiry { + return object : Expiry { + override fun expireAfterCreate(key : T, response : OAuth2AccessTokenResponse, currentTime : Long) = + SECONDS.toNanos(if (response.expiresIn!! > skewInSeconds) response.expiresIn!! - skewInSeconds else response.expiresIn!!.toLong()) + + override fun expireAfterUpdate(key : T, response : OAuth2AccessTokenResponse, currentTime : Long, currentDuration : Long) = currentDuration + override fun expireAfterRead(key : T, response : OAuth2AccessTokenResponse, currentTime : Long, currentDuration : Long) = currentDuration + } + } +} \ No newline at end of file diff --git a/token-client-core/src/main/kotlin/no/nav/security/token/support/client/core/OAuth2ClientException.kt b/token-client-core/src/main/kotlin/no/nav/security/token/support/client/core/OAuth2ClientException.kt new file mode 100644 index 00000000..0ff328cc --- /dev/null +++ b/token-client-core/src/main/kotlin/no/nav/security/token/support/client/core/OAuth2ClientException.kt @@ -0,0 +1,3 @@ +package no.nav.security.token.support.client.core + +class OAuth2ClientException @JvmOverloads constructor (message : String?, cause : Throwable? = null) : RuntimeException(message, cause) \ No newline at end of file diff --git a/token-client-core/src/main/kotlin/no/nav/security/token/support/client/core/OAuth2GrantType.kt b/token-client-core/src/main/kotlin/no/nav/security/token/support/client/core/OAuth2GrantType.kt new file mode 100644 index 00000000..f12f371d --- /dev/null +++ b/token-client-core/src/main/kotlin/no/nav/security/token/support/client/core/OAuth2GrantType.kt @@ -0,0 +1,14 @@ +package no.nav.security.token.support.client.core + + data class OAuth2GrantType(@JvmField val value : String) { + fun value() = value + + companion object { + @JvmField + val JWT_BEARER = OAuth2GrantType("urn:ietf:params:oauth:grant-type:jwt-bearer") + @JvmField + val CLIENT_CREDENTIALS = OAuth2GrantType("client_credentials") + @JvmField + val TOKEN_EXCHANGE = OAuth2GrantType("urn:ietf:params:oauth:grant-type:token-exchange") + } +} \ No newline at end of file diff --git a/token-client-core/src/main/kotlin/no/nav/security/token/support/client/core/OAuth2ParameterNames.kt b/token-client-core/src/main/kotlin/no/nav/security/token/support/client/core/OAuth2ParameterNames.kt new file mode 100644 index 00000000..c2931220 --- /dev/null +++ b/token-client-core/src/main/kotlin/no/nav/security/token/support/client/core/OAuth2ParameterNames.kt @@ -0,0 +1,17 @@ +package no.nav.security.token.support.client.core + +object OAuth2ParameterNames { + + const val GRANT_TYPE = "grant_type" + const val CLIENT_ID = "client_id" + const val CLIENT_SECRET = "client_secret" + const val ASSERTION = "assertion" + const val REQUESTED_TOKEN_USE = "requested_token_use" + const val SCOPE = "scope" + const val CLIENT_ASSERTION_TYPE = "client_assertion_type" + const val CLIENT_ASSERTION = "client_assertion" + const val SUBJECT_TOKEN_TYPE = "subject_token_type" + const val SUBJECT_TOKEN = "subject_token" + const val AUDIENCE = "audience" + const val RESOURCE = "resource" +} \ No newline at end of file diff --git a/token-client-core/src/main/kotlin/no/nav/security/token/support/client/core/auth/ClientAssertion.kt b/token-client-core/src/main/kotlin/no/nav/security/token/support/client/core/auth/ClientAssertion.kt new file mode 100644 index 00000000..7349cbeb --- /dev/null +++ b/token-client-core/src/main/kotlin/no/nav/security/token/support/client/core/auth/ClientAssertion.kt @@ -0,0 +1,52 @@ +package no.nav.security.token.support.client.core.auth + +import com.nimbusds.jose.JOSEException +import com.nimbusds.jose.JOSEObjectType.* +import com.nimbusds.jose.JWSAlgorithm.* +import com.nimbusds.jose.JWSHeader +import com.nimbusds.jose.crypto.RSASSASigner +import com.nimbusds.jose.jwk.RSAKey +import com.nimbusds.jwt.JWTClaimsSet +import com.nimbusds.jwt.JWTClaimsSet.Builder +import com.nimbusds.jwt.SignedJWT +import java.net.URI +import java.time.Instant +import java.time.Instant.* +import java.util.Date +import java.util.UUID +import no.nav.security.token.support.client.core.ClientAuthenticationProperties + +class ClientAssertion(private val tokenEndpointUrl : URI, private val clientId : String, private val rsaKey : RSAKey, private val expiryInSeconds : Int) { + constructor(tokenEndpointUrl: URI, auth : ClientAuthenticationProperties) : this(tokenEndpointUrl, auth.clientId, auth.clientRsaKey!!, EXPIRY_IN_SECONDS) + + fun assertion() = + now().run { + createSignedJWT(rsaKey, Builder() + .audience(tokenEndpointUrl.toString()) + .expirationTime(Date.from(plusSeconds(expiryInSeconds.toLong()))) + .issuer(clientId) + .subject(clientId) + .claim("jti", UUID.randomUUID().toString()) + .notBeforeTime(Date.from(this)) + .issueTime(Date.from(this)) + .build()).serialize() + } + + fun assertionType() = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer" + + private fun createSignedJWT(rsaJwk : RSAKey, claimsSet : JWTClaimsSet) = + + runCatching { + SignedJWT(JWSHeader.Builder(RS256) + .keyID(rsaJwk.keyID) + .type(JWT).build(), claimsSet).apply { + sign(RSASSASigner(rsaJwk.toPrivateKey())) + } + }.getOrElse { + throw RuntimeException(it) + } + + companion object { + private const val EXPIRY_IN_SECONDS = 60 + } +} \ No newline at end of file diff --git a/token-client-core/src/main/kotlin/no/nav/security/token/support/client/core/context/JwtBearerTokenResolver.kt b/token-client-core/src/main/kotlin/no/nav/security/token/support/client/core/context/JwtBearerTokenResolver.kt new file mode 100644 index 00000000..65b48957 --- /dev/null +++ b/token-client-core/src/main/kotlin/no/nav/security/token/support/client/core/context/JwtBearerTokenResolver.kt @@ -0,0 +1,7 @@ +package no.nav.security.token.support.client.core.context + +import java.util.Optional + +fun interface JwtBearerTokenResolver { + fun token() : Optional +} \ No newline at end of file diff --git a/token-client-core/src/main/kotlin/no/nav/security/token/support/client/core/http/OAuth2HttpClient.kt b/token-client-core/src/main/kotlin/no/nav/security/token/support/client/core/http/OAuth2HttpClient.kt new file mode 100644 index 00000000..282f0cd1 --- /dev/null +++ b/token-client-core/src/main/kotlin/no/nav/security/token/support/client/core/http/OAuth2HttpClient.kt @@ -0,0 +1,8 @@ +package no.nav.security.token.support.client.core.http + +import no.nav.security.token.support.client.core.oauth2.OAuth2AccessTokenResponse + +interface OAuth2HttpClient { + + fun post(oAuth2HttpRequest : OAuth2HttpRequest) : OAuth2AccessTokenResponse? +} \ No newline at end of file diff --git a/token-client-core/src/main/kotlin/no/nav/security/token/support/client/core/http/OAuth2HttpHeaders.kt b/token-client-core/src/main/kotlin/no/nav/security/token/support/client/core/http/OAuth2HttpHeaders.kt new file mode 100644 index 00000000..76fe755f --- /dev/null +++ b/token-client-core/src/main/kotlin/no/nav/security/token/support/client/core/http/OAuth2HttpHeaders.kt @@ -0,0 +1,38 @@ +package no.nav.security.token.support.client.core.http + +import java.lang.String.CASE_INSENSITIVE_ORDER +import java.util.Objects +import java.util.TreeMap + +class OAuth2HttpHeaders (val headers : Map>) { + + override fun equals(other : Any?) : Boolean { + if (this === other) return true + if (other == null || javaClass != other.javaClass) return false + val that = other as OAuth2HttpHeaders + return headers == that.headers + } + + override fun hashCode() = Objects.hash(headers) + + override fun toString() = javaClass.getSimpleName() + " [headers=" + headers + "]" + + class Builder(private val headers : TreeMap> = TreeMap(CASE_INSENSITIVE_ORDER)) { + + fun header(name : String, value : String) = + this.also { + headers.computeIfAbsent(name) { ArrayList(1) } + .add(value) + } + + fun build() = of(headers) + } + + companion object { + @JvmStatic + fun of(headers : Map>) = OAuth2HttpHeaders(headers) + + @JvmStatic + fun builder() = Builder() + } +} \ No newline at end of file diff --git a/token-client-core/src/main/kotlin/no/nav/security/token/support/client/core/http/OAuth2HttpRequest.kt b/token-client-core/src/main/kotlin/no/nav/security/token/support/client/core/http/OAuth2HttpRequest.kt new file mode 100644 index 00000000..be3e92cb --- /dev/null +++ b/token-client-core/src/main/kotlin/no/nav/security/token/support/client/core/http/OAuth2HttpRequest.kt @@ -0,0 +1,31 @@ +package no.nav.security.token.support.client.core.http + +import java.net.URI +import java.util.Collections.unmodifiableMap + +class OAuth2HttpRequest (val tokenEndpointUrl : URI?, val oAuth2HttpHeaders : OAuth2HttpHeaders?, val formParameters : Map) { + + + class OAuth2HttpRequestBuilder @JvmOverloads constructor(private var tokenEndpointUrl : URI? = null, + private var oAuth2HttpHeaders : OAuth2HttpHeaders? = null, + private var formParameters: MutableMap = mutableMapOf()) { + fun tokenEndpointUrl(tokenEndpointUrl : URI?) = this.also { it.tokenEndpointUrl = tokenEndpointUrl } + + fun oAuth2HttpHeaders(oAuth2HttpHeaders : OAuth2HttpHeaders?) = this.also { it.oAuth2HttpHeaders = oAuth2HttpHeaders } + + fun formParameter(key : String, value : String) = this.also { formParameters[key] = value } + + fun formParameters(entries: Map): OAuth2HttpRequestBuilder = this.also { formParameters.putAll(entries) } + + fun build(): OAuth2HttpRequest = OAuth2HttpRequest(tokenEndpointUrl, oAuth2HttpHeaders, unmodifiableMap(formParameters)) + + @Override + override fun toString() = "OAuth2HttpRequest.OAuth2HttpRequestBuilder(tokenEndpointUrl=$tokenEndpointUrl, oAuth2HttpHeaders=$oAuth2HttpHeaders, entries=$formParameters" + + + } + companion object { + fun builder() = OAuth2HttpRequestBuilder() + + } +} \ No newline at end of file diff --git a/token-client-core/src/main/kotlin/no/nav/security/token/support/client/core/jwk/JwkFactory.kt b/token-client-core/src/main/kotlin/no/nav/security/token/support/client/core/jwk/JwkFactory.kt new file mode 100644 index 00000000..7fe06318 --- /dev/null +++ b/token-client-core/src/main/kotlin/no/nav/security/token/support/client/core/jwk/JwkFactory.kt @@ -0,0 +1,77 @@ +package no.nav.security.token.support.client.core.jwk + +import com.nimbusds.jose.jwk.JWKSet +import com.nimbusds.jose.jwk.RSAKey +import com.nimbusds.jose.jwk.RSAKey.Builder +import com.nimbusds.jose.util.Base64URL.* +import java.io.InputStream +import java.nio.charset.StandardCharsets.* +import java.nio.file.Files +import java.nio.file.Path +import java.security.KeyStore +import java.security.MessageDigest.* +import java.security.NoSuchAlgorithmException +import org.slf4j.LoggerFactory + +object JwkFactory { + + private val LOG = LoggerFactory.getLogger(JwkFactory::class.java) + @JvmStatic + fun fromJsonFile(filePath : String) = + runCatching { + LOG.debug("Attempting to read jwk from path: {}", Path.of(filePath).toAbsolutePath()) + fromJson(Files.readString(Path.of(filePath), UTF_8)) + }.getOrElse { + throw JwkInvalidException(it) + } + + + @JvmStatic + fun fromJson(jsonJwk : String) = + runCatching { + RSAKey.parse(jsonJwk) + }.getOrElse { + throw JwkInvalidException(it) + } + + + @JvmStatic + fun fromKeyStore(alias : String, keyStoreFile : InputStream, password : String) = + with(fromKeyStore(keyStoreFile, password).getKeyByKeyId(alias) as RSAKey) { + Builder(this) + .keyID(getX509CertSHA1Thumbprint(this)) + .build() + } + + private fun fromKeyStore(keyStoreFile : InputStream, password : String) = + runCatching { + KeyStore.getInstance("JKS").run { + with(password.toCharArray()) { + load(keyStoreFile, this) + JWKSet.load(this@run) { this } + } + } + }.getOrElse { + throw RuntimeException(it) + } + + + private fun getX509CertSHA1Thumbprint(rsaKey : RSAKey) : String? { + return runCatching { + rsaKey.parsedX509CertChain.stream() + .findFirst() + .orElse(null)?.let { createSHA1DigestBase64Url(it.encoded) } + }.getOrElse { + throw RuntimeException(it) + } + } + + private fun createSHA1DigestBase64Url(bytes : ByteArray) = + runCatching { + encode(getInstance("SHA-1").digest(bytes)).toString() + }.getOrElse { + throw RuntimeException(it) + } + + class JwkInvalidException(cause : Throwable) : RuntimeException(cause) +} \ No newline at end of file diff --git a/token-client-core/src/main/kotlin/no/nav/security/token/support/client/core/oauth2/AbstractOAuth2GrantRequest.kt b/token-client-core/src/main/kotlin/no/nav/security/token/support/client/core/oauth2/AbstractOAuth2GrantRequest.kt new file mode 100644 index 00000000..a2fb3812 --- /dev/null +++ b/token-client-core/src/main/kotlin/no/nav/security/token/support/client/core/oauth2/AbstractOAuth2GrantRequest.kt @@ -0,0 +1,18 @@ +package no.nav.security.token.support.client.core.oauth2 + +import java.util.Objects +import no.nav.security.token.support.client.core.ClientProperties +import no.nav.security.token.support.client.core.OAuth2GrantType + +abstract class AbstractOAuth2GrantRequest(val grantType : OAuth2GrantType, val clientProperties : ClientProperties) { + + override fun equals(other : Any?) : Boolean { + if (this === other) return true + if (other == null || javaClass != other.javaClass) return false + val that = other as AbstractOAuth2GrantRequest + return grantType == that.grantType && clientProperties == that.clientProperties + } + + override fun hashCode() = Objects.hash(grantType, clientProperties) + override fun toString() = javaClass.getSimpleName() + " [oAuth2GrantType=" + grantType + ", clientProperties=" + clientProperties + "]" +} \ No newline at end of file diff --git a/token-client-core/src/main/kotlin/no/nav/security/token/support/client/core/oauth2/AbstractOAuth2TokenClient.kt b/token-client-core/src/main/kotlin/no/nav/security/token/support/client/core/oauth2/AbstractOAuth2TokenClient.kt new file mode 100644 index 00000000..6bd3751d --- /dev/null +++ b/token-client-core/src/main/kotlin/no/nav/security/token/support/client/core/oauth2/AbstractOAuth2TokenClient.kt @@ -0,0 +1,99 @@ +package no.nav.security.token.support.client.core.oauth2 + +import com.nimbusds.oauth2.sdk.auth.ClientAuthenticationMethod +import com.nimbusds.oauth2.sdk.auth.ClientAuthenticationMethod.* +import java.lang.String.* +import java.nio.charset.StandardCharsets +import java.util.Base64 +import java.util.Optional +import no.nav.security.token.support.client.core.ClientProperties +import no.nav.security.token.support.client.core.OAuth2ClientException +import no.nav.security.token.support.client.core.OAuth2GrantType +import no.nav.security.token.support.client.core.OAuth2ParameterNames +import no.nav.security.token.support.client.core.auth.ClientAssertion +import no.nav.security.token.support.client.core.http.OAuth2HttpClient +import no.nav.security.token.support.client.core.http.OAuth2HttpHeaders +import no.nav.security.token.support.client.core.http.OAuth2HttpRequest + +abstract class AbstractOAuth2TokenClient internal constructor(private val oAuth2HttpClient : OAuth2HttpClient) { + + fun getTokenResponse(grantRequest : T) : OAuth2AccessTokenResponse? { + val clientProperties = grantRequest?.clientProperties ?: throw OAuth2ClientException("ClientProperties cannot be null") + return try { + val formParameters = createDefaultFormParameters(grantRequest) + formParameters.putAll(formParameters(grantRequest)) + val oAuth2HttpRequest = OAuth2HttpRequest.builder() + .tokenEndpointUrl(clientProperties.tokenEndpointUrl) + .oAuth2HttpHeaders(OAuth2HttpHeaders.of(tokenRequestHeaders(clientProperties))) + .formParameters(formParameters) + .build() + oAuth2HttpClient.post(oAuth2HttpRequest) + } + catch (e : Exception) { + if (e !is OAuth2ClientException) { + throw OAuth2ClientException("received exception $e when invoking token endpoint=${clientProperties.tokenEndpointUrl}", e) + } + throw e + } + } + + private fun tokenRequestHeaders(clientProperties : ClientProperties) : Map> { + val headers = HashMap>() + headers["Accept"] = listOf(CONTENT_TYPE_JSON) + headers["Content-Type"] = listOf(CONTENT_TYPE_FORM_URL_ENCODED) + val auth = clientProperties.authentication + if (CLIENT_SECRET_BASIC == auth.clientAuthMethod) { + headers["Authorization"] = listOf("Basic " + basicAuth(auth.clientId, auth.clientSecret!!)) + } + return headers + } + + fun createDefaultFormParameters(grantRequest : T) : MutableMap { + val clientProperties = grantRequest?.clientProperties ?: throw OAuth2ClientException("ClientProperties cannot be null") + val formParameters : MutableMap = LinkedHashMap(clientAuthenticationFormParameters(grantRequest)) + formParameters[OAuth2ParameterNames.GRANT_TYPE] = grantRequest.grantType.value() + if (clientProperties.grantType != OAuth2GrantType.TOKEN_EXCHANGE) { + formParameters[OAuth2ParameterNames.SCOPE] = join(" ", clientProperties.scope) + } + return formParameters + } + + private fun clientAuthenticationFormParameters(grantRequest : T) : Map { + val clientProperties = grantRequest!!.clientProperties + val formParameters : MutableMap = LinkedHashMap() + val auth = clientProperties.authentication + if (CLIENT_SECRET_POST == auth.clientAuthMethod) { + formParameters[OAuth2ParameterNames.CLIENT_ID] = auth.clientId + formParameters[OAuth2ParameterNames.CLIENT_SECRET] = auth.clientSecret!! + } + else if (PRIVATE_KEY_JWT == auth.clientAuthMethod) { + val clientAssertion = ClientAssertion(clientProperties.tokenEndpointUrl!!, auth) + formParameters[OAuth2ParameterNames.CLIENT_ID] = auth.clientId + formParameters[OAuth2ParameterNames.CLIENT_ASSERTION_TYPE] = clientAssertion.assertionType() + formParameters[OAuth2ParameterNames.CLIENT_ASSERTION] = clientAssertion.assertion() + } + return formParameters + } + + private fun basicAuth(username : String, password : String) : String { + val charset = StandardCharsets.UTF_8 + val encoder = charset.newEncoder() + return if (encoder.canEncode(username) && encoder.canEncode(password)) { + val credentialsString = "$username:$password" + val encodedBytes = Base64.getEncoder().encode(credentialsString.toByteArray(StandardCharsets.UTF_8)) + String(encodedBytes, StandardCharsets.UTF_8) + } + else { + throw IllegalArgumentException("Username or password contains characters that cannot be encoded to " + charset.displayName()) + } + } + + protected abstract fun formParameters(grantRequest : T) : Map + override fun toString() = javaClass.getSimpleName() + " [oAuth2HttpClient=" + oAuth2HttpClient + "]" + + companion object { + + private const val CONTENT_TYPE_FORM_URL_ENCODED = "application/x-www-form-urlencoded;charset=UTF-8" + private const val CONTENT_TYPE_JSON = "application/json;charset=UTF-8" + } +} \ No newline at end of file diff --git a/token-client-core/src/main/kotlin/no/nav/security/token/support/client/core/oauth2/ClientCredentialsGrantRequest.kt b/token-client-core/src/main/kotlin/no/nav/security/token/support/client/core/oauth2/ClientCredentialsGrantRequest.kt new file mode 100644 index 00000000..a0df8bc4 --- /dev/null +++ b/token-client-core/src/main/kotlin/no/nav/security/token/support/client/core/oauth2/ClientCredentialsGrantRequest.kt @@ -0,0 +1,6 @@ +package no.nav.security.token.support.client.core.oauth2 + +import no.nav.security.token.support.client.core.ClientProperties +import no.nav.security.token.support.client.core.OAuth2GrantType.Companion.CLIENT_CREDENTIALS + +class ClientCredentialsGrantRequest(clientProperties : ClientProperties) : AbstractOAuth2GrantRequest(CLIENT_CREDENTIALS, clientProperties) \ No newline at end of file diff --git a/token-client-core/src/main/kotlin/no/nav/security/token/support/client/core/oauth2/ClientCredentialsTokenClient.kt b/token-client-core/src/main/kotlin/no/nav/security/token/support/client/core/oauth2/ClientCredentialsTokenClient.kt new file mode 100644 index 00000000..14e787bd --- /dev/null +++ b/token-client-core/src/main/kotlin/no/nav/security/token/support/client/core/oauth2/ClientCredentialsTokenClient.kt @@ -0,0 +1,8 @@ +package no.nav.security.token.support.client.core.oauth2 + +import no.nav.security.token.support.client.core.http.OAuth2HttpClient + +class ClientCredentialsTokenClient(oAuth2HttpClient : OAuth2HttpClient) : AbstractOAuth2TokenClient(oAuth2HttpClient) { + + override fun formParameters(grantRequest : ClientCredentialsGrantRequest) = emptyMap() +} \ No newline at end of file diff --git a/token-client-core/src/main/kotlin/no/nav/security/token/support/client/core/oauth2/OAuth2AccessTokenResponse.kt b/token-client-core/src/main/kotlin/no/nav/security/token/support/client/core/oauth2/OAuth2AccessTokenResponse.kt new file mode 100644 index 00000000..ed784309 --- /dev/null +++ b/token-client-core/src/main/kotlin/no/nav/security/token/support/client/core/oauth2/OAuth2AccessTokenResponse.kt @@ -0,0 +1,7 @@ +package no.nav.security.token.support.client.core.oauth2 + + data class OAuth2AccessTokenResponse (var access_token : String? = null, var expires_at : Int? = null, var expires_in : Int? = 60, private val additionalParameters : Map = emptyMap()) { + val accessToken = access_token + val expiresAt = expires_at + val expiresIn = expires_in +} \ No newline at end of file diff --git a/token-client-core/src/main/kotlin/no/nav/security/token/support/client/core/oauth2/OAuth2AccessTokenService.kt b/token-client-core/src/main/kotlin/no/nav/security/token/support/client/core/oauth2/OAuth2AccessTokenService.kt new file mode 100644 index 00000000..30e957d8 --- /dev/null +++ b/token-client-core/src/main/kotlin/no/nav/security/token/support/client/core/oauth2/OAuth2AccessTokenService.kt @@ -0,0 +1,72 @@ +package no.nav.security.token.support.client.core.oauth2 + +import com.github.benmanes.caffeine.cache.Cache +import java.util.function.Function +import org.slf4j.LoggerFactory +import no.nav.security.token.support.client.core.ClientProperties +import no.nav.security.token.support.client.core.OAuth2ClientException +import no.nav.security.token.support.client.core.OAuth2GrantType.Companion.CLIENT_CREDENTIALS +import no.nav.security.token.support.client.core.OAuth2GrantType.Companion.JWT_BEARER +import no.nav.security.token.support.client.core.OAuth2GrantType.Companion.TOKEN_EXCHANGE +import no.nav.security.token.support.client.core.context.JwtBearerTokenResolver + +class OAuth2AccessTokenService @JvmOverloads constructor(private val tokenResolver : JwtBearerTokenResolver, + private val onBehalfOfTokenClient : OnBehalfOfTokenClient, + private val clientCredentialsTokenClient : ClientCredentialsTokenClient, + private val tokenExchangeClient : TokenExchangeClient, + var clientCredentialsGrantCache : Cache? = null, + var exchangeGrantCache : Cache? = null, + var onBehalfOfGrantCache : Cache? = null) { + + + + fun getAccessToken(clientProperties : ClientProperties) : OAuth2AccessTokenResponse? { + log.debug("Getting access_token for grant={}", clientProperties.grantType) + return when (clientProperties.grantType) { + JWT_BEARER -> executeOnBehalfOf(clientProperties) + CLIENT_CREDENTIALS -> executeClientCredentials(clientProperties) + TOKEN_EXCHANGE -> executeTokenExchange(clientProperties) + else -> throw OAuth2ClientException("invalid grant-type=${clientProperties.grantType.value()} from OAuth2ClientConfig.OAuth2Client. grant-type not in supported grant-types ($SUPPORTED_GRANT_TYPES)") + } + } + + private fun executeOnBehalfOf(clientProperties : ClientProperties) = + getFromCacheIfEnabled(onBehalfOfGrantRequest(clientProperties), onBehalfOfGrantCache, onBehalfOfTokenClient::getTokenResponse) + + private fun executeTokenExchange(clientProperties : ClientProperties) = + getFromCacheIfEnabled(tokenExchangeGrantRequest(clientProperties), exchangeGrantCache, tokenExchangeClient::getTokenResponse) + + private fun executeClientCredentials(clientProperties : ClientProperties) = + getFromCacheIfEnabled(ClientCredentialsGrantRequest(clientProperties), clientCredentialsGrantCache, clientCredentialsTokenClient::getTokenResponse) + + private fun tokenExchangeGrantRequest(clientProperties : ClientProperties) = + TokenExchangeGrantRequest(clientProperties, tokenResolver.token() + .orElseThrow { + OAuth2ClientException("no authenticated jwt token found in validation context, cannot do token exchange") + }) + + private fun onBehalfOfGrantRequest(clientProperties : ClientProperties) = + OnBehalfOfGrantRequest(clientProperties, tokenResolver.token() + .orElseThrow { + OAuth2ClientException("no authenticated jwt token found in validation context, cannot do on-behalf-of") + }) + + override fun toString() = + "${javaClass.getSimpleName()} [clientCredentialsGrantCache=$clientCredentialsGrantCache, onBehalfOfGrantCache=$onBehalfOfGrantCache, tokenExchangeClient=$tokenExchangeClient, tokenResolver=$tokenResolver, onBehalfOfTokenClient=$onBehalfOfTokenClient, clientCredentialsTokenClient=$clientCredentialsTokenClient, exchangeGrantCache=$exchangeGrantCache]" + companion object { + + private val SUPPORTED_GRANT_TYPES = listOf(JWT_BEARER, CLIENT_CREDENTIALS, TOKEN_EXCHANGE + ) + private val log = LoggerFactory.getLogger(OAuth2AccessTokenService::class.java) + private fun getFromCacheIfEnabled(grantRequest : T, cache : Cache?, + accessTokenResponseClient : Function) = + if (cache != null) { + log.debug("cache is enabled so attempt to get from cache or update cache if not present.") + cache[grantRequest, accessTokenResponseClient] + } + else { + log.debug("cache is not set, invoke client directly") + accessTokenResponseClient.apply(grantRequest) + } + } +} \ No newline at end of file diff --git a/token-client-core/src/main/kotlin/no/nav/security/token/support/client/core/oauth2/OnBehalfOfGrantRequest.kt b/token-client-core/src/main/kotlin/no/nav/security/token/support/client/core/oauth2/OnBehalfOfGrantRequest.kt new file mode 100644 index 00000000..fb68d0cb --- /dev/null +++ b/token-client-core/src/main/kotlin/no/nav/security/token/support/client/core/oauth2/OnBehalfOfGrantRequest.kt @@ -0,0 +1,19 @@ +package no.nav.security.token.support.client.core.oauth2 + +import java.util.Objects +import no.nav.security.token.support.client.core.ClientProperties +import no.nav.security.token.support.client.core.OAuth2GrantType +import no.nav.security.token.support.client.core.OAuth2GrantType.Companion.JWT_BEARER + +class OnBehalfOfGrantRequest(clientProperties : ClientProperties, val assertion : String) : AbstractOAuth2GrantRequest(JWT_BEARER, clientProperties) { + + override fun equals(other : Any?) : Boolean { + if (this === other) return true + if (other == null || javaClass != other.javaClass) return false + if (!super.equals(other)) return false + val that = other as OnBehalfOfGrantRequest + return assertion == that.assertion + } + + override fun hashCode() = Objects.hash(super.hashCode(), assertion) +} \ No newline at end of file diff --git a/token-client-core/src/main/kotlin/no/nav/security/token/support/client/core/oauth2/OnBehalfOfTokenClient.kt b/token-client-core/src/main/kotlin/no/nav/security/token/support/client/core/oauth2/OnBehalfOfTokenClient.kt new file mode 100644 index 00000000..fb0e66a8 --- /dev/null +++ b/token-client-core/src/main/kotlin/no/nav/security/token/support/client/core/oauth2/OnBehalfOfTokenClient.kt @@ -0,0 +1,18 @@ +package no.nav.security.token.support.client.core.oauth2 + +import no.nav.security.token.support.client.core.OAuth2ParameterNames.ASSERTION +import no.nav.security.token.support.client.core.OAuth2ParameterNames.REQUESTED_TOKEN_USE +import no.nav.security.token.support.client.core.http.OAuth2HttpClient + +class OnBehalfOfTokenClient(oAuth2HttpClient : OAuth2HttpClient) : AbstractOAuth2TokenClient(oAuth2HttpClient) { + + override fun formParameters(grantRequest : OnBehalfOfGrantRequest) = + LinkedHashMap().apply { + put(ASSERTION,grantRequest.assertion) + put(REQUESTED_TOKEN_USE,REQUESTED_TOKEN_USE_VALUE) + } + + companion object { + private const val REQUESTED_TOKEN_USE_VALUE = "on_behalf_of" + } +} \ No newline at end of file diff --git a/token-client-core/src/main/kotlin/no/nav/security/token/support/client/core/oauth2/TokenExchangeClient.kt b/token-client-core/src/main/kotlin/no/nav/security/token/support/client/core/oauth2/TokenExchangeClient.kt new file mode 100644 index 00000000..ac92a240 --- /dev/null +++ b/token-client-core/src/main/kotlin/no/nav/security/token/support/client/core/oauth2/TokenExchangeClient.kt @@ -0,0 +1,20 @@ +package no.nav.security.token.support.client.core.oauth2 + +import no.nav.security.token.support.client.core.OAuth2ParameterNames.AUDIENCE +import no.nav.security.token.support.client.core.OAuth2ParameterNames.RESOURCE +import no.nav.security.token.support.client.core.OAuth2ParameterNames.SUBJECT_TOKEN +import no.nav.security.token.support.client.core.OAuth2ParameterNames.SUBJECT_TOKEN_TYPE +import no.nav.security.token.support.client.core.http.OAuth2HttpClient + +class TokenExchangeClient(oAuth2HttpClient : OAuth2HttpClient) : AbstractOAuth2TokenClient(oAuth2HttpClient) { + + override fun formParameters(grantRequest : TokenExchangeGrantRequest) = + LinkedHashMap().apply { + grantRequest.clientProperties.tokenExchange.run { + put(SUBJECT_TOKEN_TYPE, this!!.subjectTokenType()) + put(SUBJECT_TOKEN,grantRequest.subjectToken) + put(AUDIENCE, audience) + resource?.takeIf { it.isNotEmpty() }?.let { put(RESOURCE, it) } + } + } +} \ No newline at end of file diff --git a/token-client-core/src/main/kotlin/no/nav/security/token/support/client/core/oauth2/TokenExchangeGrantRequest.kt b/token-client-core/src/main/kotlin/no/nav/security/token/support/client/core/oauth2/TokenExchangeGrantRequest.kt new file mode 100644 index 00000000..e91fab58 --- /dev/null +++ b/token-client-core/src/main/kotlin/no/nav/security/token/support/client/core/oauth2/TokenExchangeGrantRequest.kt @@ -0,0 +1,19 @@ +package no.nav.security.token.support.client.core.oauth2 + +import java.util.Objects +import no.nav.security.token.support.client.core.ClientProperties +import no.nav.security.token.support.client.core.OAuth2GrantType.Companion.TOKEN_EXCHANGE + +class TokenExchangeGrantRequest(clientProperties : ClientProperties, val subjectToken : String) : AbstractOAuth2GrantRequest(TOKEN_EXCHANGE, + clientProperties) { + + override fun equals(o : Any?) : Boolean { + if (this === o) return true + if (o == null || javaClass != o.javaClass) return false + if (!super.equals(o)) return false + val that = o as TokenExchangeGrantRequest + return subjectToken == that.subjectToken + } + + override fun hashCode() = Objects.hash(super.hashCode(), subjectToken) +} \ No newline at end of file diff --git a/token-client-core/src/test/java/no/nav/security/token/support/client/core/ClientAuthenticationPropertiesTest.java b/token-client-core/src/test/java/no/nav/security/token/support/client/core/ClientAuthenticationPropertiesTest.java deleted file mode 100644 index c6578905..00000000 --- a/token-client-core/src/test/java/no/nav/security/token/support/client/core/ClientAuthenticationPropertiesTest.java +++ /dev/null @@ -1,51 +0,0 @@ -package no.nav.security.token.support.client.core; - - -import com.nimbusds.oauth2.sdk.auth.ClientAuthenticationMethod; -import org.junit.jupiter.api.Test; - -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static org.junit.jupiter.api.Assertions.assertNotNull; - -class ClientAuthenticationPropertiesTest { - - @Test - void validAuthenticationProperties() { - assertNotNull(new ClientAuthenticationProperties( - "client", - null, - "secret", - null)); - assertNotNull(new ClientAuthenticationProperties( - "client", - ClientAuthenticationMethod.CLIENT_SECRET_POST, - "secret", - null)); - } - - @Test - void invalidAuthenticationProperties() { - assertThatExceptionOfType(IllegalArgumentException.class) - .isThrownBy(() -> instanceWith(ClientAuthenticationMethod.TLS_CLIENT_AUTH)); - assertThatExceptionOfType(IllegalArgumentException.class) - .isThrownBy(() -> instanceWith(ClientAuthenticationMethod.SELF_SIGNED_TLS_CLIENT_AUTH)); - assertThatExceptionOfType(IllegalArgumentException.class) - .isThrownBy(() -> instanceWith(ClientAuthenticationMethod.CLIENT_SECRET_JWT)); - assertThatExceptionOfType(IllegalArgumentException.class) - .isThrownBy(() -> instanceWith(ClientAuthenticationMethod.NONE)); - - assertThatExceptionOfType(IllegalArgumentException.class) - .isThrownBy(() -> ClientAuthenticationProperties.builder() - .clientAuthMethod(ClientAuthenticationMethod.NONE) - .build()); - } - - private static void instanceWith(ClientAuthenticationMethod clientAuthenticationMethod) { - new ClientAuthenticationProperties( - "client", - clientAuthenticationMethod, - "secret", - null); - } - -} diff --git a/token-client-core/src/test/java/no/nav/security/token/support/client/core/ClientPropertiesTest.java b/token-client-core/src/test/java/no/nav/security/token/support/client/core/ClientPropertiesTest.java deleted file mode 100644 index 8f25fb05..00000000 --- a/token-client-core/src/test/java/no/nav/security/token/support/client/core/ClientPropertiesTest.java +++ /dev/null @@ -1,100 +0,0 @@ -package no.nav.security.token.support.client.core; - -import com.nimbusds.oauth2.sdk.auth.ClientAuthenticationMethod; -import org.junit.jupiter.api.Test; - -import java.io.IOException; -import java.net.URI; -import java.util.List; - -import static no.nav.security.token.support.client.core.TestUtils.jsonResponse; -import static no.nav.security.token.support.client.core.TestUtils.withMockServer; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static org.junit.jupiter.api.Assertions.assertNotNull; - -class ClientPropertiesTest { - - private final String wellKnownJson = - """ - { - "issuer" : "https://someissuer", - "token_endpoint" : "https://someissuer/token", - "jwks_uri" : "https://someissuer/jwks", - "grant_types_supported" : [ "urn:ietf:params:oauth:grant-type:token-exchange" ], - "token_endpoint_auth_methods_supported" : [ "private_key_jwt" ], - "token_endpoint_auth_signing_alg_values_supported" : [ "RS256" ], - "subject_types_supported" : [ "public" ] - } - """; - - - private static ClientProperties clientPropertiesFromWellKnown(URI wellKnownUrl) { - return new ClientProperties( - null, - wellKnownUrl, - OAuth2GrantType.CLIENT_CREDENTIALS, - List.of("scope1", "scope2"), - clientAuth(), - null, - tokenExchange() - ); - } - - private static ClientAuthenticationProperties clientAuth() { - return new ClientAuthenticationProperties( - "client", - ClientAuthenticationMethod.CLIENT_SECRET_BASIC, - "secret", - null); - } - - private static ClientProperties.TokenExchangeProperties tokenExchange() { - return new ClientProperties.TokenExchangeProperties( - "aud1", - null - ); - } - - private static ClientProperties clientPropertiesFromGrantType(OAuth2GrantType grantType) { - return new ClientProperties( - URI.create("http://token"), - null, - grantType, - List.of("scope1", "scope2"), - clientAuth(), - null, - tokenExchange() - ); - } - - @Test - void validGrantTypes() { - assertNotNull(clientPropertiesFromGrantType(OAuth2GrantType.JWT_BEARER)); - assertNotNull(clientPropertiesFromGrantType(OAuth2GrantType.CLIENT_CREDENTIALS)); - assertNotNull(clientPropertiesFromGrantType(OAuth2GrantType.TOKEN_EXCHANGE)); - } - - @Test - void invalidGrantTypes() { - assertThatExceptionOfType(IllegalArgumentException.class) - .isThrownBy(() -> clientPropertiesFromGrantType(new OAuth2GrantType("somegrantNotSupported"))); - } - - @Test - void ifWellKnownUrlIsNotNullShouldRetrieveMetadataAndSetTokenEndpoint() throws IOException { - withMockServer( - s -> { - s.enqueue(jsonResponse(wellKnownJson)); - assertNotNull(clientPropertiesFromWellKnown(s.url("/well-known").uri()).getTokenEndpointUrl()); - } - ); - } - - @Test - void incorrectWellKnownUrlShouldThrowException(){ - assertThatExceptionOfType(OAuth2ClientException.class) - .isThrownBy(() -> - clientPropertiesFromWellKnown(URI.create("http://localhost:1234/notfound")) - ); - } -} diff --git a/token-client-core/src/test/java/no/nav/security/token/support/client/core/TestUtils.java b/token-client-core/src/test/java/no/nav/security/token/support/client/core/TestUtils.java deleted file mode 100644 index 683a67ae..00000000 --- a/token-client-core/src/test/java/no/nav/security/token/support/client/core/TestUtils.java +++ /dev/null @@ -1,110 +0,0 @@ -package no.nav.security.token.support.client.core; - -import com.nimbusds.jwt.JWT; -import com.nimbusds.jwt.JWTClaimsSet; -import com.nimbusds.jwt.PlainJWT; -import com.nimbusds.oauth2.sdk.auth.ClientAuthenticationMethod; -import okhttp3.mockwebserver.MockResponse; -import okhttp3.mockwebserver.MockWebServer; -import okhttp3.mockwebserver.RecordedRequest; - -import java.io.IOException; -import java.io.UnsupportedEncodingException; -import java.net.URI; -import java.net.URLEncoder; -import java.nio.charset.StandardCharsets; -import java.time.Instant; -import java.time.LocalDateTime; -import java.time.ZoneId; -import java.util.*; -import java.util.function.Consumer; - -import static org.assertj.core.api.Assertions.assertThat; - -@SuppressWarnings("WeakerAccess") -public class TestUtils { - - public static final String CONTENT_TYPE_FORM_URL_ENCODED = "application/x-www-form-urlencoded;charset=UTF-8"; - public static final String CONTENT_TYPE_JSON = "application/json;charset=UTF-8"; - - public static ClientProperties clientProperties(String tokenEndpointUrl, OAuth2GrantType oAuth2GrantType) { - return ClientProperties.builder() - .grantType(oAuth2GrantType) - .scope(List.of("scope1", "scope2")) - .tokenEndpointUrl(URI.create(tokenEndpointUrl)) - .authentication(ClientAuthenticationProperties.builder() - .clientAuthMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC) - .clientId("client1") - .clientSecret("clientSecret1") - .build()) - .build(); - } - - public static ClientProperties tokenExchangeClientProperties( - String tokenEndpointUrl, - OAuth2GrantType oAuth2GrantType, - String clientPrivateKey - ) { - return ClientProperties.builder() - .grantType(oAuth2GrantType) - .tokenEndpointUrl(URI.create(tokenEndpointUrl)) - .authentication(ClientAuthenticationProperties.builder() - .clientAuthMethod(ClientAuthenticationMethod.PRIVATE_KEY_JWT) - .clientId("client1") - .clientJwk(clientPrivateKey) - .build()) - .tokenExchange(ClientProperties.TokenExchangeProperties.builder() - .audience("audience1") - .build()) - .build(); - } - - public static void withMockServer(Consumer test) throws IOException { - MockWebServer server = new MockWebServer(); - server.start(); - test.accept(server); - server.shutdown(); - } - - public static MockResponse jsonResponse(String json) { - return new MockResponse() - .setHeader("Content-Type", "application/json;charset=UTF-8") - .setBody(json); - } - - public static void assertPostMethodAndJsonHeaders(RecordedRequest recordedRequest) { - assertThat(recordedRequest.getMethod()).isEqualTo("POST"); - assertThat(recordedRequest.getHeader("Accept")).isEqualTo(CONTENT_TYPE_JSON); - assertThat(recordedRequest.getHeader("Content-Type")).isEqualTo(CONTENT_TYPE_FORM_URL_ENCODED); - } - - public static String decodeBasicAuth(RecordedRequest recordedRequest) { - return Optional.ofNullable(recordedRequest.getHeaders().get("Authorization")) - .map(s -> s.split("Basic ")) - .filter(pair -> pair.length == 2) - .map(pair -> Base64.getDecoder().decode(pair[1])) - .map(bytes -> new String(bytes, StandardCharsets.UTF_8)) - .orElse(""); - } - - public static JWT jwt(String sub) { - Instant expiry = LocalDateTime.now().atZone(ZoneId.systemDefault()).plusSeconds(60).toInstant(); - return new PlainJWT(new JWTClaimsSet.Builder() - .subject(sub) - .audience("thisapi") - .issuer("someIssuer") - .expirationTime(Date.from(expiry)) - .claim("jti", UUID.randomUUID().toString()) - .build()); - } - - public static String encodeValue(String value) { - String encodedUrl = null; - try { - encodedUrl = URLEncoder.encode(value, StandardCharsets.UTF_8.toString()); - } catch (UnsupportedEncodingException e) { - e.printStackTrace(); - } - return encodedUrl; - } -} diff --git a/token-client-core/src/test/java/no/nav/security/token/support/client/core/auth/ClientAssertionTest.java b/token-client-core/src/test/java/no/nav/security/token/support/client/core/auth/ClientAssertionTest.java deleted file mode 100644 index 59337bb6..00000000 --- a/token-client-core/src/test/java/no/nav/security/token/support/client/core/auth/ClientAssertionTest.java +++ /dev/null @@ -1,67 +0,0 @@ -package no.nav.security.token.support.client.core.auth; - -import com.nimbusds.jose.JOSEException; -import com.nimbusds.jose.JOSEObjectType; -import com.nimbusds.jose.JWSAlgorithm; -import com.nimbusds.jose.JWSVerifier; -import com.nimbusds.jose.crypto.RSASSAVerifier; -import com.nimbusds.jwt.JWTClaimsSet; -import com.nimbusds.jwt.SignedJWT; -import com.nimbusds.oauth2.sdk.auth.ClientAuthenticationMethod; -import no.nav.security.token.support.client.core.ClientAuthenticationProperties; -import no.nav.security.token.support.client.core.ClientProperties; -import no.nav.security.token.support.client.core.OAuth2GrantType; -import org.junit.jupiter.api.Test; - -import java.net.URI; -import java.text.ParseException; -import java.time.Instant; -import java.util.Date; - -import static org.assertj.core.api.Assertions.assertThat; - -class ClientAssertionTest { - - @Test - void testCreateAssertion() throws ParseException, JOSEException { - ClientAuthenticationProperties clientAuth = ClientAuthenticationProperties.builder() - .clientJwk("src/test/resources/jwk.json") - .clientId("client1") - .clientAuthMethod(ClientAuthenticationMethod.PRIVATE_KEY_JWT) - .build(); - - ClientProperties clientProperties = ClientProperties.builder() - .grantType(OAuth2GrantType.CLIENT_CREDENTIALS) - .tokenEndpointUrl(URI.create("http://token")) - .authentication(clientAuth) - .build(); - - Instant now = Instant.now(); - - ClientAssertion clientAssertion = new ClientAssertion( - clientProperties.getTokenEndpointUrl(), - clientProperties.getAuthentication()); - - assertThat(clientAssertion).isNotNull(); - assertThat(clientAssertion.assertionType()).isEqualTo("urn:ietf:params:oauth:client-assertion-type:jwt-bearer"); - - String assertion = clientAssertion.assertion(); - assertThat(clientAssertion.assertion()).isNotNull(); - - SignedJWT signedJWT = SignedJWT.parse(assertion); - String keyId = clientProperties.getAuthentication().getClientRsaKey().getKeyID(); - assertThat(signedJWT.getHeader().getKeyID()).isEqualTo(keyId); - assertThat(signedJWT.getHeader().getType()).isEqualTo(JOSEObjectType.JWT); - assertThat(signedJWT.getHeader().getAlgorithm()).isEqualTo(JWSAlgorithm.RS256); - - JWSVerifier verifier = new RSASSAVerifier(clientAuth.getClientRsaKey()); - assertThat(signedJWT.verify(verifier)).isTrue(); - - JWTClaimsSet claims = signedJWT.getJWTClaimsSet(); - assertThat(claims.getSubject()).isEqualTo(clientAuth.getClientId()); - assertThat(claims.getIssuer()).isEqualTo(clientAuth.getClientId()); - assertThat(claims.getAudience()).containsExactly(clientProperties.getTokenEndpointUrl().toString()); - assertThat(claims.getExpirationTime()).isAfter(Date.from(now)); - assertThat(claims.getNotBeforeTime()).isBefore(claims.getExpirationTime()); - } -} diff --git a/token-client-core/src/test/java/no/nav/security/token/support/client/core/http/OAuth2HttpHeadersTest.java b/token-client-core/src/test/java/no/nav/security/token/support/client/core/http/OAuth2HttpHeadersTest.java deleted file mode 100644 index 3b41718c..00000000 --- a/token-client-core/src/test/java/no/nav/security/token/support/client/core/http/OAuth2HttpHeadersTest.java +++ /dev/null @@ -1,24 +0,0 @@ -package no.nav.security.token.support.client.core.http; - -import org.junit.jupiter.api.Test; - -import java.util.List; -import java.util.Map; - -import static org.assertj.core.api.Assertions.assertThat; - -class OAuth2HttpHeadersTest { - - @Test - void test() { - OAuth2HttpHeaders httpHeadersFromBuilder = OAuth2HttpHeaders.builder() - .header("header1", "header1value1") - .header("header1", "header1value2") - .build(); - OAuth2HttpHeaders httpHeadersFromOf = OAuth2HttpHeaders.of(Map.of("header1", List.of("header1value1", - "header1value2"))); - assertThat(httpHeadersFromBuilder).isEqualTo(httpHeadersFromOf); - assertThat(httpHeadersFromBuilder.headers()).hasSize(1); - assertThat(httpHeadersFromBuilder.headers()).isEqualTo(httpHeadersFromOf.headers()); - } -} diff --git a/token-client-core/src/test/java/no/nav/security/token/support/client/core/http/SimpleOAuth2HttpClient.java b/token-client-core/src/test/java/no/nav/security/token/support/client/core/http/SimpleOAuth2HttpClient.java deleted file mode 100644 index bad00b44..00000000 --- a/token-client-core/src/test/java/no/nav/security/token/support/client/core/http/SimpleOAuth2HttpClient.java +++ /dev/null @@ -1,66 +0,0 @@ -package no.nav.security.token.support.client.core.http; - -import com.fasterxml.jackson.databind.DeserializationFeature; -import com.fasterxml.jackson.databind.ObjectMapper; -import no.nav.security.token.support.client.core.oauth2.OAuth2AccessTokenResponse; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.IOException; -import java.net.URLEncoder; -import java.net.http.HttpClient; -import java.net.http.HttpRequest; -import java.net.http.HttpResponse; -import java.nio.charset.StandardCharsets; -import java.util.Optional; -import java.util.stream.Collectors; - -public class SimpleOAuth2HttpClient implements OAuth2HttpClient { - - private static final Logger log = LoggerFactory.getLogger(SimpleOAuth2HttpClient.class); - private final ObjectMapper objectMapper; - - public SimpleOAuth2HttpClient() { - this.objectMapper = new ObjectMapper() - .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); - } - - - @Override - public OAuth2AccessTokenResponse post(OAuth2HttpRequest oAuth2HttpRequest) { - try { - HttpRequest.Builder requestBuilder = HttpRequest.newBuilder(); - - oAuth2HttpRequest.getOAuth2HttpHeaders().headers().forEach((key, value) -> value.forEach(v -> requestBuilder.header(key, v))); - - String body = oAuth2HttpRequest.getFormParameters().entrySet().stream() - .map(entry -> entry.getKey() + "=" + URLEncoder.encode(entry.getValue(), StandardCharsets.UTF_8)) - .collect(Collectors.joining("&")); - - HttpRequest httpRequest = requestBuilder - .uri(oAuth2HttpRequest.getTokenEndpointUrl()) - .POST(HttpRequest.BodyPublishers.ofString(body)) - .build(); - - HttpResponse response = - HttpClient.newHttpClient().send(httpRequest, HttpResponse.BodyHandlers.ofString()); - - return objectMapper.readValue(bodyAsString(response), OAuth2AccessTokenResponse.class); - } catch (IOException | InterruptedException e) { - throw new RuntimeException(e); - } - } - - private String bodyAsString(HttpResponse response){ - if(response != null){ - log.debug("received response in client, body={}", response.body()); - return Optional.of(response) - .filter(r -> r.statusCode() == 200) - .map(HttpResponse::body) - .orElseThrow(() -> - new RuntimeException("received status code=" + response.statusCode() - + " and response body=" + response.body() + " from authorization server.")); - } - throw new RuntimeException("response cannot be null."); - } -} diff --git a/token-client-core/src/test/java/no/nav/security/token/support/client/core/jwk/JwkFactoryTest.java b/token-client-core/src/test/java/no/nav/security/token/support/client/core/jwk/JwkFactoryTest.java deleted file mode 100644 index 0df5f8ab..00000000 --- a/token-client-core/src/test/java/no/nav/security/token/support/client/core/jwk/JwkFactoryTest.java +++ /dev/null @@ -1,61 +0,0 @@ -package no.nav.security.token.support.client.core.jwk; - -import com.nimbusds.jose.jwk.RSAKey; -import com.nimbusds.jose.util.Base64URL; -import org.junit.jupiter.api.Test; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.IOException; -import java.io.InputStream; -import java.security.KeyStore; -import java.security.KeyStoreException; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.security.cert.Certificate; -import java.security.cert.CertificateException; - -import static org.assertj.core.api.Assertions.assertThat; - -class JwkFactoryTest { - - private static final String KEY_STORE_FILE = "/selfsigned.jks"; - private static final String ALIAS = "client_assertion"; - private static final Logger log = LoggerFactory.getLogger(JwkFactoryTest.class); - - @Test - void getKeyFromJwkFile() { - RSAKey rsaKey = JwkFactory.fromJsonFile("src/test/resources/jwk.json"); - assertThat(rsaKey.getKeyID()).isEqualTo("jlAX4HYKW4hyhZgSmUyOmVAqMUw"); - assertThat(rsaKey.isPrivate()).isTrue(); - assertThat(rsaKey.getPrivateExponent()).hasToString("J_mMSpq8k4WH9GKeS6d1kPVrQz2jDslAy3b3zrBuiSdNtKgUN7jFhGXaiY-cAg3efhMc-MWwPa0raKEN9xQRtIdbJurJbNG3viCvo_8FNs5lmFCUIktuO12zvsJS63q-i1zsZ7_esYQHbeDqg9S3q98c2EIO8lxQvPBcq-OIjdxfuanAEWJIRNuvNkK5I0AcqF_Q_KeFQDHo5sWUkwyPCaddd-ogS_YDeK3eeUpQbElrusdv0Ai0iYBPukzEHz1aL8PbaYru9f6Alor6yt9Lc_FNKfi-gnNFdpg3-uqVEh-MhEXgyN1RkeZzt0Kk9rylHumjSpwEgzuuA2L3WnycUQ"); - } - - @Test - void getKeyFromKeystore() { - RSAKey rsaKey = JwkFactory.fromKeyStore( - ALIAS, - JwkFactoryTest.class.getResourceAsStream(KEY_STORE_FILE), - "Test1234" - ); - assertThat(rsaKey.getKeyID()).isEqualTo(certificateThumbprintSHA1()); - assertThat(rsaKey.isPrivate()).isTrue(); - } - - private static String certificateThumbprintSHA1() { - try { - KeyStore keyStore = KeyStore.getInstance("JKS"); - keyStore.load(inputStream(KEY_STORE_FILE), "Test1234".toCharArray()); - Certificate cert = keyStore.getCertificate(ALIAS); - MessageDigest sha1 = MessageDigest.getInstance("SHA-1"); - return Base64URL.encode(sha1.digest(cert.getEncoded())).toString(); - } catch (KeyStoreException | IOException | CertificateException | NoSuchAlgorithmException e) { - throw new RuntimeException(e); - } - } - - private static InputStream inputStream(String resource) { - return JwkFactoryTest.class.getResourceAsStream(resource); - } - -} diff --git a/token-client-core/src/test/java/no/nav/security/token/support/client/core/oauth2/ClientCredentialsTokenClientTest.java b/token-client-core/src/test/java/no/nav/security/token/support/client/core/oauth2/ClientCredentialsTokenClientTest.java deleted file mode 100644 index 32ce6bff..00000000 --- a/token-client-core/src/test/java/no/nav/security/token/support/client/core/oauth2/ClientCredentialsTokenClientTest.java +++ /dev/null @@ -1,178 +0,0 @@ -package no.nav.security.token.support.client.core.oauth2; - -import com.nimbusds.oauth2.sdk.auth.ClientAuthenticationMethod; -import no.nav.security.token.support.client.core.ClientAuthenticationProperties; -import no.nav.security.token.support.client.core.ClientProperties; -import no.nav.security.token.support.client.core.OAuth2ClientException; -import no.nav.security.token.support.client.core.OAuth2GrantType; -import no.nav.security.token.support.client.core.http.SimpleOAuth2HttpClient; -import okhttp3.mockwebserver.MockWebServer; -import okhttp3.mockwebserver.RecordedRequest; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import java.io.IOException; - -import static no.nav.security.token.support.client.core.TestUtils.*; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; - -class ClientCredentialsTokenClientTest { - - private static final String TOKEN_RESPONSE = "{\n" + - " \"token_type\": \"Bearer\",\n" + - " \"scope\": \"scope1 scope2\",\n" + - " \"expires_at\": 1568141495,\n" + - " \"expires_in\": 3599,\n" + - " \"ext_expires_in\": 3599,\n" + - " \"access_token\": \"\",\n" + - " \"refresh_token\": \"\"\n" + - "}\n"; - - private static final String ERROR_RESPONSE = "{\"error\": \"some client error occurred\"}"; - - private String tokenEndpointUrl; - private MockWebServer server; - - private ClientCredentialsTokenClient client; - - @BeforeEach - void setup() throws IOException { - this.server = new MockWebServer(); - this.server.start(); - this.tokenEndpointUrl = this.server.url("/oauth2/v2/token").toString(); - this.client = new ClientCredentialsTokenClient(new SimpleOAuth2HttpClient()); - } - - @AfterEach - void cleanup() throws Exception { - this.server.shutdown(); - } - - @Test - void getTokenResponseWithDefaultClientAuthMethod() throws InterruptedException { - this.server.enqueue(jsonResponse(TOKEN_RESPONSE)); - ClientProperties clientProperties = clientProperties(tokenEndpointUrl, OAuth2GrantType.CLIENT_CREDENTIALS); - OAuth2AccessTokenResponse response = - client.getTokenResponse(new ClientCredentialsGrantRequest(clientProperties)); - RecordedRequest recordedRequest = this.server.takeRequest(); - assertPostMethodAndJsonHeaders(recordedRequest); - assertThatClientAuthMethodIsClientSecretBasic(recordedRequest, clientProperties); - String body = recordedRequest.getBody().readUtf8(); - assertThatRequestBodyContainsFormParameters(body); - assertThatResponseContainsAccessToken(response); - - } - - @Test - void getTokenResponseWithClientSecretBasic() throws InterruptedException { - this.server.enqueue(jsonResponse(TOKEN_RESPONSE)); - ClientProperties clientProperties = clientProperties(tokenEndpointUrl, OAuth2GrantType.CLIENT_CREDENTIALS); - OAuth2AccessTokenResponse response = - client.getTokenResponse(new ClientCredentialsGrantRequest(clientProperties)); - RecordedRequest recordedRequest = this.server.takeRequest(); - assertPostMethodAndJsonHeaders(recordedRequest); - assertThatClientAuthMethodIsClientSecretBasic(recordedRequest, clientProperties); - String body = recordedRequest.getBody().readUtf8(); - assertThatRequestBodyContainsFormParameters(body); - assertThatResponseContainsAccessToken(response); - - } - - @Test - void getTokenResponseWithClientSecretPost() throws InterruptedException { - this.server.enqueue(jsonResponse(TOKEN_RESPONSE)); - ClientProperties clientProperties = clientProperties(tokenEndpointUrl, OAuth2GrantType.CLIENT_CREDENTIALS) - .toBuilder() - .authentication(ClientAuthenticationProperties.builder() - .clientId("client") - .clientSecret("secret") - .clientAuthMethod(ClientAuthenticationMethod.CLIENT_SECRET_POST) - .build()) - .build(); - - OAuth2AccessTokenResponse response = - client.getTokenResponse(new ClientCredentialsGrantRequest(clientProperties)); - RecordedRequest recordedRequest = this.server.takeRequest(); - assertPostMethodAndJsonHeaders(recordedRequest); - String body = recordedRequest.getBody().readUtf8(); - assertThatClientAuthMethodIsClientSecretPost(body, clientProperties); - assertThatRequestBodyContainsFormParameters(body); - assertThatResponseContainsAccessToken(response); - - } - - @Test - void getTokenResponseWithPrivateKeyJwt() throws InterruptedException { - this.server.enqueue(jsonResponse(TOKEN_RESPONSE)); - ClientProperties clientProperties = clientProperties(tokenEndpointUrl, OAuth2GrantType.CLIENT_CREDENTIALS) - .toBuilder() - .authentication(ClientAuthenticationProperties.builder() - .clientId("client") - .clientAuthMethod(ClientAuthenticationMethod.PRIVATE_KEY_JWT) - .clientJwk("src/test/resources/jwk.json") - .build()) - .build(); - - OAuth2AccessTokenResponse response = - client.getTokenResponse(new ClientCredentialsGrantRequest(clientProperties)); - RecordedRequest recordedRequest = this.server.takeRequest(); - assertPostMethodAndJsonHeaders(recordedRequest); - String body = recordedRequest.getBody().readUtf8(); - assertThatClientAuthMethodIsPrivateKeyJwt(body, clientProperties); - assertThatRequestBodyContainsFormParameters(body); - assertThatResponseContainsAccessToken(response); - } - - @Test - void getTokenResponseError() { - this.server.enqueue(jsonResponse(ERROR_RESPONSE).setResponseCode(400)); - assertThatExceptionOfType(OAuth2ClientException.class) - .isThrownBy(() -> client.getTokenResponse(new ClientCredentialsGrantRequest(clientProperties( - tokenEndpointUrl, - OAuth2GrantType.CLIENT_CREDENTIALS - )))); - } - - private static void assertThatResponseContainsAccessToken(OAuth2AccessTokenResponse response) { - assertThat(response).isNotNull(); - assertThat(response.getAccessToken()).isNotBlank(); - assertThat(response.getExpiresAt()).isPositive(); - assertThat(response.getExpiresIn()).isPositive(); - } - - private static void assertThatClientAuthMethodIsPrivateKeyJwt( - String body, - ClientProperties clientProperties) { - ClientAuthenticationProperties auth = clientProperties.getAuthentication(); - assertThat(auth.getClientAuthMethod().getValue()).isEqualTo("private_key_jwt"); - assertThat(body).contains("client_id=" + encodeValue(auth.getClientId())); - assertThat(body).contains("client_assertion_type=" + encodeValue( - "urn:ietf:params:oauth:client-assertion-type:jwt-bearer")); - assertThat(body).contains("client_assertion=" + "ey"); - } - - private static void assertThatClientAuthMethodIsClientSecretPost( - String body, - ClientProperties clientProperties) { - ClientAuthenticationProperties auth = clientProperties.getAuthentication(); - assertThat(auth.getClientAuthMethod().getValue()).isEqualTo("client_secret_post"); - assertThat(body).contains("client_id=" + encodeValue(auth.getClientId())); - assertThat(body).contains("client_secret=" + encodeValue(auth.getClientSecret())); - } - - private static void assertThatClientAuthMethodIsClientSecretBasic(RecordedRequest recordedRequest, - ClientProperties clientProperties) { - ClientAuthenticationProperties auth = clientProperties.getAuthentication(); - assertThat(auth.getClientAuthMethod().getValue()).isEqualTo("client_secret_basic"); - assertThat(recordedRequest.getHeaders().get("Authorization")).isNotBlank(); - String usernamePwd = decodeBasicAuth(recordedRequest); - assertThat(usernamePwd).isEqualTo(auth.getClientId() + ":" + auth.getClientSecret()); - } - - private static void assertThatRequestBodyContainsFormParameters(String formParameters) { - assertThat(formParameters).contains("grant_type=client_credentials"); - assertThat(formParameters).contains("scope=scope1+scope2"); - } -} diff --git a/token-client-core/src/test/java/no/nav/security/token/support/client/core/oauth2/OAuth2AccessTokenServiceTest.java b/token-client-core/src/test/java/no/nav/security/token/support/client/core/oauth2/OAuth2AccessTokenServiceTest.java deleted file mode 100644 index 2f32aea4..00000000 --- a/token-client-core/src/test/java/no/nav/security/token/support/client/core/oauth2/OAuth2AccessTokenServiceTest.java +++ /dev/null @@ -1,280 +0,0 @@ -package no.nav.security.token.support.client.core.oauth2; - -import com.github.benmanes.caffeine.cache.Cache; -import com.nimbusds.jwt.JWT; -import com.nimbusds.jwt.JWTClaimsSet; -import com.nimbusds.jwt.PlainJWT; -import no.nav.security.token.support.client.core.ClientProperties; -import no.nav.security.token.support.client.core.OAuth2CacheFactory; -import no.nav.security.token.support.client.core.OAuth2ClientException; -import no.nav.security.token.support.client.core.OAuth2GrantType; -import no.nav.security.token.support.client.core.context.JwtBearerTokenResolver; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; - -import java.time.Instant; -import java.time.LocalDateTime; -import java.time.ZoneId; -import java.util.Arrays; -import java.util.Date; -import java.util.Optional; -import java.util.UUID; - -import static no.nav.security.token.support.client.core.TestUtils.clientProperties; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static org.mockito.Mockito.*; - - -class OAuth2AccessTokenServiceTest { - - @Mock - private OnBehalfOfTokenClient onBehalfOfTokenResponseClient; - @Mock - private ClientCredentialsTokenClient clientCredentialsTokenResponseClient; - @Mock - private TokenExchangeClient exchangeTokeResponseClient; - - @Mock - private JwtBearerTokenResolver assertionResolver; - - private OAuth2AccessTokenService oAuth2AccessTokenService; - - @BeforeEach - void setup() { - MockitoAnnotations.initMocks(this); - - Cache oboCache = - OAuth2CacheFactory.accessTokenResponseCache(10, 1); - Cache clientCredentialsCache = - OAuth2CacheFactory.accessTokenResponseCache(10, 1); - Cache exchangeTokenCache = - OAuth2CacheFactory.accessTokenResponseCache(10, 1); - - oAuth2AccessTokenService = new OAuth2AccessTokenService( - assertionResolver, - onBehalfOfTokenResponseClient, - clientCredentialsTokenResponseClient, - exchangeTokeResponseClient); - oAuth2AccessTokenService.setOnBehalfOfGrantCache(oboCache); - oAuth2AccessTokenService.setClientCredentialsGrantCache(clientCredentialsCache); - oAuth2AccessTokenService.setExchangeGrantCache(exchangeTokenCache); - } - - @Test - void getAccessTokenOnBehalfOf() { - ClientProperties clientProperties = onBehalfOfProperties(); - when(assertionResolver.token()).thenReturn(Optional.of(jwt("sub1").serialize())); - String firstAccessToken = "first_access_token"; - when(onBehalfOfTokenResponseClient.getTokenResponse(any(OnBehalfOfGrantRequest.class))) - .thenReturn(accessTokenResponse(firstAccessToken, 60)); - - OAuth2AccessTokenResponse oAuth2AccessTokenResponse1 = - oAuth2AccessTokenService.getAccessToken(clientProperties); - verify(onBehalfOfTokenResponseClient, times(1)).getTokenResponse(any(OnBehalfOfGrantRequest.class)); - assertThat(oAuth2AccessTokenResponse1).hasNoNullFieldsOrProperties(); - assertThat(oAuth2AccessTokenResponse1.getAccessToken()).isEqualTo("first_access_token"); - } - - @Test - void getAccessTokenClientCredentials() { - ClientProperties clientProperties = clientCredentialsProperties(); - - String firstAccessToken = "first_access_token"; - when(clientCredentialsTokenResponseClient.getTokenResponse(any(ClientCredentialsGrantRequest.class))) - .thenReturn(accessTokenResponse(firstAccessToken, 60)); - - OAuth2AccessTokenResponse oAuth2AccessTokenResponse1 = - oAuth2AccessTokenService.getAccessToken(clientProperties); - verify(clientCredentialsTokenResponseClient, times(1)).getTokenResponse(any(ClientCredentialsGrantRequest.class)); - assertThat(oAuth2AccessTokenResponse1).hasNoNullFieldsOrProperties(); - assertThat(oAuth2AccessTokenResponse1.getAccessToken()).isEqualTo("first_access_token"); - } - - private static ClientProperties exchangeProperties() { - return exchangeProperties("audience1"); - } - - @Test - void getAccessTokenOnBehalfOfNoAuthenticatedTokenFound() { - assertThatExceptionOfType(OAuth2ClientException.class) - .isThrownBy(() -> oAuth2AccessTokenService.getAccessToken(onBehalfOfProperties())) - .withMessageContaining("no authenticated jwt token found in validation context, cannot do on-behalf-of"); - } - - @Test - void getAccessTokenOnBehalfOf_WithCache_MultipleTimes_SameClientConfig() { - ClientProperties clientProperties = onBehalfOfProperties(); - - when(assertionResolver.token()).thenReturn(Optional.of(jwt("sub1").serialize())); - - //should invoke client and populate cache - String firstAccessToken = "first_access_token"; - when(onBehalfOfTokenResponseClient.getTokenResponse(any(OnBehalfOfGrantRequest.class))) - .thenReturn(accessTokenResponse(firstAccessToken, 60)); - - OAuth2AccessTokenResponse oAuth2AccessTokenResponse1 = - oAuth2AccessTokenService.getAccessToken(clientProperties); - verify(onBehalfOfTokenResponseClient, times(1)).getTokenResponse(any(OnBehalfOfGrantRequest.class)); - assertThat(oAuth2AccessTokenResponse1).hasNoNullFieldsOrProperties(); - assertThat(oAuth2AccessTokenResponse1.getAccessToken()).isEqualTo("first_access_token"); - - //should get response from cache and NOT invoke client - reset(onBehalfOfTokenResponseClient); - OAuth2AccessTokenResponse oAuth2AccessTokenResponse2 = - oAuth2AccessTokenService.getAccessToken(clientProperties); - verify(onBehalfOfTokenResponseClient, never()).getTokenResponse(any(OnBehalfOfGrantRequest.class)); - assertThat(oAuth2AccessTokenResponse2.getAccessToken()).isEqualTo("first_access_token"); - - //another user/token but same clientconfig, should invoke client and populate cache - reset(assertionResolver); - when(assertionResolver.token()).thenReturn(Optional.of(jwt("sub2").serialize())); - - reset(onBehalfOfTokenResponseClient); - String secondAccessToken = "second_access_token"; - when(onBehalfOfTokenResponseClient.getTokenResponse(any(OnBehalfOfGrantRequest.class))) - .thenReturn(accessTokenResponse(secondAccessToken, 60)); - OAuth2AccessTokenResponse oAuth2AccessTokenResponse3 = - oAuth2AccessTokenService.getAccessToken(clientProperties); - verify(onBehalfOfTokenResponseClient, times(1)).getTokenResponse(any(OnBehalfOfGrantRequest.class)); - assertThat(oAuth2AccessTokenResponse3.getAccessToken()).isEqualTo(secondAccessToken); - - } - - @Test - void getAccessTokenClientCredentials_WithCache_MultipleTimes() { - ClientProperties clientProperties = clientCredentialsProperties(); - - //should invoke client and populate cache - String firstAccessToken = "first_access_token"; - when(clientCredentialsTokenResponseClient.getTokenResponse(any(ClientCredentialsGrantRequest.class))) - .thenReturn(accessTokenResponse(firstAccessToken, 60)); - - OAuth2AccessTokenResponse oAuth2AccessTokenResponse1 = - oAuth2AccessTokenService.getAccessToken(clientProperties); - verify(clientCredentialsTokenResponseClient, times(1)).getTokenResponse(any(ClientCredentialsGrantRequest.class)); - assertThat(oAuth2AccessTokenResponse1).hasNoNullFieldsOrProperties(); - assertThat(oAuth2AccessTokenResponse1.getAccessToken()).isEqualTo("first_access_token"); - - //should get response from cache and NOT invoke client - reset(clientCredentialsTokenResponseClient); - OAuth2AccessTokenResponse oAuth2AccessTokenResponse2 = - oAuth2AccessTokenService.getAccessToken(clientProperties); - verify(clientCredentialsTokenResponseClient, never()).getTokenResponse(any(ClientCredentialsGrantRequest.class)); - assertThat(oAuth2AccessTokenResponse2.getAccessToken()).isEqualTo("first_access_token"); - - //another clientconfig, should invoke client and populate cache - clientProperties = clientCredentialsProperties("scope3"); - - reset(clientCredentialsTokenResponseClient); - String secondAccessToken = "second_access_token"; - when(clientCredentialsTokenResponseClient.getTokenResponse(any(ClientCredentialsGrantRequest.class))) - .thenReturn(accessTokenResponse(secondAccessToken, 60)); - OAuth2AccessTokenResponse oAuth2AccessTokenResponse3 = - oAuth2AccessTokenService.getAccessToken(clientProperties); - verify(clientCredentialsTokenResponseClient, times(1)).getTokenResponse(any(ClientCredentialsGrantRequest.class)); - assertThat(oAuth2AccessTokenResponse3.getAccessToken()).isEqualTo(secondAccessToken); - - } - - @Test - void testCacheEntryIsEvictedOnExpiry() throws InterruptedException { - ClientProperties clientProperties = onBehalfOfProperties(); - when(assertionResolver.token()).thenReturn(Optional.of(jwt("sub1").serialize())); - - //should invoke client and populate cache - String firstAccessToken = "first_access_token"; - when(onBehalfOfTokenResponseClient.getTokenResponse(any(OnBehalfOfGrantRequest.class))) - .thenReturn(accessTokenResponse(firstAccessToken, 1)); - - OAuth2AccessTokenResponse oAuth2AccessTokenResponse1 = - oAuth2AccessTokenService.getAccessToken(clientProperties); - verify(onBehalfOfTokenResponseClient, times(1)).getTokenResponse(any(OnBehalfOfGrantRequest.class)); - assertThat(oAuth2AccessTokenResponse1).hasNoNullFieldsOrProperties(); - assertThat(oAuth2AccessTokenResponse1.getAccessToken()).isEqualTo("first_access_token"); - - Thread.sleep(1000); - - //entry should be missing from cache due to expiry - reset(onBehalfOfTokenResponseClient); - String secondAccessToken = "second_access_token"; - when(onBehalfOfTokenResponseClient.getTokenResponse(any(OnBehalfOfGrantRequest.class))) - .thenReturn(accessTokenResponse(secondAccessToken, 1)); - OAuth2AccessTokenResponse oAuth2AccessTokenResponse2 = - oAuth2AccessTokenService.getAccessToken(clientProperties); - verify(onBehalfOfTokenResponseClient, times(1)).getTokenResponse(any(OnBehalfOfGrantRequest.class)); - assertThat(oAuth2AccessTokenResponse2.getAccessToken()).isEqualTo(secondAccessToken); - } - - private static JWT jwt(String sub) { - Instant expiry = LocalDateTime.now().atZone(ZoneId.systemDefault()).plusSeconds(60).toInstant(); - return new PlainJWT(new JWTClaimsSet.Builder() - .subject(sub) - .audience("thisapi") - .issuer("someIssuer") - .expirationTime(Date.from(expiry)) - .claim("jti", UUID.randomUUID().toString()) - .build()); - } - - private static ClientProperties clientCredentialsProperties() { - return clientCredentialsProperties("scope1", "scope2"); - } - - private static ClientProperties clientCredentialsProperties(String... scope) { - return clientProperties("http://token", OAuth2GrantType.CLIENT_CREDENTIALS) - .toBuilder() - .scope(Arrays.asList(scope)) - .build(); - } - - private static ClientProperties exchangeProperties(String audience) { - return clientProperties("http://token", OAuth2GrantType.TOKEN_EXCHANGE) - .toBuilder() - .tokenExchange( - ClientProperties.TokenExchangeProperties.builder() - .audience(audience) - .build()) - .build(); - } - - @Test - void getAccessTokenExchange() { - ClientProperties clientProperties = exchangeProperties(); - when(assertionResolver.token()).thenReturn(Optional.of(jwt("sub1").serialize())); - String firstAccessToken = "first_access_token"; - when(exchangeTokeResponseClient.getTokenResponse(any(TokenExchangeGrantRequest.class))) - .thenReturn(accessTokenResponse(firstAccessToken, 60)); - - OAuth2AccessTokenResponse oAuth2AccessTokenResponse1 = - oAuth2AccessTokenService.getAccessToken(clientProperties); - verify(exchangeTokeResponseClient, times(1)).getTokenResponse(any(TokenExchangeGrantRequest.class)); - assertThat(oAuth2AccessTokenResponse1).hasNoNullFieldsOrProperties(); - assertThat(oAuth2AccessTokenResponse1.getAccessToken()).isEqualTo("first_access_token"); - } - - private static ClientProperties onBehalfOfProperties() { - return clientProperties("http://token", OAuth2GrantType.JWT_BEARER); - } - - private static OAuth2AccessTokenResponse accessTokenResponse(String assertion, int expiresIn) { - return new OAuth2AccessTokenResponse() { - @Override - public String getAccessToken() { - return assertion; - } - - @Override - public int getExpiresAt() { - return Math.toIntExact((Instant.now().plusSeconds(expiresIn).getEpochSecond())); - } - - @Override - public int getExpiresIn() { - return expiresIn; - } - }; - } -} diff --git a/token-client-core/src/test/java/no/nav/security/token/support/client/core/oauth2/OnBehalfOfTokenClientTest.java b/token-client-core/src/test/java/no/nav/security/token/support/client/core/oauth2/OnBehalfOfTokenClientTest.java deleted file mode 100644 index ce6867a9..00000000 --- a/token-client-core/src/test/java/no/nav/security/token/support/client/core/oauth2/OnBehalfOfTokenClientTest.java +++ /dev/null @@ -1,89 +0,0 @@ -package no.nav.security.token.support.client.core.oauth2; - -import no.nav.security.token.support.client.core.ClientProperties; -import no.nav.security.token.support.client.core.OAuth2ClientException; -import no.nav.security.token.support.client.core.OAuth2GrantType; -import no.nav.security.token.support.client.core.http.SimpleOAuth2HttpClient; -import okhttp3.mockwebserver.MockWebServer; -import okhttp3.mockwebserver.RecordedRequest; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.mockito.MockitoAnnotations; - -import java.io.IOException; -import java.net.URLEncoder; -import java.nio.charset.StandardCharsets; - -import static no.nav.security.token.support.client.core.TestUtils.*; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; - -class OnBehalfOfTokenClientTest { - - private static final String TOKEN_RESPONSE = "{\n" + - " \"token_type\": \"Bearer\",\n" + - " \"scope\": \"scope1 scope2\",\n" + - " \"expires_at\": 1568141495,\n" + - " \"ext_expires_in\": 3599,\n" + - " \"expires_in\": 3599,\n" + - " \"access_token\": \"\",\n" + - " \"refresh_token\": \"\"\n" + - "}\n"; - - private static final String ERROR_RESPONSE = "{\"error\": \"some client error occurred\"}"; - - private static final String TOKEN_ENDPOINT = "/oauth2/v2.0/token"; - private OnBehalfOfTokenClient onBehalfOfTokenResponseClient; - private String tokenEndpointUrl; - private MockWebServer server; - - @BeforeEach - void setup() throws IOException { - MockitoAnnotations.initMocks(this); - this.server = new MockWebServer(); - this.server.start(); - this.tokenEndpointUrl = this.server.url(TOKEN_ENDPOINT).toString(); - onBehalfOfTokenResponseClient = new OnBehalfOfTokenClient(new SimpleOAuth2HttpClient()); - } - - @AfterEach - void teardown() throws IOException { - server.shutdown(); - } - - @Test - void getTokenResponse() throws InterruptedException { - this.server.enqueue(jsonResponse(TOKEN_RESPONSE)); - String assertion = jwt("sub1").serialize(); - ClientProperties clientProperties = clientProperties(this.tokenEndpointUrl, OAuth2GrantType.JWT_BEARER); - OnBehalfOfGrantRequest oAuth2OnBehalfOfGrantRequest = new OnBehalfOfGrantRequest(clientProperties, assertion); - OAuth2AccessTokenResponse response = - onBehalfOfTokenResponseClient.getTokenResponse(oAuth2OnBehalfOfGrantRequest); - - RecordedRequest recordedRequest = server.takeRequest(); - assertPostMethodAndJsonHeaders(recordedRequest); - String formParameters = recordedRequest.getBody().readUtf8(); - assertThat(formParameters).contains("grant_type=" + URLEncoder.encode(OAuth2GrantType.JWT_BEARER.value(), - StandardCharsets.UTF_8)) - .contains("scope=scope1+scope2") - .contains("requested_token_use=on_behalf_of") - .contains("assertion=" + assertion); - - assertThat(response).isNotNull(); - assertThat(response.getAccessToken()).isNotBlank(); - assertThat(response.getExpiresAt()).isPositive(); - assertThat(response.getExpiresIn()).isPositive(); - } - - @Test - void getTokenResponseWithError() { - this.server.enqueue(jsonResponse(ERROR_RESPONSE).setResponseCode(400)); - String assertion = jwt("sub1").serialize(); - ClientProperties clientProperties = clientProperties(this.tokenEndpointUrl, OAuth2GrantType.JWT_BEARER); - OnBehalfOfGrantRequest oAuth2OnBehalfOfGrantRequest = new OnBehalfOfGrantRequest(clientProperties, assertion); - assertThatExceptionOfType(OAuth2ClientException.class) - .isThrownBy(() -> onBehalfOfTokenResponseClient.getTokenResponse(oAuth2OnBehalfOfGrantRequest)) - .withMessageContaining(ERROR_RESPONSE); - } -} diff --git a/token-client-core/src/test/java/no/nav/security/token/support/client/core/oauth2/TokenExchangeClientTest.java b/token-client-core/src/test/java/no/nav/security/token/support/client/core/oauth2/TokenExchangeClientTest.java deleted file mode 100644 index 15ac88bc..00000000 --- a/token-client-core/src/test/java/no/nav/security/token/support/client/core/oauth2/TokenExchangeClientTest.java +++ /dev/null @@ -1,112 +0,0 @@ -package no.nav.security.token.support.client.core.oauth2; - -import com.nimbusds.oauth2.sdk.auth.ClientAuthenticationMethod; -import no.nav.security.token.support.client.core.*; -import no.nav.security.token.support.client.core.http.SimpleOAuth2HttpClient; -import okhttp3.mockwebserver.MockWebServer; -import okhttp3.mockwebserver.RecordedRequest; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import java.io.IOException; - -import static no.nav.security.token.support.client.core.TestUtils.*; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; - -class TokenExchangeClientTest { - - private static final String TOKEN_RESPONSE = "{\n" + - " \"token_type\": \"Bearer\",\n" + - " \"scope\": \"scope1 scope2\",\n" + - " \"expires_at\": 1568141495,\n" + - " \"expires_in\": 3599,\n" + - " \"ext_expires_in\": 3599,\n" + - " \"access_token\": \"\"\n" + - "}\n"; - - private static final String ERROR_RESPONSE = "{\"error\": \"some client error occurred\"}"; - - private String tokenEndpointUrl; - private MockWebServer server; - - private TokenExchangeClient tokenExchangeClient; - - private String subjectToken; - - @BeforeEach - void setup() throws IOException { - this.server = new MockWebServer(); - this.server.start(); - this.tokenEndpointUrl = this.server.url("/oauth2/v2/token").toString(); - this.tokenExchangeClient = new TokenExchangeClient(new SimpleOAuth2HttpClient()); - this.subjectToken = jwt("somesub").serialize(); - } - - @AfterEach - void cleanup() throws Exception { - this.server.shutdown(); - } - - @Test - void getTokenResponseWithPrivateKeyJwtAndExchangeProperties() throws InterruptedException { - this.server.enqueue(jsonResponse(TOKEN_RESPONSE)); - ClientProperties clientProperties = tokenExchangeClientProperties( - tokenEndpointUrl, - OAuth2GrantType.TOKEN_EXCHANGE, - "src/test/resources/jwk.json" - ) - .toBuilder() - .authentication(ClientAuthenticationProperties.builder() - .clientId("client") - .clientAuthMethod(ClientAuthenticationMethod.PRIVATE_KEY_JWT) - .clientJwk("src/test/resources/jwk.json") - .build()) - .build(); - - OAuth2AccessTokenResponse response = - tokenExchangeClient.getTokenResponse(new TokenExchangeGrantRequest(clientProperties, subjectToken)); - RecordedRequest recordedRequest = this.server.takeRequest(); - assertPostMethodAndJsonHeaders(recordedRequest); - String body = recordedRequest.getBody().readUtf8(); - assertThatClientAuthMethodIsPrivateKeyJwt(body, clientProperties); - assertThatRequestBodyContainsTokenExchangeFormParameters(body); - assertThatResponseContainsAccessToken(response); - } - - @Test - void getTokenResponseError() { - this.server.enqueue(jsonResponse(ERROR_RESPONSE).setResponseCode(400)); - assertThatExceptionOfType(OAuth2ClientException.class) - .isThrownBy(() -> tokenExchangeClient.getTokenResponse(new TokenExchangeGrantRequest(clientProperties( - tokenEndpointUrl, - OAuth2GrantType.TOKEN_EXCHANGE - ), subjectToken))); - } - - private static void assertThatResponseContainsAccessToken(OAuth2AccessTokenResponse response) { - assertThat(response).isNotNull(); - assertThat(response.getAccessToken()).isNotBlank(); - assertThat(response.getExpiresAt()).isPositive(); - assertThat(response.getExpiresIn()).isPositive(); - } - - private static void assertThatClientAuthMethodIsPrivateKeyJwt( - String body, - ClientProperties clientProperties) { - ClientAuthenticationProperties auth = clientProperties.getAuthentication(); - assertThat(auth.getClientAuthMethod().getValue()).isEqualTo("private_key_jwt"); - assertThat(body).contains("client_id=" + encodeValue(auth.getClientId())); - assertThat(body).contains("client_assertion_type=" + encodeValue( - "urn:ietf:params:oauth:client-assertion-type:jwt-bearer")); - assertThat(body).contains("client_assertion=" + "ey"); - } - - private void assertThatRequestBodyContainsTokenExchangeFormParameters(String formParameters) { - assertThat(formParameters).contains(OAuth2ParameterNames.GRANT_TYPE + "=" + encodeValue(OAuth2GrantType.TOKEN_EXCHANGE.value())); - assertThat(formParameters).contains(OAuth2ParameterNames.AUDIENCE + "=" + "audience1"); - assertThat(formParameters).contains(OAuth2ParameterNames.SUBJECT_TOKEN_TYPE + "=" + encodeValue("urn:ietf:params:oauth:token-type:jwt")); - assertThat(formParameters).contains(OAuth2ParameterNames.SUBJECT_TOKEN + "=" + subjectToken); - } -} diff --git a/token-client-core/src/test/kotlin/no/nav/security/token/support/client/core/ClientAuthenticationPropertiesTest.kt b/token-client-core/src/test/kotlin/no/nav/security/token/support/client/core/ClientAuthenticationPropertiesTest.kt new file mode 100644 index 00000000..a37dec67 --- /dev/null +++ b/token-client-core/src/test/kotlin/no/nav/security/token/support/client/core/ClientAuthenticationPropertiesTest.kt @@ -0,0 +1,37 @@ +package no.nav.security.token.support.client.core + +import com.nimbusds.oauth2.sdk.auth.ClientAuthenticationMethod +import org.assertj.core.api.Assertions +import org.junit.jupiter.api.Test +import no.nav.security.token.support.client.core.ClientAuthenticationProperties.Companion.builder + +internal class ClientAuthenticationPropertiesTest { + + @Test + fun invalidAuthenticationProperties() { + Assertions.assertThatExceptionOfType(IllegalArgumentException::class.java) + .isThrownBy { instanceWith(ClientAuthenticationMethod.TLS_CLIENT_AUTH) } + Assertions.assertThatExceptionOfType(IllegalArgumentException::class.java) + .isThrownBy { instanceWith(ClientAuthenticationMethod.SELF_SIGNED_TLS_CLIENT_AUTH) } + Assertions.assertThatExceptionOfType(IllegalArgumentException::class.java) + .isThrownBy { instanceWith(ClientAuthenticationMethod.CLIENT_SECRET_JWT) } + Assertions.assertThatExceptionOfType(IllegalArgumentException::class.java) + .isThrownBy { instanceWith(ClientAuthenticationMethod.NONE) } + Assertions.assertThatExceptionOfType(IllegalArgumentException::class.java) + .isThrownBy { + builder("client1", ClientAuthenticationMethod.NONE) + .build() + } + } + + companion object { + + private fun instanceWith(clientAuthenticationMethod : ClientAuthenticationMethod) { + ClientAuthenticationProperties( + "client", + clientAuthenticationMethod, + "secret", + null) + } + } +} \ No newline at end of file diff --git a/token-client-core/src/test/kotlin/no/nav/security/token/support/client/core/ClientPropertiesTest.kt b/token-client-core/src/test/kotlin/no/nav/security/token/support/client/core/ClientPropertiesTest.kt new file mode 100644 index 00000000..21a89555 --- /dev/null +++ b/token-client-core/src/test/kotlin/no/nav/security/token/support/client/core/ClientPropertiesTest.kt @@ -0,0 +1,99 @@ +package no.nav.security.token.support.client.core + +import com.nimbusds.oauth2.sdk.auth.ClientAuthenticationMethod +import java.io.IOException +import java.net.URI +import java.util.function.Consumer +import okhttp3.mockwebserver.MockWebServer +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test +import no.nav.security.token.support.client.core.ClientProperties.TokenExchangeProperties +import no.nav.security.token.support.client.core.TestUtils.jsonResponse +import no.nav.security.token.support.client.core.TestUtils.withMockServer + +internal class ClientPropertiesTest { + + private val wellKnownJson = """ + { + "issuer" : "https://someissuer", + "token_endpoint" : "https://someissuer/token", + "jwks_uri" : "https://someissuer/jwks", + "grant_types_supported" : [ "urn:ietf:params:oauth:grant-type:token-exchange" ], + "token_endpoint_auth_methods_supported" : [ "private_key_jwt" ], + "token_endpoint_auth_signing_alg_values_supported" : [ "RS256" ], + "subject_types_supported" : [ "public" ] + } + + """.trimIndent() + + @Test + fun validGrantTypes() { + Assertions.assertNotNull(clientPropertiesFromGrantType(OAuth2GrantType.JWT_BEARER)) + Assertions.assertNotNull(clientPropertiesFromGrantType(OAuth2GrantType.CLIENT_CREDENTIALS)) + Assertions.assertNotNull(clientPropertiesFromGrantType(OAuth2GrantType.TOKEN_EXCHANGE)) + } + + @Test + fun invalidGrantTypes() { + org.assertj.core.api.Assertions.assertThatExceptionOfType(IllegalArgumentException::class.java) + .isThrownBy { clientPropertiesFromGrantType(OAuth2GrantType("somegrantNotSupported")) } + } + + @Test + @Throws(IOException::class) + fun ifWellKnownUrlIsNotNullShouldRetrieveMetadataAndSetTokenEndpoint() { + withMockServer( + Consumer { s : MockWebServer? -> + s!!.enqueue(jsonResponse(wellKnownJson)) + Assertions.assertNotNull(clientPropertiesFromWellKnown(s + .url("/well-known").toUri()).tokenEndpointUrl) + } + ) + } + + @Test + fun incorrectWellKnownUrlShouldThrowException() { + org.assertj.core.api.Assertions.assertThatExceptionOfType(OAuth2ClientException::class.java) + .isThrownBy { clientPropertiesFromWellKnown(URI.create("http://localhost:1234/notfound")) } + } + + companion object { + + private fun clientPropertiesFromWellKnown(wellKnownUrl : URI) : ClientProperties { + return ClientProperties( + null, + wellKnownUrl, + OAuth2GrantType.CLIENT_CREDENTIALS, listOf("scope1", "scope2"), + clientAuth(), + null, + tokenExchange() + ) + } + + private fun clientAuth() : ClientAuthenticationProperties { + return ClientAuthenticationProperties( + "client", + ClientAuthenticationMethod.CLIENT_SECRET_BASIC, + "secret", + null) + } + + private fun tokenExchange() : TokenExchangeProperties { + return TokenExchangeProperties( + "aud1", + null + ) + } + + private fun clientPropertiesFromGrantType(grantType : OAuth2GrantType) : ClientProperties { + return ClientProperties( + URI.create("http://token"), + null, + grantType, listOf("scope1", "scope2"), + clientAuth(), + null, + tokenExchange() + ) + } + } +} \ No newline at end of file diff --git a/token-client-core/src/test/kotlin/no/nav/security/token/support/client/core/TestUtils.kt b/token-client-core/src/test/kotlin/no/nav/security/token/support/client/core/TestUtils.kt new file mode 100644 index 00000000..a4be2481 --- /dev/null +++ b/token-client-core/src/test/kotlin/no/nav/security/token/support/client/core/TestUtils.kt @@ -0,0 +1,110 @@ +package no.nav.security.token.support.client.core + +import com.nimbusds.jwt.JWT +import com.nimbusds.jwt.JWTClaimsSet.Builder +import com.nimbusds.jwt.PlainJWT +import com.nimbusds.oauth2.sdk.auth.ClientAuthenticationMethod +import java.io.IOException +import java.io.UnsupportedEncodingException +import java.net.URI +import java.net.URLEncoder +import java.nio.charset.StandardCharsets +import java.time.LocalDateTime +import java.time.ZoneId +import java.util.Base64 +import java.util.Date +import java.util.Optional +import java.util.UUID +import java.util.function.Consumer +import okhttp3.mockwebserver.MockResponse +import okhttp3.mockwebserver.MockWebServer +import okhttp3.mockwebserver.RecordedRequest +import org.assertj.core.api.Assertions +import no.nav.security.token.support.client.core.ClientAuthenticationProperties.Companion.builder +import no.nav.security.token.support.client.core.ClientProperties.Companion.builder +import no.nav.security.token.support.client.core.ClientProperties.TokenExchangeProperties + +object TestUtils { + + const val CONTENT_TYPE_FORM_URL_ENCODED = "application/x-www-form-urlencoded;charset=UTF-8" + const val CONTENT_TYPE_JSON = "application/json;charset=UTF-8" + @JvmStatic + fun clientProperties(tokenEndpointUrl : String?, oAuth2GrantType : OAuth2GrantType?) : ClientProperties { + return builder(oAuth2GrantType!!, builder("client1", ClientAuthenticationMethod.CLIENT_SECRET_BASIC) + .clientSecret("clientSecret1") + .build()) + .scope(listOf("scope1", "scope2")) + .tokenEndpointUrl(URI.create(tokenEndpointUrl)) + .build() + } + + fun tokenExchangeClientProperties( + tokenEndpointUrl : String?, + oAuth2GrantType : OAuth2GrantType?, + clientPrivateKey : String? + ) : ClientProperties { + return builder(oAuth2GrantType!!, builder("client1", ClientAuthenticationMethod.PRIVATE_KEY_JWT) + .clientJwk(clientPrivateKey!!) + .build()) + .tokenEndpointUrl(URI.create(tokenEndpointUrl)) + .tokenExchange(TokenExchangeProperties("audience")) + .build() + } + + @JvmStatic + @Throws(IOException::class) + fun withMockServer(test : Consumer) { + val server = MockWebServer() + server.start() + test.accept(server) + server.shutdown() + } + + @JvmStatic + fun jsonResponse(json : String?) : MockResponse { + return MockResponse() + .setHeader("Content-Type", "application/json;charset=UTF-8") + .setBody(json!!) + } + + @JvmStatic + fun assertPostMethodAndJsonHeaders(recordedRequest : RecordedRequest) { + Assertions.assertThat(recordedRequest.method).isEqualTo("POST") + Assertions.assertThat(recordedRequest.getHeader("Accept")).isEqualTo(CONTENT_TYPE_JSON) + Assertions.assertThat(recordedRequest.getHeader("Content-Type")).isEqualTo(CONTENT_TYPE_FORM_URL_ENCODED) + } + + @JvmStatic + fun decodeBasicAuth(recordedRequest : RecordedRequest) : String { + return Optional.ofNullable(recordedRequest.headers["Authorization"]) + .map { s : String -> s.split("Basic ".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() } + .filter { pair : Array -> pair.size == 2 } + .map { pair : Array -> Base64.getDecoder().decode(pair[1]) } + .map { bytes : ByteArray? -> String(bytes!!, StandardCharsets.UTF_8) } + .orElse("") + } + + @JvmStatic + fun jwt(sub : String?) : JWT { + val expiry = LocalDateTime.now().atZone(ZoneId.systemDefault()).plusSeconds(60).toInstant() + return PlainJWT(Builder() + .subject(sub) + .audience("thisapi") + .issuer("someIssuer") + .expirationTime(Date.from(expiry)) + .claim("jti", UUID.randomUUID().toString()) + .build()) + } + + @JvmStatic + fun encodeValue(value : String?) : String? { + var encodedUrl : String? = null + try { + encodedUrl = URLEncoder.encode(value, StandardCharsets.UTF_8.toString()) + } + catch (e : UnsupportedEncodingException) { + e.printStackTrace() + } + return encodedUrl + } +} \ No newline at end of file diff --git a/token-client-core/src/test/kotlin/no/nav/security/token/support/client/core/auth/ClientAssertionTest.kt b/token-client-core/src/test/kotlin/no/nav/security/token/support/client/core/auth/ClientAssertionTest.kt new file mode 100644 index 00000000..5ee784a1 --- /dev/null +++ b/token-client-core/src/test/kotlin/no/nav/security/token/support/client/core/auth/ClientAssertionTest.kt @@ -0,0 +1,54 @@ +package no.nav.security.token.support.client.core.auth + +import com.nimbusds.jose.JOSEException +import com.nimbusds.jose.JOSEObjectType +import com.nimbusds.jose.JWSAlgorithm +import com.nimbusds.jose.JWSVerifier +import com.nimbusds.jose.crypto.RSASSAVerifier +import com.nimbusds.jwt.SignedJWT +import com.nimbusds.oauth2.sdk.auth.ClientAuthenticationMethod +import java.net.URI +import java.text.ParseException +import java.time.Instant +import java.util.Date +import java.util.Objects +import org.assertj.core.api.Assertions +import org.junit.jupiter.api.Test +import no.nav.security.token.support.client.core.ClientAuthenticationProperties.Companion.builder +import no.nav.security.token.support.client.core.ClientProperties.Companion.builder +import no.nav.security.token.support.client.core.OAuth2GrantType + +internal class ClientAssertionTest { + + @Test + @Throws(ParseException::class, JOSEException::class) + fun testCreateAssertion() { + val clientAuth = builder("client1", ClientAuthenticationMethod.PRIVATE_KEY_JWT) + .clientJwk("src/test/resources/jwk.json") + .build() + val clientProperties = builder(OAuth2GrantType.CLIENT_CREDENTIALS, clientAuth) + .tokenEndpointUrl(URI.create("http://token")) + .build() + val now = Instant.now() + val clientAssertion = ClientAssertion( + clientProperties.tokenEndpointUrl!!, + clientProperties.authentication) + Assertions.assertThat(clientAssertion).isNotNull() + Assertions.assertThat(clientAssertion.assertionType()).isEqualTo("urn:ietf:params:oauth:client-assertion-type:jwt-bearer") + val assertion = clientAssertion.assertion() + Assertions.assertThat(clientAssertion.assertion()).isNotNull() + val signedJWT = SignedJWT.parse(assertion) + val keyId = Objects.requireNonNull(clientProperties.authentication.clientRsaKey)?.keyID + Assertions.assertThat(signedJWT.header.keyID).isEqualTo(keyId) + Assertions.assertThat(signedJWT.header.type).isEqualTo(JOSEObjectType.JWT) + Assertions.assertThat(signedJWT.header.algorithm).isEqualTo(JWSAlgorithm.RS256) + val verifier : JWSVerifier = RSASSAVerifier(Objects.requireNonNull(clientAuth.clientRsaKey)) + Assertions.assertThat(signedJWT.verify(verifier)).isTrue() + val claims = signedJWT.jwtClaimsSet + Assertions.assertThat(claims.subject).isEqualTo(clientAuth.clientId) + Assertions.assertThat(claims.issuer).isEqualTo(clientAuth.clientId) + Assertions.assertThat(claims.audience).containsExactly(clientProperties.tokenEndpointUrl.toString()) + Assertions.assertThat(claims.expirationTime).isAfter(Date.from(now)) + Assertions.assertThat(claims.notBeforeTime).isBefore(claims.expirationTime) + } +} \ No newline at end of file diff --git a/token-client-core/src/test/kotlin/no/nav/security/token/support/client/core/http/OAuth2HttpHeadersTest.kt b/token-client-core/src/test/kotlin/no/nav/security/token/support/client/core/http/OAuth2HttpHeadersTest.kt new file mode 100644 index 00000000..02df73ae --- /dev/null +++ b/token-client-core/src/test/kotlin/no/nav/security/token/support/client/core/http/OAuth2HttpHeadersTest.kt @@ -0,0 +1,23 @@ +package no.nav.security.token.support.client.core.http + +import java.util.Map +import org.assertj.core.api.Assertions +import org.junit.jupiter.api.Test +import no.nav.security.token.support.client.core.http.OAuth2HttpHeaders.Companion.builder +import no.nav.security.token.support.client.core.http.OAuth2HttpHeaders.Companion.of + +internal class OAuth2HttpHeadersTest { + + @Test + fun test() { + val httpHeadersFromBuilder = builder() + .header("header1", "header1value1") + .header("header1", "header1value2") + .build() + val httpHeadersFromOf = of(Map.of("header1", listOf("header1value1", + "header1value2"))) + Assertions.assertThat(httpHeadersFromBuilder).isEqualTo(httpHeadersFromOf) + Assertions.assertThat(httpHeadersFromBuilder.headers).hasSize(1) + Assertions.assertThat(httpHeadersFromBuilder.headers).isEqualTo(httpHeadersFromOf.headers) + } +} \ No newline at end of file diff --git a/token-client-core/src/test/kotlin/no/nav/security/token/support/client/core/http/SimpleOAuth2HttpClient.kt b/token-client-core/src/test/kotlin/no/nav/security/token/support/client/core/http/SimpleOAuth2HttpClient.kt new file mode 100644 index 00000000..aa3f2239 --- /dev/null +++ b/token-client-core/src/test/kotlin/no/nav/security/token/support/client/core/http/SimpleOAuth2HttpClient.kt @@ -0,0 +1,74 @@ +package no.nav.security.token.support.client.core.http + +import com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.module.kotlin.KotlinModule +import java.io.IOException +import java.net.URLEncoder +import java.net.http.HttpClient +import java.net.http.HttpRequest +import java.net.http.HttpRequest.BodyPublishers +import java.net.http.HttpResponse +import java.net.http.HttpResponse.BodyHandlers +import java.nio.charset.StandardCharsets +import java.util.Optional +import java.util.function.Consumer +import java.util.stream.Collectors +import kotlin.collections.Map.Entry +import org.slf4j.LoggerFactory +import no.nav.security.token.support.client.core.oauth2.OAuth2AccessTokenResponse + +class SimpleOAuth2HttpClient : OAuth2HttpClient { + + private val objectMapper : ObjectMapper + + init { + objectMapper = ObjectMapper().registerModule(KotlinModule.Builder().build()) + .configure(FAIL_ON_UNKNOWN_PROPERTIES, false) + } + + override fun post(oAuth2HttpRequest : OAuth2HttpRequest) : OAuth2AccessTokenResponse? { + return try { + val requestBuilder = HttpRequest.newBuilder() + oAuth2HttpRequest.oAuth2HttpHeaders!! + .headers.forEach { (key : String?, value : List) -> + value.forEach( + Consumer { v : String? -> requestBuilder.header(key, v) }) + } + val body = oAuth2HttpRequest.formParameters.entries.stream() + .map { (key, value) : Entry -> key + "=" + URLEncoder.encode(value, StandardCharsets.UTF_8) } + .collect(Collectors.joining("&")) + val httpRequest = requestBuilder + .uri(oAuth2HttpRequest.tokenEndpointUrl) + .POST(BodyPublishers.ofString(body)) + .build() + val response = HttpClient.newHttpClient().send(httpRequest, BodyHandlers.ofString()) + objectMapper.readValue(bodyAsString(response), OAuth2AccessTokenResponse::class.java) + } + catch (e : IOException) { + throw RuntimeException(e) + } + catch (e : InterruptedException) { + throw RuntimeException(e) + } + } + + private fun bodyAsString(response : HttpResponse?) : String { + if (response != null) { + log.debug("received response in client, body={}", response.body()) + return Optional.of(response) + .filter { r : HttpResponse -> r.statusCode() == 200 } + .map { obj : HttpResponse -> obj.body() } + .orElseThrow { + RuntimeException("received status code=" + response.statusCode() + + " and response body=" + response.body() + " from authorization server.") + } + } + throw RuntimeException("response cannot be null.") + } + + companion object { + + private val log = LoggerFactory.getLogger(SimpleOAuth2HttpClient::class.java) + } +} \ No newline at end of file diff --git a/token-client-core/src/test/kotlin/no/nav/security/token/support/client/core/jwk/JwkFactoryTest.kt b/token-client-core/src/test/kotlin/no/nav/security/token/support/client/core/jwk/JwkFactoryTest.kt new file mode 100644 index 00000000..051f75f1 --- /dev/null +++ b/token-client-core/src/test/kotlin/no/nav/security/token/support/client/core/jwk/JwkFactoryTest.kt @@ -0,0 +1,71 @@ +package no.nav.security.token.support.client.core.jwk + +import com.nimbusds.jose.util.Base64URL +import java.io.IOException +import java.io.InputStream +import java.security.KeyStore +import java.security.KeyStoreException +import java.security.MessageDigest +import java.security.NoSuchAlgorithmException +import java.security.cert.CertificateException +import org.assertj.core.api.Assertions +import org.junit.jupiter.api.Test +import org.slf4j.LoggerFactory +import no.nav.security.token.support.client.core.jwk.JwkFactory.fromJsonFile +import no.nav.security.token.support.client.core.jwk.JwkFactory.fromKeyStore + +internal class JwkFactoryTest { + + @Test + fun keyFromJwkFile() { + val rsaKey = fromJsonFile("src/test/resources/jwk.json") + Assertions.assertThat(rsaKey.keyID).isEqualTo("jlAX4HYKW4hyhZgSmUyOmVAqMUw") + Assertions.assertThat(rsaKey.isPrivate).isTrue() + Assertions + .assertThat(rsaKey.privateExponent) + .hasToString("J_mMSpq8k4WH9GKeS6d1kPVrQz2jDslAy3b3zrBuiSdNtKgUN7jFhGXaiY-cAg3efhMc-MWwPa0raKEN9xQRtIdbJurJbNG3viCvo_8FNs5lmFCUIktuO12zvsJS63q-i1zsZ7_esYQHbeDqg9S3q98c2EIO8lxQvPBcq-OIjdxfuanAEWJIRNuvNkK5I0AcqF_Q_KeFQDHo5sWUkwyPCaddd-ogS_YDeK3eeUpQbElrusdv0Ai0iYBPukzEHz1aL8PbaYru9f6Alor6yt9Lc_FNKfi-gnNFdpg3-uqVEh-MhEXgyN1RkeZzt0Kk9rylHumjSpwEgzuuA2L3WnycUQ") + } + + @Test + fun keyFromKeystore() { + val rsaKey = fromKeyStore( + ALIAS, + JwkFactoryTest::class.java.getResourceAsStream(KEY_STORE_FILE), + "Test1234" + ) + Assertions.assertThat(rsaKey.keyID).isEqualTo(certificateThumbprintSHA1()) + Assertions.assertThat(rsaKey.isPrivate).isTrue() + } + + companion object { + + private const val KEY_STORE_FILE = "/selfsigned.jks" + private const val ALIAS = "client_assertion" + private val log = LoggerFactory.getLogger(JwkFactoryTest::class.java) + private fun certificateThumbprintSHA1() : String { + return try { + val keyStore = KeyStore.getInstance("JKS") + keyStore.load(inputStream(KEY_STORE_FILE), "Test1234".toCharArray()) + val cert = keyStore.getCertificate(ALIAS) + val sha1 = MessageDigest.getInstance("SHA-1") + Base64URL.encode(sha1.digest(cert.encoded)).toString() + } + catch (e : KeyStoreException) { + throw RuntimeException(e) + } + catch (e : IOException) { + throw RuntimeException(e) + } + catch (e : CertificateException) { + throw RuntimeException(e) + } + catch (e : NoSuchAlgorithmException) { + throw RuntimeException(e) + } + } + + private fun inputStream(resource : String) : InputStream { + return JwkFactoryTest::class.java.getResourceAsStream(resource) + } + } +} \ No newline at end of file diff --git a/token-client-core/src/test/kotlin/no/nav/security/token/support/client/core/oauth2/ClientCredentialsTokenClientTest.kt b/token-client-core/src/test/kotlin/no/nav/security/token/support/client/core/oauth2/ClientCredentialsTokenClientTest.kt new file mode 100644 index 00000000..ad616c6a --- /dev/null +++ b/token-client-core/src/test/kotlin/no/nav/security/token/support/client/core/oauth2/ClientCredentialsTokenClientTest.kt @@ -0,0 +1,185 @@ +package no.nav.security.token.support.client.core.oauth2 + +import com.nimbusds.oauth2.sdk.auth.ClientAuthenticationMethod +import java.io.IOException +import java.net.URI +import okhttp3.mockwebserver.MockWebServer +import okhttp3.mockwebserver.RecordedRequest +import org.assertj.core.api.Assertions +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import no.nav.security.token.support.client.core.ClientAuthenticationProperties.Companion.builder +import no.nav.security.token.support.client.core.ClientProperties +import no.nav.security.token.support.client.core.ClientProperties.Companion.builder +import no.nav.security.token.support.client.core.OAuth2ClientException +import no.nav.security.token.support.client.core.OAuth2GrantType +import no.nav.security.token.support.client.core.TestUtils.assertPostMethodAndJsonHeaders +import no.nav.security.token.support.client.core.TestUtils.clientProperties +import no.nav.security.token.support.client.core.TestUtils.decodeBasicAuth +import no.nav.security.token.support.client.core.TestUtils.encodeValue +import no.nav.security.token.support.client.core.TestUtils.jsonResponse +import no.nav.security.token.support.client.core.http.SimpleOAuth2HttpClient + +internal class ClientCredentialsTokenClientTest { + + private var tokenEndpointUrl : String? = null + private var server : MockWebServer? = null + private var client : ClientCredentialsTokenClient? = null + @BeforeEach + @Throws(IOException::class) + fun setup() { + server = MockWebServer() + server!!.start() + tokenEndpointUrl = server!!.url("/oauth2/v2/token").toString() + client = ClientCredentialsTokenClient(SimpleOAuth2HttpClient()) + } + + @AfterEach + @Throws(Exception::class) + fun cleanup() { + server!!.shutdown() + } + + @Test + fun tokenResponseWithDefaultClientAuthMethod() { + server!!.enqueue(jsonResponse(TOKEN_RESPONSE)) + val clientProperties = clientProperties(tokenEndpointUrl, OAuth2GrantType.CLIENT_CREDENTIALS) + val response = client!!.getTokenResponse(ClientCredentialsGrantRequest(clientProperties)) + val recordedRequest = server!!.takeRequest() + assertPostMethodAndJsonHeaders(recordedRequest) + assertThatClientAuthMethodIsClientSecretBasic(recordedRequest, clientProperties) + val body = recordedRequest.body.readUtf8() + assertThatRequestBodyContainsFormParameters(body) + assertThatResponseContainsAccessToken(response) + } + + @Test + fun tokenResponseWithClientSecretBasic() { + server!!.enqueue(jsonResponse(TOKEN_RESPONSE)) + val clientProperties = clientProperties(tokenEndpointUrl, OAuth2GrantType.CLIENT_CREDENTIALS) + val response = client!!.getTokenResponse(ClientCredentialsGrantRequest(clientProperties)) + val recordedRequest = server!!.takeRequest() + assertPostMethodAndJsonHeaders(recordedRequest) + assertThatClientAuthMethodIsClientSecretBasic(recordedRequest, clientProperties) + val body = recordedRequest.body.readUtf8() + assertThatRequestBodyContainsFormParameters(body) + assertThatResponseContainsAccessToken(response) + } + + @Test + fun tokenResponseWithClientSecretPost(){ + server!!.enqueue(jsonResponse(TOKEN_RESPONSE)) + /* ClientProperties clientProperties = clientProperties(tokenEndpointUrl, CLIENT_CREDENTIALS) + .toBuilder() + .authentication(ClientAuthenticationProperties.builder("client",CLIENT_SECRET_POST) + .clientSecret("secret") + .build()) + .build();*/ + val clientProperties = builder(OAuth2GrantType.CLIENT_CREDENTIALS, builder("client", ClientAuthenticationMethod.CLIENT_SECRET_POST) + .clientSecret("secret").build()) + .tokenEndpointUrl(URI.create(tokenEndpointUrl)) + .scope(listOf("scope1", "scope2")) + .build() + val response = client!!.getTokenResponse(ClientCredentialsGrantRequest(clientProperties)) + val recordedRequest = server!!.takeRequest() + assertPostMethodAndJsonHeaders(recordedRequest) + val body = recordedRequest.body.readUtf8() + assertThatClientAuthMethodIsClientSecretPost(body, clientProperties) + assertThatRequestBodyContainsFormParameters(body) + assertThatResponseContainsAccessToken(response) + } + + @Test + fun tokenResponseWithPrivateKeyJwt() + { + server!!.enqueue(jsonResponse(TOKEN_RESPONSE)) + /* ClientProperties clientProperties = clientProperties(tokenEndpointUrl, CLIENT_CREDENTIALS) + .toBuilder() + .authentication(ClientAuthenticationProperties.builder("client",PRIVATE_KEY_JWT) + .clientJwk("src/test/resources/jwk.json") + .build()) + .build(); +*/ + val clientProperties = builder(OAuth2GrantType.CLIENT_CREDENTIALS, builder("client", ClientAuthenticationMethod.PRIVATE_KEY_JWT) + .clientSecret("secret") + .clientJwk("src/test/resources/jwk.json") + .build()) + .tokenEndpointUrl(URI.create(tokenEndpointUrl)) + .scope(listOf("scope1", "scope2")) + .build() + val response = client!!.getTokenResponse(ClientCredentialsGrantRequest(clientProperties)) + val recordedRequest = server!!.takeRequest() + assertPostMethodAndJsonHeaders(recordedRequest) + val body = recordedRequest.body.readUtf8() + assertThatClientAuthMethodIsPrivateKeyJwt(body, clientProperties) + assertThatRequestBodyContainsFormParameters(body) + assertThatResponseContainsAccessToken(response) + } + + @Test + fun tokenResponseError() { + server!!.enqueue(jsonResponse(ERROR_RESPONSE).setResponseCode(400)) + Assertions.assertThatExceptionOfType(OAuth2ClientException::class.java) + .isThrownBy { + client!!.getTokenResponse(ClientCredentialsGrantRequest(clientProperties( + tokenEndpointUrl, + OAuth2GrantType.CLIENT_CREDENTIALS + ))) + } + } + + companion object { + + private const val TOKEN_RESPONSE = "{\n" + + " \"token_type\": \"Bearer\",\n" + + " \"scope\": \"scope1 scope2\",\n" + + " \"expires_at\": 1568141495,\n" + + " \"expires_in\": 3599,\n" + + " \"ext_expires_in\": 3599,\n" + + " \"access_token\": \"\",\n" + + " \"refresh_token\": \"\"\n" + + "}\n" + private const val ERROR_RESPONSE = "{\"error\": \"some client error occurred\"}" + private fun assertThatResponseContainsAccessToken(response : OAuth2AccessTokenResponse?) { + Assertions.assertThat(response).isNotNull() + Assertions.assertThat(response!!.accessToken).isNotBlank() + Assertions.assertThat(response.expiresAt).isPositive() + Assertions.assertThat(response.expiresIn).isPositive() + } + + private fun assertThatClientAuthMethodIsPrivateKeyJwt( + body : String, + clientProperties : ClientProperties) { + val auth = clientProperties.authentication + Assertions.assertThat(auth.clientAuthMethod.value).isEqualTo("private_key_jwt") + Assertions.assertThat(body).contains("client_id=" + encodeValue(auth.clientId)) + Assertions.assertThat(body).contains("client_assertion_type=" + encodeValue( + "urn:ietf:params:oauth:client-assertion-type:jwt-bearer")) + Assertions.assertThat(body).contains("client_assertion=" + "ey") + } + + private fun assertThatClientAuthMethodIsClientSecretPost( + body : String, + clientProperties : ClientProperties) { + val auth = clientProperties.authentication + Assertions.assertThat(auth.clientAuthMethod.value).isEqualTo("client_secret_post") + Assertions.assertThat(body).contains("client_id=" + encodeValue(auth.clientId)) + Assertions.assertThat(body).contains("client_secret=" + encodeValue(auth.clientSecret)) + } + + private fun assertThatClientAuthMethodIsClientSecretBasic(recordedRequest : RecordedRequest, + clientProperties : ClientProperties) { + val auth = clientProperties.authentication + Assertions.assertThat(auth.clientAuthMethod.value).isEqualTo("client_secret_basic") + Assertions.assertThat(recordedRequest.headers["Authorization"]).isNotBlank() + val usernamePwd = decodeBasicAuth(recordedRequest) + Assertions.assertThat(usernamePwd).isEqualTo(auth.clientId + ":" + auth.clientSecret) + } + + private fun assertThatRequestBodyContainsFormParameters(formParameters : String) { + Assertions.assertThat(formParameters).contains("grant_type=client_credentials") + Assertions.assertThat(formParameters).contains("scope=scope1+scope2") + } + } +} \ No newline at end of file diff --git a/token-client-core/src/test/kotlin/no/nav/security/token/support/client/core/oauth2/OAuth2AccessTokenServiceTest.kt b/token-client-core/src/test/kotlin/no/nav/security/token/support/client/core/oauth2/OAuth2AccessTokenServiceTest.kt new file mode 100644 index 00000000..b79d0268 --- /dev/null +++ b/token-client-core/src/test/kotlin/no/nav/security/token/support/client/core/oauth2/OAuth2AccessTokenServiceTest.kt @@ -0,0 +1,227 @@ +package no.nav.security.token.support.client.core.oauth2 + +import com.nimbusds.jwt.JWTClaimsSet.Builder +import com.nimbusds.jwt.PlainJWT +import java.time.Instant +import java.time.LocalDateTime.* +import java.time.ZoneId.* +import java.util.Arrays +import java.util.Date +import java.util.Optional +import java.util.UUID +import org.assertj.core.api.Assertions.* +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.mockito.Mock +import org.mockito.Mockito +import org.mockito.Mockito.never +import org.mockito.Mockito.reset +import org.mockito.Mockito.times +import org.mockito.Mockito.verify +import org.mockito.Mockito.`when` +import org.mockito.MockitoAnnotations +import no.nav.security.token.support.client.core.ClientProperties.TokenExchangeProperties +import no.nav.security.token.support.client.core.OAuth2CacheFactory.accessTokenResponseCache +import no.nav.security.token.support.client.core.OAuth2ClientException +import no.nav.security.token.support.client.core.OAuth2GrantType +import no.nav.security.token.support.client.core.OAuth2GrantType.Companion.CLIENT_CREDENTIALS +import no.nav.security.token.support.client.core.OAuth2GrantType.Companion.TOKEN_EXCHANGE +import no.nav.security.token.support.client.core.TestUtils.clientProperties +import no.nav.security.token.support.client.core.context.JwtBearerTokenResolver + +internal class OAuth2AccessTokenServiceTest { + + private inline fun reifiedAny(type: Class): T = Mockito.any(type) + + + @Mock + private lateinit var onBehalfOfTokenResponseClient : OnBehalfOfTokenClient + + @Mock + private lateinit var clientCredentialsTokenResponseClient : ClientCredentialsTokenClient + + @Mock + private lateinit var exchangeTokeResponseClient : TokenExchangeClient + + @Mock + private lateinit var assertionResolver : JwtBearerTokenResolver + private lateinit var oAuth2AccessTokenService : OAuth2AccessTokenService + + @BeforeEach + fun setup() { + MockitoAnnotations.initMocks(this) + val oboCache = accessTokenResponseCache(10, 1) + val clientCredentialsCache = accessTokenResponseCache(10, 1) + val exchangeTokenCache = accessTokenResponseCache(10, 1) + oAuth2AccessTokenService = OAuth2AccessTokenService(assertionResolver, onBehalfOfTokenResponseClient, clientCredentialsTokenResponseClient, exchangeTokeResponseClient) + oAuth2AccessTokenService.onBehalfOfGrantCache = oboCache + oAuth2AccessTokenService.clientCredentialsGrantCache = clientCredentialsCache + oAuth2AccessTokenService.exchangeGrantCache = exchangeTokenCache + } + + + @Test + fun accessTokenOnBehalfOf() { + `when`(assertionResolver.token()).thenReturn(Optional.of(jwt("sub1").serialize())) + val firstAccessToken = "first_access_token" + `when`(onBehalfOfTokenResponseClient.getTokenResponse(reifiedAny(OnBehalfOfGrantRequest::class.java))) + .thenReturn(accessTokenResponse(firstAccessToken, 60)) + val res = oAuth2AccessTokenService.getAccessToken(onBehalfOfProperties()) + verify(onBehalfOfTokenResponseClient).getTokenResponse(reifiedAny( OnBehalfOfGrantRequest::class.java)) + assertThat(res).hasNoNullFieldsOrProperties() + assertThat(res!!.accessToken).isEqualTo("first_access_token") + } + + @Test + fun accessTokenClientCredentials() { + val firstAccessToken = "first_access_token" + `when`(clientCredentialsTokenResponseClient.getTokenResponse(reifiedAny(ClientCredentialsGrantRequest::class.java))) + .thenReturn(accessTokenResponse(firstAccessToken, 60)) + val res = oAuth2AccessTokenService.getAccessToken(clientCredentialsProperties()) + verify(clientCredentialsTokenResponseClient).getTokenResponse(reifiedAny(ClientCredentialsGrantRequest::class.java)) + assertThat(res).hasNoNullFieldsOrProperties() + assertThat(res!!.accessToken).isEqualTo("first_access_token") + } + + @Test + fun accessTokenOnBehalfOfNoAuthenticatedTokenFound() { + assertThatExceptionOfType(OAuth2ClientException::class.java) + .isThrownBy { oAuth2AccessTokenService.getAccessToken(onBehalfOfProperties()) } + .withMessageContaining("no authenticated jwt token found in validation context, cannot do on-behalf-of") + } + + @Test + fun accessTokenOnBehalfOf_WithCache_MultipleTimes_SameClientConfig() { + val clientProperties = onBehalfOfProperties() + `when`(assertionResolver.token()).thenReturn(Optional.of(jwt("sub1").serialize())) + + //should invoke client and populate cache + val firstAccessToken = "first_access_token" + `when`(onBehalfOfTokenResponseClient.getTokenResponse(reifiedAny(OnBehalfOfGrantRequest::class.java))) + .thenReturn(accessTokenResponse(firstAccessToken, 60)) + val res = oAuth2AccessTokenService.getAccessToken(clientProperties) + verify(onBehalfOfTokenResponseClient).getTokenResponse(reifiedAny(OnBehalfOfGrantRequest::class.java)) + assertThat(res).hasNoNullFieldsOrProperties() + assertThat(res!!.accessToken).isEqualTo("first_access_token") + + //should get response from cache and NOT invoke client + reset(onBehalfOfTokenResponseClient) + val res2 = oAuth2AccessTokenService.getAccessToken(clientProperties) + verify(onBehalfOfTokenResponseClient, never()).getTokenResponse(reifiedAny(OnBehalfOfGrantRequest::class.java)) + assertThat(res2!!.accessToken).isEqualTo("first_access_token") + + //another user/token but same clientconfig, should invoke client and populate cache + reset(assertionResolver) + `when`(assertionResolver.token()).thenReturn(Optional.of(jwt("sub2").serialize())) + reset(onBehalfOfTokenResponseClient) + val secondAccessToken = "second_access_token" + `when`(onBehalfOfTokenResponseClient.getTokenResponse(reifiedAny(OnBehalfOfGrantRequest::class.java))) + .thenReturn(accessTokenResponse(secondAccessToken, 60)) + val res3 = oAuth2AccessTokenService.getAccessToken(clientProperties) + verify(onBehalfOfTokenResponseClient).getTokenResponse(reifiedAny(OnBehalfOfGrantRequest::class.java)) + assertThat(res3!!.accessToken).isEqualTo(secondAccessToken) + } + + @Test + fun accessTokenClientCredentials_WithCache_MultipleTimes() { + var clientProperties = clientCredentialsProperties() + + //should invoke client and populate cache + val firstAccessToken = "first_access_token" + `when`(clientCredentialsTokenResponseClient.getTokenResponse(reifiedAny( + ClientCredentialsGrantRequest::class.java))) + .thenReturn(accessTokenResponse(firstAccessToken, 60)) + val res1 = oAuth2AccessTokenService.getAccessToken(clientProperties) + verify(clientCredentialsTokenResponseClient).getTokenResponse(reifiedAny(ClientCredentialsGrantRequest::class.java)) + assertThat(res1).hasNoNullFieldsOrProperties() + assertThat(res1!!.accessToken).isEqualTo("first_access_token") + + //should get response from cache and NOT invoke client + reset(clientCredentialsTokenResponseClient) + val res2 = oAuth2AccessTokenService.getAccessToken(clientProperties) + verify(clientCredentialsTokenResponseClient, never()).getTokenResponse(reifiedAny( + ClientCredentialsGrantRequest::class.java)) + assertThat(res2!!.accessToken).isEqualTo("first_access_token") + + //another clientconfig, should invoke client and populate cache + clientProperties = clientCredentialsProperties("scope3") + reset(clientCredentialsTokenResponseClient) + val secondAccessToken = "second_access_token" + `when`(clientCredentialsTokenResponseClient.getTokenResponse(reifiedAny(ClientCredentialsGrantRequest::class.java))) + .thenReturn(accessTokenResponse(secondAccessToken, 60)) + val res3 = oAuth2AccessTokenService.getAccessToken(clientProperties) + verify(clientCredentialsTokenResponseClient).getTokenResponse(reifiedAny(ClientCredentialsGrantRequest::class.java)) + assertThat(res3!!.accessToken).isEqualTo(secondAccessToken) + } + + @Test + @Throws(InterruptedException::class) + fun testCacheEntryIsEvictedOnExpiry() { + val clientProperties = onBehalfOfProperties() + `when`(assertionResolver.token()).thenReturn(Optional.of(jwt("sub1").serialize())) + + //should invoke client and populate cache + val firstAccessToken = "first_access_token" + `when`(onBehalfOfTokenResponseClient.getTokenResponse(reifiedAny(OnBehalfOfGrantRequest::class.java))) + .thenReturn(accessTokenResponse(firstAccessToken, 1)) + val res1 = oAuth2AccessTokenService.getAccessToken(clientProperties) + verify(onBehalfOfTokenResponseClient).getTokenResponse(reifiedAny(OnBehalfOfGrantRequest::class.java)) + assertThat(res1).hasNoNullFieldsOrProperties() + assertThat(res1!!.accessToken).isEqualTo("first_access_token") + Thread.sleep(1000) + + //entry should be missing from cache due to expiry + reset(onBehalfOfTokenResponseClient) + val secondAccessToken = "second_access_token" + `when`(onBehalfOfTokenResponseClient.getTokenResponse(reifiedAny(OnBehalfOfGrantRequest::class.java))) + .thenReturn(accessTokenResponse(secondAccessToken, 1)) + val res2 = oAuth2AccessTokenService.getAccessToken(clientProperties) + verify(onBehalfOfTokenResponseClient).getTokenResponse(reifiedAny(OnBehalfOfGrantRequest::class.java)) + assertThat(res2!!.accessToken).isEqualTo(secondAccessToken) + } + + @Test + fun accessTokenExchange() { + val clientProperties = exchangeProperties() + `when`(assertionResolver.token()).thenReturn(Optional.of(jwt("sub1").serialize())) + val firstAccessToken = "first_access_token" + `when`(exchangeTokeResponseClient.getTokenResponse(reifiedAny( + TokenExchangeGrantRequest::class.java))) + .thenReturn(accessTokenResponse(firstAccessToken, 60)) + val res1 = oAuth2AccessTokenService.getAccessToken(clientProperties) + verify(exchangeTokeResponseClient, times(1)).getTokenResponse(reifiedAny( + TokenExchangeGrantRequest::class.java)) + assertThat(res1).hasNoNullFieldsOrProperties() + assertThat(res1!!.accessToken).isEqualTo("first_access_token") + } + + companion object { + + private fun jwt(sub : String) = PlainJWT(Builder() + .subject(sub) + .audience("thisapi") + .issuer("someIssuer") + .expirationTime(Date.from(now().atZone(systemDefault()).plusSeconds(60).toInstant())) + .claim("jti", UUID.randomUUID().toString()) + .build()) + + private fun clientCredentialsProperties() = clientCredentialsProperties("scope1", "scope2") + + private fun clientCredentialsProperties(vararg scope : String) = + clientProperties("http://token", CLIENT_CREDENTIALS) + .toBuilder() + .scope(Arrays.asList(*scope)) + .build() + + private fun exchangeProperties(audience : String = "audience") = + clientProperties("http://token", TOKEN_EXCHANGE) + .toBuilder() + .tokenExchange(TokenExchangeProperties(audience)) + .build() + + private fun onBehalfOfProperties() = clientProperties("http://token", OAuth2GrantType.JWT_BEARER) + + private fun accessTokenResponse(assertion : String, expiresIn : Int) = + OAuth2AccessTokenResponse(assertion, Math.toIntExact(Instant.now().plusSeconds(expiresIn.toLong()).epochSecond), expiresIn) + } +} \ No newline at end of file diff --git a/token-client-core/src/test/kotlin/no/nav/security/token/support/client/core/oauth2/OnBehalfOfTokenClientTest.kt b/token-client-core/src/test/kotlin/no/nav/security/token/support/client/core/oauth2/OnBehalfOfTokenClientTest.kt new file mode 100644 index 00000000..567e0a79 --- /dev/null +++ b/token-client-core/src/test/kotlin/no/nav/security/token/support/client/core/oauth2/OnBehalfOfTokenClientTest.kt @@ -0,0 +1,87 @@ +package no.nav.security.token.support.client.core.oauth2 + +import java.io.IOException +import java.net.URLEncoder +import java.nio.charset.StandardCharsets +import okhttp3.mockwebserver.MockWebServer +import org.assertj.core.api.Assertions +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.mockito.MockitoAnnotations +import no.nav.security.token.support.client.core.OAuth2ClientException +import no.nav.security.token.support.client.core.OAuth2GrantType +import no.nav.security.token.support.client.core.TestUtils.assertPostMethodAndJsonHeaders +import no.nav.security.token.support.client.core.TestUtils.clientProperties +import no.nav.security.token.support.client.core.TestUtils.jsonResponse +import no.nav.security.token.support.client.core.TestUtils.jwt +import no.nav.security.token.support.client.core.http.SimpleOAuth2HttpClient + +internal class OnBehalfOfTokenClientTest { + + private var onBehalfOfTokenResponseClient : OnBehalfOfTokenClient? = null + private var tokenEndpointUrl : String? = null + private var server : MockWebServer? = null + @BeforeEach + @Throws(IOException::class) + fun setup() { + MockitoAnnotations.initMocks(this) + server = MockWebServer() + server!!.start() + tokenEndpointUrl = server!!.url(TOKEN_ENDPOINT).toString() + onBehalfOfTokenResponseClient = OnBehalfOfTokenClient(SimpleOAuth2HttpClient()) + } + + @AfterEach + @Throws(IOException::class) + fun teardown() { + server!!.shutdown() + } + + @Test + fun tokenResponse() { + server!!.enqueue(jsonResponse(TOKEN_RESPONSE)) + val assertion = jwt("sub1").serialize() + val clientProperties = clientProperties(tokenEndpointUrl, OAuth2GrantType.JWT_BEARER) + val oAuth2OnBehalfOfGrantRequest = OnBehalfOfGrantRequest(clientProperties, assertion) + val response = onBehalfOfTokenResponseClient!!.getTokenResponse(oAuth2OnBehalfOfGrantRequest) + val recordedRequest = server!!.takeRequest() + assertPostMethodAndJsonHeaders(recordedRequest) + val formParameters = recordedRequest.body.readUtf8() + Assertions.assertThat(formParameters).contains("grant_type=" + URLEncoder.encode(OAuth2GrantType.JWT_BEARER.value(), + StandardCharsets.UTF_8)) + .contains("scope=scope1+scope2") + .contains("requested_token_use=on_behalf_of") + .contains("assertion=$assertion") + Assertions.assertThat(response).isNotNull() + Assertions.assertThat(response!!.accessToken).isNotBlank() + Assertions.assertThat(response.expiresAt).isPositive() + Assertions.assertThat(response.expiresIn).isPositive() + } + + @Test + fun tokenResponseWithError() { + server!!.enqueue(jsonResponse(ERROR_RESPONSE).setResponseCode(400)) + val assertion = jwt("sub1").serialize() + val clientProperties = clientProperties(tokenEndpointUrl, OAuth2GrantType.JWT_BEARER) + val oAuth2OnBehalfOfGrantRequest = OnBehalfOfGrantRequest(clientProperties, assertion) + Assertions.assertThatExceptionOfType(OAuth2ClientException::class.java) + .isThrownBy { onBehalfOfTokenResponseClient!!.getTokenResponse(oAuth2OnBehalfOfGrantRequest) } + .withMessageContaining(ERROR_RESPONSE) + } + + companion object { + + private const val TOKEN_RESPONSE = "{\n" + + " \"token_type\": \"Bearer\",\n" + + " \"scope\": \"scope1 scope2\",\n" + + " \"expires_at\": 1568141495,\n" + + " \"ext_expires_in\": 3599,\n" + + " \"expires_in\": 3599,\n" + + " \"access_token\": \"\",\n" + + " \"refresh_token\": \"\"\n" + + "}\n" + private const val ERROR_RESPONSE = "{\"error\": \"some client error occurred\"}" + private const val TOKEN_ENDPOINT = "/oauth2/v2.0/token" + } +} \ No newline at end of file diff --git a/token-client-core/src/test/kotlin/no/nav/security/token/support/client/core/oauth2/TokenExchangeClientTest.kt b/token-client-core/src/test/kotlin/no/nav/security/token/support/client/core/oauth2/TokenExchangeClientTest.kt new file mode 100644 index 00000000..dfff7fef --- /dev/null +++ b/token-client-core/src/test/kotlin/no/nav/security/token/support/client/core/oauth2/TokenExchangeClientTest.kt @@ -0,0 +1,123 @@ +package no.nav.security.token.support.client.core.oauth2 + +import com.nimbusds.oauth2.sdk.auth.ClientAuthenticationMethod +import java.io.IOException +import java.net.URI +import okhttp3.mockwebserver.MockWebServer +import org.assertj.core.api.Assertions +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import no.nav.security.token.support.client.core.ClientAuthenticationProperties.Companion.builder +import no.nav.security.token.support.client.core.ClientProperties +import no.nav.security.token.support.client.core.ClientProperties.Companion.builder +import no.nav.security.token.support.client.core.ClientProperties.TokenExchangeProperties +import no.nav.security.token.support.client.core.OAuth2ClientException +import no.nav.security.token.support.client.core.OAuth2GrantType +import no.nav.security.token.support.client.core.OAuth2ParameterNames +import no.nav.security.token.support.client.core.TestUtils.assertPostMethodAndJsonHeaders +import no.nav.security.token.support.client.core.TestUtils.clientProperties +import no.nav.security.token.support.client.core.TestUtils.encodeValue +import no.nav.security.token.support.client.core.TestUtils.jsonResponse +import no.nav.security.token.support.client.core.TestUtils.jwt +import no.nav.security.token.support.client.core.http.SimpleOAuth2HttpClient + +internal class TokenExchangeClientTest { + + private var tokenEndpointUrl : String? = null + private var server : MockWebServer? = null + private var tokenExchangeClient : TokenExchangeClient? = null + private var subjectToken : String? = null + @BeforeEach + @Throws(IOException::class) + fun setup() { + server = MockWebServer() + server!!.start() + tokenEndpointUrl = server!!.url("/oauth2/v2/token").toString() + tokenExchangeClient = TokenExchangeClient(SimpleOAuth2HttpClient()) + subjectToken = jwt("somesub").serialize() + } + + @AfterEach + @Throws(Exception::class) + fun cleanup() { + server!!.shutdown() + } + + @Test + fun tokenResponseWithPrivateKeyJwtAndExchangeProperties() { + server!!.enqueue(jsonResponse(TOKEN_RESPONSE)) + /* ClientProperties clientProperties = tokenExchangeClientProperties( + tokenEndpointUrl, + TOKEN_EXCHANGE, + "src/test/resources/jwk.json" + ) + .toBuilder() + .authentication(ClientAuthenticationProperties.builder("client",PRIVATE_KEY_JWT) + .clientJwk("src/test/resources/jwk.json") + .build()) + .build(); +*/ + val clientProperties = builder(OAuth2GrantType.TOKEN_EXCHANGE, builder("client1", ClientAuthenticationMethod.PRIVATE_KEY_JWT) + .clientJwk("src/test/resources/jwk.json") + .build()) + .tokenEndpointUrl(URI.create(tokenEndpointUrl)) + .tokenExchange(TokenExchangeProperties("audience")).build() + val response = tokenExchangeClient!!.getTokenResponse(TokenExchangeGrantRequest(clientProperties, subjectToken!!)) + val recordedRequest = server!!.takeRequest() + assertPostMethodAndJsonHeaders(recordedRequest) + val body = recordedRequest.body.readUtf8() + assertThatClientAuthMethodIsPrivateKeyJwt(body, clientProperties) + assertThatRequestBodyContainsTokenExchangeFormParameters(body) + assertThatResponseContainsAccessToken(response) + } + + @Test + fun tokenResponseError() { + server!!.enqueue(jsonResponse(ERROR_RESPONSE).setResponseCode(400)) + Assertions.assertThatExceptionOfType(OAuth2ClientException::class.java) + .isThrownBy { + tokenExchangeClient!!.getTokenResponse(TokenExchangeGrantRequest(clientProperties( + tokenEndpointUrl, + OAuth2GrantType.TOKEN_EXCHANGE + ), subjectToken!!)) + } + } + + private fun assertThatRequestBodyContainsTokenExchangeFormParameters(formParameters : String) { + Assertions.assertThat(formParameters).contains(OAuth2ParameterNames.GRANT_TYPE + "=" + encodeValue(OAuth2GrantType.TOKEN_EXCHANGE.value())) + Assertions.assertThat(formParameters).contains(OAuth2ParameterNames.AUDIENCE + "=" + "audience") + Assertions.assertThat(formParameters).contains(OAuth2ParameterNames.SUBJECT_TOKEN_TYPE + "=" + encodeValue("urn:ietf:params:oauth:token-type:jwt")) + Assertions.assertThat(formParameters).contains(OAuth2ParameterNames.SUBJECT_TOKEN + "=" + subjectToken) + } + + companion object { + + private const val TOKEN_RESPONSE = "{\n" + + " \"token_type\": \"Bearer\",\n" + + " \"scope\": \"scope1 scope2\",\n" + + " \"expires_at\": 1568141495,\n" + + " \"expires_in\": 3599,\n" + + " \"ext_expires_in\": 3599,\n" + + " \"access_token\": \"\"\n" + + "}\n" + private const val ERROR_RESPONSE = "{\"error\": \"some client error occurred\"}" + private fun assertThatResponseContainsAccessToken(response : OAuth2AccessTokenResponse?) { + Assertions.assertThat(response).isNotNull() + Assertions.assertThat(response!!.accessToken).isNotBlank() + Assertions.assertThat(response.expiresAt).isPositive() + Assertions.assertThat(response.expiresIn).isPositive() + } + + private fun assertThatClientAuthMethodIsPrivateKeyJwt( + body : String, + clientProperties : ClientProperties) { + val auth = clientProperties.authentication + Assertions.assertThat(auth.clientAuthMethod.value).isEqualTo("private_key_jwt") + Assertions.assertThat(body).contains("client_id=" + encodeValue(auth.clientId)) + Assertions.assertThat(body).contains("client_assertion_type=" + encodeValue( + "urn:ietf:params:oauth:client-assertion-type:jwt-bearer")) + Assertions.assertThat(body).contains("client_assertion=" + "ey") + } + } +} \ No newline at end of file diff --git a/token-client-kotlin-demo/src/main/kotlin/no/nav/security/token/support/ktor/oauth/OAuth2Cache.kt b/token-client-kotlin-demo/src/main/kotlin/no/nav/security/token/support/ktor/oauth/OAuth2Cache.kt index d4cd92db..4cf74612 100644 --- a/token-client-kotlin-demo/src/main/kotlin/no/nav/security/token/support/ktor/oauth/OAuth2Cache.kt +++ b/token-client-kotlin-demo/src/main/kotlin/no/nav/security/token/support/ktor/oauth/OAuth2Cache.kt @@ -11,12 +11,10 @@ import java.util.concurrent.TimeUnit data class OAuth2CacheConfig( val enabled: Boolean, val maximumSize: Long = 1000, - val evictSkew: Long = 5, -) { + val evictSkew: Long = 5) { fun cache( cacheContext: CoroutineScope, - loader: suspend (GrantRequest) -> OAuth2AccessTokenResponse - ): AsyncLoadingCache = + loader: suspend (GrantRequest) -> OAuth2AccessTokenResponse): AsyncLoadingCache = Caffeine.newBuilder() .expireAfter(evictOnResponseExpiresIn(evictSkew)) .maximumSize(maximumSize) @@ -31,23 +29,17 @@ data class OAuth2CacheConfig( override fun expireAfterCreate( key: GrantRequest, response: OAuth2AccessTokenResponse, - currentTime: Long - ): Long { + currentTime: Long): Long { val seconds = - if (response.expiresIn > skewInSeconds) response.expiresIn - skewInSeconds else response.expiresIn + if (response.expiresIn!! > skewInSeconds) response.expiresIn!! - skewInSeconds else response.expiresIn!! .toLong() return TimeUnit.SECONDS.toNanos(seconds) } - override fun expireAfterUpdate( - key: GrantRequest, response: OAuth2AccessTokenResponse, - currentTime: Long, currentDuration: Long - ): Long = currentDuration + override fun expireAfterUpdate(key: GrantRequest, response: OAuth2AccessTokenResponse, currentTime: Long, currentDuration: Long): Long = currentDuration - override fun expireAfterRead( - key: GrantRequest, response: OAuth2AccessTokenResponse, currentTime: Long, currentDuration: Long - ): Long = currentDuration + override fun expireAfterRead(key: GrantRequest, response: OAuth2AccessTokenResponse, currentTime: Long, currentDuration: Long): Long = currentDuration } } -} +} \ No newline at end of file diff --git a/token-client-kotlin-demo/src/main/kotlin/no/nav/security/token/support/ktor/oauth/OAuth2Client.kt b/token-client-kotlin-demo/src/main/kotlin/no/nav/security/token/support/ktor/oauth/OAuth2Client.kt index e01cd416..8995dd43 100644 --- a/token-client-kotlin-demo/src/main/kotlin/no/nav/security/token/support/ktor/oauth/OAuth2Client.kt +++ b/token-client-kotlin-demo/src/main/kotlin/no/nav/security/token/support/ktor/oauth/OAuth2Client.kt @@ -124,7 +124,7 @@ internal suspend fun HttpClient.tokenRequest( if (clientAuthProperties.clientAuthMethod == ClientAuthenticationMethod.CLIENT_SECRET_BASIC) { header( "Authorization", - "Basic ${basicAuth(clientAuthProperties.clientId, clientAuthProperties.clientSecret)}" + "Basic ${basicAuth(clientAuthProperties.clientId, clientAuthProperties.clientSecret!!)}" ) } } @@ -136,7 +136,7 @@ private fun ParametersBuilder.appendClientAuthParams( when (clientAuthProperties.clientAuthMethod) { ClientAuthenticationMethod.CLIENT_SECRET_POST -> { append(OAuth2ParameterNames.CLIENT_ID, clientAuthProperties.clientId) - append(OAuth2ParameterNames.CLIENT_SECRET, clientAuthProperties.clientSecret) + append(OAuth2ParameterNames.CLIENT_SECRET, clientAuthProperties.clientSecret!!) } ClientAuthenticationMethod.PRIVATE_KEY_JWT -> { val clientAssertion = ClientAssertion(URI.create(tokenEndpointUrl), clientAuthProperties) diff --git a/token-client-spring/src/main/kotlin/no/nav/security/token/support/client/spring/oauth2/DefaultOAuth2HttpClient.kt b/token-client-spring/src/main/kotlin/no/nav/security/token/support/client/spring/oauth2/DefaultOAuth2HttpClient.kt index 0dc31694..3f92633a 100644 --- a/token-client-spring/src/main/kotlin/no/nav/security/token/support/client/spring/oauth2/DefaultOAuth2HttpClient.kt +++ b/token-client-spring/src/main/kotlin/no/nav/security/token/support/client/spring/oauth2/DefaultOAuth2HttpClient.kt @@ -28,10 +28,10 @@ import org.springframework.web.client.RestOperations LinkedMultiValueMap().apply { setAll(formParameters) }, headers(this), POST, - tokenEndpointUrl) + tokenEndpointUrl!!) } - private fun headers(req: OAuth2HttpRequest): HttpHeaders = HttpHeaders().apply { putAll(req.oAuth2HttpHeaders.headers()) } + private fun headers(req: OAuth2HttpRequest): HttpHeaders = HttpHeaders().apply { req.oAuth2HttpHeaders?.let { putAll(it.headers) } } override fun toString() = "$javaClass.simpleName [restTemplate=$restOperations]" } \ No newline at end of file diff --git a/token-client-spring/src/main/kotlin/no/nav/security/token/support/client/spring/oauth2/OAuth2ClientRequestInterceptor.kt b/token-client-spring/src/main/kotlin/no/nav/security/token/support/client/spring/oauth2/OAuth2ClientRequestInterceptor.kt index 5a685097..7ecb49e1 100644 --- a/token-client-spring/src/main/kotlin/no/nav/security/token/support/client/spring/oauth2/OAuth2ClientRequestInterceptor.kt +++ b/token-client-spring/src/main/kotlin/no/nav/security/token/support/client/spring/oauth2/OAuth2ClientRequestInterceptor.kt @@ -22,10 +22,10 @@ import org.springframework.http.client.ClientHttpResponse class OAuth2ClientRequestInterceptor(private val properties: ClientConfigurationProperties, private val service: OAuth2AccessTokenService, private val matcher: ClientConfigurationPropertiesMatcher) : ClientHttpRequestInterceptor { - override fun intercept(req: HttpRequest, body: ByteArray, execution: ClientHttpRequestExecution): ClientHttpResponse { - matcher.findProperties(properties, req.uri).orElse(null) - ?.let { req.headers.setBearerAuth(service.getAccessToken(it).accessToken) } - return execution.execute(req, body) + override fun intercept(req: HttpRequest, body: ByteArray, execution: ClientHttpRequestExecution): ClientHttpResponse { + matcher.findProperties(properties, req.uri).orElse(null) + ?.let { service.getAccessToken(it)?.accessToken?.let { it1 -> req.headers.setBearerAuth(it1) } } + return execution.execute(req, body) } override fun toString() = "$javaClass.simpleName [properties=$properties, service=$service, matcher=$matcher]" diff --git a/token-client-spring/src/test/kotlin/no/nav/security/token/support/client/spring/oauth2/ClientConfigurationPropertiesTest.kt b/token-client-spring/src/test/kotlin/no/nav/security/token/support/client/spring/oauth2/ClientConfigurationPropertiesTest.kt index 275f8d21..b4a8d196 100644 --- a/token-client-spring/src/test/kotlin/no/nav/security/token/support/client/spring/oauth2/ClientConfigurationPropertiesTest.kt +++ b/token-client-spring/src/test/kotlin/no/nav/security/token/support/client/spring/oauth2/ClientConfigurationPropertiesTest.kt @@ -44,7 +44,7 @@ internal class ClientConfigurationPropertiesTest { assertThat(clientConfigurationProperties.registration).isNotNull val clientProperties = clientConfigurationProperties.registration["example1-token-exchange1"] assertThat(clientProperties).isNotNull - assertThat(clientProperties!!.tokenExchange.audience).isNotBlank + assertThat(clientProperties!!.tokenExchange?.audience).isNotBlank } @Test @@ -67,8 +67,8 @@ internal class ClientConfigurationPropertiesTest { fun testDifferentClientPropsShouldNOTBeEqualAndShouldMakeSurroundingRequestsUnequalToo() { val props = clientConfigurationProperties.registration assertThat(props.size).isGreaterThan(1) - val p1 = props.get("example1-onbehalfof") - val p2 = props.get("example1-onbehalfof2") + val p1 = props.get("example1-onbehalfof")!! + val p2 = props.get("example1-onbehalfof2")!! assertThat(p1 == p2).isFalse val assertion = "123" val r1 = OnBehalfOfGrantRequest(p1, assertion) diff --git a/token-client-spring/src/test/kotlin/no/nav/security/token/support/client/spring/oauth2/OAuth2AccessTokenServiceIntegrationTest.kt b/token-client-spring/src/test/kotlin/no/nav/security/token/support/client/spring/oauth2/OAuth2AccessTokenServiceIntegrationTest.kt index 9501f705..4cfbf69c 100644 --- a/token-client-spring/src/test/kotlin/no/nav/security/token/support/client/spring/oauth2/OAuth2AccessTokenServiceIntegrationTest.kt +++ b/token-client-spring/src/test/kotlin/no/nav/security/token/support/client/spring/oauth2/OAuth2AccessTokenServiceIntegrationTest.kt @@ -88,19 +88,19 @@ internal class OAuth2AccessTokenServiceIntegrationTest { val auth = clientProperties.authentication Assertions.assertThat(usernamePwd).isEqualTo(auth.clientId + ":" + auth.clientSecret) Assertions.assertThat(body).contains( - "grant_type=" + URLEncoder.encode( - OAuth2GrantType.JWT_BEARER.value, - StandardCharsets.UTF_8)) + "grant_type=" + URLEncoder.encode( + OAuth2GrantType.JWT_BEARER.value, + StandardCharsets.UTF_8)) Assertions.assertThat(body).contains( - "scope=" + URLEncoder.encode( - java.lang.String.join(" ", clientProperties.scope), - StandardCharsets.UTF_8)) + "scope=" + URLEncoder.encode( + java.lang.String.join(" ", clientProperties.scope), + StandardCharsets.UTF_8)) Assertions.assertThat(body).contains("requested_token_use=on_behalf_of") Assertions.assertThat(body).contains("assertion=" + assertionResolver.token().orElse(null)) Assertions.assertThat(response).isNotNull - Assertions.assertThat(response.accessToken).isNotBlank - Assertions.assertThat(response.expiresAt).isGreaterThan(0) - Assertions.assertThat(response.expiresIn).isGreaterThan(0) + Assertions.assertThat(response?.accessToken).isNotBlank + Assertions.assertThat(response?.expiresAt).isGreaterThan(0) + Assertions.assertThat(response?.expiresIn).isGreaterThan(0) } @get:Throws(InterruptedException::class) @@ -108,7 +108,7 @@ internal class OAuth2AccessTokenServiceIntegrationTest { val accessTokenUsingTokenExhange: Unit get() { var clientProperties = clientConfigurationProperties.registration["example1-token" + - "-exchange1"] + "-exchange1"] Assertions.assertThat(clientProperties).isNotNull clientProperties = clientProperties!!.toBuilder() .tokenEndpointUrl(tokenEndpointUrl) @@ -122,14 +122,14 @@ internal class OAuth2AccessTokenServiceIntegrationTest { val body = request.body.readUtf8() Assertions.assertThat(headers["Content-Type"]).contains("application/x-www-form-urlencoded") Assertions.assertThat(body).contains( - "grant_type=" + URLEncoder.encode( - OAuth2GrantType.TOKEN_EXCHANGE.value, - StandardCharsets.UTF_8)) + "grant_type=" + URLEncoder.encode( + OAuth2GrantType.TOKEN_EXCHANGE.value, + StandardCharsets.UTF_8)) Assertions.assertThat(body).contains("subject_token=" + assertionResolver.token().orElse(null)) Assertions.assertThat(response).isNotNull - Assertions.assertThat(response.accessToken).isNotBlank - Assertions.assertThat(response.expiresAt).isGreaterThan(0) - Assertions.assertThat(response.expiresIn).isGreaterThan(0) + Assertions.assertThat(response?.accessToken).isNotBlank + Assertions.assertThat(response?.expiresAt).isGreaterThan(0) + Assertions.assertThat(response?.expiresIn).isGreaterThan(0) } @get:Throws(InterruptedException::class) @@ -158,38 +158,38 @@ internal class OAuth2AccessTokenServiceIntegrationTest { Assertions.assertThat(usernamePwd).isEqualTo(auth.clientId + ":" + auth.clientSecret) Assertions.assertThat(body).contains("grant_type=client_credentials") Assertions.assertThat(body).contains( - "scope=" + URLEncoder.encode( - java.lang.String.join(" ", clientProperties.scope), - StandardCharsets.UTF_8)) + "scope=" + URLEncoder.encode( + java.lang.String.join(" ", clientProperties.scope), + StandardCharsets.UTF_8)) Assertions.assertThat(body).doesNotContain("requested_token_use=on_behalf_of") Assertions.assertThat(body).doesNotContain("assertion=") Assertions.assertThat(response).isNotNull - Assertions.assertThat(response.accessToken).isNotBlank - Assertions.assertThat(response.expiresAt).isGreaterThan(0) - Assertions.assertThat(response.expiresIn).isGreaterThan(0) + Assertions.assertThat(response?.accessToken).isNotBlank + Assertions.assertThat(response?.expiresAt).isGreaterThan(0) + Assertions.assertThat(response?.expiresIn).isGreaterThan(0) } companion object { private const val TOKEN_RESPONSE = "{\n" + - " \"token_type\": \"Bearer\",\n" + - " \"scope\": \"scope1 scope2\",\n" + - " \"expires_at\": 1568141495,\n" + - " \"ext_expires_in\": 3599,\n" + - " \"expires_in\": 3599,\n" + - " \"access_token\": \"\",\n" + - " \"refresh_token\": \"\"\n" + - "}\n" + " \"token_type\": \"Bearer\",\n" + + " \"scope\": \"scope1 scope2\",\n" + + " \"expires_at\": 1568141495,\n" + + " \"ext_expires_in\": 3599,\n" + + " \"expires_in\": 3599,\n" + + " \"access_token\": \"\",\n" + + " \"refresh_token\": \"\"\n" + + "}\n" private val log = LoggerFactory.getLogger(OAuth2AccessTokenServiceIntegrationTest::class.java) private fun tokenValidationContext(sub: String): TokenValidationContext { val expiry = LocalDateTime.now().atZone(ZoneId.systemDefault()).plusSeconds(60).toInstant() val jwt: JWT = PlainJWT( - Builder() - .subject(sub) - .audience("thisapi") - .issuer("someIssuer") - .expirationTime(Date.from(expiry)) - .claim("jti", UUID.randomUUID().toString()) - .build()) + Builder() + .subject(sub) + .audience("thisapi") + .issuer("someIssuer") + .expirationTime(Date.from(expiry)) + .claim("jti", UUID.randomUUID().toString()) + .build()) val map: MutableMap = HashMap() map["issuer1"] = JwtToken(jwt.serialize()) return TokenValidationContext(map) diff --git a/token-client-spring/src/test/kotlin/no/nav/security/token/support/client/spring/oauth2/OAuth2ClientConfigurationWithCacheTest.kt b/token-client-spring/src/test/kotlin/no/nav/security/token/support/client/spring/oauth2/OAuth2ClientConfigurationWithCacheTest.kt index f2a6c46e..506f1ac3 100644 --- a/token-client-spring/src/test/kotlin/no/nav/security/token/support/client/spring/oauth2/OAuth2ClientConfigurationWithCacheTest.kt +++ b/token-client-spring/src/test/kotlin/no/nav/security/token/support/client/spring/oauth2/OAuth2ClientConfigurationWithCacheTest.kt @@ -28,12 +28,12 @@ internal class OAuth2ClientConfigurationWithCacheTest { @Autowired private lateinit var oAuth2AccessTokenService: OAuth2AccessTokenService - private lateinit var onBehalfOfCache: Cache - private lateinit var clientCredentialsCache: Cache + private lateinit var onBehalfOfCache: Cache + private lateinit var clientCredentialsCache: Cache @BeforeEach fun before() { - onBehalfOfCache = oAuth2AccessTokenService.onBehalfOfGrantCache - clientCredentialsCache = oAuth2AccessTokenService.clientCredentialsGrantCache + onBehalfOfCache = oAuth2AccessTokenService.onBehalfOfGrantCache!! + clientCredentialsCache = oAuth2AccessTokenService.clientCredentialsGrantCache!! } @Test diff --git a/token-client-spring/src/test/kotlin/no/nav/security/token/support/client/spring/oauth2/OAuth2ClientConfigurationWithoutCacheTest.kt b/token-client-spring/src/test/kotlin/no/nav/security/token/support/client/spring/oauth2/OAuth2ClientConfigurationWithoutCacheTest.kt index 20e78081..0b4d909a 100644 --- a/token-client-spring/src/test/kotlin/no/nav/security/token/support/client/spring/oauth2/OAuth2ClientConfigurationWithoutCacheTest.kt +++ b/token-client-spring/src/test/kotlin/no/nav/security/token/support/client/spring/oauth2/OAuth2ClientConfigurationWithoutCacheTest.kt @@ -27,8 +27,8 @@ internal class OAuth2ClientConfigurationWithoutCacheTest { @Autowired private lateinit var oAuth2AccessTokenService: OAuth2AccessTokenService - private var onBehalfOfCache: Cache? = null - private var clientCredentialsCache: Cache? = null + private var onBehalfOfCache: Cache? = null + private var clientCredentialsCache: Cache? = null @BeforeEach fun before() { onBehalfOfCache = oAuth2AccessTokenService.onBehalfOfGrantCache diff --git a/token-client-spring/src/test/resources/application-test.yml b/token-client-spring/src/test/resources/application-test.yml index 611ab516..4b0d0e6e 100644 --- a/token-client-spring/src/test/resources/application-test.yml +++ b/token-client-spring/src/test/resources/application-test.yml @@ -34,6 +34,7 @@ no.nav.security.jwt.client: authentication: client-id: testclient client-secret: testsecret + client-auth-method: client_secret_basic example1-clientcredentials3: token-endpoint-url: http://tokens.no From 9c29b5980655ce69506a1f6990c5d191ff5181be Mon Sep 17 00:00:00 2001 From: Jan-Olav Eide Date: Fri, 1 Dec 2023 13:47:13 +0100 Subject: [PATCH 2/5] version for maven coompiler plugin --- pom.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/pom.xml b/pom.xml index f54d2fe5..f8beb967 100644 --- a/pom.xml +++ b/pom.xml @@ -105,6 +105,7 @@ org.apache.maven.plugins maven-compiler-plugin + 3.11.0 -parameters From d8939389e9cbaf1f2210a7a7106643d07067ef7d Mon Sep 17 00:00:00 2001 From: Jan-Olav Eide Date: Fri, 1 Dec 2023 13:55:13 +0100 Subject: [PATCH 3/5] explicit plugin versions --- token-client-spring-demo/pom.xml | 1 + token-validation-filter/pom.xml | 2 +- token-validation-spring-demo/pom.xml | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/token-client-spring-demo/pom.xml b/token-client-spring-demo/pom.xml index 66827d49..1a2286bf 100644 --- a/token-client-spring-demo/pom.xml +++ b/token-client-spring-demo/pom.xml @@ -54,6 +54,7 @@ org.springframework.boot spring-boot-maven-plugin + ${spring-boot.version} org.apache.maven.plugins diff --git a/token-validation-filter/pom.xml b/token-validation-filter/pom.xml index 4a33efaa..4e6c3a66 100644 --- a/token-validation-filter/pom.xml +++ b/token-validation-filter/pom.xml @@ -32,7 +32,7 @@ org.apache.maven.plugins - maven-source-plugin + maven-compiler-plugin org.apache.maven.plugins diff --git a/token-validation-spring-demo/pom.xml b/token-validation-spring-demo/pom.xml index 4c7661df..b2193215 100644 --- a/token-validation-spring-demo/pom.xml +++ b/token-validation-spring-demo/pom.xml @@ -57,6 +57,7 @@ org.springframework.boot spring-boot-maven-plugin + ${spring-boot.version} org.apache.maven.plugins From 48b74c90a7aacce4c214595538c3685561d4a7d2 Mon Sep 17 00:00:00 2001 From: Jan-Olav Eide Date: Fri, 1 Dec 2023 14:52:51 +0100 Subject: [PATCH 4/5] unnecessary git status --- .../token/support/client/core/OAuth2CacheFactory.kt | 3 +-- .../client/core/oauth2/TokenExchangeGrantRequest.kt | 10 +++++----- .../client/core/http/OAuth2HttpHeadersTest.kt | 12 +++++------- 3 files changed, 11 insertions(+), 14 deletions(-) diff --git a/token-client-core/src/main/kotlin/no/nav/security/token/support/client/core/OAuth2CacheFactory.kt b/token-client-core/src/main/kotlin/no/nav/security/token/support/client/core/OAuth2CacheFactory.kt index 530933df..928aa473 100644 --- a/token-client-core/src/main/kotlin/no/nav/security/token/support/client/core/OAuth2CacheFactory.kt +++ b/token-client-core/src/main/kotlin/no/nav/security/token/support/client/core/OAuth2CacheFactory.kt @@ -18,8 +18,7 @@ object OAuth2CacheFactory { private fun evictOnResponseExpiresIn(skewInSeconds : Long) : Expiry { return object : Expiry { override fun expireAfterCreate(key : T, response : OAuth2AccessTokenResponse, currentTime : Long) = - SECONDS.toNanos(if (response.expiresIn!! > skewInSeconds) response.expiresIn!! - skewInSeconds else response.expiresIn!!.toLong()) - + SECONDS.toNanos(if (response.expiresIn!! > skewInSeconds) response.expiresIn - skewInSeconds else response.expiresIn.toLong()) override fun expireAfterUpdate(key : T, response : OAuth2AccessTokenResponse, currentTime : Long, currentDuration : Long) = currentDuration override fun expireAfterRead(key : T, response : OAuth2AccessTokenResponse, currentTime : Long, currentDuration : Long) = currentDuration } diff --git a/token-client-core/src/main/kotlin/no/nav/security/token/support/client/core/oauth2/TokenExchangeGrantRequest.kt b/token-client-core/src/main/kotlin/no/nav/security/token/support/client/core/oauth2/TokenExchangeGrantRequest.kt index e91fab58..1707b845 100644 --- a/token-client-core/src/main/kotlin/no/nav/security/token/support/client/core/oauth2/TokenExchangeGrantRequest.kt +++ b/token-client-core/src/main/kotlin/no/nav/security/token/support/client/core/oauth2/TokenExchangeGrantRequest.kt @@ -7,11 +7,11 @@ import no.nav.security.token.support.client.core.OAuth2GrantType.Companion.TOKEN class TokenExchangeGrantRequest(clientProperties : ClientProperties, val subjectToken : String) : AbstractOAuth2GrantRequest(TOKEN_EXCHANGE, clientProperties) { - override fun equals(o : Any?) : Boolean { - if (this === o) return true - if (o == null || javaClass != o.javaClass) return false - if (!super.equals(o)) return false - val that = o as TokenExchangeGrantRequest + override fun equals(other : Any?) : Boolean { + if (this === other) return true + if (other == null || javaClass != other.javaClass) return false + if (!super.equals(other)) return false + val that = other as TokenExchangeGrantRequest return subjectToken == that.subjectToken } diff --git a/token-client-core/src/test/kotlin/no/nav/security/token/support/client/core/http/OAuth2HttpHeadersTest.kt b/token-client-core/src/test/kotlin/no/nav/security/token/support/client/core/http/OAuth2HttpHeadersTest.kt index 02df73ae..25fe786c 100644 --- a/token-client-core/src/test/kotlin/no/nav/security/token/support/client/core/http/OAuth2HttpHeadersTest.kt +++ b/token-client-core/src/test/kotlin/no/nav/security/token/support/client/core/http/OAuth2HttpHeadersTest.kt @@ -1,7 +1,6 @@ package no.nav.security.token.support.client.core.http -import java.util.Map -import org.assertj.core.api.Assertions +import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.Test import no.nav.security.token.support.client.core.http.OAuth2HttpHeaders.Companion.builder import no.nav.security.token.support.client.core.http.OAuth2HttpHeaders.Companion.of @@ -14,10 +13,9 @@ internal class OAuth2HttpHeadersTest { .header("header1", "header1value1") .header("header1", "header1value2") .build() - val httpHeadersFromOf = of(Map.of("header1", listOf("header1value1", - "header1value2"))) - Assertions.assertThat(httpHeadersFromBuilder).isEqualTo(httpHeadersFromOf) - Assertions.assertThat(httpHeadersFromBuilder.headers).hasSize(1) - Assertions.assertThat(httpHeadersFromBuilder.headers).isEqualTo(httpHeadersFromOf.headers) + val httpHeadersFromOf = of(mutableMapOf(Pair("header1", listOf("header1value1", "header1value2")))) + assertThat(httpHeadersFromBuilder).isEqualTo(httpHeadersFromOf) + assertThat(httpHeadersFromBuilder.headers).hasSize(1) + assertThat(httpHeadersFromBuilder.headers).isEqualTo(httpHeadersFromOf.headers) } } \ No newline at end of file From e9777408b97589a5a55911cb4eff34c2b21e3d57 Mon Sep 17 00:00:00 2001 From: Jan-Olav Eide Date: Fri, 1 Dec 2023 15:22:21 +0100 Subject: [PATCH 5/5] bli kvitt noen advarsler --- .../client/core/oauth2/OAuth2AccessTokenServiceTest.kt | 2 +- .../client/core/oauth2/OnBehalfOfTokenClientTest.kt | 2 +- .../client/spring/oauth2/DefaultOAuth2HttpClient.kt | 6 +++--- .../validation/ConfigurableJwtTokenValidatorTest.java | 7 ++++--- .../token/support/jaxrs/TestTokenGeneratorResource.java | 9 +++++---- 5 files changed, 14 insertions(+), 12 deletions(-) diff --git a/token-client-core/src/test/kotlin/no/nav/security/token/support/client/core/oauth2/OAuth2AccessTokenServiceTest.kt b/token-client-core/src/test/kotlin/no/nav/security/token/support/client/core/oauth2/OAuth2AccessTokenServiceTest.kt index b79d0268..8d97be7f 100644 --- a/token-client-core/src/test/kotlin/no/nav/security/token/support/client/core/oauth2/OAuth2AccessTokenServiceTest.kt +++ b/token-client-core/src/test/kotlin/no/nav/security/token/support/client/core/oauth2/OAuth2AccessTokenServiceTest.kt @@ -49,7 +49,7 @@ internal class OAuth2AccessTokenServiceTest { @BeforeEach fun setup() { - MockitoAnnotations.initMocks(this) + MockitoAnnotations.openMocks(this) val oboCache = accessTokenResponseCache(10, 1) val clientCredentialsCache = accessTokenResponseCache(10, 1) val exchangeTokenCache = accessTokenResponseCache(10, 1) diff --git a/token-client-core/src/test/kotlin/no/nav/security/token/support/client/core/oauth2/OnBehalfOfTokenClientTest.kt b/token-client-core/src/test/kotlin/no/nav/security/token/support/client/core/oauth2/OnBehalfOfTokenClientTest.kt index 567e0a79..7ffe9ec7 100644 --- a/token-client-core/src/test/kotlin/no/nav/security/token/support/client/core/oauth2/OnBehalfOfTokenClientTest.kt +++ b/token-client-core/src/test/kotlin/no/nav/security/token/support/client/core/oauth2/OnBehalfOfTokenClientTest.kt @@ -25,7 +25,7 @@ internal class OnBehalfOfTokenClientTest { @BeforeEach @Throws(IOException::class) fun setup() { - MockitoAnnotations.initMocks(this) + MockitoAnnotations.openMocks(this) server = MockWebServer() server!!.start() tokenEndpointUrl = server!!.url(TOKEN_ENDPOINT).toString() diff --git a/token-client-spring/src/main/kotlin/no/nav/security/token/support/client/spring/oauth2/DefaultOAuth2HttpClient.kt b/token-client-spring/src/main/kotlin/no/nav/security/token/support/client/spring/oauth2/DefaultOAuth2HttpClient.kt index 3f92633a..61fce842 100644 --- a/token-client-spring/src/main/kotlin/no/nav/security/token/support/client/spring/oauth2/DefaultOAuth2HttpClient.kt +++ b/token-client-spring/src/main/kotlin/no/nav/security/token/support/client/spring/oauth2/DefaultOAuth2HttpClient.kt @@ -15,11 +15,11 @@ import org.springframework.web.client.RestOperations constructor(builder: RestTemplateBuilder) :this(builder.build()) - override fun post(req: OAuth2HttpRequest) = + override fun post(oAuth2HttpRequest: OAuth2HttpRequest) = try { - restOperations.exchange(convert(req), OAuth2AccessTokenResponse::class.java).body + restOperations.exchange(convert(oAuth2HttpRequest), OAuth2AccessTokenResponse::class.java).body } catch (e: HttpStatusCodeException) { - throw OAuth2ClientException("Received $e.statusCode from tokenendpoint $req.tokenEndpointUrl with responsebody $e.responseBodyAsString", e) + throw OAuth2ClientException("Received $e.statusCode from tokenendpoint $oAuth2HttpRequest.tokenEndpointUrl with responsebody $e.responseBodyAsString", e) } private fun convert(req: OAuth2HttpRequest) = diff --git a/token-validation-core/src/test/java/no/nav/security/token/support/core/validation/ConfigurableJwtTokenValidatorTest.java b/token-validation-core/src/test/java/no/nav/security/token/support/core/validation/ConfigurableJwtTokenValidatorTest.java index 67ac617b..5aa72efe 100644 --- a/token-validation-core/src/test/java/no/nav/security/token/support/core/validation/ConfigurableJwtTokenValidatorTest.java +++ b/token-validation-core/src/test/java/no/nav/security/token/support/core/validation/ConfigurableJwtTokenValidatorTest.java @@ -1,5 +1,6 @@ package no.nav.security.token.support.core.validation; +import com.nimbusds.jose.jwk.source.JWKSourceBuilder; import com.nimbusds.jose.jwk.source.RemoteJWKSet; import com.nimbusds.jwt.JWT; import no.nav.security.token.support.core.exceptions.JwtTokenValidatorException; @@ -36,10 +37,10 @@ private ConfigurableJwtTokenValidator tokenValidator(String issuer, List return new ConfigurableJwtTokenValidator( issuer, optionalClaims, - new RemoteJWKSet<>(URI.create("https://someurl").toURL(), new MockResourceRetriever()) - ); + // JWKSourceBuilder.create(URI.create("https://someurl").toURL(),new MockResourceRetriever()).build()); + new RemoteJWKSet<>(URI.create("https://someurl").toURL(), new MockResourceRetriever())); } catch (MalformedURLException e) { throw new RuntimeException(e); } } -} +} \ No newline at end of file diff --git a/token-validation-jaxrs/src/test/java/no/nav/security/token/support/jaxrs/TestTokenGeneratorResource.java b/token-validation-jaxrs/src/test/java/no/nav/security/token/support/jaxrs/TestTokenGeneratorResource.java index 5a50ca79..446193e6 100644 --- a/token-validation-jaxrs/src/test/java/no/nav/security/token/support/jaxrs/TestTokenGeneratorResource.java +++ b/token-validation-jaxrs/src/test/java/no/nav/security/token/support/jaxrs/TestTokenGeneratorResource.java @@ -18,6 +18,7 @@ import java.io.IOException; import java.net.URI; import java.nio.charset.Charset; +import java.util.Objects; @Path("local") public class TestTokenGeneratorResource { @@ -63,7 +64,7 @@ public Response addCookie( SignedJWT token = JwtTokenGenerator.createSignedJWT(subject); return Response.status(redirect == null ? Response.Status.OK : Response.Status.FOUND) .location(redirect == null ? null : URI.create(redirect)) - .cookie(new NewCookie(cookieName, token.serialize(), "/", "localhost", "", -1, false)) + .cookie(new NewCookie.Builder(cookieName).value(token.serialize()).path("/").domain("localhost").maxAge(-1).secure(false).build()) .build(); } @@ -72,7 +73,7 @@ public Response addCookie( @Path("/jwks") public String jwks() throws IOException { return IOUtils.readInputStreamToString( - getClass().getResourceAsStream(JwkGenerator.DEFAULT_JWKSET_FILE), + Objects.requireNonNull(getClass().getResourceAsStream(JwkGenerator.DEFAULT_JWKSET_FILE)), Charset.defaultCharset()); } @@ -87,7 +88,7 @@ public JWKSet jwkSet() { @GET @Path("/metadata") public String metadata() throws IOException { - return IOUtils.readInputStreamToString(getClass().getResourceAsStream("/metadata.json"), + return IOUtils.readInputStreamToString(Objects.requireNonNull(getClass().getResourceAsStream("/metadata.json")), Charset.defaultCharset()); } @@ -117,4 +118,4 @@ public String[] getParams() { return params; } } -} +} \ No newline at end of file