This demo showcaes key concepts in protecting API resources using Okta's API Access Management.
A highly stylized sample SPA is provided(in the spa folder) to drive the demo. This make believe e-commerce website incorporates the following functionality:
- Browse anonymously
- Anonymous -> Known user with low firction "signup"
- Site contents protected until a user "registers" and provides payment.
The SPA's interaction with the resource server (in the serverless folder) are the main demo points showcasing how Okta protects API resources with OAuth 2.0.
All the identity functionality is powered by Okta. And as a bonus, the demo also provides a sample integration with Stripe Checkout, so we get to see a sample integration between the identity provider (Okta) and payments platform (Stripe).
- Okta developer account. Signup here.
- Optional: Stripe account (to see the billing integration). Signup here.
- Serverless: The sample APIs are built on Serverless and is in the serverless folder. See install instructions.
- Terraform 0.14.x: To automatically provision Okta resources (lots of manual steps if we do it by hand!). Install for your operating system.
Note: Make sure to use Terraform 0.14.x
A visitor to the website (the demo SPA) browses around but cannot access content and is prompted to sign-up instead. The sign-up process only requires the visitor to provide an email. The "Sign Up" button makes request to the signup.js Lambda function, which creates the user in Okta, logs the user in and returns an Okta sessionToken. The SPA uses the sessionToken to start an Okta session by using the /authorize endpoint to retrieve an id_token and an access_token for the SPA.
Going forward, all API calls are protected by the access_token retrieved above.
"Progress" the "prospect" user (we only know their email) to a "customer" by collecting preferences and most importantly, payment info.
By providing email, the website allows the visitor is see limited content. But in order to gain full access of the site, they are prompted to subscribe to the service by providing payment (via Stripe). A registration form collects information from the visitor to complete their profile (e.g. by entering full name and selecting preferences). And the "Submit" button makes a request to the subscribe.js Lambda function, which forwards the profile updates to Okta. It also initiates a Stripe Checkout Session and returns the "session id" that's required for the SPA to redirect to Stripe Checkout (More on this later).
AWS API Gateway is configured with the custom Lambda authorizer auth.js to prevent access to the API unless the access_token retrieved previously is present in the request.
The user accesseses site contents by making API calls. While each call requires an access_token, the resources' API also looks at claims in the token to determine the level of access. Okta is configured to return token claims that describe whether or not the user is a "prospect" or "customer", so the video.js Lambda function immediately knows of the user context without having to make additional lookups to the user-store. In other words, based on the token's claims, the API returns different results.
cd into the terraform folder
Then, rename the "sample" tfvars file
mv terraform.tfvars.sample terraform.tfvarsAnd edit in the values:
| var | value | 
|---|---|
| org_name | The "subdomain" part of the Okta developer account's url. e.g. dev-668899 | 
| base_url | The Okta developer account's URL hostname: either oktapreview.comorokta.com | 
| api_token | Get an API token from the Okta developer account's admin UI | 
Now run terraform init
terraform init
Then "plan"
terraform plan
Then apply
terraform apply
Enter "yes" at the prompt.
Take a look using the Okta Admin UI after this is done and notice the resources that were provisioned:
- A couple custom profile attributes
- A couple groups
- An OIDC app
- An AuthorizationServer with some custom Scopes and Claims, and a couple specific Access Policy Rules
At the completion of terraform apply, you'll see some outputs for ids of resources provisioned. We need those ids in our local environment files for the SPA and Serverless.
Using the Terraform outputs, generate a .env.development.local file for the SPA:
(The the command below in the /terraform folder)
terraform output | grep issuer | sed -e "s/issuer/VUE_APP_ISSUER/g" > spa.env.development.local \
&& terraform output | grep client_id | sed -e "s/client_id/VUE_APP_CLIENT_ID/g" >> spa.env.development.local \
&& terraform output | grep prospect_group_id | sed -e "s/prospect_group_id/VUE_APP_PROSPECT_GROUP_ID/g" >> spa.env.development.local \
&& terraform output | grep customer_group_id | sed -e "s/customer_group_id/VUE_APP_CUSTOMER_GROUP_ID/g" >> spa.env.development.local \
&& cp spa.env.development.local ../spa/.env.development.localThe above script generates the
.env.development.localfile in thespafolder. Examine its contents and make any changes or fixes if necessary.
Next, generate a .env.json file for Serverless:
(The the command below in the /terraform folder)
touch serverless.env.json \
&& echo "{" > serverless.env.json \
&& echo '  "AWS_PROFILE": "serverless-admin",' >> serverless.env.json \
&& echo '  "AWS_REGION": "us-west-2",' >> serverless.env.json \
&& echo '  "ENVIRONMENT": "dev",' >> serverless.env.json \
&& terraform output | grep issuer | sed -e 's/issuer =/  "ISS":/g' | sed -e 's/$/,/g' >> serverless.env.json \
&& echo '  "AUD": "api://bod.unidemo",' >> serverless.env.json \
&& cat terraform.tfvars | grep api_token | sed -e 's/api_token/  "API_KEY"/g' | sed -e 's/=/: /g' | sed -e 's/$/,/g' >> serverless.env.json \
&& terraform output | grep prospect_group_id | sed -e 's/prospect_group_id =/  "PROSPECT_GROUP_ID":/g' | sed -e 's/$/,/g' >> serverless.env.json \
&& terraform output | grep customer_group_id | sed -e 's/customer_group_id =/  "CUSTOMER_GROUP_ID":/g' >> serverless.env.json \
&& echo "}" >> serverless.env.json \
&& cp serverless.env.json ../serverless/.env.jsonThe above script generates the
.env.jsonfile in the/serverlessfolder. Examine its contents and edit if necessary (use the sample.env.json.sampleas a guide)
Edit the following 2 variables (to match your AWS environment) and leave the rest alone (unless there are formatting errors):
var value AWS_PROFILE setup (or use an existing) AWS profile using aws-cli. See instructionsAWS_REGION aws region where you want to deploy to 
cd into the serverless folder
and install the dependencies
npm install
We don't have to deploy. For testing and demo purposes, we'll use serverless-offline, which emulates AWS Lambda and API Gateway. This should already be installed during npm install.
serverless offline start
This'll bring up the API on localhost:3000
cd into the spa folder
and install the dependencies
npm install
then compile and serve
npm run serve
The SPA and the resource server are now both up. Open up your browser to http://localhost:8080 to use the demo
The demo app provides a "Signin with Facebook" example but this needs to be configured in Okta first:
- See the add Facebook instructions on how to configure Facebook as an external identity provider to Okta.
- Obtain the "idp id" after configuration is complete
- Add VUE_APP_FB_ID=<idp id>to the SPA's.env.development.localfile.
An e-commerce site demo isn't complete without billing integration. And one of the easiest payment form integrations is Stripe Checkout.
Here's our basic implementation:
- At the end of the subscribe.js Lambda function, we POSTa Stripe Checkout session.- When initiating the Checkout session, we provide the Stripe API with the mandatory success_urlandcancel_url.
- Also realize that prior to this, we've already created the user object in Okta. Thus, we also provide the Stripe Session API the client_reference_id, setting it equal to the Okta user id. This part is CRITICAL because we're going to use it later in the webhook.
 
