Newer Version Available

This content describes an older version of this product. View Latest

Token Exchange Handler Validation and Subject Mapping

When you have multiple apps and microservices serving data to an app—and a central identity provider authenticating users—the OAuth 2.0 token exchange flow simplifies your integrations. By exchanging a token from the identity provider for a Salesforce access token, you can give users access to their Salesforce data in your app without redesigning your integration pattern. During the token exchange flow, a user who is authenticated with the identity provider requests access to Salesforce data in your app. Because the user is already logged in, the app can pass the user’s tokens straight to Salesforce. Before Salesforce can grant its own tokens in return, it uses an Apex token exchange handler to validate the tokens from the identity provider and map them to a Salesforce user. To build your validation and subject mapping processes, create a class that extends the Auth.Oauth2TokenExchangeHandler Apex class. In addition to creating the token exchange handler Apex class, you must define an OauthTokenExchangeHandler metadata type.
Available in: Enterprise, Unlimited, Performance, and Developer Editions

Here’s an example of the general format of the token exchange handler Apex class.

1global abstract class Oauth2TokenExchangeHandler {
2    
3    //First method called in the handler
4    global virtual Auth.TokenValidationResult validateIncomingToken(String appDeveloperName, Auth.IntegratingAppType appType, String incomingToken, Auth.OAuth2TokenExchangeType tokenType) {
5        //Validate the identity provider’s token. Depending on your use case and token type, write validation logic that does these things:
6        // Use the token to make a callout to the identity provider’s User Info endpoint
7        // Use the token to make a callout to identity provider’s Introspection endpoint
8        // Validate a SAML response
9        // Validate a JWT locally
10        // The appDeveloperName is the developer name of the Connected App or External Client App
11        //The IntegratingAppType is an ENUM that is either a Connected App or External Client App
12        // Once you validate the token, return true or false
13        return null; 
14    }
15    
16    //Second method called in the handler
17    global virtual User getUserForTokenSubject(Id networkId, Auth.TokenValidationResult result, Boolean canCreateUser, String appDeveloperName, Auth.IntegratingAppType appType) {
18        //To map the subject of the token to a Salesforce user, write code that does these things:
19        // Get data directly from the token, and query for the user in Salesforce
20        // Get data from the identity provider’s User Info endpoint using the token and query for the user in Salesforce
21        // Get data from the SAML assertion and query for the user in Salesforce
22        
23        // If the user is not in Salesforce, and canCreateUser is true, set up a User object
24        // This includes external users, so it can include an account and contact
25        
26        // If the user Id is null, Salesforce automatically inserts the user(assuming that canCreateUser is true)
27        return null; 
28    }
29}

The way you validate your tokens and map subjects is up to you and depends on your use case, identity provider, and token type. Use these examples to get started.

Validate a JWT

To validate tokens during the OAuth 2.0 token exchange flow, use the validateIncomingToken method in the Auth.Oauth2TokenExchangeHandler class.

In this example, the handler validates a JSON Web Token (JWT) from the identity provider. The handler determines that the incoming token is a JWT and uses the validateJWTWithKey method in the Auth.JWTUtil class to validate the JWT with a public key. The resulting Auth.TokenValidationResult class contains information about whether the token is valid.

1global override Auth.TokenValidationResult validateIncomingToken(String appDeveloperName, Auth.IntegratingAppType appType, String incomingToken, Auth.OAuth2TokenExchangeType tokenType) {        
2        if (tokenType == Auth.OAuth2TokenExchangeType.JWT) {
3           // Validates the JWT with a a public key, but we also provide methods to validate it with a certificate (Auth.JWTUtil.validateJWTWithCert) or with a keys endpoint (Auth.JWTUtil.validateJWTWithKeysEndpoint)
4           Auth.JWT jwt = Auth.JWTUtil.validateJWTWithKey(incomingToken,'MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMI...');
5           return new Auth.TokenValidationResult(true);
6        }
7        
8        return new Auth.TokenValidationResult(false); // Returns a general 'Token handler validation failed' message that you can customize
9}

Validate an Opaque Access Token

For opaque access tokens, call out to the introspection or user info endpoints on the external identity provider. In this example, the handler sends a POST request to the identity provider’s introspection endpoint. It parses the identity provider’s JSON response so that Salesforce can understand and validate it using the validateIncomingToken method.

