Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feanil/api docs #32971

Merged
merged 10 commits into from
Aug 18, 2023
1 change: 1 addition & 0 deletions docs/concepts/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ Concepts and Guides
frontend/styling
frontend/bootstrap
frontend/static_assets
rest_apis
39 changes: 39 additions & 0 deletions docs/concepts/rest_apis.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
edx-platform REST API Concepts
robrap marked this conversation as resolved.
Show resolved Hide resolved
##############################

APIs in the edx-platform fall into one of two categories.

#. **Personal APIs** that only let you manipluate resources related to your
user (the single user associated with the OAuth2 Application)

#. **Machine-to-machine APIs** that allow you to manipulate other users and
system resources so long as the user associated with the OAuth2 application
has the permissions to do so.

The best way to interact with the APIs is to get a JWT Token associated with a
user and then pass that to the server as a part of the request header.

You can get a JWT one of two ways:

#. Exchange the username and password for a user to get their JWT (see
:ref:`JWT from user`)

#. Get a JWT associated with an OAuth2 Application (the application is
associated with your user) that allows you to manipulate other users and
system resources so long as the user associated with the OAuth2 application
has the permissions to do so. (see :ref:`JWT from application`)

.. note:: JWTs by default expire every hour so when they expire you'll have to
get a new one before you can call the API again.

.. seealso::

* :doc:`/how-tos/use_the_api`

* :doc:`/references/auth_code_samples`

* `OAuth2, JWT and Mobile <https://openedx.atlassian.net/wiki/spaces/AC/pages/42599769/OAuth2+JWT+and+Mobile>`_

* `Open edX Rest API Conventions <https://openedx.atlassian.net/wiki/spaces/AC/pages/18350757/Open+edX+REST+API+Conventions>`_

* `edX Enterprise REST API Auth Guide <https://edx-enterprise-api.readthedocs.io/en/latest/authentication.html>`_
90 changes: 90 additions & 0 deletions docs/how-tos/use_the_api.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
How To Use the REST API
#######################

.. How-tos should have a short introduction sentence that captures the user's goal and introduces the steps.

This how-to will help you get setup to be able to make authenticated requests to
the edx-platform REST API.

Assumptions
***********

.. This section should contain a bulleted list of assumptions you have of the
person who is following the How-to. The assumptions may link to other
how-tos if possible.

* You have access to the edx-platform Django Admin (``/admin``) Panel.

* You have a user that you want to make the rest calls as (``UserA``).

* You are familiar with `the basics of HTTP and Rest`_

* For the purposes of this tutorial we'll assume your LMS is located at
https://lms.example.com

.. _the basics of HTTP and Rest: https://code.tutsplus.com/a-beginners-guide-to-http-and-rest--net-16340t

Steps
*****

.. A task should have 3 - 7 steps. Tasks with more should be broken down into digestible chunks.

#. Go to https://lms.example.com/admin/oauth2_provider/application/

#. Click :guilabel:`Add Application`

#. Choose "UserA" for the user.

#. Choose ``Confidential`` Client Type

#. Choose "Client Credentials" for the Authorization Grant Type

#. Set a name for your application.

#. Save the ``client_id`` and ``client_secret``.
feanil marked this conversation as resolved.
Show resolved Hide resolved

#. The best way to interact with the edx-platform REST API is by making
requests using the JWT Authorization header. Use the ``client_id`` and
``client_secret`` to get a JWT token.

.. code-block:: python

import base64
import requests

client_id = "vovj0AItd9EnrOKjkDli0HpSF9HoooaTY9yueafn"
# Client secrets should not be exposed in your code, we put it here to
# make the example more clear.
client_secret = "a3Fkwr24dfDSlIXt3v3q4Ob41CYQNZyGmtK8Y8ax0srpIa2vJON3OC5Rvj1i1wizsIUv1W1qM1Q2XPeuyjucNixsHXZsuw1dn2B9nH3IyjSvuFb5KoydDvWX8Hx8znqD"

credential = f"{client_id}:{client_secret}"
encoded_credential = base64.b64encode(credential.encode("utf-8")).decode("utf-8")

headers = {"Authorization": f"Basic {encoded_credential}", "Cache-Control": "no-cache"}
robrap marked this conversation as resolved.
Show resolved Hide resolved
data = {"grant_type": "client_credentials", "token_type": "jwt"}
robrap marked this conversation as resolved.
Show resolved Hide resolved

token_request = requests.post(
"http://lms.example.com/oauth2/access_token", headers=headers, data=data
)
access_token = token_request.json()["access_token"]


#. The code above will produce a JWT token that you can use to hit any existing
feanil marked this conversation as resolved.
Show resolved Hide resolved
edx-platform API endpoint.

.. code-block:: python
:name: Example, get all courses you're enrolled in.
:caption: Example, get all of UserA's Enrollments


enrollment_request = requests.get(
"http://lms.example.com/api/enrollment/v1/enrollment",
headers={"Authorization": f"JWT {access_token}"},
)


.. seealso::

* :doc:`/concepts/rest_apis`

