Skip to content

Commit

Permalink
Merge branch 'main' into avoid-null-pointer-exception
Browse files Browse the repository at this point in the history
  • Loading branch information
poovamraj authored Jul 17, 2023
2 parents 1e26583 + 81b516e commit 28d6aaa
Show file tree
Hide file tree
Showing 11 changed files with 302 additions and 20 deletions.
2 changes: 1 addition & 1 deletion EXAMPLES.md
Original file line number Diff line number Diff line change
Expand Up @@ -993,7 +993,7 @@ Note that Organizations is currently only available to customers on our Enterpri

```kotlin
WebAuthProvider.login(account)
.withOrganization(organizationId)
.withOrganization(organizationIdOrName)
.start(this, callback)
```

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.auth0.android.authentication.storage

import com.auth0.android.Auth0Exception
import com.auth0.android.result.Credentials

/**
* Represents an error raised by the [CredentialsManager].
Expand All @@ -18,4 +19,13 @@ public class CredentialsManagerException internal constructor(
*/
public val isDeviceIncompatible: Boolean
get() = cause is IncompatibleDeviceException

/**
* Returns the refreshed [Credentials] if exception is thrown right before saving them.
* This will avoid users being logged out unnecessarily and allows to handle failure case as needed
*
* Set incase [IncompatibleDeviceException] or [CryptoException] is thrown while saving the refreshed [Credentials]
*/
public var refreshedCredentials: Credentials? = null
internal set
}
Original file line number Diff line number Diff line change
Expand Up @@ -532,6 +532,7 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT
request.addParameter("scope", scope)
}

val freshCredentials: Credentials
try {
val fresh = request.execute()
val expiresAt = fresh.expiresAt.time
Expand All @@ -554,17 +555,14 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT
//non-empty refresh token for refresh token rotation scenarios
val updatedRefreshToken =
if (TextUtils.isEmpty(fresh.refreshToken)) credentials.refreshToken else fresh.refreshToken
val refreshed = Credentials(
freshCredentials = Credentials(
fresh.idToken,
fresh.accessToken,
fresh.type,
updatedRefreshToken,
fresh.expiresAt,
fresh.scope
)
saveCredentials(refreshed)
callback.onSuccess(refreshed)
decryptCallback = null
} catch (error: Auth0Exception) {
callback.onFailure(
CredentialsManagerException(
Expand All @@ -573,7 +571,21 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT
)
)
decryptCallback = null
return@execute
}

try {
saveCredentials(freshCredentials)
callback.onSuccess(freshCredentials)
} catch (error: CredentialsManagerException) {
val exception = CredentialsManagerException(
"An error occurred while saving the refreshed Credentials.", error)
if(error.cause is IncompatibleDeviceException || error.cause is CryptoException) {
exception.refreshedCredentials = freshCredentials
}
callback.onFailure(exception)
}
decryptCallback = null
}
}

