OAuth docs, including decisions

This commit is contained in:
Nimisha Asthagiri
2018-01-20 17:13:03 -05:00
parent 53a6358523
commit 293e4f895a
15 changed files with 1053 additions and 5 deletions

View File

@@ -0,0 +1,102 @@
OAuth Dispatch App (OAuth2 Provider Interface)
----------------------------------------------
The OAuth Dispatch app is the topmost interface to `OAuth2`_ provider
functionality. See decisions_ for its historical journey.
.. _OAuth2: https://tools.ietf.org/html/rfc6749
.. _decisions: decisions/
Background
----------
This section provides a few highlights on the code to provide a
high-level perspective on where different aspects of the OAuth2 flow
reside. For additional information, see `Open edX Authentication`_.
.. _Open edX Authentication: https://openedx.atlassian.net/wiki/spaces/PLAT/pages/160912480/Open+edX+Authentication
Provider code
~~~~~~~~~~~~~
* The oauth_dispatch_ app provides the top-most entry points to the OAuth2
Provider views.
* Its `validator module`_ ensures Restricted Applications only receive expired
tokens.
* Its `Access Token View`_ returns JWTs as access tokens when a JWT token_type
is requested.
* It uses an edX custom JwtBuilder_ implementation to create the JWT.
* The JwtBuilder_ uses the pyjwkest_ library for implementation of `JSON Web
Signature (JWS)`_ and other crypto to build and sign JWT tokens.
.. _oauth_dispatch: https://github.com/edx/edx-platform/tree/master/openedx/core/djangoapps/oauth_dispatch
.. _validator module: https://github.com/edx/edx-platform/blob/master/openedx/core/djangoapps/oauth_dispatch/dot_overrides/validators.py
.. _Access Token View: https://github.com/edx/edx-platform/blob/d21a09828072504bc97a2e05883c1241e3a35da9/openedx/core/djangoapps/oauth_dispatch/views.py#L89
.. _JwtBuilder: https://github.com/edx/edx-platform/blob/d21a09828072504bc97a2e05883c1241e3a35da9/openedx/core/lib/token_utils.py#L15
.. _pyjwkest: https://github.com/IdentityPython/pyjwkest
.. _JSON Web Signature (JWS): https://tools.ietf.org/html/draft-ietf-jose-json-web-signature-41
Clients & REST API Clients code
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
* edX services, including LMS, use the edx-rest-api-client_ library
to make OAuth2 client requests and REST API calls.
* Built on top of slumber_, the edx-rest-api-client_ provides
a utility to retrieve an access token from the LMS. Its Auth_
classes create appropriate HTTP Authorization headers with
*Bearer* or *JWT* insertions as needed.
* It makes use of the PyJWT_ library for cryptographically creating
JWT tokens.
* **Note:** Creation of JWT tokens in our system should only be done
by the OAuth Provider. This will break once we use *asymmetric* signing
keys, for which remote services will not have the private keys.
.. _edx-rest-api-client: https://github.com/edx/edx-rest-api-client
.. _slumber: https://github.com/samgiles/slumber
.. _Auth: https://github.com/edx/edx-rest-api-client/blob/master/edx_rest_api_client/auth.py
.. _PyJWT: https://github.com/jpadilla/pyjwt
Authentication by REST endpoints
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
* Recently created edX REST endpoints use the `Django Rest Framework (DRF)`_.
The REST endpoint declares which type(s) of authentication it supports
or defaults to the *DEFAULT_AUTHENTICATION_CLASSES* value in DRF's
*REST_FRAMEWORK* Django setting.
* edX REST endpoints that support JWTs as access tokens declare the custom
edX JwtAuthentication_ class in its DRF authentication_classes_ scheme.
* JwtAuthentication_ is implemented in the edx-drf-extensions_ library.
* JwtAuthentication_ extends the JSONWebTokenAuthentication_ class
implemented in the django-rest-framework-jwt_ library.
* JwtAuthentication_ is used to authenticate an API request only
if it is listed in the endpoint's authentication_classes_ and the
request's Authorization header specifies "JWT" instead of "Bearer".
* **Note:** The Credentials service has its own implementation of
JwtAuthentication_ and should be converted to use the common
implementation in edx-drf-extensions_.
* **Note:** There is also an auth-backends_ repo that should eventually
go away once Open ID Connect is no longer used. The only remaining
user of its EdXOpenIdConnect_ class is the edx-analytics-dashboard_.
.. _Django Rest Framework (DRF): https://github.com/encode/django-rest-framework
.. _JwtAuthentication: https://github.com/edx/edx-drf-extensions/blob/1db9f5e3e5130a1e0f43af2035489b3ed916d245/edx_rest_framework_extensions/authentication.py#L153
.. _authentication_classes: http://www.django-rest-framework.org/api-guide/authentication/#setting-the-authentication-scheme
.. _edx-drf-extensions: https://github.com/edx/edx-drf-extensions
.. _django-rest-framework-jwt: https://github.com/GetBlimp/django-rest-framework-jwt
.. _JSONWebTokenAuthentication: https://github.com/GetBlimp/django-rest-framework-jwt/blob/0a0bd402ec21fd6b9a5f715d114411836fbb2923/rest_framework_jwt/authentication.py#L71
.. _auth-backends: https://github.com/edx/auth-backends
.. _EdXOpenIdConnect: https://github.com/edx/auth-backends/blob/31c944289da0eec7148279d7ada61553dbb61f9e/auth_backends/backends.py#L63
.. _edx-analytics-dashboard: https://github.com/edx/edx-analytics-dashboard

