From 98b2b38d0d4a2dd12834fe0366b9e42a424ac6a8 Mon Sep 17 00:00:00 2001 From: Luciano Balmaceda Date: Tue, 19 Jan 2021 16:17:58 +0100 Subject: [PATCH 1/5] remove jwt-decode.android dependency, use own Jwt class. --- auth0/build.gradle | 1 - .../storage/BaseCredentialsManager.kt | 2 +- .../authentication/storage/JWTDecoder.java | 6 +- .../android/provider/IdTokenVerifier.java | 10 +-- .../auth0/android/provider/OAuthManager.kt | 11 ++- .../android/provider/SignatureVerifier.java | 11 ++- .../com/auth0/android/request/internal/Jwt.kt | 71 +++++++++++++++++++ .../storage/CredentialsManagerTest.kt | 30 ++++---- .../storage/JWTDecoderTest.java | 20 +++--- .../storage/SecureCredentialsManagerTest.kt | 30 ++++---- .../AsymmetricSignatureVerifierTest.java | 14 ++-- .../android/provider/IdTokenVerifierTest.java | 38 +++++----- 12 files changed, 155 insertions(+), 89 deletions(-) create mode 100644 auth0/src/main/java/com/auth0/android/request/internal/Jwt.kt diff --git a/auth0/build.gradle b/auth0/build.gradle index 4e137e155..0409bbbaf 100644 --- a/auth0/build.gradle +++ b/auth0/build.gradle @@ -95,7 +95,6 @@ dependencies { implementation 'com.squareup.okhttp3:okhttp:4.9.0' implementation 'com.squareup.okhttp3:logging-interceptor:4.9.0' implementation 'com.google.code.gson:gson:2.8.6' - implementation 'com.auth0.android:jwtdecode:1.3.0' testImplementation 'junit:junit:4.13.1' testImplementation 'org.hamcrest:java-hamcrest:2.0.0.0' diff --git a/auth0/src/main/java/com/auth0/android/authentication/storage/BaseCredentialsManager.kt b/auth0/src/main/java/com/auth0/android/authentication/storage/BaseCredentialsManager.kt index bc3bb4b63..4142ab249 100644 --- a/auth0/src/main/java/com/auth0/android/authentication/storage/BaseCredentialsManager.kt +++ b/auth0/src/main/java/com/auth0/android/authentication/storage/BaseCredentialsManager.kt @@ -100,7 +100,7 @@ public abstract class BaseCredentialsManager internal constructor( var expiresAt = credentials.expiresAt!!.time if (credentials.idToken != null) { val idToken = jwtDecoder.decode(credentials.idToken) - val idTokenExpiresAtDate = idToken.expiresAt + val idTokenExpiresAtDate = idToken.getExpiresAt() if (idTokenExpiresAtDate != null) { expiresAt = min(idTokenExpiresAtDate.time, expiresAt) } diff --git a/auth0/src/main/java/com/auth0/android/authentication/storage/JWTDecoder.java b/auth0/src/main/java/com/auth0/android/authentication/storage/JWTDecoder.java index f6e9ed6e2..459dac5fe 100644 --- a/auth0/src/main/java/com/auth0/android/authentication/storage/JWTDecoder.java +++ b/auth0/src/main/java/com/auth0/android/authentication/storage/JWTDecoder.java @@ -1,6 +1,6 @@ package com.auth0.android.authentication.storage; -import com.auth0.android.jwt.JWT; +import com.auth0.android.request.internal.Jwt; /** * Bridge class for decoding JWTs. @@ -11,7 +11,7 @@ class JWTDecoder { JWTDecoder() { } - JWT decode(String jwt) { - return new JWT(jwt); + Jwt decode(String jwt) { + return new Jwt(jwt); } } diff --git a/auth0/src/main/java/com/auth0/android/provider/IdTokenVerifier.java b/auth0/src/main/java/com/auth0/android/provider/IdTokenVerifier.java index 5d86a9e65..29d979b7a 100644 --- a/auth0/src/main/java/com/auth0/android/provider/IdTokenVerifier.java +++ b/auth0/src/main/java/com/auth0/android/provider/IdTokenVerifier.java @@ -2,7 +2,7 @@ import androidx.annotation.NonNull; -import com.auth0.android.jwt.JWT; +import com.auth0.android.request.internal.Jwt; import java.util.Calendar; import java.util.Date; @@ -25,7 +25,7 @@ class IdTokenVerifier { * @param verifyOptions the verification options, like audience, issuer, algorithm. * @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 { + void verify(@NonNull Jwt token, @NonNull IdTokenVerificationOptions verifyOptions) throws TokenValidationException { verifyOptions.getSignatureVerifier().verify(token); if (isEmpty(token.getIssuer())) { @@ -69,7 +69,7 @@ void verify(@NonNull JWT token, @NonNull IdTokenVerificationOptions verifyOption } if (verifyOptions.getNonce() != null) { - String nonceClaim = token.getClaim(NONCE_CLAIM).asString(); + String nonceClaim = token.getNonce(); if (isEmpty(nonceClaim)) { throw new TokenValidationException("Nonce (nonce) claim must be a string present in the ID token"); } @@ -79,7 +79,7 @@ void verify(@NonNull JWT token, @NonNull IdTokenVerificationOptions verifyOption } if (audience.size() > 1) { - String azpClaim = token.getClaim(AZP_CLAIM).asString(); + String azpClaim = token.getAuthorizedParty(); if (isEmpty(azpClaim)) { throw new TokenValidationException("Authorized Party (azp) claim must be a string present in the ID token when Audience (aud) claim has multiple values"); } @@ -89,7 +89,7 @@ void verify(@NonNull JWT token, @NonNull IdTokenVerificationOptions verifyOption } if (verifyOptions.getMaxAge() != null) { - Date authTime = token.getClaim(AUTH_TIME_CLAIM).asDate(); + Date authTime = token.getAuthenticationTime(); if (authTime == null) { throw new TokenValidationException("Authentication Time (auth_time) claim must be a number present in the ID token when Max Age (max_age) is specified"); } diff --git a/auth0/src/main/java/com/auth0/android/provider/OAuthManager.kt b/auth0/src/main/java/com/auth0/android/provider/OAuthManager.kt index c8e58a787..802f5a798 100644 --- a/auth0/src/main/java/com/auth0/android/provider/OAuthManager.kt +++ b/auth0/src/main/java/com/auth0/android/provider/OAuthManager.kt @@ -11,8 +11,7 @@ import com.auth0.android.Auth0Exception import com.auth0.android.authentication.AuthenticationAPIClient import com.auth0.android.authentication.AuthenticationException import com.auth0.android.callback.Callback -import com.auth0.android.jwt.DecodeException -import com.auth0.android.jwt.JWT +import com.auth0.android.request.internal.Jwt import com.auth0.android.result.Credentials import java.security.SecureRandom import java.util.* @@ -134,9 +133,9 @@ internal class OAuthManager( validationCallback.onFailure(TokenValidationException("ID token is required but missing")) return } - val decodedIdToken: JWT = try { - JWT(idToken!!) - } catch (ignored: DecodeException) { + val decodedIdToken: Jwt = try { + Jwt(idToken!!) + } catch (ignored: Exception) { validationCallback.onFailure(TokenValidationException("ID token could not be decoded")) return } @@ -167,7 +166,7 @@ internal class OAuthManager( } } } - val tokenKeyId = decodedIdToken.header["kid"] + val tokenKeyId = decodedIdToken.getKeyId() SignatureVerifier.forAsymmetricAlgorithm(tokenKeyId, apiClient, signatureVerifierCallback) } diff --git a/auth0/src/main/java/com/auth0/android/provider/SignatureVerifier.java b/auth0/src/main/java/com/auth0/android/provider/SignatureVerifier.java index b2a4b1325..33ebbf859 100644 --- a/auth0/src/main/java/com/auth0/android/provider/SignatureVerifier.java +++ b/auth0/src/main/java/com/auth0/android/provider/SignatureVerifier.java @@ -7,7 +7,7 @@ import com.auth0.android.authentication.AuthenticationException; import com.auth0.android.callback.AuthenticationCallback; import com.auth0.android.callback.Callback; -import com.auth0.android.jwt.JWT; +import com.auth0.android.request.internal.Jwt; import java.security.InvalidKeyException; import java.security.PublicKey; @@ -31,12 +31,9 @@ abstract class SignatureVerifier { * @param token the ID token to have its signature validated * @throws TokenValidationException if the signature is not valid */ - void verify(@NonNull JWT token) throws TokenValidationException { - String tokenAlg = token.getHeader().get("alg"); - String[] tokenParts = token.toString().split("\\."); - - checkAlgorithm(tokenAlg); - checkSignature(tokenParts); + void verify(@NonNull Jwt token) throws TokenValidationException { + checkAlgorithm(token.getAlgorithm()); + checkSignature(token.getParts()); } private void checkAlgorithm(String tokenAlgorithm) throws TokenValidationException { diff --git a/auth0/src/main/java/com/auth0/android/request/internal/Jwt.kt b/auth0/src/main/java/com/auth0/android/request/internal/Jwt.kt new file mode 100644 index 000000000..bb27b4b33 --- /dev/null +++ b/auth0/src/main/java/com/auth0/android/request/internal/Jwt.kt @@ -0,0 +1,71 @@ +package com.auth0.android.request.internal + +import android.util.Base64 +import com.google.gson.reflect.TypeToken +import java.util.* + + +internal class Jwt(rawToken: String) { + + val parts: Array + private val decodedHeader: Map + private val decodedPayload: Map + + // TODO: Convert fun to properties. Assign once on the init method + + // header + fun getType(): String = decodedHeader["typ"] as String + fun getAlgorithm(): String = decodedHeader["alg"] as String + fun getKeyId(): String? = decodedHeader["kid"] as String? + + // payload + fun getSubject(): String? = decodedPayload["sub"] as String? + fun getIssuer(): String? = decodedPayload["iss"] as String? + fun getNonce(): String? = decodedPayload["nonce"] as String? + fun getIssuedAt(): Date? = (decodedPayload["iat"] as? Double)?.let { Date(it.toLong() * 1000) } + fun getExpiresAt(): Date? = (decodedPayload["exp"] as? Double)?.let { Date(it.toLong() * 1000) } + fun getAuthorizedParty(): String? = decodedPayload["azp"] as String? + fun getAuthenticationTime(): Date? = + (decodedPayload["auth_time"] as? Double)?.let { Date(it.toLong() * 1000) } + + fun getAudience(): List { + return when (val aud = decodedPayload["aud"]) { + is String -> listOf(aud) + is List<*> -> aud as List + else -> emptyList() + } + } + + init { + parts = splitToken(rawToken) + val jsonHeader = parts[0].decodeBase64() + val jsonPayload = parts[1].decodeBase64() + val mapAdapter = GsonProvider.gson.getAdapter(object : TypeToken>() {}) + decodedHeader = mapAdapter.fromJson(jsonHeader) + decodedPayload = mapAdapter.fromJson(jsonPayload) + } + + private fun splitToken(token: String): Array { + var parts = token.split(".").toTypedArray() + if (parts.size == 2 && token.endsWith(".")) { + // Tokens with alg='none' have empty String as Signature. + parts = arrayOf(parts[0], parts[1], "") + } + if (parts.size != 3) { + throw java.lang.IllegalArgumentException( + String.format( + "The token was expected to have 3 parts, but got %s.", + parts.size + ) + ) + } + return parts + } + + private fun String.decodeBase64(): String { + val bytes: ByteArray = + Base64.decode(this, Base64.URL_SAFE or Base64.NO_WRAP or Base64.NO_PADDING) + return String(bytes, Charsets.UTF_8) + } + +} \ No newline at end of file diff --git a/auth0/src/test/java/com/auth0/android/authentication/storage/CredentialsManagerTest.kt b/auth0/src/test/java/com/auth0/android/authentication/storage/CredentialsManagerTest.kt index e4eb124b2..1be8657d5 100644 --- a/auth0/src/test/java/com/auth0/android/authentication/storage/CredentialsManagerTest.kt +++ b/auth0/src/test/java/com/auth0/android/authentication/storage/CredentialsManagerTest.kt @@ -3,8 +3,8 @@ package com.auth0.android.authentication.storage import com.auth0.android.authentication.AuthenticationAPIClient import com.auth0.android.authentication.AuthenticationException import com.auth0.android.callback.Callback -import com.auth0.android.jwt.JWT import com.auth0.android.request.Request +import com.auth0.android.request.internal.Jwt import com.auth0.android.result.Credentials import com.auth0.android.result.CredentialsMock import com.auth0.android.util.Clock @@ -355,8 +355,8 @@ public class CredentialsManagerTest { client.renewAuth("refreshToken") ).thenReturn(request) val newDate = Date(CredentialsMock.ONE_HOUR_AHEAD_MS + ONE_HOUR_SECONDS * 1000) - val jwtMock = mock() - Mockito.`when`(jwtMock.expiresAt).thenReturn(newDate) + val jwtMock = mock() + Mockito.`when`(jwtMock.getExpiresAt()).thenReturn(newDate) Mockito.`when`(jwtDecoder.decode("newId")).thenReturn(jwtMock) manager.getCredentials("some scope", 0, callback) verify(request).start( @@ -416,8 +416,8 @@ public class CredentialsManagerTest { client.renewAuth("refreshToken") ).thenReturn(request) val newDate = Date(CredentialsMock.ONE_HOUR_AHEAD_MS + ONE_HOUR_SECONDS * 1000) - val jwtMock = mock() - Mockito.`when`(jwtMock.expiresAt).thenReturn(newDate) + val jwtMock = mock() + Mockito.`when`(jwtMock.getExpiresAt()).thenReturn(newDate) Mockito.`when`(jwtDecoder.decode("newId")).thenReturn(jwtMock) manager.getCredentials("some scope", 0, callback) verify(request).start( @@ -478,8 +478,8 @@ public class CredentialsManagerTest { ).thenReturn(request) val newDate = Date(CredentialsMock.CURRENT_TIME_MS + 61 * 1000) // New token expires in minTTL + 1 second - val jwtMock = mock() - Mockito.`when`(jwtMock.expiresAt).thenReturn(newDate) + val jwtMock = mock() + Mockito.`when`(jwtMock.getExpiresAt()).thenReturn(newDate) Mockito.`when`(jwtDecoder.decode("newId")).thenReturn(jwtMock) manager.getCredentials(null, 60, callback) // 60 seconds of minTTL verify(request, never()) @@ -539,8 +539,8 @@ public class CredentialsManagerTest { client.renewAuth("refreshToken") ).thenReturn(request) val newDate = Date(CredentialsMock.ONE_HOUR_AHEAD_MS) - val jwtMock = mock() - Mockito.`when`(jwtMock.expiresAt).thenReturn(newDate) + val jwtMock = mock() + Mockito.`when`(jwtMock.getExpiresAt()).thenReturn(newDate) Mockito.`when`(jwtDecoder.decode("newId")).thenReturn(jwtMock) manager.getCredentials(callback) verify(request, never()) @@ -601,8 +601,8 @@ public class CredentialsManagerTest { ).thenReturn(request) val newDate = Date(CredentialsMock.CURRENT_TIME_MS + 59 * 1000) // New token expires in minTTL - 1 second - val jwtMock = mock() - Mockito.`when`(jwtMock.expiresAt).thenReturn(newDate) + val jwtMock = mock() + Mockito.`when`(jwtMock.getExpiresAt()).thenReturn(newDate) Mockito.`when`(jwtDecoder.decode("newId")).thenReturn(jwtMock) manager.getCredentials(null, 60, callback) // 60 seconds of minTTL verify(request, never()) @@ -654,8 +654,8 @@ public class CredentialsManagerTest { client.renewAuth("refreshToken") ).thenReturn(request) val newDate = Date(CredentialsMock.ONE_HOUR_AHEAD_MS) - val jwtMock = mock() - Mockito.`when`(jwtMock.expiresAt).thenReturn(newDate) + val jwtMock = mock() + Mockito.`when`(jwtMock.getExpiresAt()).thenReturn(newDate) Mockito.`when`(jwtDecoder.decode("newId")).thenReturn(jwtMock) manager.getCredentials(callback) verify(request, never()) @@ -863,8 +863,8 @@ public class CredentialsManagerTest { } private fun prepareJwtDecoderMock(expiresAt: Date?) { - val jwtMock = mock() - Mockito.`when`(jwtMock.expiresAt).thenReturn(expiresAt) + val jwtMock = mock() + Mockito.`when`(jwtMock.getExpiresAt()).thenReturn(expiresAt) Mockito.`when`(jwtDecoder.decode("idToken")).thenReturn(jwtMock) } diff --git a/auth0/src/test/java/com/auth0/android/authentication/storage/JWTDecoderTest.java b/auth0/src/test/java/com/auth0/android/authentication/storage/JWTDecoderTest.java index e6bd8711c..09495f461 100644 --- a/auth0/src/test/java/com/auth0/android/authentication/storage/JWTDecoderTest.java +++ b/auth0/src/test/java/com/auth0/android/authentication/storage/JWTDecoderTest.java @@ -1,6 +1,6 @@ package com.auth0.android.authentication.storage; -import com.auth0.android.jwt.JWT; +import com.auth0.android.request.internal.Jwt; import org.junit.Test; import org.junit.runner.RunWith; @@ -14,25 +14,25 @@ public class JWTDecoderTest { @Test public void shouldDecodeAToken() { - String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"; - JWT jwt1 = new JWT(token); + String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibm9uY2UiOiJyZWFsbHkgcmFuZG9tIHRleHQiLCJpYXQiOjE1MTYyMzkwMjJ9.LQ7QuKKvXAHwc4_EaN4VkdTe5ElDrTlo48J8QuO5j0o"; + Jwt jwt1 = new Jwt(token); - JWT jwt2 = new JWTDecoder().decode(token); + Jwt jwt2 = new JWTDecoder().decode(token); //Header claims - assertThat(jwt1.getHeader().get("alg"), is("HS256")); - assertThat(jwt1.getHeader().get("typ"), is("JWT")); + assertThat(jwt1.getAlgorithm(), is("HS256")); + assertThat(jwt1.getType(), is("JWT")); - assertThat(jwt2.getHeader().get("typ"), is("JWT")); - assertThat(jwt2.getHeader().get("alg"), is("HS256")); + assertThat(jwt2.getType(), is("JWT")); + assertThat(jwt2.getAlgorithm(), is("HS256")); //Payload claims assertThat(jwt1.getSubject(), is("1234567890")); assertThat(jwt1.getIssuedAt().getTime(), is(1516239022000L)); - assertThat(jwt1.getClaim("name").asString(), is("John Doe")); + assertThat(jwt1.getNonce(), is("really random text")); assertThat(jwt2.getSubject(), is("1234567890")); assertThat(jwt2.getIssuedAt().getTime(), is(1516239022000L)); - assertThat(jwt2.getClaim("name").asString(), is("John Doe")); + assertThat(jwt2.getNonce(), is("really random text")); } } \ No newline at end of file diff --git a/auth0/src/test/java/com/auth0/android/authentication/storage/SecureCredentialsManagerTest.kt b/auth0/src/test/java/com/auth0/android/authentication/storage/SecureCredentialsManagerTest.kt index c23b96e03..efbfdf55e 100644 --- a/auth0/src/test/java/com/auth0/android/authentication/storage/SecureCredentialsManagerTest.kt +++ b/auth0/src/test/java/com/auth0/android/authentication/storage/SecureCredentialsManagerTest.kt @@ -10,9 +10,9 @@ import com.auth0.android.Auth0 import com.auth0.android.authentication.AuthenticationAPIClient import com.auth0.android.authentication.AuthenticationException import com.auth0.android.callback.Callback -import com.auth0.android.jwt.JWT import com.auth0.android.request.Request import com.auth0.android.request.internal.GsonProvider +import com.auth0.android.request.internal.Jwt import com.auth0.android.result.Credentials import com.auth0.android.result.CredentialsMock import com.auth0.android.util.Clock @@ -501,8 +501,8 @@ public class SecureCredentialsManagerTest { insertTestCredentials(false, true, true, expiresAt) val newDate = Date(CredentialsMock.CURRENT_TIME_MS + 61 * 1000) // new token expires in minTTL + 1 seconds - val jwtMock = mock() - Mockito.`when`(jwtMock.expiresAt).thenReturn(newDate) + val jwtMock = mock() + Mockito.`when`(jwtMock.getExpiresAt()).thenReturn(newDate) Mockito.`when`(jwtDecoder.decode("newId")).thenReturn(jwtMock) Mockito.`when`( client.renewAuth("refreshToken") @@ -566,8 +566,8 @@ public class SecureCredentialsManagerTest { insertTestCredentials(false, true, true, expiresAt) val newDate = Date(CredentialsMock.CURRENT_TIME_MS + 59 * 1000) // new token expires in minTTL - 1 seconds - val jwtMock = mock() - Mockito.`when`(jwtMock.expiresAt).thenReturn(newDate) + val jwtMock = mock() + Mockito.`when`(jwtMock.getExpiresAt()).thenReturn(newDate) Mockito.`when`(jwtDecoder.decode("newId")).thenReturn(jwtMock) Mockito.`when`( client.renewAuth("refreshToken") @@ -613,8 +613,8 @@ public class SecureCredentialsManagerTest { val expiresAt = Date(CredentialsMock.ONE_HOUR_AHEAD_MS) // non expired credentials insertTestCredentials(false, true, true, expiresAt) // "scope" is set val newDate = Date(CredentialsMock.ONE_HOUR_AHEAD_MS + ONE_HOUR_SECONDS * 1000) - val jwtMock = mock() - Mockito.`when`(jwtMock.expiresAt).thenReturn(newDate) + val jwtMock = mock() + Mockito.`when`(jwtMock.getExpiresAt()).thenReturn(newDate) Mockito.`when`(jwtDecoder.decode("newId")).thenReturn(jwtMock) Mockito.`when`( client.renewAuth("refreshToken") @@ -678,8 +678,8 @@ public class SecureCredentialsManagerTest { Date(CredentialsMock.CURRENT_TIME_MS) // current time means expired credentials insertTestCredentials(false, true, true, expiresAt) val newDate = Date(CredentialsMock.ONE_HOUR_AHEAD_MS) - val jwtMock = mock() - Mockito.`when`(jwtMock.expiresAt).thenReturn(newDate) + val jwtMock = mock() + Mockito.`when`(jwtMock.getExpiresAt()).thenReturn(newDate) Mockito.`when`(jwtDecoder.decode("newId")).thenReturn(jwtMock) Mockito.`when`( client.renewAuth("refreshToken") @@ -757,8 +757,8 @@ public class SecureCredentialsManagerTest { Date(CredentialsMock.CURRENT_TIME_MS) // current time means expired credentials insertTestCredentials(false, true, true, expiresAt) val newDate = Date(CredentialsMock.ONE_HOUR_AHEAD_MS) - val jwtMock = mock() - Mockito.`when`(jwtMock.expiresAt).thenReturn(newDate) + val jwtMock = mock() + Mockito.`when`(jwtMock.getExpiresAt()).thenReturn(newDate) Mockito.`when`(jwtDecoder.decode("newId")).thenReturn(jwtMock) Mockito.`when`( client.renewAuth("refreshToken") @@ -822,8 +822,8 @@ public class SecureCredentialsManagerTest { val expiresAt = Date(CredentialsMock.CURRENT_TIME_MS) insertTestCredentials(false, true, true, expiresAt) val newDate = Date(CredentialsMock.ONE_HOUR_AHEAD_MS) - val jwtMock = mock() - Mockito.`when`(jwtMock.expiresAt).thenReturn(newDate) + val jwtMock = mock() + Mockito.`when`(jwtMock.getExpiresAt()).thenReturn(newDate) Mockito.`when`(jwtDecoder.decode("newId")).thenReturn(jwtMock) Mockito.`when`( client.renewAuth("refreshToken") @@ -1362,8 +1362,8 @@ public class SecureCredentialsManagerTest { } private fun prepareJwtDecoderMock(expiresAt: Date?) { - val jwtMock = mock() - Mockito.`when`(jwtMock.expiresAt).thenReturn(expiresAt) + val jwtMock = mock() + Mockito.`when`(jwtMock.getExpiresAt()).thenReturn(expiresAt) Mockito.`when`(jwtDecoder.decode("idToken")).thenReturn(jwtMock) } diff --git a/auth0/src/test/java/com/auth0/android/provider/AsymmetricSignatureVerifierTest.java b/auth0/src/test/java/com/auth0/android/provider/AsymmetricSignatureVerifierTest.java index 019339c98..ed0cba120 100644 --- a/auth0/src/test/java/com/auth0/android/provider/AsymmetricSignatureVerifierTest.java +++ b/auth0/src/test/java/com/auth0/android/provider/AsymmetricSignatureVerifierTest.java @@ -1,6 +1,6 @@ package com.auth0.android.provider; -import com.auth0.android.jwt.JWT; +import com.auth0.android.request.internal.Jwt; import org.junit.Rule; import org.junit.Test; @@ -29,9 +29,9 @@ public void sameInstanceCanVerifyMultipleTokens() throws Exception { String signedToken2 = createTestJWT("RS256", createJWTBody("sub")); String signedToken3 = createTestJWT("RS256", createJWTBody("aud")); - verifier.verify(new JWT(signedToken1)); - verifier.verify(new JWT(signedToken2)); - verifier.verify(new JWT(signedToken3)); + verifier.verify(new Jwt(signedToken1)); + verifier.verify(new Jwt(signedToken2)); + verifier.verify(new Jwt(signedToken3)); } @Test @@ -47,7 +47,7 @@ public void shouldThrowWhenSignatureIsInvalid() throws Exception { String[] parts = signedToken.split("\\."); signedToken = parts[0] + "." + parts[1] + ".unexpected-signature"; - verifier.verify(new JWT(signedToken)); + verifier.verify(new Jwt(signedToken)); } @Test @@ -60,7 +60,7 @@ public void shouldThrowWhenAlgorithmIsNotSupported() throws Exception { String noneToken = createTestJWT("none", createJWTBody()); - verifier.verify(new JWT(noneToken)); + verifier.verify(new Jwt(noneToken)); } @Test @@ -73,6 +73,6 @@ public void shouldThrowWhenAlgorithmIsSymmetric() throws Exception { String hsToken = createTestJWT("HS256", createJWTBody()); - verifier.verify(new JWT(hsToken)); + verifier.verify(new Jwt(hsToken)); } } \ No newline at end of file diff --git a/auth0/src/test/java/com/auth0/android/provider/IdTokenVerifierTest.java b/auth0/src/test/java/com/auth0/android/provider/IdTokenVerifierTest.java index 21b3ccd99..bb8dc76ef 100644 --- a/auth0/src/test/java/com/auth0/android/provider/IdTokenVerifierTest.java +++ b/auth0/src/test/java/com/auth0/android/provider/IdTokenVerifierTest.java @@ -1,6 +1,6 @@ package com.auth0.android.provider; -import com.auth0.android.jwt.JWT; +import com.auth0.android.request.internal.Jwt; import org.junit.Before; import org.junit.Rule; @@ -54,7 +54,7 @@ public void shouldPassAllClaimsVerification() throws Exception { jwtBody.put("nonce", EXPECTED_NONCE); String token = createTestJWT("none", jwtBody); - JWT jwt = new JWT(token); + Jwt jwt = new Jwt(token); options.setNonce(EXPECTED_NONCE); options.setMaxAge(60 * 2); idTokenVerifier.verify(jwt, options); @@ -71,7 +71,7 @@ public void shouldFailWhenSignatureIsInvalid() throws Exception { String token = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiSm9obiBEb2UifQ.FL7foy7kV9SVoC6GLEqwatuYz39BWoEUpZ9sv00zg2oJneJFkwPYYBCj92xu0Fry7zqLRkhFeveUKtSgZV6AinDvdWWH9Is8ku3l871ut-ECiR8-Co7qdIbQet3IhiLggHko4Z9Ez7F-pWmppV7BRJmYdFjbrurLfgN191VE9xC8AmnzSIPTFczg9g_aycqhea4ncd9YjiGV2QlmNB4q1aCZ3V7QyO4KwJnnLeI4tykXjNRVXfPuInaE_f0TpzpRbzJelAGhL5cmO_b0kJswCEqonYMvsVdGqM9jxWMebs7L2k2s2nZ3MQNo-gVIv3E2GfaBpCgGxO-8kyh8sBal3A"; String[] parts = token.split("\\."); token = parts[0] + "." + parts[1] + ".no-signature"; - JWT jwt = new JWT(token); + Jwt jwt = new Jwt(token); idTokenVerifier.verify(jwt, options); } @@ -82,7 +82,7 @@ public void shouldFailWhenIssuerClaimIsMissing() throws Exception { Map jwtBody = createJWTBody("iss"); String token = createTestJWT("none", jwtBody); - JWT jwt = new JWT(token); + Jwt jwt = new Jwt(token); idTokenVerifier.verify(jwt, options); } @@ -94,7 +94,7 @@ public void shouldFailWhenIssuerClaimHasUnexpectedValue() throws Exception { Map jwtBody = createJWTBody(); jwtBody.put("iss", "--invalid--"); String token = createTestJWT("none", jwtBody); - JWT jwt = new JWT(token); + Jwt jwt = new Jwt(token); idTokenVerifier.verify(jwt, options); } @@ -105,7 +105,7 @@ public void shouldFailWhenSubjectClaimIsMissing() throws Exception { Map jwtBody = createJWTBody("sub"); String token = createTestJWT("none", jwtBody); - JWT jwt = new JWT(token); + Jwt jwt = new Jwt(token); idTokenVerifier.verify(jwt, options); } @@ -116,7 +116,7 @@ public void shouldFailWhenAudienceClaimIsMissing() throws Exception { Map jwtBody = createJWTBody("aud"); String token = createTestJWT("none", jwtBody); - JWT jwt = new JWT(token); + Jwt jwt = new Jwt(token); idTokenVerifier.verify(jwt, options); } @@ -128,7 +128,7 @@ public void shouldFailWhenAudienceClaimHasUnexpectedValue() throws Exception { Map jwtBody = createJWTBody(); jwtBody.put("aud", "--invalid--"); String token = createTestJWT("none", jwtBody); - JWT jwt = new JWT(token); + Jwt jwt = new Jwt(token); idTokenVerifier.verify(jwt, options); } @@ -140,7 +140,7 @@ public void shouldFailWhenAudienceClaimArrayDoesNotContainExpectedValue() throws Map jwtBody = createJWTBody(); jwtBody.put("aud", new String[]{"--invalid-1--", "--invalid-2--"}); String token = createTestJWT("none", jwtBody); - JWT jwt = new JWT(token); + Jwt jwt = new Jwt(token); idTokenVerifier.verify(jwt, options); } @@ -152,7 +152,7 @@ public void shouldFailWhenAudienceClaimIsMultipleElementsArrayAndAuthorizedParty Map jwtBody = createJWTBody(); jwtBody.put("aud", EXPECTED_AUDIENCE_ARRAY); String token = createTestJWT("none", jwtBody); - JWT jwt = new JWT(token); + Jwt jwt = new Jwt(token); idTokenVerifier.verify(jwt, options); } @@ -165,7 +165,7 @@ public void shouldFailWhenAudienceClaimIsMultipleElementsArrayAndAuthorizedParty jwtBody.put("aud", EXPECTED_AUDIENCE_ARRAY); jwtBody.put("azp", "--invalid--"); String token = createTestJWT("none", jwtBody); - JWT jwt = new JWT(token); + Jwt jwt = new Jwt(token); idTokenVerifier.verify(jwt, options); } @@ -173,7 +173,7 @@ public void shouldFailWhenAudienceClaimIsMultipleElementsArrayAndAuthorizedParty public void shouldNotFailWhenNonceClaimIsMissingButNotRequired() throws Exception { Map jwtBody = createJWTBody("nonce"); String token = createTestJWT("none", jwtBody); - JWT jwt = new JWT(token); + Jwt jwt = new Jwt(token); idTokenVerifier.verify(jwt, options); } @@ -184,7 +184,7 @@ public void shouldFailWhenNonceClaimIsMissingAndRequired() throws Exception { Map jwtBody = createJWTBody("nonce"); String token = createTestJWT("none", jwtBody); - JWT jwt = new JWT(token); + Jwt jwt = new Jwt(token); options.setNonce(EXPECTED_NONCE); idTokenVerifier.verify(jwt, options); } @@ -197,7 +197,7 @@ public void shouldFailWhenNonceClaimIsRequiredAndHasUnexpectedValue() throws Exc Map jwtBody = createJWTBody(); jwtBody.put("nonce", "--invalid--"); String token = createTestJWT("none", jwtBody); - JWT jwt = new JWT(token); + Jwt jwt = new Jwt(token); options.setNonce(EXPECTED_NONCE); idTokenVerifier.verify(jwt, options); } @@ -209,7 +209,7 @@ public void shouldFailWhenExpiresAtClaimIsMissing() throws Exception { Map jwtBody = createJWTBody("exp"); String token = createTestJWT("none", jwtBody); - JWT jwt = new JWT(token); + Jwt jwt = new Jwt(token); idTokenVerifier.verify(jwt, options); } @@ -223,7 +223,7 @@ public void shouldFailWhenExpiresAtClaimHasUnexpectedValue() throws Exception { long pastExp = clock - 2 * 60; // 2 min jwtBody.put("exp", pastExp); String token = createTestJWT("none", jwtBody); - JWT jwt = new JWT(token); + Jwt jwt = new Jwt(token); idTokenVerifier.verify(jwt, options); } @@ -234,7 +234,7 @@ public void shouldFailWhenIssuedAtClaimIsMissing() throws Exception { Map jwtBody = createJWTBody("iat"); String token = createTestJWT("none", jwtBody); - JWT jwt = new JWT(token); + Jwt jwt = new Jwt(token); idTokenVerifier.verify(jwt, options); } @@ -245,7 +245,7 @@ public void shouldFailWhenMaxAgeIsSetButAuthTimeClaimIsMissing() throws Exceptio Map jwtBody = createJWTBody("auth_time"); String token = createTestJWT("none", jwtBody); - JWT jwt = new JWT(token); + Jwt jwt = new Jwt(token); options.setMaxAge(60 * 2); idTokenVerifier.verify(jwt, options); } @@ -259,7 +259,7 @@ public void shouldFailWhenMaxAgeIsSetAndAuthTimeClaimHasUnexpectedValue() throws long clock = FIXED_CLOCK_CURRENT_TIME_MS / 1000; jwtBody.put("auth_time", clock - 3600); String token = createTestJWT("none", jwtBody); - JWT jwt = new JWT(token); + Jwt jwt = new Jwt(token); options.setMaxAge(2 * 60); idTokenVerifier.verify(jwt, options); } From 8a18c9e13a3c1d94a3ff537bdc8f8f79354451ae Mon Sep 17 00:00:00 2001 From: Luciano Balmaceda Date: Tue, 19 Jan 2021 16:18:20 +0100 Subject: [PATCH 2/5] disable jetifier plugin --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 98bed167d..dbc950620 100644 --- a/gradle.properties +++ b/gradle.properties @@ -16,6 +16,6 @@ org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 # https://developer.android.com/topic/libraries/support-library/androidx-rn android.useAndroidX=true # Automatically convert third-party libraries to use AndroidX -android.enableJetifier=true +android.enableJetifier=false # Kotlin code style for this project: "official" or "obsolete": kotlin.code.style=official \ No newline at end of file From 622af0c3c25345c3e19c4c768d549514efe6c25c Mon Sep 17 00:00:00 2001 From: Luciano Balmaceda Date: Tue, 19 Jan 2021 19:25:23 +0100 Subject: [PATCH 3/5] add tests and error handling --- .../com/auth0/android/request/internal/Jwt.kt | 3 +- .../auth0/android/request/internal/JwtTest.kt | 230 ++++++++++++++++++ 2 files changed, 232 insertions(+), 1 deletion(-) create mode 100644 auth0/src/test/java/com/auth0/android/request/internal/JwtTest.kt diff --git a/auth0/src/main/java/com/auth0/android/request/internal/Jwt.kt b/auth0/src/main/java/com/auth0/android/request/internal/Jwt.kt index bb27b4b33..92483d92c 100644 --- a/auth0/src/main/java/com/auth0/android/request/internal/Jwt.kt +++ b/auth0/src/main/java/com/auth0/android/request/internal/Jwt.kt @@ -1,6 +1,7 @@ package com.auth0.android.request.internal import android.util.Base64 +import com.auth0.android.provider.TokenValidationException import com.google.gson.reflect.TypeToken import java.util.* @@ -52,7 +53,7 @@ internal class Jwt(rawToken: String) { parts = arrayOf(parts[0], parts[1], "") } if (parts.size != 3) { - throw java.lang.IllegalArgumentException( + throw TokenValidationException( String.format( "The token was expected to have 3 parts, but got %s.", parts.size diff --git a/auth0/src/test/java/com/auth0/android/request/internal/JwtTest.kt b/auth0/src/test/java/com/auth0/android/request/internal/JwtTest.kt new file mode 100644 index 000000000..9bb110e35 --- /dev/null +++ b/auth0/src/test/java/com/auth0/android/request/internal/JwtTest.kt @@ -0,0 +1,230 @@ +package com.auth0.android.request.internal + +import android.util.Base64 +import androidx.test.espresso.matcher.ViewMatchers.assertThat +import com.auth0.android.provider.TokenValidationException +import com.google.gson.stream.MalformedJsonException +import org.hamcrest.Matchers.* +import org.hamcrest.collection.IsEmptyCollection +import org.junit.Assert +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import java.nio.charset.Charset +import java.util.* + + +@RunWith(RobolectricTestRunner::class) +public class JwtTest { + + @Test + public fun shouldThrowIfLessThan3Parts() { + Assert.assertThrows( + "The token was expected to have 3 parts, but got 2.", + TokenValidationException::class.java + ) { + Jwt("two.parts") + } + } + + @Test + public fun shouldThrowIfMoreThan3Parts() { + Assert.assertThrows( + "The token was expected to have 3 parts, but got 4.", + TokenValidationException::class.java + ) { + Jwt("this.has.four.parts") + } + } + + @Test + public fun shouldThrowIfItsNotBase64Encoded() { + Assert.assertThrows( + "Received bytes didn't correspond to a valid Base64 encoded string.", + IllegalArgumentException::class.java + ) { + Jwt("thisIsNot.Base64_Enc.oded") + } + } + + @Test + public fun shouldThrowIfPayloadHasInvalidJSONFormat() { + Assert.assertThrows( + "The token's payload had an invalid JSON format.", + MalformedJsonException::class.java + ) { + Jwt("eyJhbGciOiJIUzI1NiJ9.e2F9.HtPWFL4M0n-jwSEOuBeGIscY5CvElN9O5LH_ag7jHrY") + } + } + + // Parts + + @Test + public fun shouldGetParts() { + val jwt = Jwt("eyJhbGciOiJIUzI1NiJ9.e30.XmNK3GpH3Ys_7wsYBfq4C3M6goz71I7dTgUkuIa5lyQ") + assertThat(jwt, `is`(notNullValue())) + assertThat( + jwt.parts, + `is`( + arrayContaining( + "eyJhbGciOiJIUzI1NiJ9", + "e30", + "XmNK3GpH3Ys_7wsYBfq4C3M6goz71I7dTgUkuIa5lyQ" + ) + ) + ) + } + + + // Public Claims + + @Test + public fun shouldGetIssuer() { + val jwt = + Jwt("eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJKb2huIERvZSJ9.SgXosfRR_IwCgHq5lF3tlM-JHtpucWCRSaVuoHTbWbQ"); + assertThat(jwt, `is`(notNullValue())) + assertThat(jwt.getIssuer(), `is`("John Doe")) + } + + @Test + public fun shouldGetNullIssuerIfMissing() { + val jwt = Jwt("eyJhbGciOiJIUzI1NiJ9.e30.something"); + assertThat(jwt, `is`(notNullValue())) + + assertThat(jwt.getIssuer(), `is`(nullValue())) + } + + @Test + public fun shouldGetSubject() { + val jwt = + Jwt("eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJUb2szbnMifQ.RudAxkslimoOY3BLl2Ghny3BrUKu9I1ZrXzCZGDJtNs"); + assertThat(jwt, `is`(notNullValue())) + assertThat(jwt.getSubject(), `is`("Tok3ns")) + } + + @Test + public fun shouldGetNullSubjectIfMissing() { + val jwt = Jwt("eyJhbGciOiJIUzI1NiJ9.e30.something"); + assertThat(jwt, `is`(notNullValue())) + + assertThat(jwt.getSubject(), `is`(nullValue())) + } + + @Test + public fun shouldGetArrayAudience() { + val jwt = + Jwt("eyJhbGciOiJIUzI1NiJ9.eyJhdWQiOlsiSG9wZSIsIlRyYXZpcyIsIlNvbG9tb24iXX0.Tm4W8WnfPjlmHSmKFakdij0on2rWPETpoM7Sh0u6-S4"); + assertThat(jwt, `is`(notNullValue())) + assertThat(jwt.getAudience(), `is`(hasSize(3))) + assertThat(jwt.getAudience(), `is`(hasItems("Hope", "Travis", "Solomon"))) + } + + @Test + public fun shouldGetStringAudience() { + val jwt = + Jwt("eyJhbGciOiJIUzI1NiJ9.eyJhdWQiOiJKYWNrIFJleWVzIn0.a4I9BBhPt1OB1GW67g2P1bEHgi6zgOjGUL4LvhE9Dgc"); + assertThat(jwt, `is`(notNullValue())) + assertThat(jwt.getAudience(), `is`(hasSize(1))) + assertThat(jwt.getAudience(), `is`(hasItems("Jack Reyes"))) + } + + @Test + public fun shouldGetEmptyListAudienceIfMissing() { + val jwt = Jwt("eyJhbGciOiJIUzI1NiJ9.e30.something"); + assertThat(jwt, `is`(notNullValue())) + + assertThat(jwt.getAudience(), IsEmptyCollection.empty()) + } + + @Test + public fun shouldDeserializeDatesUsingLong() { + val jwt = + Jwt("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjIxNDc0OTM2NDcsImF1dGhfdGltZSI6MjE0NzQ5MzY0NywiZXhwIjoyMTQ3NDkzNjQ3fQ.N4xAEtbb9pv_w2B7gVZZQbuRrcUJFJ_aZvhi8rlnw30"); + assertThat(jwt, `is`(notNullValue())) + + val secs = Integer.MAX_VALUE + 10000L; + val expectedDate = Date(secs * 1000); + assertThat(jwt.getIssuedAt(), `is`(expectedDate)) + assertThat(jwt.getExpiresAt(), `is`(expectedDate)) + assertThat(jwt.getAuthenticationTime(), `is`(expectedDate)) + } + + @Test + public fun shouldGetExpirationTime() { + val jwt = + Jwt("eyJhbGciOiJIUzI1NiJ9.eyJleHAiOjE0NzY3MjcwODZ9.HF2RrW-0L0nTIgiM8Ov7MWabIEZl4PQs07E43BphnXw"); + assertThat(jwt, `is`(notNullValue())) + assertThat(jwt.getExpiresAt(), `is`(instanceOf(Date::class.java))) + val ms = 1476727086L * 1000; + val expectedDate = Date(ms) + assertThat(jwt.getExpiresAt(), `is`(notNullValue())) + assertThat(jwt.getExpiresAt(), `is`(equalTo(expectedDate))) + } + + @Test + public fun shouldGetNullExpirationTimeIfMissing() { + val jwt = Jwt("eyJhbGciOiJIUzI1NiJ9.e30.something"); + assertThat(jwt, `is`(notNullValue())) + + assertThat(jwt.getExpiresAt(), `is`(nullValue())) + } + + @Test + public fun shouldGetIssuedAt() { + val jwt = + Jwt("eyJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE0NzY3MjcwODZ9.HcNvTtoB-Wj4KUqsl9y2u2f2Ve2JrlL1X4xIPwGNy68"); + assertThat(jwt, `is`(notNullValue())) + assertThat( + jwt.getIssuedAt(), `is`(instanceOf(Date::class.java)) + ) + val ms = 1476727086L * 1000; + val expectedDate = Date(ms); + assertThat(jwt.getIssuedAt(), `is`(notNullValue())) + assertThat(jwt.getIssuedAt(), `is`(equalTo(expectedDate))) + } + + @Test + public fun shouldGetNullIssuedAtIfMissing() { + val jwt = Jwt("eyJhbGciOiJIUzI1NiJ9.e30.something"); + assertThat(jwt, `is`(notNullValue())) + + assertThat(jwt.getIssuedAt(), `is`(nullValue())) + } + //Helper Methods + + /** + * Creates a new JWT with custom time claims. + * + * @param iatMs iat value in MILLISECONDS + * @param expMs exp value in MILLISECONDS + * @return a JWT + */ + private fun customTimeJWT(iatMs: Long?, expMs: Long?): Jwt { + val header = encodeString("{}"); + val bodyBuilder = StringBuilder("{"); + if (iatMs != null) { + val iatSeconds = iatMs / 1000; + bodyBuilder.append("\"iat\":\"").append(iatSeconds).append("\""); + } + if (expMs != null) { + if (iatMs != null) { + bodyBuilder.append(","); + } + val expSeconds = expMs / 1000; + bodyBuilder.append("\"exp\":\"").append(expSeconds).append("\""); + } + bodyBuilder.append("}"); + val body: String = encodeString(bodyBuilder.toString()); + val signature: String = "sign"; + return Jwt(String.format("%s.%s.%s", header, body, signature)); + } + + private fun encodeString(source: String): String { + val bytes = Base64.encode( + source.toByteArray(Charset.defaultCharset()), + Base64.URL_SAFE or Base64.NO_WRAP or Base64.NO_PADDING + ) + return String(bytes, Charset.defaultCharset()) + } + +} \ No newline at end of file From a4db72d0e4d83762e4b6c6f0718b44446bcc68f3 Mon Sep 17 00:00:00 2001 From: Luciano Balmaceda Date: Tue, 19 Jan 2021 19:35:58 +0100 Subject: [PATCH 4/5] migrate functions to properties --- .../storage/BaseCredentialsManager.kt | 2 +- .../auth0/android/provider/OAuthManager.kt | 2 +- .../com/auth0/android/request/internal/Jwt.kt | 55 +++++++++++-------- .../storage/CredentialsManagerTest.kt | 14 ++--- .../storage/JWTDecoderTest.java | 7 +-- .../storage/SecureCredentialsManagerTest.kt | 14 ++--- .../auth0/android/request/internal/JwtTest.kt | 40 +++++++------- 7 files changed, 72 insertions(+), 62 deletions(-) diff --git a/auth0/src/main/java/com/auth0/android/authentication/storage/BaseCredentialsManager.kt b/auth0/src/main/java/com/auth0/android/authentication/storage/BaseCredentialsManager.kt index 4142ab249..bc3bb4b63 100644 --- a/auth0/src/main/java/com/auth0/android/authentication/storage/BaseCredentialsManager.kt +++ b/auth0/src/main/java/com/auth0/android/authentication/storage/BaseCredentialsManager.kt @@ -100,7 +100,7 @@ public abstract class BaseCredentialsManager internal constructor( var expiresAt = credentials.expiresAt!!.time if (credentials.idToken != null) { val idToken = jwtDecoder.decode(credentials.idToken) - val idTokenExpiresAtDate = idToken.getExpiresAt() + val idTokenExpiresAtDate = idToken.expiresAt if (idTokenExpiresAtDate != null) { expiresAt = min(idTokenExpiresAtDate.time, expiresAt) } diff --git a/auth0/src/main/java/com/auth0/android/provider/OAuthManager.kt b/auth0/src/main/java/com/auth0/android/provider/OAuthManager.kt index 802f5a798..2522eb826 100644 --- a/auth0/src/main/java/com/auth0/android/provider/OAuthManager.kt +++ b/auth0/src/main/java/com/auth0/android/provider/OAuthManager.kt @@ -166,7 +166,7 @@ internal class OAuthManager( } } } - val tokenKeyId = decodedIdToken.getKeyId() + val tokenKeyId = decodedIdToken.keyId SignatureVerifier.forAsymmetricAlgorithm(tokenKeyId, apiClient, signatureVerifierCallback) } diff --git a/auth0/src/main/java/com/auth0/android/request/internal/Jwt.kt b/auth0/src/main/java/com/auth0/android/request/internal/Jwt.kt index 92483d92c..76386a37f 100644 --- a/auth0/src/main/java/com/auth0/android/request/internal/Jwt.kt +++ b/auth0/src/main/java/com/auth0/android/request/internal/Jwt.kt @@ -6,36 +6,28 @@ import com.google.gson.reflect.TypeToken import java.util.* +/** + * Internal class meant to decode the given token of type JWT and provide access to its claims. + */ internal class Jwt(rawToken: String) { - val parts: Array private val decodedHeader: Map private val decodedPayload: Map - - // TODO: Convert fun to properties. Assign once on the init method + val parts: Array // header - fun getType(): String = decodedHeader["typ"] as String - fun getAlgorithm(): String = decodedHeader["alg"] as String - fun getKeyId(): String? = decodedHeader["kid"] as String? + val algorithm: String + val keyId: String? // payload - fun getSubject(): String? = decodedPayload["sub"] as String? - fun getIssuer(): String? = decodedPayload["iss"] as String? - fun getNonce(): String? = decodedPayload["nonce"] as String? - fun getIssuedAt(): Date? = (decodedPayload["iat"] as? Double)?.let { Date(it.toLong() * 1000) } - fun getExpiresAt(): Date? = (decodedPayload["exp"] as? Double)?.let { Date(it.toLong() * 1000) } - fun getAuthorizedParty(): String? = decodedPayload["azp"] as String? - fun getAuthenticationTime(): Date? = - (decodedPayload["auth_time"] as? Double)?.let { Date(it.toLong() * 1000) } - - fun getAudience(): List { - return when (val aud = decodedPayload["aud"]) { - is String -> listOf(aud) - is List<*> -> aud as List - else -> emptyList() - } - } + val subject: String? + val issuer: String? + val nonce: String? + val issuedAt: Date? + val expiresAt: Date? + val authorizedParty: String? + val authenticationTime: Date? + val audience: List init { parts = splitToken(rawToken) @@ -44,6 +36,25 @@ internal class Jwt(rawToken: String) { val mapAdapter = GsonProvider.gson.getAdapter(object : TypeToken>() {}) decodedHeader = mapAdapter.fromJson(jsonHeader) decodedPayload = mapAdapter.fromJson(jsonPayload) + + // header claims + algorithm = decodedHeader["alg"] as String + keyId = decodedHeader["kid"] as String? + + // payload claims + subject = decodedPayload["sub"] as String? + issuer = decodedPayload["iss"] as String? + nonce = decodedPayload["nonce"] as String? + issuedAt = (decodedPayload["iat"] as? Double)?.let { Date(it.toLong() * 1000) } + expiresAt = (decodedPayload["exp"] as? Double)?.let { Date(it.toLong() * 1000) } + authorizedParty = decodedPayload["azp"] as String? + authenticationTime = + (decodedPayload["auth_time"] as? Double)?.let { Date(it.toLong() * 1000) } + audience = when (val aud = decodedPayload["aud"]) { + is String -> listOf(aud) + is List<*> -> aud as List + else -> emptyList() + } } private fun splitToken(token: String): Array { diff --git a/auth0/src/test/java/com/auth0/android/authentication/storage/CredentialsManagerTest.kt b/auth0/src/test/java/com/auth0/android/authentication/storage/CredentialsManagerTest.kt index 1be8657d5..3c28edb3d 100644 --- a/auth0/src/test/java/com/auth0/android/authentication/storage/CredentialsManagerTest.kt +++ b/auth0/src/test/java/com/auth0/android/authentication/storage/CredentialsManagerTest.kt @@ -356,7 +356,7 @@ public class CredentialsManagerTest { ).thenReturn(request) val newDate = Date(CredentialsMock.ONE_HOUR_AHEAD_MS + ONE_HOUR_SECONDS * 1000) val jwtMock = mock() - Mockito.`when`(jwtMock.getExpiresAt()).thenReturn(newDate) + Mockito.`when`(jwtMock.expiresAt).thenReturn(newDate) Mockito.`when`(jwtDecoder.decode("newId")).thenReturn(jwtMock) manager.getCredentials("some scope", 0, callback) verify(request).start( @@ -417,7 +417,7 @@ public class CredentialsManagerTest { ).thenReturn(request) val newDate = Date(CredentialsMock.ONE_HOUR_AHEAD_MS + ONE_HOUR_SECONDS * 1000) val jwtMock = mock() - Mockito.`when`(jwtMock.getExpiresAt()).thenReturn(newDate) + Mockito.`when`(jwtMock.expiresAt).thenReturn(newDate) Mockito.`when`(jwtDecoder.decode("newId")).thenReturn(jwtMock) manager.getCredentials("some scope", 0, callback) verify(request).start( @@ -479,7 +479,7 @@ public class CredentialsManagerTest { val newDate = Date(CredentialsMock.CURRENT_TIME_MS + 61 * 1000) // New token expires in minTTL + 1 second val jwtMock = mock() - Mockito.`when`(jwtMock.getExpiresAt()).thenReturn(newDate) + Mockito.`when`(jwtMock.expiresAt).thenReturn(newDate) Mockito.`when`(jwtDecoder.decode("newId")).thenReturn(jwtMock) manager.getCredentials(null, 60, callback) // 60 seconds of minTTL verify(request, never()) @@ -540,7 +540,7 @@ public class CredentialsManagerTest { ).thenReturn(request) val newDate = Date(CredentialsMock.ONE_HOUR_AHEAD_MS) val jwtMock = mock() - Mockito.`when`(jwtMock.getExpiresAt()).thenReturn(newDate) + Mockito.`when`(jwtMock.expiresAt).thenReturn(newDate) Mockito.`when`(jwtDecoder.decode("newId")).thenReturn(jwtMock) manager.getCredentials(callback) verify(request, never()) @@ -602,7 +602,7 @@ public class CredentialsManagerTest { val newDate = Date(CredentialsMock.CURRENT_TIME_MS + 59 * 1000) // New token expires in minTTL - 1 second val jwtMock = mock() - Mockito.`when`(jwtMock.getExpiresAt()).thenReturn(newDate) + Mockito.`when`(jwtMock.expiresAt).thenReturn(newDate) Mockito.`when`(jwtDecoder.decode("newId")).thenReturn(jwtMock) manager.getCredentials(null, 60, callback) // 60 seconds of minTTL verify(request, never()) @@ -655,7 +655,7 @@ public class CredentialsManagerTest { ).thenReturn(request) val newDate = Date(CredentialsMock.ONE_HOUR_AHEAD_MS) val jwtMock = mock() - Mockito.`when`(jwtMock.getExpiresAt()).thenReturn(newDate) + Mockito.`when`(jwtMock.expiresAt).thenReturn(newDate) Mockito.`when`(jwtDecoder.decode("newId")).thenReturn(jwtMock) manager.getCredentials(callback) verify(request, never()) @@ -864,7 +864,7 @@ public class CredentialsManagerTest { private fun prepareJwtDecoderMock(expiresAt: Date?) { val jwtMock = mock() - Mockito.`when`(jwtMock.getExpiresAt()).thenReturn(expiresAt) + Mockito.`when`(jwtMock.expiresAt).thenReturn(expiresAt) Mockito.`when`(jwtDecoder.decode("idToken")).thenReturn(jwtMock) } diff --git a/auth0/src/test/java/com/auth0/android/authentication/storage/JWTDecoderTest.java b/auth0/src/test/java/com/auth0/android/authentication/storage/JWTDecoderTest.java index 09495f461..dbe600608 100644 --- a/auth0/src/test/java/com/auth0/android/authentication/storage/JWTDecoderTest.java +++ b/auth0/src/test/java/com/auth0/android/authentication/storage/JWTDecoderTest.java @@ -14,17 +14,16 @@ public class JWTDecoderTest { @Test public void shouldDecodeAToken() { - String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibm9uY2UiOiJyZWFsbHkgcmFuZG9tIHRleHQiLCJpYXQiOjE1MTYyMzkwMjJ9.LQ7QuKKvXAHwc4_EaN4VkdTe5ElDrTlo48J8QuO5j0o"; + String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImFsaWNlIn0.eyJzdWIiOiIxMjM0NTY3ODkwIiwibm9uY2UiOiJyZWFsbHkgcmFuZG9tIHRleHQiLCJpYXQiOjE1MTYyMzkwMjJ9.rYG-HEs1EKKDhwQIoEg32_p-NQzNi5rB7akqGnH_q4k"; Jwt jwt1 = new Jwt(token); Jwt jwt2 = new JWTDecoder().decode(token); //Header claims assertThat(jwt1.getAlgorithm(), is("HS256")); - assertThat(jwt1.getType(), is("JWT")); - - assertThat(jwt2.getType(), is("JWT")); + assertThat(jwt1.getKeyId(), is("alice")); assertThat(jwt2.getAlgorithm(), is("HS256")); + assertThat(jwt2.getKeyId(), is("alice")); //Payload claims assertThat(jwt1.getSubject(), is("1234567890")); diff --git a/auth0/src/test/java/com/auth0/android/authentication/storage/SecureCredentialsManagerTest.kt b/auth0/src/test/java/com/auth0/android/authentication/storage/SecureCredentialsManagerTest.kt index efbfdf55e..2cf1e7c48 100644 --- a/auth0/src/test/java/com/auth0/android/authentication/storage/SecureCredentialsManagerTest.kt +++ b/auth0/src/test/java/com/auth0/android/authentication/storage/SecureCredentialsManagerTest.kt @@ -502,7 +502,7 @@ public class SecureCredentialsManagerTest { val newDate = Date(CredentialsMock.CURRENT_TIME_MS + 61 * 1000) // new token expires in minTTL + 1 seconds val jwtMock = mock() - Mockito.`when`(jwtMock.getExpiresAt()).thenReturn(newDate) + Mockito.`when`(jwtMock.expiresAt).thenReturn(newDate) Mockito.`when`(jwtDecoder.decode("newId")).thenReturn(jwtMock) Mockito.`when`( client.renewAuth("refreshToken") @@ -567,7 +567,7 @@ public class SecureCredentialsManagerTest { val newDate = Date(CredentialsMock.CURRENT_TIME_MS + 59 * 1000) // new token expires in minTTL - 1 seconds val jwtMock = mock() - Mockito.`when`(jwtMock.getExpiresAt()).thenReturn(newDate) + Mockito.`when`(jwtMock.expiresAt).thenReturn(newDate) Mockito.`when`(jwtDecoder.decode("newId")).thenReturn(jwtMock) Mockito.`when`( client.renewAuth("refreshToken") @@ -614,7 +614,7 @@ public class SecureCredentialsManagerTest { insertTestCredentials(false, true, true, expiresAt) // "scope" is set val newDate = Date(CredentialsMock.ONE_HOUR_AHEAD_MS + ONE_HOUR_SECONDS * 1000) val jwtMock = mock() - Mockito.`when`(jwtMock.getExpiresAt()).thenReturn(newDate) + Mockito.`when`(jwtMock.expiresAt).thenReturn(newDate) Mockito.`when`(jwtDecoder.decode("newId")).thenReturn(jwtMock) Mockito.`when`( client.renewAuth("refreshToken") @@ -679,7 +679,7 @@ public class SecureCredentialsManagerTest { insertTestCredentials(false, true, true, expiresAt) val newDate = Date(CredentialsMock.ONE_HOUR_AHEAD_MS) val jwtMock = mock() - Mockito.`when`(jwtMock.getExpiresAt()).thenReturn(newDate) + Mockito.`when`(jwtMock.expiresAt).thenReturn(newDate) Mockito.`when`(jwtDecoder.decode("newId")).thenReturn(jwtMock) Mockito.`when`( client.renewAuth("refreshToken") @@ -758,7 +758,7 @@ public class SecureCredentialsManagerTest { insertTestCredentials(false, true, true, expiresAt) val newDate = Date(CredentialsMock.ONE_HOUR_AHEAD_MS) val jwtMock = mock() - Mockito.`when`(jwtMock.getExpiresAt()).thenReturn(newDate) + Mockito.`when`(jwtMock.expiresAt).thenReturn(newDate) Mockito.`when`(jwtDecoder.decode("newId")).thenReturn(jwtMock) Mockito.`when`( client.renewAuth("refreshToken") @@ -823,7 +823,7 @@ public class SecureCredentialsManagerTest { insertTestCredentials(false, true, true, expiresAt) val newDate = Date(CredentialsMock.ONE_HOUR_AHEAD_MS) val jwtMock = mock() - Mockito.`when`(jwtMock.getExpiresAt()).thenReturn(newDate) + Mockito.`when`(jwtMock.expiresAt).thenReturn(newDate) Mockito.`when`(jwtDecoder.decode("newId")).thenReturn(jwtMock) Mockito.`when`( client.renewAuth("refreshToken") @@ -1363,7 +1363,7 @@ public class SecureCredentialsManagerTest { private fun prepareJwtDecoderMock(expiresAt: Date?) { val jwtMock = mock() - Mockito.`when`(jwtMock.getExpiresAt()).thenReturn(expiresAt) + Mockito.`when`(jwtMock.expiresAt).thenReturn(expiresAt) Mockito.`when`(jwtDecoder.decode("idToken")).thenReturn(jwtMock) } diff --git a/auth0/src/test/java/com/auth0/android/request/internal/JwtTest.kt b/auth0/src/test/java/com/auth0/android/request/internal/JwtTest.kt index 9bb110e35..06dd81f9b 100644 --- a/auth0/src/test/java/com/auth0/android/request/internal/JwtTest.kt +++ b/auth0/src/test/java/com/auth0/android/request/internal/JwtTest.kt @@ -83,7 +83,7 @@ public class JwtTest { val jwt = Jwt("eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJKb2huIERvZSJ9.SgXosfRR_IwCgHq5lF3tlM-JHtpucWCRSaVuoHTbWbQ"); assertThat(jwt, `is`(notNullValue())) - assertThat(jwt.getIssuer(), `is`("John Doe")) + assertThat(jwt.issuer, `is`("John Doe")) } @Test @@ -91,7 +91,7 @@ public class JwtTest { val jwt = Jwt("eyJhbGciOiJIUzI1NiJ9.e30.something"); assertThat(jwt, `is`(notNullValue())) - assertThat(jwt.getIssuer(), `is`(nullValue())) + assertThat(jwt.issuer, `is`(nullValue())) } @Test @@ -99,7 +99,7 @@ public class JwtTest { val jwt = Jwt("eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJUb2szbnMifQ.RudAxkslimoOY3BLl2Ghny3BrUKu9I1ZrXzCZGDJtNs"); assertThat(jwt, `is`(notNullValue())) - assertThat(jwt.getSubject(), `is`("Tok3ns")) + assertThat(jwt.subject, `is`("Tok3ns")) } @Test @@ -107,7 +107,7 @@ public class JwtTest { val jwt = Jwt("eyJhbGciOiJIUzI1NiJ9.e30.something"); assertThat(jwt, `is`(notNullValue())) - assertThat(jwt.getSubject(), `is`(nullValue())) + assertThat(jwt.subject, `is`(nullValue())) } @Test @@ -115,8 +115,8 @@ public class JwtTest { val jwt = Jwt("eyJhbGciOiJIUzI1NiJ9.eyJhdWQiOlsiSG9wZSIsIlRyYXZpcyIsIlNvbG9tb24iXX0.Tm4W8WnfPjlmHSmKFakdij0on2rWPETpoM7Sh0u6-S4"); assertThat(jwt, `is`(notNullValue())) - assertThat(jwt.getAudience(), `is`(hasSize(3))) - assertThat(jwt.getAudience(), `is`(hasItems("Hope", "Travis", "Solomon"))) + assertThat(jwt.audience, `is`(hasSize(3))) + assertThat(jwt.audience, `is`(hasItems("Hope", "Travis", "Solomon"))) } @Test @@ -124,8 +124,8 @@ public class JwtTest { val jwt = Jwt("eyJhbGciOiJIUzI1NiJ9.eyJhdWQiOiJKYWNrIFJleWVzIn0.a4I9BBhPt1OB1GW67g2P1bEHgi6zgOjGUL4LvhE9Dgc"); assertThat(jwt, `is`(notNullValue())) - assertThat(jwt.getAudience(), `is`(hasSize(1))) - assertThat(jwt.getAudience(), `is`(hasItems("Jack Reyes"))) + assertThat(jwt.audience, `is`(hasSize(1))) + assertThat(jwt.audience, `is`(hasItems("Jack Reyes"))) } @Test @@ -133,7 +133,7 @@ public class JwtTest { val jwt = Jwt("eyJhbGciOiJIUzI1NiJ9.e30.something"); assertThat(jwt, `is`(notNullValue())) - assertThat(jwt.getAudience(), IsEmptyCollection.empty()) + assertThat(jwt.audience, IsEmptyCollection.empty()) } @Test @@ -144,9 +144,9 @@ public class JwtTest { val secs = Integer.MAX_VALUE + 10000L; val expectedDate = Date(secs * 1000); - assertThat(jwt.getIssuedAt(), `is`(expectedDate)) - assertThat(jwt.getExpiresAt(), `is`(expectedDate)) - assertThat(jwt.getAuthenticationTime(), `is`(expectedDate)) + assertThat(jwt.issuedAt, `is`(expectedDate)) + assertThat(jwt.expiresAt, `is`(expectedDate)) + assertThat(jwt.authenticationTime, `is`(expectedDate)) } @Test @@ -154,11 +154,11 @@ public class JwtTest { val jwt = Jwt("eyJhbGciOiJIUzI1NiJ9.eyJleHAiOjE0NzY3MjcwODZ9.HF2RrW-0L0nTIgiM8Ov7MWabIEZl4PQs07E43BphnXw"); assertThat(jwt, `is`(notNullValue())) - assertThat(jwt.getExpiresAt(), `is`(instanceOf(Date::class.java))) + assertThat(jwt.expiresAt, `is`(instanceOf(Date::class.java))) val ms = 1476727086L * 1000; val expectedDate = Date(ms) - assertThat(jwt.getExpiresAt(), `is`(notNullValue())) - assertThat(jwt.getExpiresAt(), `is`(equalTo(expectedDate))) + assertThat(jwt.expiresAt, `is`(notNullValue())) + assertThat(jwt.expiresAt, `is`(equalTo(expectedDate))) } @Test @@ -166,7 +166,7 @@ public class JwtTest { val jwt = Jwt("eyJhbGciOiJIUzI1NiJ9.e30.something"); assertThat(jwt, `is`(notNullValue())) - assertThat(jwt.getExpiresAt(), `is`(nullValue())) + assertThat(jwt.expiresAt, `is`(nullValue())) } @Test @@ -175,12 +175,12 @@ public class JwtTest { Jwt("eyJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE0NzY3MjcwODZ9.HcNvTtoB-Wj4KUqsl9y2u2f2Ve2JrlL1X4xIPwGNy68"); assertThat(jwt, `is`(notNullValue())) assertThat( - jwt.getIssuedAt(), `is`(instanceOf(Date::class.java)) + jwt.issuedAt, `is`(instanceOf(Date::class.java)) ) val ms = 1476727086L * 1000; val expectedDate = Date(ms); - assertThat(jwt.getIssuedAt(), `is`(notNullValue())) - assertThat(jwt.getIssuedAt(), `is`(equalTo(expectedDate))) + assertThat(jwt.issuedAt, `is`(notNullValue())) + assertThat(jwt.issuedAt, `is`(equalTo(expectedDate))) } @Test @@ -188,7 +188,7 @@ public class JwtTest { val jwt = Jwt("eyJhbGciOiJIUzI1NiJ9.e30.something"); assertThat(jwt, `is`(notNullValue())) - assertThat(jwt.getIssuedAt(), `is`(nullValue())) + assertThat(jwt.issuedAt, `is`(nullValue())) } //Helper Methods From 577bdf537ca2c36ebf510839568dc34569bad353 Mon Sep 17 00:00:00 2001 From: Luciano Balmaceda Date: Tue, 19 Jan 2021 19:49:50 +0100 Subject: [PATCH 5/5] allow to pass a cause --- .../main/java/com/auth0/android/provider/OAuthManager.kt | 4 ++-- .../com/auth0/android/provider/TokenValidationException.kt | 6 +++++- .../src/main/java/com/auth0/android/request/internal/Jwt.kt | 3 +-- .../test/java/com/auth0/android/request/internal/JwtTest.kt | 4 ++-- 4 files changed, 10 insertions(+), 7 deletions(-) diff --git a/auth0/src/main/java/com/auth0/android/provider/OAuthManager.kt b/auth0/src/main/java/com/auth0/android/provider/OAuthManager.kt index 2522eb826..91dfc48c3 100644 --- a/auth0/src/main/java/com/auth0/android/provider/OAuthManager.kt +++ b/auth0/src/main/java/com/auth0/android/provider/OAuthManager.kt @@ -135,8 +135,8 @@ internal class OAuthManager( } val decodedIdToken: Jwt = try { Jwt(idToken!!) - } catch (ignored: Exception) { - validationCallback.onFailure(TokenValidationException("ID token could not be decoded")) + } catch (error: Exception) { + validationCallback.onFailure(TokenValidationException("ID token could not be decoded", error)) return } val signatureVerifierCallback: Callback = diff --git a/auth0/src/main/java/com/auth0/android/provider/TokenValidationException.kt b/auth0/src/main/java/com/auth0/android/provider/TokenValidationException.kt index fbfc8c588..c1449ac3d 100644 --- a/auth0/src/main/java/com/auth0/android/provider/TokenValidationException.kt +++ b/auth0/src/main/java/com/auth0/android/provider/TokenValidationException.kt @@ -5,4 +5,8 @@ import com.auth0.android.Auth0Exception /** * Exception thrown when the validation of the ID token failed. */ -internal class TokenValidationException(message: String) : Auth0Exception(message) \ No newline at end of file +internal class TokenValidationException @JvmOverloads constructor( + message: String, + cause: Throwable? = null +) : + Auth0Exception(message, cause) \ No newline at end of file diff --git a/auth0/src/main/java/com/auth0/android/request/internal/Jwt.kt b/auth0/src/main/java/com/auth0/android/request/internal/Jwt.kt index 76386a37f..39f51cc4c 100644 --- a/auth0/src/main/java/com/auth0/android/request/internal/Jwt.kt +++ b/auth0/src/main/java/com/auth0/android/request/internal/Jwt.kt @@ -1,7 +1,6 @@ package com.auth0.android.request.internal import android.util.Base64 -import com.auth0.android.provider.TokenValidationException import com.google.gson.reflect.TypeToken import java.util.* @@ -64,7 +63,7 @@ internal class Jwt(rawToken: String) { parts = arrayOf(parts[0], parts[1], "") } if (parts.size != 3) { - throw TokenValidationException( + throw IllegalArgumentException( String.format( "The token was expected to have 3 parts, but got %s.", parts.size diff --git a/auth0/src/test/java/com/auth0/android/request/internal/JwtTest.kt b/auth0/src/test/java/com/auth0/android/request/internal/JwtTest.kt index 06dd81f9b..3ce5b0fbb 100644 --- a/auth0/src/test/java/com/auth0/android/request/internal/JwtTest.kt +++ b/auth0/src/test/java/com/auth0/android/request/internal/JwtTest.kt @@ -21,7 +21,7 @@ public class JwtTest { public fun shouldThrowIfLessThan3Parts() { Assert.assertThrows( "The token was expected to have 3 parts, but got 2.", - TokenValidationException::class.java + IllegalArgumentException::class.java ) { Jwt("two.parts") } @@ -31,7 +31,7 @@ public class JwtTest { public fun shouldThrowIfMoreThan3Parts() { Assert.assertThrows( "The token was expected to have 3 parts, but got 4.", - TokenValidationException::class.java + IllegalArgumentException::class.java ) { Jwt("this.has.four.parts") }