Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Generate and save State and Nonce variables for WebAuthProvider #50

Merged
merged 1 commit into from
Dec 12, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 20 additions & 17 deletions auth0/src/main/java/com/auth0/android/provider/WebAuthProvider.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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);

Expand All @@ -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;
}

Expand All @@ -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<String, String> 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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -466,7 +472,6 @@ public void shouldSetState() throws Exception {
assertThat(uri, hasParamWithValue("state", "abcdefg"));
}


//nonce

@Test
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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));
Expand All @@ -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));
Expand All @@ -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);
Expand All @@ -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<Credentials> credentialsCaptor = ArgumentCaptor.forClass(Credentials.class);
Expand All @@ -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<Credentials> credentialsCaptor = ArgumentCaptor.forClass(Credentials.class);
Expand All @@ -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));
Expand All @@ -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));
Expand Down Expand Up @@ -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;
}
}