Garth Egbert

.NET - OAuth2 Workflow: Part 2 - Access Token & State

Blog Post created by Garth Egbert Expert on Apr 7, 2017

Ok, hopefully you successfully produced the Sucess!! message from Part 1.

I thought getting the redirect wired up as far as the Success message was a good starting point, but there are a few details that I glossed over for the sake of simplicity, which I'll cover in this post. 

 

I am not sure who the audience is going to be, and will try to explain things as clearly as I can.  If the walk through seems too basic, or too detailed, I apologize.  I'm happy to make adjustments based on feedback.

 

To follow along with this post, download the associated source code from this git branch:

 

If you're using Sql Server, there is a folder in the Visual Studio solution "SqlServer" containing scripts to create associated tables.  The database I'm using is named [oauth2Test].  The scripts were created with Sql Server 2008, and tested on Sql Server 2014, Community Edition is more than adequate.  If you're using a database other than Sql Server, you should be able to pull the schema from the scripts.

 

You will see a few places where I have implemented NLog.  The logging files should be stored here: c:/logs/oauth2Test, as defined in the NLog.config file.  Add your own logging where needed to enhance the output; this can help to follow the sequence of event.

 

Amendment to Part 1

In Part 1 I mentioned the Developer Key and storing values in the web.config.  I forgot to mention the "Key" variable:

This is a required value in order for you to be able to request the user access key.  In the source code associated with this post the value will be stored in the web.config <appSettings> variable [oauth2ClientKey]

 

We need to keep track of user state

If you look at the documenation for Step 1 of the workflow (OAuth2 - Redirect users to request Canvas access), specifically the more detailed definition of GET login/oauth2/auth, there is a reference to the variable state.  Although this is an optional parameter, it is very useful.  Particularly if you are running multiple web servers behind a load balancer where the user session state is unique from one server to the next.

 

Another important detail to note is that the LTI launch request happens only once.  That means you receive the LTI launch parameters only when the user launches the app from Canvas.  Once the app is launched it's running on your server, not the Canvas server, and you will not receive any additional launch parameters.  To illustrate this:

  1. Launch your test app from Canvas with the debugger running and a breakpoint in your oauth2Request, and take a look at the parameters you receive (i.e. the variables that the helper class parse out of the request).
  2. Place a breakpoint in the oauth2Response method, click the "Authorize" button in the browser and review the parameters you receive.  You will find no LTI parameters.  Even though the test app is clearly presented on the Canvas page, you now have no idea who the user is.  At this point your app is loaded and running on your web server and is not receiving any further parameters from Canvas.
  3. Right click on the iFrame where your "Success!!" message is displayed, and select "Reload frame", again with the breakpoint in the oauth2Response method.  Inspect the parameters you receive, there are no LTI parameters.

Point being, the [state] parameter allows us to create a unique identifier that we can use to keep track of our user.

 

When Canvas redirects back to the test app calling the oauth2Response method, it is essentially the same scenario.  Canvas is not making another LTI launch request.  It is entirely up to you to keep track of which request belongs to which user.  This is where persistent caching comes in to play (I mentioned a database at the end of Part 1), and the state variable plays an important role.

 

Caching User State

