1public class QRCodeLoginController {
2
3 // The generated one-time-use hyperlink for the QR code payload.
4 public String qrCodeOneTimeLoginHyperlink {get;set;}
5
6 // The self-signed certificate name.
7 private String selfSignedCertName = 'JWT_Bearer';
8
9 // The connected app client ID.
10 // (From Setup, Manage Apps, Connected Apps, and Manage Consumer Details.)
11 private String jwtClientId = 'CONNECTED_APP_CLIENT_ID';
12
13 // The Salesforce instance URL.
14 private String instanceUrl = 'INSTANCE_URL';
15
16 // The Salesforce Identity API OAuth 2.0 token endpoint URL.
17 private String oauth2TokenEndpoint = instanceUrl + '/services/oauth2/token';
18
19 // The Salesforce Identity API OAuth 2.0 UI bridge single access endpoint URL.
20 private String oauth2SingleAccessEndpoint = instanceUrl +
21 '/services/oauth2/singleaccess';
22
23 // The mobile app’s client ID.
24 // (From Setup, Manage Apps, Connected Apps, and Manage Consumer Details.)
25 private String mobileClientId = 'MOBILE_CLIENT_ID';
26
27 // The connected app’s callback URL.
28 // (From Setup, Manage Apps, Connected Apps, and Manage Consumer Details.)
29 private String callbackURL = 'CALLBACK_URL';
30
31 // The mobile app’s deep link URL.
32 // (Includes the custom scheme, host, and path
33 // that the app is configured to be linked from)
34 private String mobileDeepLinkUrl = 'MOBILE_DEEP_LINK_URL';
35
36 /**
37 * Generate the QR code’s one-time-login hyperlink. The operation is
38 * asynchronous and the generated hyperlink is stored in
39 * `qrCodeOneTimeLoginHyperlink`.
40 */
41 public PageReference generateQrCodeOneTimeLoginHyperlink() {
42
43 // Generate PKCE code verifier and code challenge.
44 String codeVerifier = generateCodeVerifier();
45 String codeChallenge = generateCodeChallenge(codeVerifier);
46
47 // Generate the client start URL.
48 String clientStartUrl = '/services/oauth2/authorize' +
49 '?response_type=code&client_id=' + encode(mobileClientId) +
50 '&redirect_uri=' + encode(callbackURL) + '&code_challenge=' +
51 encode(codeChallenge);
52
53 // Generate the UI Bridge API Front Door URL.
54 String uiBridgeFrontDoorUrl = generateUiBridgeFrontDoorUrl(clientStartUrl);
55
56 // Assemble the log in QR code’s JSON payload.
57 Map<String, String> jsonPayload = new Map<String, String>();
58 jsonPayload.put('frontdoor_bridge_url', uiBridgeFrontDoorUrl);
59 jsonPayload.put('pkce_code_verifier', codeVerifier);
60 String jsonPayloadString = JSON.serialize(jsonPayload);
61 String jsonPayloadUrlEncoded = EncodingUtil.urlEncode(
62 jsonPayloadString, 'UTF-8');
63
64
65 qrCodeOneTimeLoginHyperlink = mobileDeepLinkUrl + '?bridgeJson=' +
66 jsonPayloadUrlEncoded;
67 return null;
68 }
69
70 /*
71 * Generates the UI Bridge API Front Door URL by using the provided client
72 * start URL.
73 */
74 private String generateUiBridgeFrontDoorUrl(String startUrl) {
75 String accessToken = getAccessToken();
76
77
78 Http http = new Http();
79 HttpRequest request = new HttpRequest();
80 request.setMethod('POST');
81
82
83 String url = oauth2SingleAccessEndpoint;
84 request.setEndpoint(url);
85
86
87 String body = 'redirect_uri=' + encode(startUrl);
88 request.setBody(body);
89
90
91 request.setHeader('Content-Type','application/x-www-form-urlencoded');
92 request.setHeader('Authorization','Bearer ' + accessToken);
93
94
95 HttpResponse response = http.send(request);
96 SingleAccessResponse singleAccessResponse =
97 (SingleAccessResponse)JSON.deserialize(response.getBody(),
98 SingleAccessResponse.class);
99
100
101 return singleAccessResponse.frontdoor_uri;
102 }
103
104
105 /*
106 * Gets the access token.
107 */
108 private String getAccessToken() {
109 Auth.JWT jwt = new Auth.JWT();
110 jwt.setSub(UserInfo.getUserName());
111 jwt.setAud('https://login.test1.pc-rnd.salesforce.com');
112 jwt.setIss(jwtClientId);
113
114
115 // Additional claims to set scope
116 Map<String, Object> claims = new Map<String, Object>();
117
118
119 // Create the object that signs the JWT bearer token with a hardcoded
120 // certificate developer name for POC.
121 Auth.JWS jws = new Auth.JWS(jwt, selfSignedCertName);
122
123
124 // POST the JWT bearer token.
125 Auth.JWTBearerTokenExchange bearer =
126 new Auth.JWTBearerTokenExchange(oauth2TokenEndpoint, jws);
127 String accessToken = bearer.getAccessToken();
128
129
130 return accessToken;
131 }
132
133
134 /*
135 * Generates a PKCE code verifier.
136 */
137 private String generateCodeVerifier() {
138 // Code verifier set up.
139 String codeVerifierCharacterSet =
140 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~';
141 Integer codeVerifierLength = 128;
142
143 // Generate code verifier string.
144 String codeVerifier = '';
145 for (Integer i = 0; i < codeVerifierLength; i++) {
146 Integer index = Math.mod(Math.abs(Crypto.getRandomInteger()),
147 codeVerifierCharacterSet.length());
148 codeVerifier += codeVerifierCharacterSet.substring(index, index + 1);
149 }
150
151 // Encode code verifier string to Base64 spec.
152 codeVerifier = EncodingUtil.base64Encode(Blob.valueOf(codeVerifier));
153
154 // Encode code verifier Base64 to Base64 URL-safe spec.
155 codeVerifier = codeVerifier
156 .replace('+', '-').replace('/', '_').replace('=', '');
157 return codeVerifier;
158 }
159
160 /*
161 * Generates a PKCE code challenge from the provided code verifier.
162 */
163 private String generateCodeChallenge(String codeVerifier) {
164 // Generate code challenge string from code verifier.
165 Blob codeVerifierBlob = Blob.valueOf(codeVerifier);
166 Blob codeChallenge256Blob = Crypto.generateDigest('SHA-256', codeVerifierBlob);
167
168 // Encode code challenge string to Base64 spec.
169 String codeChallengeBase64Encoded =
170 EncodingUtil.base64Encode(codeChallenge256Blob);
171
172 // Encode code challenge Base64 to Base64 URL-safe spec.
173 String codeChallengeBase64UrlSafeEncoded =
174 codeChallengeBase64Encoded.replace('+', '-').replace('/', '_').replace('=', '');
175
176 return codeChallengeBase64UrlSafeEncoded;
177 }
178
179 /*
180 * URL encodes a string.
181 */
182 private String encode(String value) {
183 return EncodingUtil.urlEncode(value, 'UTF-8');
184 }
185
186 /*
187 * Encodes a given Base64 string to the Base64 URL-safe spec.
188 */
189 private String base64ToBase64UrlSafe(String base64Value) {
190 return base64Value.replace('+', '-').replace('/', '_').replace('=', '');
191 }
192
193 /*
194 * A class to model responses from the
195 * Salesforce Identity OAuth 2.0 UI bridge single access endpoint.
196 */
197 private class SingleAccessResponse {
198 public string frontdoor_uri;
199 }
200}