@@ -10,3 +10,4 @@ Concepts and Guides
|
||||
frontend/styling
|
||||
frontend/bootstrap
|
||||
frontend/static_assets
|
||||
rest_apis
|
||||
|
||||
39
docs/concepts/rest_apis.rst
Normal file
39
docs/concepts/rest_apis.rst
Normal file
@@ -0,0 +1,39 @@
|
||||
edx-platform REST API Concepts
|
||||
##############################
|
||||
|
||||
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
docs/how-tos/use_the_api.rst
Normal file
90
docs/how-tos/use_the_api.rst
Normal file
@@ -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``.
|
||||
|
||||
#. 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"}
|
||||
data = {"grant_type": "client_credentials", "token_type": "jwt"}
|
||||
|
||||
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
|
||||
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
docs/references/auth_code_samples.rst
Normal file
171
docs/references/auth_code_samples.rst
Normal file
@@ -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'])
|
||||
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'}]
|
||||
@@ -4,6 +4,7 @@ References
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
:glob:
|
||||
:caption: Table of Contents
|
||||
|
||||
*
|
||||
docstrings/index
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user