This project implements the ability to toggle between both authentication methods that Brightspace provides:
See Configurations for details on how to set the various configurations that are used in the authentication process.
The code for the ID/Key Authentication can be found in the idkeyauth.js file. The following outlines the implemented functionality:
-
There are several different open source SDKs built by D2L that implement the ID/Key Authentication protocal. This solution is using the JavaScript SDK and is imported in the project in the server.js file with the following code:
d2l = require('valence')
-
Once the SDK is imported we can create application context using the Instance URL, the Application Key and the Application Id. The Application Key and Application Id are received when an application is registered in Brightspace using the 'Manage Extensibility' tool. The code for creating this context is:
const appContext = new d2l.ApplicationContext(configs.instanceUrl, configs.applicationId, configs.applicationKey);
-
The
/idkeyauth
route exists in the project to initiate the ID/Key Authentication protocol using the created application context. When this route is navigated to in the browser the user is redirected to the Learning Environment where they are pompted to accept the application's ability to make APIs on their behalf. You can see in the callback that we call thecreateUserContext
which grabs userId and userKey from the query parameters returned from Brightspace. -
Once they have accepted the terms the user is redirected to the
/idkeycallback
route where the received userKey and userId are stored in a cookie so that subsequent requests can be signed using this context. The follwing code is how the context is setup again and used:// Grab the UserId and UserKey from the cookie. const userId = req.cookies[configs.cookieName].userId; const userKey = req.cookies[configs.cookieName].userKey; // Setup user context using the values from the cookie. const userContext = appContext.createUserContextWithValues(configs.instanceScheme + '//' + configs.instanceUrl, configs.instancePort, userId, userKey); // Create an authenticated URL using the SDK. const apiCallUrl = userContext.createAuthenticatedUrl(apiPath, 'GET');
The code for the OAuth 2.0 implementation can be found in the oauth.js file. Out of the box there are many supported OAuth 2.0 libraries that you can use in order to make your authenticated requests and support you through the authentication workflow. One thing to keep in mind is that OAuth 2.0 requires the calling application to be granted scopes
that represent what routes the OAuth client is authorized to access.
Currently for the samples the following scopes:
core:*:*
The following is the workflow the sample has implemented:
- When the
/oauth
route is navigated to in the browser the OAuth 2.0 implementation is initiated. - The first order of business is to attain an authorization code from the Authorization Endpoint. In order to recieve an auth code there are several configurations that need to be sent as query parameters. The following code illustrates this:
// Using the imported 'querystring' library, create the query parameter list passing in the required variables. const authCodeParams = querystring.stringify({ response_type: 'code', redirect_uri: helpers.getRedirectUri(req), client_id: configs.clientId, scope: configs.authCodeScope, state: configs.state }); // Redirect the user to the authentication endpoint with the query parameters. res.redirect(configs.authEndpoint + '?' + authCodeParams);
- Once the user has granted the application permission the user is redirected back to the
/oauthcallback
route. In the callback the recieved Authorization Code is exchanged for an Access Token by calling the Token Endpoint that can then be used to make API calls. The following code is responsible for this exchange:// Retrieve the authorization code from the query parameter. const authorizationCode = req.query.code; // Verify that the state passed into the request for an Auth code matches the state passed back to the callback. const state = req.query.state; if (state !== configs.state) { console.log('The state value from the authorization request was incorrect.'); res.status(500).send({ error: 'STATE mistmatch - authorization request could not be completed.' }); return; } // Set the values that will be sent to the Token Endpoint through the body of the request. const payload = querystring.stringify({ grant_type: 'authorization_code', redirect_uri: configs.getRedirectUri(req), code: authorizationCode }); // Using the 'superagent' library with the client_id and client_secret sent through the headers as Basic Authorization and the payload sent as the body. request .post(configs.tokenEndpoint) .auth(configs.clientId, configs.clientSecret) .send(payload) .end(function(err, response) { if (err) { console.log('Access Token Error', err.response || err); res.redirect('/auth'); } else if(response.statusCode !== 200) { res.status(response.statusCode).send(response.error); } else { const accessToken = response.body.access_token; // Save the access token into a cookie to be retrieved later in order to make a request. res.cookie(configs.cookieName, { accessToken: accessToken }, configs.cookieOptions); // Redirect the user back to the index page. res.redirect('/?authenticationType=oauth'); } });
- Now that the Access Token has been saved in the cookie, it can be retrieved later and added as an 'Authorization' header in API requests. The following is an example of this:
// Retrieve access token from the cookie. const accessToken = req.cookies[configs.cookieName].accessToken; // Set the Authorization header with the access token. request .get( whoamiRoute ) .set('Authorization', `Bearer ${accessToken}`) .end(function(error, response) { if (error) { console.log('Error calling the who am I route', error); res.status(500).send({ error: error }); } else if(response.statusCode !== 200) { res.status(response.statusCode).send(response.error); } else { res.status(200).send(response.text); } });