Expand Down
24 changes: 17 additions & 7 deletions auth0/src/main/java/com/auth0/android/provider/IdTokenVerifier.kt
Original file line number Diff line number Diff line change
Expand Up @@ -59,13 +59,23 @@ internal class IdTokenVerifier {
throw NonceClaimMismatchException(verifyOptions.nonce, nonceClaim)
}
}
if (verifyOptions.organization != null) {
val orgClaim = token.organizationId
if (TextUtils.isEmpty(orgClaim)) {
throw OrgClaimMissingException()
}
if (verifyOptions.organization != orgClaim) {
throw OrgClaimMismatchException(verifyOptions.organization, orgClaim)
verifyOptions.organization?.let {organizationInput ->
if(organizationInput.startsWith("org_")) {
val orgClaim = token.organizationId
if (TextUtils.isEmpty(orgClaim)) {
throw OrgClaimMissingException()
}
if (organizationInput != orgClaim) {
throw OrgClaimMismatchException(organizationInput, orgClaim)
}
} else {
val orgNameClaim = token.organizationName
if (TextUtils.isEmpty(orgNameClaim)) {
throw OrgNameClaimMissingException()
}
if (!organizationInput.equals(orgNameClaim, true)) {
throw OrgNameClaimMismatchException(organizationInput, orgNameClaim)
}
}
}
if (audience.size > 1) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,43 @@ public class OrgClaimMismatchException internal constructor(expected: String?, r
}
}

/**
* This Exception is thrown when Organization Name (org_name) claim is missing in the ID Token
*/
public class OrgNameClaimMissingException internal constructor() : TokenValidationException(MESSAGE) {
private companion object {
private const val MESSAGE = "Organization Name (org_name) claim must be a string present in the ID token"
}

/**
* To avoid backward compatibility issue, we still have the toString conversion similar to the
* old [TokenValidationException] that was thrown
*/
override fun toString(): String {
return "${this.javaClass.superclass.name}: $message"
}
}

/**
* This Exception is thrown when the Organization Name (org_name) claim found in the ID token is not the
* one that was expected
*/
public class OrgNameClaimMismatchException internal constructor(expected: String?, received: String?) :
TokenValidationException(message(expected, received)) {
private companion object {
private fun message(expected: String?, received: String?): String =
"Organization Name (org_name) claim mismatch in the ID token; expected \"$expected\", found \"$received\""
}

/**
* To avoid backward compatibility issue, we still have the toString conversion similar to the
* old [TokenValidationException] that was thrown
*/
override fun toString(): String {
return "${this.javaClass.superclass.name}: $message"
}
}

/**
* This Exception is thrown when Authorized Party (azp) claim is missing in the ID Token
*/
Expand Down
2 changes: 2 additions & 0 deletions auth0/src/main/java/com/auth0/android/request/internal/Jwt.kt
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ internal class Jwt(rawToken: String) {
val issuer: String?
val nonce: String?
val organizationId: String?
val organizationName: String?
val issuedAt: Date?
val expiresAt: Date?
val authorizedParty: String?
Expand All @@ -46,6 +47,7 @@ internal class Jwt(rawToken: String) {
issuer = decodedPayload["iss"] as String?
nonce = decodedPayload["nonce"] as String?
organizationId = decodedPayload["org_id"] as String?
organizationName = decodedPayload["org_name"] 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?
Expand Down
4 changes: 4 additions & 0 deletions auth0/src/main/java/com/auth0/android/result/Credentials.kt
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,10 @@ public open class Credentials(
public var recoveryCode: String? = null
internal set

override fun toString(): String {
return "Credentials(idToken='xxxxx', accessToken='xxxxx', type='$type', refreshToken='xxxxx', expiresAt='$expiresAt', scope='$scope')"
}

public val user: UserProfile get() {
val (_, payload) = Jwt.splitToken(idToken)
val gson = GsonProvider.gson
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ import java.util.*
import java.util.concurrent.Executor
import org.junit.Assert.assertThrows
import org.junit.Assert.assertTrue
import java.lang.Exception

@RunWith(RobolectricTestRunner::class)
public class SecureCredentialsManagerTest {
Expand Down Expand Up @@ -417,6 +418,124 @@ public class SecureCredentialsManagerTest {
verify(storage, never()).remove("com.auth0.credentials_can_refresh")
}

@Test
public fun shouldFailOnSavingRefreshedCredentialsInGetCredentialsWhenCryptoExceptionIsThrown() {
val expiresAt = Date(CredentialsMock.ONE_HOUR_AHEAD_MS) // non expired credentials
insertTestCredentials(false, true, true, expiresAt, "scope") // "scope" is set
val newDate = Date(CredentialsMock.ONE_HOUR_AHEAD_MS + ONE_HOUR_SECONDS * 1000)
val jwtMock = mock<Jwt>()
Mockito.`when`(jwtMock.expiresAt).thenReturn(newDate)
Mockito.`when`(jwtDecoder.decode("newId")).thenReturn(jwtMock)
Mockito.`when`(
client.renewAuth("refreshToken")
).thenReturn(request)

// Trigger success
val expectedCredentials =
Credentials("newId", "newAccess", "newType", "refreshToken", newDate, "different scope")
Mockito.`when`(request.execute()).thenReturn(expectedCredentials)

val expectedJson = gson.toJson(expectedCredentials)
Mockito.`when`(crypto.encrypt(expectedJson.toByteArray()))
.thenThrow(CryptoException("CryptoException is thrown"))
manager.getCredentials("different scope", 0, callback) // minTTL of 0 seconds (default)
verify(request)
.addParameter(eq("scope"), eq("different scope"))
verify(callback).onFailure(
exceptionCaptor.capture()
)

// Verify the returned credentials are the latest
val exception = exceptionCaptor.firstValue
val retrievedCredentials = exception.refreshedCredentials
MatcherAssert.assertThat(exception, Is.`is`(Matchers.notNullValue()))
MatcherAssert.assertThat(exception.message, Is.`is`("An error occurred while saving the refreshed Credentials."))
MatcherAssert.assertThat(exception.cause!!.message, Is.`is`("A change on the Lock Screen security settings have deemed the encryption keys invalid and have been recreated. Please try saving the credentials again."))
MatcherAssert.assertThat(retrievedCredentials, Is.`is`(Matchers.notNullValue()))
MatcherAssert.assertThat(retrievedCredentials!!.idToken, Is.`is`("newId"))
MatcherAssert.assertThat(retrievedCredentials.accessToken, Is.`is`("newAccess"))
MatcherAssert.assertThat(retrievedCredentials.type, Is.`is`("newType"))
MatcherAssert.assertThat(retrievedCredentials.refreshToken, Is.`is`("refreshToken"))
MatcherAssert.assertThat(retrievedCredentials.expiresAt, Is.`is`(newDate))
MatcherAssert.assertThat(retrievedCredentials.scope, Is.`is`("different scope"))
}

@Test
public fun shouldFailOnSavingRefreshedCredentialsInGetCredentialsWhenIncompatibleDeviceExceptionIsThrown() {
val expiresAt = Date(CredentialsMock.ONE_HOUR_AHEAD_MS) // non expired credentials
insertTestCredentials(false, true, true, expiresAt, "scope") // "scope" is set
val newDate = Date(CredentialsMock.ONE_HOUR_AHEAD_MS + ONE_HOUR_SECONDS * 1000)
val jwtMock = mock<Jwt>()
Mockito.`when`(jwtMock.expiresAt).thenReturn(newDate)
Mockito.`when`(jwtDecoder.decode("newId")).thenReturn(jwtMock)
Mockito.`when`(
client.renewAuth("refreshToken")
).thenReturn(request)

// Trigger success
val expectedCredentials =
Credentials("newId", "newAccess", "newType", "refreshToken", newDate, "different scope")
Mockito.`when`(request.execute()).thenReturn(expectedCredentials)

val expectedJson = gson.toJson(expectedCredentials)
Mockito.`when`(crypto.encrypt(expectedJson.toByteArray()))
.thenThrow(IncompatibleDeviceException(Exception()))
manager.getCredentials("different scope", 0, callback) // minTTL of 0 seconds (default)
verify(request)
.addParameter(eq("scope"), eq("different scope"))
verify(callback).onFailure(
exceptionCaptor.capture()
)

// Verify the returned credentials are the latest
val exception = exceptionCaptor.firstValue
val retrievedCredentials = exception.refreshedCredentials
MatcherAssert.assertThat(exception, Is.`is`(Matchers.notNullValue()))
MatcherAssert.assertThat(exception.message, Is.`is`("An error occurred while saving the refreshed Credentials."))
MatcherAssert.assertThat(exception.cause!!.message, Is.`is`("This device is not compatible with the SecureCredentialsManager class."))
MatcherAssert.assertThat(retrievedCredentials, Is.`is`(Matchers.notNullValue()))
MatcherAssert.assertThat(retrievedCredentials!!.idToken, Is.`is`("newId"))
MatcherAssert.assertThat(retrievedCredentials.accessToken, Is.`is`("newAccess"))
MatcherAssert.assertThat(retrievedCredentials.type, Is.`is`("newType"))
MatcherAssert.assertThat(retrievedCredentials.refreshToken, Is.`is`("refreshToken"))
MatcherAssert.assertThat(retrievedCredentials.expiresAt, Is.`is`(newDate))
MatcherAssert.assertThat(retrievedCredentials.scope, Is.`is`("different scope"))
}


@Test
public fun shouldFailWithoutRefreshedCredentialsInExceptionOnSavingRefreshedCredentialsInGetCredentialsWhenDifferentExceptionIsThrown() {
val expiresAt = Date(CredentialsMock.ONE_HOUR_AHEAD_MS) // non expired credentials
insertTestCredentials(false, true, true, expiresAt, "scope") // "scope" is set
val newDate = Date(CredentialsMock.ONE_HOUR_AHEAD_MS + ONE_HOUR_SECONDS * 1000)
val jwtMock = mock<Jwt>()
Mockito.`when`(jwtMock.expiresAt).thenReturn(newDate)
Mockito.`when`(jwtDecoder.decode("newId")).thenReturn(jwtMock)
Mockito.`when`(
client.renewAuth("refreshToken")
).thenReturn(request)

// Trigger success
val expectedCredentials =
Credentials("", "", "newType", "refreshToken", newDate, "different scope")
Mockito.`when`(request.execute()).thenReturn(expectedCredentials)

manager.getCredentials("different scope", 0, callback) // minTTL of 0 seconds (default)
verify(request)
.addParameter(eq("scope"), eq("different scope"))
verify(callback).onFailure(
exceptionCaptor.capture()
)

// Verify the returned credentials are the latest
val exception = exceptionCaptor.firstValue
val retrievedCredentials = exception.refreshedCredentials
MatcherAssert.assertThat(exception, Is.`is`(Matchers.notNullValue()))
MatcherAssert.assertThat(exception.message, Is.`is`("An error occurred while saving the refreshed Credentials."))
MatcherAssert.assertThat(exception.cause!!.message, Is.`is`("Credentials must have a valid date of expiration and a valid access_token or id_token value."))
MatcherAssert.assertThat(retrievedCredentials, Is.`is`(Matchers.nullValue()))
}

@Test
public fun shouldFailOnGetCredentialsWhenNoAccessTokenOrIdTokenWasSaved() {
verifyNoMoreInteractions(client)
Expand Down
Loading

0 comments on commit 28d6aaa

Please sign in to comment.