Skip to content

Commit a66d88d

Browse files
spetrovwitrisnavibhorgoswamirodrigoareis
authored
ForgeRock Android SDK 4.8.6 Release (#523)
* SDKS-4658: Set code verifier to null for Apple Sign-In The `codeVerifier` is explicitly set to `null` in the `AuthorizationRequest.Builder` for the Apple Sign-In handler. * SDKS-4714: Use empty request body in self-service session endpoint (#510) * SDKS-4714: Use empty request body in self-service session endpoint Remove the `EMPTY` constant from OkHttp's `RequestBody` and has been replaced with `"".toRequestBody()`. * Update Session.kt Signed-off-by: Andy Witrisna <andy.witrisna@forgerock.com> --------- Signed-off-by: Andy Witrisna <andy.witrisna@forgerock.com> * Add migration support for Ping SDK with DefaultStorageClient (#511) * Increase sleep time in FRUserMockTest to ensure token expiration (#513) The `Thread.sleep` duration in `FRUserMockTest.kt` is increased from 1000ms to 2000ms to allow sufficient time for tokens to expire before testing the refresh token flow. * ForgeRock Android SDK 4.8.4 Release preparation (#512) * Updated for the ForgeRock 4.8.4 release * Fixed various e2e test cases. * SDKS-4947 Updating README with maintenance information (#515) * Updating README with maintenance information * Rename contribution file name * SDKS-5037 - Upgrade bcpkix-jdk18on from 1.81 to 1.84 to address CVE-2026-5588 (#516) * ForgeRock Android SDK 4.8.5 Release preparation (#517) * Removed Sonatype OSS Index Scan from the CI (this service has been discontinued) * Updated version number for the ForgeRock 4.8.5 release * Fix mend tasks to fail the pipeline if critical or high vulnerabilities are found * SDKS-5096: Enhance WebAuthn registration by adding optional device name (#519) * SDKS-5096: Enhance WebAuthn registration by adding optional device name parameter * SDKS-5120: Add authenticationValidityDuration support to device binding and signing (#520) * SDKS-5120: Allow configurable authenticationValidityDuration in DeviceBindingCallback and DeviceSigningVerifierCallback * PR review - guard authentication validity duration. * [fix] SDKS-5114 Handle AM 400 for Push Number Challenge with distinct exception (#521) * [fix] SDKS-5114 Handle AM 400 for Push Number Challenge with distinct exception AM 8.1.0 (OPENAM-24154) returns HTTP 400 when a user selects the wrong number in a Push Number Challenge. Previously the SDK surfaced a generic PushMechanismException with no semantic meaning. This fix introduces PushNumberChallengeException (extends PushMechanismException) so callers can distinguish a wrong-number rejection from other failures, and leaves the PushNotification in its pending state so the app can offer a retry. Phases: - phase 1: Create PushNumberChallengeException (8804d77c) - phase 2: Handle 400 for Push Number Challenge in PushResponder (fe17bf07) - phase 3: Unit tests for 400 number-challenge path (4d57f2ca) - code review: Remove body logging, fix test builder placement (cc910478) Refs: SDKS-5114 * Addressing comment from Stoyan * Updated version number for the ForgeRock 4.8.6 release (#522) --------- Signed-off-by: Andy Witrisna <andy.witrisna@forgerock.com> Co-authored-by: Andy Witrisna <witrisna@gmail.com> Co-authored-by: Andy Witrisna <andy.witrisna@forgerock.com> Co-authored-by: Vibhor Goswami <vibhor.goswami@gmail.com> Co-authored-by: Rodrigo Reis <rodrigo.reis@forgerock.com>
1 parent 6874182 commit a66d88d

17 files changed

Lines changed: 433 additions & 53 deletions

File tree

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
## [4.8.6]
2+
3+
#### Fixed
4+
- Fixed an issue where selecting the wrong number in a Push Number Challenge returned a generic error instead of a distinct exception [SDKS-5114]
5+
- Fixed an issue where the custom device name provided during WebAuthn registration was ignored when `supportsJsonResponse` is enabled. [SDKS-5096]
6+
- Added configurable `authenticationValidityDuration` to `DeviceBindingCallback` and `DeviceSigningVerifierCallback`, allowing the biometric key authorization window to be extended beyond the default 5 seconds. [SDKS-5120]
7+
18
## [4.8.5]
29

310
#### Fixed

forgerock-auth/src/main/java/org/forgerock/android/auth/callback/Binding.kt

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2022 - 2025 Ping Identity Corporation. All rights reserved.
2+
* Copyright (c) 2022 - 2026 Ping Identity Corporation. All rights reserved.
33
*
44
* This software may be modified and distributed under the terms
55
* of the MIT license. See the LICENSE file for details.
@@ -9,6 +9,7 @@ package org.forgerock.android.auth.callback
99
import android.os.OperationCanceledException
1010
import kotlinx.coroutines.CancellationException
1111
import kotlinx.coroutines.TimeoutCancellationException
12+
import org.forgerock.android.auth.DEFAULT_AUTHENTICATION_VALIDITY_DURATION
1213
import org.forgerock.android.auth.Logger
1314
import org.forgerock.android.auth.devicebind.DeviceAuthenticator
1415
import org.forgerock.android.auth.devicebind.DeviceBindingErrorStatus.Abort
@@ -22,6 +23,7 @@ import kotlin.time.toDuration
2223

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

26+
2527
/**
2628
* Device Binding interface to provide utility method for [DeviceBindingCallback] and [DeviceSigningVerifierCallback]
2729
*/
@@ -83,6 +85,16 @@ interface Binding {
8385

8486
fun setClientError(clientError: String?)
8587

88+
/**
89+
* Validates that [authenticationValidityDuration] is greater than 0.
90+
* The Android Keystore treats 0 and -1 as special values that disable the time-based window
91+
* and require fresh user authentication on every single key use.
92+
* @throws IllegalArgumentException if [value] is not greater than 0
93+
*/
94+
fun validateAuthenticationValidityDuration(value: Int) {
95+
require(value > 0) { "authenticationValidityDuration must be greater than 0" }
96+
}
97+
8698
/**
8799
* Default function to identify [DeviceAuthenticator]
88100
*/

forgerock-auth/src/main/java/org/forgerock/android/auth/callback/DeviceBindingCallback.kt

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2022 - 2025 Ping Identity Corporation. All rights reserved.
2+
* Copyright (c) 2022 - 2026 Ping Identity Corporation. All rights reserved.
33
*
44
* This software may be modified and distributed under the terms
55
* of the MIT license. See the LICENSE file for details.
@@ -9,6 +9,7 @@ package org.forgerock.android.auth.callback
99
import android.content.Context
1010
import android.util.Base64
1111
import androidx.annotation.Keep
12+
import org.forgerock.android.auth.DEFAULT_AUTHENTICATION_VALIDITY_DURATION
1213
import kotlinx.coroutines.CoroutineScope
1314
import kotlinx.coroutines.Dispatchers
1415
import kotlinx.coroutines.launch
@@ -104,6 +105,26 @@ open class DeviceBindingCallback : AbstractCallback, Binding {
104105
lateinit var attestation: Attestation
105106
private set
106107

108+
/**
109+
* The duration in seconds for which the generated key remains valid for user authentication
110+
* without requiring a fresh biometric/credential prompt. Only affects biometric-backed key
111+
* types ([DeviceBindingAuthenticationType.BIOMETRIC_ONLY] and
112+
* [DeviceBindingAuthenticationType.BIOMETRIC_ALLOW_FALLBACK]); ignored for [DeviceBindingAuthenticationType.NONE]
113+
* and [DeviceBindingAuthenticationType.APPLICATION_PIN].
114+
*
115+
* Must be greater than 0. The Android Keystore treats 0 and -1 as special values that disable
116+
* the time-based window and require fresh user authentication on every single key use — which
117+
* is very different from any positive duration. Setting this to 0 or -1 will throw
118+
* [IllegalArgumentException].
119+
*
120+
* Defaults to [DEFAULT_AUTHENTICATION_VALIDITY_DURATION] seconds.
121+
*/
122+
var authenticationValidityDuration: Int = DEFAULT_AUTHENTICATION_VALIDITY_DURATION
123+
set(value) {
124+
validateAuthenticationValidityDuration(value)
125+
field = value
126+
}
127+
107128
init {
108129
//If attestation is not provided, default to NONE
109130
if (!::attestation.isInitialized) {
@@ -222,7 +243,7 @@ open class DeviceBindingCallback : AbstractCallback, Binding {
222243
.build().identifier,
223244
prompt: Prompt? = null) {
224245

225-
deviceAuthenticator.initialize(userId, prompt ?: Prompt(title, subtitle, description))
246+
deviceAuthenticator.initialize(userId, authenticationValidityDuration, prompt ?: Prompt(title, subtitle, description))
226247

227248
if (deviceAuthenticator.isSupported(context, attestation).not()) {
228249
handleException(DeviceBindingException(Unsupported()))

forgerock-auth/src/main/java/org/forgerock/android/auth/callback/DeviceSigningVerifierCallback.kt

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2022 - 2025 Ping Identity Corporation. All rights reserved.
2+
* Copyright (c) 2022 - 2026 Ping Identity Corporation. All rights reserved.
33
*
44
* This software may be modified and distributed under the terms
55
* of the MIT license. See the LICENSE file for details.
@@ -8,6 +8,7 @@ package org.forgerock.android.auth.callback
88

99
import android.content.Context
1010
import androidx.annotation.Keep
11+
import org.forgerock.android.auth.DEFAULT_AUTHENTICATION_VALIDITY_DURATION
1112
import kotlinx.coroutines.CoroutineScope
1213
import kotlinx.coroutines.Dispatchers
1314
import kotlinx.coroutines.launch
@@ -114,6 +115,26 @@ open class DeviceSigningVerifierCallback : AbstractCallback, Binding {
114115
super.setValue(clientError, 1)
115116
}
116117

118+
/**
119+
* The duration in seconds for which the generated key remains valid for user authentication
120+
* without requiring a fresh biometric/credential prompt. Only affects biometric-backed key
121+
* types ([DeviceBindingAuthenticationType.BIOMETRIC_ONLY] and
122+
* [DeviceBindingAuthenticationType.BIOMETRIC_ALLOW_FALLBACK]); ignored for [DeviceBindingAuthenticationType.NONE]
123+
* and [DeviceBindingAuthenticationType.APPLICATION_PIN].
124+
*
125+
* Must be greater than 0. The Android Keystore treats 0 and -1 as special values that disable
126+
* the time-based window and require fresh user authentication on every single key use — which
127+
* is very different from any positive duration. Setting this to 0 or -1 will throw
128+
* [IllegalArgumentException].
129+
*
130+
* Defaults to [DEFAULT_AUTHENTICATION_VALIDITY_DURATION] seconds.
131+
*/
132+
var authenticationValidityDuration: Int = DEFAULT_AUTHENTICATION_VALIDITY_DURATION
133+
set(value) {
134+
validateAuthenticationValidityDuration(value)
135+
field = value
136+
}
137+
117138
/**
118139
* Sign the challenge with bounded device keys.
119140
*
@@ -178,7 +199,7 @@ open class DeviceSigningVerifierCallback : AbstractCallback, Binding {
178199
customClaims: Map<String, Any> = emptyMap(),
179200
prompt: Prompt? = null) {
180201

181-
deviceAuthenticator.initialize(userKey.userId, prompt?: Prompt(title, subtitle, description))
202+
deviceAuthenticator.initialize(userKey.userId, authenticationValidityDuration, prompt?: Prompt(title, subtitle, description))
182203

183204
if (deviceAuthenticator.isSupported(context).not()) {
184205
handleException(DeviceBindingException(Unsupported()))

forgerock-auth/src/main/java/org/forgerock/android/auth/callback/WebAuthnRegistrationCallback.kt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2022 - 2025 Ping Identity Corporation. All rights reserved.
2+
* Copyright (c) 2022 - 2026 Ping Identity Corporation. All rights reserved.
33
*
44
* This software may be modified and distributed under the terms
55
* of the MIT license. See the LICENSE file for details.
@@ -61,9 +61,9 @@ open class WebAuthnRegistrationCallback : MetadataCallback, WebAuthnCallback {
6161
webAuthnRegistration.options = webAuthnRegistration.options.cloneWith(
6262
ResidentKeyRequirement.valueOf(it))
6363
}
64-
var result = webAuthnRegistration.register(context)
65-
deviceName?.apply { result += "::$deviceName" }
66-
setHiddenCallbackValue(node, result);
64+
65+
val result = webAuthnRegistration.register(context, deviceName)
66+
setHiddenCallbackValue(node, result)
6767
} catch (e: Exception) {
6868
setErrorRethrow(node, e)
6969
}

forgerock-auth/src/main/java/org/forgerock/android/auth/devicebind/DeviceBindAuthenticators.kt

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2022 - 2025 Ping Identity Corporation. All rights reserved.
2+
* Copyright (c) 2022 - 2026 Ping Identity Corporation. All rights reserved.
33
*
44
* This software may be modified and distributed under the terms
55
* of the MIT license. See the LICENSE file for details.
@@ -22,6 +22,7 @@ import com.nimbusds.jwt.SignedJWT
2222
import kotlinx.parcelize.Parcelize
2323
import org.forgerock.android.auth.CryptoKey
2424
import org.forgerock.android.auth.Logger
25+
import org.forgerock.android.auth.DEFAULT_AUTHENTICATION_VALIDITY_DURATION
2526
import org.forgerock.android.auth.callback.Attestation
2627
import org.forgerock.android.auth.callback.DeviceBindingAuthenticationType
2728
import java.security.PrivateKey
@@ -72,7 +73,10 @@ interface DeviceAuthenticator {
7273
* @param kid Generated kid from the Preference
7374
* @param userId userId received from server
7475
* @param challenge challenge received from server
75-
* @param customClaims A map of custom claims to be added to the jws payload
76+
* @param expiration token expiration time
77+
* @param attestation Attestation type to decide whether to include certificate chain in the JWS header
78+
*
79+
* @return the signed JWT as String
7680
*/
7781
fun sign(context: Context,
7882
keyPair: KeyPair,
@@ -89,7 +93,7 @@ interface DeviceAuthenticator {
8993
if (attestation !is Attestation.None) {
9094
builder.x509CertChain(getCertificateChain(userId))
9195
}
92-
val jwk = builder.build();
96+
val jwk = builder.build()
9397
val signedJWT = SignedJWT(JWSHeader.Builder(parse(getAlgorithm()))
9498
.keyID(kid).jwk(jwk).build(),
9599
JWTClaimsSet.Builder().subject(userId)
@@ -210,7 +214,16 @@ interface DeviceAuthenticator {
210214

211215
}
212216

213-
fun DeviceAuthenticator.initialize(userId: String, prompt: Prompt): DeviceAuthenticator {
217+
/**
218+
* Initialize the DeviceAuthenticator with userId, authentication validity duration and prompt
219+
* @param userId The user ID for which the keys will be generated.
220+
* @param authenticationValidityDuration The duration (in seconds) for which the generated key remains valid
221+
* for user authentication. Passed to the underlying crypto key during key generation.
222+
* @param prompt The Prompt to modify the title, subtitle, description
223+
*
224+
* @return The initialized DeviceAuthenticator instance.
225+
*/
226+
fun DeviceAuthenticator.initialize(userId: String, authenticationValidityDuration: Int, prompt: Prompt): DeviceAuthenticator {
214227

215228
//Inject objects
216229
if (this is BiometricAuthenticator) {
@@ -219,15 +232,26 @@ fun DeviceAuthenticator.initialize(userId: String, prompt: Prompt): DeviceAuthen
219232
prompt.description,
220233
deviceBindAuthenticationType = this.type()))
221234
}
222-
initialize(userId)
235+
initialize(userId, authenticationValidityDuration)
223236
this.prompt(prompt)
224237
return this
225238
}
226239

227-
fun DeviceAuthenticator.initialize(userId: String): DeviceAuthenticator {
240+
/**
241+
* Initialize the DeviceAuthenticator with userId and authentication validity duration
242+
* @param userId The user ID for which the keys will be generated.
243+
* @param authenticationValidityDuration The duration (in seconds) for which the generated key remains valid
244+
* for user authentication. Passed to the underlying crypto key during key generation. Defaults to 5 seconds.
245+
*
246+
* @return The initialized DeviceAuthenticator instance.
247+
*/
248+
fun DeviceAuthenticator.initialize(
249+
userId: String,
250+
authenticationValidityDuration: Int = DEFAULT_AUTHENTICATION_VALIDITY_DURATION,
251+
): DeviceAuthenticator {
228252
//Inject objects
229253
if (this is CryptoAware) {
230-
this.setKey(CryptoKey(userId))
254+
this.setKey(CryptoKey(userId, authenticationValidityDuration))
231255
}
232256
return this
233257
}
Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
/*
2-
* Copyright (c) 2025 Ping Identity Corporation. All rights reserved.
2+
* Copyright (c) 2025 - 2026 Ping Identity Corporation. All rights reserved.
33
*
44
* This software may be modified and distributed under the terms
55
* of the MIT license. See the LICENSE file for details.
66
*/
77

88
package org.forgerock.android.auth.webauthn
99

10+
import android.annotation.SuppressLint
1011
import kotlinx.serialization.Serializable
1112

13+
@SuppressLint("UnsafeOptInUsageError")
1214
@Serializable
1315
data class WebAuthnOutcome(val authenticatorAttachment: String , val legacyData: String)

forgerock-auth/src/main/java/org/forgerock/android/auth/webauthn/WebAuthnRegistration.kt

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2022 - 2025 Ping Identity Corporation. All rights reserved.
2+
* Copyright (c) 2022 - 2026 Ping Identity Corporation. All rights reserved.
33
*
44
* This software may be modified and distributed under the terms
55
* of the MIT license. See the LICENSE file for details.
@@ -27,8 +27,6 @@ import kotlinx.coroutines.tasks.await
2727
import kotlinx.coroutines.withContext
2828
import kotlinx.serialization.encodeToString
2929
import kotlinx.serialization.json.Json
30-
import kotlinx.serialization.json.buildJsonObject
31-
import kotlinx.serialization.json.put
3230
import org.forgerock.android.auth.WebAuthnDataRepository
3331
import org.json.JSONArray
3432
import org.json.JSONException
@@ -149,7 +147,16 @@ open class WebAuthnRegistration() : WebAuthn() {
149147
return getCredentials(excludeCredentials)
150148
}
151149

152-
suspend fun register(context: Context): String {
150+
/**
151+
* Registers a new WebAuthn credential.
152+
*
153+
* @param context Application context used to launch the registration intent.
154+
* @param deviceName Optional friendly name appended to the response so the server can label the credential.
155+
*
156+
* @return The registration outcome as a `::` delimited string, or a JSON-encoded
157+
* [WebAuthnOutcome] when the server supports JSON responses.
158+
*/
159+
suspend fun register(context: Context, deviceName: String? = null): String {
153160
val publicKeyCredential = getPublicKeyCredential(context)
154161
val response = publicKeyCredential.response as AuthenticatorAttestationResponse
155162
val sb = StringBuilder()
@@ -173,10 +180,11 @@ open class WebAuthnRegistration() : WebAuthn() {
173180
persist(context, source)
174181
}
175182
}
183+
deviceName?.let { sb.append("::$it") }
176184
return if (supportsJsonResponse) {
177185
val outcome = WebAuthnOutcome(publicKeyCredential.authenticatorAttachment ?: "platform",
178186
sb.toString())
179-
return Json.encodeToString(outcome)
187+
Json.encodeToString(outcome)
180188
} else {
181189
sb.toString()
182190
}

0 commit comments

Comments
 (0)