-
Notifications
You must be signed in to change notification settings - Fork 69
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
APP-2887 - Authentication API #173
Changes from 18 commits
f2da794
f3ce243
fde5a81
869cece
ca57cea
a6f3379
7ff1290
b9b5936
094d900
71c5b4a
bb78690
79cb884
d7a9556
6d99f22
4e93fed
f45e5ba
e6d03ac
8c252d3
69e1baf
36f2745
bd69a8d
3dd45d5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
# Authentication | ||
|
||
To be done... | ||
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
package com.symphony.bdk.core.api.invoker; | ||
|
||
/** | ||
* New {@link ApiClient} instances provider. | ||
*/ | ||
public interface ApiClientProvider { | ||
|
||
/** | ||
* Creates a new {@link ApiClient} instance. | ||
* | ||
* @return a new {@link ApiClient} instance. | ||
*/ | ||
ApiClient newInstance(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. really good approach, we can use different libraries to generate clients!!! |
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -55,4 +55,13 @@ public ApiException(int code, String message, Map<String, List<String>> response | |
this.responseHeaders = responseHeaders; | ||
this.responseBody = responseBody; | ||
} | ||
|
||
/** | ||
* Check if response status if unauthorized or not. | ||
* | ||
* @return true if response status is 401, false otherwise | ||
*/ | ||
public boolean isUnauthorized() { | ||
return this.code == 401; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should use Enum HttpStatus instead of |
||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
package com.symphony.bdk.core.api.invoker; | ||
|
||
import lombok.Getter; | ||
import org.apiguardian.api.API; | ||
|
||
import java.util.List; | ||
import java.util.Map; | ||
|
||
/** | ||
* Runtime version of the {@link ApiException}. | ||
*/ | ||
@Getter | ||
@API(status = API.Status.EXPERIMENTAL) | ||
public class ApiRuntimeException extends RuntimeException { | ||
|
||
private final int code; | ||
private final Map<String, List<String>> responseHeaders; | ||
private final String responseBody; | ||
|
||
public ApiRuntimeException(ApiException source) { | ||
super(source); | ||
this.code = source.getCode(); | ||
this.responseHeaders = source.getResponseHeaders(); | ||
this.responseBody = source.getResponseBody(); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
package com.symphony.bdk.core.api.invoker.jersey2; | ||
|
||
import com.symphony.bdk.core.api.invoker.ApiClient; | ||
import com.symphony.bdk.core.api.invoker.ApiClientProvider; | ||
|
||
/** | ||
* Provides new {@link ApiClientJersey2} implementation of the {@link ApiClient} interface. | ||
*/ | ||
public class ApiClientProviderJersey2 implements ApiClientProvider { | ||
|
||
/** | ||
* {@inheritDoc} | ||
*/ | ||
@Override | ||
public ApiClient newInstance() { | ||
return new ApiClientJersey2(); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
com.symphony.bdk.core.api.invoker.jersey2.ApiClientProviderJersey2 |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -97,6 +97,12 @@ | |
<version>${jackson.version}</version> | ||
</dependency> | ||
|
||
<dependency> | ||
<groupId>com.fasterxml.jackson.dataformat</groupId> | ||
<artifactId>jackson-dataformat-yaml</artifactId> | ||
<version>2.11.0</version> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. management of version in properties sections is better!!! |
||
</dependency> | ||
|
||
<!-- ******************************** --> | ||
<!-- * CodeGen related dependencies * --> | ||
<!-- ******************************** --> | ||
|
@@ -129,6 +135,17 @@ | |
<artifactId>mockserver-netty</artifactId> | ||
<scope>test</scope> | ||
</dependency> | ||
<dependency> | ||
<groupId>org.mockito</groupId> | ||
<artifactId>mockito-core</artifactId> | ||
<scope>test</scope> | ||
</dependency> | ||
<dependency> | ||
<groupId>com.symphony.platformsolutions</groupId> | ||
<artifactId>symphony-bdk-core-invoker-jersey2</artifactId> | ||
<version>1.2.0-SNAPSHOT</version> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. idem!!! |
||
<scope>test</scope> | ||
</dependency> | ||
|
||
</dependencies> | ||
|
||
|
This file was deleted.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
package com.symphony.bdk.core; | ||
|
||
import com.symphony.bdk.core.auth.AuthSession; | ||
import com.symphony.bdk.core.auth.AuthenticatorFactory; | ||
import com.symphony.bdk.core.auth.OboAuthenticator; | ||
import com.symphony.bdk.core.auth.exception.AuthInitializationException; | ||
import com.symphony.bdk.core.service.Obo; | ||
import com.symphony.bdk.core.client.ApiClientFactory; | ||
import com.symphony.bdk.core.config.model.BdkConfig; | ||
import com.symphony.bdk.core.service.V4MessageService; | ||
|
||
import lombok.extern.slf4j.Slf4j; | ||
import org.apiguardian.api.API; | ||
|
||
/** | ||
* BDK entry point. | ||
*/ | ||
@Slf4j | ||
@API(status = API.Status.EXPERIMENTAL) | ||
public class SymphonyBdk { | ||
|
||
private final ApiClientFactory apiClientFactory; | ||
|
||
private final AuthSession botSession; | ||
private final OboAuthenticator oboAuthenticator; | ||
|
||
public SymphonyBdk(BdkConfig config) throws AuthInitializationException { | ||
|
||
this.apiClientFactory = new ApiClientFactory(config); | ||
|
||
final AuthenticatorFactory authenticatorFactory = new AuthenticatorFactory( | ||
config, | ||
apiClientFactory.getLoginClient(), | ||
apiClientFactory.getRelayClient() | ||
); | ||
|
||
this.botSession = authenticatorFactory.getBotAuthenticator().authenticateBot(); | ||
this.oboAuthenticator = authenticatorFactory.getOboAuthenticator(); | ||
} | ||
|
||
public V4MessageService messages() { | ||
return new V4MessageService(this.apiClientFactory.getAgentClient(), this.botSession); | ||
} | ||
|
||
public V4MessageService messages(Obo.Handle oboHandle) { | ||
AuthSession oboSession; | ||
if (oboHandle.hasUsername()) { | ||
oboSession = this.oboAuthenticator.authenticateByUsername(oboHandle.getUsername()); | ||
} else { | ||
oboSession = this.oboAuthenticator.authenticateByUserId(oboHandle.getUserId()); | ||
} | ||
return new V4MessageService(this.apiClientFactory.getAgentClient(), oboSession); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
package com.symphony.bdk.core.auth; | ||
|
||
import com.symphony.bdk.core.auth.exception.AuthUnauthorizedException; | ||
|
||
import org.apiguardian.api.API; | ||
|
||
import javax.annotation.Nullable; | ||
|
||
/** | ||
* Authentication session handle. The {@link AuthSession#refresh()} will trigger a re-auth against the API endpoints. | ||
* <p> | ||
* You should keep using the same token until you receive a HTTP 401, at which you should re-authenticate and | ||
* get a new token for a new session. | ||
* </p> | ||
*/ | ||
@API(status = API.Status.STABLE) | ||
public interface AuthSession { | ||
|
||
/** | ||
* Pod's authentication token. | ||
* | ||
* @return the Pod session token | ||
*/ | ||
@Nullable String getSessionToken(); | ||
|
||
/** | ||
* KeyManager's authentication token. | ||
* | ||
* @return the KeyManager token, null if OBO | ||
*/ | ||
@Nullable String getKeyManagerToken(); | ||
|
||
/** | ||
* Trigger re-authentication to refresh tokens. | ||
*/ | ||
void refresh() throws AuthUnauthorizedException; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,88 @@ | ||
package com.symphony.bdk.core.auth; | ||
|
||
import com.symphony.bdk.core.api.invoker.ApiClient; | ||
import com.symphony.bdk.core.auth.exception.AuthInitializationException; | ||
import com.symphony.bdk.core.auth.impl.BotAuthenticatorRSAImpl; | ||
import com.symphony.bdk.core.auth.impl.OboAuthenticatorRSAImpl; | ||
import com.symphony.bdk.core.auth.jwt.JwtHelper; | ||
import com.symphony.bdk.core.config.model.BdkConfig; | ||
|
||
import lombok.extern.slf4j.Slf4j; | ||
import org.apache.commons.io.IOUtils; | ||
import org.apiguardian.api.API; | ||
|
||
import java.io.FileInputStream; | ||
import java.io.IOException; | ||
import java.nio.charset.StandardCharsets; | ||
import java.security.GeneralSecurityException; | ||
import java.security.PrivateKey; | ||
|
||
import javax.annotation.Nonnull; | ||
|
||
/** | ||
* Factory class that provides new instances for the main authenticators : | ||
* <ul> | ||
* <li>{@link BotAuthenticator} : to authenticate the main Bot service account</li> | ||
* <li>{@link OboAuthenticator} : to perform on-behalf-of authentication</li> | ||
* </ul> | ||
*/ | ||
@Slf4j | ||
@API(status = API.Status.STABLE) | ||
public class AuthenticatorFactory { | ||
|
||
private final BdkConfig config; | ||
private final ApiClient loginApiClient; | ||
private final ApiClient relayApiClient; | ||
|
||
private JwtHelper jwtHelper = new JwtHelper(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can be final? |
||
|
||
public AuthenticatorFactory(@Nonnull BdkConfig bdkConfig, @Nonnull ApiClient loginClient, @Nonnull ApiClient relayClient) { | ||
this.config = bdkConfig; | ||
this.loginApiClient = loginClient; | ||
this.relayApiClient = relayClient; | ||
} | ||
|
||
/** | ||
* Creates a new instance of a {@link BotAuthenticator} service. | ||
* | ||
* @return a new {@link BotAuthenticator} instance. | ||
*/ | ||
public @Nonnull BotAuthenticator getBotAuthenticator() throws AuthInitializationException { | ||
|
||
return new BotAuthenticatorRSAImpl( | ||
this.config.getBot().getUsername(), | ||
this.loadPrivateKeyFromPath(this.config.getBot().getPrivateKeyPath()), | ||
this.loginApiClient, | ||
this.relayApiClient | ||
); | ||
} | ||
|
||
/** | ||
* Creates a new instance of an {@link OboAuthenticator} service. | ||
* | ||
* @return a new {@link OboAuthenticator} instance. | ||
*/ | ||
public @Nonnull OboAuthenticator getOboAuthenticator() throws AuthInitializationException { | ||
|
||
return new OboAuthenticatorRSAImpl( | ||
this.config.getApp().getAppId(), | ||
this.loadPrivateKeyFromPath(this.config.getApp().getPrivateKeyPath()), | ||
this.loginApiClient | ||
); | ||
} | ||
|
||
private PrivateKey loadPrivateKeyFromPath(String privateKeyPath) throws AuthInitializationException { | ||
log.debug("Loading RSA privateKey from path : {}", privateKeyPath); | ||
try { | ||
return this.jwtHelper.parseRSAPrivateKey(IOUtils.toString(new FileInputStream(privateKeyPath), StandardCharsets.UTF_8)); | ||
} catch (GeneralSecurityException e) { | ||
final String message = "Unable to parse RSA Private Key located at " + privateKeyPath; | ||
log.error(message, e); | ||
throw new AuthInitializationException(message, e); | ||
} catch (IOException e) { | ||
final String message = "Unable to read or find RSA Private Key from path " + privateKeyPath; | ||
log.error(message, e); | ||
throw new AuthInitializationException(message, e); | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
package com.symphony.bdk.core.auth; | ||
|
||
import org.apiguardian.api.API; | ||
|
||
import javax.annotation.Nonnull; | ||
|
||
/** | ||
* Bot authenticator service. | ||
*/ | ||
@API(status = API.Status.STABLE) | ||
public interface BotAuthenticator { | ||
|
||
/** | ||
* Authenticates a Bot's service account. | ||
* | ||
* @return the authentication session. | ||
*/ | ||
@Nonnull AuthSession authenticateBot(); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
package com.symphony.bdk.core.auth; | ||
|
||
import org.apiguardian.api.API; | ||
|
||
import javax.annotation.Nonnull; | ||
|
||
/** | ||
* On-behalf-of authenticator service. | ||
*/ | ||
@API(status = API.Status.STABLE) | ||
public interface OboAuthenticator { | ||
|
||
/** | ||
* Authenticates on-behalf-of a particular user using his username. | ||
* | ||
* @param username Username of the user. | ||
* @return the authentication session. | ||
*/ | ||
@Nonnull AuthSession authenticateByUsername(@Nonnull String username); | ||
|
||
/** | ||
* Authenticates on behalf of a particular user using his userId. | ||
* | ||
* @param userId Id of the user. | ||
* @return the authentication sessions. | ||
*/ | ||
@Nonnull AuthSession authenticateByUserId(@Nonnull Long userId); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
package com.symphony.bdk.core.auth.exception; | ||
|
||
import org.apiguardian.api.API; | ||
|
||
import javax.annotation.Nonnull; | ||
|
||
/** | ||
* Thrown when unable to read/parse a RSA Private Key or a certificate. | ||
*/ | ||
@API(status = API.Status.STABLE) | ||
public class AuthInitializationException extends Exception { | ||
|
||
public AuthInitializationException(@Nonnull String message, @Nonnull Throwable source) { | ||
super(message, source); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
create a new story to create the documentation!!! please mention the JIRA story here and put in the ticket as acceptance criteria : "Documentation must contain explanation about Client Provider and what happens if 2 different providers are configured"