Skip to content

Commit

Permalink
Making realm parameter optional for passkeys (#776)
Browse files Browse the repository at this point in the history
  • Loading branch information
pmathew92 authored Dec 2, 2024
2 parents d58083d + b51aca2 commit d5d2597
Show file tree
Hide file tree
Showing 6 changed files with 85 additions and 41 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ public class AuthenticationAPIClient @VisibleForTesting(otherwise = VisibleForTe

/**
* Sign-in a user using passkeys.
* This should be called after the client has received the passkey challenge and auth-session from the server
* This should be called after the client has received the passkey challenge from the server and generated the public key response.
* 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)
Expand All @@ -175,19 +175,19 @@ public class AuthenticationAPIClient @VisibleForTesting(otherwise = VisibleForTe
* ```
*
* @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
* @param authResponse the [PublicKeyCredentials] authentication response
* @param realm the connection to use. If excluded, the application will use the default connection configured in the tenant
* @return a request to configure and start that will yield [Credentials]
*/
public fun signinWithPasskey(
authSession: String,
authResponse: PublicKeyCredentials,
realm: String
realm: String? = null
): AuthenticationRequest {
val params = ParameterBuilder.newBuilder().apply {
setGrantType(ParameterBuilder.GRANT_TYPE_PASSKEY)
set(AUTH_SESSION_KEY, authSession)
setRealm(realm)
realm?.let { setRealm(it) }
}.asDictionary()

return loginWithToken(params)
Expand All @@ -198,6 +198,44 @@ public class AuthenticationAPIClient @VisibleForTesting(otherwise = VisibleForTe
}


/**
* Sign-in a user using passkeys.
* This should be called after the client has received the passkey challenge from the server and generated the public key response.
* 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.
*
* 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 in JSON string format that follows the standard webauthn json format
* @param realm the connection to use. If excluded, the application will use the default connection configured in the tenant
* @return a request to configure and start that will yield [Credentials]
*/
public fun signinWithPasskey(
authSession: String,
authResponse: String,
realm: String? = null
): AuthenticationRequest {
val publicKeyCredentials = gson.fromJson(
authResponse,
PublicKeyCredentials::class.java
)
return signinWithPasskey(authSession, publicKeyCredentials, realm)
}


/**
* Sign-up a user and returns a challenge for private and public key generation.
* The default scope used is 'openid profile email'.
Expand All @@ -217,22 +255,22 @@ public class AuthenticationAPIClient @VisibleForTesting(otherwise = VisibleForTe
* ```
*
* @param userData user information of the client
* @param realm default connection to use
* @param realm the connection to use. If excluded, the application will use the default connection configured in the tenant
* @return a request to configure and start that will yield [PasskeyRegistrationChallenge]
*/
public fun signupWithPasskey(
userData: UserData,
realm: String
realm: String? = null
): Request<PasskeyRegistrationChallenge, AuthenticationException> {
val user = Gson().toJsonTree(userData)
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)
setRealm(realm)
realm?.let { setRealm(it) }
}.asDictionary()

val passkeyRegistrationChallengeAdapter: JsonAdapter<PasskeyRegistrationChallenge> =
Expand Down Expand Up @@ -261,11 +299,11 @@ public class AuthenticationAPIClient @VisibleForTesting(otherwise = VisibleForTe
* })
* ```
*
* @param realm A default connection name
* @param realm the connection to use. If excluded, the application will use the default connection configured in the tenant
* @return a request to configure and start that will yield [PasskeyChallenge]
*/
public fun passkeyChallenge(
realm: String
realm: String? = null
): Request<PasskeyChallenge, AuthenticationException> {
val url = auth0.getDomainUrl().toHttpUrl().newBuilder()
.addPathSegment(PASSKEY_PATH)
Expand All @@ -274,7 +312,7 @@ public class AuthenticationAPIClient @VisibleForTesting(otherwise = VisibleForTe

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

val passkeyChallengeAdapter: JsonAdapter<PasskeyChallenge> = GsonAdapter(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,11 +54,6 @@ internal class PasskeyManager(
callback: Callback<Credentials, AuthenticationException>,
executor: Executor = Executors.newSingleThreadExecutor()
) {

if (realm == null) {
callback.onFailure(AuthenticationException("Realm is required for passkey authentication"))
return
}
authenticationAPIClient.signupWithPasskey(userData, realm)
.addParameters(parameters)
.start(object : Callback<PasskeyRegistrationChallenge, AuthenticationException> {
Expand Down Expand Up @@ -120,10 +115,6 @@ internal class PasskeyManager(
callback: Callback<Credentials, AuthenticationException>,
executor: Executor = Executors.newSingleThreadExecutor()
) {
if (realm == null) {
callback.onFailure(AuthenticationException("Realm is required for passkey authentication"))
return
}
authenticationAPIClient.passkeyChallenge(realm)
.start(object : Callback<PasskeyChallenge, AuthenticationException> {
override fun onSuccess(result: PasskeyChallenge) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import com.auth0.android.authentication.ParameterBuilder.Companion.newBuilder
import com.auth0.android.provider.JwtTestUtils
import com.auth0.android.request.HttpMethod
import com.auth0.android.request.NetworkingClient
import com.auth0.android.request.PublicKeyCredentials
import com.auth0.android.request.RequestOptions
import com.auth0.android.request.ServerResponse
import com.auth0.android.request.internal.RequestFactory
Expand Down Expand Up @@ -191,7 +192,7 @@ public class AuthenticationAPIClientTest {
val callback = MockAuthenticationCallback<Credentials>()
val auth0 = auth0
val client = AuthenticationAPIClient(auth0)
client.signinWithPasskey("auth-session", mock(), MY_CONNECTION)
client.signinWithPasskey("auth-session", mock<PublicKeyCredentials>(), MY_CONNECTION)
.start(callback)
ShadowLooper.idleMainLooper()
assertThat(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import com.auth0.android.authentication.AuthenticationException
import com.auth0.android.authentication.request.AuthenticationRequestMock
import com.auth0.android.authentication.request.RequestMock
import com.auth0.android.callback.Callback
import com.auth0.android.request.PublicKeyCredentials
import com.auth0.android.request.UserData
import com.auth0.android.result.AuthParamsPublicKey
import com.auth0.android.result.AuthenticatorSelection
Expand Down Expand Up @@ -135,7 +136,13 @@ public class PasskeyManagerTest {
`when`(authenticationAPIClient.signupWithPasskey(userMetadata, "testRealm")).thenReturn(
RequestMock(passkeyRegistrationChallengeResponse, null)
)
`when`(authenticationAPIClient.signinWithPasskey(any(), any(), any())).thenReturn(
`when`(
authenticationAPIClient.signinWithPasskey(
any(),
any<PublicKeyCredentials>(),
any()
)
).thenReturn(
AuthenticationRequestMock(
Credentials(
"expectedIdToken",
Expand Down Expand Up @@ -178,7 +185,7 @@ public class PasskeyManagerTest {

verify(authenticationAPIClient).signupWithPasskey(userMetadata, "testRealm")
verify(credentialManager).createCredentialAsync(eq(context), any(), any(), any(), any())
verify(authenticationAPIClient).signinWithPasskey(any(), any(), any())
verify(authenticationAPIClient).signinWithPasskey(any(), any<PublicKeyCredentials>(), any())
verify(callback).onSuccess(credentialsCaptor.capture())
Assert.assertEquals("codeAccess", credentialsCaptor.firstValue.accessToken)
Assert.assertEquals("codeScope", credentialsCaptor.firstValue.scope)
Expand All @@ -205,7 +212,11 @@ public class PasskeyManagerTest {
serialExecutor
)
verify(authenticationAPIClient).signupWithPasskey(userMetadata, "testRealm")
verify(authenticationAPIClient, never()).signinWithPasskey(any(), any(), any())
verify(authenticationAPIClient, never()).signinWithPasskey(
any(),
any<PublicKeyCredentials>(),
any()
)
verify(credentialManager, never()).createCredentialAsync(
any(),
any(),
Expand Down Expand Up @@ -251,7 +262,11 @@ public class PasskeyManagerTest {
)
verify(authenticationAPIClient).signupWithPasskey(userMetadata, "testRealm")
verify(credentialManager).createCredentialAsync(eq(context), any(), any(), any(), any())
verify(authenticationAPIClient, never()).signinWithPasskey(any(), any(), any())
verify(authenticationAPIClient, never()).signinWithPasskey(
any(),
any<PublicKeyCredentials>(),
any()
)
verify(callback).onFailure(exceptionCaptor.capture())
Assert.assertEquals(
AuthenticationException::class.java,
Expand All @@ -277,7 +292,7 @@ public class PasskeyManagerTest {
PublicKeyCredential(registrationResponseJSON)
)

`when`(authenticationAPIClient.signinWithPasskey(any(), any(), any())).thenReturn(
`when`(authenticationAPIClient.signinWithPasskey(any(), any<PublicKeyCredentials>(), any())).thenReturn(
AuthenticationRequestMock(
Credentials(
"expectedIdToken",
Expand Down Expand Up @@ -309,7 +324,7 @@ public class PasskeyManagerTest {
any(),
any()
)
verify(authenticationAPIClient).signinWithPasskey(any(), any(), any())
verify(authenticationAPIClient).signinWithPasskey(any(), any<PublicKeyCredentials>(), any())
verify(callback).onSuccess(credentialsCaptor.capture())
Assert.assertEquals("codeAccess", credentialsCaptor.firstValue.accessToken)
Assert.assertEquals("codeScope", credentialsCaptor.firstValue.scope)
Expand All @@ -335,7 +350,7 @@ public class PasskeyManagerTest {
any(),
any()
)
verify(authenticationAPIClient, never()).signinWithPasskey(any(), any(), any())
verify(authenticationAPIClient, never()).signinWithPasskey(any(), any<PublicKeyCredentials>(), any())
verify(callback).onFailure(error)
}

Expand Down Expand Up @@ -369,7 +384,7 @@ public class PasskeyManagerTest {
any(),
any()
)
verify(authenticationAPIClient, never()).signinWithPasskey(any(), any(), any())
verify(authenticationAPIClient, never()).signinWithPasskey(any(), any<PublicKeyCredentials>(), any())
verify(callback).onFailure(exceptionCaptor.capture())
Assert.assertEquals(
AuthenticationException::class.java,
Expand Down
19 changes: 9 additions & 10 deletions sample/src/main/java/com/auth0/sample/DatabaseLoginFragment.kt
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ class DatabaseLoginFragment : Fragment() {
}

binding.btSignupPasskey.setOnClickListener {
passkeySignup()
passkeySignup(binding.textEmail.text.toString())
}

binding.btSignInPasskey.setOnClickListener {
Expand All @@ -129,7 +129,7 @@ class DatabaseLoginFragment : Fragment() {

binding.btSignupPasskeyAsync.setOnClickListener {
launchAsync {
passkeySignupAsync()
passkeySignupAsync(binding.textEmail.text.toString())
}
}

Expand Down Expand Up @@ -486,11 +486,11 @@ class DatabaseLoginFragment : Fragment() {
}
}

private fun passkeySignup() {
private fun passkeySignup(email: String) {
authenticationApiClient.signupWithPasskey(
UserData(
email = "jndoe@email.com"
), "Username-Password-Authentication"
email = email
)
).start(object : Callback<PasskeyRegistrationChallenge, AuthenticationException> {
override fun onSuccess(result: PasskeyRegistrationChallenge) {
val passKeyRegistrationChallenge = result
Expand Down Expand Up @@ -558,7 +558,7 @@ class DatabaseLoginFragment : Fragment() {
}

private fun passkeySignin() {
authenticationApiClient.passkeyChallenge("Username-Password-Authentication")
authenticationApiClient.passkeyChallenge()
.start(object : Callback<PasskeyChallenge, AuthenticationException> {
override fun onSuccess(result: PasskeyChallenge) {
val passkeyChallengeResponse = result
Expand Down Expand Up @@ -631,12 +631,11 @@ class DatabaseLoginFragment : Fragment() {
})
}

private suspend fun passkeySignupAsync() {
private suspend fun passkeySignupAsync(email: String) {

try {
val challenge = authenticationApiClient.signupWithPasskey(
UserData(email = "jdoe@email.com"),
"Username-Password-Authentication"
UserData(email = email)
).await()

val request = CreatePublicKeyCredentialRequest(
Expand Down Expand Up @@ -682,7 +681,7 @@ class DatabaseLoginFragment : Fragment() {
try {

val challenge =
authenticationApiClient.passkeyChallenge("Username-Password-Authentication")
authenticationApiClient.passkeyChallenge()
.await()

val request = GetPublicKeyCredentialOption(Gson().toJson(challenge.authParamsPublicKey))
Expand Down
2 changes: 1 addition & 1 deletion sample/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<resources>
<string name="app_name">Auth0 SDK Sample</string>
<string name="com_auth0_domain">pmathew.acmetest.org</string>
<string name="com_auth0_domain">mathewp.acmetest.org</string>
<string name="com_auth0_client_id">gkba7X6OJM2b0cdlUlTCqXD7AwT3FYVV</string>
<string name="com_auth0_scheme">demo</string>
</resources>

0 comments on commit d5d2597

Please sign in to comment.