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.tfvars
And 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.com or okta.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.local
The above script generates the
.env.development.local
file in thespa
folder. 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.json
The above script generates the
.env.json
file in the/serverless
folder. Examine its contents and edit if necessary (use the sample.env.json.sample
as 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.local
file.
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
POST
a Stripe Checkout session.- When initiating the Checkout session, we provide the Stripe API with the mandatory
success_url
andcancel_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.completed
event to our stripe.js (the webhook) Lambda function.- The webhook updates the Okta user identified by
client_reference_id
and sets the custom profile attributestripeCustomerId
to 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_url
url. 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
stripeCustomerId
claim in the id and access tokens. - If the webhook has done it's job, the
stripeCustomerId
cliam 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.json
file in the/serverless
folder.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 id
STRIPE_WEBHOOK_SECRET The CLI printed a webhook secret key to the console when you started the stripe listen command
in the previous step -
When presented with the Stripe Checkout page, use the test credit card number
4242424242424242
. Enter any future expiration date and CVC.