Skip to content
84 changes: 70 additions & 14 deletions src/main/java/com/auth0/client/auth/AuthAPI.java
Original file line number Diff line number Diff line change
Expand Up @@ -1395,6 +1395,27 @@ public Request<CreatedOtpResponse> addOtpAuthenticator(String mfaToken) {
return request;
}


private BaseRequest<CreatedOobResponse> createBaseOobRequest(String mfaToken, List<String> oobChannels) {
String url = baseUrl
.newBuilder()
.addPathSegment("mfa")
.addPathSegment("associate")
.build()
.toString();

BaseRequest<CreatedOobResponse> request = new BaseRequest<>(client, null, url, HttpMethod.POST, new TypeReference<CreatedOobResponse>() {
});

request.addParameter("authenticator_types", Collections.singletonList("oob"));
request.addParameter("oob_channels", oobChannels);
request.addParameter(KEY_CLIENT_ID, clientId);
addClientAuthentication(request, false);
request.addHeader("Authorization", "Bearer " + mfaToken);

return request;
}

/**
* Associates or adds a new OOB authenticator for multi-factor authentication (MFA).
* Confidential clients (Regular Web Apps) <strong>must</strong> have a client secret configured on this {@code AuthAPI} instance.
Expand All @@ -1415,32 +1436,67 @@ public Request<CreatedOtpResponse> addOtpAuthenticator(String mfaToken) {
* @param phoneNumber The phone number for "sms" or "voice" channels. May be null if not using "sms" or "voice".
* @return a Request to execute.
* @see <a href="https://auth0.com/docs/api/authentication#add-an-authenticator">Add an Authenticator API documentation</a>
* @deprecated Use {@linkplain #addOobAuthenticator(String, List, String, String)} instead.
*/
@Deprecated
public Request<CreatedOobResponse> addOobAuthenticator(String mfaToken, List<String> oobChannels, String phoneNumber) {
Asserts.assertNotNull(mfaToken, "mfa token");
Asserts.assertNotNull(oobChannels, "OOB channels");
if (oobChannels.contains("sms") || oobChannels.contains("voice")) {
Asserts.assertNotNull(phoneNumber, "phone number");
}

String url = baseUrl
.newBuilder()
.addPathSegment("mfa")
.addPathSegment("associate")
.build()
.toString();

BaseRequest<CreatedOobResponse> request = new BaseRequest<>(client, null, url, HttpMethod.POST, new TypeReference<CreatedOobResponse>() {
});
BaseRequest<CreatedOobResponse> request = createBaseOobRequest(mfaToken, oobChannels);

request.addParameter("authenticator_types", Collections.singletonList("oob"));
request.addParameter("oob_channels", oobChannels);
request.addParameter(KEY_CLIENT_ID, clientId);
if (phoneNumber != null) {
request.addParameter("phone_number", phoneNumber);
}
addClientAuthentication(request, false);
request.addHeader("Authorization", "Bearer " + mfaToken);

return request;
}

/**
* Associates or adds a new OOB authenticator for multi-factor authentication (MFA).
* Confidential clients (Regular Web Apps) <strong>must</strong> have a client secret configured on this {@code AuthAPI} instance.
* <pre>
* {@code
* try {
* CreatedOobResponse result = authAPI.addOobAuthenticator("the-mfa-token", Arrays.asList("sms", "email"), "phone-number", "email-address")
* .execute()
* .getBody();
* } catch (Auth0Exception e) {
* //Something happened
* }
* }
* </pre>
*
* @param mfaToken The token received from mfa_required error. Must not be null.
* @param oobChannels The type of OOB channels supported by the client. Must not be null.
* @param phoneNumber The phone number for "sms" or "voice" channels. May be null if not using "sms" or "voice".
* @param emailAddress The email address for "email" channel. May be null if not using "email".
* @return a Request to execute.
* @see <a href="https://auth0.com/docs/secure/multi-factor-authentication/authenticate-using-ropg-flow-with-mfa/enroll-challenge-sms-voice-authenticators#enroll-with-sms-or-voice">Enroll with SMS or voice</a>
*/
public Request<CreatedOobResponse> addOobAuthenticator(String mfaToken, List<String> oobChannels, String phoneNumber, String emailAddress) {
Asserts.assertNotNull(mfaToken, "mfa token");
Asserts.assertNotNull(oobChannels, "OOB channels");
if (oobChannels.contains("sms") || oobChannels.contains("voice")) {
Asserts.assertNotNull(phoneNumber, "phone number");
}
if (oobChannels.contains("email")) {
Asserts.assertNotNull(emailAddress, "email address");
}

BaseRequest<CreatedOobResponse> request = createBaseOobRequest(mfaToken, oobChannels);
if (phoneNumber != null) {
request.addParameter("phone_number", phoneNumber);
}
if (emailAddress != null) {
request.addParameter("email", emailAddress);
}

return request;
}

