Skip to content

Commit

Permalink
Introducing Common JWT support for Pod APIs (#618)
Browse files Browse the repository at this point in the history
Goal of this implementation is to be able to start using the common JWT to call pod public APIs.
First of all the usage of common jwt can be configured by a configuration property in the application.yaml file (commonJwt.enabled=true).

When the feature is enabled, the Authorization token will be used as header for each Pod call instead of the sessionToken.
To do so, we are programmatically enforcing a new Authentication scheme (OAuthorization) which is in charge of adding the Authorization token in the headers and removing the sessionToken when the feature is enabled.

Being the common jwt token expiration very low respect to the sessionToken, the OAuthSession is going to make sure that before making any Api call with the new Authorization scheme, the common jwt is up to date and in case it expires it is going to be refreshed before.
  • Loading branch information
symphony-mariacristina authored Jan 14, 2022
1 parent c69b441 commit b77d36a
Show file tree
Hide file tree
Showing 46 changed files with 1,198 additions and 434 deletions.
2 changes: 1 addition & 1 deletion docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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;

/**
Expand All @@ -56,6 +61,7 @@
* <li>{@link SessionService}</li>
* </ul>
*/
@Slf4j
@API(status = API.Status.INTERNAL)
class ServiceFactory {

Expand All @@ -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);
}
}
}

/**
Expand All @@ -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);
}

/**
Expand Down Expand Up @@ -187,4 +205,5 @@ public ApplicationService getApplicationService() {
public HealthService getHealthService() {
return new HealthService(new SystemApi(this.agentClient), new SignalsApi(this.agentClient), this.authSession);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
);
Expand All @@ -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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> authenticationRetry;
protected final ApiClient loginApiClient;
private final BdkCommonJwtConfig commonJwtConfig;

private final AuthenticationRetry<String> kmAuthenticationRetry;
private final AuthenticationRetry<Token> podAuthenticationRetry;
private final AuthenticationRetry<JwtToken> 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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public abstract class AbstractOboAuthenticator implements OboAuthenticator {
protected final String appId;
private final AuthenticationRetry<String> authenticationRetry;

public AbstractOboAuthenticator(BdkRetryConfig retryConfig, String appId) {
protected AbstractOboAuthenticator(BdkRetryConfig retryConfig, String appId) {
this.appId = appId;
this.authenticationRetry = new AuthenticationRetry<>(retryConfig);
}
Expand Down

This file was deleted.

Loading

0 comments on commit b77d36a

Please sign in to comment.