diff --git a/README.md b/README.md index 6d286c69e..e4777eda2 100644 --- a/README.md +++ b/README.md @@ -99,7 +99,16 @@ auth.connector.onelogin.client_secret | Yes | The OneLogin API client secre auth.connector.onelogin.subdomain | Yes | Your orgs OneLogin subdomain [xxxxx].onelogin.com **Assumption: The current implementation looks up group membership for a user via the member_of field on the getUserById API response.** - + +##### Okta Auth Connector + +property | required | notes +------------------------------------- | -------- | ---------- +auth.connector.okta.api_key | Yes | The Okta API key +auth.connector.okta.base_url | Yes | The Okta base url (e.g. `"https://example.okta.com"` or `"https://example.oktapreview.com"`) + + + ## Running CMS Locally First, a few properties must be configured in `cms-core-code/src/main/resources/cms-local-overrides.conf` diff --git a/gradle/dependencies.gradle b/gradle/dependencies.gradle index 415acc64f..6f84301df 100644 --- a/gradle/dependencies.gradle +++ b/gradle/dependencies.gradle @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016 Nike, Inc. + * Copyright (c) 2017 Nike, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -12,6 +12,7 @@ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. + * */ repositories { @@ -28,7 +29,6 @@ def groovyVersion = '2.3.9' dependencies { compile ( "com.nike:vault-client:1.0.0", - "com.nike.riposte:riposte-spi:$riposteVersion", "com.nike.riposte:riposte-core:$riposteVersion", "com.nike.riposte:riposte-typesafe-config:$riposteVersion", @@ -59,7 +59,8 @@ dependencies { "org.flywaydb:flyway-core:4.0", "com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.7.2", "com.squareup.okhttp3:okhttp:3.3.1", - "commons-io:commons-io:2.5" + "commons-io:commons-io:2.5", + "com.okta:okta-sdk:0.0.4" ) testCompile ( diff --git a/src/main/java/com/nike/cerberus/auth/connector/okta/EmbeddedAuthResponseDataV1.java b/src/main/java/com/nike/cerberus/auth/connector/okta/EmbeddedAuthResponseDataV1.java new file mode 100644 index 000000000..8e4cc2598 --- /dev/null +++ b/src/main/java/com/nike/cerberus/auth/connector/okta/EmbeddedAuthResponseDataV1.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2017 Nike, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.nike.cerberus.auth.connector.okta; + +import com.okta.sdk.models.factors.Factor; +import com.okta.sdk.models.users.User; + +import java.util.List; + +/** + * POJO representing embedded data within the user authentication response. + */ +public class EmbeddedAuthResponseDataV1 { + + private User user; + + private List factors; + + public User getUser() { + return user; + } + + public void setUser(User user) { + this.user = user; + } + + public List getFactors() { + return factors; + } + + public void setFactors(List factors) { + this.factors = factors; + } +} diff --git a/src/main/java/com/nike/cerberus/auth/connector/okta/OktaAuthConnector.java b/src/main/java/com/nike/cerberus/auth/connector/okta/OktaAuthConnector.java new file mode 100644 index 000000000..ef27823e7 --- /dev/null +++ b/src/main/java/com/nike/cerberus/auth/connector/okta/OktaAuthConnector.java @@ -0,0 +1,108 @@ +/* + * Copyright (c) 2017 Nike, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.nike.cerberus.auth.connector.okta; + +import com.google.common.base.Preconditions; +import com.nike.cerberus.auth.connector.AuthConnector; +import com.nike.cerberus.auth.connector.AuthData; +import com.nike.cerberus.auth.connector.AuthMfaDevice; +import com.nike.cerberus.auth.connector.AuthResponse; +import com.nike.cerberus.auth.connector.AuthStatus; +import com.okta.sdk.models.auth.AuthResult; +import com.okta.sdk.models.usergroups.UserGroup; +import org.apache.commons.lang3.StringUtils; + +import javax.inject.Inject; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * Okta version 1 API implementation of the AuthConnector interface. + */ +public class OktaAuthConnector implements AuthConnector { + + private final OktaAuthHelper oktaAuthHelper; + + @Inject + public OktaAuthConnector(final OktaAuthHelper oktaAuthHelper) { + + this.oktaAuthHelper = oktaAuthHelper; + } + + @Override + public AuthResponse authenticate(String username, String password) { + + final AuthResult authResult = oktaAuthHelper.authenticateUser(username, password, null); + final EmbeddedAuthResponseDataV1 embeddedData = oktaAuthHelper.getEmbeddedAuthData(authResult); + + final AuthData authData = new AuthData(); + final AuthResponse authResponse = new AuthResponse().setData(authData); + + + if (StringUtils.equals(authResult.getStatus(), OktaAuthHelper.AUTHENTICATION_MFA_REQUIRED_STATUS)) { + authResponse.setStatus(AuthStatus.MFA_REQUIRED); + authData.setStateToken(authResult.getStateToken()); + + embeddedData.getFactors().forEach(factor -> authData.getDevices().add(new AuthMfaDevice() + .setId(factor.getId()) + .setName(oktaAuthHelper.getDeviceName(factor)))); + } else { + authResponse.setStatus(AuthStatus.SUCCESS); + } + + authData.setUserId(String.valueOf(embeddedData.getUser().getId())); + authData.setUsername(embeddedData.getUser().getProfile().getLogin()); + + return authResponse; + } + + @Override + public AuthResponse mfaCheck(String stateToken, String deviceId, String otpToken) { + + final AuthResult userAuthResult = oktaAuthHelper.verifyFactor(deviceId, stateToken, otpToken); + final EmbeddedAuthResponseDataV1 embeddedAuthData = oktaAuthHelper.getEmbeddedAuthData(userAuthResult); + + final AuthData authData = new AuthData(); + final AuthResponse authResponse = new AuthResponse().setData(authData); + + authResponse.setStatus(AuthStatus.SUCCESS); + authData.setUserId(embeddedAuthData.getUser().getId()); + authData.setUsername(embeddedAuthData.getUser().getProfile().getLogin()); + + return authResponse; + } + + @Override + public Set getGroups(AuthData authData) { + + Preconditions.checkNotNull(authData, "auth data cannot be null."); + + final List userGroups = oktaAuthHelper.getUserGroups(authData.getUserId()); + + final Set groups = new HashSet<>(); + if (userGroups == null) { + return groups; + } + + userGroups.forEach(group -> groups.add(group.getProfile().getName())); + + return groups; + } + +} diff --git a/src/main/java/com/nike/cerberus/auth/connector/okta/OktaAuthHelper.java b/src/main/java/com/nike/cerberus/auth/connector/okta/OktaAuthHelper.java new file mode 100644 index 000000000..99c4188fd --- /dev/null +++ b/src/main/java/com/nike/cerberus/auth/connector/okta/OktaAuthHelper.java @@ -0,0 +1,187 @@ +/* + * Copyright (c) 2017 Nike, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.nike.cerberus.auth.connector.okta; + +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.PropertyNamingStrategy; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableMap; +import com.google.inject.Inject; +import com.nike.backstopper.exception.ApiException; +import com.nike.cerberus.error.DefaultApiError; +import com.okta.sdk.clients.AuthApiClient; +import com.okta.sdk.clients.UserApiClient; +import com.okta.sdk.models.auth.AuthResult; +import com.okta.sdk.models.factors.Factor; +import com.okta.sdk.models.usergroups.UserGroup; +import org.apache.commons.lang3.text.WordUtils; + +import java.io.IOException; +import java.util.List; +import java.util.Map; + +/** + * Helper methods to authenticate with Okta. + */ +public class OktaAuthHelper { + + public static final String AUTHENTICATION_MFA_REQUIRED_STATUS = "MFA_REQUIRED"; + + public static final String AUTHENTICATION_SUCCESS_STATUS = "SUCCESS"; + + private static final ImmutableMap MFA_FACTOR_NAMES = ImmutableMap.of( + "google", "Google Authenticator", + "okta" , "Okta Verify"); + + private final ObjectMapper objectMapper; + + private final AuthApiClient authClient; + + private final UserApiClient userApiClient; + + @Inject + public OktaAuthHelper(final AuthApiClient authApiClient, final UserApiClient userApiClient) { + + this.objectMapper = new ObjectMapper(); + this.objectMapper.findAndRegisterModules(); + this.objectMapper.setPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE); + this.objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + this.objectMapper.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS); + this.objectMapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); + this.objectMapper.enable(SerializationFeature.WRITE_EMPTY_JSON_ARRAYS); + this.objectMapper.enable(SerializationFeature.INDENT_OUTPUT); + + this.authClient = authApiClient; + this.userApiClient = userApiClient; + } + + /** + * Request to get user group data by the user's ID. + * + * @param userId User ID + * @return User groups + */ + protected List getUserGroups(final String userId) { + + try { + return this.userApiClient.getUserGroups(userId); + } catch (IOException ioe) { + final String msg = String.format("failed to get user groups for user (%s) for reason: %s", userId, + ioe.getMessage()); + + throw ApiException.newBuilder() + .withApiErrors(DefaultApiError.GENERIC_BAD_REQUEST) + .withExceptionMessage(msg) + .build(); + } + } + + /** + * Request for verifying a MFA factor. + * + * @param factorId MFA factor id + * @param stateToken State token + * @param passCode One Time Passcode from MFA factor + * @return Session login token + */ + protected AuthResult verifyFactor(final String factorId, + final String stateToken, + final String passCode) { + + final AuthResult authResult; + try { + authResult = this.authClient.authenticateWithFactor(stateToken, factorId, passCode); + } catch (IOException ioe) { + final String msg = String.format("stateToken: %s failed to verify 2nd factor for reason: %s", + stateToken, ioe.getMessage()); + + throw ApiException.newBuilder() + .withApiErrors(DefaultApiError.AUTH_BAD_CREDENTIALS) + .withExceptionMessage(msg) + .build(); + } + + return authResult; + } + + /** + * Authenticate a user with Okta + * + * @param username Okta username + * @param password Okta password + * @param relayState Deep link to redirect user to after authentication + * @return Session login token + */ + protected AuthResult authenticateUser(final String username, final String password, + final String relayState) { + + try { + return this.authClient.authenticate(username, password, relayState); + } catch (IOException ioe) { + final String msg = String.format("failed to authenticate user (%s) for reason: %s", username, + ioe.getMessage()); + + throw ApiException.newBuilder() + .withApiErrors(DefaultApiError.AUTH_BAD_CREDENTIALS) + .withExceptionMessage(msg) + .build(); } + } + + /** + * Convenience method for parsing the Okta response and mapping it to a class. + * + * @param authResult The Okta authentication result object + * @return Deserialized object from the response body + */ + protected EmbeddedAuthResponseDataV1 getEmbeddedAuthData(final AuthResult authResult) { + + Preconditions.checkArgument(authResult != null, "auth result cannot be null."); + + final Map embedded = authResult.getEmbedded(); + + try { + final String embeddedJson = objectMapper.writeValueAsString(embedded); + return objectMapper.readValue(embeddedJson, EmbeddedAuthResponseDataV1.class); + } catch (IOException e) { + throw ApiException.newBuilder() + .withApiErrors(DefaultApiError.INTERNAL_SERVER_ERROR) + .withExceptionCause(e) + .withExceptionMessage("Error parsing the embedded auth data from Okta.") + .build(); + } + } + + /** + * Print a user-friendly name for a MFA device + * @param factor - Okta MFA factor + * @return Device name + */ + protected String getDeviceName(final Factor factor) { + + Preconditions.checkArgument(factor != null, "factor cannot be null."); + + final String factorProvider = factor.getProvider().toLowerCase(); + if (MFA_FACTOR_NAMES.containsKey(factorProvider)) { + return MFA_FACTOR_NAMES.get(factorProvider); + } + + return WordUtils.capitalizeFully(factorProvider); + } +} diff --git a/src/main/java/com/nike/cerberus/server/config/guice/CmsGuiceModule.java b/src/main/java/com/nike/cerberus/server/config/guice/CmsGuiceModule.java index 52c0172c2..c00abac25 100644 --- a/src/main/java/com/nike/cerberus/server/config/guice/CmsGuiceModule.java +++ b/src/main/java/com/nike/cerberus/server/config/guice/CmsGuiceModule.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016 Nike, Inc. + * Copyright (c) 2017 Nike, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -12,11 +12,13 @@ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. + * */ package com.nike.cerberus.server.config.guice; import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.base.Preconditions; import com.google.inject.name.Names; import com.nike.backstopper.apierror.projectspecificinfo.ProjectApiErrors; import com.nike.cerberus.config.CmsEnvPropertiesLoader; @@ -63,6 +65,9 @@ import com.google.inject.AbstractModule; import com.google.inject.Provides; +import com.okta.sdk.clients.AuthApiClient; +import com.okta.sdk.clients.UserApiClient; +import com.okta.sdk.framework.ApiClientConfiguration; import com.typesafe.config.Config; import com.typesafe.config.ConfigValueFactory; @@ -294,4 +299,26 @@ public CmsRequestSecurityValidator authRequestSecurityValidator( public CompletableFuture appInfoFuture(AsyncHttpClientHelper asyncHttpClientHelper) { return AwsUtil.getAppInfoFutureWithAwsInfo(asyncHttpClientHelper); } + + @Singleton + @Provides + public AuthApiClient authApiClient(@Named("auth.connector.okta.api_key") final String oktaApiKey, + @Named("auth.connector.okta.base_url") final String baseUrl) { + + Preconditions.checkArgument(oktaApiKey != null, "okta api key cannot be null"); + Preconditions.checkArgument(baseUrl != null, "okta base url cannot be null"); + + return new AuthApiClient(new ApiClientConfiguration(baseUrl, oktaApiKey)); + } + + @Singleton + @Provides + public UserApiClient userApiClient(@Named("auth.connector.okta.api_key") final String oktaApiKey, + @Named("auth.connector.okta.base_url") final String baseUrl) { + + Preconditions.checkArgument(oktaApiKey != null, "okta api key cannot be null"); + Preconditions.checkArgument(baseUrl != null, "okta base url cannot be null"); + + return new UserApiClient(new ApiClientConfiguration(baseUrl, oktaApiKey)); + } } diff --git a/src/test/java/com/nike/cerberus/auth/connector/okta/OktaAuthConnectorTest.java b/src/test/java/com/nike/cerberus/auth/connector/okta/OktaAuthConnectorTest.java new file mode 100644 index 000000000..0f0a25a14 --- /dev/null +++ b/src/test/java/com/nike/cerberus/auth/connector/okta/OktaAuthConnectorTest.java @@ -0,0 +1,201 @@ +/* + * Copyright (c) 2017 Nike, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.nike.cerberus.auth.connector.okta; + +import com.google.common.collect.Lists; +import com.nike.cerberus.auth.connector.AuthData; +import com.nike.cerberus.auth.connector.AuthResponse; +import com.okta.sdk.models.auth.AuthResult; +import com.okta.sdk.models.factors.Factor; +import com.okta.sdk.models.usergroups.UserGroup; +import com.okta.sdk.models.usergroups.UserGroupProfile; +import com.okta.sdk.models.users.User; +import com.okta.sdk.models.users.UserProfile; +import org.apache.commons.lang3.StringUtils; +import org.junit.Before; +import org.junit.Test; + +import java.util.List; +import java.util.Set; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.Matchers.anyObject; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * Tests the OktaAuthConnector class + */ +public class OktaAuthConnectorTest { + + // class under test + private OktaAuthConnector oktaAuthConnector; + + // dependencies + private OktaAuthHelper oktaAuthHelper; + + @Before + public void setup() { + + // mock dependencies + oktaAuthHelper = mock(OktaAuthHelper.class); + + // create test object + oktaAuthConnector = new OktaAuthConnector(oktaAuthHelper); + } + + ///////////////////////// + // Helper Methods + ///////////////////////// + + private EmbeddedAuthResponseDataV1 mockEmbedded(String email, String id, List factors) { + + UserProfile profile = mock(UserProfile.class); + when(profile.getLogin()).thenReturn(email); + + User user = mock(User.class); + when(user.getProfile()).thenReturn(profile); + when(user.getId()).thenReturn(id); + + EmbeddedAuthResponseDataV1 embedded = mock(EmbeddedAuthResponseDataV1.class); + when(embedded.getUser()).thenReturn(user); + when(embedded.getFactors()).thenReturn(factors); + + return embedded; + } + + private Factor mockFactor(String provider, String id) { + + Factor factor = mock(Factor.class); + when(factor.getId()).thenReturn(id); + when(factor.getProvider()).thenReturn(provider); + when(oktaAuthHelper.getDeviceName(factor)).thenCallRealMethod(); + + return factor; + } + + ///////////////////////// + // Test Methods + ///////////////////////// + + @Test + public void authenticateHappySuccess() throws Exception { + + String username = "username"; + String password = "password"; + + String email = "email"; + String id = "id"; + EmbeddedAuthResponseDataV1 embedded = mockEmbedded(email, id, null); + + AuthResult authResult = mock(AuthResult.class); + when(authResult.getStatus()).thenReturn(OktaAuthHelper.AUTHENTICATION_SUCCESS_STATUS); + + when(oktaAuthHelper.authenticateUser(username, password, null)).thenReturn(authResult); + when(oktaAuthHelper.getEmbeddedAuthData(authResult)).thenReturn(embedded); + + // do the call + AuthResponse result = this.oktaAuthConnector.authenticate(username, password); + + // verify results + assertEquals(result.getData().getUserId(), id); + assertEquals(result.getData().getUsername(), email); + } + + @Test + public void authenticateHappyMfaSuccess() throws Exception { + + String username = "username"; + String password = "password"; + + String email = "email"; + String id = "id"; + String provider = "provider"; + String deviceId = "device id"; + Factor factor = mockFactor(provider, deviceId); + EmbeddedAuthResponseDataV1 embedded = mockEmbedded(email, id, Lists.newArrayList(factor)); + + AuthResult authResult = mock(AuthResult.class); + when(authResult.getStatus()).thenReturn(OktaAuthHelper.AUTHENTICATION_MFA_REQUIRED_STATUS); + when(authResult.getStateToken()).thenReturn("state token"); + + when(oktaAuthHelper.authenticateUser(username, password, null)).thenReturn(authResult); + when(oktaAuthHelper.getEmbeddedAuthData(authResult)).thenReturn(embedded); + + // do the call + AuthResponse result = this.oktaAuthConnector.authenticate(username, password); + + // verify results + assertEquals(id, result.getData().getUserId()); + assertEquals(email, result.getData().getUsername()); + assertEquals(1, result.getData().getDevices().size()); + assertEquals(deviceId, result.getData().getDevices().get(0).getId()); + assertEquals(StringUtils.capitalize(provider), result.getData().getDevices().get(0).getName()); + } + + @Test + public void mfaCheckHappy() { + + String stateToken = "state token"; + String deviceId = "device id"; + String otpToken = "otp token"; + + String email = "email"; + String id = "id"; + EmbeddedAuthResponseDataV1 embedded = mockEmbedded(email, id, null); + + when(oktaAuthHelper.getEmbeddedAuthData(anyObject())).thenReturn(embedded); + + // do the call + AuthResponse result = this.oktaAuthConnector.mfaCheck(stateToken, deviceId, otpToken); + + // verify results + assertEquals(id, result.getData().getUserId()); + assertEquals(email, result.getData().getUsername()); + } + + @Test + public void getGroupsHappy() { + + String id = "id"; + AuthData authData = mock(AuthData.class); + when(authData.getUserId()).thenReturn(id); + + String name1 = "name 1"; + UserGroupProfile profile1 = mock(UserGroupProfile.class); + UserGroup group1 = mock(UserGroup.class); + when(profile1.getName()).thenReturn(name1); + when(group1.getProfile()).thenReturn(profile1); + + String name2 = "name 2"; + UserGroupProfile profile2 = mock(UserGroupProfile.class); + UserGroup group2 = mock(UserGroup.class); + when(profile2.getName()).thenReturn(name2); + when(group2.getProfile()).thenReturn(profile2); + + when(oktaAuthHelper.getUserGroups(id)).thenReturn(Lists.newArrayList(group1, group2)); + + // do the call + Set result = this.oktaAuthConnector.getGroups(authData); + + // verify results + assertTrue(result.contains(name1)); + assertTrue(result.contains(name2)); + } +} \ No newline at end of file diff --git a/src/test/java/com/nike/cerberus/auth/connector/okta/OktaAuthHelperTest.java b/src/test/java/com/nike/cerberus/auth/connector/okta/OktaAuthHelperTest.java new file mode 100644 index 000000000..7dab36eeb --- /dev/null +++ b/src/test/java/com/nike/cerberus/auth/connector/okta/OktaAuthHelperTest.java @@ -0,0 +1,202 @@ +/* + * Copyright (c) 2017 Nike, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.nike.cerberus.auth.connector.okta; + +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import com.nike.backstopper.exception.ApiException; +import com.okta.sdk.clients.AuthApiClient; +import com.okta.sdk.clients.UserApiClient; +import com.okta.sdk.models.auth.AuthResult; +import com.okta.sdk.models.factors.Factor; +import com.okta.sdk.models.usergroups.UserGroup; +import org.apache.commons.lang3.StringUtils; +import org.junit.Before; +import org.junit.Test; + +import java.io.IOException; +import java.util.List; +import java.util.Map; + +import static org.junit.Assert.assertEquals; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Matchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * Tests the OktaAuthHelper class + */ +public class OktaAuthHelperTest { + + // class under test + private OktaAuthHelper oktaAuthHelper; + + // dependencies + private AuthApiClient authApiClient; + private UserApiClient userApiClient; + + @Before + public void setup() { + + // mock dependencies + authApiClient = mock(AuthApiClient.class); + userApiClient = mock(UserApiClient.class); + + // create test object + oktaAuthHelper = new OktaAuthHelper(authApiClient, userApiClient); + } + + ///////////////////////// + // Helper Methods + ///////////////////////// + + + ///////////////////////// + // Test Methods + ///////////////////////// + + @Test + public void getUserGroupsHappy() throws Exception { + + String id = "id"; + UserGroup group = mock(UserGroup.class); + when(userApiClient.getUserGroups(id)).thenReturn(Lists.newArrayList(group)); + + // do the call + List result = this.oktaAuthHelper.getUserGroups(id); + + // verify results + assertTrue(result.contains(group)); + } + + @Test(expected = ApiException.class) + public void getUserGroupsFails() throws Exception { + + when(userApiClient.getUserGroups(anyString())).thenThrow(IOException.class); + + // do the call + this.oktaAuthHelper.getUserGroups("id"); + } + + @Test + public void verifyFactorHappy() throws Exception { + + String factorId = "factor id"; + String stateToken = "state token"; + String passCode = "pass code"; + + AuthResult authResult = mock(AuthResult.class); + when(authApiClient.authenticateWithFactor(stateToken, factorId, passCode)).thenReturn(authResult); + + AuthResult result = this.oktaAuthHelper.verifyFactor(factorId, stateToken, passCode); + + assertEquals(authResult, result); + } + + @Test(expected = ApiException.class) + public void verifyFactorFailsIO() throws Exception { + + when(authApiClient.authenticateWithFactor(anyString(), anyString(), anyString())).thenThrow(IOException.class); + + // do the call + this.oktaAuthHelper.verifyFactor("factor id", "state token", "pass code"); + } + + @Test + public void authenticateUserHappy() throws Exception { + + String username = "username"; + String password = "password"; + String relayState = "relay state"; + + AuthResult authResult = mock(AuthResult.class); + when(this.authApiClient.authenticate(username, password, relayState)).thenReturn(authResult); + + AuthResult result = this.oktaAuthHelper.authenticateUser(username, password, relayState); + + assertEquals(result, authResult); + } + + @Test(expected = ApiException.class) + public void authenticateUserFails() throws Exception { + + when(authApiClient.authenticate(anyString(), anyString(), anyString())).thenThrow(IOException.class); + + // do the call + this.oktaAuthHelper.authenticateUser("username", "password", "relay state"); + } + + @Test + public void getEmbbeddedAuthDataHappy() { + + Map user = Maps.newHashMap(); + List> factors = Lists.newArrayList(Maps.newHashMap()); + Map embedded = Maps.newHashMap(); + embedded.put("user", user); + embedded.put("factors", factors); + + AuthResult authResult = mock(AuthResult.class); + when(authResult.getEmbedded()).thenReturn(embedded); + + EmbeddedAuthResponseDataV1 result = this.oktaAuthHelper.getEmbeddedAuthData(authResult); + + assertNotNull(result.getUser()); + assertNotNull(result.getFactors()); + } + + @Test(expected = IllegalArgumentException.class) + public void getEmbbeddedAuthDataFailsNullResult() { + + this.oktaAuthHelper.getEmbeddedAuthData(null); + + } + + @Test + public void getDeviceName() { + + String provider = "provider"; + Factor factor = mock(Factor.class); + when(factor.getProvider()).thenReturn(provider); + + String result = this.oktaAuthHelper.getDeviceName(factor); + + assertEquals(StringUtils.capitalize(provider), result); + } + + @Test + public void getDeviceNameGoogle() { + + String provider = "GOOGLE"; + Factor factor = mock(Factor.class); + when(factor.getProvider()).thenReturn(provider); + + String result = this.oktaAuthHelper.getDeviceName(factor); + + assertEquals("Google Authenticator", result); + } + + @Test(expected = IllegalArgumentException.class) + public void getDeviceNameFailsNullFactor() { + + this.oktaAuthHelper.getDeviceName(null); + + } +} \ No newline at end of file