Blog Infos
Author
Published
Topics
Published
Topics

Photo by Scott Webb

 

Saving user credentials has traditionally been a bit hit-and-miss in Android, but a new library released earlier this month looks like a promising way to address that.

What we want is straightforward:

  • A way of saving a user’s username/password (or other credential) on registration or first sign in
  • A one-tap way to sign in using a saved password
  • For a user’s saved passwords to be securely saved to their Google account, so they survive app reinstallation and are accessible across all their devices

The new Jetpack Credential Manager library offers all the above in an easy-to-use package. This article shows how you can add this to your app.

Here is a demo of what we’re going to build:

Screen recording showing Credential Manager in action, saving and using saved credentials

On first sign in, the user gets the option to save their credentials. On subsequent sign ins, they can just use the saved credentials.

In this demo we’re using username/password credentials. But one of the important assets of this library is its flexible support for whatever authentication method the user chooses. So it supports federated login tokens, passkeys (FIDO2 / private key authentication), or anything else.

The code for this article is available on my GitHub.

Setting up Jetpack Credential Manager

Adding the library to your project is done with Gradle:

//Credentials support
implementation("androidx.credentials:credentials:1.2.0-alpha01")

// optional - needed for credentials support from play services, for devices running
// Android 13 and below.
implementation("androidx.credentials:credentials-play-services-auth:1.2.0-alpha01")

To do anything with this library, you need an instance of CredentialManager. It’s helpful to create this lazily (so it’s instantiated only when needed). Here application is an instance of my Application class, available inside my AndroidViewModel:

private val credentialManager by lazy {
CredentialManager.create(application)
}

Now we can refer to credentialManager throughout the view model, with the confidence it’ll be there when we need it, and only created once.

Saving a credential

When the user registers or signs in successfully, we want to offer to save those credentials to their store. That’s straightforward:

val result = credentialManager.createCredential(
request = CreatePasswordRequest(username, password),
activity = activity,
)

That activity parameter is the current activity. In a Compose function you can get that using LocalContext.current.getActivity(). It’s needed because the OS is going to display an overlay on your app, asking the user whether they want to save the credential:

 

Screenshot showing the Save password? dialog in an Android app

The user will be asked to save their password after successful authentication

 

The above code needs:

  1. Error handling, as there’s lots of different exceptions that credentialManager.createCredential() can throw.
  2. To be run asynchronously.

Putting all that together, we get:

//Typically you would run this function only after a successful sign-in. No point in saving
//credentials that aren't correct.
private suspend fun saveCredential(activity: Activity, username: String, password: String) {
try {
//Ask the user for permission to add the credentials to their store
credentialManager.createCredential(
request = CreatePasswordRequest(username, password),
activity = activity,
)
Log.v("CredentialTest", "Credentials successfully added")
}
catch (e: CreateCredentialCancellationException) {
//do nothing, the user chose not to save the credential
Log.v("CredentialTest", "User cancelled the save")
}
catch (e: CreateCredentialException) {
Log.v("CredentialTest", "Credential save error", e)
}
}

Note how this is a suspend fun so that it runs in the coroutine of the calling code.

Also note how we separately catch CreateCredentialCancellationException and promptly ignore it. That exception is thrown when the user chooses not to save the password (say, by dismissing the popup). It doesn’t make sense to report that back to the UI, since the user already knows.

How this fits into the sign in / registration process

You call saveCredential(…) above when the user has successfully registered, or when they have successfully signed in by manually entering a username or password.

In the demo app, I’ve written the function signInOrSignUpWithEnteredCredential(…) to demonstrate:

fun signInOrSignUpWithEnteredCredential(activity: Activity, username: String, password: String) {
viewModelScope.launch {
val signInSuccess = true
//do some sign in or sign up logic here
// signInSuccess = doSomeSignInOrSignUpWork(username, password)
//then if successful...
if (signInSuccess) {
//Set signedInPasswordCredential - this is a flag to indicate to the UI that we're now
//signed in.
signedInPasswordCredential.value = PasswordCredential(username, password)
//...And offer to the user to save the credential to the store.
saveCredential(activity, username, password)
}
}
}

Job Offers

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

,

Putting a Jetpack on your legacy codebase

At Pinterest, we are always working on using the latest technologies when possible in our 10+ year old codebase.
Watch Video

Putting a Jetpack on your legacy codebase

Kurt Nelson
Senior Software Engineer

Putting a Jetpack on your legacy codebase

Kurt Nelson
Senior Software Engi ...

Putting a Jetpack on your legacy codebase

Kurt Nelson
Senior Software Engineer

Jobs

This also shows how the sign in process and the save credential process can both be run in the same coroutine (and why, therefore, we made saveCredential(…) a suspend fun).

Getting a credential

Once the credential has been saved, you can use it to log in next time. To get a saved credential, use this code:

//Tell the credential library that we're only interested in password credentials
val getCredRequest = GetCredentialRequest(
listOf(GetPasswordOption())
)
//Show the user a dialog allowing them to pick a saved credential
val credentialResponse = credentialManager.getCredential(
request = getCredRequest,
activity = activity,
)
//Return the selected credential (as long as it's a username/password)
return credentialResponse.credential as? PasswordCredential

