Mobile authentication with JWTs

Context

  1. Mobile needs a way to call some ecommerce payment APIs using JWT tokens.

  2. Mobile is switching the mobile authentication flow from opaque (Bearer) access tokens to JWTs to simplify the edx-platform authentication story.

  3. Ownership: 

    • Arch-BOM squad owns Open edX authentication. 

    • Security Working Group has security expertise.

    • Mobile squad owns mobile experience, including authentication.

Current Proposal

  • Implement switch from opaque access tokens (Bearer) to JWT access tokens by making the following changes:

    1. Request the initial access token be returned using the JWT format rather than the opaque (Bearer) format.

    2. Update the exchange of 1st party access token for new login session to accept JWT in addition to Bearer access token.

    3. Update the exchange of 3rd party access token for 1st party access token to accept JWT in addition to Bearer access token.

Documenting Decision

We must write an ADR (Architectural Decision Record) to capture the current context, any decisions being made, and their consequences.

Bearer to JWT Proposal Feedback

It is important to get this right such that we don’t introduce any additional security risks.

Token Expiration

One major difference between JWT and Bearer access tokens is that the JWT is non-revocable. There is no database lookup for the JWT access token and it is simply trusted if found to be valid. 

One consequence of this is that a JWT should have a short lifetime in order to limit security risks if the token is hijacked.

Currently, the JWT access token has a 10 hour lifetime, but it should have a maximum lifetime of 1 hour (matching the current lifetime of JWT cookie).

Private ticket for this work:

User Account Status

For 1st-party token exchange for session login using JWTs, it is especially important to ensure that the user account has not been disabled, since the JWT is non-revocable. We can use Django’s has_usable_password for this.

This new check should apply to either token type. Also note that if an account is disabled, any active session is removed as explained here.

Password Grant Check

For the currently proposed 1st-party token exchange for session login using JWTs, we would need an equivalent check for _is_grant_password to not expand permissiveness of the endpoint. 

We will add the grant type into the JWT payload, so that it can be checked when used.

Asymmetrically Signed JWT

In addition to the password grant check, we may need to check if the JWT was asymmetrically signed by the LMS.

If a JWT can be created (signed) by another IDA, then if that IDA is compromised, the attacker will be able to create JWTs that are accepted by all the other IDAs, including the LMS. If those JWTs can then be exchanged for session cookies, this would allow for full compromise of any user.

Asymmetric signing indicates that we're probably distributing the public (verification) key to the other IDAs and only exposing the private (signing) key to the LMS, which is very important if JWTs can be "escalated" to sessions.

Decision:

  • Add method to edx-drf-extensions like get_decoded_jwt_from_auth, but that will decode only asymmetric JWTs.

    • Probably requires a call to decode or decode_complete, providing a list of just the asymmetric algorithms.

    • It would be nice if we could provide a good error message for symmetric JWTs.

  • Configure mobile oauth client as a restricted client and test that it gets asymmetric JWTs.

Rejected Alternative:

  • Complete (marked as Done, but is unfinished) so the LMS only supports symmetric JWTs. Note: although this is the right way to go, it will probably take too long, so we will continue to defer.