Skip to content

Commit 025d6fa

Browse files
committed
Added support for native passkey authentication
1 parent 59fad7e commit 025d6fa

File tree

17 files changed

+845
-41
lines changed

17 files changed

+845
-41
lines changed

auth0/build.gradle

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,11 @@ version = getVersionFromFile()
3434
logger.lifecycle("Using version ${version} for ${name}")
3535

3636
android {
37-
compileSdkVersion 31
37+
compileSdkVersion 34
3838

3939
defaultConfig {
4040
minSdkVersion 21
41-
targetSdkVersion 31
41+
targetSdkVersion 34
4242
versionCode 1
4343
versionName project.version
4444

@@ -77,13 +77,14 @@ ext {
7777
powermockVersion = '2.0.9'
7878
coroutinesVersion = '1.6.2'
7979
biometricLibraryVersion = '1.1.0'
80+
credentialManagerVersion = "1.3.0"
8081
}
8182

8283

8384
dependencies {
8485
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
8586
implementation 'androidx.core:core-ktx:1.6.0'
86-
implementation 'androidx.appcompat:appcompat:1.3.0'
87+
implementation 'androidx.appcompat:appcompat:1.6.0'
8788
implementation 'androidx.browser:browser:1.4.0'
8889
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion"
8990
implementation "com.squareup.okhttp3:okhttp:$okhttpVersion"
@@ -110,6 +111,9 @@ dependencies {
110111
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion"
111112

112113
testImplementation "androidx.biometric:biometric:$biometricLibraryVersion"
114+
115+
implementation "androidx.credentials:credentials-play-services-auth:$credentialManagerVersion"
116+
implementation "androidx.credentials:credentials:$credentialManagerVersion"
113117
}
114118

115119
apply from: rootProject.file('gradle/jacoco.gradle')

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

Lines changed: 113 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +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
1517
import com.auth0.android.result.UserProfile
1618
import com.google.gson.Gson
1719
import okhttp3.HttpUrl.Companion.toHttpUrl
@@ -87,8 +89,7 @@ public class AuthenticationAPIClient @VisibleForTesting(otherwise = VisibleForTe
8789
.set(USERNAME_KEY, usernameOrEmail)
8890
.set(PASSWORD_KEY, password)
8991
.setGrantType(ParameterBuilder.GRANT_TYPE_PASSWORD_REALM)
90-
.setRealm(realmOrConnection)
91-
.asDictionary()
92+
.setRealm(realmOrConnection).asDictionary()
9293
return loginWithToken(parameters)
9394
}
9495

@@ -151,6 +152,96 @@ public class AuthenticationAPIClient @VisibleForTesting(otherwise = VisibleForTe
151152
return loginWithToken(parameters)
152153
}
153154

155+
156+
/**
157+
* Log in a user using passkeys.
158+
* This should be called after the client has received the Passkey challenge and Auth-session from the server .
159+
* 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.
160+
*
161+
* @param authSession the auth session received from the server as part of the public challenge request.
162+
* @param authResponse the public key credential response to be sent to the server
163+
* @param parameters additional parameters to be sent as part of the request
164+
* @return a request to configure and start that will yield [Credentials]
165+
*/
166+
internal fun signinWithPasskey(
167+
authSession: String,
168+
authResponse: PublicKeyCredentialResponse,
169+
parameters: Map<String, String>
170+
): AuthenticationRequest {
171+
val params = ParameterBuilder.newBuilder().apply {
172+
setGrantType(ParameterBuilder.GRANT_TYPE_PASSKEY)
173+
set(AUTH_SESSION_KEY, authSession)
174+
addAll(parameters)
175+
}.asDictionary()
176+
177+
return loginWithToken(params)
178+
.addParameter(AUTH_RESPONSE_KEY, Gson().toJsonTree(authResponse)) as AuthenticationRequest
179+
}
180+
181+
182+
/**
183+
* Register a user and returns a challenge.
184+
* 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.
185+
*
186+
* @param userMetadata user information of the client
187+
* @param parameters additional parameter to be sent as part of the request
188+
* @return a request to configure and start that will yield [PasskeyRegistrationResponse]
189+
*/
190+
internal fun signupWithPasskey(
191+
userMetadata: UserMetadataRequest,
192+
parameters: Map<String, String>,
193+
): Request<PasskeyRegistrationResponse, AuthenticationException> {
194+
val user = Gson().toJsonTree(userMetadata)
195+
val url = auth0.getDomainUrl().toHttpUrl().newBuilder()
196+
.addPathSegment(PASSKEY_PATH)
197+
.addPathSegment(REGISTER_PATH)
198+
.build()
199+
200+
val params = ParameterBuilder.newBuilder().apply {
201+
setClientId(clientId)
202+
parameters[ParameterBuilder.REALM_KEY]?.let {
203+
setRealm(it)
204+
}
205+
}.asDictionary()
206+
207+
val passkeyRegistrationAdapter: JsonAdapter<PasskeyRegistrationResponse> = GsonAdapter(
208+
PasskeyRegistrationResponse::class.java, gson
209+
)
210+
val post = factory.post(url.toString(), passkeyRegistrationAdapter)
211+
.addParameters(params) as BaseRequest<PasskeyRegistrationResponse, AuthenticationException>
212+
post.addParameter(USER_PROFILE_KEY, user)
213+
return post
214+
}
215+
216+
217+
/**
218+
* Request for a challenge to initiate a passkey login flow
219+
* 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.
220+
*
221+
* @param realm An optional connection name
222+
* @return a request to configure and start that will yield [PasskeyChallengeResponse]
223+
*/
224+
internal fun passkeyChallenge(
225+
realm: String?
226+
): Request<PasskeyChallengeResponse, AuthenticationException> {
227+
val url = auth0.getDomainUrl().toHttpUrl().newBuilder()
228+
.addPathSegment(PASSKEY_PATH)
229+
.addPathSegment(CHALLENGE_PATH)
230+
.build()
231+
232+
val parameters = ParameterBuilder.newBuilder().apply {
233+
setClientId(clientId)
234+
realm?.let { setRealm(it) }
235+
}.asDictionary()
236+
237+
val passkeyChallengeAdapter: JsonAdapter<PasskeyChallengeResponse> = GsonAdapter(
238+
PasskeyChallengeResponse::class.java, gson
239+
)
240+
241+
return factory.post(url.toString(), passkeyChallengeAdapter)
242+
.addParameters(parameters)
243+
}
244+
154245
/**
155246
* Log in a user using an Out Of Band authentication code after they have received the 'mfa_required' error.
156247
* The MFA token tells the server the username or email, password, and realm values sent on the first request.
@@ -695,8 +786,7 @@ public class AuthenticationAPIClient @VisibleForTesting(otherwise = VisibleForTe
695786
val parameters = ParameterBuilder.newBuilder()
696787
.setClientId(clientId)
697788
.setGrantType(ParameterBuilder.GRANT_TYPE_AUTHORIZATION_CODE)
698-
.set(OAUTH_CODE_KEY, authorizationCode)
699-
.set(REDIRECT_URI_KEY, redirectUri)
789+
.set(OAUTH_CODE_KEY, authorizationCode).set(REDIRECT_URI_KEY, redirectUri)
700790
.set("code_verifier", codeVerifier)
701791
.asDictionary()
702792
val url = auth0.getDomainUrl().toHttpUrl().newBuilder()
@@ -736,26 +826,26 @@ public class AuthenticationAPIClient @VisibleForTesting(otherwise = VisibleForTe
736826
.addPathSegment(OAUTH_PATH)
737827
.addPathSegment(TOKEN_PATH)
738828
.build()
739-
val requestParameters = ParameterBuilder.newBuilder()
740-
.setClientId(clientId)
741-
.addAll(parameters)
742-
.asDictionary()
829+
val requestParameters =
830+
ParameterBuilder.newBuilder()
831+
.setClientId(clientId)
832+
.addAll(parameters)
833+
.asDictionary()
743834
val credentialsAdapter: JsonAdapter<Credentials> = GsonAdapter(
744835
Credentials::class.java, gson
745836
)
746837
val request = BaseAuthenticationRequest(
747-
factory.post(url.toString(), credentialsAdapter),
748-
clientId,
749-
baseURL
838+
factory.post(url.toString(), credentialsAdapter), clientId, baseURL
750839
)
751840
request.addParameters(requestParameters)
752841
return request
753842
}
754843

755844
private fun profileRequest(): Request<UserProfile, AuthenticationException> {
756-
val url = auth0.getDomainUrl().toHttpUrl().newBuilder()
757-
.addPathSegment(USER_INFO_PATH)
758-
.build()
845+
val url =
846+
auth0.getDomainUrl().toHttpUrl().newBuilder()
847+
.addPathSegment(USER_INFO_PATH)
848+
.build()
759849
val userProfileAdapter: JsonAdapter<UserProfile> = GsonAdapter(
760850
UserProfile::class.java, gson
761851
)
@@ -782,6 +872,9 @@ public class AuthenticationAPIClient @VisibleForTesting(otherwise = VisibleForTe
782872
private const val SUBJECT_TOKEN_KEY = "subject_token"
783873
private const val SUBJECT_TOKEN_TYPE_KEY = "subject_token_type"
784874
private const val USER_METADATA_KEY = "user_metadata"
875+
private const val AUTH_SESSION_KEY = "auth_session"
876+
private const val AUTH_RESPONSE_KEY = "authn_response"
877+
private const val USER_PROFILE_KEY = "user_profile"
785878
private const val SIGN_UP_PATH = "signup"
786879
private const val DB_CONNECTIONS_PATH = "dbconnections"
787880
private const val CHANGE_PASSWORD_PATH = "change_password"
@@ -793,24 +886,23 @@ public class AuthenticationAPIClient @VisibleForTesting(otherwise = VisibleForTe
793886
private const val REVOKE_PATH = "revoke"
794887
private const val MFA_PATH = "mfa"
795888
private const val CHALLENGE_PATH = "challenge"
889+
private const val PASSKEY_PATH = "passkey"
890+
private const val REGISTER_PATH = "register"
796891
private const val HEADER_AUTHORIZATION = "Authorization"
797892
private const val WELL_KNOWN_PATH = ".well-known"
798893
private const val JWKS_FILE_PATH = "jwks.json"
799894
private fun createErrorAdapter(): ErrorAdapter<AuthenticationException> {
800895
val mapAdapter = forMap(GsonProvider.gson)
801896
return object : ErrorAdapter<AuthenticationException> {
802897
override fun fromRawResponse(
803-
statusCode: Int,
804-
bodyText: String,
805-
headers: Map<String, List<String>>
898+
statusCode: Int, bodyText: String, headers: Map<String, List<String>>
806899
): AuthenticationException {
807900
return AuthenticationException(bodyText, statusCode)
808901
}
809902

810903
@Throws(IOException::class)
811904
override fun fromJsonResponse(
812-
statusCode: Int,
813-
reader: Reader
905+
statusCode: Int, reader: Reader
814906
): AuthenticationException {
815907
val values = mapAdapter.fromJson(reader)
816908
return AuthenticationException(values, statusCode)
@@ -819,13 +911,11 @@ public class AuthenticationAPIClient @VisibleForTesting(otherwise = VisibleForTe
819911
override fun fromException(cause: Throwable): AuthenticationException {
820912
if (isNetworkError(cause)) {
821913
return AuthenticationException(
822-
"Failed to execute the network request",
823-
NetworkErrorException(cause)
914+
"Failed to execute the network request", NetworkErrorException(cause)
824915
)
825916
}
826917
return AuthenticationException(
827-
"Something went wrong",
828-
Auth0Exception("Something went wrong", cause)
918+
"Something went wrong", Auth0Exception("Something went wrong", cause)
829919
)
830920
}
831921
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,7 @@ public class ParameterBuilder private constructor(parameters: Map<String, String
159159
"http://auth0.com/oauth/grant-type/passwordless/otp"
160160
public const val GRANT_TYPE_TOKEN_EXCHANGE: String =
161161
"urn:ietf:params:oauth:grant-type:token-exchange"
162+
public const val GRANT_TYPE_PASSKEY :String = "urn:okta:params:oauth:grant-type:webauthn"
162163
public const val SCOPE_OPENID: String = "openid"
163164
public const val SCOPE_OFFLINE_ACCESS: String = "openid offline_access"
164165
public const val SCOPE_KEY: String = "scope"

0 commit comments

Comments
 (0)