diff --git a/auth0/src/main/java/com/auth0/android/provider/WebAuthProvider.java b/auth0/src/main/java/com/auth0/android/provider/WebAuthProvider.java index 374c15a09..0be036581 100644 --- a/auth0/src/main/java/com/auth0/android/provider/WebAuthProvider.java +++ b/auth0/src/main/java/com/auth0/android/provider/WebAuthProvider.java @@ -407,11 +407,11 @@ private boolean authorize(@NonNull AuthorizeResult data) { ex = new AuthenticationException("a0.invalid_configuration", "The application isn't configured properly for the social connection. Please check your Auth0's application configuration"); } callback.onFailure(ex); - } else if (values.containsKey(KEY_STATE) && !values.get(KEY_STATE).equals(getState())) { - Log.e(TAG, String.format("Received state doesn't match. Received %s but expected %s", values.get(KEY_STATE), getState())); + } else if (values.containsKey(KEY_STATE) && !values.get(KEY_STATE).equals(parameters.get(KEY_STATE))) { + Log.e(TAG, String.format("Received state doesn't match. Received %s but expected %s", values.get(KEY_STATE), parameters.get(KEY_STATE))); final AuthenticationException ex = new AuthenticationException("access_denied", "The received state is invalid. Try again."); callback.onFailure(ex); - } else if (getResponseType().contains(RESPONSE_TYPE_ID_TOKEN) && !hasValidNonce(getNonce(), values.get(KEY_ID_TOKEN))) { + } else if (getResponseType().contains(RESPONSE_TYPE_ID_TOKEN) && !hasValidNonce(parameters.get(KEY_NONCE), values.get(KEY_ID_TOKEN))) { Log.e(TAG, "Received nonce doesn't match."); final AuthenticationException ex = new AuthenticationException("access_denied", "The received nonce is invalid. Try again."); callback.onFailure(ex); @@ -487,11 +487,11 @@ static Credentials mergeCredentials(Credentials urlCredentials, Credentials code } @VisibleForTesting - static boolean hasValidNonce(String nonce, String token) { + static boolean hasValidNonce(@Nullable String nonce, @NonNull String token) { try { final JWT idToken = new JWT(token); final Claim nonceClaim = idToken.getClaim(KEY_NONCE); - return nonceClaim != null && nonce.equals(nonceClaim.asString()); + return nonce != null && nonceClaim != null && nonce.equals(nonceClaim.asString()); } catch (DecodeException e) { return false; } @@ -520,12 +520,17 @@ Uri buildAuthorizeUri() { } if (getResponseType().contains(RESPONSE_TYPE_ID_TOKEN)) { - queryParameters.put(KEY_NONCE, getNonce()); + final String nonce = getRandomString(parameters.get(KEY_NONCE)); + parameters.put(KEY_NONCE, nonce); + queryParameters.put(KEY_NONCE, nonce); } if (account.getTelemetry() != null) { queryParameters.put(KEY_TELEMETRY, account.getTelemetry().getValue()); } - queryParameters.put(KEY_STATE, getState()); + + final String state = getRandomString(parameters.get(KEY_STATE)); + parameters.put(KEY_STATE, state); + queryParameters.put(KEY_STATE, state); queryParameters.put(KEY_CLIENT_ID, account.getClientId()); queryParameters.put(KEY_REDIRECT_URI, redirectUri); @@ -534,7 +539,7 @@ Uri buildAuthorizeUri() { builder.appendQueryParameter(entry.getKey(), entry.getValue()); } Uri uri = builder.build(); - logDebug("The parsed CallbackURI contains the following values: " + "Using the following AuthorizeURI: " + uri.toString()); + logDebug("Using the following AuthorizeURI: " + uri.toString()); return uri; } @@ -553,23 +558,21 @@ static WebAuthProvider getInstance() { return providerInstance; } - - private String getState() { - return parameters.containsKey(KEY_STATE) ? parameters.get(KEY_STATE) : secureRandomString(); + @VisibleForTesting + String getRandomString(@Nullable String defaultValue) { + return defaultValue != null ? defaultValue : secureRandomString(); } - String getScope() { - return this.parameters.get(KEY_SCOPE) != null ? this.parameters.get(KEY_SCOPE) : SCOPE_TYPE_OPENID; + @VisibleForTesting + Map getParameters() { + return parameters; } + @VisibleForTesting boolean isLoggingEnabled() { return loggingEnabled; } - private String getNonce() { - return parameters.containsKey(KEY_NONCE) ? parameters.get(KEY_NONCE) : secureRandomString(); - } - private String getResponseType() { return parameters.get(KEY_RESPONSE_TYPE); } diff --git a/auth0/src/test/java/com/auth0/android/provider/WebAuthProviderTest.java b/auth0/src/test/java/com/auth0/android/provider/WebAuthProviderTest.java index 05b4ff7a2..9bf705c19 100644 --- a/auth0/src/test/java/com/auth0/android/provider/WebAuthProviderTest.java +++ b/auth0/src/test/java/com/auth0/android/provider/WebAuthProviderTest.java @@ -7,7 +7,9 @@ import android.content.Intent; import android.content.res.Resources; import android.net.Uri; +import android.support.annotation.NonNull; import android.support.annotation.Nullable; +import android.util.Base64; import com.auth0.android.Auth0; import com.auth0.android.authentication.AuthenticationException; @@ -27,6 +29,7 @@ import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; +import java.io.UnsupportedEncodingException; import java.util.HashMap; import java.util.Map; import java.util.Set; @@ -42,6 +45,7 @@ import static android.support.test.espresso.intent.matcher.UriMatchers.hasParamWithValue; import static android.support.test.espresso.intent.matcher.UriMatchers.hasPath; import static android.support.test.espresso.intent.matcher.UriMatchers.hasScheme; +import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.CoreMatchers.not; import static org.hamcrest.CoreMatchers.notNullValue; @@ -64,6 +68,8 @@ public class WebAuthProviderTest { private static final int REQUEST_CODE = 11; + private static final String KEY_STATE = "state"; + private static final String KEY_NONCE = "nonce"; @Mock private AuthCallback callback; @@ -466,7 +472,6 @@ public void shouldSetState() throws Exception { assertThat(uri, hasParamWithValue("state", "abcdefg")); } - //nonce @Test @@ -588,6 +593,29 @@ public void shouldSetNonce() throws Exception { assertThat(uri, hasParamWithValue("nonce", "abcdefg")); } + @Test + public void shouldGenerateRandomStringIfDefaultValueMissing() throws Exception { + WebAuthProvider.init(account) + .start(activity, callback); + String random1 = WebAuthProvider.getInstance().getRandomString(null); + String random2 = WebAuthProvider.getInstance().getRandomString(null); + + assertThat(random1, is(notNullValue())); + assertThat(random2, is(notNullValue())); + assertThat(random1, is(not(equalTo(random2)))); + } + + @Test + public void shouldNotGenerateRandomStringIfDefaultValuePresent() throws Exception { + WebAuthProvider.init(account) + .start(activity, callback); + String random1 = WebAuthProvider.getInstance().getRandomString("some"); + String random2 = WebAuthProvider.getInstance().getRandomString("some"); + + assertThat(random1, is("some")); + assertThat(random2, is("some")); + } + // auth0 related @@ -906,10 +934,14 @@ public void shouldFailToStartWithInvalidAuthorizeURI() throws Exception { @Test public void shouldResumeWithRequestCodeWithResponseTypeIdToken() throws Exception { WebAuthProvider.init(account) - .withNonce("1234567890") .withResponseType(ResponseType.ID_TOKEN) .start(activity, callback, REQUEST_CODE); - final Intent intent = createAuthIntent(createHash("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJub25jZSI6IjEyMzQ1Njc4OTAifQ.oUb6xFIEPJQrFbel_Js4SaOwpFfM_kxHxI7xDOHgghk", null, null, null, null, null)); + + String sentState = WebAuthProvider.getInstance().getParameters().get(KEY_STATE); + String sentNonce = WebAuthProvider.getInstance().getParameters().get(KEY_NONCE); + assertThat(sentState, is(not(isEmptyOrNullString()))); + assertThat(sentNonce, is(not(isEmptyOrNullString()))); + final Intent intent = createAuthIntent(createHash(customNonceJWT(sentNonce), null, null, null, sentState, null)); assertTrue(WebAuthProvider.resume(REQUEST_CODE, Activity.RESULT_OK, intent)); verify(callback).onSuccess(any(Credentials.class)); @@ -918,10 +950,15 @@ public void shouldResumeWithRequestCodeWithResponseTypeIdToken() throws Exceptio @Test public void shouldResumeWithIntentWithResponseTypeIdToken() throws Exception { WebAuthProvider.init(account) - .withNonce("1234567890") .withResponseType(ResponseType.ID_TOKEN) .start(activity, callback); - final Intent intent = createAuthIntent(createHash("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJub25jZSI6IjEyMzQ1Njc4OTAifQ.oUb6xFIEPJQrFbel_Js4SaOwpFfM_kxHxI7xDOHgghk", null, null, null, null, null)); + + String sentState = WebAuthProvider.getInstance().getParameters().get(KEY_STATE); + + String sentNonce = WebAuthProvider.getInstance().getParameters().get(KEY_NONCE); + assertThat(sentState, is(not(isEmptyOrNullString()))); + assertThat(sentNonce, is(not(isEmptyOrNullString()))); + final Intent intent = createAuthIntent(createHash(customNonceJWT(sentNonce), null, null, null, sentState, null)); assertTrue(WebAuthProvider.resume(intent)); verify(callback).onSuccess(any(Credentials.class)); @@ -940,7 +977,6 @@ public Object answer(InvocationOnMock invocation) throws Throwable { }).when(pkce).getToken(any(String.class), eq(callback)); WebAuthProvider.init(account) - .withState("1234567890") .useCodeGrant(true) .withPKCE(pkce) .start(activity, callback); @@ -962,11 +998,14 @@ public Object answer(InvocationOnMock invocation) throws Throwable { } }).when(pkce).getToken(any(String.class), codeCallbackCaptor.capture()); WebAuthProvider.init(account) - .withState("1234567890") .useCodeGrant(true) .withPKCE(pkce) .start(activity, callback); - final Intent intent = createAuthIntent(createHash("urlId", "urlAccess", "urlRefresh", "urlType", "1234567890", null)); + + String sentState = WebAuthProvider.getInstance().getParameters().get(KEY_STATE); + + assertThat(sentState, is(not(isEmptyOrNullString()))); + final Intent intent = createAuthIntent(createHash("urlId", "urlAccess", "urlRefresh", "urlType", sentState, null)); assertTrue(WebAuthProvider.resume(intent)); final ArgumentCaptor credentialsCaptor = ArgumentCaptor.forClass(Credentials.class); @@ -992,11 +1031,14 @@ public Object answer(InvocationOnMock invocation) throws Throwable { } }).when(pkce).getToken(any(String.class), codeCallbackCaptor.capture()); WebAuthProvider.init(account) - .withState("1234567890") .useCodeGrant(true) .withPKCE(pkce) .start(activity, callback, REQUEST_CODE); - final Intent intent = createAuthIntent(createHash("urlId", "urlAccess", "urlRefresh", "urlType", "1234567890", null)); + + String sentState = WebAuthProvider.getInstance().getParameters().get(KEY_STATE); + + assertThat(sentState, is(not(isEmptyOrNullString()))); + final Intent intent = createAuthIntent(createHash("urlId", "urlAccess", "urlRefresh", "urlType", sentState, null)); assertTrue(WebAuthProvider.resume(REQUEST_CODE, Activity.RESULT_OK, intent)); final ArgumentCaptor credentialsCaptor = ArgumentCaptor.forClass(Credentials.class); @@ -1012,10 +1054,13 @@ public Object answer(InvocationOnMock invocation) throws Throwable { @Test public void shouldResumeWithIntentWithImplicitGrant() throws Exception { WebAuthProvider.init(account) - .withState("1234567890") .useCodeGrant(false) .start(activity, callback); - final Intent intent = createAuthIntent(createHash("iToken", "aToken", null, "refresh_token", "1234567890", null)); + + String sentState = WebAuthProvider.getInstance().getParameters().get(KEY_STATE); + + assertThat(sentState, is(not(isEmptyOrNullString()))); + final Intent intent = createAuthIntent(createHash("urlId", "urlAccess", "urlRefresh", "urlType", sentState, null)); assertTrue(WebAuthProvider.resume(intent)); verify(callback).onSuccess(any(Credentials.class)); @@ -1024,10 +1069,13 @@ public void shouldResumeWithIntentWithImplicitGrant() throws Exception { @Test public void shouldResumeWithRequestCodeWithImplicitGrant() throws Exception { WebAuthProvider.init(account) - .withState("1234567890") .useCodeGrant(false) .start(activity, callback, REQUEST_CODE); - final Intent intent = createAuthIntent(createHash("iToken", "aToken", null, "refresh_token", "1234567890", null)); + + String sentState = WebAuthProvider.getInstance().getParameters().get(KEY_STATE); + + assertThat(sentState, is(not(isEmptyOrNullString()))); + final Intent intent = createAuthIntent(createHash("urlId", "urlAccess", "urlRefresh", "urlType", sentState, null)); assertTrue(WebAuthProvider.resume(REQUEST_CODE, Activity.RESULT_OK, intent)); verify(callback).onSuccess(any(Credentials.class)); @@ -1400,4 +1448,22 @@ private String createHash(@Nullable String idToken, @Nullable String accessToken } return hash.length() == 1 ? "" : hash; } + + private String customNonceJWT(@NonNull String nonce) { + String header = encodeString("{}"); + String bodyBuilder = "{\"nonce\":\"" + nonce + "\"}"; + String body = encodeString(bodyBuilder); + String signature = "sign"; + return String.format("%s.%s.%s", header, body, signature); + } + + private String encodeString(String source) { + byte[] bytes = Base64.encode(source.getBytes(), Base64.URL_SAFE | Base64.NO_WRAP | Base64.NO_PADDING); + String res = ""; + try { + res = new String(bytes, "UTF-8"); + } catch (UnsupportedEncodingException ignored) { + } + return res; + } } \ No newline at end of file