Skip to content

Commit

Permalink
Added Passkey authentication support (#764)
Browse files Browse the repository at this point in the history
  • Loading branch information
pmathew92 authored Oct 28, 2024
2 parents 59fad7e + 4304a05 commit b4dbd62
Show file tree
Hide file tree
Showing 24 changed files with 1,386 additions and 44 deletions.
3 changes: 2 additions & 1 deletion EXAMPLES.md
Original file line number Diff line number Diff line change
Expand Up @@ -1242,4 +1242,5 @@ You might encounter errors similar to `PKIX path building failed: sun.security.p
The rules should be applied automatically if your application is using `minifyEnabled = true`. If you want to include them manually check the [proguard directory](proguard).
By default you should at least use the following files:
* `proguard-okio.pro`
* `proguard-gson.pro`
* `proguard-gson.pro`
* `proguard-jetpack.pro`
12 changes: 8 additions & 4 deletions auth0/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -34,18 +34,18 @@ version = getVersionFromFile()
logger.lifecycle("Using version ${version} for ${name}")

android {
compileSdkVersion 31
compileSdkVersion 34

defaultConfig {
minSdkVersion 21
targetSdkVersion 31
targetSdkVersion 34
versionCode 1
versionName project.version

buildConfigField "String", "LIBRARY_NAME", "\"$project.rootProject.name\""
buildConfigField "String", "VERSION_NAME", "\"${project.version}\""

consumerProguardFiles '../proguard/proguard-gson.pro', '../proguard/proguard-okio.pro'
consumerProguardFiles '../proguard/proguard-gson.pro', '../proguard/proguard-okio.pro', '../proguard/proguard-jetpack.pro'
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
lintOptions {
Expand Down Expand Up @@ -77,13 +77,14 @@ ext {
powermockVersion = '2.0.9'
coroutinesVersion = '1.6.2'
biometricLibraryVersion = '1.1.0'
credentialManagerVersion = "1.3.0"
}


dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
implementation 'androidx.core:core-ktx:1.6.0'
implementation 'androidx.appcompat:appcompat:1.3.0'
implementation 'androidx.appcompat:appcompat:1.6.0'
implementation 'androidx.browser:browser:1.4.0'
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion"
implementation "com.squareup.okhttp3:okhttp:$okhttpVersion"
Expand All @@ -110,6 +111,9 @@ dependencies {
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion"

testImplementation "androidx.biometric:biometric:$biometricLibraryVersion"

implementation "androidx.credentials:credentials-play-services-auth:$credentialManagerVersion"
implementation "androidx.credentials:credentials:$credentialManagerVersion"
}

apply from: rootProject.file('gradle/jacoco.gradle')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +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.UserProfile
import com.google.gson.Gson
import okhttp3.HttpUrl.Companion.toHttpUrl
Expand Down Expand Up @@ -151,6 +153,102 @@ public class AuthenticationAPIClient @VisibleForTesting(otherwise = VisibleForTe
return loginWithToken(parameters)
}


/**
* Log in a user using passkeys.
* This should be called after the client has received the Passkey challenge and Auth-session from the server .
* 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
* @return a request to configure and start that will yield [Credentials]
*/
internal fun signinWithPasskey(
authSession: String,
authResponse: PublicKeyCredentialResponse,
parameters: Map<String, String>
): AuthenticationRequest {
val params = ParameterBuilder.newBuilder().apply {
setGrantType(ParameterBuilder.GRANT_TYPE_PASSKEY)
set(AUTH_SESSION_KEY, authSession)
addAll(parameters)
}.asDictionary()

return loginWithToken(params)
.addParameter(
AUTH_RESPONSE_KEY,
Gson().toJsonTree(authResponse)
) as AuthenticationRequest
}


/**
* Register a user and returns a challenge.
* 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]
*/
internal fun signupWithPasskey(
userMetadata: UserMetadataRequest,
parameters: Map<String, String>,
): Request<PasskeyRegistrationResponse, AuthenticationException> {
val user = Gson().toJsonTree(userMetadata)
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)
}
}.asDictionary()

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


/**
* Request for a challenge to initiate a 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]
*/
internal fun passkeyChallenge(
realm: String?
): Request<PasskeyChallengeResponse, 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) }
}.asDictionary()

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

return factory.post(url.toString(), passkeyChallengeAdapter)
.addParameters(parameters)
}

/**
* Log in a user using an Out Of Band authentication code after they have received the 'mfa_required' error.
* The MFA token tells the server the username or email, password, and realm values sent on the first request.
Expand Down Expand Up @@ -695,8 +793,7 @@ public class AuthenticationAPIClient @VisibleForTesting(otherwise = VisibleForTe
val parameters = ParameterBuilder.newBuilder()
.setClientId(clientId)
.setGrantType(ParameterBuilder.GRANT_TYPE_AUTHORIZATION_CODE)
.set(OAUTH_CODE_KEY, authorizationCode)
.set(REDIRECT_URI_KEY, redirectUri)
.set(OAUTH_CODE_KEY, authorizationCode).set(REDIRECT_URI_KEY, redirectUri)
.set("code_verifier", codeVerifier)
.asDictionary()
val url = auth0.getDomainUrl().toHttpUrl().newBuilder()
Expand Down Expand Up @@ -736,26 +833,26 @@ public class AuthenticationAPIClient @VisibleForTesting(otherwise = VisibleForTe
.addPathSegment(OAUTH_PATH)
.addPathSegment(TOKEN_PATH)
.build()
val requestParameters = ParameterBuilder.newBuilder()
.setClientId(clientId)
.addAll(parameters)
.asDictionary()
val requestParameters =
ParameterBuilder.newBuilder()
.setClientId(clientId)
.addAll(parameters)
.asDictionary()
val credentialsAdapter: JsonAdapter<Credentials> = GsonAdapter(
Credentials::class.java, gson
)
val request = BaseAuthenticationRequest(
factory.post(url.toString(), credentialsAdapter),
clientId,
baseURL
factory.post(url.toString(), credentialsAdapter), clientId, baseURL
)
request.addParameters(requestParameters)
return request
}

private fun profileRequest(): Request<UserProfile, AuthenticationException> {
val url = auth0.getDomainUrl().toHttpUrl().newBuilder()
.addPathSegment(USER_INFO_PATH)
.build()
val url =
auth0.getDomainUrl().toHttpUrl().newBuilder()
.addPathSegment(USER_INFO_PATH)
.build()
val userProfileAdapter: JsonAdapter<UserProfile> = GsonAdapter(
UserProfile::class.java, gson
)
Expand All @@ -782,6 +879,9 @@ public class AuthenticationAPIClient @VisibleForTesting(otherwise = VisibleForTe
private const val SUBJECT_TOKEN_KEY = "subject_token"
private const val SUBJECT_TOKEN_TYPE_KEY = "subject_token_type"
private const val USER_METADATA_KEY = "user_metadata"
private const val AUTH_SESSION_KEY = "auth_session"
private const val AUTH_RESPONSE_KEY = "authn_response"
private const val USER_PROFILE_KEY = "user_profile"
private const val SIGN_UP_PATH = "signup"
private const val DB_CONNECTIONS_PATH = "dbconnections"
private const val CHANGE_PASSWORD_PATH = "change_password"
Expand All @@ -793,24 +893,23 @@ public class AuthenticationAPIClient @VisibleForTesting(otherwise = VisibleForTe
private const val REVOKE_PATH = "revoke"
private const val MFA_PATH = "mfa"
private const val CHALLENGE_PATH = "challenge"
private const val PASSKEY_PATH = "passkey"
private const val REGISTER_PATH = "register"
private const val HEADER_AUTHORIZATION = "Authorization"
private const val WELL_KNOWN_PATH = ".well-known"
private const val JWKS_FILE_PATH = "jwks.json"
private fun createErrorAdapter(): ErrorAdapter<AuthenticationException> {
val mapAdapter = forMap(GsonProvider.gson)
return object : ErrorAdapter<AuthenticationException> {
override fun fromRawResponse(
statusCode: Int,
bodyText: String,
headers: Map<String, List<String>>
statusCode: Int, bodyText: String, headers: Map<String, List<String>>
): AuthenticationException {
return AuthenticationException(bodyText, statusCode)
}

@Throws(IOException::class)
override fun fromJsonResponse(
statusCode: Int,
reader: Reader
statusCode: Int, reader: Reader
): AuthenticationException {
val values = mapAdapter.fromJson(reader)
return AuthenticationException(values, statusCode)
Expand All @@ -819,13 +918,11 @@ public class AuthenticationAPIClient @VisibleForTesting(otherwise = VisibleForTe
override fun fromException(cause: Throwable): AuthenticationException {
if (isNetworkError(cause)) {
return AuthenticationException(
"Failed to execute the network request",
NetworkErrorException(cause)
"Failed to execute the network request", NetworkErrorException(cause)
)
}
return AuthenticationException(
"Something went wrong",
Auth0Exception("Something went wrong", cause)
"Something went wrong", Auth0Exception("Something went wrong", cause)
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ public class ParameterBuilder private constructor(parameters: Map<String, String
"http://auth0.com/oauth/grant-type/passwordless/otp"
public const val GRANT_TYPE_TOKEN_EXCHANGE: String =
"urn:ietf:params:oauth:grant-type:token-exchange"
public const val GRANT_TYPE_PASSKEY :String = "urn:okta:params:oauth:grant-type:webauthn"
public const val SCOPE_OPENID: String = "openid"
public const val SCOPE_OFFLINE_ACCESS: String = "openid offline_access"
public const val SCOPE_KEY: String = "scope"
Expand Down
Loading

0 comments on commit b4dbd62

Please sign in to comment.