This repository has been archived by the owner on Jan 12, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 28
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add Okta authentication support (#10)
Add Okta authentication support
- Loading branch information
Showing
8 changed files
with
789 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
49 changes: 49 additions & 0 deletions
49
src/main/java/com/nike/cerberus/auth/connector/okta/EmbeddedAuthResponseDataV1.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<Factor> factors; | ||
|
||
public User getUser() { | ||
return user; | ||
} | ||
|
||
public void setUser(User user) { | ||
this.user = user; | ||
} | ||
|
||
public List<Factor> getFactors() { | ||
return factors; | ||
} | ||
|
||
public void setFactors(List<Factor> factors) { | ||
this.factors = factors; | ||
} | ||
} |
108 changes: 108 additions & 0 deletions
108
src/main/java/com/nike/cerberus/auth/connector/okta/OktaAuthConnector.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<String> getGroups(AuthData authData) { | ||
|
||
Preconditions.checkNotNull(authData, "auth data cannot be null."); | ||
|
||
final List<UserGroup> userGroups = oktaAuthHelper.getUserGroups(authData.getUserId()); | ||
|
||
final Set<String> groups = new HashSet<>(); | ||
if (userGroups == null) { | ||
return groups; | ||
} | ||
|
||
userGroups.forEach(group -> groups.add(group.getProfile().getName())); | ||
|
||
return groups; | ||
} | ||
|
||
} |
187 changes: 187 additions & 0 deletions
187
src/main/java/com/nike/cerberus/auth/connector/okta/OktaAuthHelper.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<String, String> 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<UserGroup> 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<String, Object> 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); | ||
} | ||
} |
Oops, something went wrong.