Skip to content

Commit

Permalink
Merge pull request #519 from auth0/feat-credman-results
Browse files Browse the repository at this point in the history
Improve authenticated flow of the Credentials Manager
  • Loading branch information
lbalmaceda committed Nov 10, 2021
2 parents 3b5fdeb + 13dda39 commit b9d4269
Show file tree
Hide file tree
Showing 4 changed files with 256 additions and 34 deletions.
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
* 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(
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(
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

0 comments on commit b9d4269

Please sign in to comment.