This is a two-step process. Firstly we formulate a GetCredentialRequest, which tells the library what sorts of credentials we want (in our case only GetPasswordOption(), i.e. username/password). Then we show the dialog allowing the user to pick which credential to use using CredentialManager.getCredential(…).

Screenshot showing overlay asking user if they want to sign in with a saved credential

Offering the user to sign in using their saved credential. If there are multiple credentials available, the user will get to choose from a list.

As before this needs to be run asynchronously, and it needs error handling as there are lots of different types of exceptions which may result.

Putting it all together, we get:

private suspend fun getCredential(activity: Activity): PasswordCredential? {
try {
//Tell the credential library that we're only interested in password credentials
val getCredRequest = GetCredentialRequest(
listOf(GetPasswordOption())
)
//Show the user a dialog allowing them to pick a saved credential
val credentialResponse = credentialManager.getCredential(
request = getCredRequest,
activity = activity,
)
//Return the selected credential (as long as it's a username/password)
return credentialResponse.credential as? PasswordCredential
}
catch (e: GetCredentialCancellationException) {
//User cancelled the request. Return nothing
return null
}
catch (e: NoCredentialException) {
//We don't have a matching credential
return null
}
catch (e: GetCredentialException) {
Log.e("CredentialTest", "Error getting credential", e)
throw e
}
}

As before we’re using suspend fun so that it runs in the caller’s coroutine.

There are two potential exceptions of note:

  • GetCredentialCancellationException indicates that the user chose not to grant you permission to access any credentials, for example by dismissing the dialog
  • NoCredentialException indicates that the user has no credentials stored for your app

There are plenty of more complex exceptions described in the API docs. Here we’re just rethrowing them so the calling code can decide what to do.

Incidentally, you can see why there’s no need for the app to request any permissions here. It’s totally safe for the user, since they will manually pick which credentials the app can have access to.

And the saved credential will even be available on the user’s other devices, so long as they’re logged into Android with the same Google account.

How this fits into the sign in process

When the user clicks the Sign In With Saved Credentials button, we try and get the saved credentials and then use them to sign in.

fun signInWithSavedCredential(activity: Activity) {
viewModelScope.launch {
try {
val passwordCredential = getCredential(activity) ?: return@launch
val signInSuccess = true
//Run your app's sign in logic using the returned password credential
// signInSuccess = doSomeSignInWork(username, password)
//then if successful...
if (signInSuccess) {
//Indicate to the UI that we're now signed in.
signedInPasswordCredential.value = passwordCredential
}
}
catch (e: Exception) {
Log.e("CredentialTest", "Error getting credential", e)
}
}
}

As before, this is all done in a single coroutine which shows why it’s helpful to make getCredential(…) a suspend function.

Making it even simpler than that

You could go one step further, and make it so the user doesn’t even have to tap a Sign In With Saved Credentials button.

To do so, attempt a getCredential() as soon as the login screen is shown in your app. The user may not have any credentials saved — but if so, you haven’t bothered them as no popups will have been shown. And if they do have credentials saved, and they grant you permission to use one, you can sign in straight away.

Passkeys, FIDO2, private keys and other kinds of credential

Jetpack Credential Manager supports almost any kind of credential:

  • To save a passkey (FIDO2, private key credentials), create a CreatePublicKeyCredentialRequest object, passing in your WebAuthn JSON spec to the constructor. Then use this object as the request argument of credentialManager.createCredential().
  • To retrieve a passkey, the list passed to GetCredentialRequest’s constructor should contain a GetPublicKeyCredentialOption instance (constructed using the server’s request JSON spec).
  • To save any other type of credential — including something proprietary — instantiate a CreateCustomCredentialRequest and pass it to credentialManager.createCredential(). To retrieve it, instantiate a GetCustomCredentialOption and add it to the list passed to GetCredentialRequest.

For more information on using passkeys with Jetpack Credential Manager, there is a developer guide.

The code for this article is available on my GitHub.

So that’s how we save and re-use users’ passwords. I found the Credential Manager library a little confusing to start with, but in the end the flow became clear enough. It’s currently in Alpha but when it reaches maturity there’s no reason not to use it in every app that uses authentication.

I hope this guide has been helpful. If you have any thoughts or questions please leave a comment.

Tom Colvin is an Android and security specialist. He is available as a freelancer. He is the co-founder of the app development specialists Apptaura, where he still mentors new developers.

This article was previously published on proandroiddev.com

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
With the introduction to Compose Google changed the way we wrote UIs in android.…
READ MORE
blog
What is CompositionLocal ? when/how can we use it? How to pass widely used…
READ MORE
blog
I have been playing around with Compose and recently implemented video playback in a…
READ MORE
blog
Hi everyone! We (Kaspresso Team and AvitoTech) are back with more about automated Android testing. Previously…
READ MORE

Leave a Reply

Your email address will not be published. Required fields are marked *

Fill out this field
Fill out this field
Please enter a valid email address.

Menu