Skip to content

Commit e6cdaf9

Browse files
Add the ability to set preferImmediatelyAvailableCredentials on passkey requests (#405)
1 parent 1582dbf commit e6cdaf9

5 files changed

Lines changed: 46 additions & 16 deletions

File tree

source/sdk/build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ plugins {
1616
}
1717

1818
val publishGroupId = "com.stytch.sdk"
19-
val publishVersion = "0.62.4"
19+
val publishVersion = "0.63.0"
2020
val publishArtifactId = "sdk"
2121

2222
android {

source/sdk/src/main/java/com/stytch/sdk/consumer/passkeys/Passkeys.kt

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,22 +14,35 @@ import java.util.concurrent.CompletableFuture
1414
*/
1515

1616
public interface Passkeys {
17+
/**
18+
* Data class used for passing options to the Google Credential Manager
19+
* @property preferImmediatelyAvailableCredentials set this to `true` if you prefer the operation to return
20+
* immediately when there is no available passkey registration offering instead of falling back to discovering
21+
* remote options, and false otherwise
22+
*/
23+
public data class PasskeyCredentialOptions(
24+
val preferImmediatelyAvailableCredentials: Boolean = false,
25+
)
26+
1727
/**
1828
* Data class used for wrapping parameters used with Passkeys registration
1929
* @property activity an activity context for launching the native Passkeys UI
2030
* @property domain the domain of the Passkey registration. Do not include the protocol
31+
* @property options options to pass to the credential creation request
2132
*/
2233
@JacocoExcludeGenerated
2334
public data class RegisterParameters(
2435
val activity: Activity,
2536
val domain: String,
37+
val options: PasskeyCredentialOptions = PasskeyCredentialOptions(true),
2638
)
2739

2840
/**
2941
* Data class used for wrapping parameters used with Passkeys authentication
3042
* @property activity an activity context for launching the native Passkeys UI
3143
* @property domain the domain of the Passkey registration. Do not include the protocol
3244
* @property sessionDurationMinutes indicates how long the session should last before it expires
45+
* @property options options to pass to the credential retrieval request
3346
*/
3447
@JacocoExcludeGenerated
3548
public data class AuthenticateParameters
@@ -38,6 +51,7 @@ public interface Passkeys {
3851
val activity: Activity,
3952
val domain: String,
4053
val sessionDurationMinutes: Int = StytchClient.configurationManager.options.defaultSessionDuration,
54+
val options: PasskeyCredentialOptions = PasskeyCredentialOptions(false),
4155
)
4256

4357
/**

source/sdk/src/main/java/com/stytch/sdk/consumer/passkeys/PasskeysImpl.kt

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,14 @@ import java.util.concurrent.CompletableFuture
3434
internal interface PasskeysProvider {
3535
suspend fun createPublicKeyCredential(
3636
startResponse: WebAuthnRegisterStartData,
37+
options: Passkeys.PasskeyCredentialOptions,
3738
activity: Activity,
3839
dispatchers: StytchDispatchers,
3940
): CreatePublicKeyCredentialResponse
4041

4142
suspend fun getPublicKeyCredential(
4243
startResponse: WebAuthnAuthenticateStartData,
44+
options: Passkeys.PasskeyCredentialOptions,
4345
activity: Activity,
4446
dispatchers: StytchDispatchers,
4547
): PublicKeyCredential
@@ -48,13 +50,14 @@ internal interface PasskeysProvider {
4850
private class PasskeysProviderImpl : PasskeysProvider {
4951
override suspend fun createPublicKeyCredential(
5052
startResponse: WebAuthnRegisterStartData,
53+
options: Passkeys.PasskeyCredentialOptions,
5154
activity: Activity,
5255
dispatchers: StytchDispatchers,
5356
): CreatePublicKeyCredentialResponse {
5457
val createPublicKeyCredentialRequest =
5558
CreatePublicKeyCredentialRequest(
5659
requestJson = startResponse.publicKeyCredentialCreationOptions,
57-
preferImmediatelyAvailableCredentials = true,
60+
preferImmediatelyAvailableCredentials = options.preferImmediatelyAvailableCredentials,
5861
)
5962
val credentialManager = CredentialManager.create(activity)
6063
return withContext(dispatchers.ui) {
@@ -68,6 +71,7 @@ private class PasskeysProviderImpl : PasskeysProvider {
6871

6972
override suspend fun getPublicKeyCredential(
7073
startResponse: WebAuthnAuthenticateStartData,
74+
options: Passkeys.PasskeyCredentialOptions,
7175
activity: Activity,
7276
dispatchers: StytchDispatchers,
7377
): PublicKeyCredential {
@@ -79,7 +83,11 @@ private class PasskeysProviderImpl : PasskeysProvider {
7983
return withContext(dispatchers.ui) {
8084
credentialManager.getCredential(
8185
context = activity,
82-
request = GetCredentialRequest(listOf(getPublicKeyCredentialOption)),
86+
request =
87+
GetCredentialRequest(
88+
credentialOptions = listOf(getPublicKeyCredentialOption),
89+
preferImmediatelyAvailableCredentials = options.preferImmediatelyAvailableCredentials,
90+
),
8391
)
8492
}.credential as PublicKeyCredential
8593
// if credential is not a PublicKeyCredential, it will error and be caught in the calling class
@@ -112,6 +120,7 @@ internal class PasskeysImpl internal constructor(
112120
val credentialResponse =
113121
provider.createPublicKeyCredential(
114122
startResponse = startResponse,
123+
options = parameters.options,
115124
activity = parameters.activity,
116125
dispatchers = dispatchers,
117126
)
@@ -167,6 +176,7 @@ internal class PasskeysImpl internal constructor(
167176
val credentialResponse =
168177
provider.getPublicKeyCredential(
169178
startResponse = startResponse,
179+
options = parameters.options,
170180
activity = parameters.activity,
171181
dispatchers = dispatchers,
172182
)

source/sdk/src/test/java/com/stytch/sdk/consumer/passkeys/PasskeysImplTest.kt

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ internal class PasskeysImplTest {
9292
val result = impl.register(mockk(relaxed = true))
9393
assert(result is StytchResult.Error)
9494
coVerify(exactly = 1) { mockApi.registerStart(any(), any(), any(), any()) }
95-
coVerify(exactly = 0) { mockPasskeysProvider.createPublicKeyCredential(any(), any(), any()) }
95+
coVerify(exactly = 0) { mockPasskeysProvider.createPublicKeyCredential(any(), any(), any(), any()) }
9696
coVerify(exactly = 0) { mockApi.register(any()) }
9797
}
9898

@@ -103,11 +103,11 @@ internal class PasskeysImplTest {
103103
coEvery {
104104
mockApi.registerStart(any(), any(), any(), any())
105105
} returns StytchResult.Success(mockk(relaxed = true))
106-
coEvery { mockPasskeysProvider.createPublicKeyCredential(any(), any(), any()) } throws Exception()
106+
coEvery { mockPasskeysProvider.createPublicKeyCredential(any(), any(), any(), any()) } throws Exception()
107107
val result = impl.register(mockk(relaxed = true))
108108
assert(result is StytchResult.Error)
109109
coVerify(exactly = 1) { mockApi.registerStart(any(), any(), any(), any()) }
110-
coVerify(exactly = 1) { mockPasskeysProvider.createPublicKeyCredential(any(), any(), any()) }
110+
coVerify(exactly = 1) { mockPasskeysProvider.createPublicKeyCredential(any(), any(), any(), any()) }
111111
coVerify(exactly = 0) { mockApi.register(any()) }
112112
}
113113

@@ -123,13 +123,14 @@ internal class PasskeysImplTest {
123123
any(),
124124
any(),
125125
any(),
126+
any(),
126127
)
127128
} returns mockk(relaxed = true)
128129
coEvery { mockApi.register(any()) } returns StytchResult.Error(mockk(relaxed = true))
129130
val result = impl.register(mockk(relaxed = true))
130131
assert(result is StytchResult.Error)
131132
coVerify(exactly = 1) { mockApi.registerStart(any(), any(), any(), any()) }
132-
coVerify(exactly = 1) { mockPasskeysProvider.createPublicKeyCredential(any(), any(), any()) }
133+
coVerify(exactly = 1) { mockPasskeysProvider.createPublicKeyCredential(any(), any(), any(), any()) }
133134
coVerify(exactly = 1) { mockApi.register(any()) }
134135
}
135136

@@ -145,14 +146,15 @@ internal class PasskeysImplTest {
145146
any(),
146147
any(),
147148
any(),
149+
any(),
148150
)
149151
} returns mockk(relaxed = true)
150152
val mockSuccessResponse = mockk<WebAuthnRegisterResponse>(relaxed = true)
151153
coEvery { mockApi.register(any()) } returns mockSuccessResponse
152154
every { mockSuccessResponse.launchSessionUpdater(any(), any()) } just runs
153155
impl.register(mockk(relaxed = true))
154156
coVerify(exactly = 1) { mockApi.registerStart(any(), any(), any(), any()) }
155-
coVerify(exactly = 1) { mockPasskeysProvider.createPublicKeyCredential(any(), any(), any()) }
157+
coVerify(exactly = 1) { mockPasskeysProvider.createPublicKeyCredential(any(), any(), any(), any()) }
156158
coVerify(exactly = 1) { mockApi.register(any()) }
157159
verify(exactly = 1) { mockSuccessResponse.launchSessionUpdater(any(), any()) }
158160
}
@@ -184,7 +186,7 @@ internal class PasskeysImplTest {
184186
assert(result is StytchResult.Error)
185187
coVerify(exactly = 1) { mockApi.authenticateStartSecondary(any(), any()) }
186188
coVerify(exactly = 0) { mockApi.authenticateStartPrimary(any(), any()) }
187-
coVerify(exactly = 0) { mockPasskeysProvider.getPublicKeyCredential(any(), any(), any()) }
189+
coVerify(exactly = 0) { mockPasskeysProvider.getPublicKeyCredential(any(), any(), any(), any()) }
188190
coVerify(exactly = 0) { mockApi.authenticate(any(), any()) }
189191
}
190192

@@ -198,7 +200,7 @@ internal class PasskeysImplTest {
198200
assert(result is StytchResult.Error)
199201
coVerify(exactly = 0) { mockApi.authenticateStartSecondary(any(), any()) }
200202
coVerify(exactly = 1) { mockApi.authenticateStartPrimary(any(), any()) }
201-
coVerify(exactly = 0) { mockPasskeysProvider.getPublicKeyCredential(any(), any(), any()) }
203+
coVerify(exactly = 0) { mockPasskeysProvider.getPublicKeyCredential(any(), any(), any(), any()) }
202204
coVerify(exactly = 0) { mockApi.authenticate(any(), any()) }
203205
}
204206

@@ -213,12 +215,12 @@ internal class PasskeysImplTest {
213215
any(),
214216
)
215217
} returns StytchResult.Success(mockk(relaxed = true))
216-
coEvery { mockPasskeysProvider.getPublicKeyCredential(any(), any(), any()) } throws Exception()
218+
coEvery { mockPasskeysProvider.getPublicKeyCredential(any(), any(), any(), any()) } throws Exception()
217219
val result = impl.authenticate(mockk(relaxed = true))
218220
assert(result is StytchResult.Error)
219221
coVerify(exactly = 0) { mockApi.authenticateStartSecondary(any(), any()) }
220222
coVerify(exactly = 1) { mockApi.authenticateStartPrimary(any(), any()) }
221-
coVerify(exactly = 1) { mockPasskeysProvider.getPublicKeyCredential(any(), any(), any()) }
223+
coVerify(exactly = 1) { mockPasskeysProvider.getPublicKeyCredential(any(), any(), any(), any()) }
222224
coVerify(exactly = 0) { mockApi.authenticate(any(), any()) }
223225
}
224226

@@ -233,13 +235,14 @@ internal class PasskeysImplTest {
233235
any(),
234236
)
235237
} returns StytchResult.Success(mockk(relaxed = true))
236-
coEvery { mockPasskeysProvider.getPublicKeyCredential(any(), any(), any()) } returns mockk(relaxed = true)
238+
coEvery { mockPasskeysProvider.getPublicKeyCredential(any(), any(), any(), any()) } returns
239+
mockk(relaxed = true)
237240
coEvery { mockApi.authenticate(any(), any()) } returns StytchResult.Error(mockk(relaxed = true))
238241
val result = impl.authenticate(mockk(relaxed = true))
239242
assert(result is StytchResult.Error)
240243
coVerify(exactly = 0) { mockApi.authenticateStartSecondary(any(), any()) }
241244
coVerify(exactly = 1) { mockApi.authenticateStartPrimary(any(), any()) }
242-
coVerify(exactly = 1) { mockPasskeysProvider.getPublicKeyCredential(any(), any(), any()) }
245+
coVerify(exactly = 1) { mockPasskeysProvider.getPublicKeyCredential(any(), any(), any(), any()) }
243246
coVerify(exactly = 1) { mockApi.authenticate(any(), any()) }
244247
}
245248

@@ -254,14 +257,16 @@ internal class PasskeysImplTest {
254257
any(),
255258
)
256259
} returns StytchResult.Success(mockk(relaxed = true))
257-
coEvery { mockPasskeysProvider.getPublicKeyCredential(any(), any(), any()) } returns mockk(relaxed = true)
260+
coEvery {
261+
mockPasskeysProvider.getPublicKeyCredential(any(), any(), any(), any())
262+
} returns mockk(relaxed = true)
258263
val mockSuccessResponse = mockk<WebAuthnAuthenticateResponse>(relaxed = true)
259264
coEvery { mockApi.authenticate(any(), any()) } returns mockSuccessResponse
260265
every { mockSuccessResponse.launchSessionUpdater(any(), any()) } just runs
261266
impl.authenticate(mockk(relaxed = true))
262267
coVerify(exactly = 0) { mockApi.authenticateStartSecondary(any(), any()) }
263268
coVerify(exactly = 1) { mockApi.authenticateStartPrimary(any(), any()) }
264-
coVerify(exactly = 1) { mockPasskeysProvider.getPublicKeyCredential(any(), any(), any()) }
269+
coVerify(exactly = 1) { mockPasskeysProvider.getPublicKeyCredential(any(), any(), any(), any()) }
265270
coVerify(exactly = 1) { mockApi.authenticate(any(), any()) }
266271
verify(exactly = 1) { mockSuccessResponse.launchSessionUpdater(any(), any()) }
267272
}

source/sdk/src/test/java/com/stytch/sdk/consumer/passkeys/PasskeysTest.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ internal class PasskeysTest {
1919
activity = mockActivity,
2020
domain = "test.domain.com",
2121
sessionDurationMinutes = DEFAULT_SESSION_TIME_MINUTES,
22+
options = Passkeys.PasskeyCredentialOptions(false),
2223
)
2324
assert(params == expected)
2425
}

0 commit comments

Comments
 (0)