diff --git a/docs/configuration.md b/docs/configuration.md index 011a2b4dc..ca49f4e34 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -127,7 +127,7 @@ datafeed: initialIntervalMillis: 2000 multiplier: 1.5 maxIntervalMillis: 10000 - + retry: maxAttempts: 6 # set '-1' for an infinite number of attempts, default value is '10' initialIntervalMillis: 2000 diff --git a/symphony-bdk-core/src/main/java/com/symphony/bdk/core/ServiceFactory.java b/symphony-bdk-core/src/main/java/com/symphony/bdk/core/ServiceFactory.java index 2d07fff6c..3aa8424e2 100644 --- a/symphony-bdk-core/src/main/java/com/symphony/bdk/core/ServiceFactory.java +++ b/symphony-bdk-core/src/main/java/com/symphony/bdk/core/ServiceFactory.java @@ -1,6 +1,10 @@ package com.symphony.bdk.core; +import static com.symphony.bdk.core.auth.impl.OAuthentication.BEARER_AUTH; + import com.symphony.bdk.core.auth.AuthSession; +import com.symphony.bdk.core.auth.impl.OAuthSession; +import com.symphony.bdk.core.auth.impl.OAuthentication; import com.symphony.bdk.core.client.ApiClientFactory; import com.symphony.bdk.core.config.model.BdkConfig; import com.symphony.bdk.core.retry.RetryWithRecoveryBuilder; @@ -43,6 +47,7 @@ import com.symphony.bdk.http.api.ApiClient; import com.symphony.bdk.template.api.TemplateEngine; +import lombok.extern.slf4j.Slf4j; import org.apiguardian.api.API; /** @@ -56,6 +61,7 @@ *
  • {@link SessionService}
  • * */ +@Slf4j @API(status = API.Status.INTERNAL) class ServiceFactory { @@ -68,13 +74,24 @@ class ServiceFactory { private final RetryWithRecoveryBuilder retryBuilder; public ServiceFactory(ApiClientFactory apiClientFactory, AuthSession authSession, BdkConfig config) { + this.config = config; this.podClient = apiClientFactory.getPodClient(); this.agentClient = apiClientFactory.getAgentClient(); this.datafeedAgentClient = apiClientFactory.getDatafeedAgentClient(); this.authSession = authSession; this.templateEngine = TemplateEngine.getDefaultImplementation(); - this.config = config; this.retryBuilder = new RetryWithRecoveryBuilder<>().retryConfig(config.getRetry()); + + if (config.isCommonJwtEnabled()) { + if (config.isOboConfigured()) { + throw new UnsupportedOperationException("Common JWT feature is not available yet in OBO mode," + + " please set commonJwt.enabled to false."); + } else { + final OAuthSession oAuthSession = new OAuthSession(authSession); + this.podClient.getAuthentications().put(BEARER_AUTH, new OAuthentication(oAuthSession::getBearerToken)); + this.podClient.addEnforcedAuthenticationScheme(BEARER_AUTH); + } + } } /** @@ -83,7 +100,8 @@ public ServiceFactory(ApiClientFactory apiClientFactory, AuthSession authSession * @return a new {@link UserService} instance. */ public UserService getUserService() { - return new UserService(new UserApi(podClient), new UsersApi(podClient), new AuditTrailApi(agentClient), authSession, retryBuilder); + return new UserService(new UserApi(podClient), new UsersApi(podClient), new AuditTrailApi(agentClient), authSession, + retryBuilder); } /** @@ -187,4 +205,5 @@ public ApplicationService getApplicationService() { public HealthService getHealthService() { return new HealthService(new SystemApi(this.agentClient), new SignalsApi(this.agentClient), this.authSession); } + } diff --git a/symphony-bdk-core/src/main/java/com/symphony/bdk/core/auth/AuthSession.java b/symphony-bdk-core/src/main/java/com/symphony/bdk/core/auth/AuthSession.java index 09d059e18..17424846e 100644 --- a/symphony-bdk-core/src/main/java/com/symphony/bdk/core/auth/AuthSession.java +++ b/symphony-bdk-core/src/main/java/com/symphony/bdk/core/auth/AuthSession.java @@ -23,6 +23,17 @@ public interface AuthSession { */ @Nullable String getSessionToken(); + /** + * Pod's Common JWT authentication token. When commonJwt.enabled is set to true in the configuration, an OAuth + * authentication scheme is used where the session token acts as the refresh token and the authorization token is a + * short lived access token. + * + * @return the Pod Authorization token + */ + @Nullable default String getAuthorizationToken() throws AuthUnauthorizedException { + return null; + } + /** * KeyManager's authentication token. * diff --git a/symphony-bdk-core/src/main/java/com/symphony/bdk/core/auth/AuthenticatorFactory.java b/symphony-bdk-core/src/main/java/com/symphony/bdk/core/auth/AuthenticatorFactory.java index 882daca25..99677bca9 100644 --- a/symphony-bdk-core/src/main/java/com/symphony/bdk/core/auth/AuthenticatorFactory.java +++ b/symphony-bdk-core/src/main/java/com/symphony/bdk/core/auth/AuthenticatorFactory.java @@ -74,6 +74,8 @@ BotAuthenticator getBotAuthenticator() throws AuthInitializationException { return new BotAuthenticatorCertImpl( this.config.getRetry(), this.config.getBot().getUsername(), + this.config.getCommonJwt(), + this.apiClientFactory.getLoginClient(), this.apiClientFactory.getSessionAuthClient(), this.apiClientFactory.getKeyAuthClient() ); @@ -86,6 +88,7 @@ BotAuthenticator getBotAuthenticator() throws AuthInitializationException { return new BotAuthenticatorRsaImpl( this.config.getRetry(), this.config.getBot().getUsername(), + this.config.getCommonJwt(), this.loadPrivateKeyFromAuthenticationConfig(this.config.getBot()), this.apiClientFactory.getLoginClient(), this.apiClientFactory.getRelayClient() diff --git a/symphony-bdk-core/src/main/java/com/symphony/bdk/core/auth/exception/AuthUnauthorizedException.java b/symphony-bdk-core/src/main/java/com/symphony/bdk/core/auth/exception/AuthUnauthorizedException.java index c7a3471af..8ecdabd68 100644 --- a/symphony-bdk-core/src/main/java/com/symphony/bdk/core/auth/exception/AuthUnauthorizedException.java +++ b/symphony-bdk-core/src/main/java/com/symphony/bdk/core/auth/exception/AuthUnauthorizedException.java @@ -16,6 +16,10 @@ @API(status = API.Status.STABLE) public class AuthUnauthorizedException extends Exception { + public AuthUnauthorizedException(@Nonnull String message) { + super(message); + } + public AuthUnauthorizedException(@Nonnull String message, @Nonnull ApiException source) { super(message, source); } diff --git a/symphony-bdk-core/src/main/java/com/symphony/bdk/core/auth/impl/AbstractBotAuthenticator.java b/symphony-bdk-core/src/main/java/com/symphony/bdk/core/auth/impl/AbstractBotAuthenticator.java index 079fe5922..57b9cf359 100644 --- a/symphony-bdk-core/src/main/java/com/symphony/bdk/core/auth/impl/AbstractBotAuthenticator.java +++ b/symphony-bdk-core/src/main/java/com/symphony/bdk/core/auth/impl/AbstractBotAuthenticator.java @@ -2,35 +2,86 @@ import com.symphony.bdk.core.auth.BotAuthenticator; import com.symphony.bdk.core.auth.exception.AuthUnauthorizedException; +import com.symphony.bdk.core.config.model.BdkCommonJwtConfig; import com.symphony.bdk.core.config.model.BdkRetryConfig; +import com.symphony.bdk.gen.api.AuthenticationApi; +import com.symphony.bdk.gen.api.model.JwtToken; +import com.symphony.bdk.gen.api.model.Token; import com.symphony.bdk.http.api.ApiClient; import com.symphony.bdk.http.api.ApiException; +import lombok.extern.slf4j.Slf4j; import org.apiguardian.api.API; +import javax.annotation.Nonnull; + /** * Abstract class to factorize the {@link BotAuthenticator} logic between RSA and certificate, * especially the retry logic on top of HTTP calls. */ +@Slf4j @API(status = API.Status.INTERNAL) public abstract class AbstractBotAuthenticator implements BotAuthenticator { - private final AuthenticationRetry authenticationRetry; + protected final ApiClient loginApiClient; + private final BdkCommonJwtConfig commonJwtConfig; + + private final AuthenticationRetry kmAuthenticationRetry; + private final AuthenticationRetry podAuthenticationRetry; + private final AuthenticationRetry idmAuthenticationRetry; - public AbstractBotAuthenticator(BdkRetryConfig retryConfig) { - authenticationRetry = new AuthenticationRetry<>(retryConfig); + protected AbstractBotAuthenticator(BdkRetryConfig retryConfig, + @Nonnull BdkCommonJwtConfig commonJwtConfig, @Nonnull ApiClient loginApiClient) { + kmAuthenticationRetry = new AuthenticationRetry<>(retryConfig); + podAuthenticationRetry = new AuthenticationRetry<>(retryConfig); + idmAuthenticationRetry = new AuthenticationRetry<>(retryConfig); + this.commonJwtConfig = commonJwtConfig; + this.loginApiClient = loginApiClient; } - protected String retrieveToken(ApiClient client) throws AuthUnauthorizedException { + protected abstract String retrieveKeyManagerToken() throws AuthUnauthorizedException; + + protected String retrieveKeyManagerToken(ApiClient client) throws AuthUnauthorizedException { + final String unauthorizedMessage = String.format("Service account \"%s\" is not authorized to authenticate. " + + "Check if credentials are valid.", getBotUsername()); + + return kmAuthenticationRetry.executeAndRetry("AbstractBotAuthenticator.retrieveKeyManagerToken", + client.getBasePath(), () -> doRetrieveToken(client).getToken(), unauthorizedMessage); + } + + protected abstract Token retrieveSessionToken() throws AuthUnauthorizedException; + + protected Token retrieveSessionToken(ApiClient client) throws AuthUnauthorizedException { final String unauthorizedMessage = String.format("Service account \"%s\" is not authorized to authenticate. " + "Check if credentials are valid.", getBotUsername()); - return authenticationRetry.executeAndRetry("AbstractBotAuthenticator.retrieveToken", client.getBasePath(), - () -> authenticateAndGetToken(client), unauthorizedMessage); + return podAuthenticationRetry.executeAndRetry("AbstractBotAuthenticator.retrieveSessionToken", + client.getBasePath(), () -> this.doRetrieveToken(client), unauthorizedMessage); } - protected abstract String authenticateAndGetToken(ApiClient client) throws ApiException; + /** + * Login API to retrieve a token is the same for KM and pod. + */ + protected abstract Token doRetrieveToken(ApiClient client) throws ApiException; + + protected String retrieveAuthorizationToken(String sessionToken) throws AuthUnauthorizedException { + log.debug("Start retrieving authorizationToken using RSA authentication..."); + return this.doRetrieveAuthorizationToken(this.loginApiClient, sessionToken).getAccessToken(); + } + + private JwtToken doRetrieveAuthorizationToken(ApiClient client, String sessionToken) + throws AuthUnauthorizedException { + final String unauthorizedMessage = String.format("Service account \"%s\" is not authorized to authenticate. " + + "Check if credentials are valid.", getBotUsername()); + + // we are not using any scopes for now when calling pod APIs + return idmAuthenticationRetry.executeAndRetry("AbstractBotAuthenticator.retrieveAuthorizationToken", + client.getBasePath(), () -> new AuthenticationApi(client).idmTokensPost(sessionToken, ""), unauthorizedMessage); + } protected abstract String getBotUsername(); + public boolean isCommonJwtEnabled() { + return commonJwtConfig.getEnabled(); + } } diff --git a/symphony-bdk-core/src/main/java/com/symphony/bdk/core/auth/impl/AbstractOboAuthenticator.java b/symphony-bdk-core/src/main/java/com/symphony/bdk/core/auth/impl/AbstractOboAuthenticator.java index 1e90c901f..2b56e6576 100644 --- a/symphony-bdk-core/src/main/java/com/symphony/bdk/core/auth/impl/AbstractOboAuthenticator.java +++ b/symphony-bdk-core/src/main/java/com/symphony/bdk/core/auth/impl/AbstractOboAuthenticator.java @@ -21,7 +21,7 @@ public abstract class AbstractOboAuthenticator implements OboAuthenticator { protected final String appId; private final AuthenticationRetry authenticationRetry; - public AbstractOboAuthenticator(BdkRetryConfig retryConfig, String appId) { + protected AbstractOboAuthenticator(BdkRetryConfig retryConfig, String appId) { this.appId = appId; this.authenticationRetry = new AuthenticationRetry<>(retryConfig); } diff --git a/symphony-bdk-core/src/main/java/com/symphony/bdk/core/auth/impl/AuthSessionCertImpl.java b/symphony-bdk-core/src/main/java/com/symphony/bdk/core/auth/impl/AuthSessionCertImpl.java deleted file mode 100644 index e98d9fea3..000000000 --- a/symphony-bdk-core/src/main/java/com/symphony/bdk/core/auth/impl/AuthSessionCertImpl.java +++ /dev/null @@ -1,54 +0,0 @@ -package com.symphony.bdk.core.auth.impl; - -import com.symphony.bdk.core.auth.AuthSession; -import com.symphony.bdk.core.auth.exception.AuthUnauthorizedException; - -import org.apiguardian.api.API; - -/** - * {@link AuthSession} impl for certificate authentication mode. - */ -@API(status = API.Status.INTERNAL) -public class AuthSessionCertImpl implements AuthSession { - - private final BotAuthenticatorCertImpl authenticator; - - private String sessionToken; - private String keyManagerToken; - - public AuthSessionCertImpl(BotAuthenticatorCertImpl authenticator) { - this.authenticator = authenticator; - } - - /** - * {@inheritDoc} - */ - @Override - public String getSessionToken() { - return this.sessionToken; - } - - /** - * {@inheritDoc} - */ - @Override - public String getKeyManagerToken() { - return this.keyManagerToken; - } - - /** - * {@inheritDoc} - */ - @Override - public void refresh() throws AuthUnauthorizedException { - this.sessionToken = authenticator.retrieveSessionToken(); - this.keyManagerToken = authenticator.retrieveKeyManagerToken(); - } - - /** - * This method is only visible for testing. - */ - protected BotAuthenticatorCertImpl getAuthenticator() { - return authenticator; - } -} diff --git a/symphony-bdk-core/src/main/java/com/symphony/bdk/core/auth/impl/AuthSessionImpl.java b/symphony-bdk-core/src/main/java/com/symphony/bdk/core/auth/impl/AuthSessionImpl.java new file mode 100644 index 000000000..41c73f7b1 --- /dev/null +++ b/symphony-bdk-core/src/main/java/com/symphony/bdk/core/auth/impl/AuthSessionImpl.java @@ -0,0 +1,124 @@ +package com.symphony.bdk.core.auth.impl; + +import com.symphony.bdk.core.auth.AuthSession; +import com.symphony.bdk.core.auth.exception.AuthUnauthorizedException; +import com.symphony.bdk.core.auth.jwt.JwtHelper; +import com.symphony.bdk.gen.api.model.Token; + +import com.fasterxml.jackson.core.JsonProcessingException; +import org.apiguardian.api.API; + +import java.time.Duration; +import java.time.Instant; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + + +/** + * {@link AuthSession} impl for regular authentication mode. + */ +@API(status = API.Status.INTERNAL) +public class AuthSessionImpl implements AuthSession { + + public static final Duration LEEWAY = Duration.ofSeconds(5); + private final AbstractBotAuthenticator authenticator; + + /** + * Long-lived Session JWT Token (for pod APIs). + */ + private String sessionToken; + + /** + * Long-lived KM Token (for KM APIs). + */ + private String keyManagerToken; + + /** + * Short-lived access Token (for pod APIs). + */ + private String authorizationToken; + private Long authTokenExpirationDate; + + + public AuthSessionImpl(@Nonnull AbstractBotAuthenticator authenticator) { + this.authenticator = authenticator; + } + + /** + * {@inheritDoc} + */ + @Override + public @Nullable + String getSessionToken() { + return this.sessionToken; + } + + /** + * {@inheritDoc} + */ + @Override + public @Nullable + String getAuthorizationToken() throws AuthUnauthorizedException { + if(this.authorizationToken == null || this.authTokenExpirationDate == null) { + throw new UnsupportedOperationException("Common JWT feature is not available in your pod, " + + "SBE version should be at least 20.14."); + } + if (Instant.now().plus(LEEWAY).isAfter(Instant.ofEpochSecond(authTokenExpirationDate))) { + refresh(); + } + return this.authorizationToken; + } + /** + * {@inheritDoc} + */ + @Override + public @Nullable + String getKeyManagerToken() { + return this.keyManagerToken; + } + + /** + * {@inheritDoc} + */ + @Override + public void refresh() throws AuthUnauthorizedException { + if (this.sessionToken == null || !authenticator.isCommonJwtEnabled()) { + refreshAllTokens(); + } else { + // as we are using a short-lived token and a refresh token, let's first try to refresh the short lived token + // this way we avoid generating extra login events + try { + this.authorizationToken = authenticator.retrieveAuthorizationToken(sessionToken); + refreshExpirationDate(); + } catch (AuthUnauthorizedException e) { + refreshAllTokens(); + } + } + } + + private void refreshAllTokens() throws AuthUnauthorizedException { + Token authToken = authenticator.retrieveSessionToken(); + this.authorizationToken = authToken.getAuthorizationToken(); + this.sessionToken = authToken.getToken(); + refreshExpirationDate(); + this.keyManagerToken = this.authenticator.retrieveKeyManagerToken(); + } + + private void refreshExpirationDate() throws AuthUnauthorizedException { + if (this.authorizationToken != null) { + try { + this.authTokenExpirationDate = JwtHelper.extractExpirationDate(authorizationToken); + } catch (JsonProcessingException | AuthUnauthorizedException e) { + throw new AuthUnauthorizedException("Unable to parse the Authorization token received."); + } + } + } + + /** + * This method is only visible for testing. + */ + protected AbstractBotAuthenticator getAuthenticator() { + return this.authenticator; + } +} diff --git a/symphony-bdk-core/src/main/java/com/symphony/bdk/core/auth/impl/AuthSessionRsaImpl.java b/symphony-bdk-core/src/main/java/com/symphony/bdk/core/auth/impl/AuthSessionRsaImpl.java deleted file mode 100644 index 744afc30b..000000000 --- a/symphony-bdk-core/src/main/java/com/symphony/bdk/core/auth/impl/AuthSessionRsaImpl.java +++ /dev/null @@ -1,57 +0,0 @@ -package com.symphony.bdk.core.auth.impl; - -import com.symphony.bdk.core.auth.AuthSession; -import com.symphony.bdk.core.auth.exception.AuthUnauthorizedException; - -import org.apiguardian.api.API; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -/** - * {@link AuthSession} impl for regular authentication mode. - */ -@API(status = API.Status.INTERNAL) -public class AuthSessionRsaImpl implements AuthSession { - - private final BotAuthenticatorRsaImpl authenticator; - - private String sessionToken; - private String keyManagerToken; - - public AuthSessionRsaImpl(@Nonnull BotAuthenticatorRsaImpl authenticator) { - this.authenticator = authenticator; - } - - /** - * {@inheritDoc} - */ - @Override - public @Nullable String getSessionToken() { - return this.sessionToken; - } - - /** - * {@inheritDoc} - */ - @Override - public @Nullable String getKeyManagerToken() { - return this.keyManagerToken; - } - - /** - * {@inheritDoc} - */ - @Override - public void refresh() throws AuthUnauthorizedException { - this.sessionToken = this.authenticator.retrieveSessionToken(); - this.keyManagerToken = this.authenticator.retrieveKeyManagerToken(); - } - - /** - * This method is only visible for testing. - */ - protected BotAuthenticatorRsaImpl getAuthenticator() { - return this.authenticator; - } -} diff --git a/symphony-bdk-core/src/main/java/com/symphony/bdk/core/auth/impl/BotAuthenticatorCertImpl.java b/symphony-bdk-core/src/main/java/com/symphony/bdk/core/auth/impl/BotAuthenticatorCertImpl.java index c93a38b92..621464336 100644 --- a/symphony-bdk-core/src/main/java/com/symphony/bdk/core/auth/impl/BotAuthenticatorCertImpl.java +++ b/symphony-bdk-core/src/main/java/com/symphony/bdk/core/auth/impl/BotAuthenticatorCertImpl.java @@ -2,6 +2,7 @@ import com.symphony.bdk.core.auth.AuthSession; import com.symphony.bdk.core.auth.exception.AuthUnauthorizedException; +import com.symphony.bdk.core.config.model.BdkCommonJwtConfig; import com.symphony.bdk.core.config.model.BdkRetryConfig; import com.symphony.bdk.gen.api.CertificateAuthenticationApi; import com.symphony.bdk.gen.api.model.Token; @@ -29,9 +30,11 @@ public class BotAuthenticatorCertImpl extends AbstractBotAuthenticator { public BotAuthenticatorCertImpl( @Nonnull BdkRetryConfig retryConfig, @Nonnull String username, + @Nonnull BdkCommonJwtConfig commonJwtConfig, + @Nonnull ApiClient loginClient, @Nonnull ApiClient sessionAuthClient, @Nonnull ApiClient keyAuthClient) { - super(retryConfig); + super(retryConfig, commonJwtConfig, loginClient); this.sessionAuthClient = sessionAuthClient; this.keyAuthClient = keyAuthClient; this.username = username; @@ -43,28 +46,30 @@ public BotAuthenticatorCertImpl( @Override @Nonnull public AuthSession authenticateBot() throws AuthUnauthorizedException { - AuthSessionCertImpl authSession = new AuthSessionCertImpl(this); + AuthSessionImpl authSession = new AuthSessionImpl(this); authSession.refresh(); return authSession; } + @Override @Nonnull - protected String retrieveSessionToken() throws AuthUnauthorizedException { - log.debug("Start retrieving sessionToken using certificate authentication..."); - return retrieveToken(this.sessionAuthClient); + protected Token retrieveSessionToken() throws AuthUnauthorizedException { + log.debug("Start retrieving authentication tokens using certificate authentication..."); + return retrieveSessionToken(this.sessionAuthClient); } + @Override @Nonnull protected String retrieveKeyManagerToken() throws AuthUnauthorizedException { log.debug("Start retrieving keyManagerToken using certificate authentication..."); - return retrieveToken(this.keyAuthClient); + return retrieveKeyManagerToken(this.keyAuthClient); } @Override - protected String authenticateAndGetToken(ApiClient client) throws ApiException { + protected Token doRetrieveToken(ApiClient client) throws ApiException { final Token token = new CertificateAuthenticationApi(client).v1AuthenticatePost(); log.debug("{} successfully retrieved.", token.getName()); - return token.getToken(); + return token; } @Override diff --git a/symphony-bdk-core/src/main/java/com/symphony/bdk/core/auth/impl/BotAuthenticatorRsaImpl.java b/symphony-bdk-core/src/main/java/com/symphony/bdk/core/auth/impl/BotAuthenticatorRsaImpl.java index f20ad2284..9d0e9cc93 100644 --- a/symphony-bdk-core/src/main/java/com/symphony/bdk/core/auth/impl/BotAuthenticatorRsaImpl.java +++ b/symphony-bdk-core/src/main/java/com/symphony/bdk/core/auth/impl/BotAuthenticatorRsaImpl.java @@ -3,6 +3,7 @@ import com.symphony.bdk.core.auth.AuthSession; import com.symphony.bdk.core.auth.exception.AuthUnauthorizedException; import com.symphony.bdk.core.auth.jwt.JwtHelper; +import com.symphony.bdk.core.config.model.BdkCommonJwtConfig; import com.symphony.bdk.core.config.model.BdkRetryConfig; import com.symphony.bdk.gen.api.AuthenticationApi; import com.symphony.bdk.gen.api.model.AuthenticateRequest; @@ -29,20 +30,19 @@ public class BotAuthenticatorRsaImpl extends AbstractBotAuthenticator { private final String username; private final PrivateKey privateKey; - private final ApiClient loginApiClient; private final ApiClient relayApiClient; public BotAuthenticatorRsaImpl( @Nonnull BdkRetryConfig retryConfig, @Nonnull String username, + @Nonnull BdkCommonJwtConfig commonJwtConfig, @Nonnull PrivateKey privateKey, @Nonnull ApiClient loginApiClient, @Nonnull ApiClient relayApiClient ) { - super(retryConfig); + super(retryConfig, commonJwtConfig, loginApiClient); this.username = username; this.privateKey = privateKey; - this.loginApiClient = loginApiClient; this.relayApiClient = relayApiClient; } @@ -51,30 +51,30 @@ public BotAuthenticatorRsaImpl( */ @Override public @Nonnull AuthSession authenticateBot() throws AuthUnauthorizedException { - final AuthSessionRsaImpl authSession = new AuthSessionRsaImpl(this); + final AuthSessionImpl authSession = new AuthSessionImpl(this); authSession.refresh(); return authSession; } - protected String retrieveSessionToken() throws AuthUnauthorizedException { - log.debug("Start retrieving sessionToken using RSA authentication..."); - return this.retrieveToken(this.loginApiClient); + protected Token retrieveSessionToken() throws AuthUnauthorizedException { + log.debug("Start retrieving authentication tokens using RSA authentication..."); + return this.retrieveSessionToken(this.loginApiClient); } protected String retrieveKeyManagerToken() throws AuthUnauthorizedException { log.debug("Start retrieving keyManagerToken using RSA authentication..."); - return this.retrieveToken(this.relayApiClient); + return this.retrieveKeyManagerToken(this.relayApiClient); } @Override - protected String authenticateAndGetToken(ApiClient client) throws ApiException { + protected Token doRetrieveToken(ApiClient client) throws ApiException { final String jwt = JwtHelper.createSignedJwt(this.username, JwtHelper.JWT_EXPIRATION_MILLIS, this.privateKey); final AuthenticateRequest req = new AuthenticateRequest(); req.setToken(jwt); final Token token = new AuthenticationApi(client).pubkeyAuthenticatePost(req); log.debug("{} successfully retrieved.", token.getName()); - return token.getToken(); + return token; } @Override diff --git a/symphony-bdk-core/src/main/java/com/symphony/bdk/core/auth/impl/OAuthSession.java b/symphony-bdk-core/src/main/java/com/symphony/bdk/core/auth/impl/OAuthSession.java new file mode 100644 index 000000000..67c25f3a1 --- /dev/null +++ b/symphony-bdk-core/src/main/java/com/symphony/bdk/core/auth/impl/OAuthSession.java @@ -0,0 +1,26 @@ +package com.symphony.bdk.core.auth.impl; + +import com.symphony.bdk.core.auth.AuthSession; +import com.symphony.bdk.core.auth.exception.AuthUnauthorizedException; +import com.symphony.bdk.http.api.ApiException; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apiguardian.api.API; + +@Slf4j +@RequiredArgsConstructor +@API(status = API.Status.INTERNAL) +public +class OAuthSession { + + private final AuthSession authSession; + + public String getBearerToken() throws ApiException { + try { + return authSession.getAuthorizationToken(); + } catch (AuthUnauthorizedException e) { + throw new ApiException(401, e); + } + } +} diff --git a/symphony-bdk-core/src/main/java/com/symphony/bdk/core/auth/impl/OAuthentication.java b/symphony-bdk-core/src/main/java/com/symphony/bdk/core/auth/impl/OAuthentication.java new file mode 100644 index 000000000..42dcd16a0 --- /dev/null +++ b/symphony-bdk-core/src/main/java/com/symphony/bdk/core/auth/impl/OAuthentication.java @@ -0,0 +1,24 @@ +package com.symphony.bdk.core.auth.impl; +import com.symphony.bdk.core.util.function.SupplierWithApiException; +import com.symphony.bdk.http.api.ApiException; +import com.symphony.bdk.http.api.auth.Authentication; + +import lombok.RequiredArgsConstructor; +import org.apiguardian.api.API; + +import java.util.Map; + +@RequiredArgsConstructor +@API(status = API.Status.INTERNAL) +public class OAuthentication implements Authentication { + + public static final String BEARER_AUTH = "bearerAuth"; + + private final SupplierWithApiException bearerAuthSupplier; + + @Override + public void apply(Map headerParams) throws ApiException { + headerParams.remove("sessionToken"); + headerParams.put("Authorization", this.bearerAuthSupplier.get()); + } +} diff --git a/symphony-bdk-core/src/main/java/com/symphony/bdk/core/auth/jwt/JwtHelper.java b/symphony-bdk-core/src/main/java/com/symphony/bdk/core/auth/jwt/JwtHelper.java index 914a97b76..74c361f2d 100644 --- a/symphony-bdk-core/src/main/java/com/symphony/bdk/core/auth/jwt/JwtHelper.java +++ b/symphony-bdk-core/src/main/java/com/symphony/bdk/core/auth/jwt/JwtHelper.java @@ -1,8 +1,11 @@ package com.symphony.bdk.core.auth.jwt; import com.symphony.bdk.core.auth.exception.AuthInitializationException; +import com.symphony.bdk.core.auth.exception.AuthUnauthorizedException; +import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; import io.jsonwebtoken.Claims; import io.jsonwebtoken.JwtException; import io.jsonwebtoken.Jwts; @@ -45,61 +48,62 @@ public class JwtHelper { // Expiration of the jwt, 5 minutes maximum, use a little less than that in case of different clock skews public static final Long JWT_EXPIRATION_MILLIS = 240_000L; - // PKCS#8 format - private static final String PEM_PRIVATE_START = "-----BEGIN PRIVATE KEY-----"; - private static final String PEM_PRIVATE_END = "-----END PRIVATE KEY-----"; + // PKCS#8 format + private static final String PEM_PRIVATE_START = "-----BEGIN PRIVATE KEY-----"; + private static final String PEM_PRIVATE_END = "-----END PRIVATE KEY-----"; - // PKCS#1 format - private static final String PEM_RSA_PRIVATE_START = "-----BEGIN RSA PRIVATE KEY-----"; + // PKCS#1 format + private static final String PEM_RSA_PRIVATE_START = "-----BEGIN RSA PRIVATE KEY-----"; - // X509 certificate + // X509 certificate private static final String BEGIN_CERTIFICATE = "-----BEGIN CERTIFICATE-----"; private static final String END_CERTIFICATE = "-----END CERTIFICATE-----"; private static final ObjectMapper mapper = new ObjectMapper(); - /** - * Creates a JWT with the provided user name and expiration date, signed with the provided private key. - * @param user the username to authenticate; will be verified by the pod - * @param expiration of the authentication request in milliseconds; cannot be longer than the value defined on the - * pod - * @param privateKey the private RSA key to be used to sign the authentication request; will be checked on the pod - * against - * the public key stored for the user - * @return a signed JWT for a specific user (or subject) - */ - public static String createSignedJwt(String user, long expiration, Key privateKey) { - return Jwts.builder() - .setSubject(user) - .setExpiration(new Date(System.currentTimeMillis() + expiration)) - .signWith(SignatureAlgorithm.RS512, privateKey) - .compact(); - } - - /** - * Creates a RSA Private Key from a PEM String. It supports PKCS#1 and PKCS#8 string formats. - * - * @param pemPrivateKey RSA Private Key content - * @return a {@link PrivateKey} instance - * @throws GeneralSecurityException On invalid Private Key - */ - public static PrivateKey parseRsaPrivateKey(final String pemPrivateKey) - throws GeneralSecurityException { - - // PKCS#8 format - if (pemPrivateKey.contains(PEM_PRIVATE_START)) { - return parsePKCS8PrivateKey(pemPrivateKey); - } - // PKCS#1 format - else if (pemPrivateKey.contains(PEM_RSA_PRIVATE_START)) { - return parsePKCS1PrivateKey(pemPrivateKey); - } - // format not detected - else { - throw new GeneralSecurityException("Invalid private key. Header not recognized, only PKCS8 and PKCS1 format are allowed."); - } - } + /** + * Creates a JWT with the provided user name and expiration date, signed with the provided private key. + * + * @param user the username to authenticate; will be verified by the pod + * @param expiration of the authentication request in milliseconds; cannot be longer than the value defined on the + * pod + * @param privateKey the private RSA key to be used to sign the authentication request; will be checked on the pod + * against + * the public key stored for the user + * @return a signed JWT for a specific user (or subject) + */ + public static String createSignedJwt(String user, long expiration, Key privateKey) { + return Jwts.builder() + .setSubject(user) + .setExpiration(new Date(System.currentTimeMillis() + expiration)) + .signWith(SignatureAlgorithm.RS512, privateKey) + .compact(); + } + + /** + * Creates a RSA Private Key from a PEM String. It supports PKCS#1 and PKCS#8 string formats. + * + * @param pemPrivateKey RSA Private Key content + * @return a {@link PrivateKey} instance + * @throws GeneralSecurityException On invalid Private Key + */ + public static PrivateKey parseRsaPrivateKey(final String pemPrivateKey) throws GeneralSecurityException { + + // PKCS#8 format + if (pemPrivateKey.contains(PEM_PRIVATE_START)) { + return parsePKCS8PrivateKey(pemPrivateKey); + } + // PKCS#1 format + else if (pemPrivateKey.contains(PEM_RSA_PRIVATE_START)) { + return parsePKCS1PrivateKey(pemPrivateKey); + } + // format not detected + else { + throw new GeneralSecurityException( + "Invalid private key. Header not recognized, only PKCS8 and PKCS1 format are allowed."); + } + } /** * Validates a jwt against a certificate. @@ -117,35 +121,64 @@ public static UserClaim validateJwt(String jwt, String certificate) throws AuthI return mapper.convertValue(body.get("user"), UserClaim.class); } catch (JwtException e) { - throw new AuthInitializationException("Unable to validate jwt", e); + throw new AuthInitializationException("Unable to validate JWT", e); + } + } + + /** + * Extract the expiration date (in seconds) from the input jwt. If the jwt uses the Beare prefix, it + * will be removed before parsing. This function is not validating the jwt signature. + * + * @param jwt to be parsed + * @return expiration date in seconds + * @throws JsonProcessingException if parsing fails + */ + public static Long extractExpirationDate(String jwt) throws JsonProcessingException, AuthUnauthorizedException { + String claimsObj = extractDecodedClaims(dropBearer(jwt)); + ObjectNode claims = mapper.readValue(claimsObj, ObjectNode.class); + if(claims.has(Claims.EXPIRATION) && claims.get(Claims.EXPIRATION).isNumber()) { + return claims.get(Claims.EXPIRATION).asLong(); + } + throw new AuthUnauthorizedException("Unable to find expiration date in the Common JWT."); + } + + private static String extractDecodedClaims(String jwt) throws AuthUnauthorizedException { + String[] jwtSplit = jwt.split("\\."); + if (jwtSplit.length < 3) { + throw new AuthUnauthorizedException("Unable to parse JWT"); + } + return new String(Base64.getDecoder().decode(jwtSplit[1])); + } + + private static String dropBearer(String jwt) { + return jwt.replace("Bearer ", ""); + } + + private static PrivateKey parsePKCS1PrivateKey(String pemPrivateKey) throws GeneralSecurityException { + try (final PemReader pemReader = new PemReader(new StringReader(pemPrivateKey))) { + final PemObject privateKeyObject = pemReader.readPemObject(); + final RSAPrivateKey rsa = RSAPrivateKey.getInstance(privateKeyObject.getContent()); + final RSAPrivateCrtKeyParameters privateKeyParameter = new RSAPrivateCrtKeyParameters( + rsa.getModulus(), + rsa.getPublicExponent(), + rsa.getPrivateExponent(), + rsa.getPrime1(), + rsa.getPrime2(), + rsa.getExponent1(), + rsa.getExponent2(), + rsa.getCoefficient() + ); + + return new JcaPEMKeyConverter().getPrivateKey(PrivateKeyInfoFactory.createPrivateKeyInfo(privateKeyParameter)); + } catch (IOException | IllegalStateException | IllegalArgumentException e) { + throw new GeneralSecurityException( + "Unable to parse the private key. Check if the format of your private key is correct.", e); } } - private static PrivateKey parsePKCS1PrivateKey(String pemPrivateKey) throws GeneralSecurityException { - try (final PemReader pemReader = new PemReader(new StringReader(pemPrivateKey))) { - final PemObject privateKeyObject = pemReader.readPemObject(); - final RSAPrivateKey rsa = RSAPrivateKey.getInstance(privateKeyObject.getContent()); - final RSAPrivateCrtKeyParameters privateKeyParameter = new RSAPrivateCrtKeyParameters( - rsa.getModulus(), - rsa.getPublicExponent(), - rsa.getPrivateExponent(), - rsa.getPrime1(), - rsa.getPrime2(), - rsa.getExponent1(), - rsa.getExponent2(), - rsa.getCoefficient() - ); - - return new JcaPEMKeyConverter().getPrivateKey(PrivateKeyInfoFactory.createPrivateKeyInfo(privateKeyParameter)); - } catch (IOException | IllegalStateException | IllegalArgumentException e) { - throw new GeneralSecurityException("Unable to parse the private key. Check if the format of your private key is correct.", e); - } - } - - private static PrivateKey parsePKCS8PrivateKey(String pemPrivateKey) throws GeneralSecurityException { + private static PrivateKey parsePKCS8PrivateKey(String pemPrivateKey) throws GeneralSecurityException { try { - final String privateKeyString = pemPrivateKey - .replace(PEM_PRIVATE_START, "") + final String privateKeyString = pemPrivateKey.replace(PEM_PRIVATE_START, "") .replace(PEM_PRIVATE_END, "") .replace("\\n", "\n") .replaceAll("\\s", ""); @@ -154,7 +187,8 @@ private static PrivateKey parsePKCS8PrivateKey(String pemPrivateKey) throws Gene return KeyFactory.getInstance("RSA").generatePrivate(new PKCS8EncodedKeySpec(keyBytes)); } catch (InvalidKeySpecException | NoSuchAlgorithmException e) { - throw new GeneralSecurityException("Unable to parse the pem private key. Check if the format of your private key is correct.", e); + throw new GeneralSecurityException( + "Unable to parse the pem private key. Check if the format of your private key is correct.", e); } } @@ -165,11 +199,12 @@ protected static Certificate parseX509Certificate(String certificate) throws Aut .replace(END_CERTIFICATE, "") .replaceAll("\\s", ""); - byte [] decoded = Base64.getDecoder().decode(sanitizedBase64Der); + byte[] decoded = Base64.getDecoder().decode(sanitizedBase64Der); return CertificateFactory.getInstance("X.509").generateCertificate(new ByteArrayInputStream(decoded)); } catch (CertificateException e) { - throw new AuthInitializationException("Unable to parse the certificate. Check if the format of your certificate is correct.", e); + throw new AuthInitializationException( + "Unable to parse the certificate. Check if the format of your certificate is correct.", e); } } } diff --git a/symphony-bdk-core/src/main/java/com/symphony/bdk/core/client/loadbalancing/LoadBalancedApiClient.java b/symphony-bdk-core/src/main/java/com/symphony/bdk/core/client/loadbalancing/LoadBalancedApiClient.java index 79bc543e0..ada425318 100644 --- a/symphony-bdk-core/src/main/java/com/symphony/bdk/core/client/loadbalancing/LoadBalancedApiClient.java +++ b/symphony-bdk-core/src/main/java/com/symphony/bdk/core/client/loadbalancing/LoadBalancedApiClient.java @@ -118,6 +118,14 @@ public Map getAuthentications() { return this.apiClient.getAuthentications(); } + /** + * {@inheritDoc} + */ + @Override + public void addEnforcedAuthenticationScheme(String name) { + this.apiClient.addEnforcedAuthenticationScheme(name); + } + private void validateLoadBalancingConfiguration(BdkConfig config) { final BdkLoadBalancingConfig agentLoadBalancing = config.getAgent().getLoadBalancing(); if (agentLoadBalancing == null) { diff --git a/symphony-bdk-core/src/main/java/com/symphony/bdk/core/config/model/BdkCommonJwtConfig.java b/symphony-bdk-core/src/main/java/com/symphony/bdk/core/config/model/BdkCommonJwtConfig.java new file mode 100644 index 000000000..0bf156f60 --- /dev/null +++ b/symphony-bdk-core/src/main/java/com/symphony/bdk/core/config/model/BdkCommonJwtConfig.java @@ -0,0 +1,18 @@ +package com.symphony.bdk.core.config.model; + +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import org.apiguardian.api.API; + +@Getter +@Setter +@Slf4j +@API(status = API.Status.EXPERIMENTAL) +public class BdkCommonJwtConfig { + protected Boolean enabled; + + public BdkCommonJwtConfig() { + this.enabled = false; + } +} diff --git a/symphony-bdk-core/src/main/java/com/symphony/bdk/core/config/model/BdkConfig.java b/symphony-bdk-core/src/main/java/com/symphony/bdk/core/config/model/BdkConfig.java index 0c3f8af53..301ac6400 100644 --- a/symphony-bdk-core/src/main/java/com/symphony/bdk/core/config/model/BdkConfig.java +++ b/symphony-bdk-core/src/main/java/com/symphony/bdk/core/config/model/BdkConfig.java @@ -25,6 +25,7 @@ public class BdkConfig extends BdkServerConfig { private BdkRetryConfig retry = new BdkRetryConfig(); private BdkDatafeedConfig datafeed = new BdkDatafeedConfig(); + private BdkCommonJwtConfig commonJwt = new BdkCommonJwtConfig(); /** * Check if OBO is configured. Checks {@link BdkExtAppConfig#isConfigured()} on field {@link #app}. @@ -39,6 +40,15 @@ public boolean isBotConfigured() { return bot != null && isNotEmpty(bot.getUsername()); } + /** + * Check if Common JWT feature is enabled. Checks {@link BdkCommonJwtConfig#getEnabled()} ()} on field {@link #commonJwt}. + * + * @return true if Common JWT is enabled. + */ + public boolean isCommonJwtEnabled() { + return this.getCommonJwt().getEnabled(); + } + /** * Returns the retry configuration used for DataFeed services. * diff --git a/symphony-bdk-core/src/test/java/com/symphony/bdk/core/ServiceFactoryTest.java b/symphony-bdk-core/src/test/java/com/symphony/bdk/core/ServiceFactoryTest.java index 43e7264b2..b154aab19 100644 --- a/symphony-bdk-core/src/test/java/com/symphony/bdk/core/ServiceFactoryTest.java +++ b/symphony-bdk-core/src/test/java/com/symphony/bdk/core/ServiceFactoryTest.java @@ -1,25 +1,33 @@ package com.symphony.bdk.core; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import com.symphony.bdk.core.auth.AuthSession; import com.symphony.bdk.core.client.ApiClientFactory; import com.symphony.bdk.core.config.BdkConfigLoader; import com.symphony.bdk.core.config.exception.BdkConfigException; +import com.symphony.bdk.core.config.model.BdkBotConfig; +import com.symphony.bdk.core.config.model.BdkCommonJwtConfig; import com.symphony.bdk.core.config.model.BdkConfig; import com.symphony.bdk.core.config.model.BdkDatafeedConfig; -import com.symphony.bdk.core.service.health.HealthService; -import com.symphony.bdk.core.service.session.SessionService; +import com.symphony.bdk.core.config.model.BdkExtAppConfig; import com.symphony.bdk.core.service.application.ApplicationService; import com.symphony.bdk.core.service.connection.ConnectionService; import com.symphony.bdk.core.service.datafeed.DatafeedLoop; import com.symphony.bdk.core.service.datafeed.impl.DatafeedLoopV1; import com.symphony.bdk.core.service.datafeed.impl.DatafeedLoopV2; +import com.symphony.bdk.core.service.health.HealthService; import com.symphony.bdk.core.service.message.MessageService; import com.symphony.bdk.core.service.presence.PresenceService; +import com.symphony.bdk.core.service.session.SessionService; import com.symphony.bdk.core.service.signal.SignalService; import com.symphony.bdk.core.service.stream.StreamService; import com.symphony.bdk.core.service.user.UserService; @@ -34,6 +42,7 @@ public class ServiceFactoryTest { private ServiceFactory serviceFactory; private ApiClientFactory apiClientFactory; private AuthSession mAuthSession; + private ApiClient mPodClient; private BdkConfig config; private UserV2 botInfo; @@ -42,7 +51,7 @@ void setUp() throws BdkConfigException { this.config = BdkConfigLoader.loadFromClasspath("/config/config.yaml"); this.botInfo = mock(UserV2.class); this.mAuthSession = mock(AuthSession.class); - ApiClient mPodClient = mock(ApiClient.class); + this.mPodClient = mock(ApiClient.class); ApiClient mAgentClient = mock(ApiClient.class); this.apiClientFactory = mock(ApiClientFactory.class); @@ -123,4 +132,42 @@ void getDatafeedServiceTest() { assertEquals(datafeedServiceV2.getClass(), DatafeedLoopV2.class); } + + @Test + void testPodApiClientConfigWithCommonJwt() { + BdkCommonJwtConfig bdkCommonJwtConfig = this.config.getCommonJwt(); + bdkCommonJwtConfig.setEnabled(true); + config.setApp(new BdkExtAppConfig()); + + this.serviceFactory = new ServiceFactory(this.apiClientFactory, mAuthSession, config); + + assertFalse(config.isOboConfigured()); + assertTrue(config.isBotConfigured()); + assertTrue(config.isCommonJwtEnabled()); + verify(mPodClient).getAuthentications(); + verify(mPodClient).addEnforcedAuthenticationScheme(eq("bearerAuth")); + } + + @Test + void testPodApiClientConfigWithCommonJwtInOboMode() { + BdkCommonJwtConfig bdkCommonJwtConfig = this.config.getCommonJwt(); + bdkCommonJwtConfig.setEnabled(true); + + assertTrue(config.isOboConfigured()); + assertTrue(config.isBotConfigured()); + assertTrue(config.isCommonJwtEnabled()); + assertThrows(UnsupportedOperationException.class, ()-> new ServiceFactory(this.apiClientFactory, mAuthSession, config)); + } + + @Test + void testPodApiClientConfigWithCommonJwtInOboOnlyMode() { + BdkCommonJwtConfig bdkCommonJwtConfig = this.config.getCommonJwt(); + bdkCommonJwtConfig.setEnabled(true); + config.setBot(new BdkBotConfig()); + + assertTrue(config.isOboConfigured()); + assertFalse(config.isBotConfigured()); + assertTrue(config.isCommonJwtEnabled()); + assertThrows(UnsupportedOperationException.class, ()-> new ServiceFactory(this.apiClientFactory, mAuthSession, config)); + } } diff --git a/symphony-bdk-core/src/test/java/com/symphony/bdk/core/auth/JwtHelperTest.java b/symphony-bdk-core/src/test/java/com/symphony/bdk/core/auth/JwtHelperTest.java index a82767ec3..982f5e4a8 100644 --- a/symphony-bdk-core/src/test/java/com/symphony/bdk/core/auth/JwtHelperTest.java +++ b/symphony-bdk-core/src/test/java/com/symphony/bdk/core/auth/JwtHelperTest.java @@ -5,9 +5,11 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import com.symphony.bdk.core.auth.exception.AuthInitializationException; +import com.symphony.bdk.core.auth.exception.AuthUnauthorizedException; import com.symphony.bdk.core.auth.jwt.JwtHelper; import com.symphony.bdk.core.auth.jwt.UserClaim; +import com.fasterxml.jackson.core.JsonProcessingException; import com.migcomponents.migbase64.Base64; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; @@ -35,10 +37,14 @@ * Test class for the {@link JwtHelper}. */ @Slf4j -class JwtHelperTest { +public class JwtHelperTest { public static final String CERT_PASSWORD = "changeit"; public static final String CERT_ALIAS = "1"; + public static final String JWT = "Bearer eyJraWQiOiJGNG5Xak9WbTRBZU9JYUtEL2JCUWNleXI5MW89IiwiYWxnIjoiUlMyNTYifQ." + + "eyJleHAiOjE2NDEzMDgyNzgsInN1YiI6IjEzMDU2NzAwNTgwOTE1IiwiZXh0X3BvZF9pZCI6MTkwLCJwb2xpY3lfaWQiOiJhcHAiLCJlbnRpdGx" + + "lbWVudHMiOiIifQ.signature"; + private static final String JWT_EXP_INVALID = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIn0.zhWFI4bw81QLE49UnklwMlThgt2ktUOs5M1HKjENgRE.signature"; @Test void loadPkcs8PrivateKey() throws GeneralSecurityException { @@ -104,6 +110,28 @@ public void testValidateJwtWithInvalidJwt() { assertThrows(AuthInitializationException.class, () -> JwtHelper.validateJwt("invalid jwt", certificatePem)); } + @Test + public void testExtractExpirationDate() throws Exception { + Long expirationDate = JwtHelper.extractExpirationDate(JWT); + + assertNotNull(expirationDate); + } + + @Test + public void testExtractExpirationDateInvalidJwt() { + assertThrows(AuthUnauthorizedException.class, () -> JwtHelper.extractExpirationDate("invalid jwt")); + } + + @Test + public void testExtractExpirationDateInvalidParsing() { + assertThrows(JsonProcessingException.class, () -> JwtHelper.extractExpirationDate("invalid.common.jwt")); + } + + @Test + public void testExtractExpirationDateInvalidExpData() { + assertThrows(AuthUnauthorizedException.class, () -> JwtHelper.extractExpirationDate(JWT_EXP_INVALID)); + } + @SneakyThrows private static String generatePkcs8RsaPrivateKey() { final KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA"); diff --git a/symphony-bdk-core/src/test/java/com/symphony/bdk/core/auth/impl/AbstractBotAuthenticatorTest.java b/symphony-bdk-core/src/test/java/com/symphony/bdk/core/auth/impl/AbstractBotAuthenticatorTest.java index e94809c24..a14a8d9bc 100644 --- a/symphony-bdk-core/src/test/java/com/symphony/bdk/core/auth/impl/AbstractBotAuthenticatorTest.java +++ b/symphony-bdk-core/src/test/java/com/symphony/bdk/core/auth/impl/AbstractBotAuthenticatorTest.java @@ -2,6 +2,7 @@ import static com.symphony.bdk.core.test.BdkRetryConfigTestHelper.ofMinimalInterval; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doReturn; @@ -14,15 +15,20 @@ import com.symphony.bdk.core.auth.AuthSession; import com.symphony.bdk.core.auth.exception.AuthUnauthorizedException; +import com.symphony.bdk.core.config.model.BdkCommonJwtConfig; import com.symphony.bdk.core.config.model.BdkRetryConfig; +import com.symphony.bdk.gen.api.model.JwtToken; +import com.symphony.bdk.gen.api.model.Token; import com.symphony.bdk.http.api.ApiClient; import com.symphony.bdk.http.api.ApiException; +import com.symphony.bdk.http.api.ApiResponse; import com.symphony.bdk.http.api.ApiRuntimeException; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import java.net.SocketTimeoutException; +import java.util.Collections; import javax.annotation.Nonnull; import javax.ws.rs.ProcessingException; @@ -32,12 +38,22 @@ class AbstractBotAuthenticatorTest { private ApiClient apiClient; private static class TestBotAuthenticator extends AbstractBotAuthenticator { - public TestBotAuthenticator(BdkRetryConfig retryConfig) { - super(retryConfig); + public TestBotAuthenticator(BdkRetryConfig retryConfig, ApiClient apiClient) { + super(retryConfig, new BdkCommonJwtConfig(), apiClient); } @Override - protected String authenticateAndGetToken(ApiClient client) throws ApiException { + protected Token retrieveSessionToken() { + return null; + } + + @Override + protected String retrieveKeyManagerToken() { + return null; + } + + @Override + protected Token doRetrieveToken(ApiClient client) throws ApiException { return null; } @@ -60,55 +76,106 @@ void setUp() { } @Test - void testSuccess() throws ApiException, AuthUnauthorizedException { + void testSuccess() throws AuthUnauthorizedException, ApiException { final String token = "12324"; - AbstractBotAuthenticator botAuthenticator = spy(new TestBotAuthenticator(ofMinimalInterval())); - doReturn(token).when(botAuthenticator).authenticateAndGetToken(any()); + AbstractBotAuthenticator botAuthenticator = spy(new TestBotAuthenticator(ofMinimalInterval(), apiClient)); + doReturn(new Token().token(token)).when(botAuthenticator).doRetrieveToken(any()); + + assertEquals(token, botAuthenticator.retrieveKeyManagerToken(apiClient)); + verify(botAuthenticator, times(1)).doRetrieveToken(any()); + } + + @Test + void testSuccessAuthToken() throws ApiException, AuthUnauthorizedException { + final Token token = new Token(); + token.setToken("12324"); + token.setAuthorizationToken("Bearer qwerty"); + + AbstractBotAuthenticator botAuthenticator = spy(new TestBotAuthenticator(ofMinimalInterval(), apiClient)); + doReturn(token).when(botAuthenticator).doRetrieveToken(any()); + + assertEquals(token, botAuthenticator.retrieveSessionToken(apiClient)); + verify(botAuthenticator, times(1)).doRetrieveToken(any()); + } - assertEquals(token, botAuthenticator.retrieveToken(apiClient)); - verify(botAuthenticator, times(1)).authenticateAndGetToken(any()); + @Test + void testSuccessBearerToken() throws AuthUnauthorizedException, ApiException { + JwtToken token = new JwtToken(); + token.setAccessToken("qwertyui"); + AbstractBotAuthenticator botAuthenticator = new TestBotAuthenticator(ofMinimalInterval(), apiClient); + when(apiClient.invokeAPI(any(), any(), any(), any(),any(), any(), any(),any(),any(),any(),any())) + .thenReturn(new ApiResponse<>(200, Collections.emptyMap(), token)); + + assertEquals("qwertyui", botAuthenticator.retrieveAuthorizationToken("sessionToken")); } @Test void testUnauthorized() throws ApiException { - AbstractBotAuthenticator botAuthenticator = spy(new TestBotAuthenticator(ofMinimalInterval())); - doThrow(new ApiException(401, "")).when(botAuthenticator).authenticateAndGetToken(any()); + AbstractBotAuthenticator botAuthenticator = spy(new TestBotAuthenticator(ofMinimalInterval(), apiClient)); + doThrow(new ApiException(401, "")).when(botAuthenticator).doRetrieveToken(any()); - assertThrows(AuthUnauthorizedException.class, () -> botAuthenticator.retrieveToken(apiClient)); - verify(botAuthenticator, times(1)).authenticateAndGetToken(any()); + assertThrows(AuthUnauthorizedException.class, () -> botAuthenticator.retrieveKeyManagerToken(apiClient)); + verify(botAuthenticator, times(1)).doRetrieveToken(any()); } @Test void testUnexpectedApiException() throws ApiException { - AbstractBotAuthenticator botAuthenticator = spy(new TestBotAuthenticator(ofMinimalInterval())); - doThrow(new ApiException(404, "")).when(botAuthenticator).authenticateAndGetToken(any()); + AbstractBotAuthenticator botAuthenticator = spy(new TestBotAuthenticator(ofMinimalInterval(), apiClient)); + doThrow(new ApiException(404, "")).when(botAuthenticator).doRetrieveToken(any()); - assertThrows(ApiRuntimeException.class, () -> botAuthenticator.retrieveToken(apiClient)); - verify(botAuthenticator, times(1)).authenticateAndGetToken(any()); + assertThrows(ApiRuntimeException.class, () -> botAuthenticator.retrieveKeyManagerToken(apiClient)); + verify(botAuthenticator, times(1)).doRetrieveToken(any()); } @Test - void testShouldRetry() throws ApiException, AuthUnauthorizedException { + void testShouldRetry() throws AuthUnauthorizedException, ApiException { final String token = "12324"; - AbstractBotAuthenticator botAuthenticator = spy(new TestBotAuthenticator(ofMinimalInterval(4))); + AbstractBotAuthenticator botAuthenticator = spy(new TestBotAuthenticator(ofMinimalInterval(4), + apiClient)); + doThrow(new ApiException(429, "")) + .doThrow(new ApiException(503, "")) + .doThrow(new ProcessingException(new SocketTimeoutException())) + .doReturn(new Token().token(token)) + .when(botAuthenticator).doRetrieveToken(any()); + + assertEquals(token, botAuthenticator.retrieveKeyManagerToken(apiClient)); + verify(botAuthenticator, times(4)).doRetrieveToken(any()); + } + + @Test + void testShouldRetryAuthToken() throws ApiException, AuthUnauthorizedException { + final Token token = new Token(); + token.setToken("12324"); + token.setAuthorizationToken("Bearer qwerty"); + + AbstractBotAuthenticator botAuthenticator = spy(new TestBotAuthenticator(ofMinimalInterval(4), + apiClient)); doThrow(new ApiException(429, "")) .doThrow(new ApiException(503, "")) .doThrow(new ProcessingException(new SocketTimeoutException())) .doReturn(token) - .when(botAuthenticator).authenticateAndGetToken(any()); + .when(botAuthenticator).doRetrieveToken(any()); - assertEquals(token, botAuthenticator.retrieveToken(apiClient)); - verify(botAuthenticator, times(4)).authenticateAndGetToken(any()); + assertEquals(token, botAuthenticator.retrieveSessionToken(apiClient)); + verify(botAuthenticator, times(4)).doRetrieveToken(any()); } @Test void testRetriesExhausted() throws ApiException { - AbstractBotAuthenticator botAuthenticator = spy(new TestBotAuthenticator(ofMinimalInterval(2))); - doThrow(new ApiException(429, "")).when(botAuthenticator).authenticateAndGetToken(any()); + AbstractBotAuthenticator botAuthenticator = spy(new TestBotAuthenticator(ofMinimalInterval(2), + apiClient)); + doThrow(new ApiException(429, "")).when(botAuthenticator).doRetrieveToken(any()); + + assertThrows(ApiRuntimeException.class, () -> botAuthenticator.retrieveKeyManagerToken(apiClient)); + verify(botAuthenticator, times(2)).doRetrieveToken(any()); + } + + @Test + void isCommonJwtEnabled() { + TestBotAuthenticator authenticator = new TestBotAuthenticator(ofMinimalInterval(), apiClient); - assertThrows(ApiRuntimeException.class, () -> botAuthenticator.retrieveToken(apiClient)); - verify(botAuthenticator, times(2)).authenticateAndGetToken(any()); + assertFalse(authenticator.isCommonJwtEnabled()); } } diff --git a/symphony-bdk-core/src/test/java/com/symphony/bdk/core/auth/impl/AbstractOboAuthenticatorTest.java b/symphony-bdk-core/src/test/java/com/symphony/bdk/core/auth/impl/AbstractOboAuthenticatorTest.java index 0f55a3816..37e6181c1 100644 --- a/symphony-bdk-core/src/test/java/com/symphony/bdk/core/auth/impl/AbstractOboAuthenticatorTest.java +++ b/symphony-bdk-core/src/test/java/com/symphony/bdk/core/auth/impl/AbstractOboAuthenticatorTest.java @@ -38,13 +38,12 @@ public TestAbstractOboAuthenticator(BdkRetryConfig retryConfig) { } @Override - protected String authenticateAndRetrieveOboSessionToken(String appSessionToken, Long userId) throws ApiException { + protected String authenticateAndRetrieveOboSessionToken(@Nonnull String appSessionToken, @Nonnull Long userId) throws ApiException { return null; } @Override - protected String authenticateAndRetrieveOboSessionToken(String appSessionToken, String username) - throws ApiException { + protected String authenticateAndRetrieveOboSessionToken(@Nonnull String appSessionToken, @Nonnull String username) throws ApiException { return null; } @@ -60,18 +59,17 @@ protected String getBasePath() { @Nonnull @Override - public AuthSession authenticateByUsername(String username) { + public AuthSession authenticateByUsername(@Nonnull String username) { return mock(AuthSession.class); } @Nonnull @Override - public AuthSession authenticateByUserId(Long userId) { + public AuthSession authenticateByUserId(@Nonnull Long userId) { return mock(AuthSession.class); } } - // test retrieveOboSessionTokenByUserId() @Test void testRetrieveTokenByUserIdSuccess() throws ApiException, AuthUnauthorizedException { final String token = "12324"; @@ -132,7 +130,6 @@ void testRetrieveTokenByUserIdRetriesExhausted() throws ApiException, AuthUnauth verify(authenticator, times(2)).authenticateAndRetrieveOboSessionToken(anyString(), anyLong()); } - // test retrieveOboSessionTokenByUsername() @Test void testRetrieveTokenByUsernameSuccess() throws ApiException, AuthUnauthorizedException { final String token = "12324"; @@ -193,7 +190,6 @@ void testRetrieveTokenByUsernameRetriesExhausted() throws ApiException, AuthUnau verify(authenticator, times(2)).authenticateAndRetrieveOboSessionToken(anyString(), anyString()); } - // test retrieveAppSessionToken() @Test void testRetrieveAppSessionTokenSuccess() throws ApiException, AuthUnauthorizedException { final String token = "12324"; diff --git a/symphony-bdk-core/src/test/java/com/symphony/bdk/core/auth/impl/AuthSessionCertImplTest.java b/symphony-bdk-core/src/test/java/com/symphony/bdk/core/auth/impl/AuthSessionCertImplTest.java deleted file mode 100644 index d8e9f254a..000000000 --- a/symphony-bdk-core/src/test/java/com/symphony/bdk/core/auth/impl/AuthSessionCertImplTest.java +++ /dev/null @@ -1,39 +0,0 @@ -package com.symphony.bdk.core.auth.impl; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import com.symphony.bdk.core.auth.exception.AuthUnauthorizedException; - -import org.junit.jupiter.api.Test; - -import java.util.UUID; - -/** - * Test class for {@link AuthSessionCertImpl} - */ -public class AuthSessionCertImplTest { - - @Test - void testRefresh() throws AuthUnauthorizedException { - - final String sessionToken = UUID.randomUUID().toString(); - final String kmToken = UUID.randomUUID().toString(); - - final BotAuthenticatorCertImpl auth = mock(BotAuthenticatorCertImpl.class); - when(auth.retrieveSessionToken()).thenReturn(sessionToken); - when(auth.retrieveKeyManagerToken()).thenReturn(kmToken); - - final AuthSessionCertImpl session = new AuthSessionCertImpl(auth); - session.refresh(); - - assertEquals(sessionToken, session.getSessionToken()); - assertEquals(kmToken, session.getKeyManagerToken()); - - verify(auth, times(1)).retrieveSessionToken(); - verify(auth, times(1)).retrieveKeyManagerToken(); - } -} diff --git a/symphony-bdk-core/src/test/java/com/symphony/bdk/core/auth/impl/AuthSessionImplTest.java b/symphony-bdk-core/src/test/java/com/symphony/bdk/core/auth/impl/AuthSessionImplTest.java new file mode 100644 index 000000000..52363cbfa --- /dev/null +++ b/symphony-bdk-core/src/test/java/com/symphony/bdk/core/auth/impl/AuthSessionImplTest.java @@ -0,0 +1,160 @@ +package com.symphony.bdk.core.auth.impl; + +import static com.symphony.bdk.core.auth.JwtHelperTest.JWT; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.symphony.bdk.core.auth.exception.AuthUnauthorizedException; +import com.symphony.bdk.gen.api.model.Token; + +import org.junit.jupiter.api.Test; + +import java.util.UUID; + +class AuthSessionImplTest { + + @Test + void testRefresh() throws AuthUnauthorizedException { + + final String sessionToken = UUID.randomUUID().toString(); + Token authToken = new Token(); + authToken.setToken(sessionToken); + final String kmToken = UUID.randomUUID().toString(); + + final BotAuthenticatorRsaImpl auth = mock(BotAuthenticatorRsaImpl.class); + when(auth.retrieveSessionToken()).thenReturn(authToken); + when(auth.retrieveKeyManagerToken()).thenReturn(kmToken); + + final AuthSessionImpl session = new AuthSessionImpl(auth); + session.refresh(); + + assertEquals(sessionToken, session.getSessionToken()); + assertEquals(kmToken, session.getKeyManagerToken()); + + verify(auth, times(1)).retrieveSessionToken(); + verify(auth, times(1)).retrieveKeyManagerToken(); + } + + @Test + void testRefreshAuthTokenException() throws AuthUnauthorizedException { + + Token token = new Token(); + token.setAuthorizationToken("Invalid jwt"); + + final BotAuthenticatorRsaImpl auth = mock(BotAuthenticatorRsaImpl.class); + when(auth.retrieveSessionToken()).thenReturn(token); + + assertThrows(AuthUnauthorizedException.class, new AuthSessionImpl(auth)::refresh); + } + + @Test + void testRefreshBearerTokenOnly() throws AuthUnauthorizedException { + + final String sessionToken = UUID.randomUUID().toString(); + final String kmToken = UUID.randomUUID().toString(); + + final BotAuthenticatorRsaImpl auth = mock(BotAuthenticatorRsaImpl.class); + when(auth.retrieveSessionToken()).thenReturn(getToken(sessionToken)); + when(auth.retrieveKeyManagerToken()).thenReturn(kmToken); + when(auth.isCommonJwtEnabled()).thenReturn(true); + when(auth.retrieveAuthorizationToken(sessionToken)).thenReturn(JWT); + + final AuthSessionImpl session = new AuthSessionImpl(auth); + + // first refresh initialise the tokens + session.refresh(); + + verify(auth, times(1)).retrieveSessionToken(); + verify(auth, never()).retrieveAuthorizationToken(any()); + + // second refresh should try only with the bearer token + session.refresh(); + + verify(auth).retrieveAuthorizationToken(any()); + } + + @Test + void testGetAuthorizationTokenWithRefresh() throws AuthUnauthorizedException { + + final String sessionToken = UUID.randomUUID().toString(); + final String kmToken = UUID.randomUUID().toString(); + + final BotAuthenticatorRsaImpl auth = mock(BotAuthenticatorRsaImpl.class); + when(auth.retrieveSessionToken()).thenReturn(getToken(sessionToken)); + when(auth.retrieveKeyManagerToken()).thenReturn(kmToken); + when(auth.isCommonJwtEnabled()).thenReturn(true); + when(auth.retrieveAuthorizationToken(sessionToken)).thenReturn(JWT); + + final AuthSessionImpl session = new AuthSessionImpl(auth); + + // first refresh initialise the tokens + session.refresh(); + + verify(auth, times(1)).retrieveSessionToken(); + verify(auth, never()).retrieveAuthorizationToken(any()); + + // getting auth token checks if token is expired and refresh bearer token only + session.getAuthorizationToken(); + + verify(auth).retrieveAuthorizationToken(any()); + verify(auth, times(1)).retrieveSessionToken(); + } + + @Test + void testGetAuthorizationTokenWhenNotSupported() throws AuthUnauthorizedException { + + final String sessionToken = UUID.randomUUID().toString(); + final String kmToken = UUID.randomUUID().toString(); + Token authToken = new Token(); + authToken.setToken(sessionToken); + final BotAuthenticatorRsaImpl auth = mock(BotAuthenticatorRsaImpl.class); + when(auth.retrieveSessionToken()).thenReturn(authToken); + when(auth.retrieveKeyManagerToken()).thenReturn(kmToken); + when(auth.isCommonJwtEnabled()).thenReturn(true); + + final AuthSessionImpl session = new AuthSessionImpl(auth); + + assertThrows(UnsupportedOperationException.class, session::getAuthorizationToken); + } + + @Test + void testGetAuthorizationTokenWhenRefreshBearerFails() throws AuthUnauthorizedException { + + final String sessionToken = UUID.randomUUID().toString(); + final String kmToken = UUID.randomUUID().toString(); + + final BotAuthenticatorRsaImpl auth = mock(BotAuthenticatorRsaImpl.class); + when(auth.retrieveSessionToken()).thenReturn(getToken(sessionToken)); + when(auth.retrieveKeyManagerToken()).thenReturn(kmToken); + when(auth.isCommonJwtEnabled()).thenReturn(true); + when(auth.retrieveAuthorizationToken(sessionToken)).thenThrow(AuthUnauthorizedException.class); + + final AuthSessionImpl session = new AuthSessionImpl(auth); + + // first refresh initialise the tokens + session.refresh(); + + verify(auth, times(1)).retrieveSessionToken(); + verify(auth, never()).retrieveAuthorizationToken(any()); + + // getting auth token fails then we refresh all tokens + session.getAuthorizationToken(); + + verify(auth).retrieveAuthorizationToken(any()); + verify(auth, times(2)).retrieveSessionToken(); + } + + private Token getToken(String sessionToken) { + Token authToken = new Token(); + authToken.setToken(sessionToken); + authToken.setAuthorizationToken(JWT); + return authToken; + } + +} diff --git a/symphony-bdk-core/src/test/java/com/symphony/bdk/core/auth/impl/AuthSessionOboImplTest.java b/symphony-bdk-core/src/test/java/com/symphony/bdk/core/auth/impl/AuthSessionOboImplTest.java index 303d6e3b7..e47decfe5 100644 --- a/symphony-bdk-core/src/test/java/com/symphony/bdk/core/auth/impl/AuthSessionOboImplTest.java +++ b/symphony-bdk-core/src/test/java/com/symphony/bdk/core/auth/impl/AuthSessionOboImplTest.java @@ -60,6 +60,7 @@ void testRefreshForUserId() throws AuthUnauthorizedException { assertEquals(sessionToken, session.getSessionToken()); assertNull(session.getKeyManagerToken()); + assertNull(session.getAuthorizationToken()); verify(auth, times(1)).retrieveOboSessionTokenByUserId(eq(1234L)); verify(auth, times(0)).retrieveOboSessionTokenByUsername(anyString()); diff --git a/symphony-bdk-core/src/test/java/com/symphony/bdk/core/auth/impl/AuthSessionRsaImplTest.java b/symphony-bdk-core/src/test/java/com/symphony/bdk/core/auth/impl/AuthSessionRsaImplTest.java deleted file mode 100644 index acb2c3cff..000000000 --- a/symphony-bdk-core/src/test/java/com/symphony/bdk/core/auth/impl/AuthSessionRsaImplTest.java +++ /dev/null @@ -1,35 +0,0 @@ -package com.symphony.bdk.core.auth.impl; - -import com.symphony.bdk.core.auth.exception.AuthUnauthorizedException; -import org.junit.jupiter.api.Test; - -import java.util.UUID; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.mockito.Mockito.*; - -/** - * Test class for the {@link AuthSessionRsaImpl}. - */ -class AuthSessionRsaImplTest { - - @Test - void testRefresh() throws AuthUnauthorizedException { - - final String sessionToken = UUID.randomUUID().toString(); - final String kmToken = UUID.randomUUID().toString(); - - final BotAuthenticatorRsaImpl auth = mock(BotAuthenticatorRsaImpl.class); - when(auth.retrieveSessionToken()).thenReturn(sessionToken); - when(auth.retrieveKeyManagerToken()).thenReturn(kmToken); - - final AuthSessionRsaImpl session = new AuthSessionRsaImpl(auth); - session.refresh(); - - assertEquals(sessionToken, session.getSessionToken()); - assertEquals(kmToken, session.getKeyManagerToken()); - - verify(auth, times(1)).retrieveSessionToken(); - verify(auth, times(1)).retrieveKeyManagerToken(); - } -} diff --git a/symphony-bdk-core/src/test/java/com/symphony/bdk/core/auth/impl/BotAuthenticatorCertImplTest.java b/symphony-bdk-core/src/test/java/com/symphony/bdk/core/auth/impl/BotAuthenticatorCertImplTest.java index a105d0c1c..fe669103a 100644 --- a/symphony-bdk-core/src/test/java/com/symphony/bdk/core/auth/impl/BotAuthenticatorCertImplTest.java +++ b/symphony-bdk-core/src/test/java/com/symphony/bdk/core/auth/impl/BotAuthenticatorCertImplTest.java @@ -7,8 +7,10 @@ import com.symphony.bdk.core.auth.AuthSession; import com.symphony.bdk.core.auth.exception.AuthUnauthorizedException; +import com.symphony.bdk.core.config.model.BdkCommonJwtConfig; import com.symphony.bdk.core.test.BdkMockServer; import com.symphony.bdk.core.test.BdkMockServerExtension; +import com.symphony.bdk.gen.api.model.Token; import com.symphony.bdk.http.api.ApiRuntimeException; import org.junit.jupiter.api.BeforeEach; @@ -29,6 +31,7 @@ public class BotAuthenticatorCertImplTest { void init(final BdkMockServer mockServer) { this.authenticator = new BotAuthenticatorCertImpl( ofMinimalInterval(1), "botUsername", + new BdkCommonJwtConfig(), mockServer.newApiClient("/login"), mockServer.newApiClient("/sessionauth"), mockServer.newApiClient("/keyauth")); @@ -41,16 +44,16 @@ void testAuthenticateBot(final BdkMockServer mockServer) throws AuthUnauthorized final AuthSession session = this.authenticator.authenticateBot(); assertNotNull(session); - assertEquals(AuthSessionCertImpl.class, session.getClass()); - assertEquals(this.authenticator, ((AuthSessionCertImpl) session).getAuthenticator()); + assertEquals(AuthSessionImpl.class, session.getClass()); + assertEquals(this.authenticator, ((AuthSessionImpl) session).getAuthenticator()); } @Test - void testRetrieveSessionToken(final BdkMockServer mockServer) throws AuthUnauthorizedException { + void testRetrieveAuthToken(final BdkMockServer mockServer) throws AuthUnauthorizedException { mockServer.onPost(SESSIONAUTH_AUTHENTICATE_URL, res -> res.withBody("{ \"token\": \"1234\", \"name\": \"sessionToken\" }")); - final String sessionToken = this.authenticator.retrieveSessionToken(); - assertEquals("1234", sessionToken); + final Token authToken = this.authenticator.retrieveSessionToken(); + assertEquals("1234", authToken.getToken()); } @Test diff --git a/symphony-bdk-core/src/test/java/com/symphony/bdk/core/auth/impl/BotAuthenticatorRsaImplTest.java b/symphony-bdk-core/src/test/java/com/symphony/bdk/core/auth/impl/BotAuthenticatorRsaImplTest.java index d3d392e0e..17c69a952 100644 --- a/symphony-bdk-core/src/test/java/com/symphony/bdk/core/auth/impl/BotAuthenticatorRsaImplTest.java +++ b/symphony-bdk-core/src/test/java/com/symphony/bdk/core/auth/impl/BotAuthenticatorRsaImplTest.java @@ -7,9 +7,11 @@ import com.symphony.bdk.core.auth.AuthSession; import com.symphony.bdk.core.auth.exception.AuthUnauthorizedException; +import com.symphony.bdk.core.config.model.BdkCommonJwtConfig; import com.symphony.bdk.core.test.BdkMockServer; import com.symphony.bdk.core.test.BdkMockServerExtension; import com.symphony.bdk.core.test.RsaTestHelper; +import com.symphony.bdk.gen.api.model.Token; import com.symphony.bdk.http.api.ApiRuntimeException; import org.junit.jupiter.api.BeforeEach; @@ -33,7 +35,7 @@ void init(final BdkMockServer mockServer) { this.authenticator = new BotAuthenticatorRsaImpl( ofMinimalInterval(1), "username", - PRIVATE_KEY, + new BdkCommonJwtConfig(), PRIVATE_KEY, mockServer.newApiClient("/login"), mockServer.newApiClient("/relay") ); @@ -46,8 +48,8 @@ void testAuthenticateBot(final BdkMockServer mockServer) throws AuthUnauthorized final AuthSession session = this.authenticator.authenticateBot(); assertNotNull(session); - assertEquals(AuthSessionRsaImpl.class, session.getClass()); - assertEquals(this.authenticator, ((AuthSessionRsaImpl) session).getAuthenticator()); + assertEquals(AuthSessionImpl.class, session.getClass()); + assertEquals(this.authenticator, ((AuthSessionImpl) session).getAuthenticator()); } @Test @@ -55,8 +57,8 @@ void testRetrieveSessionToken(final BdkMockServer mockServer) throws AuthUnautho mockServer.onPost("/login/pubkey/authenticate", res -> res.withBody("{ \"token\": \"1234\", \"name\": \"sessionToken\" }")); - final String sessionToken = this.authenticator.retrieveSessionToken(); - assertEquals("1234", sessionToken); + final Token authToken = this.authenticator.retrieveSessionToken(); + assertEquals("1234", authToken.getToken()); } @Test diff --git a/symphony-bdk-core/src/test/java/com/symphony/bdk/core/auth/impl/OAuthSessionTest.java b/symphony-bdk-core/src/test/java/com/symphony/bdk/core/auth/impl/OAuthSessionTest.java new file mode 100644 index 000000000..661221406 --- /dev/null +++ b/symphony-bdk-core/src/test/java/com/symphony/bdk/core/auth/impl/OAuthSessionTest.java @@ -0,0 +1,42 @@ +package com.symphony.bdk.core.auth.impl; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.symphony.bdk.core.auth.AuthSession; +import com.symphony.bdk.core.auth.exception.AuthUnauthorizedException; +import com.symphony.bdk.http.api.ApiException; + +import org.junit.jupiter.api.Test; + +class OAuthSessionTest { + private final AuthSession authSession = mock(AuthSession.class); + + @Test + void testGetBearerToken() throws AuthUnauthorizedException, ApiException { + String token = "Bearer Token"; + when(authSession.getAuthorizationToken()).thenReturn(token); + + OAuthSession session = new OAuthSession(authSession); + assertEquals(token, session.getBearerToken()); + } + + @Test + void testGetBearerTokenFails() throws AuthUnauthorizedException { + when(authSession.getAuthorizationToken()).thenThrow(AuthUnauthorizedException.class); + + OAuthSession session = new OAuthSession(authSession); + assertThrows(ApiException.class, session::getBearerToken); + } + + @Test + void testGetBearerTokenNotSupported() throws AuthUnauthorizedException { + when(authSession.getAuthorizationToken()).thenThrow(UnsupportedOperationException.class); + + OAuthSession session = new OAuthSession(authSession); + assertThrows(UnsupportedOperationException.class, session::getBearerToken); + } + +} diff --git a/symphony-bdk-core/src/test/java/com/symphony/bdk/core/auth/impl/OAuthenticationTest.java b/symphony-bdk-core/src/test/java/com/symphony/bdk/core/auth/impl/OAuthenticationTest.java new file mode 100644 index 000000000..f1752d3dd --- /dev/null +++ b/symphony-bdk-core/src/test/java/com/symphony/bdk/core/auth/impl/OAuthenticationTest.java @@ -0,0 +1,60 @@ +package com.symphony.bdk.core.auth.impl; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.symphony.bdk.core.auth.AuthSession; +import com.symphony.bdk.core.auth.exception.AuthUnauthorizedException; +import com.symphony.bdk.core.auth.impl.OAuthSession; +import com.symphony.bdk.core.auth.impl.OAuthentication; +import com.symphony.bdk.http.api.ApiException; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.HashMap; +import java.util.Map; + +class OAuthenticationTest { + private Map headerParams; + private OAuthentication auth; + + @BeforeEach + void setUp() throws AuthUnauthorizedException { + this.headerParams = new HashMap<>(); + AuthSession authSession = mock(AuthSession.class); + when(authSession.getAuthorizationToken()).thenReturn("Bearer jwt"); + final OAuthSession oAuthSession = new OAuthSession(authSession); + this.auth = new OAuthentication(oAuthSession::getBearerToken); + } + + @Test + void testApplyWhenSessionTokenPresent() throws ApiException { + headerParams.put("sessionToken", "sessionValue"); + + auth.apply(headerParams); + + assertFalse(headerParams.containsKey("sessionToken")); + assertTrue(headerParams.containsKey("Authorization")); + } + + @Test + void testApplyWhenNoSessionToken() throws ApiException { + auth.apply(headerParams); + + assertFalse(headerParams.containsKey("sessionToken")); + assertTrue(headerParams.containsKey("Authorization")); + } + + @Test + void testApplyWithException() throws ApiException { + OAuthSession oAuthSession = mock(OAuthSession.class); + when(oAuthSession.getBearerToken()).thenThrow(ApiException.class); + this.auth = new OAuthentication(oAuthSession::getBearerToken); + + assertThrows(ApiException.class, ()-> auth.apply(headerParams)); + } +} diff --git a/symphony-bdk-core/src/test/java/com/symphony/bdk/core/client/loadbalancing/RegularLoadBalancedApiClientTest.java b/symphony-bdk-core/src/test/java/com/symphony/bdk/core/client/loadbalancing/RegularLoadBalancedApiClientTest.java index 833f9a38e..24b724ec9 100644 --- a/symphony-bdk-core/src/test/java/com/symphony/bdk/core/client/loadbalancing/RegularLoadBalancedApiClientTest.java +++ b/symphony-bdk-core/src/test/java/com/symphony/bdk/core/client/loadbalancing/RegularLoadBalancedApiClientTest.java @@ -1,6 +1,7 @@ package com.symphony.bdk.core.client.loadbalancing; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; @@ -138,4 +139,22 @@ public void testInvokeApiIsDelegatedAndRotateCalledWhenNonSticky() throws ApiExc eq(formParams), eq(accept), eq(contentType), eq(authNames), eq(returnType)); verify(loadBalancedApiClient, times(1)).rotate(); } + + @Test + public void testGetBasePath(){ + when(apiClient.getBasePath()).thenReturn("/pod"); + RegularLoadBalancedApiClient loadBalancedApiClient = new RegularLoadBalancedApiClient(config, apiClientFactory); + + String path = loadBalancedApiClient.getBasePath(); + + assertEquals("/pod", path); + } + + @Test + public void testGetAuthentications(){ + when(apiClient.getBasePath()).thenReturn("/pod"); + RegularLoadBalancedApiClient loadBalancedApiClient = new RegularLoadBalancedApiClient(config, apiClientFactory); + + assertNotNull(loadBalancedApiClient.getAuthentications()); + } } diff --git a/symphony-bdk-core/src/test/java/com/symphony/bdk/core/config/BdkConfigTest.java b/symphony-bdk-core/src/test/java/com/symphony/bdk/core/config/BdkConfigTest.java index 3758087d7..9dd2b5e02 100644 --- a/symphony-bdk-core/src/test/java/com/symphony/bdk/core/config/BdkConfigTest.java +++ b/symphony-bdk-core/src/test/java/com/symphony/bdk/core/config/BdkConfigTest.java @@ -8,6 +8,7 @@ import com.symphony.bdk.core.client.exception.ApiClientInitializationException; import com.symphony.bdk.core.config.model.BdkBotConfig; import com.symphony.bdk.core.config.model.BdkCertificateConfig; +import com.symphony.bdk.core.config.model.BdkCommonJwtConfig; import com.symphony.bdk.core.config.model.BdkConfig; import com.symphony.bdk.core.config.model.BdkExtAppConfig; @@ -43,6 +44,16 @@ void testIsOboConfigured() { assertIsOboConfigured("appId", null, "cert", "pass", true); } + @Test + void testIsCommonJwtEnabled() { + final BdkConfig config = new BdkConfig(); + BdkCommonJwtConfig bdkCommonJwtConfig = new BdkCommonJwtConfig(); + bdkCommonJwtConfig.setEnabled(true); + config.setCommonJwt(bdkCommonJwtConfig); + + assertTrue(config.isCommonJwtEnabled()); + } + @Test void testBdkCertificateConfigFromClasspath() { BdkCertificateConfig certificateConfig = new BdkCertificateConfig("classpath:/certs/identity.p12", "password"); diff --git a/symphony-bdk-core/src/test/java/com/symphony/bdk/core/service/datafeed/impl/DatafeedLoopV1Test.java b/symphony-bdk-core/src/test/java/com/symphony/bdk/core/service/datafeed/impl/DatafeedLoopV1Test.java index 03b6fd7e8..a6142323b 100644 --- a/symphony-bdk-core/src/test/java/com/symphony/bdk/core/service/datafeed/impl/DatafeedLoopV1Test.java +++ b/symphony-bdk-core/src/test/java/com/symphony/bdk/core/service/datafeed/impl/DatafeedLoopV1Test.java @@ -16,7 +16,7 @@ import com.symphony.bdk.core.auth.AuthSession; import com.symphony.bdk.core.auth.exception.AuthUnauthorizedException; -import com.symphony.bdk.core.auth.impl.AuthSessionRsaImpl; +import com.symphony.bdk.core.auth.impl.AuthSessionImpl; import com.symphony.bdk.core.client.loadbalancing.LoadBalancedApiClient; import com.symphony.bdk.core.config.BdkConfigLoader; import com.symphony.bdk.core.config.exception.BdkConfigException; @@ -104,7 +104,7 @@ void init() throws BdkConfigException { } private void initializeAuthSession() { - this.authSession = Mockito.mock(AuthSessionRsaImpl.class); + this.authSession = Mockito.mock(AuthSessionImpl.class); when(this.authSession.getSessionToken()).thenReturn("1234"); when(this.authSession.getKeyManagerToken()).thenReturn("1234"); } diff --git a/symphony-bdk-core/src/test/java/com/symphony/bdk/core/service/datafeed/impl/DatafeedLoopV2Test.java b/symphony-bdk-core/src/test/java/com/symphony/bdk/core/service/datafeed/impl/DatafeedLoopV2Test.java index 10f09b1a3..afd651418 100644 --- a/symphony-bdk-core/src/test/java/com/symphony/bdk/core/service/datafeed/impl/DatafeedLoopV2Test.java +++ b/symphony-bdk-core/src/test/java/com/symphony/bdk/core/service/datafeed/impl/DatafeedLoopV2Test.java @@ -16,7 +16,7 @@ import com.symphony.bdk.core.auth.AuthSession; import com.symphony.bdk.core.auth.exception.AuthUnauthorizedException; -import com.symphony.bdk.core.auth.impl.AuthSessionRsaImpl; +import com.symphony.bdk.core.auth.impl.AuthSessionImpl; import com.symphony.bdk.core.config.BdkConfigLoader; import com.symphony.bdk.core.config.exception.BdkConfigException; import com.symphony.bdk.core.config.model.BdkConfig; @@ -76,7 +76,7 @@ void setUp() throws BdkConfigException { bdkConfig.setRetry(ofMinimalInterval(2)); this.botInfo = Mockito.mock(UserV2.class); - this.authSession = Mockito.mock(AuthSessionRsaImpl.class); + this.authSession = Mockito.mock(AuthSessionImpl.class); when(this.authSession.getSessionToken()).thenReturn("1234"); when(this.authSession.getKeyManagerToken()).thenReturn("1234"); diff --git a/symphony-bdk-core/src/test/java/com/symphony/bdk/core/service/message/MessageServiceTest.java b/symphony-bdk-core/src/test/java/com/symphony/bdk/core/service/message/MessageServiceTest.java index 1c37fd2f1..0ef205631 100644 --- a/symphony-bdk-core/src/test/java/com/symphony/bdk/core/service/message/MessageServiceTest.java +++ b/symphony-bdk-core/src/test/java/com/symphony/bdk/core/service/message/MessageServiceTest.java @@ -84,7 +84,6 @@ class MessageServiceTest { private static final String V1_ALLOWED_TYPES = "/pod/v1/files/allowedTypes"; private static final String V1_STREAM_ATTACHMENTS = "/pod/v1/streams/{sid}/attachments"; private static final String V1_MESSAGE_GET = "/agent/v1/message/{id}"; - private static final String V2_MESSAGE_IDS = "/pod/v2/admin/streams/{streamId}/messageIds"; private static final String V1_MESSAGE_RECEIPTS = "/pod/v1/admin/messages/{messageId}/receipts"; private static final String V1_MESSAGE_RELATIONSHIPS = "/pod/v1/admin/messages/{messageId}/metadata/relationships"; diff --git a/symphony-bdk-http/symphony-bdk-http-api/src/main/java/com/symphony/bdk/http/api/ApiClient.java b/symphony-bdk-http/symphony-bdk-http-api/src/main/java/com/symphony/bdk/http/api/ApiClient.java index bdc39af79..d79449860 100644 --- a/symphony-bdk-http/symphony-bdk-http-api/src/main/java/com/symphony/bdk/http/api/ApiClient.java +++ b/symphony-bdk-http/symphony-bdk-http-api/src/main/java/com/symphony/bdk/http/api/ApiClient.java @@ -104,6 +104,13 @@ ApiResponse invokeAPI( */ Map getAuthentications(); + /** + * Enforce an authentication scheme using the authentication name. + * + * @param name of the authentication + */ + void addEnforcedAuthenticationScheme(String name); + /** * Change target server according to the load balancing configuration, applies only for calls to the agent. * Default implementation does nothing. diff --git a/symphony-bdk-http/symphony-bdk-http-api/src/main/java/com/symphony/bdk/http/api/ApiException.java b/symphony-bdk-http/symphony-bdk-http-api/src/main/java/com/symphony/bdk/http/api/ApiException.java index 59520c55b..016c6a8ff 100644 --- a/symphony-bdk-http/symphony-bdk-http-api/src/main/java/com/symphony/bdk/http/api/ApiException.java +++ b/symphony-bdk-http/symphony-bdk-http-api/src/main/java/com/symphony/bdk/http/api/ApiException.java @@ -11,85 +11,97 @@ /** * Main exception raised when invoking {@link ApiClient#invokeAPI(String, String, List, Object, Map, Map, Map, String, String, String[], TypeReference)}. - * + *

    * Initially generated by the OpenAPI Maven Generator */ @Getter @API(status = API.Status.STABLE) public class ApiException extends Exception { - private int code = 0; - private Map> responseHeaders = null; - private String responseBody = null; + private int code = 0; + private Map> responseHeaders = null; + private String responseBody = null; - /** - * Creates new {@link ApiException} instance. - * - * @param message the detail message. - * @param throwable the cause. - */ - public ApiException(String message, Throwable throwable) { - super(message, throwable); - } + /** + * Creates new {@link ApiException} instance. + * + * @param message the detail message. + * @param throwable the cause. + */ + public ApiException(String message, Throwable throwable) { + super(message, throwable); + } - /** - * Creates new {@link ApiException} instance. - * - * @param code the HTTP response status code. - * @param message the detail message. - */ - public ApiException(int code, String message) { - super(message); - this.code = code; - } + /** + * Creates new {@link ApiException} instance. + * + * @param code the HTTP response status code. + * @param throwable the cause. + */ + public ApiException(int code, Throwable throwable) { + super(throwable); + this.code = code; + } - /** - * Creates new {@link ApiException} instance. - * - * @param code the HTTP response status code. - * @param message the detail message. - * @param responseHeaders list of headers returned by the server. - * @param responseBody content of the response sent back by the server. - */ - public ApiException(int code, String message, Map> responseHeaders, String responseBody) { - this(code, message); - this.responseHeaders = responseHeaders; - this.responseBody = responseBody; - } /** - * Check if response status is unauthorized or not. - * - * @return true if response status is 401, false otherwise - */ - public boolean isUnauthorized() { - return this.code == HttpURLConnection.HTTP_UNAUTHORIZED; - } + * Creates new {@link ApiException} instance. + * + * @param code the HTTP response status code. + * @param message the detail message. + */ + public ApiException(int code, String message) { + super(message); + this.code = code; + } - /** - * Check if response status is client error or not - * - * @return true if response status is 400, false otherwise - */ - public boolean isClientError() { - return this.code == HttpURLConnection.HTTP_BAD_REQUEST; - } + /** + * Creates new {@link ApiException} instance. + * + * @param code the HTTP response status code. + * @param message the detail message. + * @param responseHeaders list of headers returned by the server. + * @param responseBody content of the response sent back by the server. + */ + public ApiException(int code, String message, Map> responseHeaders, String responseBody) { + this(code, message); + this.responseHeaders = responseHeaders; + this.responseBody = responseBody; + } - /** - * Check if response status is a server error (5xx) but not an internal server error (500) - * - * @return true if response status strictly greater than 500, false otherwise - */ - public boolean isServerError() { - return this.code >= HttpURLConnection.HTTP_INTERNAL_ERROR; - } + /** + * Check if response status is unauthorized or not. + * + * @return true if response status is 401, false otherwise + */ + public boolean isUnauthorized() { + return this.code == HttpURLConnection.HTTP_UNAUTHORIZED; + } - /** - * Check if response status corresponds to a too many requests error (429) - * - * @return true if error code is 429 - */ - public boolean isTooManyRequestsError() { - return this.code == 429; - } + /** + * Check if response status is client error or not + * + * @return true if response status is 400, false otherwise + */ + public boolean isClientError() { + return this.code == HttpURLConnection.HTTP_BAD_REQUEST; + } + + /** + * Check if response status is a server error (5xx) but not an internal server error (500) + * + * @return true if response status strictly greater than 500, false otherwise + */ + public boolean isServerError() { + return this.code >= HttpURLConnection.HTTP_INTERNAL_ERROR; + } + + /** + * Check if response status corresponds to a too many requests error (429) + * + * @return true if error code is 429 + */ + public boolean isTooManyRequestsError() { + return this.code == 429; + } } diff --git a/symphony-bdk-http/symphony-bdk-http-api/src/main/java/com/symphony/bdk/http/api/auth/Authentication.java b/symphony-bdk-http/symphony-bdk-http-api/src/main/java/com/symphony/bdk/http/api/auth/Authentication.java index daee603d9..341166899 100644 --- a/symphony-bdk-http/symphony-bdk-http-api/src/main/java/com/symphony/bdk/http/api/auth/Authentication.java +++ b/symphony-bdk-http/symphony-bdk-http-api/src/main/java/com/symphony/bdk/http/api/auth/Authentication.java @@ -1,6 +1,7 @@ package com.symphony.bdk.http.api.auth; import com.symphony.bdk.http.api.ApiClient; +import com.symphony.bdk.http.api.ApiException; import com.symphony.bdk.http.api.Pair; import org.apiguardian.api.API; @@ -19,8 +20,7 @@ public interface Authentication { /** * Apply authentication settings to header and query params. * - * @param queryParams List of query parameters * @param headerParams Map of header parameters */ - void applyToParams(List queryParams, Map headerParams); + void apply(Map headerParams) throws ApiException; } diff --git a/symphony-bdk-http/symphony-bdk-http-jersey2/src/main/java/com/symphony/bdk/http/jersey2/ApiClientJersey2.java b/symphony-bdk-http/symphony-bdk-http-jersey2/src/main/java/com/symphony/bdk/http/jersey2/ApiClientJersey2.java index 0e6e2fc7f..073ff4135 100644 --- a/symphony-bdk-http/symphony-bdk-http-jersey2/src/main/java/com/symphony/bdk/http/jersey2/ApiClientJersey2.java +++ b/symphony-bdk-http/symphony-bdk-http-jersey2/src/main/java/com/symphony/bdk/http/jersey2/ApiClientJersey2.java @@ -28,6 +28,7 @@ import java.nio.file.Files; import java.nio.file.StandardCopyOption; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.List; @@ -35,6 +36,7 @@ import java.util.Map.Entry; import java.util.regex.Matcher; import java.util.regex.Pattern; +import java.util.stream.Stream; import javax.ws.rs.HttpMethod; import javax.ws.rs.ProcessingException; @@ -59,6 +61,7 @@ public class ApiClientJersey2 implements ApiClient { protected Map defaultHeaderMap; protected String tempFolderPath; protected Map authentications; + protected List enforcedAuthenticationSchemes; public ApiClientJersey2(final Client httpClient, String basePath, Map defaultHeaders, String temporaryFolderPath) { @@ -67,6 +70,7 @@ public ApiClientJersey2(final Client httpClient, String basePath, Map(defaultHeaders); this.tempFolderPath = temporaryFolderPath; this.authentications = new HashMap<>(); + this.enforcedAuthenticationSchemes = new ArrayList<>(); } /** @@ -91,7 +95,7 @@ public ApiResponse invokeAPI( // to support (constant) query string in `path`, e.g. "/posts?draft=1" WebTarget target = httpClient.target(this.basePath + path); - this.updateParamsForAuth(authNames, queryParams, headerParams); + this.updateParamsForAuth(authNames, headerParams); if (queryParams != null) { for (Pair queryParam : queryParams) { @@ -372,6 +376,14 @@ public Map getAuthentications() { return this.authentications; } + /** + * {@inheritDoc} + */ + @Override + public void addEnforcedAuthenticationScheme(String name) { + this.enforcedAuthenticationSchemes.add(name); + } + /** * Check if the given MIME is a JSON MIME. * JSON MIME examples: @@ -552,18 +564,29 @@ protected Map> buildResponseHeaders(Response response) { * * @param authNames The authentications to apply */ - protected void updateParamsForAuth(String[] authNames, List queryParams, Map headerParams) { + protected void updateParamsForAuth(String[] authNames, Map headerParams) throws ApiException { - if (authNames == null) { + if (authNames == null && this.enforcedAuthenticationSchemes.isEmpty()) { return; } + authNames = withEnforcedSecurityScheme(authNames); + for (String authName : authNames) { Authentication auth = this.authentications.get(authName); if (auth == null) { throw new RuntimeException("Authentication undefined: " + authName); } - auth.applyToParams(queryParams, headerParams); + auth.apply(headerParams); } } + + private String[] withEnforcedSecurityScheme(String[] authNames) { + + if (authNames == null) { + authNames = new String[0]; + } + + return Stream.concat(this.enforcedAuthenticationSchemes.stream(), Arrays.stream(authNames)).toArray(String[]::new); + } } diff --git a/symphony-bdk-http/symphony-bdk-http-jersey2/src/test/java/com/symphony/bdk/http/jersey2/ApiClientJersey2Test.java b/symphony-bdk-http/symphony-bdk-http-jersey2/src/test/java/com/symphony/bdk/http/jersey2/ApiClientJersey2Test.java index 25bb5b313..255882e13 100644 --- a/symphony-bdk-http/symphony-bdk-http-jersey2/src/test/java/com/symphony/bdk/http/jersey2/ApiClientJersey2Test.java +++ b/symphony-bdk-http/symphony-bdk-http-jersey2/src/test/java/com/symphony/bdk/http/jersey2/ApiClientJersey2Test.java @@ -50,9 +50,7 @@ void init( when(statusInfo.getFamily()).thenReturn(Response.Status.Family.SUCCESSFUL); when(response.getHeaders()).thenReturn(new MultivaluedHashMap<>()); this.apiClient = new ApiClientJersey2(client, "", Collections.emptyMap(), ""); - this.apiClient.getAuthentications().put("testAuth", (queryParams, headerParams) -> { - headerParams.put("Authorization", "test"); - }); + this.apiClient.getAuthentications().put("testAuth", headerParams -> headerParams.put("Authorization", "test")); } @Test diff --git a/symphony-bdk-http/symphony-bdk-http-webclient/src/main/java/com/symphony/bdk/http/webclient/ApiClientWebClient.java b/symphony-bdk-http/symphony-bdk-http-webclient/src/main/java/com/symphony/bdk/http/webclient/ApiClientWebClient.java index 3c6746250..16e795bd8 100644 --- a/symphony-bdk-http/symphony-bdk-http-webclient/src/main/java/com/symphony/bdk/http/webclient/ApiClientWebClient.java +++ b/symphony-bdk-http/symphony-bdk-http-webclient/src/main/java/com/symphony/bdk/http/webclient/ApiClientWebClient.java @@ -30,11 +30,13 @@ import java.io.File; import java.net.SocketTimeoutException; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.stream.Collectors; +import java.util.stream.Stream; /** * Spring WebClient implementation for the {@link ApiClient} interface called by generated code. @@ -46,12 +48,14 @@ public class ApiClientWebClient implements ApiClient { protected final String basePath; protected final Map defaultHeaderMap; protected Map authentications; + protected List enforcedAuthenticationSchemes; public ApiClientWebClient(final WebClient webClient, String basePath, Map defaultHeaders) { this.webClient = webClient; this.basePath = basePath; this.defaultHeaderMap = new HashMap<>(defaultHeaders); this.authentications = new HashMap<>(); + this.enforcedAuthenticationSchemes = new ArrayList<>(); } /** @@ -77,7 +81,7 @@ public ApiResponse invokeAPI( throw new ApiException(500, "unknown method type " + method); } - this.updateParamsForAuth(authNames, queryParams, headerParams); + this.updateParamsForAuth(authNames, headerParams); WebClient.RequestBodySpec requestBodySpec = this.webClient.method(httpMethod).uri(uriBuilder -> { @@ -258,21 +262,30 @@ private void serializeApiClientBodyPart(String paramKey, ApiClientBodyPart bodyP * * @param authNames The authentications to apply */ - private void updateParamsForAuth(String[] authNames, List queryParams, Map headerParams) { + private void updateParamsForAuth(String[] authNames, Map headerParams) throws ApiException { - if (authNames == null) { + if (authNames == null && this.enforcedAuthenticationSchemes.isEmpty()) { return; } - + authNames = withEnforcedSecurityScheme(authNames); for (String authName : authNames) { Authentication auth = this.authentications.get(authName); if (auth == null) { throw new RuntimeException("Authentication undefined: " + authName); } - auth.applyToParams(queryParams, headerParams); + auth.apply(headerParams); } } + private String[] withEnforcedSecurityScheme(String[] authNames) { + if (authNames == null) { + authNames = new String[0]; + } + + return Stream.concat(this.enforcedAuthenticationSchemes.stream(), Arrays.stream(authNames)).toArray(String[]::new); + } + + /** * {@inheritDoc} */ @@ -418,4 +431,12 @@ public String escapeString(String str) { public Map getAuthentications() { return this.authentications; } + + /** + * {@inheritDoc} + */ + @Override + public void addEnforcedAuthenticationScheme(String name) { + this.enforcedAuthenticationSchemes.add(name); + } } diff --git a/symphony-bdk-http/symphony-bdk-http-webclient/src/test/java/com/symphony/bdk/http/webclient/ApiClientWebClientTest.java b/symphony-bdk-http/symphony-bdk-http-webclient/src/test/java/com/symphony/bdk/http/webclient/ApiClientWebClientTest.java index cf27ccab6..6a8d51238 100644 --- a/symphony-bdk-http/symphony-bdk-http-webclient/src/test/java/com/symphony/bdk/http/webclient/ApiClientWebClientTest.java +++ b/symphony-bdk-http/symphony-bdk-http-webclient/src/test/java/com/symphony/bdk/http/webclient/ApiClientWebClientTest.java @@ -45,9 +45,7 @@ class ApiClientWebClientTest { void setUp(final BdkMockServer mockServer) { this.apiClient = mockServer.newApiClient(""); - this.apiClient.getAuthentications().put("testAuth", (queryParams, headerParams) -> { - headerParams.put("Authorization", "test"); - }); + this.apiClient.getAuthentications().put("testAuth", headerParams -> headerParams.put("Authorization", "test")); } @Test @@ -69,7 +67,7 @@ void testInvokeApiTest(final BdkMockServer mockServer) throws ApiException { final Map headers = new HashMap<>(); headers.put("sessionToken", "test-token"); - + this.apiClient.addEnforcedAuthenticationScheme("testAuth"); ApiResponse response = this.apiClient.invokeAPI("/test-api", "GET", null, null, headers, null, null, null, "application/json", new String[] { "testAuth" }, new TypeReference() {}); @@ -343,6 +341,7 @@ void parameterToPairsTest() { pairs.addAll(this.apiClient.parameterToPairs("ssv", "ssv", Arrays.asList("test1", "test2"))); pairs.addAll(this.apiClient.parameterToPairs("tsv", "tsv", Arrays.asList("test1", "test2"))); pairs.addAll(this.apiClient.parameterToPairs("pipes", "pipes", Arrays.asList("test1", "test2"))); + pairs.addAll(this.apiClient.parameterToPairs("pipes", "", Arrays.asList("test1", "test2"))); assertEquals(7, pairs.size()); assertEquals("test-value", pairs.get(0).getValue()); diff --git a/symphony-bdk-spring/symphony-bdk-core-spring-boot-starter/src/main/java/com/symphony/bdk/spring/config/BdkCoreConfig.java b/symphony-bdk-spring/symphony-bdk-core-spring-boot-starter/src/main/java/com/symphony/bdk/spring/config/BdkCoreConfig.java index 4a13c3f4e..fbac79436 100644 --- a/symphony-bdk-spring/symphony-bdk-core-spring-boot-starter/src/main/java/com/symphony/bdk/spring/config/BdkCoreConfig.java +++ b/symphony-bdk-spring/symphony-bdk-core-spring-boot-starter/src/main/java/com/symphony/bdk/spring/config/BdkCoreConfig.java @@ -1,17 +1,20 @@ package com.symphony.bdk.spring.config; +import static com.symphony.bdk.core.auth.impl.OAuthentication.BEARER_AUTH; + import com.symphony.bdk.core.auth.AuthSession; import com.symphony.bdk.core.auth.AuthenticatorFactory; import com.symphony.bdk.core.auth.ExtensionAppTokensRepository; +import com.symphony.bdk.core.auth.impl.OAuthSession; +import com.symphony.bdk.core.auth.impl.OAuthentication; import com.symphony.bdk.core.auth.exception.AuthInitializationException; import com.symphony.bdk.core.auth.exception.AuthUnauthorizedException; import com.symphony.bdk.core.auth.impl.InMemoryTokensRepository; import com.symphony.bdk.core.client.ApiClientFactory; -import com.symphony.bdk.gen.api.model.ExtensionAppTokens; +import com.symphony.bdk.core.config.model.BdkConfig; import com.symphony.bdk.http.api.ApiClient; import com.symphony.bdk.http.jersey2.ApiClientBuilderProviderJersey2; import com.symphony.bdk.spring.SymphonyBdkCoreProperties; - import com.symphony.bdk.template.api.TemplateEngine; import com.symphony.bdk.template.freemarker.FreeMarkerEngine; @@ -21,6 +24,8 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.annotation.Bean; +import java.util.Optional; + /** * Configuration and injection of the main BDK/Core classes as beans within the Spring application context. */ @@ -30,7 +35,8 @@ public class BdkCoreConfig { @Bean @ConditionalOnMissingBean public ApiClientFactory apiClientFactory(SymphonyBdkCoreProperties properties) { - return new ApiClientFactory(properties, new ApiClientBuilderProviderJersey2()); // TODO create RestTemplate/or WebClient implementation + return new ApiClientFactory(properties, + new ApiClientBuilderProviderJersey2()); // TODO create RestTemplate/or WebClient implementation } @Bean(name = "agentApiClient") @@ -44,8 +50,19 @@ public ApiClient datafeedAgentApiClient(ApiClientFactory apiClientFactory) { } @Bean(name = "podApiClient") - public ApiClient podApiClient(ApiClientFactory apiClientFactory) { - return apiClientFactory.getPodClient(); + public ApiClient podApiClient(ApiClientFactory apiClientFactory, Optional botSession, BdkConfig config) { + ApiClient client = apiClientFactory.getPodClient(); + if (config.isCommonJwtEnabled()) { + if (config.isOboConfigured()) { + throw new UnsupportedOperationException( + "Common JWT feature is not available yet in OBO mode, please set commonJwt.enabled to false."); + } else if (botSession.isPresent()) { + final OAuthSession oAuthSession = new OAuthSession(botSession.get()); + client.getAuthentications().put(BEARER_AUTH, new OAuthentication(oAuthSession::getBearerToken)); + client.addEnforcedAuthenticationScheme(BEARER_AUTH); + } + } + return client; } @Bean(name = "relayApiClient") @@ -78,7 +95,8 @@ public ExtensionAppTokensRepository extensionAppTokensRepository() { @Bean @ConditionalOnMissingBean - public AuthenticatorFactory authenticatorFactory(SymphonyBdkCoreProperties properties, ApiClientFactory apiClientFactory, ExtensionAppTokensRepository extensionAppTokensRepository) { + public AuthenticatorFactory authenticatorFactory(SymphonyBdkCoreProperties properties, + ApiClientFactory apiClientFactory, ExtensionAppTokensRepository extensionAppTokensRepository) { return new AuthenticatorFactory(properties, apiClientFactory, extensionAppTokensRepository); } diff --git a/symphony-bdk-spring/symphony-bdk-core-spring-boot-starter/src/test/java/com/symphony/bdk/spring/SymphonyBdkAutoConfigurationTest.java b/symphony-bdk-spring/symphony-bdk-core-spring-boot-starter/src/test/java/com/symphony/bdk/spring/SymphonyBdkAutoConfigurationTest.java index b4e944cab..8717e8ac5 100644 --- a/symphony-bdk-spring/symphony-bdk-core-spring-boot-starter/src/test/java/com/symphony/bdk/spring/SymphonyBdkAutoConfigurationTest.java +++ b/symphony-bdk-spring/symphony-bdk-core-spring-boot-starter/src/test/java/com/symphony/bdk/spring/SymphonyBdkAutoConfigurationTest.java @@ -10,6 +10,7 @@ import com.symphony.bdk.core.client.loadbalancing.DatafeedLoadBalancedApiClient; import com.symphony.bdk.core.service.datafeed.DatafeedLoop; import com.symphony.bdk.gen.api.SystemApi; +import com.symphony.bdk.http.api.ApiClient; import com.symphony.bdk.spring.annotation.SlashAnnotationProcessor; import com.symphony.bdk.spring.config.BdkActivityConfig; import com.symphony.bdk.spring.config.BdkOboServiceConfig; @@ -134,6 +135,80 @@ void shouldInitializeCustomAuthenticatorsIfTheyExist() { }); } + @Test + void shouldAddAuthenticationIfCommonJwtEnabled() { + final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withPropertyValues( + "bdk.pod.scheme=http", + "bdk.pod.host=localhost", + + "bdk.agent.scheme=http", + "bdk.agent.host=localhost", + + "bdk.keyManager.scheme=http", + "bdk.keyManager.host=localhost", + + "bdk.bot.username=testBot", + "bdk.bot.privateKey.path=classpath:/privatekey.pem", + + "bdk.commonJwt.enabled=true" + ) + .withUserConfiguration(SymphonyBdkMockedConfiguration.class) + .withConfiguration(AutoConfigurations.of(SymphonyBdkAutoConfiguration.class)); + + contextRunner.run(context -> { + assertThat(context).hasBean("podApiClient"); + ApiClient podClient = (ApiClient) context.getBean("podApiClient"); + assertThat(podClient.getAuthentications()).isNotEmpty(); + assertThat(podClient.getAuthentications()).containsKey("bearerAuth"); + }); + } + + @Test + void shouldFailOnOboWithCommonJwtEnabled() { + final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withPropertyValues( + "bdk.host=localhost", + + "bdk.bot.username=testBot", + "bdk.bot.privateKey.path=classpath:/privatekey.pem", + + "bdk.app.appId=my-app", + "bdk.app.privateKey.path=classpath:/privatekey.pem", + + "bdk.commonJwt.enabled=true" + ) + .withUserConfiguration(SymphonyBdkMockedConfiguration.class) + .withConfiguration(AutoConfigurations.of(SymphonyBdkAutoConfiguration.class)); + + + contextRunner.run(context -> { + assertThat(context).hasFailed(); + assertThat(context).getFailure().hasRootCauseInstanceOf(UnsupportedOperationException.class); + }); + } + + @Test + void shouldFailOnOboOnlyWithCommonJwtEnabled() { + final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withPropertyValues( + "bdk.host=localhost", + + "bdk.app.appId=my-app", + "bdk.app.privateKey.path=classpath:/privatekey.pem", + + "bdk.commonJwt.enabled=true" + ) + .withUserConfiguration(SymphonyBdkMockedConfiguration.class) + .withConfiguration(AutoConfigurations.of(SymphonyBdkAutoConfiguration.class)); + + + contextRunner.run(context -> { + assertThat(context).hasFailed(); + assertThat(context).getFailure().hasRootCauseInstanceOf(UnsupportedOperationException.class); + }); + } + @Test void shouldFailOnOboAuthenticatorInitializationIfNotProperlyConfigured() { final ApplicationContextRunner contextRunner = new ApplicationContextRunner() diff --git a/symphony-bdk-spring/symphony-bdk-core-spring-boot-starter/src/test/java/com/symphony/bdk/spring/SymphonyBdkMockedConfiguration.java b/symphony-bdk-spring/symphony-bdk-core-spring-boot-starter/src/test/java/com/symphony/bdk/spring/SymphonyBdkMockedConfiguration.java index 7ee8bcef6..fcdf2acd8 100644 --- a/symphony-bdk-spring/symphony-bdk-core-spring-boot-starter/src/test/java/com/symphony/bdk/spring/SymphonyBdkMockedConfiguration.java +++ b/symphony-bdk-spring/symphony-bdk-core-spring-boot-starter/src/test/java/com/symphony/bdk/spring/SymphonyBdkMockedConfiguration.java @@ -15,6 +15,8 @@ import javax.annotation.Nonnull; +import static com.symphony.bdk.core.auth.JwtHelperTest.JWT; + /** * */ @@ -84,7 +86,8 @@ public ApiClient getPodClient() { @Override public ApiClient getRelayClient() { - this.relayApiClient.onPost("/relay/pubkey/authenticate", "{ \"token\":\"123456789\", \"name\":\"keyManagerToken\" }"); + this.relayApiClient.onPost("/relay/pubkey/authenticate", + "{ \"token\":\"123456789\", \"name\":\"keyManagerToken\", \"authorizationToken\":\"Bearer " + JWT + "\" }"); return this.relayApiClient.getApiClient("/relay"); } @@ -92,7 +95,10 @@ public ApiClient getRelayClient() { @Override public ApiClient getLoginClient() { - this.loginApiClient.onPost("/login/pubkey/authenticate", "{ \"token\":\"123456789\", \"name\":\"sessionToken\" }"); + this.loginApiClient.onPost("/login/pubkey/authenticate", + "{ \"token\":\"123456789\", \"name\":\"sessionToken\", \"authorizationToken\":\"Bearer " + JWT + "\" }"); + this.loginApiClient.onPost("/login/idm/tokens", + "{ \"token_type\": \"Bearer\", \"expires_in\": 300, \"access_token\": \"" +JWT+ "\"}"); return this.loginApiClient.getApiClient("/login"); } diff --git a/symphony-bdk-spring/symphony-bdk-core-spring-boot-starter/src/test/java/com/symphony/bdk/spring/config/BdkCoreConfigTest.java b/symphony-bdk-spring/symphony-bdk-core-spring-boot-starter/src/test/java/com/symphony/bdk/spring/config/BdkCoreConfigTest.java index c0b451cd8..8dad84753 100644 --- a/symphony-bdk-spring/symphony-bdk-core-spring-boot-starter/src/test/java/com/symphony/bdk/spring/config/BdkCoreConfigTest.java +++ b/symphony-bdk-spring/symphony-bdk-core-spring-boot-starter/src/test/java/com/symphony/bdk/spring/config/BdkCoreConfigTest.java @@ -5,6 +5,7 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import com.symphony.bdk.core.auth.AuthSession; import com.symphony.bdk.core.auth.AuthenticatorFactory; import com.symphony.bdk.core.auth.BotAuthenticator; import com.symphony.bdk.core.auth.ExtensionAppAuthenticator; @@ -12,12 +13,16 @@ import com.symphony.bdk.core.auth.exception.AuthInitializationException; import com.symphony.bdk.core.auth.exception.AuthUnauthorizedException; import com.symphony.bdk.core.client.ApiClientFactory; +import com.symphony.bdk.core.config.model.BdkCommonJwtConfig; +import com.symphony.bdk.core.config.model.BdkConfig; import com.symphony.bdk.http.api.ApiClient; import com.symphony.bdk.spring.SymphonyBdkCoreProperties; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.BeanInitializationException; +import java.util.Optional; + /** * Test class for the {@link BdkCoreConfig}. Mainly for coverage... */ @@ -43,6 +48,18 @@ void shouldCreateApiClientFactory() { assertNotNull(config.apiClientFactory(props)); } + @Test + void shouldCreatePodClient() { + final BdkCoreConfig config = new BdkCoreConfig(); + final BdkConfig bdkConfig = new BdkConfig(); + BdkCommonJwtConfig bdkCommonJwtConfig = new BdkCommonJwtConfig(); + bdkCommonJwtConfig.setEnabled(true); + bdkConfig.setCommonJwt(bdkCommonJwtConfig); + final ApiClientFactory factory = mock(ApiClientFactory.class); + final AuthSession authSession = mock(AuthSession.class); + when(factory.getPodClient()).thenReturn(mock(ApiClient.class)); + assertNotNull(config.podApiClient(factory, Optional.ofNullable(authSession), bdkConfig)); + } @Test void shouldCreateKeyAuthApiClient() { final BdkCoreConfig config = new BdkCoreConfig();