View File

@@ -0,0 +1,32 @@
1. Record Architecture Decisions
--------------------------------
Status
------
Accepted
Context
-------
We would like to keep a historical record on the architectural
decisions we make with this app as it evolves over time.
Decision
--------
We will use Architecture Decision Records, as described by
Michael Nygard in `Documenting Architecture Decisions`_
.. _Documenting Architecture Decisions: http://thinkrelevance.com/blog/2011/11/15/documenting-architecture-decisions
Consequences
------------
See Michael Nygard's article, linked above.
References
----------
* https://resources.sei.cmu.edu/asset_files/Presentation/2017_017_001_497746.pdf
* https://github.com/npryce/adr-tools/tree/master/doc/adr

View File

@@ -0,0 +1,55 @@
2. Migrate to Django OAuth Toolkit
----------------------------------
Status
------
Accepted
Context
-------
The edX LMS uses the `Django OAuth Provider (DOP)`_ library in order to support
the OAuth2_ provider protocol. However, that library is now unsupported and
deprecated by the Django community. Additionally, the edX mobile apps, which are
OAuth2 clients of the LMS, need the capability to refresh tokens, which DOP does
not support for `Public Client`_ types that use `Password Credentials grant`_.
.. _OAuth2: https://tools.ietf.org/html/rfc6749
.. _Public Client: https://tools.ietf.org/html/rfc6749#section-2.1
.. _Password Credentials grant: https://tools.ietf.org/html/rfc6749#section-4.3
.. _Django OAuth Provider (DOP): https://github.com/caffeinehit/django-oauth2-provider
Decision
--------
Moving forward, we will use the `Django OAuth Toolkit (DOT)`_ library and remove
our use of DOP.
.. _Django OAuth Toolkit (DOT): https://github.com/evonove/django-oauth-toolkit
Consequences
------------
Pluses
~~~~~~
* The `Django documentation recommends DOT`_ for OAuth 2.0 support, so there
should be ample support for it in the community.
* DOT uses the well maintained and recommended OAuthLib_ library for the basic
OAuth flow so it has a solid crypto foundation.
* DOT is extensible, including its various polymorphic classes and configurable
settings.
* DOT seems to have the basic OAuth2 features that we will need for the
foreseeable future, including refresh tokens and scopes.
.. _Django documentation recommends DOT: http://www.django-rest-framework.org/api-guide/authentication/#django-oauth-toolkit
.. _OAuthLib: https://github.com/idan/oauthlib
Minuses
~~~~~~~
* We need to remove all usages of DOP before we can remove the library.

View File

