fedcm-helper.sub.js (11887B)
1 export const manifest_origin = "https://{{host}}:{{ports[https][0]}}"; 2 export const alt_manifest_origin = 'https://{{hosts[alt][]}}:{{ports[https][0]}}'; 3 export const same_site_manifest_origin = 'https://{{hosts[][www1]}}:{{ports[https][0]}}'; 4 export const default_manifest_path = '/fedcm/support/manifest.py'; 5 6 export function open_and_wait_for_popup(origin, path) { 7 return new Promise(resolve => { 8 let popup_window = window.open(origin + path); 9 10 // We rely on the popup page to send us a message when done. 11 const popup_message_handler = (event) => { 12 // We use new URL() to ensure the two origins are normalized the same 13 // way (especially so that default ports are handled identically). 14 if (new URL(event.origin).toString() == new URL(origin).toString()) { 15 popup_window.close(); 16 window.removeEventListener('message', popup_message_handler); 17 resolve(); 18 } 19 }; 20 21 window.addEventListener('message', popup_message_handler); 22 }); 23 } 24 25 // Set the identity provider cookie. 26 export function set_fedcm_cookie(host) { 27 if (host == undefined) { 28 document.cookie = 'cookie=1; SameSite=None; Path=/fedcm/support; Secure'; 29 return Promise.resolve(); 30 } else { 31 return open_and_wait_for_popup(host, '/fedcm/support/set_cookie'); 32 } 33 } 34 35 // Set the alternate identity provider cookie. 36 export function set_alt_fedcm_cookie() { 37 return set_fedcm_cookie(alt_manifest_origin); 38 } 39 40 export function setup_accounts_push(origin = manifest_origin) { 41 return open_and_wait_for_popup(origin, '/fedcm/support/push_accounts'); 42 } 43 44 export function mark_signed_in(origin = manifest_origin) { 45 return open_and_wait_for_popup(origin, '/fedcm/support/mark_signedin'); 46 } 47 48 export function mark_signed_out(origin = manifest_origin) { 49 return open_and_wait_for_popup(origin, '/fedcm/support/mark_signedout'); 50 } 51 52 // Returns FedCM CredentialRequestOptions for which navigator.credentials.get() 53 // succeeds. 54 export function request_options_with_mediation_required(manifest_filename, origin = manifest_origin) { 55 if (manifest_filename === undefined) { 56 manifest_filename = "manifest.py"; 57 } 58 const manifest_path = `${origin}/\ 59 fedcm/support/${manifest_filename}`; 60 return { 61 identity: { 62 providers: [{ 63 configURL: manifest_path, 64 clientId: '1', 65 // TODO(crbug.com/441895082): Move nonce to params when FedCmNonceInParams is enabled by default 66 nonce: '2' 67 }] 68 }, 69 mediation: 'required' 70 }; 71 } 72 73 // Returns alternate FedCM CredentialRequestOptions for which navigator.credentials.get() 74 // succeeds. 75 export function alt_request_options_with_mediation_required(manifest_filename) { 76 return request_options_with_mediation_required(manifest_filename, alt_manifest_origin); 77 } 78 79 // Returns FedCM CredentialRequestOptions with auto re-authentication. 80 // succeeds. 81 export function request_options_with_mediation_optional(manifest_filename) { 82 let options = alt_request_options_with_mediation_required(manifest_filename); 83 // Approved client 84 options.identity.providers[0].clientId = '123'; 85 options.mediation = 'optional'; 86 87 return options; 88 } 89 90 export function request_options_with_context(manifest_filename, context) { 91 if (manifest_filename === undefined) { 92 manifest_filename = "manifest.py"; 93 } 94 const manifest_path = `${manifest_origin}/\ 95 fedcm/support/${manifest_filename}`; 96 return { 97 identity: { 98 providers: [{ 99 configURL: manifest_path, 100 clientId: '1', 101 nonce: '2' 102 }], 103 context: context 104 }, 105 mediation: 'required' 106 }; 107 } 108 109 export function request_options_with_two_idps(mediation = 'required') { 110 const first_config = `${manifest_origin}${default_manifest_path}`; 111 const second_config = `${alt_manifest_origin}${default_manifest_path}`; 112 return { 113 identity: { 114 providers: [{ 115 configURL: first_config, 116 clientId: '123', 117 nonce: 'N1' 118 }, 119 { 120 configURL: second_config, 121 clientId: '456', 122 nonce: 'N2' 123 }], 124 }, 125 mediation: mediation 126 }; 127 } 128 129 // Test wrapper which does FedCM-specific setup. 130 export function fedcm_test(test_func, test_name) { 131 promise_test(async t => { 132 assert_implements(window.IdentityCredential, "FedCM is not supported"); 133 134 try { 135 await navigator.credentials.preventSilentAccess(); 136 } catch (ex) { 137 // In Chrome's content_shell, the promise will be rejected 138 // even though the part we care about succeeds. 139 } 140 141 // Turn off delays that are not useful in tests. 142 try { 143 await test_driver.set_fedcm_delay_enabled(false); 144 } catch (e) { 145 // Failure is not critical; it just might slow down tests. 146 } 147 148 // Reset cooldown in case a previous test triggered it. 149 try { 150 await test_driver.reset_fedcm_cooldown(); 151 } catch (e) { 152 // Failure is not critical. 153 } 154 155 t.add_cleanup(async () => { 156 // A fedcm_test may affect the connected account set from the IDPs, so invoke 157 // disconnect as a cleanup. 158 try { 159 await IdentityCredential.disconnect(disconnect_options("")); 160 } catch (ex) { 161 // Failure is not critical, test state is reset. 162 } 163 try { 164 await IdentityCredential.disconnect(alt_disconnect_options("")); 165 } catch (ex) { 166 // Failure is not critical, test state is reset. 167 } 168 }); 169 170 await mark_signed_in(); 171 await mark_signed_in(alt_manifest_origin); 172 await set_fedcm_cookie(); 173 await set_alt_fedcm_cookie(); 174 await test_func(t); 175 }, test_name); 176 } 177 178 function select_manifest_impl(manifest_url) { 179 const url_query = (manifest_url === undefined) 180 ? '' : `?manifest_url=${manifest_url}`; 181 182 return new Promise(resolve => { 183 const img = document.createElement('img'); 184 img.src = `/fedcm/support/select_manifest_in_root_manifest.py${url_query}`; 185 img.addEventListener('error', resolve); 186 document.body.appendChild(img); 187 }); 188 } 189 190 // Sets the manifest returned by the next fetch of /.well-known/web_identity 191 // select_manifest() only affects the next fetch and not any subsequent fetches 192 // (ex second next fetch). 193 export function select_manifest(test, test_options) { 194 // Add cleanup in case that /.well-known/web_identity is not fetched at all. 195 test.add_cleanup(async () => { 196 await select_manifest_impl(); 197 }); 198 const manifest_url = test_options.identity.providers[0].configURL; 199 return select_manifest_impl(manifest_url); 200 } 201 202 export function request_options_with_login_hint(manifest_filename, login_hint) { 203 let options = request_options_with_mediation_required(manifest_filename); 204 options.identity.providers[0].loginHint = login_hint; 205 206 return options; 207 } 208 209 export function request_options_with_domain_hint(manifest_filename, domain_hint) { 210 let options = request_options_with_mediation_required(manifest_filename); 211 options.identity.providers[0].domainHint = domain_hint; 212 213 return options; 214 } 215 216 export function fedcm_get_dialog_type_promise(t) { 217 return new Promise((resolve, reject) => { 218 async function helper() { 219 // Try to get the dialog type. If the UI is not up yet, we'll catch a 'no such alert' 220 // exception and try again in 10ms. Other exceptions will be rejected. 221 try { 222 const type = await window.test_driver.get_fedcm_dialog_type(); 223 resolve(type); 224 } catch (ex) { 225 if (String(ex).includes("no such alert")) { 226 if (t) { 227 t.step_timeout(helper, 10); 228 } else{ 229 window.setTimeout(helper, 10); 230 } 231 } else { 232 reject(ex); 233 } 234 } 235 } 236 237 helper(); 238 }); 239 } 240 241 242 export async function fedcm_settles_without_dialog(t, cred_promise) { 243 let dialog_promise = fedcm_get_dialog_type_promise(t); 244 let result = await Promise.race([cred_promise, dialog_promise]); 245 // Intentionally pass through any exceptions. 246 if (result instanceof IdentityCredential) { 247 return result; 248 } 249 throw "expected request to finish, got dialog: " + result; 250 } 251 252 export async function fedcm_expect_dialog(cred_promise, other_promise) { 253 let result = await Promise.race([cred_promise, other_promise]); 254 // If we got an exception, just pass it through, the caller will ensure it is 255 // the right type. 256 if (result instanceof IdentityCredential) { 257 throw "did not expect FedCM request to finish, got token: " + result.token; 258 } 259 return result; 260 } 261 262 export function fedcm_get_title_promise(t) { 263 return new Promise(resolve => { 264 async function helper() { 265 // Try to get the title. If the UI is not up yet, we'll catch an exception 266 // and try again in 10ms. 267 try { 268 const title = await window.test_driver.get_fedcm_dialog_title(); 269 resolve(title); 270 } catch (ex) { 271 t.step_timeout(helper, 10); 272 } 273 } 274 helper(); 275 }); 276 } 277 278 export async function fedcm_select_account_promise(t, account_index) { 279 let type = await fedcm_get_dialog_type_promise(t); 280 if (type != "AccountChooser") 281 throw "Incorrect dialog type: " + type; 282 await window.test_driver.select_fedcm_account(account_index); 283 } 284 285 export async function fedcm_get_and_select_first_account(t, options) { 286 const credentialPromise = navigator.credentials.get(options); 287 let type = await fedcm_expect_dialog( 288 credentialPromise, 289 fedcm_get_dialog_type_promise(t) 290 ); 291 if (type != "AccountChooser") 292 throw "Incorrect dialog type: " + type; 293 await window.test_driver.select_fedcm_account(0); 294 return credentialPromise; 295 } 296 297 export function fedcm_error_dialog_dismiss(t) { 298 return new Promise(resolve => { 299 async function helper() { 300 // Try to select the account. If the UI is not up yet, we'll catch an exception 301 // and try again in 10ms. 302 try { 303 let type = await fedcm_get_dialog_type_promise(t); 304 assert_equals(type, "Error"); 305 await window.test_driver.cancel_fedcm_dialog(); 306 resolve(); 307 } catch (ex) { 308 t.step_timeout(helper, 10); 309 } 310 } 311 helper(); 312 }); 313 } 314 315 export function fedcm_error_dialog_click_button(t, button) { 316 return new Promise(resolve => { 317 async function helper() { 318 // Try to select the account. If the UI is not up yet, we'll catch an exception 319 // and try again in 10ms. 320 try { 321 let type = await fedcm_get_dialog_type_promise(t); 322 assert_equals(type, "Error"); 323 await window.test_driver.click_fedcm_dialog_button(button); 324 resolve(); 325 } catch (ex) { 326 t.step_timeout(helper, 10); 327 } 328 } 329 helper(); 330 }); 331 } 332 333 export function disconnect_options(accountHint, manifest_filename) { 334 if (manifest_filename === undefined) { 335 manifest_filename = "manifest.py"; 336 } 337 const manifest_path = `${manifest_origin}/\ 338 fedcm/support/${manifest_filename}`; 339 return { 340 configURL: manifest_path, 341 clientId: '1', 342 accountHint: accountHint 343 }; 344 } 345 346 export function alt_disconnect_options(accountHint, manifest_filename) { 347 if (manifest_filename === undefined) { 348 manifest_filename = "manifest.py"; 349 } 350 const manifest_path = `${alt_manifest_origin}/\ 351 fedcm/support/${manifest_filename}`; 352 return { 353 configURL: manifest_path, 354 clientId: '1', 355 accountHint: accountHint 356 }; 357 } 358 359 export async function fedcm_get_flexible_tokens_credential(t, type) { 360 const options = request_options_with_mediation_required(`manifest_flexible_tokens.json`); 361 options.identity.providers[0].params = { 362 "token_type": type 363 }; 364 await select_manifest(t, options); 365 return await fedcm_get_and_select_first_account(t, options); 366 } 367 368 export function set_well_known_format(format_type) { 369 const url_query = `?format=${encodeURIComponent(format_type)}`; 370 371 return new Promise(resolve => { 372 const img = document.createElement('img'); 373 img.addEventListener('error', resolve); 374 img.src = `/fedcm/support/set-well-known-format.py${url_query}`; 375 document.body.appendChild(img); 376 }); 377 }