Oauth2 is one of the most common authentication systems on the web, especially for API access.

I found myself writing a bunch of tests against an external API recently. The API I was testing was accessed with OAuth2 authentication. To access the API first a valid Access Token needs to be retrieved.

First, I ran the authentication manually and copied and pasted the access token into my test. While that served my purposes there and then, I knew it was not a long-term solution and certainly couldn't be run in a CI environment.

After some research, the only approach that would work was to emulate a human going through the authentication process. I should add that this is for the OAuth2 “Code” flow, the most common of the OAuth2 flows.

The approach I ended up taking was to:

  1. Start an ad-hoc web server to listen for the OAuth callback.
  2. Use Selenium to run a headless browser to open the authorization URL and enter the username, password, and click the authorize button.
  3. Retrieving the token from the webserver started in step 1
  4. Saving the access token to a temporary file for quick access during development.
  5. Using it in a test

I’m working in Python but the principals would apply to other languages.

The code below will need to be customised to your needs. You will need to supply the correct endpoint URLs and the HTML elements will be named differently for your provider.

Install dependencies

pip install requests selenium pytest

Server & Selenium

Start the server in a thread to listen for the OAuth callback and then launch Selenium to perform the OAuth authentication flow.

Note: This uses sensitive information such as OAuth secrets and login passwords. Use best practices to keep these safe and ideally have a separate OAuth application and user specifically for testing. It is also advisable to exclude offline_access from the scopes as this will generate a long-lived refresh token which should also be treated as sensitive information.

"""Perform real OAuth authentication to get oauth credentials for interacting with the API* Start a simple web server to handle the OAuth callback* Open a scraper to the OAuth authorization URL* Mock the user clicking the authorize button"""import jsonimport osimport timefrom threading import Threadfrom http.server import BaseHTTPRequestHandler, HTTPServerimport requestsfrom requests.auth import HTTPBasicAuthfrom selenium import webdriverfrom selenium.webdriver.common.by import Byfrom selenium.webdriver.chrome.options import Options# hostname and port must match one of the redirect URIs in the app settingshost_name = "localhost"server_port = 8080client_id = os.getenv('CLIENT_ID')client_secret = os.getenv('SECRET')email = os.getenv('AUTHORIZATION_USER_EMAIL')password = os.getenv('AUTHORIZATION_USER_PASSWORD')tokens = None # global to hold resultdef click_authorize_button():    try:        authorize_url = f'https://example.com/authorize?response_type=code&client_id={client_id}&redirect_uri=http://{host_name}:{server_port}/callback&scope=openid&state=STATE'        chrome_options = Options()        chrome_options.add_argument("--headless=new")        driver = webdriver.Chrome(options=chrome_options)        driver.get(authorize_url)        email_field = driver.find_element(By.ID, 'email')        email_field.click()        email_field.send_keys(email)        password_button = driver.find_element(By.ID, 'password')        password_button.click()        password_button.send_keys(password)        login_button = driver.find_element(By.CLASS_NAME, 'btn-login')        login_button.click()        # may go to the approval page or redirect to the callback directly depending on the state of the session        try:            time.sleep(5)            authorize_button = driver.find_element(By.CLASS_NAME, 'btn-approve')            authorize_button.click()        except:            pass    except Exception as e:        print(e)class MyServer(BaseHTTPRequestHandler):    def do_GET(self):        global tokens        code = None        query = self.path.split('?')[1]        for part in query.split('&'):            if 'code' in part:                code = part.split('=')[1]                break        data = {            "grant_type": "authorization_code",            "code": code,            "redirect_uri": f'http://{host_name}:{server_port}/callback'        }        headers = {            "Content-Type": "application/x-www-form-urlencoded",        }        response = requests.post(            "https://example.com/token",            data=data,            headers=headers,            auth=HTTPBasicAuth(client_id, client_secret),        )        tokens = json.dumps(response.json())        self.send_response(302)        self.send_header('Location', 'http://example.com')        self.end_headers()class StoppableHTTPServer(HTTPServer):    def run(self):        self.serve_forever()def get_tokens():    server = StoppableHTTPServer((host_name, server_port), MyServer)    t = Thread(None, server.run)    t.start()    click_authorize_button()    server.shutdown()    t.join()    return tokensif __name__ == "__main__":    print(get_tokens())

Loading/Saving tokens

This function will call the above process to run the OAuth authentication and then cache the results to a file.

For development, this is handy because it saves going through the whole OAuth flow each time a test is run which is a massive speed improvement. At some stage the access token will expire, in this case, the test_tokens.json file can simply be deleted and the OAuth flow will be run again.

def oauth_credentials_setup():    """    Call to set valid oauth credentials for the Integration model    """    if os.path.exists('test_tokens.json'):        with open('test_tokens.json', 'r') as f:            tokens = f.read()    else:        tokens = get_tokens()  # runs oauth flow and returns tokens        with open('test_tokens.json', 'w') as f:            f.write(tokens)    # Do something with the tokens like inserting them into the relevant location in the database

Running a test

The below code will set up fresh credentials at the start of tests that need it

def test_task():    oauth_credentials_setup()    # Run the test

Alternatively, depending on your needs, the OAuth token may only need to be set up once at the start of the test run. In this case, it can be done like this:

@pytest.fixture(scope="session", autouse=True)def setup_oauth(request):  oauth_credentials_setup()

Conclusion

Automated testing using external OAuth API can be a challenge. I hope this code will help you on your testing journey