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

Improve authenticated flow of the Credentials Manager #519

Merged
merged 6 commits into from
Nov 10, 2021
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
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -656,7 +656,7 @@ val manager = SecureCredentialsManager(this, authentication, storage)

You can require the user authentication to obtain credentials. This will make the manager prompt the user with the device's configured Lock Screen, which they must pass correctly in order to obtain the credentials. **This feature is only available on devices where the user has setup a secured Lock Screen** (PIN, Pattern, Password or Fingerprint).

To enable authentication you must call the `requireAuthentication` method passing a valid _Activity_ context, a request code that represents the authentication call, and the title and description to display in the Lock Screen. As seen in the snippet below, you can leave these last two parameters with `null` to use the system default resources.
To enable authentication you must call the `requireAuthentication` method passing a valid _Activity_ context, a request code that represents the authentication call, and the title and description to display in the Lock Screen. As seen in the snippet below, you can leave these last two parameters with `null` to use the system's default title and description. It's only safe to call this method before the Activity is started.

```kotlin
//You might want to define a constant with the Request Code
Expand All @@ -667,7 +667,7 @@ companion object {
manager.requireAuthentication(this, AUTH_REQ_CODE, null, null)
```

When the above conditions are met and the manager requires the user authentication, it will use the activity context to launch a new activity and wait for its result in the `onActivityResult` method. Your activity must override this method and pass the request code and result code to the manager's `checkAuthenticationResult` method to verify if this request was successful or not.
When the above conditions are met and the manager requires the user authentication, it will use the activity context to launch the Lock Screen activity and wait for its result. If your activity is a subclass of `ComponentActivity`, this will be handled automatically for you internally. Otherwise, your activity must override the `onActivityResult` method and pass the request code and result code to the manager's `checkAuthenticationResult` method to verify if this request was successful or not.

```kotlin
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
Expand Down
2 changes: 1 addition & 1 deletion auth0/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,6 @@ dependencies {
testImplementation "com.squareup.okhttp3:mockwebserver:$okhttpVersion"
testImplementation "com.squareup.okhttp3:okhttp-tls:$okhttpVersion"
testImplementation 'com.jayway.awaitility:awaitility:1.7.0'
testImplementation 'org.robolectric:robolectric:4.4'
testImplementation 'org.robolectric:robolectric:4.6.1'
testImplementation 'androidx.test.espresso:espresso-intents:3.4.0'
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,12 @@ import android.os.Build
import android.text.TextUtils
import android.util.Base64
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.IntRange
import androidx.annotation.VisibleForTesting
import androidx.lifecycle.Lifecycle
import com.auth0.android.authentication.AuthenticationAPIClient
import com.auth0.android.authentication.AuthenticationException
import com.auth0.android.callback.AuthenticationCallback
Expand All @@ -36,6 +40,7 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT
private var authenticateBeforeDecrypt: Boolean
private var authenticationRequestCode = -1
private var activity: Activity? = null
private var activityResultContract: ActivityResultLauncher<Intent>? = null

//State for retrying operations
private var decryptCallback: Callback<Credentials, CredentialsManagerException>? = null
Expand Down Expand Up @@ -63,12 +68,13 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT

/**
* Require the user to authenticate using the configured LockScreen before accessing the credentials.
* This feature is disabled by default and will only work if the device is running on Android version 21 or up and if the user
* has configured a secure LockScreen (PIN, Pattern, Password or Fingerprint).
* This method MUST be called in [Activity.onCreate]. This feature is disabled by default and will
Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's OK if they don't. I'm defaulting to the old scenario anyways, but it's better to lead them to use the new way for future compatibility & easier deprecation reasons.

* only work if the user has configured a secure LockScreen (PIN, Pattern, Password or Fingerprint).
*
*
* The activity passed as first argument here must override the [Activity.onActivityResult] method and
* call [SecureCredentialsManager.checkAuthenticationResult] with the received parameters.
* If the activity passed as first argument is a subclass of ComponentActivity, the authentication result
* will be handled internally using "Activity Results API". Otherwise, your activity must override the
* [Activity.onActivityResult] method and call [SecureCredentialsManager.checkAuthenticationResult] with
* the received parameters.
*
* @param activity a valid activity context. Will be used in the authentication request to launch a LockScreen intent.
* @param requestCode the request code to use in the authentication request. Must be a value between 1 and 255.
Expand All @@ -84,25 +90,42 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT
): Boolean {
require(!(requestCode < 1 || requestCode > 255)) { "Request code must be a value between 1 and 255." }
val kManager = activity.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
authIntent =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) kManager.createConfirmDeviceCredentialIntent(
Copy link
Contributor Author

Choose a reason for hiding this comment

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

the min SDK is already 21, no reason to keep these checks around

title,
description
) else null
authIntent = kManager.createConfirmDeviceCredentialIntent(title, description)
authenticateBeforeDecrypt =
((Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && kManager.isDeviceSecure
|| Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && kManager.isKeyguardSecure)
((Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && kManager.isDeviceSecure || kManager.isKeyguardSecure)
&& authIntent != null)
if (authenticateBeforeDecrypt) {
this.activity = activity
authenticationRequestCode = requestCode

/*
* https://developer.android.com/training/basics/intents/result#register
* Docs say it's safe to call "registerForActivityResult" BEFORE the activity is created. In practice,
* when that's not the case, a RuntimeException is thrown. The lifecycle state check below is meant to
* prevent that exception while still falling back to the old "startActivityForResult" flow. That's in
* case devs are invoking this method in places other than the Activity's "OnCreate" method.
*/
if (activity is ComponentActivity && !activity.lifecycle.currentState.isAtLeast(
Copy link
Contributor Author

Choose a reason for hiding this comment

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

If the context is a subclass of ComponentActivity and not yet STARTED, the new path will be used. Otherwise, legacy scenario.

Lifecycle.State.STARTED
)
) {
activityResultContract =
activity.registerForActivityResult(
ActivityResultContracts.StartActivityForResult(),
activity.activityResultRegistry
) {
checkAuthenticationResult(authenticationRequestCode, it.resultCode)
}
} else {
this.activity = activity
}
}
return authenticateBeforeDecrypt
}

/**
* Checks the result after showing the LockScreen to the user.
* Must be called from the [Activity.onActivityResult] method with the received parameters.
* Called internally when your activity is a subclass of ComponentActivity (using Activity Results API).
* It's safe to call this method even if [SecureCredentialsManager.requireAuthentication] was unsuccessful.
*
* @param requestCode the request code received in the onActivityResult call.
Expand Down Expand Up @@ -239,7 +262,8 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT
decryptCallback = callback
this.scope = scope
this.minTtl = minTtl
activity!!.startActivityForResult(authIntent, authenticationRequestCode)
activityResultContract?.launch(authIntent)
?: activity?.startActivityForResult(authIntent, authenticationRequestCode)
return
}
continueGetCredentials(scope, minTtl, parameters, callback)
Expand Down
Loading