Using VCR.py with MSAL

Using VCR.py with MSAL

Once upon a time I decided to write some integration tests with help of VCR.py. VCR.py simplifies and speeds up tests that make HTTP requests. It records all HTTP interactions in a cassette file. When you run the same code again, it will replay all HTTP interactions from a cassette file. Sounds great for writing tests! Little did I know.

My HTTP requests involved authentication to Azure AD. I am using MSAL library. By the time I started with VCR, I have already spent some time trying to authenticate in an easy-for-user manner. In the end, integrated windows authentication beat me hard requiring two factor authentication. I felt frustrated, but at the same time happy that running tox resulted in all green colors. Until I committed the code and then run the tests some time later.

Oh, ah! What's going on?!

======== short test summary info =========
FAILED test_application.py::TestApplication::test_application_list - AssertionError: The command failed. Exit code: 1
======== 1 failed in 6.67s ===============
ERROR: InvocationError for command 'C:\some\.tox\py37\Scripts\pytest.EXE' test_application.py --junitxml=.tox/junit.py37.xml (exited with code 1)
______ summary ___________________________
ERROR:   py37: commands failed

There was an actual error message 9. The current time MUST be before the time represented by the exp Claim. id_token was. It gave me a hint, but since I normally do not deal with authentication on a low level, I spent some time trying various things.

To spare your time, here is a recipe. The problem was in id_token saved in HTTP response. id_token is a string like this

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsb2dnZWRJbkFzIjoiYWRtaW4iLCJpYXQiOjE0MjI3Nzk2Mzh9.gzSraSYS8EXBxLN_oWnFSRgCzcmJmMjLiuyu5CSpyHI

This is a JWT token, consisting of 3 parts separated by . character. The decoded data contains expiration date, which of course is in the past for a pre-recorded interaction.

Since I am in Python, I studied the code of MSAL library that deals with the token and found out that it only decodes the payload part (at least in the function that fails). My wild attempt would be to adjust expiration time, create a new token based on the current one and hope that MSAL will accept it.

Here is the code that does just that

import time
import json
import base64
from msal.oauth2cli.oidc import decode_part


def adjust_token_time(id_token: str) -> str:
    header_enc = id_token.split('.')[0]
    payload = json.loads(decode_part(id_token.split('.')[1]))
    signature_enc = id_token.split('.')[2]

    payload["exp"] = round(time.time() + 80)  # seconds
    payload = json.dumps(payload).encode("utf-8")

    paylod_enc = base64.urlsafe_b64encode(payload).decode().strip('=')
    token = ".".join([header_enc, paylod_enc, signature_enc])
    return token

New expiration time must not exceed 2 minutes. Otherwise one gets a different MSAL error. You can see that my new token is a very dirty forgery. Nevertheless it works and I am happy for now.

This function must be executed as part of pre-processing when VCR.py plays the cassette.