Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.

Table of Contents
maxLevel3

OAuth2 Protocol and JWT

The mobile app uses the standard OAuth 2.0 protocol for authenticating users to the open edX LMS. The OAuth2 protocol supports authenticating the "client" and/or the "resource owner" by verifying "shared credentials" that were exchanged out-of-band with the "authorization server".  A successful transaction results in a valid "access token". This access token is then used to create a JWT token that the client uses for accessing resources on the "resource server" on behalf of the "resource owner".

...

Note: We use the term "authenticate" loosely here since we are not necessarily verifying the identity of the caller for all API calls.  In fact, it's possible that the user is not even present when an API call is made.  This is why the OAuth protocol is actually considered an "authorization" framework (or "delegation" to be more exact) since the "resource owner" authorizes the "client" to access resources on the "resource server" on its behalf by virtue of an "access token" obtained from an "authorization server".  However, I still prefer to use "authentication" in this context since from the resource server's perspective, the access token is simply used to "identify" the resource owner on whose behalf the request is made.  Further authorization-related matters such as permission-checking and ACLs are outside this scope.

Mobile API Authentication Classes

All APIs called by the edX mobile app are authenticated using the OAuth2AuthenticationAllowInactiveUser authentication JwtAuthentication authentication class.  It verifies the caller has a valid OAuth2 token and bypasses verification of their email address.  For a streamlined on-boarding experience, the mobile app supports ongoing usage of its features without ever requiring the user to verify their email address.

...

Note: The confusion over "active"/"inactive" versus "email-verified"/"non-verified" users strives from the fact that the "is_active" field on a user object is dual-purposed for both user states: email verification (user-initiated) and user deactivation (devOps-initiated).  This means that if we do deactivate users (devOps-initiated), that would also be bypassed by OAuth2AuthenticationAllowInactiveUser.

OAuth2 Access Tokens

...

Transition from Bearer tokens to JWT tokens

Edx mobile apps have been transitioned to use JWT tokens to access edx API's and Bearer tokens have been deprecated. JWT token is obtained by passing token_type=JWT in the payload to the /oauth2/access_token/ API as shown below in the document.

To support this transition, a temporary Waffle Switch has been introduced called oauth_dispatch.disable_jwt_for_mobile. If this switch is enabled, JWT is disabled and hence the /oauth2/access_token/ API returns Bearer token in response. This switch is intended to be enabled only in case of any error/issue reported during JWT launch to mobile end users, and is by default kept disabled.

OAuth2 Access Token → JWT token

Access tokens are created for each login attempt from mobile, which are then used to create JWT tokens used by mobile app in api calls. The mobile app can issue JWT tokens in either of the following ways:

Example Response

The response from either of the above endpoints would provide the edX-issued access JWT token as follows:

{"access_token": "5e0a0cb315e66aa96bab910faa8c70ee0ca91236

eyJhbGciOiJIUzI1NiJ9.eyJhdWQiOiAiU0VULU1FLVBMRUFTRSIsICJleHAiOiAxNjY5MTEyNDMyLCAiZ3JhbnRfdHlwZSI6ICJwYXNzd29yZCIsICJpYXQiOiAxNjY5MTA4ODMyLCAiaXNzIjogImh0dHBzOi8vaWFwLnNhbmRib3guZWR4Lm9yZy9vYXV0aDIiLCAicHJlZmVycmVkX3VzZXJuYW1lIjogImFkbWluIiwgInNjb3BlcyI6IFsicmVhZCIsICJ3cml0ZSIsICJlbWFpbCIsICJwcm9maWxlIl0sICJ2ZXJzaW9uIjogIjEuMi4wIiwgInN1YiI6ICIxOWU3N2ZlMTgyYTEwZDNjMjAwOTRlNDRjNjFjOTlmZiIsICJmaWx0ZXJzIjogWyJ1c2VyOm1lIl0sICJpc19yZXN0cmljdGVkIjogZmFsc2UsICJlbWFpbF92ZXJpZmllZCI6IHRydWUsICJlbWFpbCI6ICJhZG1pbkBleGFtcGxlLmNvbSIsICJuYW1lIjogIiIsICJmYW1pbHlfbmFtZSI6ICIiLCAiZ2l2ZW5fbmFtZSI6ICIiLCAiYWRtaW5pc3RyYXRvciI6IHRydWUsICJzdXBlcnVzZXIiOiB0cnVlfQ.7FK80Fj8DLXEEFiXvA3pZfxkgfuK7cvOSrggRzjxa_o

", "token_type": "BearerJWT", "expires_in": 2591999, "scope": ""}

Authorization

...

JWT

Once an access JWT token is obtained, it can be used to authenticate the user in any API call that supports the OAuth2AuthenticationAllowInactiveUser authentication JwtAuthentication authentication class.  The access JWT token is passed in the Bearer JWT field of the Authorization HTTP header, as follows:

Expiration

Currently, our edX-issued access JWT tokens expire in 30 days for public clients like mobile apps, and 365 days for server-side clients1 hour for mobile apps.  These values can be overridden with the settings variables OAUTHJWT_EXPIRE_DELTA_PUBLIC (for public clients) and OAUTH_EXPIRE_DELTA (for confidential clients), which each take a timedelta object for their value.

...

ACCESS_TOKEN_EXPIRE_SECONDS.

JWT -> Session Cookie

Additionally, the mobile app can exchange an access a JWT token for a session cookie that can be used in a WebView:

Note: It looks like the access_token endpoint (above) also returns session cookies along with the user's access_token.  However, this endpoint allows the client to refresh the session cookie without needing the user's credentials.

Expiration

Currently, the returned session cookie from this transaction expires in 2 weeks.

Note: the session cookie obtained on the edX website expires in 2 weeks if Remember-Me is selected, otherwise, it never expires.

OAuth2 Client Type, Client ID, and Client Secret

The OAuth2 RFC categorizes clients into 2 types based on their ability to confidentially store client (not user) credentials. 

...

Note: An authenticated client (and its Client Secret) is needed only for the Authorization Code and Client Credentials OAuth2 grant types.  The Implicit and Password grant types don't require an authenticated client.  The last (Password) grant type is what our mobile apps use.

OAuth2 Refresh Tokens

Since OAuth2 access tokens have a limited lifetime designated by their "expires_in" value, there has to be a strategy for what happens when they expire.  The OAuth2 RFC has provision for this by introducing Refresh Tokens as a means to re-authenticate with the authorization server in exchange for fresher access tokens.

Once a JWT token expires, the Refresh Token can be used to create a new access token and acquire a new JWT token from it.

Since possession of a refresh token authorizes requests for reusable access tokens, the refresh token is a proxy for the user's credentials and must be stored in a protected user-specific location (along with the user's access token).  From the OAuth2 and security standpoint, storing a refresh token is preferable over storing a user's raw credentials (passwords, etc). 

