Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,13 @@ public class AuthenticationAPIClient @VisibleForTesting(otherwise = VisibleForTe

private var dPoP: DPoP? = null

/**
* Returns whether DPoP (Demonstrating Proof of Possession) is enabled on this client.
* DPoP is enabled by calling [useDPoP].
*/
public val isDPoPEnabled: Boolean
get() = dPoP != null

/**
* Creates a new API client instance providing Auth0 account info.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import android.util.Log
import androidx.annotation.VisibleForTesting
import com.auth0.android.authentication.AuthenticationAPIClient
import com.auth0.android.callback.Callback
import com.auth0.android.dpop.DPoPException
import com.auth0.android.dpop.DPoPUtil
import com.auth0.android.result.APICredentials
import com.auth0.android.result.Credentials
import com.auth0.android.result.SSOCredentials
Expand All @@ -20,6 +22,10 @@ public abstract class BaseCredentialsManager internal constructor(
protected val storage: Storage,
private val jwtDecoder: JWTDecoder
) {

internal companion object {
internal const val KEY_DPOP_THUMBPRINT = "com.auth0.dpop_key_thumbprint"
}
private var _clock: Clock = ClockImpl()

/**
Expand Down Expand Up @@ -155,6 +161,29 @@ public abstract class BaseCredentialsManager internal constructor(
internal val currentTimeInMillis: Long
get() = _clock.getCurrentTimeMillis()

/**
* Stores the DPoP key thumbprint if DPoP was used for this credential set.
* Uses a dual strategy to store the thumbprint:
* - credentials.type == "DPoP" when server confirms DPoP but client lacks useDPoP()
* - isDPoPEnabled catches the case where client used DPoP, server returned token_type: "Bearer"
*/
protected fun saveDPoPThumbprint(credentials: Credentials) {
val dpopUsed = credentials.type.equals("DPoP", ignoreCase = true)
|| authenticationClient.isDPoPEnabled
if (dpopUsed && DPoPUtil.hasKeyPair()) {
try {
val thumbprint = DPoPUtil.getPublicKeyJWK()
if (thumbprint != null) {
storage.store(KEY_DPOP_THUMBPRINT, thumbprint)
Comment thread
pmathew92 marked this conversation as resolved.
Outdated
}
} catch (e: DPoPException) {
Log.w(this::class.java.simpleName, "Failed to store DPoP key thumbprint", e)
Comment thread
pmathew92 marked this conversation as resolved.
Outdated
Comment thread
pmathew92 marked this conversation as resolved.
Outdated
}
} else {
storage.remove(KEY_DPOP_THUMBPRINT)
}
}
Comment thread
pmathew92 marked this conversation as resolved.

/**
* Checks if the stored scope is the same as the requested one.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ public class CredentialsManager @VisibleForTesting(otherwise = VisibleForTesting
storage.store(KEY_EXPIRES_AT, credentials.expiresAt.time)
storage.store(KEY_SCOPE, credentials.scope)
storage.store(LEGACY_KEY_CACHE_EXPIRES_AT, credentials.expiresAt.time)
saveDPoPThumbprint(credentials)
}

/**
Expand Down Expand Up @@ -714,6 +715,7 @@ public class CredentialsManager @VisibleForTesting(otherwise = VisibleForTesting
storage.remove(KEY_EXPIRES_AT)
storage.remove(KEY_SCOPE)
storage.remove(LEGACY_KEY_CACHE_EXPIRES_AT)
storage.remove(KEY_DPOP_THUMBPRINT)
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ public class CredentialsManagerException :
API_ERROR,
SSO_EXCHANGE_FAILED,
MFA_REQUIRED,
DPOP_KEY_MISSING,
DPOP_NOT_CONFIGURED,
UNKNOWN_ERROR
}

Expand Down Expand Up @@ -159,6 +161,11 @@ public class CredentialsManagerException :
public val MFA_REQUIRED: CredentialsManagerException =
CredentialsManagerException(Code.MFA_REQUIRED)

public val DPOP_KEY_MISSING: CredentialsManagerException =
CredentialsManagerException(Code.DPOP_KEY_MISSING)
public val DPOP_NOT_CONFIGURED: CredentialsManagerException =
CredentialsManagerException(Code.DPOP_NOT_CONFIGURED)

public val UNKNOWN_ERROR: CredentialsManagerException = CredentialsManagerException(Code.UNKNOWN_ERROR)


Expand Down Expand Up @@ -207,6 +214,8 @@ public class CredentialsManagerException :
Code.API_ERROR -> "An error occurred while processing the request."
Code.SSO_EXCHANGE_FAILED ->"The exchange of the refresh token for SSO credentials failed."
Code.MFA_REQUIRED -> "Multi-factor authentication is required to complete the credential renewal."
Code.DPOP_KEY_MISSING -> "The stored credentials are DPoP-bound but the DPoP key pair is no longer available in the Android KeyStore. Re-authentication is required."
Code.DPOP_NOT_CONFIGURED -> "The stored credentials are DPoP-bound but the AuthenticationAPIClient used by this CredentialsManager was not configured with useDPoP(context). Call AuthenticationAPIClient(auth0).useDPoP(context) and pass the configured client to CredentialsManager."
Comment thread
pmathew92 marked this conversation as resolved.
Outdated
Code.UNKNOWN_ERROR -> "An unknown error has occurred while fetching the token. Please check the error cause for more details."
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,8 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT
)
storage.store(LEGACY_KEY_CACHE_EXPIRES_AT, credentials.expiresAt.time)
storage.store(KEY_CAN_REFRESH, canRefresh)
storage.store(KEY_TOKEN_TYPE, credentials.type)
saveDPoPThumbprint(credentials)
} catch (e: IncompatibleDeviceException) {
throw CredentialsManagerException(
CredentialsManagerException.Code.INCOMPATIBLE_DEVICE, e
Expand Down Expand Up @@ -735,6 +737,8 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT
storage.remove(KEY_EXPIRES_AT)
storage.remove(LEGACY_KEY_CACHE_EXPIRES_AT)
storage.remove(KEY_CAN_REFRESH)
storage.remove(KEY_TOKEN_TYPE)
storage.remove(KEY_DPOP_THUMBPRINT)
clearBiometricSession()
Log.d(TAG, "Credentials were just removed from the storage")
}
Expand Down Expand Up @@ -1251,6 +1255,9 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
internal const val KEY_ALIAS = "com.auth0.key"

@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
internal const val KEY_TOKEN_TYPE = "com.auth0.token_type"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: KEY_TOKEN_TYPE with the same value "com.auth0.token_type" already exists as a private const in CredentialsManager (line 764). Now we have it in two places — might drift. Since KEY_DPOP_THUMBPRINT was placed in BaseCredentialsManager for sharing, can we do the same for KEY_TOKEN_TYPE?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done


// Using NO_SESSION to represent "no session" (uninitialized state)
private const val NO_SESSION = -1L
}
Expand Down
23 changes: 23 additions & 0 deletions auth0/src/main/java/com/auth0/android/dpop/DPoP.kt
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,29 @@ public class DPoP(context: Context) {
return HeaderData(token, proof)
}

/**
* Returns whether a DPoP key pair currently exists in the Android KeyStore.
*
* This can be used to check if DPoP credentials are still available after events
* like device backup/restore or factory reset, which do not preserve KeyStore entries.
*
* ```kotlin
*
* if (!DPoP.hasKeyPair()) {
* // Key was lost — clear stored credentials and re-authenticate
* }
*
* ```
*
* @return true if a DPoP key pair exists in the KeyStore, false otherwise.
* @throws DPoPException if there is an error accessing the KeyStore.
*/
@Throws(DPoPException::class)
@JvmStatic
public fun hasKeyPair(): Boolean {
return DPoPUtil.hasKeyPair()
}
Comment thread
pmathew92 marked this conversation as resolved.

/**
* Method to clear the DPoP key pair from the keystore. It must be called when the user logs out
* to prevent reuse of the key pair in subsequent sessions.
Expand Down
Loading