/**
* Returns a list of authenticators associated with your application.
Expand Down
220 changes: 216 additions & 4 deletions src/test/java/com/auth0/client/auth/AuthAPITest.java
Original file line number Diff line number Diff line change
Expand Up @@ -1541,22 +1541,41 @@ public void addOtpAuthenticatorRequest() throws Exception {
assertThat(response.getRecoveryCodes(), notNullValue());
}

@SuppressWarnings("deprecation")
@Test
public void addOobAuthenticatorThrowsWhenTokenNull() {
public void addOobAuthenticatorDeprecatedThrowsWhenTokenNull() {
verifyThrows(IllegalArgumentException.class,
() -> api.addOobAuthenticator(null, Collections.singletonList("otp"), null),
() -> api.addOobAuthenticator(null, Collections.singletonList("auth0"), null),
"'mfa token' cannot be null!");
}

@SuppressWarnings("deprecation")
@Test
public void addOobAuthenticatorThrowsWhenChannelsNull() {
public void addOobAuthenticatorDeprecatedThrowsWhenChannelsNull() {
verifyThrows(IllegalArgumentException.class,
() -> api.addOobAuthenticator("mfaToken", null, null),
"'OOB channels' cannot be null!");
}

@SuppressWarnings("deprecation")
@Test
public void addOobAuthenticatorDeprecatedThrowsWhenPhoneNumberNull() {
verifyThrows(IllegalArgumentException.class,
() -> api.addOobAuthenticator("mfaToken", Collections.singletonList("sms"), null),
"'phone number' cannot be null!");
}

@SuppressWarnings("deprecation")
@Test
public void addOobAuthenticatorRequest() throws Exception {
public void addOobAuthenticatorDeprecatedThrowsWhenPhoneNumberNullVoiceChannel() {
verifyThrows(IllegalArgumentException.class,
() -> api.addOobAuthenticator("mfaToken", Collections.singletonList("voice"), null),
"'phone number' cannot be null!");
}

@SuppressWarnings("deprecation")
@Test
public void addOobAuthenticatorDeprecatedRequest() throws Exception {
Request<CreatedOobResponse> request = api.addOobAuthenticator("mfaToken", Collections.singletonList("sms"), "phone-number");

server.jsonResponse(AUTH_OOB_AUTHENTICATOR_RESPONSE, 200);
Expand All @@ -1579,6 +1598,199 @@ public void addOobAuthenticatorRequest() throws Exception {
assertThat(response.getRecoveryCodes(), notNullValue());
}

@SuppressWarnings("deprecation")
@Test
public void addOobAuthenticatorDeprecatedRequestWithNoPhoneNumber() throws Exception {
Request<CreatedOobResponse> request = api.addOobAuthenticator("mfaToken", Collections.singletonList("auth0"), null);

server.jsonResponse(AUTH_OOB_AUTHENTICATOR_RESPONSE, 200);
CreatedOobResponse response = request.execute().getBody();
RecordedRequest recordedRequest = server.takeRequest();

assertThat(recordedRequest, hasMethodAndPath(HttpMethod.POST, "/mfa/associate"));
assertThat(recordedRequest, hasHeader("Content-Type", "application/json"));

Map<String, Object> body = bodyFromRequest(recordedRequest);
assertThat(body, hasEntry("authenticator_types", Collections.singletonList("oob")));
assertThat(body, hasEntry("oob_channels", Collections.singletonList("auth0")));

assertThat(response, is(notNullValue()));
assertThat(response.getAuthenticatorType(), not(emptyOrNullString()));
assertThat(response.getOobChannel(), not(emptyOrNullString()));
assertThat(response.getOobCode(), not(emptyOrNullString()));
assertThat(response.getBarcodeUri(), not(emptyOrNullString()));
assertThat(response.getRecoveryCodes(), notNullValue());
}

@Test
public void addOobAuthenticatorThrowsWhenTokenNull() {
verifyThrows(IllegalArgumentException.class,
() -> api.addOobAuthenticator(null, Collections.singletonList("auth0"), null, null),
"'mfa token' cannot be null!");
}


@Test
public void addOobAuthenticatorThrowsWhenChannelsNull() {
verifyThrows(IllegalArgumentException.class,
() -> api.addOobAuthenticator("mfaToken", null, null, null),
"'OOB channels' cannot be null!");
}

@Test
public void addOobAuthenticatorThrowsWhenChannelsNullWithPhoneNumber() {
verifyThrows(IllegalArgumentException.class,
() -> api.addOobAuthenticator("mfaToken", null, "phone-number", null),
"'OOB channels' cannot be null!");
}

@Test
public void addOobAuthenticatorThrowsWhenChannelsNullWithEmail() {
verifyThrows(IllegalArgumentException.class,
() -> api.addOobAuthenticator("mfaToken", null, null, "email-address"),
"'OOB channels' cannot be null!");
}

@Test
public void addOobAuthenticatorThrowsWhenPhoneNumberNull() {
verifyThrows(IllegalArgumentException.class,
() -> api.addOobAuthenticator("mfaToken", Collections.singletonList("sms"), null, null),
"'phone number' cannot be null!");
}

@Test
public void addOobAuthenticatorThrowsWhenPhoneNumberNullWithVoiceChannel() {
verifyThrows(IllegalArgumentException.class,
() -> api.addOobAuthenticator("mfaToken", Collections.singletonList("voice"), null, null),
"'phone number' cannot be null!");
}

@Test
public void addOobAuthenticatorThrowsWhenEmailNull() {
verifyThrows(IllegalArgumentException.class,
() -> api.addOobAuthenticator("mfaToken", Collections.singletonList("email"), null, null),
"'email address' cannot be null!");
}

@Test
public void addOobAuthenticatorRequestWithPhoneNumber() throws Exception {
Request<CreatedOobResponse> request = api.addOobAuthenticator("mfaToken", Collections.singletonList("sms"), "phone-number", null);

server.jsonResponse(AUTH_OOB_AUTHENTICATOR_RESPONSE, 200);
CreatedOobResponse response = request.execute().getBody();
RecordedRequest recordedRequest = server.takeRequest();

assertThat(recordedRequest, hasMethodAndPath(HttpMethod.POST, "/mfa/associate"));
assertThat(recordedRequest, hasHeader("Content-Type", "application/json"));

Map<String, Object> body = bodyFromRequest(recordedRequest);
assertThat(body, hasEntry("authenticator_types", Collections.singletonList("oob")));
assertThat(body, hasEntry("oob_channels", Collections.singletonList("sms")));
assertThat(body, hasEntry("phone_number", "phone-number"));

assertThat(response, is(notNullValue()));
assertThat(response.getAuthenticatorType(), not(emptyOrNullString()));
assertThat(response.getOobChannel(), not(emptyOrNullString()));
assertThat(response.getOobCode(), not(emptyOrNullString()));
assertThat(response.getBarcodeUri(), not(emptyOrNullString()));
assertThat(response.getRecoveryCodes(), notNullValue());
}

@Test
public void addOobAuthenticatorRequestWithEmail() throws Exception {
Request<CreatedOobResponse> request = api.addOobAuthenticator("mfaToken", Collections.singletonList("email"), null, "email-address");

server.jsonResponse(AUTH_OOB_AUTHENTICATOR_RESPONSE, 200);
CreatedOobResponse response = request.execute().getBody();
RecordedRequest recordedRequest = server.takeRequest();

assertThat(recordedRequest, hasMethodAndPath(HttpMethod.POST, "/mfa/associate"));
assertThat(recordedRequest, hasHeader("Content-Type", "application/json"));

Map<String, Object> body = bodyFromRequest(recordedRequest);
assertThat(body, hasEntry("authenticator_types", Collections.singletonList("oob")));
assertThat(body, hasEntry("oob_channels", Collections.singletonList("email")));
assertThat(body, hasEntry("email", "email-address"));

assertThat(response, is(notNullValue()));
assertThat(response.getAuthenticatorType(), not(emptyOrNullString()));
assertThat(response.getOobChannel(), not(emptyOrNullString()));
assertThat(response.getOobCode(), not(emptyOrNullString()));
assertThat(response.getBarcodeUri(), not(emptyOrNullString()));
assertThat(response.getRecoveryCodes(), notNullValue());
}

@Test
public void addOobAuthenticatorRequestWithNoContactInfo() throws Exception {
Request<CreatedOobResponse> request = api.addOobAuthenticator("mfaToken", Collections.singletonList("auth0"), null, null);

server.jsonResponse(AUTH_OOB_AUTHENTICATOR_RESPONSE, 200);
CreatedOobResponse response = request.execute().getBody();
RecordedRequest recordedRequest = server.takeRequest();

assertThat(recordedRequest, hasMethodAndPath(HttpMethod.POST, "/mfa/associate"));
assertThat(recordedRequest, hasHeader("Content-Type", "application/json"));

Map<String, Object> body = bodyFromRequest(recordedRequest);
assertThat(body, hasEntry("authenticator_types", Collections.singletonList("oob")));
assertThat(body, hasEntry("oob_channels", Collections.singletonList("auth0")));

assertThat(response, is(notNullValue()));
assertThat(response.getAuthenticatorType(), not(emptyOrNullString()));
assertThat(response.getOobChannel(), not(emptyOrNullString()));
assertThat(response.getOobCode(), not(emptyOrNullString()));
assertThat(response.getBarcodeUri(), not(emptyOrNullString()));
assertThat(response.getRecoveryCodes(), notNullValue());
}

@Test
public void addOobAuthenticatorRequestWithMultipleChannels() throws Exception {
Request<CreatedOobResponse> request = api.addOobAuthenticator("mfaToken", Arrays.asList("sms", "email", "auth0"), "phone-number", "email-address");

server.jsonResponse(AUTH_OOB_AUTHENTICATOR_RESPONSE, 200);
CreatedOobResponse response = request.execute().getBody();
RecordedRequest recordedRequest = server.takeRequest();

assertThat(recordedRequest, hasMethodAndPath(HttpMethod.POST, "/mfa/associate"));
assertThat(recordedRequest, hasHeader("Content-Type", "application/json"));

Map<String, Object> body = bodyFromRequest(recordedRequest);
assertThat(body, hasEntry("authenticator_types", Collections.singletonList("oob")));
assertThat(body, hasEntry("oob_channels", Arrays.asList("sms", "email", "auth0")));
assertThat(body, hasEntry("phone_number", "phone-number"));
assertThat(body, hasEntry("email", "email-address"));

assertThat(response, is(notNullValue()));
assertThat(response.getAuthenticatorType(), not(emptyOrNullString()));
assertThat(response.getOobChannel(), not(emptyOrNullString()));
assertThat(response.getOobCode(), not(emptyOrNullString()));
assertThat(response.getBarcodeUri(), not(emptyOrNullString()));
assertThat(response.getRecoveryCodes(), notNullValue());
}

@Test
public void addOobAuthenticatorRequestWithAuth0Channel() throws Exception {
Request<CreatedOobResponse> request = api.addOobAuthenticator("mfaToken", Collections.singletonList("auth0"), null, "email-address");

server.jsonResponse(AUTH_OOB_AUTHENTICATOR_RESPONSE, 200);
CreatedOobResponse response = request.execute().getBody();
RecordedRequest recordedRequest = server.takeRequest();

assertThat(recordedRequest, hasMethodAndPath(HttpMethod.POST, "/mfa/associate"));
assertThat(recordedRequest, hasHeader("Content-Type", "application/json"));

Map<String, Object> body = bodyFromRequest(recordedRequest);
assertThat(body, hasEntry("authenticator_types", Collections.singletonList("oob")));
assertThat(body, hasEntry("oob_channels", Collections.singletonList("auth0")));

assertThat(response, is(notNullValue()));
assertThat(response.getAuthenticatorType(), not(emptyOrNullString()));
assertThat(response.getOobChannel(), not(emptyOrNullString()));
assertThat(response.getOobCode(), not(emptyOrNullString()));
assertThat(response.getBarcodeUri(), not(emptyOrNullString()));
assertThat(response.getRecoveryCodes(), notNullValue());
}

@Test
public void listAuthenticatorsThrowsWhenTokenNull() {
verifyThrows(IllegalArgumentException.class,
Expand Down