* :doc:`/references/auth_code_samples`
171 changes: 171 additions & 0 deletions docs/references/auth_code_samples.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
Authentication Related Code Samples
###################################

.. warning::

Access Tokens, Refresh Tokens and Client Secrets are generally considered
secret and should not live in your code. We print them here so that these
examples are useful but you should generally not expose any of these tokens
to systems or clients you don't trust.

.. _JWT from user:

Get a JWT with a Username and Password
**************************************

.. code-block::

import requests
from pprint import pprint

token_request = requests.post(
f"http://lms.example.com/oauth2/access_token",
data={
"client_id": "login-service-client-id",
"grant_type": "password",
"username": "test_user",
"password": "test_password",
"token_type": "JWT",
},
)
pprint(token_request.json())

.. code-block::
:caption: Output

{'access_token': 'eyJhbGciOiJIUzI1NiJ9.eyJhdWQiOiAibG1zLWtleSIsICJleHAiOiAxNjkyMjExNjM4LCAiZ3JhbnRfdHlwZSI6ICJwYXNzd29yZCIsICJpYXQiOiAxNjkyMjA4MDM4LCAiaXNzIjogImh0dHA6Ly9sb2NhbGhvc3Q6MTgwMDAvb2F1dGgyIiwgInByZWZlcnJlZF91c2VybmFtZSI6ICJmZWFuaWwiLCAic2NvcGVzIjogWyJyZWFkIiwgIndyaXRlIiwgImVtYWlsIiwgInByb2ZpbGUiXSwgInZlcnNpb24iOiAiMS4yLjAiLCAic3ViIjogIjVjMTBmNjZmMmQ2MzkwYjcwNjYyYzkxNGFhZTdlZjc5IiwgImZpbHRlcnMiOiBbInVzZXI6bWUiXSwgImlzX3Jlc3RyaWN0ZWQiOiBmYWxzZSwgImVtYWlsX3ZlcmlmaWVkIjogdHJ1ZSwgImVtYWlsIjogImZlYW5pbEBheGltLm9yZyIsICJuYW1lIjogIkZlYW5pbCBQYXRlbCIsICJmYW1pbHlfbmFtZSI6ICIiLCAiZ2l2ZW5fbmFtZSI6ICIiLCAiYWRtaW5pc3RyYXRvciI6IHRydWUsICJzdXBlcnVzZXIiOiB0cnVlfQ.iGFl7qsAUau0-40oq8Of0f72kguq2Hc_drijCnI2I-M',
'expires_in': 3600,
'refresh_token': 'm8iXhVlGABu52xFxVFj5rAz8xSjsRq',
'scope': 'read write email profile',
'token_type': 'JWT'}

.. note:: The client type must be ``public`` for this to work.

.. _JWT from application:

Get a JWT with a client_id and client_secret
********************************************

.. code-block::

import base64
import requests

from pprint import pprint

client_id = "ukbclQB8aPh7hgsy8ifPXkPf7fRqgUq1w21f2YZa"
# Note this should actually be secret and probably not in your code but
# provided here in the example
client_secret = "xkN0BJ19q9Jk8UPUppEtC1xe4764c81ioFtlegvokbmnAC7CFCT5gG1Og5nnFmCNc3NHNhUwWWDRVcBfnLSZ4xAlEmSePzfkFtLE06cwR1MuSc0gx9LUEjRrTs3j2vgK"

credential = f"{client_id}:{client_secret}"
encoded_credential = base64.b64encode(credential.encode("utf-8")).decode("utf-8")

headers = {"Authorization": f"Basic {encoded_credential}", "Cache-Control": "no-cache"}
data = {"grant_type": "client_credentials", "token_type": "jwt"}

token_request = requests.post(
"http://lms.example.com/oauth2/access_token", headers=headers, data=data
)

pprint(token_request.json())

.. code-block::
:caption: Output

{'access_token': 'eyJhbGciOiJIUzI1NiJ9.eyJhdWQiOiAibG1zLWtleSIsICJleHAiOiAxNjkyMjExNjM4LCAiZ3JhbnRfdHlwZSI6ICJjbGllbnQtY3JlZGVudGlhbHMiLCAiaWF0IjogMTY5MjIwODAzOCwgImlzcyI6ICJodHRwOi8vbG9jYWxob3N0OjE4MDAwL29hdXRoMiIsICJwcmVmZXJyZWRfdXNlcm5hbWUiOiAiZmVhbmlsIiwgInNjb3BlcyI6IFsicmVhZCIsICJ3cml0ZSIsICJlbWFpbCIsICJwcm9maWxlIl0sICJ2ZXJzaW9uIjogIjEuMi4wIiwgInN1YiI6ICI1YzEwZjY2ZjJkNjM5MGI3MDY2MmM5MTRhYWU3ZWY3OSIsICJmaWx0ZXJzIjogW10sICJpc19yZXN0cmljdGVkIjogZmFsc2UsICJlbWFpbF92ZXJpZmllZCI6IHRydWUsICJlbWFpbCI6ICJmZWFuaWxAYXhpbS5vcmciLCAibmFtZSI6ICJGZWFuaWwgUGF0ZWwiLCAiZmFtaWx5X25hbWUiOiAiIiwgImdpdmVuX25hbWUiOiAiIiwgImFkbWluaXN0cmF0b3IiOiB0cnVlLCAic3VwZXJ1c2VyIjogdHJ1ZX0.CX1S0QGrWKEPOHC8kUzGcvW8Ky04RCA8vU8WJrZURSw',
'expires_in': 3600,
'scope': 'read write email profile',
'token_type': 'JWT'}

