Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2022 - 2025 Ping Identity Corporation. All rights reserved.
* Copyright (c) 2022 - 2026 Ping Identity Corporation. All rights reserved.
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
Expand All @@ -9,6 +9,7 @@ package org.forgerock.android.auth.callback
import android.os.OperationCanceledException
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.TimeoutCancellationException
import org.forgerock.android.auth.DEFAULT_AUTHENTICATION_VALIDITY_DURATION
import org.forgerock.android.auth.Logger
import org.forgerock.android.auth.devicebind.DeviceAuthenticator
import org.forgerock.android.auth.devicebind.DeviceBindingErrorStatus.Abort
Expand All @@ -22,6 +23,7 @@ import kotlin.time.toDuration

private val TAG = Binding::class.java.simpleName


/**
* Device Binding interface to provide utility method for [DeviceBindingCallback] and [DeviceSigningVerifierCallback]
*/
Expand Down Expand Up @@ -83,6 +85,16 @@ interface Binding {

fun setClientError(clientError: String?)

/**
* Validates that [authenticationValidityDuration] is greater than 0.
* The Android Keystore treats 0 and -1 as special values that disable the time-based window
* and require fresh user authentication on every single key use.
* @throws IllegalArgumentException if [value] is not greater than 0
*/
fun validateAuthenticationValidityDuration(value: Int) {
require(value > 0) { "authenticationValidityDuration must be greater than 0" }
}

/**
* Default function to identify [DeviceAuthenticator]
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2022 - 2025 Ping Identity Corporation. All rights reserved.
* Copyright (c) 2022 - 2026 Ping Identity Corporation. All rights reserved.
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
Expand All @@ -9,6 +9,7 @@ package org.forgerock.android.auth.callback
import android.content.Context
import android.util.Base64
import androidx.annotation.Keep
import org.forgerock.android.auth.DEFAULT_AUTHENTICATION_VALIDITY_DURATION
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
Expand Down Expand Up @@ -104,6 +105,26 @@ open class DeviceBindingCallback : AbstractCallback, Binding {
lateinit var attestation: Attestation
private set

/**
* The duration in seconds for which the generated key remains valid for user authentication
* without requiring a fresh biometric/credential prompt. Only affects biometric-backed key
* types ([DeviceBindingAuthenticationType.BIOMETRIC_ONLY] and
* [DeviceBindingAuthenticationType.BIOMETRIC_ALLOW_FALLBACK]); ignored for [DeviceBindingAuthenticationType.NONE]
* and [DeviceBindingAuthenticationType.APPLICATION_PIN].
*
* Must be greater than 0. The Android Keystore treats 0 and -1 as special values that disable
* the time-based window and require fresh user authentication on every single key use — which
* is very different from any positive duration. Setting this to 0 or -1 will throw
* [IllegalArgumentException].
*
* Defaults to [DEFAULT_AUTHENTICATION_VALIDITY_DURATION] seconds.
*/
var authenticationValidityDuration: Int = DEFAULT_AUTHENTICATION_VALIDITY_DURATION
set(value) {
validateAuthenticationValidityDuration(value)
field = value
}

init {
//If attestation is not provided, default to NONE
if (!::attestation.isInitialized) {
Expand Down Expand Up @@ -222,7 +243,7 @@ open class DeviceBindingCallback : AbstractCallback, Binding {
.build().identifier,
prompt: Prompt? = null) {

deviceAuthenticator.initialize(userId, prompt ?: Prompt(title, subtitle, description))
deviceAuthenticator.initialize(userId, authenticationValidityDuration, prompt ?: Prompt(title, subtitle, description))

if (deviceAuthenticator.isSupported(context, attestation).not()) {
handleException(DeviceBindingException(Unsupported()))
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2022 - 2025 Ping Identity Corporation. All rights reserved.
* Copyright (c) 2022 - 2026 Ping Identity Corporation. All rights reserved.
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
Expand All @@ -8,6 +8,7 @@ package org.forgerock.android.auth.callback

import android.content.Context
import androidx.annotation.Keep
import org.forgerock.android.auth.DEFAULT_AUTHENTICATION_VALIDITY_DURATION
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
Expand Down Expand Up @@ -114,6 +115,26 @@ open class DeviceSigningVerifierCallback : AbstractCallback, Binding {
super.setValue(clientError, 1)
}

/**
* The duration in seconds for which the generated key remains valid for user authentication
* without requiring a fresh biometric/credential prompt. Only affects biometric-backed key
* types ([DeviceBindingAuthenticationType.BIOMETRIC_ONLY] and
* [DeviceBindingAuthenticationType.BIOMETRIC_ALLOW_FALLBACK]); ignored for [DeviceBindingAuthenticationType.NONE]
* and [DeviceBindingAuthenticationType.APPLICATION_PIN].
*
* Must be greater than 0. The Android Keystore treats 0 and -1 as special values that disable
* the time-based window and require fresh user authentication on every single key use — which
* is very different from any positive duration. Setting this to 0 or -1 will throw
* [IllegalArgumentException].
*
* Defaults to [DEFAULT_AUTHENTICATION_VALIDITY_DURATION] seconds.
*/
var authenticationValidityDuration: Int = DEFAULT_AUTHENTICATION_VALIDITY_DURATION
set(value) {
validateAuthenticationValidityDuration(value)
field = value
}

/**
* Sign the challenge with bounded device keys.
*
Expand Down Expand Up @@ -178,7 +199,7 @@ open class DeviceSigningVerifierCallback : AbstractCallback, Binding {
customClaims: Map<String, Any> = emptyMap(),
prompt: Prompt? = null) {

deviceAuthenticator.initialize(userKey.userId, prompt?: Prompt(title, subtitle, description))
deviceAuthenticator.initialize(userKey.userId, authenticationValidityDuration, prompt?: Prompt(title, subtitle, description))

if (deviceAuthenticator.isSupported(context).not()) {
handleException(DeviceBindingException(Unsupported()))
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2022 - 2025 Ping Identity Corporation. All rights reserved.
* Copyright (c) 2022 - 2026 Ping Identity Corporation. All rights reserved.
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
Expand All @@ -22,6 +22,7 @@ import com.nimbusds.jwt.SignedJWT
import kotlinx.parcelize.Parcelize
import org.forgerock.android.auth.CryptoKey
import org.forgerock.android.auth.Logger
import org.forgerock.android.auth.DEFAULT_AUTHENTICATION_VALIDITY_DURATION
import org.forgerock.android.auth.callback.Attestation
import org.forgerock.android.auth.callback.DeviceBindingAuthenticationType
import java.security.PrivateKey
Expand Down Expand Up @@ -72,7 +73,10 @@ interface DeviceAuthenticator {
* @param kid Generated kid from the Preference
* @param userId userId received from server
* @param challenge challenge received from server
* @param customClaims A map of custom claims to be added to the jws payload
* @param expiration token expiration time
* @param attestation Attestation type to decide whether to include certificate chain in the JWS header
*
* @return the signed JWT as String
*/
fun sign(context: Context,
keyPair: KeyPair,
Expand All @@ -89,7 +93,7 @@ interface DeviceAuthenticator {
if (attestation !is Attestation.None) {
builder.x509CertChain(getCertificateChain(userId))
}
val jwk = builder.build();
val jwk = builder.build()
val signedJWT = SignedJWT(JWSHeader.Builder(parse(getAlgorithm()))
.keyID(kid).jwk(jwk).build(),
JWTClaimsSet.Builder().subject(userId)
Expand Down Expand Up @@ -210,7 +214,16 @@ interface DeviceAuthenticator {

}

fun DeviceAuthenticator.initialize(userId: String, prompt: Prompt): DeviceAuthenticator {
/**
* Initialize the DeviceAuthenticator with userId, authentication validity duration and prompt
* @param userId The user ID for which the keys will be generated.
* @param authenticationValidityDuration The duration (in seconds) for which the generated key remains valid
* for user authentication. Passed to the underlying crypto key during key generation.
* @param prompt The Prompt to modify the title, subtitle, description
Comment thread
vibhorgoswami marked this conversation as resolved.
*
* @return The initialized DeviceAuthenticator instance.
*/
fun DeviceAuthenticator.initialize(userId: String, authenticationValidityDuration: Int, prompt: Prompt): DeviceAuthenticator {

Comment thread
vibhorgoswami marked this conversation as resolved.
//Inject objects
if (this is BiometricAuthenticator) {
Expand All @@ -219,15 +232,26 @@ fun DeviceAuthenticator.initialize(userId: String, prompt: Prompt): DeviceAuthen
prompt.description,
deviceBindAuthenticationType = this.type()))
}
initialize(userId)
initialize(userId, authenticationValidityDuration)
this.prompt(prompt)
return this
}

fun DeviceAuthenticator.initialize(userId: String): DeviceAuthenticator {
/**
* Initialize the DeviceAuthenticator with userId and authentication validity duration
* @param userId The user ID for which the keys will be generated.
* @param authenticationValidityDuration The duration (in seconds) for which the generated key remains valid
* for user authentication. Passed to the underlying crypto key during key generation. Defaults to 5 seconds.
*
* @return The initialized DeviceAuthenticator instance.
*/
fun DeviceAuthenticator.initialize(
userId: String,
authenticationValidityDuration: Int = DEFAULT_AUTHENTICATION_VALIDITY_DURATION,
): DeviceAuthenticator {
//Inject objects
if (this is CryptoAware) {
this.setKey(CryptoKey(userId))
this.setKey(CryptoKey(userId, authenticationValidityDuration))
Comment thread
vibhorgoswami marked this conversation as resolved.
}
return this
}
Expand Down
Loading
Loading