Skip to content
This repository has been archived by the owner on Jan 12, 2024. It is now read-only.

Commit

Permalink
Add Okta authentication support (#10)
Browse files Browse the repository at this point in the history
Add Okta authentication support
  • Loading branch information
sdford authored Jan 9, 2017
1 parent efd8847 commit 9f05512
Show file tree
Hide file tree
Showing 8 changed files with 789 additions and 5 deletions.
11 changes: 10 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
7 changes: 4 additions & 3 deletions gradle/dependencies.gradle
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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 {
Expand All @@ -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",
Expand Down Expand Up @@ -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 (
Expand Down
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;
}
}
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;
}

}
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);
}
}
Loading

0 comments on commit 9f05512

Please sign in to comment.