Django and Refresh Tokens

While the django library that we use for OAuth2, does support refresh tokens, it does not support Refresh Tokens for Public type Clients for the Password grant type (i.e., not for our mobile apps). 

...

However, the latest django-oauth-toolkit via the OAuthLib library (which we would like to migrate to) does support Refresh Tokens for Public type Clients for the ResourceOwnerPasswordCredentialsGrant.

OAuth2 on Refresh Tokens for Public Type Clients

The RFC does explicitly call out the support for refresh tokens for public Clients in Section 10.4.  Here is the relevant quote from the spec:

...

No Format
nopaneltrue
     When client authentication is not possible, the authorization server 
     SHOULD deploy other means to detect refresh token abuse.

     For example, the authorization server could employ refresh token
     rotation in which a new refresh token is issued with every access
     token refresh response.  The previous refresh token is invalidated
     but retained by the authorization server.  If a refresh token is
     compromised and subsequently used by both the attacker and the
     legitimate client, one of them will present an invalidated refresh
     token, which will inform the authorization server of the breach.

django-oauth-toolkit Implementation

The django-oauth-toolkit handles refresh tokens with multiple devices and access tokens correctly (not initially, but was eventually fixed).  In essence, it is implemented as a single-use Refresh Token per Access Token.  Whenever a new access token is requested with a refresh token, the presented refresh token is deleted and a new refresh token is returned along with the new access token.

Note: Old access tokens, however, remain in the database even if they have already expired.

Why not long-lived Access Tokens?

It is generally recommended to have short-lived access tokens and longer-lived refresh tokens.  But why?

...

Note: Another point to note is that as the edX platform shifts from a monolith to a more distributed microservices architecture, we plan to use OAuth + JWTs and/or a variant of OpenIDConnect as our authentication framework.  With that, JWT tokens will also be present and most probably short-lived.

Proposal for Refreshing Tokens on edX mobile apps

Unfortunately, the edX apps have been released without support for refreshing OAuth tokens, although the OAuth access tokens expire only a month after they are issued.  So for now, we have been extending the expiration date regularly every month using a devOps executed script.

Assuming we still want to refresh edX Access Tokens (see Why not long-lived Access Tokens?), here is an implementation proposal.

Code

  1. Upgrade our django OAuth library to django-oauth-toolkit, which would also affect:
    1. Insights
    2. E-Commerce
    3. Programs
  2. Update client-side code as follows:
    1. send API request with access token
    2. If access token is invalid, try to update it using refresh token
    3. if refresh request passes, update the access token and re-send the initial API request
    4. If refresh request fails, ask user to re-authenticate
  3. Update the Client IDs for the new apps (see Client Rollout Plan).

Expiration Values

  1. Set the default expiration for Access Tokens to 1 day (the accepted amount of time for a user to continue to use an unexpired token even after revocation).
  2. Set the expiration time for Refresh Tokens to 2 weeks (analogous to our session cookies).
    Note: Since a new refresh token is issued at every use, its lifetime indicates for how long a user need not use the app without being asked to log in again.  As long as the user continues to use the app within the lifetime of the refresh token, they never need to log in again.

Client Rollout Plan

  1. Create new Client IDs for edX-iOS-OAuth-v2-with-refresh and edX-Android-OAuth-v2-with-refresh to be used for the new versions of the mobile clients that will have support for refresh tokens.
  2. Run a script to extend the access token expiration time for all old mobile Clients by 100 years.
  3. Publicize the release of the new mobile apps and encourage old users to upgrade for "better security".  (Caveat: see Why not long-lived Access Tokens?)


Authentication Flow

The OAuth authentication and refresh flow for mobile in case of login or any API call. 

...