In How to Run the Basic Desktop Sample we got some OAuth basics working so let’s take a closer look at HTTP messages used by the sample.
You can follow the same process, using an HTTP debugger such as Fiddler or Charles, to look at the requests on your operating system.
Step 1. Desktop UI Reads Configuration
The Desktop App’s install program would deploy the configuration file to End User PCs, and read OAuth settings from it at runtime.
Note that some of these fields are nothing to do with OAuth Security but will be used for Deployment or Login User Experience reasons.
Step 2. Desktop UI tries to call an API during Page Load
The UI will realize it does not have an access token yet and will redirect to the Login Required page to initiate the process for getting one:
Step 3. Desktop UI downloads OAuth Metadata
When Sign In is clicked, the desktop app downloads Open Id Connect Metadata from Okta. The Desktop App will use these endpoints:
- The Authorization Endpoint for User Logins
- The Token Endpoint for Silent Token Renewal
Step 4. Desktop UI builds the Authorization Request URL
We are using the recommended flow for a desktop app, which is the Authorization Code Flow (PKCE):
The full redirect request as a URL looks like this:
This is quite a bit different from the Implicit Flow Redirect that we used for our SPA in an earlier post, with the following key differences.
|redirect_uri||A Web URL served by the Desktop App itself|
|response_type||The ‘code’ response type means the login result is an authorization code|
|scope||A new ‘offline_access’ scope must be supplied in order to get a refresh token|
|code_challenge||A random key generated differently for each authorization request|
|code_challenge_method||The algorithm used to generate the random key must (currently) be SHA256|
Step 5. Desktop UI Invokes System Browser
The Desktop UI gets a free port in our configured range (8001 – 8003) and listens on a Loopback URL for the Login Response. The System Browser is then launched with the above OAuth Redirect URL.
Step 6. Login is Processed
The Okta system receives the authorization request, then does Login Processing if required, to validate the User Name and Password.
The OAuth flow is then resumed and Okta issues a browser redirect to the Loopback URI, with the Authorization Code as a query parameter:
Step 7. Desktop App Receives Authorization Response
When the browser sends a request to http://127.0.0.1:8001, the Desktop App’s Loopback Web Server processes the HTTP request and must supply an HTTP response.
Step 8. Desktop App swaps Authorization Code for Tokens
Once the Desktop App has received the Authorization Code it immediately calls Okta’s Token Endpoint to swap the code for tokens.
That is, there are 2 steps to logins using the Authorization Code Flow:
- Phase 1 is to receive the Authorization Response
- Phase 2 is the above message, the Authorization Code Grant
There are a number of fields of interest in the above request message so let’s take a closer look:
|grant_type||The token endpoint supports a number of different grant types|
|redirect_uri||Verifies that the Authorization Code Grant is associated to the same app that issued the Authorization Request|
|code||The authorization code being swapped|
|code_verifier||Generated from the same random key as the earlier code_challenge field|
Note that the Authorization Code itself cannot be used for anything confidential and has to be swapped for tokens before our UI can call APIs.
Step 9. Desktop App Redirects to a Company Web Page
To finish up browser processing, our Desktop App redirects the user to a simple Company Post Login Page, so that we can display a reasonably polished looking page to the user.
Of course this is not necessary from an OAuth viewpoint, but it is possible to customize what the System Browser shows after a login, and the UX people at your company may care about this.
Step 10. Authorization Codes and PKCE
An Authorization Code is a short lived (< 10 minutes) login result and each code can only be used for one Authorization Code Grant message.
For Native Apps there is potential for a malicious app to register the same Redirect URI, http://127.0.0.1:8001, and possibly intercept an Authorization Code intended for our Desktop App.
Therefore the OAuth flow for Native Apps uses a technique called Proof Key for Code Exchange (PKCE) to prevent this:
- During the Authorization Request, the Authorization Server stores the Code Challenge mapped to the Authorization Code it issues
- During the Authorization Code Grant, the Authorization Server gets the stored Code Challenge and checks it has produced the Code Verifier
This mechanism prevents malicious apps from being able to get tokens if they manage to intercept an Authorization Code.
Step 11. UI Stores Refresh Token
For our first desktop sample we are just storing tokens in memory. Note that we need to be careful about storing refresh tokens, since they are long lived credentials.
Step 12. UI calls API with Access Token
We can view access token requests from our Desktop App to our API in the same manner as for our SPA, by looking at the HTTP Authorization Header:
We can view the JWT details in an online viewer such as JWT.IO and, as for our SPA, we are using small confidential tokens containing just a User Id:
The profile and email scopes mean the access token can be sent to the User Info endpoint to get the User Name and Email for the logged in user.
Step 13. First API Call is to get User Info
AppAuth-JS libraries do not currently support returning Id Tokens to our UI, so we will use the technique from our first SPA Code Sample of Getting User Info via our API:
The UI receives the response and then renders details about who the logged in user is:
Step 14. API Validates Access Token
To see this request in an HTTP debugger such as Fiddler, restart the API with an HTTPS_PROXY environment variable:
- On Windows run ‘npm run httpDebug’
- On Mac OS or Linux run ‘sudo -E npm run httpDebug’
Our API validates incoming tokens by calling the Introspection Endpoint, and usually gets an Active response indicating that the token is valid:
Our API also calls the User Info Endpoint, then Caches the Token and User Info Details in Memory: and returns the User Info to the UI:
Step 15. API returns Personalized Data
Our API then returns its ‘corporate financial data‘ and of course the real reason we are using OAuth technologies is to protect our Corporate Assets .
In our simple example we are returning the same hard coded data for every user but a real Corporate API might apply Authorization Rules based on the User Identity, as discussed in This Earlier Post.
Step 16. Id Tokens and Native Apps
Single Page Applications use the Implicit Flow, where receiving an id token is highly recommended, since it protects against some types of token substitution attack.
The id token is less important for Desktop Apps, since only the Authorization Code could be substituted, and PKCE would prevent that from working, as mentioned in the below AppAuth iOS Post:
A hybrid flow that includes an Id Token as well as an Authorization Code is possible, by specifying the below value in the authorization request:
- response_type = code id_token
Personally I would rather use the hybrid flow, so that:
- My Desktop App concepts are similar to those for my SPAs
- The UI can use extra Open Id Connect features that require id tokens
- PEN testers may perceive the security to be more complete
Step 17. Access Token Expires
Every 30 minutes our access token will expire. Our UI allows us to roughly simulate this by clicking Expire Access Token followed by Refresh Data:
The ‘expire’ operation intentionally adds characters to the access token so that when we send it to our API, and the access token is forwarded to Okta, the introspection call fails:
The API response to the UI is a 401, and the UI needs to be prepared to handle this at any time, by getting a new access token and retrying the API call:
Step 18. Access Token is Refreshed
Our Desktop App automatically handles 401s by using the Refresh Token to get a new Access Token:
This is a standard OAuth message called the Refresh Token Grant. Note that refresh tokens are not JWTs and have a vendor specific format.
Some responses to the Refresh Token Grant may contain a new ‘rolling’ refresh token, which replaces the previous one. This can help limit the lifetime of stolen refresh tokens.
Note also that it is possible to logon with all possible scopes but to request only a subset of these scopes in the Refresh Token Grant:
This enables you to use different access tokens with different privileges, and perhaps to ‘run with least privilege‘ for the majority of API calls.
Step 19. Refresh Token Expires
We can also simulate the refresh token expiring after 12 hours in a similar manner to the access token expiring every 30 minutes. Our sample again does this by adding characters to the token.
Start by selecting a particular location within our Desktop App, then click Expire Refresh Token followed by Refresh Data:
When Refresh Data is clicked, our Refresh Token Grant will fail with an Invalid Grant. We need to treat this error code specially since it means ‘User Session Expired‘:
The user is then moved to the Sign In screen, since we need to invoke the System Browser and allow the user to retry:
When signing in after session expiry, we then restore the user’s location within our Desktop App:
Step 20. Refresh Token Revocation
In the event on an attacker somehow stealing a refresh token, and gaining long lasting access to your Corporate Assets, you may want a mechanism for an IT Administrator to centrally find and cancel a refresh token.
My trial version of Okta does not have an option to view refresh tokens against users, so let’s look at how Ping Federate manages this, so that you can think about how your company might revoke tokens:
- The Authorization Server can store all refresh tokens in a database
- The IT administrator can query tokens via user / application / time
- The IT administrator can delete a database record to revoke the token
Note also that the database only stores a hash of the refresh token, so that it is not possible for a rogue IT administrator to steal and use one.
Step 21. User Explicitly Logs Out
In our previous post on Logout we discussed that:
- A common reason for wanting a logout feature is to enable testers to log on as Different Users with Different Permissions to Corporate Assets
- To do a full Open Id Connect Logout from Okta, and remove its Identity Provider cookie, requires us to capture the Id Token during login
Since we don’t capture an id token during login, our Desktop App will just do a ‘basic logout‘ that clears all tokens from memory and redirects the user to the Desktop App’s Sign In Page.
Step 22. OAuth Failures Handled
In our earlier SPA Code Sample we tested some Failure Scenarios to try to help ensure Fast Resolution of any Production OAuth Issues.
Fiddler is a great tool for manipulating OAuth requests, and I use it to test my error handling. As an interesting case, let’s look at PKCE verification.
For my Okta Authorization Server I can set a Fiddler breakpoint on the Token Endpoint via the following command:
- bpu oauth2/default/v1/token
During the Authorization Code Grant we can then capture the request and edit the PKCE verifier value:
As expected, the request is then rejected, indicating that Okta is using the earlier received Code Challenge value to validate the Code Verifier.
We can then verify that our Desktop App is handling the failure in a controlled manner, by capturing fields from the OAuth error response:
We also want to avoid a misleading response in the browser, so we show a ‘Friendly Error Page‘ in this case. Again, you may not want to do this, but it is good to know what’s possible.
Where Are We?
We’ve used AppAuth-JS libraries to implement the Authorization Code Flow (PKCE) with Good Security and control over Usability, and we understand the technical messages. Next we’ll look at some of the code behind it.