Tuesday, November 18, 2014

JWT Bearer token flow can be used for community users -- example in cURL

Abstract

Problem
How can community users authenticate to Salesforce via the API without having to give their permission?
Answer
Use the JWT Bearer Token Flow
Disclaimer
I was going to wait a while longer before posting this to make sure it was beautifully formatted and brilliantly written--but that wouldn't have helped anyone trying to solve this problem in the meantime (like I was a few weeks back).

So in the spirit of both this blog's name and agile development, I'm publishing it early, perhaps not often, but hopefully in-time for another developer.
Attributions
Thanks to Jim Rae (@JimRae2009) for suggesting this approach, inspired by his work integrating canvas with node.js on the desktop, and his related Dreamforce 2014 presentation.

Background

A client of ours has an existing, non-salesforce, website with LOTS (tens of thousands) of users.  The client also has a Salesforce ServiceCloud instance they use for all their customer support, and they wanted their customers to interact with the CRM through their website, without iframes or exposing the SF portal to their users. 
The solution is to use JWT Bearer Token Flow.  Salesforce does not support username/password authorization for community-licensed users, and the other OAuth flows require a browser to intermediate between two domains. 
Though the document above does a good job describing the flow, it's a little weak on specifics.  Luckily, there's a reference Apex implementation on github (salesforceidentity/jwt), and below I'll provide a reference implementation using cURL.

Configuring your connected app

But before starting, there's a few things to know about your connected app.

  1. Your connected app must be created inside the target Salesforce instance.  You cannot re-use the same consumer and client values across orgs unless your app is part of a package.
  2. Your connected app must also use digital signatures.  This will require creating a certificate and private key.  Openssl commands for doing this appear later in this article.
  3. You must set the "admin approved users are pre-authorized" permitted users option to avoid login errors.

Configuring your community

  1. The community profile must allow access to the Apex classes that implement your REST interface
  2. Each community user will require the "API Enabled" permission.  This cannot be specified at the profile-level.

Creating the certificate

A single openssl command can create your private key and a self-signed certificate.
openssl req \
    -subj "/C=US/ST=MI/L=Troy/O=Xede Consulting Group, Inc./CN=xede.com" \
    -newkey rsa:2048 -nodes -keyout private.key \
    -x509 -days 3650 -out public.crt
Substitute your own values for the -subj parameter.  It's a self-signed certificate so no one will believe you anyway.  The benefit of using the -subj parameter is to avoid answer the questions about the certificate interactively.
The file "public.crt" is the certificate to load into your connected app on Salesforce.

Creating a community user

If you already have a community user you can skip to the next section.  If you don't, you will need to create one to test with. 
Make sure the user that creates the community user (either from a Contact or an Account if person-accounts are enabled) has a role.  Salesforce will complain when the Contact is enabled for login if the creating user doesn't have a role.

cURL Example

DrozBook:scripts tgagne$ cat jwlogin
#!/bin/bash

if [ $# -lt 2 ]; then
 echo 1>&2 "usage: $0 username sandbox"
 exit 2
fi

export LOGINURL=https://yourportalsite.force.com/optional-part
export CLIENTID='3MVG9Gmy2zm....value from connected-app...OQzJzb4BF469Fkip'

#from https://help.salesforce.com/HTViewHelpDoc?id=remoteaccess_oauth_jwt_flow.htm#create_token

#step 1
jwtheader='{ "alg" : "RS256" }'

#step 2
jwtheader64=`echo -n $jwtheader | base64 | tr '+/' '-_' | sed -e 's/=*$//'`

timenow=`date +%s`
expires=`expr $timenow + 300`

#step3
claims=`printf '{ "iat":%s, "iss":"%s", "aud":"%s", "prn":"%s", "exp":%s }' $timenow $CLIENTID $LOGINURL $1 $expires`

#step4
claims64=`echo -n $claims | base64 | tr '+/' '-_' | sed -e 's/=*$//'`

#step5
token=`printf '%s.%s' $jwtheader64 $claims64`

#step6
signature=`echo -n $token | openssl dgst -sha256 -binary -sign private.key | base64 | tr '+/' '-_' | sed -e 's/=*$//'`

#step7
bigstring=`printf '%s.%s' $token $signature`

curl --silent \
 --data-urlencode grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer \
 --data-urlencode assertion=$bigstring \
 $LOGINURL/services/oauth2/token \
 -o login.answer

Comments on jwlogin

  • LOGINURL is the from the community's Administrative Settings tab.
  • CLIENTID is the connected app's "Consumer Key."
  • The big trick in the script above is step 6, the signing.  The token must be hashed and signed with a single openssl command.

Using the token in subsequent cURL commands

A successful response the authentication request will resemble:
{"scope":"id full custom_permissions api visualforce web openid chatter_api","instance_url":"https://allyservicingcrm--gagne2.cs7.my.salesforce.com","sfdc_community_url":"https://gagne2-gagne2.cs7.force.com/customers","token_type":"Bearer","sfdc_community_id":"0DBM00000008OTXOA2","access_token":"00DM0000001dHnA!AQsAQIf7Muuu5BDtn8SXgWDJdwFXmvLAoRcqp0jZaObiv_6js.RSjK2ZZOCU29DSPc5s5JfsHdzmQsYpeFEZg7vgj2ynWTvi"}
It makes more sense if I pretty-print it.
{
    "scope": "id full custom_permissions api visualforce web openid chatter_api",
    "instance_url": "https:\/\/mydomain--sandboxname.cs7.my.salesforce.com",
    "sfdc_community_url": "https:\/\/communityname-sandboxname.cs7.force.com\/customers",
    "token_type": "Bearer",
    "sfdc_community_id": "0DBM00000008OTXOA2",
    "access_token":  "00DM0000001dHnA!AQsAQIf7Muuu5BDtn8SXgWDJdwFXmvLAoRcqp0jZaObiv_6js.RSjK2ZZOCU29DSPc5s5JfsHdzmQsYpeFEZg7vgj2ynWTvi"
}
The two important pieces of information above are the instance_url and the access_token.

The best way to describe how to use this information is to show you a subsequent curl command before substitution, and after.

Before (source)

DrozBook:scripts tgagne$ cat rerun
#!/bin/bash

INSTANCE=`sed -e 's/^.*"instance_url":"\([^"]*\)".*$/\1/' login.answer`
TOKEN=`sed -e 's/^.*"access_token":"\([^"]*\)".*$/\1/' login.answer`
set -x
curl \
    --silent \
    -H "Authorization: Bearer $TOKEN" \
    "$INSTANCE/services/apexrest/$1"

After

DrozBook:scripts tgagne$ rerun SMInquiry/xyzzy
+ curl --silent -H 'Authorization: Bearer 00DM0000001dHnA!AQsAQIf7Muuu5BDtn8SXgWDJdwFXmvLAoRcqp0jZaObiv_6js.RSjK2ZZOCU29DSPc5s5JfsHdzmQsYpeFEZg7vgj2ynWTvi' https://mydomain--sandboxname.cs7.my.salesforce.com/services/apexrest/SMInquiry/xyzzy


Follow @TomGagne