You’re probably doing oauth wrong on Android

Daniel Llewellyn
8 min readAug 5, 2020

--

Here’s how to follow best practice

There are many implementations of oauth on Android that do not follow the recommendations set out in rfc8252 (https://tools.ietf.org/html/rfc8252) , and sadly they appear in the top results on stack overflow more often than the correct implementations. Even people who ought to know better will implement or recommend a less secure solution. It’s no wonder then that M4: Insecure Authentiation remains one of the top 10 mobile risks according to OWASP (https://owasp.org/www-project-mobile-top-10/2016-risks/m4-insecure-authentication).

If you’re an Android developer, and you have or will in future conceivably login with an oauth provider — please read this article. If you’re the kind of person who likes to skip to the end, make this article an exception to the rule — there are many pitfalls to implementation that are all too easy to ignore or overlook.

Common pitfalls

Using WebViews

rfc8252 says:

This best current practice requires that native apps MUST NOT use embedded user-agents to perform authorization requests and allows that authorization endpoints MAY take steps to detect and block authorization requests in embedded user-agents.

The reasons why not are also set out in the rfc but fall into two categories:

  • Third party applications using an oauth provider that are using embedded web views will be in full control of the user’s username and password input, and should be discouraged as they would be able to bypass the entire oauth flow. For example, the application can capture credentials and submit it on behalf of the user bypassing the specific grants that the user would wish to give to the application
  • Even when the application is a ‘first party’ application this approach should be discouraged as it violates the principle of least privilege. In addition, embedded webviews often do not allow the user to use their learned reflex to not enter credentials when the sites’ certificates are not validated as it is not possible to see such errors in a webview

Not using PKCE

We will discuss what PKCE is in more detail, but suffice to say for now that it’s a crucial part of preventing malware on an Android phone from intercepting and using authorization codes that are not meant for it. Often, you will find that PKCE is not implemented by the oauth provider and instead they are relying on flows that are designed for other types of application — namely server to server flows.

Not validating the state

This is a bit more of a softer issue than the above, and is referenced in the RFC as

recommends using the "state" parameter to link client requests and responses to prevent CSRF (Cross-Site Request Forgery) attacks

When we first redirect the client to their browser, we should use the ‘state’ variable, and when we receive the response we should verify the state variable given to us and not use the request token if it was not.

Using private URI schemes rather than claimed URI schemes

This is another ‘soft’ issue but it is detailed in the rfc as

App-claimed "https" scheme redirects are less susceptible to URI interception due to the presence of the URI authority

App claimed https is basically a way of ensuring that your app is the only app that is able to intercept the response of the auth token. The fact that we use private URI schemes (which any app can request to handle and can therefore intercept the authorization code) is actually mitigated by the use of PKCE — however, an approach using a claimed URI scheme AND PKCE is the best approach.

The dos

The reason I’ve started with the don’t is because these implementations are ubiquitous on stackoverflow and other such sites. We will walk through a sample implementation using auth0 as our oauth server, and look at how we can follow the oauth steps by hand.

Setup

First sign up for an auth0 account at https://auth0.com/signup, pick your domain and region

For our demo, select personal in the next step. Create a new application — and leave it as ‘Native’ which is the default option. In that application, go to settings and note down the client id and the domain.

Our aim for the rest of the tutorial is to follow the process set out in the RFC to login to the auth0 site.

Our first step is to build the URL. The url consists of three bits which are static (the client ID, the domain and the redirect URI) and two bits that are generated at runtime.

I’ve defined an interface like so:

interface OauthRepository {
fun getOrCreateToken(): OauthToken
fun reset()
fun validateState(state : String) : Boolean
}

Let’s start implementing it

class DefaultOauthRepository : OauthRepository {

private var oauthTokenRequest: OauthToken? = null

override fun getOrCreateToken(): OauthToken {
val token = generateString()
val state = generateString()

return oauthTokenRequest ?: OauthToken(
state,
token,
shaToken(token)
).also {
oauthTokenRequest = it
}
}

override fun reset() {
oauthTokenRequest = null
}

override fun validateState(state: String): Boolean {
return state == getOrCreateToken().state
}

private fun generateString() = ""

private fun shaToken(state: String) = ""

}

The code so far is fairly straightforward. We want to generate a token if it doesn’t exist, and reset when we’re done with it. We call ‘generateString’ twice, the first is for the state.

The role of the state

The state is used as mentioned in our ‘pitfalls’ to protect against CSRF, when we build our URL we’ll have a parameter called state which is set to a random string. When we receive the token in response, we’ll check that the same state is given back to us as we gave in the request.

PKCE

PKCE stands for proof key for code exchange. It works like this;

  • We generate a random string (43 to 128 bytes) we call this the ‘code_verifier’
  • We perform a SHA256 hash on the ‘code_verifier’ in order to produce the ‘code_challenge’
  • We append the ‘code_challenge’ to our URL.

The rest of the flow will involve a user signing in to the website in the browser, and the browser redirecting back into the application with the code as a parameter (and also the state). We then exchange that ‘code’ with the server via a POST request — when we make that request, we also pass the ‘code_verifier’

The server will check take the code_verifier you send in your request, look at the code you have sent, find the code_challenge you sent earlier, perform a SHA256 hash on the code_verifier you provided and check it matches the code_challenge you made when first requesting the auth.

This, seemingly convulted process means that if an application was able to intercept the response from the browser, say, by registering for the same redirect URI as your app, then they would not be able to retrieve any access token from the server, because they would not know the code_verifier that you generated. In fact, even if they were able to read the code_challenge you sent in the URL, they still would not know the code_verifier and would therefore not be able to exchange the code for a token.

On with the dev

We generate the random string for both the state and PKCE in the following way

private fun generateState() = with(SecureRandom()) {
val bytes = ByteArray(32)
nextBytes(bytes)
Base64.encodeToString(bytes, Base64.URL_SAFE or Base64.NO_WRAP or Base64.NO_PADDING)
}

This is as per the recommnedation in my second favourite RFC: https://tools.ietf.org/html/rfc7636

It is RECOMMENDED that the output of a suitable random number generator be used to create a 32-octet sequence. The octet sequence is then base64url-encoded to produce a 43-octet URL safe string to use as the code verifier.

Then our SHA token:

private fun shaToken(state: String) =
with(MessageDigest.getInstance("SHA-256")) {
update(state.toByteArray(Charset.defaultCharset()))
Base64.encodeToString(digest(), Base64.URL_SAFE or Base64.NO_WRAP or Base64.NO_PADDING)
}

We’ll build our URL like this:

private val url by lazy {
with(oauthRepository.getOrCreateToken()) {
"https://${OauthRetrofitService.BASE_LOGIN_URL}/authorize?response_type=code&code_challenge=${shadToken}&code_challenge_method=S256&client_id=${OauthRetrofitService.CLIENT_ID}&redirect_uri=${OauthRetrofitService.REDIRECT_URI}&scope=openid&state=${state}"
}
}

Note the BASE_LOGIN_URL CLIENT_ID and REDIRECT_URI are all from your auth0 account — on the subject of a REDIRECT_URI, let’s set that up now.

Reopen auth0 and your app, click settings and set your “Allowed callback URLs to be

myschema://com.my.schema

Remember what you put here; we’ll refer to “myschema” as the scheme, and com.my.schema as the host.

Now, I’ve added a button and overall my ‘login’ fragment looks like this:

class OauthLoginFragment : Fragment(R.layout.fragment_oauth_login) {

private val oauthRepository: OauthRepository by inject()

private val url by lazy {
with(oauthRepository.getOrCreateToken()) {
"https://${OauthRetrofitService.BASE_LOGIN_URL}/authorize?response_type=code&code_challenge=${shadToken}&code_challenge_method=S256&client_id=${OauthRetrofitService.CLIENT_ID}&redirect_uri=${OauthRetrofitService.REDIRECT_URI}&scope=openid&state=${state}"
}
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)

btnLoginOrCreateAccount.setOnClickListener {

val openBrowserIntent =
Intent(Intent.ACTION_VIEW, Uri.parse(url))
startActivity(openBrowserIntent)
}
}
}

