Skip to content

Commit

Permalink
Merge branch 'master' into passwordless-v2
Browse files Browse the repository at this point in the history
  • Loading branch information
cocojoe authored Dec 12, 2019
2 parents f276db5 + d808545 commit d49a7d9
Show file tree
Hide file tree
Showing 13 changed files with 852 additions and 207 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.auth0.android.provider;

import android.support.annotation.NonNull;

import java.util.Arrays;

/**
* Token signature verifier for RSH256 and/or HS256 algorithms.
*/
class AlgorithmNameVerifier extends SignatureVerifier {

AlgorithmNameVerifier() {
super(Arrays.asList("HS256", "RS256"));
}

@Override
protected void checkSignature(@NonNull String[] tokenParts) throws TokenValidationException {
//NO-OP
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,12 @@
import android.support.annotation.Nullable;
import android.util.Base64;

import com.auth0.android.jwt.JWT;

import java.nio.charset.Charset;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.Signature;
import java.util.Collections;

/**
* Token signature verifier for HS256 algorithms.
Expand All @@ -26,6 +25,7 @@ class AsymmetricSignatureVerifier extends SignatureVerifier {
* @throws InvalidKeyException if the public key provided is null or not of type RSA
*/
AsymmetricSignatureVerifier(@Nullable PublicKey publicKey) throws InvalidKeyException {
super(Collections.singletonList("RS256"));
try {
publicSignature = Signature.getInstance("SHA256withRSA");
publicSignature.initVerify(publicKey);
Expand All @@ -36,13 +36,12 @@ class AsymmetricSignatureVerifier extends SignatureVerifier {
}

@Override
void verifySignature(@NonNull JWT token) throws TokenValidationException {
String[] parts = token.toString().split("\\.");
String content = parts[0] + "." + parts[1];
protected void checkSignature(@NonNull String[] tokenParts) throws TokenValidationException {
String content = tokenParts[0] + "." + tokenParts[1];
byte[] contentBytes = content.getBytes(Charset.defaultCharset());
byte[] signatureBytes = Base64.decode(parts[2], Base64.URL_SAFE | Base64.NO_WRAP);
boolean valid = false;
try {
byte[] signatureBytes = Base64.decode(tokenParts[2], Base64.URL_SAFE | Base64.NO_WRAP);
publicSignature.update(contentBytes);
valid = publicSignature.verify(signatureBytes);
} catch (Exception ignored) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ class IdTokenVerifier {
* @throws TokenValidationException If the ID Token is null, its signing algorithm not supported, its signature invalid or one of its claim invalid.
*/
void verify(@NonNull JWT token, @NonNull IdTokenVerificationOptions verifyOptions) throws TokenValidationException {
verifyOptions.getSignatureVerifier().verifySignature(token);
verifyOptions.getSignatureVerifier().verify(token);

if (isEmpty(token.getIssuer())) {
throw new TokenValidationException("Issuer (iss) claim must be a string present in the ID token");
Expand All @@ -41,11 +41,11 @@ void verify(@NonNull JWT token, @NonNull IdTokenVerificationOptions verifyOption
}

final List<String> audience = token.getAudience();
if (audience == null) {
if (audience == null || audience.isEmpty()) {
throw new TokenValidationException("Audience (aud) claim must be a string or array of strings present in the ID token");
}
if (!audience.contains(verifyOptions.getAudience())) {
throw new TokenValidationException(String.format("Audience (aud) claim mismatch in the ID token; expected \"%s\" but found \"%s\"", verifyOptions.getAudience(), token.getAudience()));
throw new TokenValidationException(String.format("Audience (aud) claim mismatch in the ID token; expected \"%s\" but was not one of \"%s\"", verifyOptions.getAudience(), token.getAudience()));
}

final Calendar cal = Calendar.getInstance();
Expand Down Expand Up @@ -76,7 +76,6 @@ void verify(@NonNull JWT token, @NonNull IdTokenVerificationOptions verifyOption
throw new TokenValidationException(String.format("Issued At (iat) claim error in the ID token; current time (%d) is before issued at time (%d)", now.getTime() / 1000, iatDate.getTime() / 1000));
}


if (verifyOptions.getNonce() != null) {
String nonceClaim = token.getClaim(NONCE_CLAIM).asString();
if (isEmpty(nonceClaim)) {
Expand Down
32 changes: 20 additions & 12 deletions auth0/src/main/java/com/auth0/android/provider/OAuthManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
import com.auth0.android.Auth0;
import com.auth0.android.authentication.AuthenticationAPIClient;
import com.auth0.android.authentication.AuthenticationException;
import com.auth0.android.callback.AuthenticationCallback;
import com.auth0.android.callback.BaseCallback;
import com.auth0.android.jwt.DecodeException;
import com.auth0.android.jwt.JWT;
Expand Down Expand Up @@ -41,6 +40,7 @@ class OAuthManager extends ResumableManager {
private static final String ERROR_VALUE_ACCESS_DENIED = "access_denied";
private static final String ERROR_VALUE_UNAUTHORIZED = "unauthorized";
private static final String ERROR_VALUE_LOGIN_REQUIRED = "login_required";
private static final String ERROR_VALUE_ID_TOKEN_VALIDATION_FAILED = "Could not verify the ID token";
private static final String METHOD_SHA_256 = "S256";
private static final String KEY_CODE_CHALLENGE = "code_challenge";
private static final String KEY_CODE_CHALLENGE_METHOD = "code_challenge_method";
Expand All @@ -52,7 +52,6 @@ class OAuthManager extends ResumableManager {
private static final String KEY_ID_TOKEN = "id_token";
private static final String KEY_ACCESS_TOKEN = "access_token";
private static final String KEY_TOKEN_TYPE = "token_type";
private static final String KEY_REFRESH_TOKEN = "refresh_token";
private static final String KEY_EXPIRES_IN = "expires_in";
private static final String KEY_CODE = "code";
private static final String KEY_SCOPE = "scope";
Expand Down Expand Up @@ -147,7 +146,7 @@ boolean resume(AuthorizeResult result) {

if (frontChannelIdTokenExpected) {
//Must be response_type=id_token (or additional values)
assertValidIdToken(frontChannelCredentials.getIdToken(), new AuthenticationCallback<Void>() {
assertValidIdToken(frontChannelCredentials.getIdToken(), new BaseCallback<Void, TokenValidationException>() {
@Override
public void onSuccess(Void ignored) {
if (!shouldUsePKCE()) {
Expand All @@ -167,8 +166,9 @@ public void onSuccess(@NonNull Credentials credentials) {
}

@Override
public void onFailure(AuthenticationException error) {
callback.onFailure(error);
public void onFailure(TokenValidationException error) {
AuthenticationException wrappedError = new AuthenticationException(ERROR_VALUE_ID_TOKEN_VALIDATION_FAILED, error);
callback.onFailure(wrappedError);
}
});
return true;
Expand All @@ -185,24 +185,25 @@ public void onFailure(AuthenticationException error) {

@Override
public void onSuccess(@NonNull final Credentials credentials) {
assertValidIdToken(credentials.getIdToken(), new AuthenticationCallback<Void>() {
assertValidIdToken(credentials.getIdToken(), new BaseCallback<Void, TokenValidationException>() {
@Override
public void onSuccess(Void ignored) {
Credentials finalCredentials = mergeCredentials(frontChannelCredentials, credentials);
callback.onSuccess(finalCredentials);
}

@Override
public void onFailure(AuthenticationException error) {
callback.onFailure(error);
public void onFailure(TokenValidationException error) {
AuthenticationException wrappedError = new AuthenticationException(ERROR_VALUE_ID_TOKEN_VALIDATION_FAILED, error);
callback.onFailure(wrappedError);
}
});
}
});
return true;
}

private void assertValidIdToken(String idToken, final AuthenticationCallback<Void> validationCallback) {
private void assertValidIdToken(String idToken, final BaseCallback<Void, TokenValidationException> validationCallback) {
if (TextUtils.isEmpty(idToken)) {
validationCallback.onFailure(new TokenValidationException("ID token is required but missing"));
return;
Expand All @@ -215,11 +216,10 @@ private void assertValidIdToken(String idToken, final AuthenticationCallback<Voi
return;
}

SignatureVerifier.forToken(decodedIdToken, apiClient, new BaseCallback<SignatureVerifier, TokenValidationException>() {
BaseCallback<SignatureVerifier, TokenValidationException> signatureVerifierCallback = new BaseCallback<SignatureVerifier, TokenValidationException>() {

@Override
public void onFailure(TokenValidationException error) {
//TODO Test that the exception cause message is readable by the end user
validationCallback.onFailure(error);
}

Expand All @@ -242,7 +242,15 @@ public void onSuccess(SignatureVerifier signatureVerifier) {
validationCallback.onFailure(exc);
}
}
});
};

String tokenAlg = decodedIdToken.getHeader().get("alg");
if (account.isOIDCConformant() || "RS256".equals(tokenAlg)) {
String tokenKeyId = decodedIdToken.getHeader().get("kid");
SignatureVerifier.forAsymmetricAlgorithm(tokenKeyId, apiClient, signatureVerifierCallback);
} else {
SignatureVerifier.forUnknownAlgorithm(signatureVerifierCallback);
}
}

private long getCurrentTimeInMillis() {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.auth0.android.provider;

import android.support.annotation.NonNull;
import android.support.annotation.Nullable;

import com.auth0.android.authentication.AuthenticationAPIClient;
import com.auth0.android.authentication.AuthenticationException;
Expand All @@ -10,53 +11,76 @@

import java.security.InvalidKeyException;
import java.security.PublicKey;
import java.util.List;
import java.util.Map;

/**
* Abstract class meant to verify tokens signed with HS256 and RS256 signatures.
*/
abstract class SignatureVerifier {

private final List<String> supportedAlgorithms;

SignatureVerifier(List<String> supportedAlgorithms) {
this.supportedAlgorithms = supportedAlgorithms;
}

/**
* Verifies that the given token's signature is valid, deeming the payload inside it authentic
*
* @param token the ID token to have its signature validated
* @throws TokenValidationException if the signature is not valid
*/
abstract void verifySignature(@NonNull JWT token) throws TokenValidationException;
void verify(@NonNull JWT token) throws TokenValidationException {
String tokenAlg = token.getHeader().get("alg");
String[] tokenParts = token.toString().split("\\.");

checkAlgorithm(tokenAlg);
checkSignature(tokenParts);
}

private void checkAlgorithm(String tokenAlgorithm) throws TokenValidationException {
if (!supportedAlgorithms.contains(tokenAlgorithm)) {
throw new TokenValidationException(String.format("Signature algorithm of \"%s\" is not supported. Expected the ID token to be signed with any of %s.", tokenAlgorithm, supportedAlgorithms));
}
}

abstract protected void checkSignature(@NonNull String[] tokenParts) throws TokenValidationException;


/**
* Validates the algorithm of the given token is supported and creates a new instance of a SignatureVerifier
* Creates a new SignatureVerifier for Asymmetric algorithm ("RS256"). Signature check will actually happen.
*
* @param token the ID token to create a signature verifier for
* @param apiClient api client instance to fetch the JWKS keys, if necessary
* @param callback the callback to receive the result in
* @param keyId the id of the key used to sign this token. Obtained from the token's header
* @param apiClient the Authentication API client instance. Used to fetch the JWKs
* @param callback where to receive the results
*/
static void forToken(@NonNull JWT token, @NonNull AuthenticationAPIClient apiClient, @NonNull final BaseCallback<SignatureVerifier, TokenValidationException> callback) {
String algorithmName = token.getHeader().get("alg");
if ("RS256".equals(algorithmName)) {
final String keyId = token.getHeader().get("kid");
apiClient.fetchJsonWebKeys().start(new AuthenticationCallback<Map<String, PublicKey>>() {
@Override
public void onSuccess(Map<String, PublicKey> jwks) {
PublicKey publicKey = jwks.get(keyId);
try {
callback.onSuccess(new AsymmetricSignatureVerifier(publicKey));
} catch (InvalidKeyException e) {
callback.onFailure(new TokenValidationException(String.format("Could not find a public key for kid \"%s\"", keyId)));
}
}

@Override
public void onFailure(AuthenticationException error) {
static void forAsymmetricAlgorithm(@Nullable final String keyId, @NonNull AuthenticationAPIClient apiClient, @NonNull final BaseCallback<SignatureVerifier, TokenValidationException> callback) {
apiClient.fetchJsonWebKeys().start(new AuthenticationCallback<Map<String, PublicKey>>() {
@Override
public void onSuccess(Map<String, PublicKey> jwks) {
PublicKey publicKey = jwks.get(keyId);
try {
callback.onSuccess(new AsymmetricSignatureVerifier(publicKey));
} catch (InvalidKeyException e) {
callback.onFailure(new TokenValidationException(String.format("Could not find a public key for kid \"%s\"", keyId)));
}
});
} else if ("HS256".equals(algorithmName)) {
callback.onSuccess(new SymmetricSignatureVerifier());
} else {
callback.onFailure(new TokenValidationException(String.format("Signature algorithm of \"%s\" is not supported. Expected either \"RS256\" or \"HS256\".", algorithmName)));
}
}

@Override
public void onFailure(AuthenticationException error) {
callback.onFailure(new TokenValidationException(String.format("Could not find a public key for kid \"%s\"", keyId)));
}
});
}

/**
* Creates a new SignatureVerifier for when the algorithm is unknown or not set explicitly by the user. Only algorithm name is checked.
*
* @param callback where to receive the results
*/
static void forUnknownAlgorithm(@NonNull final BaseCallback<SignatureVerifier, TokenValidationException> callback) {
callback.onSuccess(new AlgorithmNameVerifier());
}

}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
package com.auth0.android.provider;

import com.auth0.android.authentication.AuthenticationException;
import com.auth0.android.Auth0Exception;

class TokenValidationException extends AuthenticationException {
private static final String ERROR_CODE = "a0.sdk.internal_error.id_token_validation";
class TokenValidationException extends Auth0Exception {

TokenValidationException(String message) {
super(ERROR_CODE, message);
super(message);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import com.auth0.android.result.Credentials;
import com.auth0.android.result.CredentialsMock;

import org.hamcrest.CoreMatchers;
import org.hamcrest.core.Is;
import org.junit.Before;
import org.junit.Rule;
Expand All @@ -27,6 +28,7 @@

import java.util.Date;

import static org.hamcrest.Matchers.closeTo;
import static org.hamcrest.Matchers.notNullValue;
import static org.hamcrest.Matchers.nullValue;
import static org.hamcrest.core.Is.is;
Expand Down Expand Up @@ -276,7 +278,8 @@ public void shouldGetNonExpiredCredentialsFromStorage() {
assertThat(retrievedCredentials.getIdToken(), is("idToken"));
assertThat(retrievedCredentials.getRefreshToken(), is("refreshToken"));
assertThat(retrievedCredentials.getType(), is("type"));
assertThat(retrievedCredentials.getExpiresIn(), is(123456L));
assertThat(retrievedCredentials.getExpiresIn(), is(notNullValue()));
assertThat(retrievedCredentials.getExpiresIn().doubleValue(), CoreMatchers.is(closeTo(123456L, 1)));
assertThat(retrievedCredentials.getExpiresAt(), is(notNullValue()));
assertThat(retrievedCredentials.getExpiresAt().getTime(), is(expirationTime));
assertThat(retrievedCredentials.getScope(), is("scope"));
Expand Down Expand Up @@ -304,7 +307,8 @@ public void shouldGetNonExpiredCredentialsFromStorageWhenOnlyIdTokenIsAvailable(
assertThat(retrievedCredentials.getIdToken(), is("idToken"));
assertThat(retrievedCredentials.getRefreshToken(), is("refreshToken"));
assertThat(retrievedCredentials.getType(), is("type"));
assertThat(retrievedCredentials.getExpiresIn(), is(123456L));
assertThat(retrievedCredentials.getExpiresIn(), is(notNullValue()));
assertThat(retrievedCredentials.getExpiresIn().doubleValue(), CoreMatchers.is(closeTo(123456L, 1)));
assertThat(retrievedCredentials.getExpiresAt(), is(notNullValue()));
assertThat(retrievedCredentials.getExpiresAt().getTime(), is(expirationTime));
assertThat(retrievedCredentials.getScope(), is("scope"));
Expand Down Expand Up @@ -332,7 +336,8 @@ public void shouldGetNonExpiredCredentialsFromStorageWhenOnlyAccessTokenIsAvaila
assertThat(retrievedCredentials.getIdToken(), is(nullValue()));
assertThat(retrievedCredentials.getRefreshToken(), is("refreshToken"));
assertThat(retrievedCredentials.getType(), is("type"));
assertThat(retrievedCredentials.getExpiresIn(), is(123456L));
assertThat(retrievedCredentials.getExpiresIn(), is(notNullValue()));
assertThat(retrievedCredentials.getExpiresIn().doubleValue(), CoreMatchers.is(closeTo(123456L, 1)));
assertThat(retrievedCredentials.getExpiresAt(), is(notNullValue()));
assertThat(retrievedCredentials.getExpiresAt().getTime(), is(expirationTime));
assertThat(retrievedCredentials.getScope(), is("scope"));
Expand Down
Loading

0 comments on commit d49a7d9

Please sign in to comment.