Skip to content

Commit 8e57f2b

Browse files
authored
Supporting passkey via AuthenticationAPIClient (#773)
2 parents 9e28b45 + f864894 commit 8e57f2b

File tree

16 files changed

+720
-294
lines changed

16 files changed

+720
-294
lines changed

.snyk

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,16 @@ ignore:
55
SNYK-JAVA-COMFASTERXMLWOODSTOX-3091135:
66
- '*':
77
reason: Latest version of dokka has this vulnerability
8-
expires: 2024-10-31T12:19:35.000Z
8+
expires: 2024-12-31T12:54:23.000Z
99
created: 2024-08-01T12:08:37.770Z
1010
SNYK-JAVA-ORGJETBRAINSKOTLIN-2393744:
1111
- '*':
1212
reason: Latest version of dokka has this vulnerability
13-
expires: 2024-10-31T12:19:35.000Z
13+
expires: 2024-12-31T12:54:23.000Z
1414
created: 2024-08-01T12:08:55.927Z
1515
SNYK-JAVA-COMFASTERXMLJACKSONCORE-7569538:
1616
- '*':
1717
reason: Latest version of dokka has this vulnerability
18-
expires: 2024-10-31T12:19:35.000Z
18+
expires: 2024-12-31T1:54:23.000Z
1919
created: 2024-08-01T12:08:02.973Z
2020
patch: {}

EXAMPLES.md

Lines changed: 133 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -610,61 +610,155 @@ User should have a custom domain configured and passkey grant-type enabled in th
610610
To sign up a user with passkey
611611

612612
```kotlin
613-
PasskeyAuthProvider.signUp(account)
614-
.setEmail("user email")
615-
.setUserName("user name")
616-
.setPhoneNumber("phone number")
617-
.setRealm("optional connection name")
618-
.start(object: Callback<Credentials, AuthenticationException> {
619-
override fun onFailure(exception: AuthenticationException) { }
620-
621-
override fun onSuccess(credentials: Credentials) { }
622-
})
613+
// Using Coroutines
614+
try {
615+
val challenge = authenticationApiClient.signupWithPasskey(
616+
"{user-data}",
617+
"{realm}"
618+
).await()
619+
620+
//Use CredentialManager to create public key credentials
621+
val request = CreatePublicKeyCredentialRequest(
622+
Gson().toJson(challenge.authParamsPublicKey)
623+
)
624+
625+
val result = credentialManager.createCredential(requireContext(), request)
626+
627+
val authRequest = Gson().fromJson(
628+
(result as CreatePublicKeyCredentialResponse).registrationResponseJson,
629+
PublicKeyCredentials::class.java
630+
)
631+
632+
val userCredential = authenticationApiClient.signinWithPasskey(
633+
challenge.authSession, authRequest, "{realm}"
634+
)
635+
.validateClaims()
636+
.await()
637+
} catch (e: CreateCredentialException) {
638+
} catch (exception: AuthenticationException) {
639+
}
623640
```
624641
<details>
625642
<summary>Using Java</summary>
626643

627644
```java
628-
PasskeyAuthProvider authProvider = new PasskeyAuthProvider();
629-
authProvider.signUp(account)
630-
.setEmail("user email")
631-
.setUserName("user name")
632-
.setPhoneNumber("phone number")
633-
.setRealm("optional connection name")
634-
.start(new Callback<Credentials, AuthenticationException>() {
635-
@Override
636-
public void onFailure(@NonNull AuthenticationException exception) { }
637-
638-
@Override
639-
public void onSuccess(@Nullable Credentials credentials) { }
640-
});
645+
authenticationAPIClient.signupWithPasskey("{user-data}", "{realm}")
646+
.start(new Callback<PasskeyRegistrationChallenge, AuthenticationException>() {
647+
@Override
648+
public void onSuccess(PasskeyRegistrationChallenge result) {
649+
CreateCredentialRequest request =
650+
new CreatePublicKeyCredentialRequest(new Gson().toJson(result.getAuthParamsPublicKey()));
651+
credentialManager.createCredentialAsync(getContext(),
652+
request,
653+
cancellationSignal,
654+
<executor>,
655+
new CredentialManagerCallback<CreateCredentialResponse, CreateCredentialException>() {
656+
@Override
657+
public void onResult(CreateCredentialResponse createCredentialResponse) {
658+
PublicKeyCredentials credentials = new Gson().fromJson(
659+
((CreatePublicKeyCredentialResponse) createCredentialResponse).getRegistrationResponseJson(),
660+
PublicKeyCredentials.class);
661+
662+
authenticationAPIClient.signinWithPasskey(result.getAuthSession(),
663+
credentials, "{realm}")
664+
.start(new Callback<Credentials, AuthenticationException>() {
665+
@Override
666+
public void onSuccess(Credentials result) {}
667+
668+
@Override
669+
public void onFailure(@NonNull AuthenticationException error) {}
670+
});
671+
}
672+
@Override
673+
public void onError(@NonNull CreateCredentialException e) {}
674+
});
675+
}
676+
677+
@Override
678+
public void onFailure(@NonNull AuthenticationException error) {}
679+
});
641680
```
642681
</details>
643682

644683
To sign in a user with passkey
645684
```kotlin
646-
PasskeyAuthProvider.signin(account)
647-
.setRealm("Optional connection name")
648-
.start(object: Callback<Credentials, AuthenticationException> {
649-
override fun onFailure(exception: AuthenticationException) { }
685+
//Using coroutines
686+
try {
650687

651-
override fun onSuccess(credentials: Credentials) { }
652-
})
688+
val challenge =
689+
authenticationApiClient.passkeyChallenge("{realm}")
690+
.await()
691+
692+
//Use CredentialManager to create public key credentials
693+
val request = GetPublicKeyCredentialOption(Gson().toJson(challenge.authParamsPublicKey))
694+
val getCredRequest = GetCredentialRequest(
695+
listOf(request)
696+
)
697+
val result = credentialManager.getCredential(requireContext(), getCredRequest)
698+
when (val credential = result.credential) {
699+
is PublicKeyCredential -> {
700+
val authRequest = Gson().fromJson(
701+
credential.authenticationResponseJson,
702+
PublicKeyCredentials::class.java
703+
)
704+
val userCredential = authenticationApiClient.signinWithPasskey(
705+
challenge.authSession,
706+
authRequest,
707+
"{realm}"
708+
)
709+
.validateClaims()
710+
.await()
711+
}
712+
713+
else -> {}
714+
}
715+
} catch (e: GetCredentialException) {
716+
} catch (exception: AuthenticationException) {
717+
}
653718
```
654719
<details>
655720
<summary>Using Java</summary>
656721

657722
```java
658-
PasskeyAuthProvider authProvider = new PasskeyAuthProvider();
659-
authProvider.signin(account)
660-
.setRealm("optional connection name")
661-
.start(new Callback<Credentials, AuthenticationException>() {
662-
@Override
663-
public void onFailure(@NonNull AuthenticationException exception) { }
664-
665-
@Override
666-
public void onSuccess(@Nullable Credentials credentials) { }
667-
});
723+
authenticationAPIClient.passkeyChallenge("realm")
724+
.start(new Callback<PasskeyChallenge, AuthenticationException>() {
725+
@Override
726+
public void onSuccess(PasskeyChallenge result) {
727+
GetPublicKeyCredentialOption option = new GetPublicKeyCredentialOption(new Gson().toJson(result.getAuthParamsPublicKey()));
728+
GetCredentialRequest request = new GetCredentialRequest(List.of(option));
729+
credentialManager.getCredentialAsync(getContext(),
730+
request,
731+
cancellationSignal,
732+
<executor>,
733+
new CredentialManagerCallback<GetCredentialResponse, GetCredentialException>() {
734+
@Override
735+
public void onResult(GetCredentialResponse getCredentialResponse) {
736+
Credential credential = getCredentialResponse.getCredential();
737+
if (credential instanceof PublicKeyCredential) {
738+
String responseJson = ((PublicKeyCredential) credential).getAuthenticationResponseJson();
739+
PublicKeyCredentials publicKeyCredentials = new Gson().fromJson(
740+
responseJson,
741+
PublicKeyCredentials.class
742+
);
743+
authenticationAPIClient.signinWithPasskey(result.getAuthSession(), publicKeyCredentials,"{realm}")
744+
.start(new Callback<Credentials, AuthenticationException>() {
745+
@Override
746+
public void onSuccess(Credentials result) {}
747+
748+
@Override
749+
public void onFailure(@NonNull AuthenticationException error) {}
750+
});
751+
}
752+
}
753+
754+
@Override
755+
public void onError(@NonNull GetCredentialException e) {}
756+
});
757+
}
758+
759+
@Override
760+
public void onFailure(@NonNull AuthenticationException error) {}
761+
});
668762
```
669763
</details>
670764

auth0/src/main/java/com/auth0/android/authentication/AuthenticationAPIClient.kt

Lines changed: 73 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ import com.auth0.android.request.internal.ResponseUtils.isNetworkError
1212
import com.auth0.android.result.Challenge
1313
import com.auth0.android.result.Credentials
1414
import com.auth0.android.result.DatabaseUser
15-
import com.auth0.android.result.PasskeyChallengeResponse
16-
import com.auth0.android.result.PasskeyRegistrationResponse
15+
import com.auth0.android.result.PasskeyChallenge
16+
import com.auth0.android.result.PasskeyRegistrationChallenge
1717
import com.auth0.android.result.UserProfile
1818
import com.google.gson.Gson
1919
import okhttp3.HttpUrl.Companion.toHttpUrl
@@ -155,25 +155,39 @@ public class AuthenticationAPIClient @VisibleForTesting(otherwise = VisibleForTe
155155

156156

157157
/**
158-
* Log in a user using passkeys.
159-
* This should be called after the client has received the Passkey challenge and Auth-session from the server .
158+
* Sign-in a user using passkeys.
159+
* This should be called after the client has received the passkey challenge and auth-session from the server
160+
* The default scope used is 'openid profile email'.
161+
*
160162
* Requires the client to have the **Passkey** Grant Type enabled. See [Client Grant Types](https://auth0.com/docs/clients/client-grant-types)
161163
* to learn how to enable it.
162164
*
163-
* @param authSession the auth session received from the server as part of the public challenge request.
164-
* @param authResponse the public key credential response to be sent to the server
165-
* @param parameters additional parameters to be sent as part of the request
165+
* Example usage:
166+
*
167+
* ```
168+
* client.signinWithPasskey("{authSession}", "{authResponse}","{realm}")
169+
* .validateClaims() //mandatory
170+
* .addParameter("scope","scope")
171+
* .start(object: Callback<Credentials, AuthenticationException> {
172+
* override fun onFailure(error: AuthenticationException) { }
173+
* override fun onSuccess(result: Credentials) { }
174+
* })
175+
* ```
176+
*
177+
* @param authSession the auth session received from the server as part of the public key challenge request.
178+
* @param authResponse the public key credential authentication response
179+
* @param realm the default connection to use
166180
* @return a request to configure and start that will yield [Credentials]
167181
*/
168-
internal fun signinWithPasskey(
182+
public fun signinWithPasskey(
169183
authSession: String,
170-
authResponse: PublicKeyCredentialResponse,
171-
parameters: Map<String, String>
184+
authResponse: PublicKeyCredentials,
185+
realm: String
172186
): AuthenticationRequest {
173187
val params = ParameterBuilder.newBuilder().apply {
174188
setGrantType(ParameterBuilder.GRANT_TYPE_PASSKEY)
175189
set(AUTH_SESSION_KEY, authSession)
176-
addAll(parameters)
190+
setRealm(realm)
177191
}.asDictionary()
178192

179193
return loginWithToken(params)
@@ -185,64 +199,86 @@ public class AuthenticationAPIClient @VisibleForTesting(otherwise = VisibleForTe
185199

186200

187201
/**
188-
* Register a user and returns a challenge.
202+
* Sign-up a user and returns a challenge for private and public key generation.
203+
* The default scope used is 'openid profile email'.
189204
* Requires the client to have the **Passkey** Grant Type enabled. See [Client Grant Types](https://auth0.com/docs/clients/client-grant-types)
190205
* to learn how to enable it.
191206
*
192-
* @param userMetadata user information of the client
193-
* @param parameters additional parameter to be sent as part of the request
194-
* @return a request to configure and start that will yield [PasskeyRegistrationResponse]
207+
* Example usage:
208+
*
209+
*
210+
* ```
211+
* client.signupWithPasskey("{userData}","{realm}")
212+
* .addParameter("scope","scope")
213+
* .start(object: Callback<PasskeyRegistration, AuthenticationException> {
214+
* override fun onSuccess(result: PasskeyRegistration) { }
215+
* override fun onFailure(error: AuthenticationException) { }
216+
* })
217+
* ```
218+
*
219+
* @param userData user information of the client
220+
* @param realm default connection to use
221+
* @return a request to configure and start that will yield [PasskeyRegistrationChallenge]
195222
*/
196-
internal fun signupWithPasskey(
197-
userMetadata: UserMetadataRequest,
198-
parameters: Map<String, String>,
199-
): Request<PasskeyRegistrationResponse, AuthenticationException> {
200-
val user = Gson().toJsonTree(userMetadata)
223+
public fun signupWithPasskey(
224+
userData: UserData,
225+
realm: String
226+
): Request<PasskeyRegistrationChallenge, AuthenticationException> {
227+
val user = Gson().toJsonTree(userData)
201228
val url = auth0.getDomainUrl().toHttpUrl().newBuilder()
202229
.addPathSegment(PASSKEY_PATH)
203230
.addPathSegment(REGISTER_PATH)
204231
.build()
205232

206233
val params = ParameterBuilder.newBuilder().apply {
207234
setClientId(clientId)
208-
parameters[ParameterBuilder.REALM_KEY]?.let {
209-
setRealm(it)
210-
}
235+
setRealm(realm)
211236
}.asDictionary()
212237

213-
val passkeyRegistrationAdapter: JsonAdapter<PasskeyRegistrationResponse> = GsonAdapter(
214-
PasskeyRegistrationResponse::class.java, gson
215-
)
216-
val post = factory.post(url.toString(), passkeyRegistrationAdapter)
217-
.addParameters(params) as BaseRequest<PasskeyRegistrationResponse, AuthenticationException>
238+
val passkeyRegistrationChallengeAdapter: JsonAdapter<PasskeyRegistrationChallenge> =
239+
GsonAdapter(
240+
PasskeyRegistrationChallenge::class.java, gson
241+
)
242+
val post = factory.post(url.toString(), passkeyRegistrationChallengeAdapter)
243+
.addParameters(params) as BaseRequest<PasskeyRegistrationChallenge, AuthenticationException>
218244
post.addParameter(USER_PROFILE_KEY, user)
219245
return post
220246
}
221247

222248

223249
/**
224-
* Request for a challenge to initiate a passkey login flow
250+
* Request for a challenge to initiate passkey login flow
225251
* Requires the client to have the **Passkey** Grant Type enabled. See [Client Grant Types](https://auth0.com/docs/clients/client-grant-types)
226252
* to learn how to enable it.
227253
*
228-
* @param realm An optional connection name
229-
* @return a request to configure and start that will yield [PasskeyChallengeResponse]
254+
* Example usage:
255+
*
256+
* ```
257+
* client.passkeyChallenge("{realm}")
258+
* .start(object: Callback<PasskeyChallenge, AuthenticationException> {
259+
* override fun onSuccess(result: PasskeyChallenge) { }
260+
* override fun onFailure(error: AuthenticationException) { }
261+
* })
262+
* ```
263+
*
264+
* @param realm A default connection name
265+
* @return a request to configure and start that will yield [PasskeyChallenge]
230266
*/
231-
internal fun passkeyChallenge(
232-
realm: String?
233-
): Request<PasskeyChallengeResponse, AuthenticationException> {
267+
public fun passkeyChallenge(
268+
realm: String
269+
): Request<PasskeyChallenge, AuthenticationException> {
234270
val url = auth0.getDomainUrl().toHttpUrl().newBuilder()
235271
.addPathSegment(PASSKEY_PATH)
236272
.addPathSegment(CHALLENGE_PATH)
237273
.build()
238274

239275
val parameters = ParameterBuilder.newBuilder().apply {
240276
setClientId(clientId)
241-
realm?.let { setRealm(it) }
277+
setRealm(realm)
242278
}.asDictionary()
243279

244-
val passkeyChallengeAdapter: JsonAdapter<PasskeyChallengeResponse> = GsonAdapter(
245-
PasskeyChallengeResponse::class.java, gson
280+
val passkeyChallengeAdapter: JsonAdapter<PasskeyChallenge> = GsonAdapter(
281+
PasskeyChallenge::class.java, gson
246282
)
247283

248284
return factory.post(url.toString(), passkeyChallengeAdapter)

0 commit comments

Comments
 (0)