It was a lot of fun to integrate your app with Google Calendar so your users don’t have to switch between them all the time or duplicate the information. Setting it up was a breeze. And you and your coworker Bill could quickly work together on developing and testing your app’s retrieval and display of calendar events. The problem is that you and Bill did it manually only once before and have since forgotten what you did. But now you have to code and test that process to make it work for others, too.
The coding is not exactly rocket science. Just make a couple of requests to an API and process the responses. But the tests. The tests have you and Bill up in arms. How are you to test oAuth authentication? What’s there to test anyway? It’s not like Google hasn’t tested their code!
Well, that’s exactly what this post is going to tell you. You’ll learn why it is essential to test your oAuth client code properly and what you need to pay attention to designing your tests.
But first, let’s clear up a common misunderstanding.
Shouldn’t Authentication Be Authorization?
It depends. Both exist. And both (can) use oAuth.
The two sound similar, and many people, even techies, regularly mix them up, but they are indeed quite different beasts. Authentication is about gaining access. Authorization is about permissions.
This post is about testing oAuth client code, which is the code you write so a user can give you permission to connect with another app on their behalf. So it’s about authorization. Perhaps, the title should be “How to test oAuth Authorization,” but many people say authentication when they mean authorization, so let’s not force the issue.
Why You Need to Test More
You may think you’re done testing an oAuth authorization flow when the tests show that your code can retrieve a protected resource after its owner grants their permission. However, that’s just the start.
When your app is web-facing—that is, visible or accessible to anyone on the internet—it’s open to hackers. And bear in mind that hackers may not be interested in your app and its users’ data as much as they are in what your app can help them reach: the apps you connect to. That’s because when your oAuth client code is less than secure, it can be used to gain malicious access to those other apps.
Security breaches are the stuff of nightmares. They have caused companies to lose their reputation almost overnight. You really don’t want your app to be the gateway used to hack a high-profile site.
When that happens, you duck and run.
That’s why verifying the authorization calls work correctly is only the start. You also need to verify that your code is countering these threats properly.
Fortunately, countering these threats is more straightforward than understanding them. The oAuth specification includes fields in both requests and responses that are intended specifically for this purpose.
Finally, it’s all too easy to produce code that does exactly what you want in the right circumstances but leaves doors open in unforeseen circumstances, such as the authorization server not being available. So, you also must verify that your client code responds appropriately to error conditions.
Verify Your oAuth Client Is Fit for Use
Here, you’ll learn how to put your oAuth client code through its paces and verify that it takes proper measures to counter the threats it faces. To do so, let’s delve into the most used oAuth flow: the authorization code flow.
Before we get into things, though, you should be aware you only want to use this server-side because the authorization code flow openly uses the identifier and secret of your oAuth client. A client-side app (e.g., Javascript executing in a browser) is incapable of keeping that identifier and secret safe, no matter how much minification and obfuscation you use. Use PKCE in client-side apps!
Now, let’s take a look at what you need to test to verify your implementation for both steps in the authorization code flow and for the retrieval of a protected resource. To keep the list of tests as clear as possible, I added details for some in a “Notes” section below. The numbers in parentheses in the lists of tests correspond to the numbers in that section.
Step 1: Requesting Permission
Requesting permission involves a request to and a response from an authorization server. The request is known as the “Authorization Code” request as the response contains an authorization code that you need to use in the second step.
Test your implementation by verifying that your code
- Reads the client_id and redirect_uri from a secure location. (1)
- Uses the client_id and redirect_uri it read in the request it sends. (2)
- Puts a unique, non-guessable value in the state field of the request.
- Uses a unique, non-guessable value in the state field for each and every authorization code request. We’ll discuss this more in the paragraphs below this list.
- Puts additional scope values you need for your app in the request (if applicable).
- Only continues with requesting an access token when the state field of the response it receives contains the exact same value as it sent. In other words, if anything goes wrong or the states sent and received don’t match, it continues as if access was denied or not yet sought.
The state field is only marked as recommended by the oAuth specification, but it is instrumental in countering CSRF (cross-site request forgery) attacks. Authorization servers must return it unchanged in their response. A hacker can easily fake a response if the value for the state field is always the same or can be guessed or constructed from other data in the request.
The way you prevent becoming a party in an attack is by using a unique and non-guessable value in each and every request and by checking that the value in the response exactly matches what you sent. And, of course, crying wolf if it doesn’t.
Step 2: Exchanging Authorization Code for an Access Token
The second step in the authorization code flow is making a request to the authorization server to exchange the authorization code from step one for an access token that can be used to retrieve protected resources.
Test your implementation by verifying that your code
- Reads the client_id, client_secret, and redirect_uri from a secure location. (1)
- Uses the client_id, client_secret, and redirect_uri it read in the request it sends. (2)
- Uses the authorization_code it received in the response to its authorization code request.
- Puts the client_id and client_secret in a POST body when the service supports this. (3)
- Only continues down the happy path when it receives a response and that the response contains an access_token. In other words, if anything goes wrong, your code continues as though access was denied or not yet sought.
- Stores the access_token in a secure location.
- Stores any refresh_token it received with the access_token.
Step 3: Retrieving Resources
After access is granted and the authorization code has been exchanged for an access token, requests need to present that token to retrieve protected resources.
Whenever you use an access token, you can receive a “token expired” response. If you received a refresh token along with the access token (in step two), you can get a new access token using the “refresh token” request (see section 4). If you didn’t receive a refresh token, you’ll have to get your user to give you permission again using step 1.
Test your implementation by verifying that your code:
- Uses the last access token issued by the authorization server for the current user.
- Tries to get a new access token when it receives a “token expired” response and a refresh token was received together with the access token. (4)
- Only retries the resource request when refreshing the access token was successful.
- Only continues handling the resource when it receives an OK response on the resource request, either the original or the one after refreshing the token. In other words, if anything goes wrong, don’t do anything that could result in strange, unexpected behavior and possibly leaking sensitive information, such as processing an empty resource.
Step 4: Refreshing an Access Token
Access tokens can and do expire. When an authorization server sends both an access token and a refresh token in its response to the “exchange code for access token” request, you can get a new access token by using that refresh token in a “refresh token” request.
Test your implementation by verifying that your code:
- Reads the client_id and client_secret from a secure location. (1)
- Uses the client_id and client_secret it read in the request it sends. (2)
- Uses the last refresh token issued by the authorization server for the current user.
- Puts the client_id and client_secret in a POST body when the service supports this. (3)
- Only continues replacing the tokens and retrying a resource request when it receives a response and the response contains an access_token. In other words, if anything goes wrong it continues as though access was denied or not yet sought.
- Replaces the access_token in a secure location.
- Replaces the refresh_token if one was received.
- Does not replace the refresh_token if none was received. In this case, it is assumed the original remains valid.
Notes
(1) Sensitive Data
As you are already aware, before you can even begin to send authorization requests to an authorization server, you need to register your client app with it. When it’s successful, you’ll receive a client identifier and client secret, which you’ll need to identify and authenticate your app to the authorization server.
Obviously this is sensitive data. Anyone who gets their hands on it can pretend to be your application. That’s why your app’s client identifier and client secret shouldn’t be in source code!
In most cases, the authorization server will require you to specify a uri where it will send your user after they have allowed (or denied) access. This redirect_uri is fixed. It’s not possible to use a different one for each request. This is to ensure that hackers can’t make the authorization server send your users somewhere else.
(2) Superfluous or Wise?
It may feel superfluous to check that the value your code just read from storage is used in the request for which it read it, but the fact that your client reads a value from a secure location doesn’t guarantee that it uses it in the request it sends. Even if the code is there to do so. Overwriting it with a static value can help in debugging. It makes it easier to recognize the requests in logs for example. And you really don’t want to know how often that kind of debug code makes it into production (yes, despite code reviews.)
(3) Bodies Are Better Than Heads
A client app needs to authenticate itself with the service to get a token. Typically, you’d use HTTP basic authentication. But that requires you to put client_id and client_secret in the request’s query parameters. And then they become part of the uri for the browser. Putting them in a request body makes them less obvious. The fact that any authorization server worth talking to uses HTTPS should do the rest.
(4) Authorization Servers Don’t Always Play Ball
An authorization server doesn’t have to provide a refresh token along with the access token. When you don’t receive a refresh token, you’ll need to ask your user again for permission to connect to the service.
Time to Start Testing
That’s it.
There are plenty of step-by-step guides out there that’ll tell you how to test oAuth authorization flows using tool X, Y, or Z. Some talk about security threats, and many don’t.
You are now way ahead of anybody following these guides. You know what to test, so no one can inadvertently change your oAuth client code and jeopardize the security of your app. Putting that knowledge into practice will help keep you from landing in hot water.
So, go ahead and put your oAuth client code under test. And if you want to know how easy it is to create those tests using Testim, head over to the Testim application’s sign up page and start your free trial.
This post was written by Marjan Venema. Marjan is a Smart Blogger certified content marketer, working from the Netherlands with over 30 years of experience in software requirements, analysis, development, and support for the Business Planning and Analytics, Financial and Manufacturing industries. Her specialty is writing comprehensive engaging content that makes complicated and complex topics easy to understand and consume. She writes straightforward language in a conversational style and illustrates abstract topics with concrete examples.