You will of course have your own way of triggering the request — but this will open the browser to handle the web request. The next step is to create an activity to handle the response.

Response code handling

We’re going to actually implement something that we said in our pitfalls that we wouldn’t do — because it’s easier to get going — and then fix it later. First, create a new activity, and then in your manifest register it like this:

<activity android:name=".features.authentication.oauth.OauthRedirectActivity">
<intent-filter android:label="Oauth URI redirect">
<action android:name="android.intent.action.VIEW" />

<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />

<data
android:pathPrefix="/"
android:host="com.my.schema"
android:scheme="myschema" />

</intent-filter>
</activity>

Remembering your schema from before. It’s been a while since we ran the app so let’s just stub that activity so we can see what’s going on.

class OauthRedirectActivity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

val code = requireNotNull(intent?.data?.getQueryParameter("code"))
val state = requireNotNull(intent?.data?.getQueryParameter("state"))

Log.v("code", code)
Log.v("state", state)

}

Run the app, hit the button, create an account / login and then check your logs. At this point — if all’s well — you will see the token and state in your logs.

Getting the token

First, verify the state:

private val oauthRepository: OauthRepository by inject()if (oauthRepository.validateState(state)) {}

Then, we have an implementation of a retrofit service we can use to talk to the server:

data class OauthTokenResponse(
@SerializedName("access_token")
val accessToken: String,
@SerializedName("expires_in")
val expiresIn: Int,
@SerializedName("id_token")
val idToken: String,
@SerializedName("refresh_token")
val refreshToken: String,
@JsonProperty("token_type")
val SerializedName: String
)

data class OauthTokenRequest(
@SerializedName("grant_type") val grantType : String="authorization_code",
@SerializedName("client_id") val clientId : String,
@SerializedName("code_verifier") val codeVerifier : String,
@SerializedName("code") val code : String,
@SerializedName("redirect_uri") val redirectUri: String
)


public interface OauthRetrofitService {
companion object {
const val BASE_LOGIN_URL = ""
const val CLIENT_ID = ""
const val REDIRECT_URI = ""
}
@POST("/oauth/token")
suspend fun getToken(
@Body request: OauthTokenRequest
): Response<OauthTokenResponse>

}

And if we build this in our activity:

val oauthRetrofitService = Retrofit.Builder()
.baseUrl("https://${OauthRetrofitService.BASE_LOGIN_URL}")
.addConverterFactory(GsonConverterFactory.create())
.client(
OkHttpClient.Builder()
.addInterceptor(HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.BODY
}).build()
)
.build()
.create(OauthRetrofitService::class.java)

Then call it

val response = oauthRetrofitService.getToken(
OauthTokenRequest(
clientId = OauthRetrofitService.CLIENT_ID,
code = code,
codeVerifier = oauthRepository.getOrCreateToken().token,
redirectUri = OauthRetrofitService.REDIRECT_URI
)
)

We can see the second phase of our PKCE exchange here, we are sending the code that we retrieved from the app redirect and also the original code_verifier that we generated earlier.

In response, get a standard Oauth response, and we’ll use that typically by appending the access_token to the Authorization header, prefixed with “Bearer”.

There’s one more step we need to follow — which is claiming our URI Scheme. The bulk of the work involves claiming the URI on a backend server and so I will leave you in the capable hands of the android developer edocs for this particular issue

Enjoy

--

--

No responses yet