- When initiating the Checkout session, we provide the Stripe API with the mandatory 
- The SPA uses the above session id to redirect to the Stripe hosted checkout page.
- Upon checkout completion, Stripe redirects back to the SPA at success_url.
- Stripe also fires off the checkout.session.completedevent to our stripe.js (the webhook) Lambda function.- The webhook updates the Okta user identified by client_reference_idand sets the custom profile attributestripeCustomerIdto the customer id found in the payload of the event.
 
- The webhook updates the Okta user identified by 
- Meanwhile, the browser redirects back to a SPA component at the success_urlurl. Javascript on this page "polls" Okta by constantly requesting new set of id_token and access_tokens from Okta until it finds what it needs in the tokens:- We configured Okta to return the stripeCustomerIdclaim in the id and access tokens.
- If the webhook has done it's job, the stripeCustomerIdcliam should be populated in the tokens. And when it does, the SPA stops polling, updates the user-context and returns to the home page.
 
- We configured Okta to return the 
- 
Add VUE_APP_STRIPE_PUBLISHABLE_KEY=<replace-with-your-publishable-key>to the SPA's.env.development.local. You can get the value from your Stripe developer dashboard.
- 
Install the Stripe CLI and link it to your Stripe account. 
- 
Listen to webhook events and forward it to our serverless offline running on port 3000 stripe listen --forward-to http://localhost:3000/dev/stripe/webhook
- 
Add the following 3 key-values to the serverless.env.jsonfile in the/serverlessfolder.var value STRIPE_SECRET_KEY <replace-with-your-secret-key>. Get the value from your Stripe developer dashboardSTRIPE_PRICE_ID Setup a product in your Stripe developer dashboard and get its API idSTRIPE_WEBHOOK_SECRET The CLI printed a webhook secret key to the console when you started the stripe listen commandin the previous step
- 
When presented with the Stripe Checkout page, use the test credit card number 4242424242424242. Enter any future expiration date and CVC.