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

Making realm parameter optional for passkeys #776

Merged
merged 2 commits into from
Dec 2, 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
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>
Loading