diff --git a/docs/concepts/index.rst b/docs/concepts/index.rst index 01f92503795c..fa4a02125ffa 100644 --- a/docs/concepts/index.rst +++ b/docs/concepts/index.rst @@ -10,3 +10,4 @@ Concepts and Guides frontend/styling frontend/bootstrap frontend/static_assets + rest_apis diff --git a/docs/concepts/rest_apis.rst b/docs/concepts/rest_apis.rst new file mode 100644 index 000000000000..69164d74c7af --- /dev/null +++ b/docs/concepts/rest_apis.rst @@ -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 `_ + + * `Open edX Rest API Conventions `_ + + * `edX Enterprise REST API Auth Guide `_ diff --git a/docs/how-tos/use_the_api.rst b/docs/how-tos/use_the_api.rst new file mode 100644 index 000000000000..0f680dc840bc --- /dev/null +++ b/docs/how-tos/use_the_api.rst @@ -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` diff --git a/docs/references/auth_code_samples.rst b/docs/references/auth_code_samples.rst new file mode 100644 index 000000000000..98d89eb2b98f --- /dev/null +++ b/docs/references/auth_code_samples.rst @@ -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'}] diff --git a/docs/references/index.rst b/docs/references/index.rst index d1a1af3986cc..bdafc2168837 100644 --- a/docs/references/index.rst +++ b/docs/references/index.rst @@ -4,6 +4,7 @@ References .. toctree:: :maxdepth: 1 :glob: + :caption: Table of Contents * docstrings/index diff --git a/docs/references/lms_apis.rst b/docs/references/lms_apis.rst index 7c77b47e5005..3776da77a3d9 100644 --- a/docs/references/lms_apis.rst +++ b/docs/references/lms_apis.rst @@ -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