Table of Contents | ||
---|---|---|
|
OAuth2 Protocol
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 authentication transaction results in a valid "access token" that the client uses for accessing resources on the "resource server".
...
- the client is the mobile device
- the resource owner is the user using the mobile device
- the resource server is LMS or any service hosted on the edX platform that exports a public API to be called by the mobile app
- the shared credential is either
- the user's "password" (if the user has an edX password), or
- the user's "social auth ID" (for 3rd party authentication)
- the authorization server is either
- LMS (if authenticating with the user's password), or
- a 3rd party server (if authenticating with a 3rd party social auth)
- the access token is used to authenticate the user in subsequent API calls
Note: We use the term "authentication" 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 the owner's 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.
Mobile API Authentication Classes
...
Additionally, while most edX views support session authentication, some of those APIs are decorated with the SessionAuthenticationAllowInactiveUser authentication class, which also bypasses email verification. For example, the enrollment API doesn't require the user to be "active" (i.e., doesn't need a verified 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).
OAuth2 Access Tokens
The mobile app obtains an edX-issued access token in either of the following ways:
...
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 the following 2 types based on their ability to confidentially store client (not user) credentials:
Confidential
Client
Type
Clients capable of maintaining the confidentiality of their credentials (e.g., client implemented on a secure server with restricted access to the client credentials), or capable of secure client authentication using other means.
Public
Client
Type
Clients incapable of maintaining the confidentiality of their credentials (e.g., clients executing on the device used by the resource owner, such as an installed native application or a web browser-based application), and incapable of secure client authentication via any other means.
...
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).
Here are the relevant links in the django-oauth2-provider code-base:
...
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:+10.4. Refresh Tokens+
*Authorization servers MAY issue refresh tokens to* web application
clients and *native application clients*.
...
*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.
...
Determine
...
No Format |
---|
Authorization servers MAY issue refresh tokens to web
application clients and native application clients. |
However, it goes on to suggest that the server should detect unauthorized refresh token usage:
No Format |
---|
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 a new access token.
Note: The 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?
The main difference between a refresh token and an access token is that the former is used in transactions solely with the Authorization server, while the latter is (obtained from the Authorization server but) presented to the Resource server. The advantages of decoupling the tokens are: performance optimization, scalability, and revocation.
- Scalability - If access tokens are self-contained and implicitly verifiable by the Resource server, the Authorization Server would not be a single point of failure.
- Performance - Similarly, verifying an access token minimizes connections to the Authorization server and its database.
- Revocation - Compromised refresh tokens and refresh token of deactivated users can be centrally revoked, although there is a window of time during which all published access tokens may still be in use until they expire.
Given the fact that our Authorization server and the Resource server are one and the same (at this time), it is unclear whether having refresh tokens is a hard requirement for the edX mobile apps (at this time). Furthermore, the current django implementation requires querying the database every time an access token is verified since they are randomly generated values and not self-contained signed values, so the performance argument doesn't hold water. And since all tokens are kept in the database with foreign key relationships to the user, it would still be easy to centrally revoke all tokens associated with a user.
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
- Upgrade our django OAuth library to django-oauth-toolkit.
- Update client-side code as follows:
- send API request with access token
- If access token is invalid, try to update it using refresh token
- if refresh request passes, update the access token and re-send the initial API request
- If refresh request fails, ask user to re-authenticate
Expiration Values
- Set the default expiration for Access Token to 1 day (the accepted amount of time for a user to continue to use an unexpired token even after revocation).
- Set the expiration time for Refresh Tokens to 2 weeks (analogous to our session cookies).
Client Rollout Plan
- 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 have support for refresh tokens.
- Run a script to extend the access token expiration time for all old mobile Clients by 100 years.
- 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?)