Skip to content

Commit

Permalink
Supporting passkey via AuthenticationAPIClient (#773)
Browse files Browse the repository at this point in the history
  • Loading branch information
pmathew92 authored Nov 7, 2024
2 parents 9e28b45 + f864894 commit 8e57f2b
Show file tree
Hide file tree
Showing 16 changed files with 720 additions and 294 deletions.
6 changes: 3 additions & 3 deletions .snyk
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,16 @@ ignore:
SNYK-JAVA-COMFASTERXMLWOODSTOX-3091135:
- '*':
reason: Latest version of dokka has this vulnerability
expires: 2024-10-31T12:19:35.000Z
expires: 2024-12-31T12:54:23.000Z
created: 2024-08-01T12:08:37.770Z
SNYK-JAVA-ORGJETBRAINSKOTLIN-2393744:
- '*':
reason: Latest version of dokka has this vulnerability
expires: 2024-10-31T12:19:35.000Z
expires: 2024-12-31T12:54:23.000Z
created: 2024-08-01T12:08:55.927Z
SNYK-JAVA-COMFASTERXMLJACKSONCORE-7569538:
- '*':
reason: Latest version of dokka has this vulnerability
expires: 2024-10-31T12:19:35.000Z
expires: 2024-12-31T1:54:23.000Z
created: 2024-08-01T12:08:02.973Z
patch: {}
172 changes: 133 additions & 39 deletions EXAMPLES.md
Original file line number Diff line number Diff line change
Expand Up @@ -610,61 +610,155 @@ User should have a custom domain configured and passkey grant-type enabled in th
To sign up a user with passkey

```kotlin
PasskeyAuthProvider.signUp(account)
.setEmail("user email")
.setUserName("user name")
.setPhoneNumber("phone number")
.setRealm("optional connection name")
.start(object: Callback<Credentials, AuthenticationException> {
override fun onFailure(exception: AuthenticationException) { }

override fun onSuccess(credentials: Credentials) { }
})
// Using Coroutines
try {
val challenge = authenticationApiClient.signupWithPasskey(
"{user-data}",
"{realm}"
).await()

//Use CredentialManager to create public key credentials
val request = CreatePublicKeyCredentialRequest(
Gson().toJson(challenge.authParamsPublicKey)
)

val result = credentialManager.createCredential(requireContext(), request)

val authRequest = Gson().fromJson(
(result as CreatePublicKeyCredentialResponse).registrationResponseJson,
PublicKeyCredentials::class.java
)

val userCredential = authenticationApiClient.signinWithPasskey(
challenge.authSession, authRequest, "{realm}"
)
.validateClaims()
.await()
} catch (e: CreateCredentialException) {
} catch (exception: AuthenticationException) {
}
```
<details>
<summary>Using Java</summary>

```java
PasskeyAuthProvider authProvider = new PasskeyAuthProvider();
authProvider.signUp(account)
.setEmail("user email")
.setUserName("user name")
.setPhoneNumber("phone number")
.setRealm("optional connection name")
.start(new Callback<Credentials, AuthenticationException>() {
@Override
public void onFailure(@NonNull AuthenticationException exception) { }

@Override
public void onSuccess(@Nullable Credentials credentials) { }
});
authenticationAPIClient.signupWithPasskey("{user-data}", "{realm}")
.start(new Callback<PasskeyRegistrationChallenge, AuthenticationException>() {
@Override
public void onSuccess(PasskeyRegistrationChallenge result) {
CreateCredentialRequest request =
new CreatePublicKeyCredentialRequest(new Gson().toJson(result.getAuthParamsPublicKey()));
credentialManager.createCredentialAsync(getContext(),
request,
cancellationSignal,
<executor>,
new CredentialManagerCallback<CreateCredentialResponse, CreateCredentialException>() {
@Override
public void onResult(CreateCredentialResponse createCredentialResponse) {
PublicKeyCredentials credentials = new Gson().fromJson(
((CreatePublicKeyCredentialResponse) createCredentialResponse).getRegistrationResponseJson(),
PublicKeyCredentials.class);

authenticationAPIClient.signinWithPasskey(result.getAuthSession(),
credentials, "{realm}")
.start(new Callback<Credentials, AuthenticationException>() {
@Override
public void onSuccess(Credentials result) {}

@Override
public void onFailure(@NonNull AuthenticationException error) {}
});
}
@Override
public void onError(@NonNull CreateCredentialException e) {}
});
}

@Override
public void onFailure(@NonNull AuthenticationException error) {}
});
```
</details>

To sign in a user with passkey
```kotlin
PasskeyAuthProvider.signin(account)
.setRealm("Optional connection name")
.start(object: Callback<Credentials, AuthenticationException> {
override fun onFailure(exception: AuthenticationException) { }
//Using coroutines
try {

override fun onSuccess(credentials: Credentials) { }
})
val challenge =
authenticationApiClient.passkeyChallenge("{realm}")
.await()

//Use CredentialManager to create public key credentials
val request = GetPublicKeyCredentialOption(Gson().toJson(challenge.authParamsPublicKey))
val getCredRequest = GetCredentialRequest(
listOf(request)
)
val result = credentialManager.getCredential(requireContext(), getCredRequest)
when (val credential = result.credential) {
is PublicKeyCredential -> {
val authRequest = Gson().fromJson(
credential.authenticationResponseJson,
PublicKeyCredentials::class.java
)
val userCredential = authenticationApiClient.signinWithPasskey(
challenge.authSession,
authRequest,
"{realm}"
)
.validateClaims()
.await()
}

else -> {}
}
} catch (e: GetCredentialException) {
} catch (exception: AuthenticationException) {
}
```
<details>
<summary>Using Java</summary>

```java
PasskeyAuthProvider authProvider = new PasskeyAuthProvider();
authProvider.signin(account)
.setRealm("optional connection name")
.start(new Callback<Credentials, AuthenticationException>() {
@Override
public void onFailure(@NonNull AuthenticationException exception) { }

@Override
public void onSuccess(@Nullable Credentials credentials) { }
});
authenticationAPIClient.passkeyChallenge("realm")
.start(new Callback<PasskeyChallenge, AuthenticationException>() {
@Override
public void onSuccess(PasskeyChallenge result) {
GetPublicKeyCredentialOption option = new GetPublicKeyCredentialOption(new Gson().toJson(result.getAuthParamsPublicKey()));
GetCredentialRequest request = new GetCredentialRequest(List.of(option));
credentialManager.getCredentialAsync(getContext(),
request,
cancellationSignal,
<executor>,
new CredentialManagerCallback<GetCredentialResponse, GetCredentialException>() {
@Override
public void onResult(GetCredentialResponse getCredentialResponse) {
Credential credential = getCredentialResponse.getCredential();
if (credential instanceof PublicKeyCredential) {
String responseJson = ((PublicKeyCredential) credential).getAuthenticationResponseJson();
PublicKeyCredentials publicKeyCredentials = new Gson().fromJson(
responseJson,
PublicKeyCredentials.class
);
authenticationAPIClient.signinWithPasskey(result.getAuthSession(), publicKeyCredentials,"{realm}")
.start(new Callback<Credentials, AuthenticationException>() {
@Override
public void onSuccess(Credentials result) {}

@Override
public void onFailure(@NonNull AuthenticationException error) {}
});
}
}

@Override
public void onError(@NonNull GetCredentialException e) {}
});
}

@Override
public void onFailure(@NonNull AuthenticationException error) {}
});
```
</details>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ import com.auth0.android.request.internal.ResponseUtils.isNetworkError
import com.auth0.android.result.Challenge
import com.auth0.android.result.Credentials
import com.auth0.android.result.DatabaseUser
import com.auth0.android.result.PasskeyChallengeResponse
import com.auth0.android.result.PasskeyRegistrationResponse
import com.auth0.android.result.PasskeyChallenge
import com.auth0.android.result.PasskeyRegistrationChallenge
import com.auth0.android.result.UserProfile
import com.google.gson.Gson
import okhttp3.HttpUrl.Companion.toHttpUrl
Expand Down Expand Up @@ -155,25 +155,39 @@ public class AuthenticationAPIClient @VisibleForTesting(otherwise = VisibleForTe


/**
* Log in a user using passkeys.
* This should be called after the client has received the Passkey challenge and Auth-session from the server .
* Sign-in a user using passkeys.
* This should be called after the client has received the passkey challenge and auth-session from the server
* The default scope used is 'openid profile email'.
*
* Requires the client to have the **Passkey** Grant Type enabled. See [Client Grant Types](https://auth0.com/docs/clients/client-grant-types)
* to learn how to enable it.
*
* @param authSession the auth session received from the server as part of the public challenge request.
* @param authResponse the public key credential response to be sent to the server
* @param parameters additional parameters to be sent as part of the request
* Example usage:
*
* ```
* client.signinWithPasskey("{authSession}", "{authResponse}","{realm}")
* .validateClaims() //mandatory
* .addParameter("scope","scope")
* .start(object: Callback<Credentials, AuthenticationException> {
* override fun onFailure(error: AuthenticationException) { }
* override fun onSuccess(result: Credentials) { }
* })
* ```
*
* @param authSession the auth session received from the server as part of the public key challenge request.
* @param authResponse the public key credential authentication response
* @param realm the default connection to use
* @return a request to configure and start that will yield [Credentials]
*/
internal fun signinWithPasskey(
public fun signinWithPasskey(
authSession: String,
authResponse: PublicKeyCredentialResponse,
parameters: Map<String, String>
authResponse: PublicKeyCredentials,
realm: String
): AuthenticationRequest {
val params = ParameterBuilder.newBuilder().apply {
setGrantType(ParameterBuilder.GRANT_TYPE_PASSKEY)
set(AUTH_SESSION_KEY, authSession)
addAll(parameters)
setRealm(realm)
}.asDictionary()

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


/**
* Register a user and returns a challenge.
* Sign-up a user and returns a challenge for private and public key generation.
* The default scope used is 'openid profile email'.
* Requires the client to have the **Passkey** Grant Type enabled. See [Client Grant Types](https://auth0.com/docs/clients/client-grant-types)
* to learn how to enable it.
*
* @param userMetadata user information of the client
* @param parameters additional parameter to be sent as part of the request
* @return a request to configure and start that will yield [PasskeyRegistrationResponse]
* Example usage:
*
*
* ```
* client.signupWithPasskey("{userData}","{realm}")
* .addParameter("scope","scope")
* .start(object: Callback<PasskeyRegistration, AuthenticationException> {
* override fun onSuccess(result: PasskeyRegistration) { }
* override fun onFailure(error: AuthenticationException) { }
* })
* ```
*
* @param userData user information of the client
* @param realm default connection to use
* @return a request to configure and start that will yield [PasskeyRegistrationChallenge]
*/
internal fun signupWithPasskey(
userMetadata: UserMetadataRequest,
parameters: Map<String, String>,
): Request<PasskeyRegistrationResponse, AuthenticationException> {
val user = Gson().toJsonTree(userMetadata)
public fun signupWithPasskey(
userData: UserData,
realm: String
): Request<PasskeyRegistrationChallenge, AuthenticationException> {
val user = Gson().toJsonTree(userData)
val url = auth0.getDomainUrl().toHttpUrl().newBuilder()
.addPathSegment(PASSKEY_PATH)
.addPathSegment(REGISTER_PATH)
.build()

val params = ParameterBuilder.newBuilder().apply {
setClientId(clientId)
parameters[ParameterBuilder.REALM_KEY]?.let {
setRealm(it)
}
setRealm(realm)
}.asDictionary()

val passkeyRegistrationAdapter: JsonAdapter<PasskeyRegistrationResponse> = GsonAdapter(
PasskeyRegistrationResponse::class.java, gson
)
val post = factory.post(url.toString(), passkeyRegistrationAdapter)
.addParameters(params) as BaseRequest<PasskeyRegistrationResponse, AuthenticationException>
val passkeyRegistrationChallengeAdapter: JsonAdapter<PasskeyRegistrationChallenge> =
GsonAdapter(
PasskeyRegistrationChallenge::class.java, gson
)
val post = factory.post(url.toString(), passkeyRegistrationChallengeAdapter)
.addParameters(params) as BaseRequest<PasskeyRegistrationChallenge, AuthenticationException>
post.addParameter(USER_PROFILE_KEY, user)
return post
}


/**
* Request for a challenge to initiate a passkey login flow
* Request for a challenge to initiate passkey login flow
* Requires the client to have the **Passkey** Grant Type enabled. See [Client Grant Types](https://auth0.com/docs/clients/client-grant-types)
* to learn how to enable it.
*
* @param realm An optional connection name
* @return a request to configure and start that will yield [PasskeyChallengeResponse]
* Example usage:
*
* ```
* client.passkeyChallenge("{realm}")
* .start(object: Callback<PasskeyChallenge, AuthenticationException> {
* override fun onSuccess(result: PasskeyChallenge) { }
* override fun onFailure(error: AuthenticationException) { }
* })
* ```
*
* @param realm A default connection name
* @return a request to configure and start that will yield [PasskeyChallenge]
*/
internal fun passkeyChallenge(
realm: String?
): Request<PasskeyChallengeResponse, AuthenticationException> {
public fun passkeyChallenge(
realm: String
): Request<PasskeyChallenge, AuthenticationException> {
val url = auth0.getDomainUrl().toHttpUrl().newBuilder()
.addPathSegment(PASSKEY_PATH)
.addPathSegment(CHALLENGE_PATH)
.build()

val parameters = ParameterBuilder.newBuilder().apply {
setClientId(clientId)
realm?.let { setRealm(it) }
setRealm(realm)
}.asDictionary()

val passkeyChallengeAdapter: JsonAdapter<PasskeyChallengeResponse> = GsonAdapter(
PasskeyChallengeResponse::class.java, gson
val passkeyChallengeAdapter: JsonAdapter<PasskeyChallenge> = GsonAdapter(
PasskeyChallenge::class.java, gson
)

return factory.post(url.toString(), passkeyChallengeAdapter)
Expand Down
Loading

0 comments on commit 8e57f2b

Please sign in to comment.