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

Commit

Permalink
Feat/enable okta push notification (#273)
Browse files Browse the repository at this point in the history
* feat(websecurity): Enables push notifications for the Okta Verify factor.
  • Loading branch information
tunderwood authored Oct 5, 2020
1 parent 7005409 commit 2dd96b1
Show file tree
Hide file tree
Showing 26 changed files with 2,804 additions and 532 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@ dependencies {

// Okta
implementation "com.okta:okta-sdk:0.0.4"
implementation "com.okta.authn.sdk:okta-authn-sdk-api:0.1.0"
implementation "com.okta.sdk:okta-sdk-httpclient:1.2.0"
implementation "com.okta.authn.sdk:okta-authn-sdk-impl:0.1.0"
implementation "com.okta.authn.sdk:okta-authn-sdk-api:2.0.0"
implementation "com.okta.sdk:okta-sdk-httpclient:2.0.0"
implementation "com.okta.authn.sdk:okta-authn-sdk-impl:2.0.0"

// The Okta SDKs pull in an outdated version of guava that the OWASP Dep checker doesn't like
implementation group: 'com.google.guava', name: 'guava', version: "${versions.guava}"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,16 @@

package com.nike.cerberus.auth.connector.okta;

import static java.lang.Thread.sleep;

import com.google.common.base.Preconditions;
import com.nike.backstopper.exception.ApiException;
import com.nike.cerberus.auth.connector.AuthConnector;
import com.nike.cerberus.auth.connector.AuthData;
import com.nike.cerberus.auth.connector.AuthResponse;
import com.nike.cerberus.auth.connector.okta.statehandlers.InitialLoginStateHandler;
import com.nike.cerberus.auth.connector.okta.statehandlers.MfaStateHandler;
import com.nike.cerberus.auth.connector.okta.statehandlers.PushStateHandler;
import com.nike.cerberus.error.DefaultApiError;
import com.okta.authn.sdk.FactorValidationException;
import com.okta.authn.sdk.client.AuthenticationClient;
Expand Down Expand Up @@ -96,6 +99,52 @@ public AuthResponse triggerChallenge(String stateToken, String deviceId) {
}
}

/** Triggers challenge for SMS or Call factors using Okta Auth SDK. */
public AuthResponse triggerPush(String stateToken, String deviceId) {

CompletableFuture<AuthResponse> authResponseFuture = new CompletableFuture<>();
PushStateHandler stateHandler =
new PushStateHandler(oktaAuthenticationClient, authResponseFuture);

try {
oktaAuthenticationClient.verifyFactor(deviceId, stateToken, stateHandler);

AuthResponse authResponse = authResponseFuture.get(45, TimeUnit.SECONDS);
long startTime = System.currentTimeMillis();
while (authResponse.getData().getFactorResult().equals("WAITING")
&& System.currentTimeMillis() - startTime <= 55000) {
sleep(100);
authResponseFuture = new CompletableFuture<>();
stateHandler = new PushStateHandler(oktaAuthenticationClient, authResponseFuture);
oktaAuthenticationClient.verifyFactor(deviceId, stateToken, stateHandler);
authResponse = authResponseFuture.get(45, TimeUnit.SECONDS);
}
String factorResult = authResponse.getData().getFactorResult();
if (!factorResult.equals("SUCCESS")) {
if (factorResult.equals("TIMEOUT") || factorResult.equals("WAITING")) {
throw ApiException.newBuilder()
.withApiErrors(DefaultApiError.OKTA_PUSH_MFA_TIMEOUT)
.withExceptionMessage(DefaultApiError.OKTA_PUSH_MFA_TIMEOUT.getMessage())
.build();
} else if (factorResult.equals("REJECTED")) {
throw ApiException.newBuilder()
.withApiErrors(DefaultApiError.OKTA_PUSH_MFA_REJECTED)
.withExceptionMessage(DefaultApiError.OKTA_PUSH_MFA_REJECTED.getMessage())
.build();
}
}
return authResponseFuture.get(45, TimeUnit.SECONDS);
} catch (ApiException e) {
throw e;
} catch (Exception e) {
throw ApiException.newBuilder()
.withExceptionCause(e)
.withApiErrors(DefaultApiError.AUTH_RESPONSE_WAIT_FAILED)
.withExceptionMessage("Failed to trigger challenge due to timeout. Please try again.")
.build();
}
}

/** Verifies user's MFA factor using Okta Auth SDK. */
@Override
public AuthResponse mfaCheck(String stateToken, String deviceId, String otpToken) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@

import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.nike.backstopper.exception.ApiException;
import com.nike.cerberus.auth.connector.AuthData;
import com.nike.cerberus.auth.connector.AuthResponse;
Expand All @@ -29,13 +28,13 @@
import com.okta.authn.sdk.client.AuthenticationClient;
import com.okta.authn.sdk.resource.AuthenticationResponse;
import com.okta.authn.sdk.resource.Factor;
import com.okta.sdk.resource.user.factor.FactorProvider;
import com.okta.sdk.resource.user.factor.FactorType;
import com.okta.authn.sdk.resource.FactorProvider;
import com.okta.authn.sdk.resource.FactorType;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.text.WordUtils;
import org.apache.commons.text.WordUtils;

/**
* Abstract state handler to provide helper methods for authentication and MFA validation. Also
Expand All @@ -57,7 +56,7 @@ public abstract class AbstractOktaStateHandler extends AuthenticationStateHandle
ImmutableMap.of(
"google-token:software:totp", false,
"okta-token:software:totp", false,
"okta-push", true,
"okta-push", false,
"okta-call", true,
"okta-sms", true);

Expand All @@ -75,9 +74,6 @@ public abstract class AbstractOktaStateHandler extends AuthenticationStateHandle
.put("MFA_ENROLL_ACTIVATE", "Please activate your factor to complete enrollment.")
.build();

// We currently do not support push notifications for Okta MFA verification.
private static final ImmutableSet UNSUPPORTED_OKTA_MFA_TYPES = ImmutableSet.of(FactorType.PUSH);

public final AuthenticationClient client;
public final CompletableFuture<AuthResponse> authenticationResponseFuture;

Expand Down Expand Up @@ -138,17 +134,17 @@ public boolean isTriggerRequired(Factor factor) {
}

/**
* Determines if a MFA factor is currently supported by Cerberus or not
* Determines whether a trigger is required for a provided MFA factor
*
* @param factor Okta MFA factor
* @return boolean
* @return boolean trigger required
*/
public boolean isSupportedFactor(Factor factor) {
public boolean isPush(Factor factor) {

final FactorType type = factor.getType();
final FactorProvider provider = factor.getProvider();

return !(provider.equals(FactorProvider.OKTA) && UNSUPPORTED_OKTA_MFA_TYPES.contains(type));
return (provider.equals(FactorProvider.OKTA) && type == FactorType.PUSH);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@
import com.okta.authn.sdk.client.AuthenticationClient;
import com.okta.authn.sdk.resource.AuthenticationResponse;
import com.okta.authn.sdk.resource.Factor;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;

/** Initial state handler to handle relevant states during authentication. */
public class InitialLoginStateHandler extends AbstractOktaStateHandler {
Expand Down Expand Up @@ -70,10 +70,7 @@ private void handleMfaResponse(AuthenticationResponse mfaResponse) {
authData.setStateToken(mfaResponse.getStateToken());
authResponse.setStatus(AuthStatus.MFA_REQUIRED);

final List<Factor> factors =
mfaResponse.getFactors().stream()
.filter(this::isSupportedFactor)
.collect(Collectors.toList());
final List<Factor> factors = new ArrayList<>(mfaResponse.getFactors());

validateUserFactors(factors);

Expand All @@ -85,7 +82,8 @@ private void handleMfaResponse(AuthenticationResponse mfaResponse) {
new AuthMfaDevice()
.setId(factor.getId())
.setName(getDeviceName(factor))
.setRequiresTrigger(isTriggerRequired(factor))));
.setRequiresTrigger(isTriggerRequired(factor))
.setIsPush(isPush(factor))));

authenticationResponseFuture.complete(authResponse);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package com.nike.cerberus.auth.connector.okta.statehandlers;

import com.nike.cerberus.auth.connector.AuthData;
import com.nike.cerberus.auth.connector.AuthResponse;
import com.nike.cerberus.auth.connector.AuthStatus;
import com.okta.authn.sdk.client.AuthenticationClient;
import com.okta.authn.sdk.resource.AuthenticationResponse;
import java.util.concurrent.CompletableFuture;

public class PushStateHandler extends AbstractOktaStateHandler {
public PushStateHandler(
AuthenticationClient client, CompletableFuture<AuthResponse> authenticationResponseFuture) {
super(client, authenticationResponseFuture);
}

/**
* Handles MFA Challenge, when a MFA challenge has been initiated for call or sms.
*
* @param mfaChallengeResponse - Authentication response from the Completable Future
*/
@Override
public void handleMfaChallenge(AuthenticationResponse mfaChallengeResponse) {

final String userId = mfaChallengeResponse.getUser().getId();
final String userLogin = mfaChallengeResponse.getUser().getLogin();
final String factorResult = mfaChallengeResponse.getFactorResult();

final AuthData authData =
new AuthData().setUserId(userId).setUsername(userLogin).setFactorResult(factorResult);
AuthResponse authResponse =
new AuthResponse().setData(authData).setStatus(AuthStatus.MFA_CHALLENGE);

authenticationResponseFuture.complete(authResponse);
}

/**
* Handles MFA Challenge, when a MFA challenge has been initiated for call or sms.
*
* @param mfaChallengeResponse - Authentication response from the Completable Future
*/
@Override
public void handleSuccess(AuthenticationResponse mfaChallengeResponse) {

final String userId = mfaChallengeResponse.getUser().getId();
final String userLogin = mfaChallengeResponse.getUser().getLogin();
final String factorResult = mfaChallengeResponse.getStatus().toString();

final AuthData authData =
new AuthData().setUserId(userId).setUsername(userLogin).setFactorResult(factorResult);
AuthResponse authResponse = new AuthResponse().setData(authData).setStatus(AuthStatus.SUCCESS);

authenticationResponseFuture.complete(authResponse);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,11 @@
import com.okta.authn.sdk.client.AuthenticationClient;
import com.okta.authn.sdk.impl.resource.DefaultFactor;
import com.okta.authn.sdk.resource.AuthenticationResponse;
import com.okta.authn.sdk.resource.FactorProvider;
import com.okta.authn.sdk.resource.FactorType;
import com.okta.authn.sdk.resource.User;
import com.okta.sdk.resource.user.factor.FactorProvider;
import com.okta.sdk.resource.user.factor.FactorType;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import junit.framework.TestCase;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
Expand Down Expand Up @@ -160,32 +159,6 @@ public void getDeviceNameFailsNullFactor() {
this.abstractOktaStateHandler.getDeviceName(null);
}

@Test
public void isSupportedFactorFalse() {

DefaultFactor factor = mock(DefaultFactor.class);
when(factor.getType()).thenReturn(FactorType.PUSH);
when(factor.getProvider()).thenReturn(FactorProvider.OKTA);

boolean expected = false;
boolean actual = abstractOktaStateHandler.isSupportedFactor(factor);

TestCase.assertEquals(expected, actual);
}

@Test
public void isSupportedFactorTrue() {

DefaultFactor factor = mock(DefaultFactor.class);
when(factor.getType()).thenReturn(FactorType.TOKEN_SOFTWARE_TOTP);
when(factor.getProvider()).thenReturn(FactorProvider.OKTA);

boolean expected = true;
boolean actual = abstractOktaStateHandler.isSupportedFactor(factor);

TestCase.assertEquals(expected, actual);
}

@Test
public void validateUserFactorsSuccess() {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,9 @@
import com.okta.authn.sdk.client.AuthenticationClient;
import com.okta.authn.sdk.impl.resource.DefaultFactor;
import com.okta.authn.sdk.resource.AuthenticationResponse;
import com.okta.authn.sdk.resource.FactorProvider;
import com.okta.authn.sdk.resource.FactorType;
import com.okta.authn.sdk.resource.User;
import com.okta.sdk.resource.user.factor.FactorProvider;
import com.okta.sdk.resource.user.factor.FactorType;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import org.junit.Before;
Expand Down Expand Up @@ -100,44 +100,6 @@ public void handleMfaRequired() throws Exception {
assertEquals(expectedStatus, actualResponse.getStatus());
}

@Test(expected = ApiException.class)
public void handleMfaRequiredFailNoSupportedDevicesEnrolled() throws Exception {

String email = "email";
String id = "id";
AuthStatus expectedStatus = AuthStatus.MFA_REQUIRED;

FactorProvider provider = FactorProvider.OKTA;
FactorType type = FactorType.PUSH;
String deviceId = "device id";
String status = "status";

AuthenticationResponse expectedResponse = mock(AuthenticationResponse.class);

User user = mock(User.class);
when(user.getId()).thenReturn(id);
when(user.getLogin()).thenReturn(email);
when(expectedResponse.getUser()).thenReturn(user);

DefaultFactor factor = mock(DefaultFactor.class);

when(factor.getType()).thenReturn(type);
when(factor.getProvider()).thenReturn(provider);
when(factor.getStatus()).thenReturn(status);
when(factor.getId()).thenReturn(deviceId);
when(expectedResponse.getFactors()).thenReturn(Lists.newArrayList(factor));

// do the call
initialLoginStateHandler.handleMfaRequired(expectedResponse);

AuthResponse actualResponse = authenticationResponseFuture.get(1, TimeUnit.SECONDS);

// verify results
assertEquals(id, actualResponse.getData().getUserId());
assertEquals(email, actualResponse.getData().getUsername());
assertEquals(expectedStatus, actualResponse.getStatus());
}

@Test
public void handleMfaEnroll() throws Exception {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ public void mfaCheckSuccess() throws Exception {
return null;
})
.when(client)
.verifyFactor(any(), any(), any());
.verifyFactor(anyString(), isA(DefaultVerifyPassCodeFactorRequest.class), any());

// do the call
AuthResponse actualResponse = this.oktaAuthConnector.mfaCheck(stateToken, deviceId, otpToken);
Expand Down Expand Up @@ -238,7 +238,7 @@ public void mfaCheckFails() throws Exception {
return null;
})
.when(client)
.verifyFactor(any(), any(), any());
.verifyFactor(any(), isA(DefaultVerifyPassCodeFactorRequest.class), any());

// do the call
AuthResponse actualResponse = this.oktaAuthConnector.mfaCheck(stateToken, deviceId, otpToken);
Expand Down
Loading

0 comments on commit 2dd96b1

Please sign in to comment.