Skip to content

Commit

Permalink
add support for mfa using oidc conformant endpoints
Browse files Browse the repository at this point in the history
  • Loading branch information
lbalmaceda committed Feb 20, 2018
1 parent eefbfba commit 3df6d64
Show file tree
Hide file tree
Showing 8 changed files with 134 additions and 8 deletions.
1 change: 1 addition & 0 deletions auth0/src/main/java/com/auth0/android/Auth0.java
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,7 @@ public boolean isTLS12Enforced() {

/**
* Set whether to enforce TLS 1.2 on devices with API 16-21.
*
* @param enforced whether TLS 1.2 is enforced on devices with API 16-21.
*/
public void setTLS12Enforced(boolean enforced) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,12 @@
import com.auth0.android.request.Request;
import com.auth0.android.request.internal.AuthenticationErrorBuilder;
import com.auth0.android.request.internal.GsonProvider;
import com.auth0.android.request.internal.OkHttpClientFactory;
import com.auth0.android.request.internal.RequestFactory;
import com.auth0.android.result.Credentials;
import com.auth0.android.result.DatabaseUser;
import com.auth0.android.result.Delegation;
import com.auth0.android.result.UserProfile;
import com.auth0.android.request.internal.OkHttpClientFactory;
import com.auth0.android.util.Telemetry;
import com.google.gson.Gson;
import com.squareup.okhttp.HttpUrl;
Expand All @@ -54,6 +54,7 @@
import java.util.Map;

import static com.auth0.android.authentication.ParameterBuilder.GRANT_TYPE_AUTHORIZATION_CODE;
import static com.auth0.android.authentication.ParameterBuilder.GRANT_TYPE_MFA_OTP;
import static com.auth0.android.authentication.ParameterBuilder.GRANT_TYPE_PASSWORD;
import static com.auth0.android.authentication.ParameterBuilder.GRANT_TYPE_PASSWORD_REALM;
import static com.auth0.android.authentication.ParameterBuilder.ID_TOKEN_KEY;
Expand Down Expand Up @@ -81,6 +82,8 @@ public class AuthenticationAPIClient {
private static final String OAUTH_CODE_KEY = "code";
private static final String REDIRECT_URI_KEY = "redirect_uri";
private static final String TOKEN_KEY = "token";
private static final String MFA_TOKEN_KEY = "mfa_token";
private static final String ONE_TIME_PASSWORD_KEY = "otp";
private static final String DELEGATION_PATH = "delegation";
private static final String ACCESS_TOKEN_PATH = "access_token";
private static final String SIGN_UP_PATH = "signup";
Expand All @@ -97,7 +100,8 @@ public class AuthenticationAPIClient {
private static final String HEADER_AUTHORIZATION = "Authorization";

private final Auth0 auth0;
@VisibleForTesting final OkHttpClient client;
@VisibleForTesting
final OkHttpClient client;
private final Gson gson;
private final RequestFactory factory;
private final ErrorBuilder<AuthenticationException> authErrorBuilder;
Expand Down Expand Up @@ -235,6 +239,43 @@ public AuthenticationRequest login(@NonNull String usernameOrEmail, @NonNull Str
return loginWithToken(requestParameters);
}

/**
* Log in a user using the One Time Password code after they have received the 'mfa_required' error.
* The MFA token tells the server the username or email, password and realm values sent on the first request.
* Example usage:
* <pre>
* {@code
* client.loginWithOTP("{mfa token}", "{one time password}")
* .start(new BaseCallback<Credentials>() {
* {@literal}Override
* public void onSuccess(Credentials payload) { }
*
* {@literal}Override
* public void onFailure(AuthenticationException error) { }
* });
* }
* </pre>
*
* @param mfaToken the token received in the previous {@link #login(String, String, String)} response.
* @param otp the one time password code provided by the resource owner, typically obtained from an
* MFA application such as Google Authenticator or Guardian.
* @return a request to configure and start that will yield {@link Credentials}
*/
@SuppressWarnings("WeakerAccess")
public AuthenticationRequest loginWithOTP(@NonNull String mfaToken, @NonNull String otp) {
if (!auth0.isOIDCConformant()) {
throw new IllegalStateException("Clients that are non OIDC conformant can not call this endpoint.");
}

Map<String, Object> parameters = ParameterBuilder.newBuilder()
.setGrantType(GRANT_TYPE_MFA_OTP)
.set(MFA_TOKEN_KEY, mfaToken)
.set(ONE_TIME_PASSWORD_KEY, otp)
.asDictionary();

return loginWithToken(parameters);
}

/**
* Log in a user with a OAuth 'access_token' of a Identity Provider like Facebook or Twitter using <a href="https://auth0.com/docs/api/authentication#social-with-provider-s-access-token">'\oauth\access_token' endpoint</a>
* The default scope used is 'openid'.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -157,17 +157,22 @@ public boolean isInvalidConfiguration() {

/// When MFA code is required to authenticate
public boolean isMultifactorRequired() {
return "a0.mfa_required".equals(code);
return "mfa_required".equals(code) || "a0.mfa_required".equals(code);
}

/// When MFA is required and the user is not enrolled
public boolean isMultifactorEnrollRequired() {
return "a0.mfa_registration_required".equals(code);
return "a0.mfa_registration_required".equals(code) || "unsupported_challenge_type".equals(code);
}

/// When the MFA Token received on the login request has expired
public boolean isMultifactorTokenExpired() {
return "expired_token".equals(code) && "mfa_token is expired".equals(description);
}

/// When MFA code sent is invalid or expired
public boolean isMultifactorCodeInvalid() {
return "a0.mfa_invalid_code".equals(code);
return "a0.mfa_invalid_code".equals(code) || "invalid_grant".equals(code) && "Invalid otp_code.".equals(description);
}

/// When password used for SignUp does not match connection's strength requirements.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ public class ParameterBuilder {
public static final String GRANT_TYPE_PASSWORD_REALM = "http://auth0.com/oauth/grant-type/password-realm";
public static final String GRANT_TYPE_JWT = "urn:ietf:params:oauth:grant-type:jwt-bearer";
public static final String GRANT_TYPE_AUTHORIZATION_CODE = "authorization_code";
public static final String GRANT_TYPE_MFA_OTP = "http://auth0.com/oauth/grant-type/mfa-otp";

public static final String SCOPE_OPENID = "openid";
public static final String SCOPE_OFFLINE_ACCESS = "openid offline_access";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@
* Represents a delegation request for Auth0 tokens that will yield a new delegation token.
* The delegation response depends on the 'api_type' parameter.
*
* @param <T> type of object that will hold the delegation response. When requesting Auth0s 'id_token' you can
* use {@link Delegation}, otherwise youll need to provide an object that can be created from the JSON
* @param <T> type of object that will hold the delegation response. When requesting Auth0's 'id_token' you can
* use {@link Delegation}, otherwise you'll need to provide an object that can be created from the JSON
* payload or just use {@code Map<String, Object>}
*/
public class DelegationRequest<T> implements Request<T, AuthenticationException> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,8 @@ public void onFailure(AuthenticationException error) {

/**
* Checks if a non-expired pair of credentials can be obtained from this manager.
*
* @return whether there are valid credentials stored on this manager.
*/
public boolean hasValidCredentials() {
String accessToken = storage.retrieveString(KEY_ACCESS_TOKEN);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,9 @@

import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.robolectric.RobolectricTestRunner;
Expand All @@ -57,6 +59,7 @@
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.ExecutionException;

import static com.auth0.android.util.AuthenticationAPI.GENERIC_TOKEN;
import static com.auth0.android.util.AuthenticationAPI.ID_TOKEN;
Expand Down Expand Up @@ -100,6 +103,7 @@ public class AuthenticationAPIClientTest {
private Gson gson;

private AuthenticationAPI mockAPI;
private ExpectedException expectedException = ExpectedException.none();

@Before
public void setUp() throws Exception {
Expand Down Expand Up @@ -177,6 +181,43 @@ public void shouldCreateClientWithContextInfo() throws Exception {
assertThat(client.getBaseURL(), equalTo("https://" + DOMAIN + "/"));
}

@Test
public void shouldThrowOnLoginWithMFAOTPCodeWithNonOIDCClient() throws Exception {
expectedException.expect(IllegalStateException.class);
expectedException.expectMessage("Clients that are non OIDC conformant can not call this endpoint.");

Auth0 auth0 = new Auth0(CLIENT_ID, mockAPI.getDomain(), mockAPI.getDomain());
auth0.setOIDCConformant(false);
AuthenticationAPIClient client = new AuthenticationAPIClient(auth0);
client.login(SUPPORT_AUTH0_COM, "some-password", MY_CONNECTION);
}

@Test
public void shouldLoginWithMFAOTPCode() throws Exception {
mockAPI.willReturnSuccessfulLogin();
final MockAuthenticationCallback<Credentials> callback = new MockAuthenticationCallback<>();

Auth0 auth0 = new Auth0(CLIENT_ID, mockAPI.getDomain(), mockAPI.getDomain());
auth0.setOIDCConformant(true);
AuthenticationAPIClient client = new AuthenticationAPIClient(auth0);
client.loginWithOTP("ey30.the-mfa-token.value", "123456")
.start(callback);
assertThat(callback, hasPayloadOfType(Credentials.class));

final RecordedRequest request = mockAPI.takeRequest();
assertThat(request.getHeader("Accept-Language"), is(getDefaultLocale()));
Map<String, String> body = bodyFromRequest(request);

assertThat(request.getPath(), equalTo("/oauth/token"));
assertThat(body, hasEntry("client_id", CLIENT_ID));
assertThat(body, hasEntry("grant_type", "http://auth0.com/oauth/grant-type/mfa-otp"));
assertThat(body, hasEntry("mfa_token", "ey30.the-mfa-token.value"));
assertThat(body, hasEntry("otp", "123456"));
assertThat(body, not(hasKey("username")));
assertThat(body, not(hasKey("password")));
assertThat(body, not(hasKey("connection")));
}

@Test
public void shouldLoginWithUserAndPassword() throws Exception {
mockAPI.willReturnSuccessfulLogin();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;

import org.bouncycastle.jce.provider.JCEMac;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
Expand Down Expand Up @@ -138,20 +139,53 @@ public void shouldReturnNullIfMapDoesNotExist() throws Exception {
assertThat(ex4.getValue("key"), is(nullValue()));
}

@Test
public void shouldHaveExpiredMultifactorTokenOnOIDCMode() throws Exception {
values.put(CODE_KEY, "expired_token");
values.put(DESCRIPTION_KEY, "mfa_token is expired");
AuthenticationException ex = new AuthenticationException(values);
assertThat(ex.isMultifactorTokenExpired(), is(true));
}

@Test
public void shouldRequireMultifactorOnOIDCMode() throws Exception {
values.put(CODE_KEY, "mfa_required");
values.put("mfa_token", "some-random-token");
AuthenticationException ex = new AuthenticationException(values);
assertThat(ex.isMultifactorRequired(), is(true));
assertThat((String) ex.getValue("mfa_token"), is("some-random-token"));
}

@Test
public void shouldRequireMultifactor() throws Exception {
values.put(CODE_KEY, "a0.mfa_required");
AuthenticationException ex = new AuthenticationException(values);
assertThat(ex.isMultifactorRequired(), is(true));
}

@Test
public void shouldRequireMultifactorEnrollOnOIDCMode() throws Exception {
values.put(CODE_KEY, "unsupported_challenge_type");
values.put(DESCRIPTION_KEY, "User is not enrolled with guardian");
AuthenticationException ex = new AuthenticationException(values);
assertThat(ex.isMultifactorEnrollRequired(), is(true));
}

@Test
public void shouldRequireMultifactorEnroll() throws Exception {
values.put(CODE_KEY, "a0.mfa_registration_required");
AuthenticationException ex = new AuthenticationException(values);
assertThat(ex.isMultifactorEnrollRequired(), is(true));
}

@Test
public void shouldHaveInvalidMultifactorCodeOnOIDCMode() throws Exception {
values.put(CODE_KEY, "invalid_grant");
values.put(DESCRIPTION_KEY, "Invalid otp_code.");
AuthenticationException ex = new AuthenticationException(values);
assertThat(ex.isMultifactorCodeInvalid(), is(true));
}

@Test
public void shouldHaveInvalidMultifactorCode() throws Exception {
values.put(CODE_KEY, "a0.mfa_invalid_code");
Expand All @@ -171,7 +205,8 @@ public void shouldHaveNotStrongPassword() throws Exception {
public void shouldHaveNotStrongPasswordWithDetailedDescription() throws Exception {
Gson gson = GsonProvider.buildGson();
FileReader fr = new FileReader(PASSWORD_STRENGTH_ERROR_RESPONSE);
Type mapType = new TypeToken<Map<String, Object>>() {}.getType();
Type mapType = new TypeToken<Map<String, Object>>() {
}.getType();
Map<String, Object> mapPayload = gson.fromJson(fr, mapType);

AuthenticationException ex = new AuthenticationException(mapPayload);
Expand Down

0 comments on commit 3df6d64

Please sign in to comment.