Introduction
I often meet customers who want to quickly understand how the OAuth2 Authorization Code grant type works, how Proof Key for Code Exchange (PKCE) works, and how they can execute the flows programatically to understand how it all hangs together.
This blog provides a sample script to execute the OAuth2 Authorization Code grant flow, along with support for PKCE using cURL.
What is the OAuth2 Authorization Code Grant Flow?
The Authorization Code grant is a two-step interactive process used when the client, for example, a Java application running on a server, requires access to protected resources. See the following RFC for more.
The following diagram demonstrates the Authorization Code grant flow:
Diagram courtesy of BackStage documentation
To keep this blog concise, the exact steps are documented here.
What is the OAuth2 Authorization Code Grant Flow with PKCE?
The flow is similar to the regular Authorization Code grant type, but the client must generate a code that will be part of the communication between the client and the authorization server (see the following RFC for more). This code mitigates against interception attacks performed by malicious users on the authorization code itself. It should be used by mobile or a JavaScript applications requiring access to protected resources.
The PKCE flow adds three parameters on top of those used for the Authorization code grant:
- code_verifier (form parameter): Contains a random string that correlates the authorization request to the token request.
- code_challenge (query parameter): Contains a string derived from the code verifier that is sent in the authorization request, and that needs to be verified later with the code verifier.
- code_challenge_method (query parameter): Contains the method used to derive the code challenge.
The following diagram demonstrates the Authorization Code grant with PKCE flow:
Diagram courtesy of BackStage documentation
To keep this blog concise the exact steps are documented here.
Environment Setup
In order to execute the script, the following prerequisites need to be met:
- A user must exist in the Identity repository. For this example, I am using the out-of-the-box demo user.
- An OAuth2 Provider Service in the target realm needs to be set up:
- From the AM Admin UI go to > Realm > Services > Add A Service.
- Select OAuth2 Provider from the drop-down menu.
- Add openid and profile as Supported Scopes and select Create. It should look something like this:
- An OAuth2 client needs to be configured:
- From the AM Admin UI, go to Realm >Applications > Add Client.
- Set the Client ID to client, Client Secret to SecureP455word!.
- Set the redirection URIs to http://www.test.com, Scope(s) to openid and profile. See the example below:
- Within the OAuth2 client’s configuration, select the Advanced tab and add Refresh Token to the Grant Types parameter. By default, refresh tokens are not generated unless this step is executed.
OAuth2 Script Execution Steps
The script is located here. The full extract is at the bottom of this blog. Essentially it works like this:
There are 9 functions:
jqCheck: The script relies on the jq JSON processor. This function checks for its presence.
authN: A function to authenticate a user and extract their SSO tokenId.
gen_PKCEMaterial: If the PKCE flow is invoked, this function generates the Challenge and Verifier.
getAuthCode: Generates the authorization code. If the PKCE flow is invoked, the code_challenge and challenge_method (SHA256) parameters are added to the POST body. NOTE: This example does not require user consent. For customer-facing environments, the system should seek consent. Other challenge methods are supported.
getTokens: A function to generate the access, refresh, and OIDC tokens from the authorization code.
hitTokenInfo: A function to call the …/tokeninfo endpoint with the access token.
hitIntrospectAccessToken Access: A function to call the …/introspect endpoint with the access token.
hitIntrospectAccessToken Refresh: A function to call the …/introspect endpoint with the refresh token.
hitUserInfo: A function to call the …/userinfo endpoint with the access token.
To execute, run:
./oauth2_test.sh with either the non-pkce or pkce flag. For example: ./oauth2_test.sh non-pkce
To force the client to support PKCE, set the Code Verifier Parameter Required parameter in the OAuth2 Provider > Advanced tab to All requests. Note that various different modes of operation are supported for PKCE (not just All Requests), see here for more information.
Sample output:
*********************
Authenticating demo user to generate SSO token
SSO Token: RHu9Rst-JvBNI2ueKffoYhQ2US0.*AAJTSQACMDIAAlNLABx1U2o2VnhjMEpOSVNad25Pc1VLdGhjSzdzNzQ9AAR0eXBlAANDVFMAAlMxAAIwMw..**********************
Generating PKCE Verifier
Verifier is: 9flDxvKL2RyjzY5dOMngEwgv8ks9lcuKsbPCaqDBm3mFVCuhuN
Challenge is: yzk8WwovZqXsvePxQ9rh7tUnVn86hb7l90_ivdIRzs8*********************
Getting auth code
Auth code is: BZq6cwIEa8X2RbViTUzQUIDiH68*********************
Getting access and refresh tokens
using auth code BZq6cwIEa8X2RbViTUzQUIDiH68
{
“access_token”: “WiG0SvVOakf5uVPt5scjU_9vuvc”,
“refresh_token”: “_L6WwjMn-YrnC10Ruo7zTNd2JiU”,
“scope”: “openid profile”,
“id_token”: “eyJ0eXAiOiJKV1QiLCJraWQiOiJ3VTNpZklJYUxPVUFSZVJCL0ZHNmVNMVAxUU09IiwiYWxnIjoiUlMyNTYifQ.eyJhdF9oYXNoIjoieVhkRkNMeTUtaVZGSHJuRHBTcndDQSIsInN1YiI6ImRlbW8iLCJhdWRpdFRyYWNraW5nSWQiOiI0ZDdmODRhNy05ZGU4LTQwMTUtODdkMi1hY2U2YWVlNjQyNTktMTgxMyIsImlzcyI6Imh0dHA6Ly9vcGVuYW0udGVzdC5jb206OTQ5Ni9vcGVuYW0vb2F1dGgyIiwidG9rZW5OYW1lIjoiaWRfdG9rZW4iLCJhdWQiOiJjbGllbnQiLCJjX2hhc2giOiIwZmxtVVl6bGxlQnczeURpX0xaQW1nIiwiYWNyIjoiMCIsIm9yZy5mb3JnZXJvY2sub3BlbmlkY29ubmVjdC5vcHMiOiIxTmdQYk5LOURkWU9HVmhDLXlvMG8xaVdneE0iLCJhenAiOiJjbGllbnQiLCJhdXRoX3RpbWUiOjE1Njg4OTkwNjksInJlYWxtIjoiLyIsImV4cCI6MTU2ODkwMjY2OSwidG9rZW5UeXBlIjoiSldUVG9rZW4iLCJpYXQiOjE1Njg4OTkwNjl9.ihw9ATB_l3cJxVz_nlhS8tFbWwU-cn72EpxcxcHD_1V8_BPw0J0W37alOZAryGjwqYSIStkCjuujkjkOJC-x8mCudsFzJj959XAI2wBfvQy2WQRb-MctxJGeUJUX9lcjT9UhotDaei89tZI3y-KY1RSfkw46xvx19twAJ2OrzpOA3Z97mWppU4yAy3eQu_63R4 — AqgSBdtkO7etmTnkJeC-H2KmEiu9JNL2y8qz5Eh6jfLEeQTXkbf5rwfQBH1n9_V6lR2dUND-99SMcqp0hqBPV_zzHRIgEglUiWRW4ry9P-ZApU9_LX5B8TOI49-alaAeR7B8id4FVSWcYvZl_w”,
“token_type”: “Bearer”,
“expires_in”: 3599
}*********************
Hitting tokeninfo endpoint
{
“realm”: “/”,
“profile”: “”,
“scope”: [
“openid”,
“profile”
],
“client_id”: “client”,
“expires_in”: 3599,
“token_type”: “Bearer”,
“access_token”: “WiG0SvVOakf5uVPt5scjU_9vuvc”,
“grant_type”: “authorization_code”,
“auth_level”: 0,
“auditTrackingId”: “4d7f84a7–9de8–4015–87d2-ace6aee64259–1811”,
“openid”: “”
}*********************
Hitting introspect endpoint for Access token
{
“iss”: “http://openam.test.com:9496/openam/oauth2",
“sub”: “demo”,
“auditTrackingId”: “4d7f84a7–9de8–4015–87d2-ace6aee64259–1811”,
“auth_level”: 0,
“active”: true,
“scope”: “openid profile”,
“client_id”: “client”,
“user_id”: “demo”,
“token_type”: “Bearer”,
“exp”: 1568902669
}*********************
Hitting introspect endpoint for Refresh token
{
“iss”: “http://openam.test.com:9496/openam/oauth2",
“sub”: “demo”,
“auditTrackingId”: “4d7f84a7–9de8–4015–87d2-ace6aee64259–1810”,
“auth_level”: 0,
“active”: true,
“scope”: “openid profile”,
“client_id”: “client”,
“user_id”: “demo”,
“token_type”: “refresh_token”,
“exp”: 1569503869
}*********************
Hitting userinfo endpoint
{
“family_name”: “demo”,
“name”: “demo”,
“sub”: “demo”
}*********************
Conclusion
There you have it — a programatic way to understand the OAuth2 Auth Code Grant type, along with PKCE all through cURL. Enjoy!
Script Extract
Note: This script is provided for sample and demonstration purposes only. It is not supported by ForgeRock, nor is it suitable for production use:
#!/bin/bash
# Written by Darinder S Shokar - ForgeRock Customer Success
# Script requires the "jq" tool be already installed to function
# Parameters. Modify as appropriate:
REALM=root
AM_HOST=http://openam.test.com:8080/openam
AM_AUTHENTICATE=$AM_HOST/json/realms/$REALM/authenticate
AM_AUTHORIZE=$AM_HOST/oauth2/realms/$REALM/authorize
AM_ACCESS_TOKEN=$AM_HOST/oauth2/realms/$REALM/access_token
AM_TOKENINFO=$AM_HOST/oauth2/realms/$REALM/tokeninfo
AM_INTROSPECT=$AM_HOST/oauth2/realms/$REALM/introspect
AM_USERINFO=$AM_HOST/oauth2/realms/$REALM/userinfo
SCOPES=openid%20profile
REDIRECT_URL=http://www.test.com
RESPONSE_TYPE=code
CLIENT_ID=client
CLIENT_PASSWORD=SecureP455word!
B64_CREDS=`echo -n $CLIENT_ID:$CLIENT_PASSWORD | /usr/bin/base64`
VERSION_HEADER='resource=2.0, protocol=1.0'
CONTENT_TYPE='application/json'
USERNAME=demo
PASSWORD=changeit
MODE=$1
if [ -z "$1" ]; then
echo "Execute using ./oauth2_test.sh non-pkce|pkce. For example ./oauth2_test.sh pkce"
exit 1
fi
jqCheck(){
hash jq &> /dev/null
if [ $? -eq 1 ]; then
echo >&2 "The jq Command-line JSON processor is not installed on the system. Please install and re-run."
exit 1
fi
}
authN(){
clear
echo "*********************"
echo "Authenticating $USERNAME user to generate SSO token"
SSO_TOKEN=`curl -s -X POST -H "Content-Type: $CONTENT_TYPE" -H "Accept-API-Version: $VERSION_HEADER" -H "X-OpenAM-Username: $USERNAME" -H "X-OpenAM-Password: $PASSWORD" -d '' "$AM_AUTHENTICATE" | jq -r .tokenId`
echo "SSO Token: $SSO_TOKEN"
echo ""
echo "*********************"
}
gen_PKCEMaterial() {
if [ $MODE == "pkce" ]; then
echo "Generating PKCE Verifier"
VERIFIER=`cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 50 | head -n 1`
echo "Verifier is: $VERIFIER"
#Generate PKCE Challenge from Verifier and convert / + = characters"
CHALLENGE=`echo -n $VERIFIER | shasum -a 256 | cut -d " " -f 1 | xxd -r -p | base64 | tr / _ | tr + - | tr -d =`
echo "Challenge is: $CHALLENGE"
echo ""
echo "*********************"
fi
}
getAuthCode() {
echo "Getting auth code"
if [ $MODE == "pkce" ]; then
AUTH_CODE=`curl --request POST --header "Content-Type: application/x-www-form-urlencoded" --Cookie "iPlanetDirectoryPro=$SSO_TOKEN" --data "redirect_uri=$REDIRECT_URL&scope=$SCOPES&response_type=$RESPONSE_TYPE&client_id=$CLIENT_ID&csrf=$SSO_TOKEN&decision=allow&code_challenge=$CHALLENGE&code_challenge_method=S256" "$AM_AUTHORIZE" -v --stderr - | grep "code=" | cut -d '=' -f2 | cut -d '&' -f1`
else
AUTH_CODE=`curl --request POST --header "Content-Type: application/x-www-form-urlencoded" --Cookie "iPlanetDirectoryPro=$SSO_TOKEN" --data "redirect_uri=$REDIRECT_URL&scope=$SCOPES&response_type=$RESPONSE_TYPE&client_id=$CLIENT_ID&csrf=$SSO_TOKEN&decision=allow" "$AM_AUTHORIZE" -v --stderr - | grep "code=" | cut -d '=' -f2 | cut -d '&' -f1`
fi
echo "Auth code is: $AUTH_CODE"
echo ""
echo "*********************"
}
getTokens() {
# If need to introduce break to analyse auth code in CTS uncomment this:
#read -n 1 -s -r -p "Press any key to continue"
echo "Getting access and refresh tokens"
echo "using auth code $AUTH_CODE"
if [ $MODE == "pkce" ]; then
TOKENS=`curl -s -X POST --user "$CLIENT_ID:$CLIENT_PASSWORD" -H "Cache-Control: no-cache" -d 'grant_type=authorization_code&redirect_uri='$REDIRECT_URL'&code='$AUTH_CODE'&code_verifier='$VERIFIER'' -k $AM_ACCESS_TOKEN | jq .`
else
TOKENS=`curl -s -X POST --user "$CLIENT_ID:$CLIENT_PASSWORD" -H "Cache-Control: no-cache" -d 'grant_type=authorization_code&redirect_uri='$REDIRECT_URL'&code='$AUTH_CODE'' -k $AM_ACCESS_TOKEN | jq .`
fi
echo $TOKENS | jq .
ACCESS_TOKEN=`echo $TOKENS | jq -r .access_token`
REFRESH_TOKEN=`echo $TOKENS | jq -r .refresh_token`
echo ""
echo "*********************"
}
hitTokenInfo() {
echo "Hitting tokeninfo endpoint"
TOKENINFO=`curl -s "$AM_TOKENINFO?access_token=$ACCESS_TOKEN" | jq .`
echo $TOKENINFO | jq .
echo ""
echo "*********************"
}
hitIntrospectAccessToken() {
echo "Hitting introspect endpoint for ${1} token"
INTROSPECT=`curl -s --request POST --header "Authorization: Basic $B64_CREDS" --data "token=${2}" "$AM_INTROSPECT" | jq .`
echo $INTROSPECT | jq .
echo ""
echo "*********************"
}
hitUserInfo() {
echo "Hitting userinfo endpoint"
USERINFO=`curl -s --request POST --header "Authorization: Bearer $ACCESS_TOKEN" -d '' "$AM_USERINFO" | jq .`
echo $USERINFO | jq .
echo ""
echo "*********************"
}
#Functions
jqCheck
authN
gen_PKCEMaterial
getAuthCode
getTokens
hitTokenInfo
hitIntrospectAccessToken Access ${ACCESS_TOKEN}
hitIntrospectAccessToken Refresh ${REFRESH_TOKEN}
hitUserInfo