1global override Auth.TokenValidationResult validateIncomingToken(String appDeveloperName, Auth.IntegratingAppType appType, String incomingToken, Auth.OAuth2TokenExchangeType tokenType) {        
2        if (tokenType == Auth.OAuth2TokenExchangeType.ACCESS_TOKEN) {
3           // Validate the token with a call out to the introspection endpoint
4           String body = 'client_id=3MVG9AOp4kbriZ...&client_secret=71E147927AC...&token=00Dxx0000006H5T!AQEA...';
5           HttpRequest req = new HttpRequest();                            
6           req.setMethod('POST');
7           req.setEndpoint('https://my.company.com/services/oauth2/introspect');
8           req.setHeader('Content-Type', 'application/x-www-form-urlencoded');
9           req.setBody(body);
10           Http http = new Http();   
11           HttpResponse res = http.send(req);
12           
13           Boolean active;
14           String username;
15           Auth.UserData userData;
16           
17           if(res.getStatusCode() == 200) {
18                System.JSONParser parser = System.JSON.createParser(res.getBody());
19                try {
20                    while((active == null || username == null) && parser.nextToken() != null) {
21                        if (parser.getCurrentToken() == JSONToken.FIELD_NAME) {
22                            String fieldName = parser.getText();
23                            
24                            if (fieldName == 'active') {
25                                parser.nextToken();
26                                active = parser.getBooleanValue();
27                                
28                                if (!active) {
29                                  return new Auth.TokenValidationResult(false);
30                                }
31                            }
32                            if (fieldName == 'username') {
33                                parser.nextToken();
34                                username = parser.getText();
35                            }
36                        }
37                    }
38                
39                if (active != null && username != null) {
40                    userData = new Auth.UserData(null, null, null, null, null, null, username, null, null, null, null);
41                }    
42                    
43                } catch(JSONException e) {
44                    return new Auth.TokenValidationResult(false); // Returns a general 'Token handler validation failed' message that you can customize
45                }
46           } else {
47                return new Auth.TokenValidationResult(false); // Returns a general 'Token handler validation failed' message that you can customize
48           }
49           
50           return new Auth.TokenValidationResult(true, null, userData, incomingToken, tokenType, null);
51        }
52        
53        return new Auth.TokenValidationResult(false); // Returns a general 'Token handler validation failed' message that you can customize
54    }

Find or Create a User

During subject mapping, your handler finds the subject (end user) of the incoming token and tries to link it to a Salesforce user. Optionally, you can configure your handler to help create users. If the isUserCreationAllowed field on the OauthTokenExchangeHandler metadata type and the canCreateUser parameter on the Apex handler are both true, the handler can be used to set up a new user. The handler doesn’t actually create the user—it returns a User object into which Salesforce automatically inserts the user.

If necessary, to get more information about the incoming subject, the handler can call out to the external identity provider or another external system.

In this example, the handler gets information about the user from the identity provider’s token and looks for an existing Salesforce user. If no user exists, it creates a User object.

1global class MyTokenExchangeHandler extends Auth.Oauth2TokenExchangeHandler {
2    
3
4    global override Auth.TokenValidationResult validateIncomingToken(String appDeveloperName, Auth.IntegratingAppType appType, String incomingToken, Auth.OAuth2TokenExchangeType tokenType) {        
5        // Validates the incoming token
6        
7        Auth.UserData userData = new Auth.UserData('someIdentifier', 'someFirstName', 'someLastName', 'someFullName', 'someEmail', 'someLink', 'someUsername@my.org', 'en_US', 'someProvider', 'someSiteLoginUrl', null);
8        
9        return new Auth.TokenValidationResult(true, null, userData, incomingToken, tokenType, null);
10    }
11    
12
13   global override User getUserForTokenSubject(Id networkId, Auth.TokenValidationResult result, Boolean canCreateUser, String appDeveloperName, Auth.IntegratingAppType appType) {
14        String username = result.getUserData().username;
15        
16        List<User> existingUser = [SELECT Id, Username, Email, FirstName, LastName, Alias, ProfileId FROM User WHERE Username=:username LIMIT 1];
17        
18        if (!existingUser.isEmpty()) {
19            return existingUser[0];
20        }
21        
22        User u = new User();
23        u.Username = username;
24        u.Email = 'some@email.com';
25        u.LastName = 'SomeLastName';
26        u.Alias = 'MyAlias';
27        u.TimeZoneSidKey = 'America/Los_Angeles';
28        u.LocaleSidKey = 'en_US';
29        u.EmailEncodingKey = 'UTF-8';
30        
31        Profile p = [SELECT Id FROM profile WHERE name='Standard User'];
32        u.ProfileId = p.Id;
33        u.LanguageLocaleKey = 'en_US';
34     
35        return u;
36       
37    }
38}