Newer Version Available
OAuth 2.0 Token Exchange Handler Examples
| Available in: Enterprise, Unlimited, Performance, and Developer Editions |
During the OAuth 2.0 token exchange flow, when a user logs in to the primary app via the identity provider, the identity provider issues a token to the primary app. The primary app can’t use this token to directly access Salesforce data, but it can exchange the token for a Salesforce access token. To complete this exchange, the primary app uses an Apex token exchange handler. With the token exchange handler, Salesforce can issue its own access token by validating the identity provider’s token and mapping the token’s subject, which identifies the end user, to a Salesforce user.
To build an Apex token exchange handler, create a class that extends the Auth.Oauth2TokenExchangeHandler abstract class and customize its validation logic and subject mapping.
Token Exchange Handler Abstract Class
The Auth.Oauth2TokenExchangeHandler abstract class contains two methods. Use the first method, validateIncomingToken, to validate the identity provider’s token. Use the second method, getUserForTokenSubject, to map the token’s subject to a Salesforce user.
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 //This method must be overridden by the extending class
6 //Validate the identity provider’s token. Depending on your use case and token type, write validation logic that does these things:
7 // Use the token to make a callout to the identity provider’s User Info endpoint
8 // Use the token to make a callout to identity provider’s Introspection endpoint
9 // Validate a SAML response
10 // Validate a JWT locally
11 // The appDeveloperName is the developer name of the Connected App or External Client App
12 //The IntegratingAppType is an ENUM that is either a Connected App or External Client App
13 // After you validate the token, return true or false
14 return null;
15 }
16
17 //Second method called in the handler
18 global virtual User getUserForTokenSubject(Id networkId, Auth.TokenValidationResult result, Boolean canCreateUser, String appDeveloperName, Auth.IntegratingAppType appType) {
19 //This method must be overridden by the extending class
20 //To map the subject of the token to a Salesforce user, write code that does these things:
21 // Get data directly from the token, and query for the user in Salesforce
22 // Get data from the identity provider’s User Info endpoint using the token and query for the user in Salesforce
23 // Get data from the SAML assertion and query for the user in Salesforce
24
25 // If the user is not in Salesforce, and canCreateUser is true, set up a User object
26 // This includes external users, so it can include an account and contact
27
28 // If the user Id is null, Salesforce automatically inserts the user(assuming that canCreateUser is true)
29 return null;
30 }
31}The way you build your validation and subject mapping processes depends on your use case, identity provider, and token type. Use these examples to get started.
Token Exchange Handler Example Implementation
This example implementation extends the Auth.Oauth2TokenExchangeHandler abstract class.
In this example, the OAuth2TokenExchangeType enum specifies that the token is a JSON Web Token (JWT). The first method, validateIncomingToken, uses a method in the Auth.JWTUtil class to validate the token by calling an endpoint on the external identity provider.
Validating the token returns an instance of the Auth.TokenValidationResult class with information about the token and the user.
With the second method, getUserForTokenSubject, the handler gets information about the user from the token validation result. The example shows two ways to bundle the user data—either by creating a class with a custom data structure or by using the Auth.UserData class.
After the handler gets the user data from the token, it looks for a Salesforce user matching the token subject. In this example, the handler doesn’t find a user, so it creates a User object. To finish creating the user, Salesforce automatically inserts the User object for you.
1/*Token Exchange Handler Implementation Example*/
2public class MyTokenExchangeClass extends Auth.Oauth2TokenExchangeHandler{
3
4 public override Auth.TokenValidationResult validateIncomingToken(String appDeveloperName, Auth.IntegratingAppType appType, String incomingToken, Auth.OAuth2TokenExchangeType tokenType) {
5 //Depending on your incoming token, you validate it in different ways
6 //If the incoming token is an opaque access token or refresh token, validate it with a callout to the identity provider
7 //If it’s a SAML assertion, validate it by checking the XML
8 //If it’s an ID Token or JWT, try using our JWT validation methods
9 //This example assumes that the incoming token is a JWT and that there is a public keys endpoint on the identity provider
10 //Be very careful with any logic in this method, and test carefully before using
11
12 Boolean isValid = false;
13 Auth.JWT jwt;
14 //Custom data structure
15 CustomStructuredUserData customData;
16 //Standard user data structure
17 Auth.UserData userData;
18
19 if (tokenType == Auth.OAuth2TokenExchangeType.JWT || tokenType == Auth.OAuth2TokenExchangeType.ID_TOKEN) {
20 try {
21 jwt = Auth.JWTUtil.validateJWTWithKeysEndpoint(incomingToken, 'https://your-idp.com/keys', true);
22 isValid = true;
23 //These values are sourced from the JWT or ID Token
24 userData = new Auth.UserData('identifier', 'firstName', 'lastName', 'fullName', 'customer@email.com', 'link url', 'remote username', 'local', 'Provider (IDP Name)', '', new Map<String,String>());
25 //You can also pass data as generic object
26 customData = new CustomStructuredUserData();
27 } catch (Exception e) {
28 isValid = false;
29 }
30 } else if (tokenType == Auth.OAuth2TokenExchangeType.ACCESS_TOKEN || tokenType == Auth.OAuth2TokenExchangeType.REFRESH_TOKEN) {
31 //Putlogic for validating an opaque access token or refresh token here
32 //This validation typically involves a callout to the introspect or user info endpoints
33 //If you call out to the user info endpoint, make sure to pass the data from the validation into the getUserForTokenSubject method using an Apex class or the user data class
34 isValid = false;
35 } else if (tokenType == Auth.OAuth2TokenExchangeType.SAML_2) {
36 //Put logic for validating a SAML assertion here
37 //This validation involves XML parsing
38 isValid = false;
39 } else {
40 //You can add new token types. If you don’t know how to validate the token, always check the type and return false
41 isValid = false;
42 }
43
44
45 if(isValid){
46 return new Auth.TokenValidationResult(true, (object)customData, userData, incomingToken, tokenType, 'CustomErrorMessage');
47 } else {
48 return new Auth.TokenValidationResult(isValid);
49 }
50 }
51
52 public override User getUserForTokenSubject(Id networkId, Auth.TokenValidationResult result, Boolean canCreateUser, String appDeveloperName, Auth.IntegratingAppType appType) {
53 //If you passed data from the validation method, grab it now. Remember to cast back for the custom data
54 CustomStructuredUserData customData = (CustomStructuredUserData)result.data;
55 Auth.UserData userData = result.userData;
56
57 //If you don’t have any data from the token, you can perform a callout using the incoming token
58 String userToken = result.token;
59
60 //Now, search for a user
61 User u;
62 try {
63 u = [SELECT Id, IsActive FROM User WHERE email =: userData.email];
64 } catch (Exception e) {
65 //No user existed for this email address, or there were too many. Try looking harder
66 }
67
68 // If you didn’t find a user, check to see if you can create one
69 if (canCreateUser && (u == null)) {
70 u = new User();
71 u.firstName = userData.firstName;
72 u.lastName = userData.lastName;
73 //Finish setting user attributes. For external users, make sure you set up the contact/account/person account
74 //If you assign permission sets, do it in a future method to avoid mixed DML
75 //Returning the user from this method handles the insertion, so it’s not necessary to manually insert
76 }
77
78 return u;
79 }
80
81 //This class gives you a way to pass structured data between the validateIncomingToken and getUserForTokenSubject methods
82 //This example is for demonstration only. Implement this class in a way that matches the data that you are passing
83 private class CustomStructuredUserData {
84 public String customAttribute1;
85 public Integer customAttribute2;
86 public Map<String,Object> customAttribute3;
87 }
88}Examples for Validating Different Token Types
The custom logic for your implementation of the validateIncomingToken method depends on the token type. Here’s an overview of the options for different token types.
- For JWTs and ID tokens, use methods in the Auth.JWTUtil class.
- For opaque tokens, such as opaque access and refresh tokens, call out to the identity provider’s introspection or user info endpoints.
- For SAML assertions, write code to parse the XML from the assertion.
In this example, the handler validates a JWT from the identity provider. The handler determines the token type and uses the validateJWTWithKey method in the Auth.JWTUtil class to validate the JWT with a public key.
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 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}For opaque access tokens, which can’t be introspected locally on your app, call out to the introspection or user info endpoints on the external identity provider. In this example for validating an opaque token, the handler sends a POST request to the identity provider’s introspection endpoint and parses the identity provider’s JSON response so that Salesforce can understand it. It then validates the response 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 callout 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://<MyCompanyDomain>/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 }Example for Finding and Creating 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 a Salesforce user if it can’t find one. The handler doesn’t technically create the user—instead, it returns a User object. Salesforce then automatically inserts the new user into the User object for you. To create the User object, the isUserCreationAllowed field on your OauthTokenExchangeHandler metadata definition must be set to true. When you set this metadata field to true, the CanCreateUser parameter in the getUserForTokenSubject Apex method is also set to true.
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 implementation, 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}