Article REST Authorization using PKCE (Proof Key Code Exchange)

PKCE, or Proof Key Code Exchange, is an extension of the authorization code flow in OAuth 2.0 that solves the "public" client issue.   Public OAuth clients, or clients that cannot reliably or practically protect a client secret will generally use the Implicit grant flow to avoid having to deal with client credentials. This includes but is not limited to single page JavaScript applications and mobile applications.   The issue in doing this  is you lose some of the security benefits of forcing a client to authenticate and instead rely on simple matching of redirect URLs.  The secondary issue is that in implicit flows, the token is returned via the query string of the redirect callback, exposing it to potential interception.  

PKCE eliminates the need for directly authenticating a client and allows for the traditional authorization code flow.  It does this by extending the authorization code and token request to include three new values that replace the client secret.

You should first familiarize yourself with Authentication for REST using OAuth.

Step 1: An Application Generates a Code Verifier

A code verifier(code_verifier) is a random string that adheres to specific rules:

  • It should be cryptographically random string.
  • You should never use a code challenge more than once
  • It must be no less than 43 characters and no more than 128.  The closer to 128 the better.
  • It can only contain alpha numeric characters(mixed case),"-" , "." ,"_" /,"~".  Note the quotes are not valid

For the sake of example we will use this value:

code_verifier: 7i23cSQ28IZ1.dT.GgirgCld~OWcbftEZM-zIaEMspmR6xvu5IcRSBT.NmXWpXQ1.dR67XBAELy_O7V5JW7tn~GrWQD4CDhYO~ouBrOqJOdYd61mV5nSdfpoJ0n8y6V6

Step 2: An Application Derives the Code Challenge

You create the code challenge(code_challenge) by taking the code verifier value from step 1 and hash it using a SHA256 hashing algorithm.   Then the hash is converted to a Base64 Url Encoded value.   Do not confuse Base64 Url encoding with regular URL encoding.  in Base64 Url encoding, the Base64 string is manipulated as follows:

  • The padding "=" is removed
  • "+" is replaced with "-"
  • "/" is replaced "_"

There is no need to further encode this string as it is now URL safe.

When we hash our code_verifier using SHA 256, we end up with:

code_verifier: 7i23cSQ28IZ1.dT.GgirgCld~OWcbftEZM-zIaEMspmR6xvu5IcRSBT.NmXWpXQ1.dR67XBAELy_O7V5JW7tn~GrWQD4CDhYO~ouBrOqJOdYd61mV5nSdfpoJ0n8y6V6
code_challenge: ORq8qTX7awZv4TNdb8mS3sDzSUTXaix-BI-7DiU77PQ
code_challenge_method: S256

Step 3:  Request Authorization

Requesting authorization is the same as it normally is for a confidential client with the exception that we need to pass our code challenge and the code challenge method with our authorization request.  

The code challenge method(code_challenge_method) describes the hashing method used.  This value should always be S256 indicating SHA256 was used.

NOTE:  Community will allow a code_challenge_method of plain, in which case you should pass the code_verifier as the code_challenge un-hashed.    This functionality is strictly for development puproses, it is never acceptable to use this in production as you end up exposing the same potential key disclosure as the implicit grant.

https://mycommunity.com/api.ashx/v2/oauth/authorize?client_id={clientId}&response_type=code&redirect_uri={redirect_uri}&code_challenge_method=S256&code_challenge=ORq8qTX7awZv4TNdb8mS3sDzSUTXaix-BI-7DiU77PQ

Assuming you used the appropriate information, you should be issued an authorization code to use to obtain a token.

Step 4:  Obtain a Token

With the authorization code we will make a normal OAuth token request for the authorization code flow.   The only difference is instead of passing a client secret, we will present our code_verifier from step 1.   In lieu of validating the secret, the server takes the code_verifier, re-hashes it according to the code_challenge_method, then compares it to the code_challenge value presented to obtain an authorization code.  If they match and everything else is acceptable, you will be issued an access token.

