How we achieved that without any external library
Introductions
Auth0 is a popular identity and access management platform that provides developers an easy-to-use solution for securing their applications and APIs.
With Auth0, developers can quickly add authentication and authorization to their applications without having to worry about the underlying infrastructure. Auth0 supports a wide range of authentication protocols and provides a customizable login page, multi-factor authentication, and social identity providers.
In this blog post, I will show the two possible solutions offered by the Auth0 SDK to log in the user, how migrating from one another could break your UI E2E automated tests, and how to “crack” a working solution to maintain your test checks all green ✅.
Auth0 SDK
Auth0 makes authentication and authorization easy
Because you have better things to be worrying about.
And I couldn’t agree more, we developers have better things to spend our time on, login should always be an effortless implementation in an application, from all points.
In the specifics, the Android Auth0 SDK provides 2 ways of logging the user into your application:
- Native login
- WebAuth login
Native login
The SDK provides a method to invoke to log in the user using a username and password. This method has variants, one accepting a callback, and one using coroutines, suspending the current thread.
Our app had already in place the native login, using callbacks (a bit of legacy code).
The method, inside the callback, provides you a Credentials object, containing both authToken and refreshToken, which you can then save using the CredentialManager class (from the Auth0 SDK), and re-access whenever you need them to perform any authorised-only API call to your backend.
This method is pure Java/Kotlin and requires you to set up your own UI (however you want) in your app.
Our app looked like this:
We had two fields: username and password, and a checkbox to keep the user logged in. The login button is disabled until both fields are filled with valid inputs.
Once the user clicks on the login button, we perform the login with Auth0 native authentication and then navigate the user accordingly to the correct page (either the dashboard or the account setup page).
WebAuth login
The WebAuth login instead, gives you a login page (that is customisable through the Auth0 dashboard), so you don’t have to think about the UI inside your app, not entirely, at least.
You just need to tell the SDK to start the web flow. We changed our login page to look like this
It is much simpler, it just has a login button and a checkbox to keep the user signed in. Once the user clicks on the login button, the web flow is started, and the webpage is shown:
SDK setup
To use the Auth0 SDK, some setup is required, and by some, I mean “very little”.
The shared setup between the WebAuth and Native is this simple line of code:
val account = Auth0("YOUR_CLIENT_ID}", "{YOUR_DOMAIN}")
With this, you will be able to log in the user with the WebAuth without anything else.
Native
For the Native login, some more configuration is needed:
val authentication = AuthenticationAPIClient(account)
With this authentication object, you can then login the user using the following code:
authentication.login(username, password)
.setScope(authScope)
.setAudience(authAudience)
.start(callback)
With:
- authScopebeing the scope (configured on Auth0’s dashboard)
- authAudiencebeing the audience (configured on Auth0’s dashboard)
- and callbackbeing a Callback<CredentialsManager, AuthenticationException>used to proceed after the login is successful (gives back a Credentialsobject), or to show what went wrong from the AuthenticationExceptionreceived)
WebAuth
For the WebAuth instead, once you have the account object, you can directly call:
WebAuthProvider.login(account)
.withScope(authScope)
.withAudience(authAudience)
.start(activity, callback)
The parameters are the same as the ones above, with the addition of the activity (used to launch the new web activity with ACTION_VIEW from inside the SDK).
Some more configurations need to be done, as you have to specify the callback URLs (for login and logout) on the Auth0 dashboard, and inside your app. This can be done in the manifest as explained here.
Both
The code shown above allows you only to log in the user, and gives you back the Credentials. But if you don’t store and save them, you won’t be able to keep your user logged in or re-use them at will when needed.
To achieve this, some more code is needed:
val auth0Storage: SharedPreferencesStorage = SharedPreferencesStorage(context)
val credentialsManager: CredentialsManager = CredentialsManager(authentication, auth0Storage)
// authentication is already defined above
Now with the credentialManager instance, you can save the credentials, using the fun saveCredentials(credentials: Credentials) function, and also retrieve them, using the function suspend fun awaitCredentials(): Credentials. You can also check, beforehand that the manager has valid credentials, using fun hasValidCredentials(): Boolean.
With these three functions, you will be able to log in the user, save his/her credentials, and retrieve them at will, whenever you need them.
Why migrate to WebAuth?
Since we started targeting a broader audience (we’re a B2B), some of our clients asked for — or suggested it would be nice to have — a universal login that allowed them to log in using existing accounts from other platforms (e.g.: Google, Microsoft, Apple, etc…). I won’t talk about all the UX processes that we went through, but in the end, it made sense, not requiring a personalised account on our side, and allowing a user to authenticate throughout another existing account would make ours — and their — lives easier.
Migration
As you might have already seen from the code above, the migration from one to the other is pretty easy and straightforward. There aren’t many differences, and both require a minimum effort to implement, even if starting from scratch.
In fact, it took a relatively short amount of time to do the migration, even using a TDD approach, refactoring all the tests first, and then applying the changes in the code to get back on a fully passing test (I’m talking about Unit tests here).
I left UI tests as the last thing to check and update and boy that was a mistake.
The changes on the clicks to be performed were easy, as it was removing all the “fill this field with X”.
The problem was now that, after tapping on the login button, we were outside of our codebase, outside of our application, in a WebView opened from an SDK of which we have no control.
We tried using Espresso Web with androidTestImplementation ‘androidx.test.espresso:espresso-web:3.4.0’ but unfortunately, the framework was not able to find the webview, probably because it was not inside any of our view hierarchies.
I asked the community on StackOverflow if anyone else faced the same problem before, hoping for an easy solution on how to interact with the WebView launched.
No luck.
So we tried checking if it was possible to configure, only for the test folder, the object WebAuthProvider to automatically perform the login for us, sending to it a username and password. I also opened a feature request on the Auth0 Android SDK here.
No luck. Again
We had to find another solution to keep our UI E2E tests running.
Interface to the rescue!
After struggling for a bit, we thought about using an interface to solve all our problems: the initial idea was WebAuthProvider required, and perform the login for us. This way, we could have two different implementations: one for our “real” codebase, the one that would run in production and allow the user to log in, and one that would be specific for testing, overriding the default implementation, and logging in the user without opening any webview or using the WebAuthProvider.
After considering what to provide this interface and what not, we came up with this solution:
interface WebAuthLogin {
fun login(
activity: Activity,
event: OpenUniversalLogin,
onSuccess: ((result: Credentials) -> Unit),
onFailure: ((error: AuthenticationException) -> Unit)
)
}
where the OpenUniversalLogin event contains the following:
data class OpenUniversalLogin(
val auth0: Auth0,
val scheme: String,
val scope: String,
val audience: String,
)
Modules and test-substitution
With Hilt we were able to use dependency injection to configure correctly the scenarios listed above: 1 implementation for our production code, and one for testing purposes.
We used a Module to Binds the implementation of our interface. The “official” implementation, as you might have guessed by combining the code I provided, simply would do this:
WebAuthProvider.login(event.auth0)
.withParameters(mapOf(PROMPT_KEY to LOGIN_KEY))
.withScheme(event.scheme)
.withScope(event.scope)
.withAudience(event.audience)
.start(activity, object : Callback<Credentials, AuthenticationException> {
override fun onFailure(error: AuthenticationException) {
onFailure(error)
}
override fun onSuccess(result: Credentials) {
onSuccess(result)
}
}
)
The scheme is another parameter that can be configured in the Auth0 dashboard, and since we’re using a custom-defined one, we need to provide it to the WebAuthProvider object before performing the login.
The .withParameters(mapOf(PROMPT_KEY to LOGIN_KEY)) line, instead, turned out to be quite useful and required — as well — for us: it was not documented in their repository, but it forced the webview to show the login every time, even if a logout action is not performed. This was a requirement for us, since if the user logs in without the remember me functionality, upon closing the app, we would want him to log in again. But without that parameter, since the cookies of the webview were not cleared, when the user clicked the login button, the webview would immediately return to our app, with a logged user, without asking for any credentials.
Whereas for testing, we used TestInstallIn as follows:
@TestInstallIn(
components = [SingletonComponent::class],
replaces = [RealLoginBinds::class]
)
Replacing the “official implementation with a “fake” one that used the “old”-native method to log in the user and store its data in the application’s cache.
Paired with @UninstallModules() in the tests where we needed the fake implementation.
One additional problem
As you might have noticed, the login function doesn’t accept any username or password, so our tests were at the moment blind and wouldn’t know which user to log in with (we need different accounts as we’re testing different scenarios and interactions between accounts — invites, etc…).
To solve this “problem”, I had a light-bulb 💡 moment and came up with
object LoginThingy {
lateinit var username: String
lateinit var password: String
}
Kotlin allows you to define an object with lazily initialised variables. With this, our implementation of the login function in the test folder would access the parameters as LoginThingy.username and LoginThingy.password, which we would define, in a case-by-case scenario in our test as
LoginThingy.username = email
LoginThingy.password = passwordk
Conclusions
What we thought was an easy-to-implement change, turned out to be one of the most challenging tasks of the last month: not much for the code implementation, but for all the tests we had to do in order to check and re-check that everything was working fine as it was.
Last but not least, after doing the login migration, we had to implement the log-out functionality as well, but that was an easy walk compared to the long, exhausting sprint of the login, given we walked that path already.
The new login is now live in our app, and since it’s completely configured from the Auth0 dashboard, we will be able to add Google/Facebook/Apple or other universal login ways for our user base to authenticate.
It was a nice challenge overall, that kept me busy with researching online for possible solutions already in place for a while, and I really enjoyed it. Hopefully, the next migration will be easier and quicker, without too many headaches to follow!
Keep your UI test working when migrating from native Auth0 to WebAuth0 login was originally published in Better Programming on Medium, where people are continuing the conversation by highlighting and responding to this story.