Back to the GET login/oauth2/auth call.  When you send a value for [state to Canvas, that value will come back to you in the parameters received in oauth2Response.  To quote the Canvas document OAuth2 - Step 2:

"If your application passed a state parameter in step 1, it will be returned here in step 2 so that your app can tie the request and response together."

 

We are going to "tie the request and response together".

The key here is to generate a unique value for state each time a user launches your app.  Looking at the source code associated with this post you will see that I have chosen to use a GUID.

  • Guid stateId = Guid.NewGuid();

The goal is to associate our LTI values with this unique identifier so we can refer back to the LTI values when we receive our response.  To achieve this we need to store this information somewhere.  There are options for state caching.  Memcached is a great option as it will time out and expire records automatically.  Databases are a great option for persistent caching.  In this case we need both expiring cache and persistent cache (discussed later).  For that reason I am going to keep it simple, and use a database.  

 

Take a look at the SqlServer folder in the Visual Studio solution for database scripts.  I'm using Sql Server, but you should be able to use the schemas to create tables in whatever database you chose to use.  For our user state there is a table named [stateCache], with a simple schema consisting of three fields:

  • stateId - this is the GUID, or unique identifier
  • stateJson - this is the string representation of the oauthHelper object
  • tstamp - a timestamp allowing you to determine the age of the record (it will be up to you to determine your strategy to expire the state cache)

 

I have included a simple helper class to manage database records:

  • sqlHelper.cs

 

Why am i creating a new state record for every LTI launch?

I'm not sure if this is a question that anyone is considering, but it might be worth mentioning.

Let's say UserA launches your tool from sitea.instructure.com, from their English class.

Then UserA launches your tool from siteb.instructure.com, from some other class.

If you reuse the same state, your streams will cross.  You need to capture the state for each specific launch request so you can be absolutely certain you are communicating with both the proper instance of Canvas, and the exact course or assignment the user launched from in the instance.

 

Changes to the oauth2Request method

The change here is minimal.  There is a new line of code to save our launch parameters (what I'm calling our "user state" or [state]).  You will see that I am now storing a new GUID in a variable, and serializing the oauthHelper object to a json string.  Using those two values I am storing our [state] for later reference:

  • sqlHelper.storeState(stateId, jsonState);

 

Changes to the oauth2Response method

There are quite a few changes to this method, and plenty of comments.

To illustrate the missing LTI launch parameters, the first line of code is parsing all parameters received, which you can inspect for clarification.  The code is documented to explain the purpose of each step, so I'll summarize here.  You will see where the unique state identifier that we sent to Canvas is returned to us, and used to query our [state] from the database:  

  • stateJson = sqlHelper.getStateJson(Guid.Parse(state));

Now we have successfully tied the OAuth2 request in Step 1 to the OAuth2 response in Step 2.

 

With the [state] retrieved from our database cache, and the parameters received from the Canvas response, we are ready to make an API call to generate a new user access token.  Take a look at the method requestUserAccessToken to inspect the mechanics of the API call and associated parameters.

 

Request the User Access Token

Requesting the access token consists of a POST call to Canvas: POST request to login/oauth2/token

The documentation is straight forward here, the required parameters are clearly defined.  One detail to note is that the API call operates in two ways:

  • generate a new access token
  • refresh an existing access token

We'll focus first on generating a new access token.  If you follow along with the comments in the code, it should be clear where each of the parameters comes from:

  • grant_type - this is defined by logic in our code, for now the value will be "authorization_code"
  • client_id - this is the value stored in the web.config parameter oauth2ClientId, which was created in the Developer Key
  • client_secret - this is the value stored in the web.config parameter oauth2ClientKey, which was created in the Developer Key
  • redirect_uri - this value must match the "URI" that was defined when the Developer Key was created, and in this example should match the Request.Url, i.e. it should match the URL that Canvas called to our oauth2Response method
  • code - is a parameter sent to us by Canvas in their redirect to our oauth2Response method
  • refresh_token - in this example does not exist yet, and should be NULL

The comments in the code should clearly show the origin of each value and how the values are stored and referenced.

 

Assuming the API call is successful, the results are stored in another helper object: userAccessToken.cs

For illustration purposes, the values will be returned to the client for display.  

 

Important Note:  You never want to return the access token or refresh toke, or the LTI parameters, back to the client.  I am displaying them only for the purposes of demonstration.

 

You should be able to confirm the access token by inspecting the "Settings" of your profile in Canvas.  If the token is successfully created, you will see a new entry under "Approved Integrations":

Launch the app again, refresh your "Settings" in Canvas, and you will see a second entry.

It's worth mentioning here that this situation is why the refresh_token is available.  If you continue to generate a new access token every time the user launches your app, the list of access tokens in the user account will become extensive.  I'll cover the refresh process later.

 

 

Caching the User Access Token

In order to be able to reuse the token next time the user launches the app, we need to store it somewhere.  This is where we need persistent storage, to store the access token potentially for the life of the app or for the life of the user.  

 

Note: The user can invalidate the token at any time by deleting the entry in their profile "Settings" mentioned above.

 

For access token caching there is a second table in the database associated with this example: accessTokenCache

The simple helper class sqlHelper also has methods to help manage records for this table.  If you inspect code for the class userAccessToken you will see that the constructor that accepts the json string from Canvas makes a call to store the token in the cache table:

  • sqlHelper.storeUserAccessToken(this);

With the access token stored, we will be able to reuse it later, which will allow us to take advantage of "refreshing" the token instead of always generating new tokens and overwhelming the user profile.  I'll cover refreshing the token in the next post.

 

Summary

This felt like a good place to stop for now, there is some new code here to play with and some details to ponder on.   Hopefully the logic so far is easy to follow with respect to the topics covered.  I have tried not to over complicate the source.

 

There are a few key details raised in this example:

  • Having a caching strategy is important.  You will need a strategy for tracking user session state (specifically the LTI launch parameters), and a strategy for keeping track of the user access tokens that are being generated.
  • Always generating new access tokens creates a great deal of clutter in the user profile, and continues to open holes in the user account.  The long term goal is to have at most one active user access token for each user at any given time.
  • The user can invalidate the access key at any time by deleting the record in their profile settings, the user maintains full control and can lock your application whenever they like.

 

I'll start working on the next post, to cover the token refresh.

Part three of this post can be found here:

Outcomes