POST: https://mycommunity.com/api.ashx/v2/oauth/token
CONTENT-TYPE:application/x-www-form-urlencoded
BODY:
client_id={clientId}
&code={authorizationcode}
&grant_type=authorization_code
&redirect_uri={redirectUri}
&code_verifier=7i23cSQ28IZ1.dT.GgirgCld~OWcbftEZM-zIaEMspmR6xvu5IcRSBT.NmXWpXQ1.dR67XBAELy_O7V5JW7tn~GrWQD4CDhYO~ouBrOqJOdYd61mV5nSdfpoJ0n8y6V6


Sample Single Page Application

You can download all relevant files for this sample here: 5661.pkce-sample.zip

Setup

To setup the demo, 

  1. Place the pkce.html and communityoauth.js files in the root folder of an SSL encrypted website. 
  2. Using Community 12.1+, 
    1. Go to Administration > Integration > OAuth Clients and create a new OAuth client with the Main URL and Redirect URL set to the full HTTPS URL of the pkce.html file, Client Type set to "Public", Authorization Types set to "Authorization Code." Once created, copy the Client ID value and update the client to enable the "Web Service" read scope under the "Information" category.
    2. Go to Administration > Integration > CORS and enable CORS with Origins set to the HTTPS root URL of the demo app location (https://hostname/)
  3. Edit the pkce.html file and find the window.communityOAuth.initialize() function call. Update the communityBaseUrl to be the full HTTPS URL of the Community site (including the trailing /), update clientId to be the client ID of the OAuth client created in Community, update url to be the full HTTPs URL of the pkce.html file.

Demo Usage

Once installed and configured, navigating to pkce.html should show:

Clicking login will redirect to the Community where the user will login if not logged in already and will then be presented with the authorization page:

Clicking "Allow" will redirect back to the demo app and show:

Showing the OAuth token, expiration date, authorized scopes, and accessing user's display name. The app stores the token to local storage and will refresh it to maintain access to Community as long as the pkce.html page is active.

Extending the Sample

This sample is meant to be a minimal implementation to serve as a reusable and extendable implementation. It has no external framework dependencies and uses the recommended SHA-256-based hashing algorithm to authorize access via PKCE. 

The communityOAuth object created by the commmunityoauth.js file exposes options in its initialize function to adjust behavior:

  • communityBaseUrl: The full HTTPS URL of the Community with / suffix.
  • clientId: The OAuth Client ID configured in Community
  • url: The client's full HTTP URL (the location of pkce.html in the example, including pkce.html)
  • scopes: The array of requested REST API scopes to request when authorizing access with the Community. This list must be within the enabled scopes configured for the OAuth client within Community, but it can be a subset if access to all enabled scopes is not required by the app.
  • authorizationError: A function that is called when an error occurs. It is passed an object consisting of error (the error code) and description (a description of the error if received). The error codes are defined by the OAuth specification.
  • authorizationChanged: A function that is called when authorization state changes. This could be cause of a login/logout or anonymous authorization.
  • enableAnonymous: true/false. When true, a token will be retrieved for the anonymous user when a user is not explicitly authenticated. When false, no API access is possible when an explicit user is not authenticated.
  • state: The initial state string. State is persisted through authorization redirects.

Once initialized, the communityOAuth object exposes an API to enable working with Community authorization data:

  • communityOAuth.isAnonymous(): Returns true if the current token represents the anonymous user.
  • communityOAuth.isAuthorized(): Returns true if the app has a current access token.
  • communityOAuth.accessToken(): Returns the OAuth access token which can be used to issue REST requests against the Community on behalf of the authorized user.
  • communityOAuth.expires(): Returns a JavaScript Date representing the expiration date/time of the current access token. The app will attempt to maintain the token and the expiration date will change as the token is refreshed.
  • communityOAuth.grantedScopes(): Returns the scopes granted with the current token. This list can be reviewed to detect if additional scopes may be required which would require a reauthorization.
  • communityOAuth.unauthorize(): Deletes the current access token within Community and forgets the token locally. If anonymous access is enabled, the anonymous user's token will be retrieved immediately.
  • communityOAuth.authorize(): Redirects to Community to request a new authorization.
  • communityOAuth.state() and communityOAuth.state(value): Enables reading/writing the OAuth state string. When calling authorize(), the current state will be persisted through the authorization redirect and can be read in the authorizationChanged function.