@@ -0,0 +1,211 @@
3. Use JWT as OAuth2 Tokens; Remove OpenID Connect
--------------------------------------------------
Status
------
Accepted
Context
-------
The edX system has external OAuth2 client applications, including edX Mobile apps
and external partner services. In addition, there are multiple edX microservices
that are OAuth2 Clients of the LMS.
Some of the internal microservice clients require `OpenID Connect`_ features.
Specifically, they make use of the `ID Token`_ extension to get user profile
details from the LMS via the OAuth protocol. The ID Token can also be forwarded
from one microservice to another, allowing the recipient microservice to
validate the identity of the token's owner without needing to reconnect with a
centralized LMS.
We have integrated our fork of DOP_ with support for OpenID Connect. So, an
access_token request with a DOP client::
curl -X POST -d "client_id=abc&client_secret=def&grant_type=client_credentials" http://localhost:18000/oauth2/access_token/
includes an id_token field::
{
"access_token": <RANDOMLY-GENERATED-ACCESS-TOKEN>,
"id_token": <BASE64-ENCODED-ID-TOKEN>,
"expires_in": 31535999,
"token_type": "Bearer",
"scope": "profile openid email permissions"
}
where the value of BASE64-ENCODED-ID-TOKEN decodes to::
{
"family_name": "User1",
"administrator": false,
"sub": "foo",
"iss": "http://localhost:18000/oauth2",
"user_tracking_id": 1234,
"preferred_username": "user1",
"name": "User 1",
"locale": "en",
"given_name": "User 1",
"exp": 1516757075,
"iat": 1516753475,
"email": "user1@edx.org",
"aud": "bar"
}
However, OpenID Connect is a large standard with many features and is not supported by
the DOT_ implementation.
.. _OpenID Connect: http://openid.net/connect/
.. _ID Token: http://openid.net/specs/openid-connect-core-1_0.html#IDToken
.. _DOP: https://github.com/caffeinehit/django-oauth2-provider
.. _DOT: https://github.com/evonove/django-oauth-toolkit
Decision
--------
Remove our dependency on OpenID Connect since we don't really need all its
features and it isn't supported by DOT. Instead, support `JSON Web Token (JWT)`_,
which is a simpler standard and integrates well with the OAuth2 protocol.
.. _JSON Web Token (JWT): https://jwt.io/
The simplest approach is to allow OAuth2 clients to request JWT tokens in place
of randomly generated Bearer tokens. JWT tokens contain user information,
replacing the need for OpenID's ID Tokens altogether.
JWT Token
~~~~~~~~~
JWT tokens will be signed but not encrypted. We will not encrypt them as we
want the requesting Application and relying parties to be able to parse the
JWT for relevant information (like the user's name, etc).
The edX Authorization server (LMS) will selectively include data in the
JWT based on requested scopes (by the Application) and authorized scopes (by
the user). For example:
+--------------------------------+--------------------------+--------------------------------------------+
| Application requests Scope | User authorizes Scope | Authzn server (LMS) includes in JWT Payload|
+================================+==========================+============================================+
| none | n/a | - *preferred_username*: user's username |
| | | - *sub*: user's anonymous id |
+--------------------------------+--------------------------+--------------------------------------------+
| **'email'** | **'email'** | - *email*: user's email address |
+--------------------------------+--------------------------+--------------------------------------------+
| **'profile'** | **'profile'** | - *name*: user's name in their edX profile |
| | | - *family_name*: user's last name |
| | | - *given_name*: user's first name |
| | | - *administrator*: whether user is_staff |
+--------------------------------+--------------------------+--------------------------------------------+
| **'profile'** | user does not authorize | - profile data not provided |
+--------------------------------+--------------------------+--------------------------------------------+
JWT Authentication Library
~~~~~~~~~~~~~~~~~~~~~~~~~~
Use the open source `Django Rest Framework JWT library`_ as the backend
implementation for JWT token type authentication.
.. _Django Rest Framework JWT library: https://getblimp.github.io/django-rest-framework-jwt/
Requesting JWT Tokens
~~~~~~~~~~~~~~~~~~~~~
An OAuth2 client requesting a JWT token_type::
curl -X POST -d "client_id=abc&client_secret=def&grant_type=client_credentials&token_type=jwt" http://localhost:18000/oauth2/access_token/
would now receive::
{
"access_token": <BASE64-ENCODED-JWT>,
"token_type": "JWT",
"expires_in": 31535999,
"scope": "read write profile email"
}
where the value of BASE64-ENCODED-JWT decodes to what the BASE64-ENCODED-ID-TOKEN
decodes to. There would no longer be a separate id_token field, but the
access_token will now contain the data that would have been in the id_token.
**Note:** In order to use the JWT token type to access an API, the Authorization
header needs to specify "JWT" instead of "Bearer"::
curl -H "Authorization: JWT <BASE64-ENCODED-JWT>" http://localhost:18000/api/user/v1/me
Requesting Bearer Tokens
~~~~~~~~~~~~~~~~~~~~~~~~
OAuth2 Clients that are not interested in receiving JWT tokens may continue to
use the default Bearer token type::
curl -X POST -d "client_id=abc&client_secret=def&grant_type=client_credentials" http://localhost:18000/oauth2/access_token/
which returns::
{
"access_token": <RANDOMLY-GENERATED-ACCESS-TOKEN>,
"token_type": "Bearer",
"expires_in": 36000,
"scope": "read write profile email"
}
**Note:** In order to use the Bearer token type to access an API, the Authorization
header needs to specify "Bearer"::
curl -H "Authorization: Bearer <RANDOMLY-GENERATED-ACCESS-TOKEN>" http://localhost:18000/api/user/v1/me
Alternatives
------------
Our implementation of OAuth2+JWT should not be confused with the `IETF standard for
OAuth JWT Assertions`_, which is for a different purpose entirely. It uses JWTs as
a replacement for an assertion_ in the OAuth handshake. That is, it uses the JWT
as a means to *get an OAuth token* (instead of using traditional `OAuth2 grant
types`_, which require *client-secrets* or *passwords*).
Our implementation, however, returns a JWT *in place of an OAuth token*. The
Authorization server (LMS) creates/signs a JWT that binds information about the
requesting application and the authorizing user. This self-contained token can
then be validated/used by any relying party (microservice/API) for granting access.
If we did eventually support the `IETF standard for OAuth JWT Assertions`_, a client
Application would not send its *client secret* over-the-wire when requesting OAuth
Tokens. Instead, it would use the once out-of-band exchanged *client secret* to sign
its own JWT. This would be a stronger story for authenticating client Application
requests.
.. _IETF standard for OAuth JWT Assertions: https://tools.ietf.org/html/rfc7523#section-2.1
.. _assertion: https://tools.ietf.org/html/rfc7521
.. _OAuth2 grant types: https://tools.ietf.org/html/rfc6749#section-4
Consequences
------------
Pluses
~~~~~~
* The long-term design of the system will be simpler by using simpler
protocols and frameworks, such as JWT as access tokens.
* OAuth Clients obtain basic identity information within the JWT access
token without needing to hit an extra user info endpoint.
* Any microservice can validate the JWT as an assertion without making an
extra round trip to the LMS.
* Although there is no RFC or IETF standard for our use of OAuth+JWT, we
are using a relatively maintained and used `open source library`_ for our
implementation.
.. _open source library: https://getblimp.github.io/django-rest-framework-jwt
Minuses
~~~~~~~
* Token invalidation and single Logout become more difficult.
* During the transition period, there will be multiple implementations,
which may result in confusion and a more complex system. The shorter
we keep the transition period, the better.

View File

@@ -0,0 +1,54 @@
4. OAuth Dispatch as Router
---------------------------
Status
------
Accepted
Context
-------
Although we decided to transition from DOP to DOT and from OpenID Connect to
JWT Tokens, we are not able to update all of our clients and microservices
rapidly. In the meantime, the Mobile team wants to move forward with DOT to
support refresh tokens for the mobile apps.
Decision
--------
Start using DOT for new OAuth2 clients and newer versions of the Mobile app,
while supporting older DOP clients until all clients are updated to using
DOT.
The OAuth Dispatch app will function as a content-based router to the multiple
`OAuth2`_ provider implementations that will need to exist within the platform
during the transition phase. The app will route incoming OAuth2 REST requests
based on the value of the client_id field in the request. If the client_id
identifies an Application in DOT, then the request is routed to the DOT library.
Otherwise, the request is routed to the DOP library.
Once we fully execute the `transition plan from DOP to DOT`_, we will continue
to use and maintain this app as it will also contain edx-specific customizations
that we will add over time. At that point, the app will no longer act as a
router but will retain its proxy functionality to DOT.
.. _OAuth2: https://tools.ietf.org/html/rfc6749
.. _transition plan from DOP to DOT: https://openedx.atlassian.net/wiki/spaces/OpenDev/pages/327778541/OAuth+2.0+Roadmap
Consequences
------------
Pluses
~~~~~~
* The OAuth Dispatch app will provide an intermediary interface to the underlying
implementation(s), which shields the rest of the platform from changes in the
underlying libraries.
Minuses
~~~~~~~
* The LMS' security would be impacted if there are security vulnerabilities found
in either DOP or DOT. Since DOP is no longer supported, security issues may not
be addressed.

View File

@@ -0,0 +1,69 @@
5. Restricted Application for SSO
---------------------------------
Status
------
Accepted
Context
-------
External edX clients would like to use edX as an Identity Provider and verify
the edX identity of a user. The OAuth2 protocol's Authorization Code grant type
already supports this behavior and our OAuth2 clients can use it for this
purpose. However, we want to issue *scoped* access tokens to external edX
clients so end users can limit what API calls the clients make on their behalf.
The OAuth2 standard for controlling the use of access tokens is `Access Token
Scopes`_. With Scopes, an end user explicitly authorizes the actions that an
OAuth2 client is allowed to perform with the issued access token. Scopes
allow us to support SSO and issue access tokens on behalf of users, knowing
the users have approved the usage of the tokens.
However, the edX platform has not enabled wider usage of Scopes by API
endpoints, beyond the basic values of 'email' and 'profile'. It would be a
major undertaking to update all of our microservices to fully support Scopes,
while keeping the system up and running.
.. _Access Token Scopes: https://tools.ietf.org/html/rfc6749#section-3.3
Decision
--------
Implement a new model in oauth_dispatch to selectively designate DOT Applications
as "Restricted Applications". For those external clients that want SSO capability
with edX as an Identity Provider, configure them as a "Restricted Application".
Although these applications can still request access tokens via the usual
Authorization Code grant protocol, issue them only **expired** access tokens
so they cannot make unauthorized calls to our API endpoints.
Consequences
------------
Pluses
~~~~~~
* It is a minimal effort to introduce a new "Restricted Application" model
and update the oauth_dispatch logic to create expired tokens.
* SSO OAuth2 clients can now verify the identity of edX users when they
successfully receive an OAuth2 access token in the Authorization Code grant
type handshake.
* If they make the access token request with token_type=jwt, they receive
a JSON Web Token (JWT) with basic information about the user's identity,
including their username and edX Anonymous ID.
* If they include 'email' scope in their authorization request and the user
approves, the JWT will include the user's email address as well.
* If they include 'profile' scope in their authorization request and the user
approves, the JWT will include the user's full name and whether they have
staff access.
Minuses
~~~~~~~
* Returning expired tokens adds additional technical debt on top of the
debt incurred by not implementing scopes.

View File

@@ -0,0 +1,209 @@
6. Enforce Scopes in LMS APIs
-----------------------------
Status
------
Proposed
Context
-------
Although external edX clients, as Restricted Applications, can use edX
as an Identity Provider, they cannot successfully make any API calls on
behalf of users. As explained in 0005-restricted-application-for-SSO_,
edX prevents successful API calls since our API endpoints do not enforce
OAuth scopes.
For additional background information on the current implementation,
see the README_.
.. _0005-restricted-application-for-SSO: 0005-restricted-application-for-SSO.rst
.. _README: ../README.rst
Decisions
---------
Add support for enforcing OAuth2 scopes by making the following advancements
simultaneously.
1. Define and configure new OAuth2 Scopes for accessing API resources
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
* For now, we will start with an initial set of OAuth2 Scopes based on
immediate API needs. See 0007-include-organizations-in-tokens_ for
initial examples.
* OAuth2 clients should be frugal about limiting the scopes they request
in order to:
* keep the data payload small
* keep the UX of the user approval form reasonable
* follow principle of least privilege
2. Add a version number in the OAuth2 token payload
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
As a preemptive step, set a version number field (= 1) in the OAuth2 token
payload.
3. Restricted Applications receive *unexpired* JWTs, signed with a *new key*
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
* We will no longer return expired *JWTs as access tokens* to Restricted
Applications. We will sign them with a *new key* that is not shared with
unprotected microservices.
* API endpoints that are exposed by other microservices and that
support OAuth2 requests are vulnerable to exploitation until
they are also updated to enforce scopes.
* We do not want a lock-step deployment across all of our microservices.
We want to enable these changes without blocking on updating all
microservices.
* We do not want to issue unexpired *Bearer tokens* to Restricted
Applications since they will be accepted by unprotected microservices.
There's no way to retroactively inform existing microservices
to reject scope-limiting *Bearer tokens*.
* On the other hand, existing unprotected microservices will reject
*JWT tokens* signed with new keys that they do not know about. We will
make the new keys available to a microservice only after they
have been updated to enforce OAuth Scopes.
* edx_rest_framework_extensions.settings_ supports having a list of
JWT_ISSUERS instead of just a single one.
* The `edx-platform settings`_ will be updated to have a list of
JWT_ISSUERS instead of a single JWT_ISSUER in its settings (example_).
A separate settings field will keep track of which is the new issuer
key that is to be used for signing tokens for Restricted Application.
* oauth_dispatch.views.AccessTokenView.dispatch_ will be updated to
pass the new JWT key to JwtBuilder_, but only if
* the requested token_type is *"jwt"* and
* the access token is associated with a Restricted Application.
* oauth_dispatch.validators_ will be updated to return *unexpired*
JWT tokens for Restricted Applications, but ONLY if:
* the token_type in the request equals *"jwt"* and
* a `feature toggle (switch)`_ named "oauth2.unexpired_restricted_applications" is enabled.
.. _edx_rest_framework_extensions.settings: https://github.com/edx/edx-drf-extensions/blob/1db9f5e3e5130a1e0f43af2035489b3ed916d245/edx_rest_framework_extensions/settings.py#L73
.. _edx-platform settings: https://github.com/edx/edx-platform/blob/master/lms/envs/docs/README.rst
.. _example: https://github.com/edx/edx-drf-extensions/blob/1db9f5e3e5130a1e0f43af2035489b3ed916d245/test_settings.py#L51
.. _oauth_dispatch.views.AccessTokenView.dispatch: https://github.com/edx/edx-platform/blob/d21a09828072504bc97a2e05883c1241e3a35da9/openedx/core/djangoapps/oauth_dispatch/views.py#L100
.. _oauth_dispatch.validators: https://github.com/edx/edx-platform/blob/master/openedx/core/djangoapps/oauth_dispatch/dot_overrides/validators.py
4. Associate Available Scopes with Applications
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
* In order to allow open edX operators to a priori limit the
types of access an Application can request, we will allow them
to configure Application-specific "available scopes".
* Introduce a new data model that associates available scopes with
DOT Applications.
* Introduce a new Scopes Backend that extends DOT's SettingsScopes_
backend and overrides the implementation of get_available_scopes_.
* The new backend will query the new data model to retrieve
available scopes.
.. _get_available_scopes: https://github.com/evonove/django-oauth-toolkit/blob/2129f32f55cda950ef220c130dc7de55bea29caf/oauth2_provider/scopes.py#L17
.. _SettingsScopes: https://github.com/evonove/django-oauth-toolkit/blob/2129f32f55cda950ef220c130dc7de55bea29caf/oauth2_provider/scopes.py#L39
5. Associate Available Organizations with Applications
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
* See 0007-include-organizations-in-tokens_ for decisions on this.
6. Introduce a new Permission class to enforce scopes
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
* The new `custom Permission`_ class will extend DOT's TokenHasScope_
Permission class.
* The TokenHasScope_ permission allows API endpoints to declare the
scopes that they require in a *required_scopes* class variable.
* The permission class will verify that the scopes in the provided JWT
are a proper superset of the scopes required by the requested view.
* For now, the permission class will skip this verification if the
application is not a Restricted Application or if the token_type
was not a JWT token.
* **Note:** This will be an issue when microservices want to verify
scopes. Determining whether an access token is associated with a
Restricted Application is an LMS-specific capability. Given this,
we may need to include a field in the token that indicates whether
it was issued to a Restricted Application.
* If the scopes verify, the permission class will update the request
object with any organization values found in the token in an attribute
called *allowed_organizations*. The view can then limit its access
and resources by the allowed organizations.
* In order to have higher confidence that we don't inadvertently miss
protecting any API endpoints, add the new Permission class to the
`REST_FRAMEWORK's DEFAULT_PERMISSION_CLASSES`_ setting.
* **Note:** Many of our API endpoints currently override this default
by setting the *permission_classes* field on their own View or ViewSet.
So in addition to setting this default value, we will update all
(15 or so) places that include JwtAuthentication_ in their
*authentication_classes* field.
* In case of an unexpected failure with this approach in production,
use a `feature toggle (switch)`_ named "oauth2.enforce_token_scopes".
When the switch is disabled, the new Permission class fails verification
of all Restricted Application requests.
* **Note:** We currently have both `function-based Django views`_ and
class-based `Django Rest Framework (DRF)`_ views in the platform.
* Authorization enforcement using Django Permission classes is
supported only for DRF views. DRF does provide a `Python decorator`_
to add DRF support to function-based views.
* Only DRF enhanced views support JWT based authentication in our
system. They do so via the DRF-based JwtAuthentication_ class.
So we can **safely assume** that all JWT-supporting API endpoints
can be protected via DRF's Permission class.
.. _custom Permission: http://www.django-rest-framework.org/api-guide/permissions/#custom-permissions
.. _TokenHasScope: https://github.com/evonove/django-oauth-toolkit/blob/50e4df7d97af90439d27a73c5923f2c06a4961f2/oauth2_provider/contrib/rest_framework/permissions.py#L13
.. _`REST_FRAMEWORK's DEFAULT_PERMISSION_CLASSES`: http://www.django-rest-framework.org/api-guide/permissions/#setting-the-permission-policy
.. _function-based Django views: https://docs.djangoproject.com/en/2.0/topics/http/views/
.. _Django Rest Framework (DRF): http://www.django-rest-framework.org/
.. _Python decorator: http://www.django-rest-framework.org/tutorial/2-requests-and-responses/#wrapping-api-views
.. _JwtAuthentication: https://github.com/edx/edx-drf-extensions/blob/1db9f5e3e5130a1e0f43af2035489b3ed916d245/edx_rest_framework_extensions/authentication.py#L153
Consequences
------------
* Putting these changes behind a feature toggle allows us to decouple
release from deployment and disable these changes in the event of
unexpected issues.
* Minimizing the places that the feature toggle is checked (at the
time of returning unexpired tokens and at the time of validating
requests), minimizes the complexity of the code.
* By associating Scopes with DOT Applications and not Restricted
Applications, we can eventually eliminate Restricted Applications
altogether. Besides, they were introduced as a temporary concept
until Scopes were fully rolled out.
* Microservices will continue to have limited scope support. We are
consciously deciding to not address them at this time. When we do,
we will also want to simplify and consolidate their OAuth-related
logic and code.
.. _feature toggle (switch): https://openedx.atlassian.net/wiki/spaces/OpenDev/pages/40862688/Feature+Flags+and+Settings+on+edx-platform#FeatureFlagsandSettingsonedx-platform-Case1:Decouplingreleasefromdeployment
.. _0007-include-organizations-in-tokens: 0007-include-organizations-in-tokens.rst

View File

@@ -0,0 +1,189 @@
7. Include Organization in Tokens
---------------------------------
Status
------
Proposed
Context
-------
Status of Organizational Access to edX APIs
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
External edX applications would like to make server-to-server API
calls via the Client Credentials grant type to access data. However,
our APIs typically return data only to global staff users who
effectively have administrative read access to the system. This
all-or-nothing capability is unsatisfactory to meet the needs of
edX partner organizations.
Additionally, some organizations create their own web portals for
their learners, using edX as an identity provider and as the underlying
LMS. For various reasons (?), they would like to present edX data to
their learners on their own portal. Currently, they cannot access a
learner's data using our APIs.
Our API endpoints do not have more flexible capabilities since they
do not have reliably sufficient information to limit/filter API results
according to the organizational affiliation of the requesting application.
Organizational Types in the edX System
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
In the edX system, the 2 most prevalent organizational relationships
are:
* **Organization as a Content Provider**
* This is a partner organization that provides content for a course,
program, etc. Typically, such an organization will want to access
data for all learners enrolled in their courses. They may choose to
do so either via
a. bulk APIs using the *Client Credentials grant type* (e.g., to
synchronize their own data in a background process) or
b. a user-specific API on behalf of a logged-in user via the
*Authorization grant type* and *edX as the identity provider*
(e.g., to display user-specific data on their own portal).
* **Organization as a User Provider**
* This is an enterprise organization that registers users onto the
edX system, typically via an SSO-enabled portal, but with the
*organization (not edX) as the identity provider*. Such an
organization will also want to access data for all its users.
However, it is not an immediate requirement to support data
access by this organization type at this time.
Decisions
---------
In order to allow DOT Applications to access data for their own organization
without inadvertently or maliciously gaining access to data for other
organizations, (1) applications need to be linked to their own organizations,
(2) organization information needs to be cryptographically bound with
issued tokens, (3) the authorization approval form needs to present the
organization information and (4) organization limitations need to be
embedded in the scopes.
1. Associate Available Organizations with DOT Applications
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
* Create a configurable Application-specific "available organizations"
setting, which is akin to Application-specific "available scopes"
(as described in 0006-enforce-scopes-in-LMS-APIs_.
* Introduce a new data model that associates available organizations
with DOT Applications.
* The new data model will have a Foreign Key to the Organization_ table.
It will essentially be a many-to-many relationship between Organizations
and DOT Applications.
* The new data model will also have a column for specifying organization
type: *content_provider* or *user_provider*. Initially, we will only
use *content_provider*.
2. Organization Information in OAuth Tokens
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
* The organization associated with the Application will be included
in the JWT tokens requested by the Application.
* JwtBuilder_'s *build_token* functionality will be extended to include
the organization value in the token's payload. This payload is
cryptographically signed and so binds and limits the scopes in the
token to the organization.
* Since the organization value is inside the token, any relying party
that receives the token (including a microservice) will be able to
enforce scopes as limited to the organization.
.. _0006-enforce-scopes-in-LMS-APIs: 0006-enforce-scopes-in-LMS-APIs.rst
.. _Organization: https://github.com/edx/edx-organizations/blob/fa137881be9b7d330062bc32655a00c68635cfed/organizations/models.py#L14
.. _JwtBuilder: https://github.com/edx/edx-platform/blob/d3d64970c36f36a96d684571ec5b48ed645618d8/openedx/core/lib/token_utils.py#L15
3. Organization Information in Authorization Approval Form
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
When the interstitial authorization approval form is presented to the
user for granting access to a DOT Application, if the Application is
associated with an Organization, the Organization value(s) should be
presented to the user. This will make it clear to the user that the
granted access is limited to the Organization's affiliations.
4. Embed Organization Limitation Types in Scopes
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
* Since individual API endpoints need to enforce both scopes and their
corresponding organization limitations as included in the token, the
scopes themselves should indicate whether or not organization limitations
apply.
* In the event that we add additional types of organization limits in
the token, we would introduce new scopes that enforce the new
types of limits.
* This allows us to introduce new types of limits while being assured
that pre-existing API endpoints will remain protected. Since the
pre-existing endpoint is unaware of the new scope, it will
prevent access until it is updated to support the new type of
organization limit.
Scopes Examples
---------------
Here is an initial list of scopes that we may support. Notice how some
enforce organization limits and others don't. When configuring a DOT
Application, the edX operator decides how much access the application
is permitted.
+-------------------------------+----------------------------------------------------------------+
| Scope | Allowed access |
+===============================+================================================================+
| certificates:read | Retrieve any certificate |
+-------------------------------+----------------------------------------------------------------+
| certificates:read:content_org | Retrieve certificates for courses provided by the organization |
+-------------------------------+----------------------------------------------------------------+
| grades:read | Retrieve any grade |
+-------------------------------+----------------------------------------------------------------+
| grades:read:content_org | Retrieve grades for courses provided by the organization |
+-------------------------------+----------------------------------------------------------------+
| enrollments:read | Retrieve any enrollment information |
+-------------------------------+----------------------------------------------------------------+
| enrollments:read:content_org | Retrieve enrollments for courses provided by the organization |
+-------------------------------+----------------------------------------------------------------+
**Note:** Each of these scopes can be used in a server-to-server
API call (via Client Credentials) or an API call on behalf of a
single user (via Authorization Code).
Consequences
------------
* By associating Organizations with DOT Applications and not Restricted
Applications, we can eventually eliminate Restricted Applications
altogether.
* By including the organization value in the token, any relying party
that receives the token (including a microservice) will be able to
enforce the scopes as limited to the organization.
* Including the organization limitation types in the scope allows for
a secure path forward to introduce new types of limitations in the
future. It also makes it clearer to API endpoints what needs to be
enforced.
References
----------
* Examples of Scopes in other web systems
* https://developer.github.com/apps/building-oauth-apps/scopes-for-oauth-apps/
* https://developers.google.com/identity/protocols/googlescopes
* https://api.slack.com/scopes
* https://developer.spotify.com/web-api/using-scopes/
* https://developer.atlassian.com/server/hipchat/hipchat-rest-api-scopes/

View File

@@ -0,0 +1,124 @@
Manually Testing OAuth2 Provider implementation
-----------------------------------------------
This document explains how to manually test the open edX LMS' OAuth2 Provider
implementation. In order to verify that it correctly implements the
`OAuth2 standard`_, use a publicly available 3rd party standard OAuth2 client.
The steps here show how to use `Google's OAuth2 Playground`_ as the client for
testing the `Authorization Code grant type`_. However, similar steps can be used
to test other grant types if they are substituted in the appropriate places.
1. Create an OAuth2 (DOT) Application in the LMS.
i. Create or decide which LMS user will be associated with the OAuth2 application. In production, the user should be a "service user" that is distinct from "LMS end-users" that login to the system.
ii. Go to http://localhost:18000/admin/oauth2_provider/application/add/ to create a new Application.
iii. Enter a value for the "Name" field - a documentary value that uniquely describes this OAuth2 Application.
iv. Enter a value for the "User" field - from Step 1i.
v. Fill in the following required fields for the Authorization Code grant type:
- Authorization grant type: Authorization code
- Client type: Confidential
- Redirect uris: https://developers.google.com/oauthplayground
vi. The "Client id" and "Client secret" values are automatically randomly generated. You will later need these values in Step 4(iii) below to provide to the OAuth2 client.
vii. Keep the "Skip authorization" checkbox deselected in order to test the interstitial approval form in the Authorization Code protocol.
viii. Click Save.
2. Optional. Make the new Application a `Restricted Application`_ if you are testing Restricted Application features.
i. Go to http://localhost:18000/admin/oauth_dispatch/restrictedapplication/add/
ii. Find and select the new Application you created in the dropdown.
iii. Click Save.
3. Create a publicly accessible URL to the LMS if you are testing on devstack. This step is needed to support the redirecting handshake in the Authorization Code protocol from Google's server back to localhost.
i. Install `localtunnel`_:
npm install -g localtunnel
ii. Run localtunnel so it assigns a unique external url that proxies requests to the LMS on localhost:
lt --port 18000
iii. Copy the URL displayed in the terminal as you'll need it in Step 4(iii) to provide to the OAuth2 client.
4. Configure Google's OAuth2 Playground
i. Go to https://developers.google.com/oauthplayground
ii. Click on the settings wheel on the right to configure the OAuth2 client.
iii. Enter the following values:
- OAuth flow: Server-side
- OAuth endpoints: custom
- Authorization endpoint: <URL_FROM_STEP_3(iii)>/oauth2/authorize/
- Token endpoint: <URL_FROM_STEP_3(iii)>/oauth2/access_token/
- Access token location: Authorization header w/ Bearer prefix
- Access type: Online
- Force prompt: Consent screen
- OAuth Client ID: <VALUE_FROM_STEP_1(vi)>
- OAuth Client secret: <VALUE_FROM_STEP_1(vi)>
.. image:: ../images/oauth_playground_config.png
iv. Click "Close".
5. Initiate OAuth2 Authorization Code flow
i. Go to Step 1 on the left side of the OAuth2 Playground.
ii. In the "Input your own scopes" box, enter space-delimited requested scopes:
.. image:: ../images/oauth_playground_scopes.png
iii. Click the "Authorize APIs" button to initiate the OAuth2 Authorization Code protocol.
iv. Follow through any interstitial steps that are required. Specifically,
- If you aren't already logged in as an end-user on the LMS instance, you will be prompted to do so.
- If that LMS end-user hasn't already approved the requested scopes for the OAuth2 Application, then you will be prompted to do so.
- Once these interstitials are completed, the LMS will redirect back to the OAuth2 client (playground), assuming the redirect URL was correctly entered in Step 1(v).
- If successful, the LMS would have responded back to the OAuth2 client with a temporary Authorization Code.
6. Exchange the Authorization Code for an Access Token.
i. Go to Step 2 on the left side of the OAuth2 Playground. You will notice a random value in the "Authorization code" field, which was returned back by the LMS.
.. image:: ../images/oauth_playground_step2.png
ii. Click on the "Exchange authorization code for tokens" button.
iii. Note: the Authorization Code is temporary and short-lived.
iv. If successful, the LMS would have responded with a "Refresh token" and an "Access token".
.. image:: ../images/oauth_playground_tokens.png
7. Call an LMS API with the Access Token.
i. Go to Step 3 on the left side of the OAuth2 Playground. In the Request URI field, enter any LMS URL that supports OAuth2 authentication. Remember the base URL should be the URL from Step 3(iii).
.. image:: ../images/oauth_playground_request_uri.png
ii. Click on the "Send the request" button.
iii. Verify the LMS response on the right side of the OAuth2 Playground.
.. _OAuth2 standard: https://tools.ietf.org/html/rfc6749
.. _Google's OAuth2 Playground: https://developers.google.com/oauthplayground
.. _Authorization Code grant type: https://tools.ietf.org/html/rfc6749#section-4.1
.. _Restricted Application: https://github.com/edx/edx-platform/blob/dd136b457bc8a25892445fc4362ce02838179472/openedx/core/djangoapps/oauth_dispatch/models.py#L12
.. _localtunnel: https://localtunnel.github.io/www/

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

View File

@@ -76,8 +76,8 @@ class EdxOAuth2Validator(OAuth2Validator):
def _authenticate(self, username, password):
"""
Authenticate the user, allowing the user to identify themself either by
username or email
Authenticate the user, allowing the user to identify themselves either
by username or email
"""
authenticated_user = authenticate(username=username, password=password)
@@ -93,8 +93,11 @@ class EdxOAuth2Validator(OAuth2Validator):
def save_bearer_token(self, token, request, *args, **kwargs):
"""
Ensure that access tokens issued via client credentials grant are associated with the owner of the
``Application``.
Ensure that access tokens issued via client credentials grant are
associated with the owner of the ``Application``.
Also, update the `expires_in` value in the token response for
RestrictedApplications.
"""
grant_type = request.grant_type
user = request.user
@@ -120,7 +123,7 @@ class EdxOAuth2Validator(OAuth2Validator):
utc_now = datetime.utcnow().replace(tzinfo=utc)
expires_in = (access_token.expires - utc_now).total_seconds()
# assert that RestriectedApplications only issue expired tokens
# assert that RestrictedApplications only issue expired tokens
# blow up processing if we see otherwise
assert expires_in < 0