.. note:: When you get a JWT using ``client_credentials`` you don't get a
refresh token. You're just expected to make a new call with your client
credentials.

Check to see if a JWT is Expired
********************************

.. code-block::

import jwt

# See above examples for how to get a JWT token
jwt_token = token_request.json()['access_token']

try:
jwt.decode(jwt_token, "secret", audience="lms-key", algorithms=['HS256'])
feanil marked this conversation as resolved.
Show resolved Hide resolved
except jwt.ExpiredSignatureError:
# Signature has expired

Refresh a JWT Using a Refresh Token
***********************************

.. code-block::

import requests

# See "Get a JWT with a Username and Password" for how to get a refresh token.
# The response from that request will include a `refresh_token` attribute.
refresh_token = token_request.json()['refresh_token']

refreshed_token_request = requests.post(
f"http://lms.example.com/oauth2/access_token",
data={
"client_id": "login-service-client-id",
"grant_type": "refresh_token",
"refresh_token": token_request.json()['refresh_token'],
"token_type": "JWT",
},
)

pprint(refreshed_token_request.json())

.. code-block::
:caption: Output


{'access_token': 'eyJhbGciOiJIUzI1NiJ9.eyJhdWQiOiAibG1zLWtleSIsICJleHAiOiAxNjkyMjE1MTgwLCAiZ3JhbnRfdHlwZSI6ICJwYXNzd29yZCIsICJpYXQiOiAxNjkyMjExNTgwLCAiaXNzIjogImh0dHA6Ly9sb2NhbGhvc3Q6MTgwMDAvb2F1dGgyIiwgInByZWZlcnJlZF91c2VybmFtZSI6ICJmZWFuaWwiLCAic2NvcGVzIjogWyJyZWFkIiwgIndyaXRlIiwgImVtYWlsIiwgInByb2ZpbGUiXSwgInZlcnNpb24iOiAiMS4yLjAiLCAic3ViIjogIjVjMTBmNjZmMmQ2MzkwYjcwNjYyYzkxNGFhZTdlZjc5IiwgImZpbHRlcnMiOiBbInVzZXI6bWUiXSwgImlzX3Jlc3RyaWN0ZWQiOiBmYWxzZSwgImVtYWlsX3ZlcmlmaWVkIjogdHJ1ZSwgImVtYWlsIjogImZlYW5pbEBheGltLm9yZyIsICJuYW1lIjogIkZlYW5pbCBQYXRlbCIsICJmYW1pbHlfbmFtZSI6ICIiLCAiZ2l2ZW5fbmFtZSI6ICIiLCAiYWRtaW5pc3RyYXRvciI6IHRydWUsICJzdXBlcnVzZXIiOiB0cnVlfQ.oNTEk7aMFSjvEbvH_-Gu2QZE93w-CpXSIIuN-IC6BSU',
'expires_in': 3600,
'token_type': 'JWT',
'scope': 'read write email profile',
'refresh_token': 'V5fbgDt2RPVnmI6Q3c6cJ3OjVriGii'}

Use a JWT Header to call an API
*******************************

.. code-block::

# See above examples for how to get a JWT token
access_token = token_request.json()["access_token"]

enrollment_request = requests.get(
"http://lms.example.com/api/enrollment/v1/enrollment",
headers={"Authorization": f"JWT {access_token}"},
)

pprint(enrollment_request.json())

.. code-block::
:caption: Output

[{'course_details': {'course_end': None,
'course_id': 'course-v1:TestX+Course+1',
'course_modes': [{'bulk_sku': None,
'currency': 'usd',
'description': None,
'expiration_datetime': None,
'min_price': 0,
'name': 'Audit',
'sku': None,
'slug': 'audit',
'suggested_prices': ''}],
'course_name': 'Open edX Test Course',
'course_start': '2022-04-09T00:00:00Z',
'enrollment_end': None,
'enrollment_start': None,
'invite_only': False,
'pacing_type': 'Instructor Paced'},
'created': '2023-08-17T14:10:48.476967Z',
'is_active': True,
'mode': 'audit',
'user': 'test_user'}]
1 change: 1 addition & 0 deletions docs/references/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ References
.. toctree::
:maxdepth: 1
:glob:
:caption: Table of Contents

*
docstrings/index
4 changes: 4 additions & 0 deletions docs/references/lms_apis.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,9 @@ LMS APIs

The LMS currently has the following API Endpoints.

.. note::

Checkout :doc:`/how-tos/use_the_api` to learn how to authenticate against
these APIs

.. openapi:: ../lms-openapi.yaml
Loading