-
Notifications
You must be signed in to change notification settings - Fork 2
Canvas Integration #116
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
base: master
Are you sure you want to change the base?
Canvas Integration #116
Changes from all commits
61affaf
91164d1
7ee9dd1
877d330
a6fe147
0eb0f62
eb206d0
75d1093
ca4725b
5a1c975
3bd8f5f
449e834
06a75b3
70ab5a2
1594ac9
c679533
fe20d06
5bb359e
f403609
5705757
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,107 @@ | ||
| ## Canvas LMS Documentation: | ||
| Reference: https://canvas.instructure.com/doc/api/file.oauth.html | ||
|
|
||
| This guide will walk you through the process of obtaining the `client_id`, `client_secret`, and `refresh_token` for interacting with the Canvas LMS API. These credentials are essential for making authenticated API requests to Canvas. | ||
|
|
||
| ### Prerequisites | ||
|
|
||
| - You need to have administrator access to the Canvas LMS instance. | ||
|
|
||
| ### Steps to Obtain the `client_id` and `client_secret` | ||
|
|
||
| 1. **Log in to Your Canvas LMS Account**: | ||
| - Go to your Canvas LMS instance and log in with your administrator credentials. | ||
|
|
||
| 2. **Navigate to the Developer Keys Section**: | ||
| - From the Canvas dashboard, click on the **Admin** panel located on the left-hand side. | ||
| - Select the specific account (usually your institution's name) where you want to manage developer keys. | ||
| - Scroll down and click on **Developer Keys** in the left-hand menu under the **Settings** section. | ||
|
|
||
| 3. **Create a New Developer Key**: | ||
| - In the Developer Keys section, click the **+ Developer Key** button at the top-right corner. | ||
| - Choose **API Key** from the dropdown menu. | ||
|
|
||
| 4. **Fill Out the Developer Key Details**: | ||
| - **Name**: Enter a name for the Developer Key (e.g., "My Canvas API Integration"). | ||
| - **Owner's Email**: Enter administrator's email. | ||
| - **Redirect URIs**: Provide the redirect URI that will handle OAuth callbacks. This is typically a URL on your institution server where you handle OAuth responses. | ||
|
|
||
| 5. **Save and Enable the Developer Key**: | ||
| - After filling out the required information, click **Save Key**. | ||
| - Ensure the key is **enabled** by toggling the switch next to your newly created key. | ||
|
|
||
| 6. **Obtain the `client_id` and `client_secret`**: | ||
| - After saving, your `client_id` and `client_secret` will be displayed in the list of developer keys. | ||
| - **Client ID**: This is usually displayed as a numeric value in the details column. | ||
| - **Client Secret**: Click on the `show key` button and it will display the `client_secret`. | ||
|
|
||
| ### Steps to Obtain the `refresh_token` | ||
|
|
||
| 1. **Redirect User to Canvas Authorization Endpoint**: | ||
| - To obtain the `refresh_token`, you need to perform an OAuth flow. | ||
| - Direct the user to the Canvas OAuth authorization endpoint: | ||
| ``` | ||
| https://canvas.instructure.com/login/oauth2/auth?client_id=YOUR_CLIENT_ID&response_type=code&redirect_uri=YOUR_REDIRECT_URI | ||
| ``` | ||
| - Replace `YOUR_CLIENT_ID` with the `client_id` obtained earlier and `YOUR_REDIRECT_URI` with the redirect URI you configured. | ||
|
|
||
| 2. **User Authorizes the Application**: | ||
| - The user will be prompted to log in (if not already logged in) and authorize the application to access their Canvas data. | ||
|
|
||
| 3. **Handle the Authorization Code**: | ||
| - After the user authorizes the application, they will be redirected to the `redirect_uri` you provided, with an authorization `code` appended as a query parameter. | ||
| - Example: `https://your-redirect-uri.com?code=AUTHORIZATION_CODE` | ||
|
|
||
| 4. **Exchange the Authorization Code for a Refresh Token**: | ||
| - Use the authorization `code` to request an access token and refresh token by making a POST request to the Canvas token endpoint: | ||
| ``` | ||
| POST https://canvas.instructure.com/login/oauth2/token | ||
| ``` | ||
| - Include the following parameters in the request body: | ||
| - `client_id`: Your Canvas `client_id` | ||
| - `client_secret`: Your Canvas `client_secret` | ||
| - `redirect_uri`: Your `redirect_uri` used in the authorization request | ||
| - `code`: The authorization code you received | ||
| - `grant_type`: Set this to `authorization_code` | ||
|
|
||
| - Example of the POST request in `curl`: | ||
| ```bash | ||
| curl -X POST https://canvas.instructure.com/login/oauth2/token \ | ||
| -F 'client_id=YOUR_CLIENT_ID' \ | ||
| -F 'client_secret=YOUR_CLIENT_SECRET' \ | ||
| -F 'redirect_uri=YOUR_REDIRECT_URI' \ | ||
| -F 'code=AUTHORIZATION_CODE' \ | ||
| -F 'grant_type=authorization_code' | ||
| ``` | ||
|
|
||
| 5. **Extract the Refresh Token**: | ||
| - The response to the token request will include an `access_token`, a `refresh_token`, and other token information. | ||
| - **Refresh Token**: This token can be used to obtain new access tokens without requiring the user to re-authorize. | ||
|
|
||
| ### Example JSON Response from Token Request | ||
|
|
||
| ```json | ||
| { | ||
| "access_token": "ACCESS_TOKEN", | ||
| "token_type": "Bearer", | ||
| "expires_in": 3600, | ||
| "refresh_token": "REFRESH_TOKEN", | ||
| "user": { | ||
| "id": 12345, | ||
| "name": "John Doe", | ||
| "sortable_name": "Doe, John", | ||
| "short_name": "John" | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| - **`refresh_token`**: The value you will need to store securely for future use. | ||
|
|
||
| ### Important Notes | ||
|
|
||
| - **Security**: The `client_id`, `client_secret`, and `refresh_token` should be stored securely. Do not expose them in client-side code or public repositories. | ||
| - **Token Expiration**: The `access_token` typically expires after a short period (e.g., 1 hour). The `refresh_token` does not expire as quickly and can be used to obtain new `access_token`s. | ||
|
|
||
| ### Conclusion | ||
|
|
||
| By following these steps, you will obtain the necessary credentials (`client_id`, `client_secret`, and `refresh_token`) to interact with the Canvas LMS API programmatically. These credentials are essential for making authenticated requests to access and manage Canvas resources through the API. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -67,22 +67,33 @@ | |
| pmss.register_field( | ||
| name="client_id", | ||
| type=pmss.pmsstypes.TYPES.string, | ||
| description="The Google OAuth client ID", | ||
| description="The Google/Canvas OAuth client ID", | ||
| required=True | ||
| ) | ||
| pmss.register_field( | ||
| name="client_secret", | ||
| type=pmss.pmsstypes.TYPES.string, | ||
| description="The Google OAuth client secret", | ||
| description="The Google/Canvas OAuth client secret", | ||
| required=True | ||
| ) | ||
| pmss.register_field( | ||
| name='fetch_additional_info_from_teacher_on_login', | ||
| type=pmss.pmsstypes.TYPES.boolean, | ||
| description='Whether we should start an additional task that will '\ | ||
| 'fetch all text from current rosters.', | ||
| description='Whether we should start an additional task that will fetch all text from current rosters.', | ||
| default=False | ||
| ) | ||
| pmss.register_field( | ||
| name="token_uri", | ||
| type=pmss.pmsstypes.TYPES.string, | ||
| description="The Canvas OAuth token uri", | ||
| required=True | ||
| ) | ||
| pmss.register_field( | ||
| name="refresh_token", | ||
| type=pmss.pmsstypes.TYPES.string, | ||
| description="The Canvas OAuth refresh token", | ||
| required=True | ||
| ) | ||
|
|
||
|
|
||
| DEFAULT_GOOGLE_SCOPES = [ | ||
|
|
@@ -128,7 +139,11 @@ async def social_handler(request): | |
| "We only handle Google logins. Non-google Provider" | ||
| ) | ||
|
|
||
| user = await _google(request) | ||
| user = await _handle_google_authorization(request) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This function ought to handle a single option of signing in for a given API. When we create the endpoints for Oauth, we ought to initialized them separately. For example in Routes, where we initialize the google oauth route, we should use # Copied from learning_observer/routes.py:239
if 'google_oauth' in settings.settings['auth']:
debug_log("Running with Google authentication")
app.add_routes([
aiohttp.web.get(
'/auth/login/{provider:google}',
# the below call to `social_handler` ought to pass a parameter "google"
handler=learning_observer.auth.social_handler),
])For the canvas oauth routes, we should add a similar route but with The passed method should call the appropriate authorization function instead of always calling Google and sometimes calling Canvas/other LMSs |
||
|
|
||
| roster_source = settings.pmss_settings.source(types=['roster_data']) | ||
JohnDamilola marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| await _set_lms_header_information(request, roster_source) | ||
|
|
||
| if constants.USER_ID in user: | ||
| await learning_observer.auth.utils.update_session_user_info(request, user) | ||
|
|
@@ -143,6 +158,21 @@ async def social_handler(request): | |
| return aiohttp.web.HTTPFound(url) | ||
|
|
||
|
|
||
| async def _set_lms_header_information(request, roster_source): | ||
JohnDamilola marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| """ | ||
| Handles the authorization of the specified Learning Management System (LMS) | ||
| based on the roster data source and delegating the request to the appropriate handler | ||
| based on the data source type. | ||
| """ | ||
| lms_map = { | ||
| constants.CANVAS: _handle_canvas_authorization | ||
| } | ||
|
|
||
| # Handle the request depending on the roster source | ||
| if roster_source in lms_map: | ||
| return await lms_map[roster_source](request) | ||
|
|
||
|
|
||
| async def _store_teacher_info_for_background_process(id, request): | ||
| '''HACK this code stores 2 pieces of information when | ||
| teacher logs in with a social handlers. | ||
|
|
@@ -211,7 +241,37 @@ async def _process_student_documents(student): | |
| # TODO saved skipped doc ids somewhere? | ||
|
|
||
|
|
||
| async def _google(request): | ||
| async def _handle_canvas_authorization(request): | ||
| ''' | ||
| Handle Canvas authorization | ||
| ''' | ||
| if 'error' in request.query: | ||
| return {} | ||
|
|
||
| token_uri = settings.pmss_settings.token_uri(types=['lms', 'canvas_oauth']) | ||
| url = token_uri | ||
|
|
||
| params = { | ||
| "grant_type": "refresh_token", | ||
| 'client_id': settings.pmss_settings.client_id(types=['lms', 'canvas_oauth']), | ||
| 'client_secret': settings.pmss_settings.client_secret(types=['lms', 'canvas_oauth']), | ||
| "refresh_token": settings.pmss_settings.refresh_token(types=['lms', 'canvas_oauth']) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. From my understanding of the Oauth workflow, the The Google API code in LO did not implement the |
||
| } | ||
| async with aiohttp.ClientSession(loop=request.app.loop) as client: | ||
| async with client.post(url, data=params) as resp: | ||
| data = await resp.json() | ||
| assert 'access_token' in data, data | ||
|
|
||
| # get user profile | ||
| canvas_headers = {'Authorization': 'Bearer ' + data['access_token']} | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We ought to be storing more than just the |
||
| session = await aiohttp_session.get_session(request) | ||
| session[constants.CANVAS_AUTH_HEADERS] = canvas_headers | ||
| request[constants.CANVAS_AUTH_HEADERS] = canvas_headers | ||
|
|
||
| return data | ||
|
|
||
|
|
||
| async def _handle_google_authorization(request): | ||
| ''' | ||
| Handle Google login | ||
| ''' | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,67 @@ | ||
| import functools | ||
|
|
||
| import learning_observer.auth | ||
| import learning_observer.lms_integration | ||
| import learning_observer.constants as constants | ||
|
|
||
|
|
||
| LMS_NAME = constants.CANVAS | ||
|
|
||
| CANVAS_ENDPOINTS = list(map(lambda x: learning_observer.lms_integration.Endpoint(*x, "", None, LMS_NAME), [ | ||
| ("course_list", "/courses"), | ||
| ("course_roster", "/courses/{courseId}/students"), | ||
| ("course_assignments", "/courses/{courseId}/assignments"), | ||
| ("course_assignments_submissions", "/courses/{courseId}/assignments/{assignmentId}/submissions"), | ||
| ])) | ||
|
|
||
| register_cleaner_with_endpoints = functools.partial(learning_observer.lms_integration.register_cleaner, endpoints=CANVAS_ENDPOINTS) | ||
|
|
||
|
|
||
| class CanvasLMS(learning_observer.lms_integration.LMS): | ||
| def __init__(self): | ||
| super().__init__(lms_name=LMS_NAME, endpoints=CANVAS_ENDPOINTS) | ||
|
|
||
| @register_cleaner_with_endpoints("course_roster", "roster") | ||
| def clean_course_roster(canvas_json): | ||
| students = canvas_json | ||
| students_updated = [] | ||
| for student_json in students: | ||
| canvas_id = student_json['id'] | ||
| integration_id = student_json['integration_id'] | ||
| local_id = learning_observer.auth.google_id_to_user_id(integration_id) | ||
| student = { | ||
| "course_id": "1", | ||
| "user_id": local_id, | ||
| "profile": { | ||
| "id": canvas_id, | ||
| "name": { | ||
| "given_name": student_json['name'], | ||
| "family_name": student_json['name'], | ||
| "full_name": student_json['name'] | ||
| } | ||
| } | ||
| } | ||
| if 'external_ids' not in student_json: | ||
| student_json['external_ids'] = [] | ||
| student_json['external_ids'].append({"source": constants.CANVAS, "id": integration_id}) | ||
| students_updated.append(student) | ||
| return students_updated | ||
|
|
||
| @register_cleaner_with_endpoints("course_list", "courses") | ||
| def clean_course_list(canvas_json): | ||
| courses = canvas_json | ||
| courses.sort(key=lambda x: x.get('name', 'ZZ')) | ||
| return courses | ||
|
|
||
| @register_cleaner_with_endpoints("course_assignments", "assignments") | ||
| def clean_course_assignment_list(canvas_json): | ||
| assignments = canvas_json | ||
| assignments.sort(key=lambda x: x.get('name', 'ZZ')) | ||
| return assignments | ||
|
|
||
|
|
||
| canvas_lms = CanvasLMS() | ||
|
|
||
|
|
||
| def initialize_canvas_routes(app): | ||
JohnDamilola marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| canvas_lms.initialize_routes(app) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -101,3 +101,10 @@ modules: | |
| writing_observer: | ||
| use_nlp: false | ||
| openai_api_key: '' # can also be set with OPENAI_API_KEY environment variable | ||
| lms: | ||
| canvas_oauth: | ||
| lms_api: {canvas-lms-api} | ||
| token_uri: {canvas-token-uri} | ||
| client_id: {canvas-client-id} | ||
| client_secret: {canvas-client-secret} | ||
| refresh_token: {canvas-refresh-token} | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @bradley-erickson When is the time to move this to PSS? |
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Code like this won't scale easily to more LMSs. We ought to have a better auth system that can handle multiple auths at once.
Example: a teacher might need their roster from Canvas, but also needs to sign into Google to get updated document text.