wissel.net

Usability - Productivity - Business - The web - Singapore & Twins

Reuse a 3rd Party Json Web Token (JWT) for Salesforce authentication


The scenario

You run an app, could be a mobile native, a SPA, a PWA or just an application with JavaScript logic, in your domain that needs to incorporate data from your Salesforce instance or one of your Salesforce communities.

Users have authenticated with your website and the app is using a JWT Bearer Token to establish identity. You don't want to bother users with an additional authentication.

What you need

Salesforce has very specific requirements how a JWT must be formed to qualify for authentication. For example the token can be valid only for 5 minutes. It is very unlikely that your token matches the requirements.

Therefore you will need to extract the user identity from existing token, while checking that it isn't spoofed and create a new token that you present to Salesforce to obtain the session token. So you need:

  1. The key that can be used to verify the existing token. This could be a simple String, used for symmetrical signature or an X509 Public Key
  2. A private key for Salesforce to sign a new JWT (See below)
  3. A configured Connected App in Salesforce where you upload they full certificate and obtain the Consumer Key
  4. Some place to run the code, like Heroku

Authentication Flow for 3rd party JWT

Create your certificate for Salesforce

Salesforce uses a X509 certificate to validate the signatures. The various formats can be a little pain, since PKCS has many flavors. To save yourself some pain, here is the script to generate what you need. Please note the script prompts for input:

openssl genrsa -des3 -passout pass:somepass -out server.pass.key 2048
openssl rsa -passin pass:somepass -in server.pass.key -out temp.key
rm server.pass.key
openssl req -new -key temp.key -out server.csr
openssl x509 -req -sha256 -days 365 -in server.csr -signkey temp.key -out server.crt
openssl pkcs8 -topk8 -inform PEM -outform PEM -nocrypt -in temp.key -out server.key
rm temp.key

Configure Salesforce

In Salesforce you use the App Manager to create a new Connected App:

  • Fill in Connected App Name, API Name and Contact eMail
  • Check ?Enable OAuth Settings?
  • Fill in a value for Callback URL, http://localhost/callback will do. We won't use that value
  • Check ?Use digital signatures?. Upload the certificate server.crt you created in the step before
  • for the ?Selected OAuth Scopes? you probably need api id web and others, depending on your use case. What you must have is refresh_token offline_access, otherwise authentication will fail
  • Check ?Require secret for Web Server Flow? (default setting anyway)
  • Save the entry. Note down the consumer key, you will need it in the code

The code (Java Edition)

I will show you the essential functions, storing and retrieving variables securely and wrapping a web API around is left to the reader. I wrote the sample using Eclipse vert.x in case you wonder where the JsonObject comes from.

Extracting the user from the 3rd party JWT

Typically the user is stored in the subject property of the JWT Claim. Depending on the signature (symmetric encryption or public key), you need a different function to obtain the key, the extraction works the same:

public String extractSubjectFromJWT(final String incomingJWT) throws SignatureException {
        // Depending on production or demo mode we work with different keys
        if (this.isX509Key()) {
            Key key;
            try {
                key = this.getX509Key();
            } catch (InvalidKeySpecException | NoSuchAlgorithmException e) {
                this.logger.fatal(e.getMessage(), e);
                return null;
            }
            return Jwts.parser().setSigningKey(key).parseClaimsJws(incomingJWT).getBody().getSubject();
        } else {
            final byte[] key = this.getPlainKey();
            return Jwts.parser().setSigningKey(key).parseClaimsJws(incomingJWT).getBody().getSubject();
        }
    }

Generate the Salesforce compliant JWT key

The key requires the user as subject, the login URL as audience and the Consumer Key as issuer. The key must be the X509 private key. The validity needs to be 5 min into the future.

public String createJwt(final String subject) throws Exception {

        final String keyString = System.getenv("SFDC_KEY");
        final KeyFactory kf = KeyFactory.getInstance("RSA");
        final byte[] encoded = Base64.getDecoder().decode(keyString);
        final PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(encoded);
        final Key key = kf.generatePrivate(spec);

        final Claims claims = Jwts.claims();
        claims.setIssuer("Salesforce Connected APP ID");
        claims.setAudience("https://login.salesforce.com");
        claims.setSubject(subject);
        claims.setExpiration(new Date(System.currentTimeMillis() + 300000));

        final String compactJws = Jwts.builder()
                .setHeaderParam("alg", "RS256")
                .addClaims(claims)
                .signWith(SignatureAlgorithm.RS256, key)
                .compact();

        return compactJws;
    }

Exchange the JWT for a OAuth session token

Salesforce requires to POST a http form (yep, form not JSON) to the authentication endpoint /services/oauth2/token and returns JSON if when it is successful or fails:

private void handleAuth(final Message<JsonObject> incoming) {

        // Create a web client to post to Salesforce
        final WebClientOptions wco = new WebClientOptions();
        wco.setUserAgent("SDFC VertX Authenticator");
        wco.setTryUseCompression(true);
        final JsonObject incomingJson = incoming.body();
        final String jwt = incomingJson.getString("jwt");

        final WebClient sfdcClient = WebClient.create(this.vertx, wco);
        final MultiMap form = MultiMap.caseInsensitiveMultiMap();
        form.set("grant_type", "urn:ietf:params:oauth:grant-type:jwt-bearer");
        form.set("assertion", jwt);

        final int port = 443;
        // use login.salesforce.com for production
        final String host = "https://test.salesforce.com"; 
        final String path = "/services/oauth2/token";

        sfdcClient.post(port, host, path)
                .putHeader("Content-Type", "application/x-www-form-urlencoded").ssl(true)
                .sendForm(form, postReturn -> {
                    this.processOAuthResult(incoming, postReturn);
                });
    }

    private void processOAuthResult(final Message<JsonObject> incoming,
            final AsyncResult<HttpResponse<Buffer>> postReturn) {
        final JsonObject result = postReturn.result().bodyAsJsonObject();
        incoming.reply(result);

    }

As usual: YMMV


Posted by on 2018-05-03 10:42 | Comments (0) | categories: Heroku Salesforce

Comments

  1. No comments yet, be the first to comment