Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Supporting passkey via AuthenticationAPIClient #773

Merged
merged 4 commits into from
Nov 7, 2024
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
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
Loading