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

Added Passkey authentication support #764

Merged
merged 4 commits into from
Oct 28, 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
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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a note, changing these versions we have to ensure any of our existing behaviour isn't breaking

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@poovamraj Any particular feature you see which need to be checked ? I did a high level testing and didn't see any issues

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
Loading