test_oauth_tokens.js (7232B)
1 /* Any copyright is dedicated to the Public Domain. 2 * http://creativecommons.org/publicdomain/zero/1.0/ */ 3 4 "use strict"; 5 6 const { FxAccounts } = ChromeUtils.importESModule( 7 "resource://gre/modules/FxAccounts.sys.mjs" 8 ); 9 const { FxAccountsClient } = ChromeUtils.importESModule( 10 "resource://gre/modules/FxAccountsClient.sys.mjs" 11 ); 12 var { AccountState } = ChromeUtils.importESModule( 13 "resource://gre/modules/FxAccounts.sys.mjs" 14 ); 15 16 function promiseNotification(topic) { 17 return new Promise(resolve => { 18 let observe = () => { 19 Services.obs.removeObserver(observe, topic); 20 resolve(); 21 }; 22 Services.obs.addObserver(observe, topic); 23 }); 24 } 25 26 // Just enough mocks so we can avoid hawk and storage. 27 function MockStorageManager() {} 28 29 MockStorageManager.prototype = { 30 promiseInitialized: Promise.resolve(), 31 32 initialize(accountData) { 33 this.accountData = accountData; 34 }, 35 36 finalize() { 37 return Promise.resolve(); 38 }, 39 40 getAccountData() { 41 return Promise.resolve(this.accountData); 42 }, 43 44 updateAccountData(updatedFields) { 45 for (let [name, value] of Object.entries(updatedFields)) { 46 if (value == null) { 47 delete this.accountData[name]; 48 } else { 49 this.accountData[name] = value; 50 } 51 } 52 return Promise.resolve(); 53 }, 54 55 deleteAccountData() { 56 this.accountData = null; 57 return Promise.resolve(); 58 }, 59 }; 60 61 function MockFxAccountsClient(activeTokens) { 62 this._email = "nobody@example.com"; 63 this._verified = false; 64 65 this.accountStatus = function (uid) { 66 return Promise.resolve(!!uid && !this._deletedOnServer); 67 }; 68 69 this.signOut = function () { 70 return Promise.resolve(); 71 }; 72 this.registerDevice = function () { 73 return Promise.resolve(); 74 }; 75 this.updateDevice = function () { 76 return Promise.resolve(); 77 }; 78 this.signOutAndDestroyDevice = function () { 79 return Promise.resolve(); 80 }; 81 this.getDeviceList = function () { 82 return Promise.resolve(); 83 }; 84 this.accessTokenWithSessionToken = function ( 85 sessionTokenHex, 86 clientId, 87 scope, 88 ttl 89 ) { 90 let token = `token${this.numTokenFetches}`; 91 if (ttl) { 92 token += `-ttl-${ttl}`; 93 } 94 this.numTokenFetches += 1; 95 this.activeTokens.add(token); 96 print("accessTokenWithSessionToken returning token", token); 97 return Promise.resolve({ access_token: token, ttl }); 98 }; 99 this.oauthDestroy = sinon.stub().callsFake((_clientId, token) => { 100 this.activeTokens.delete(token); 101 return Promise.resolve(); 102 }); 103 104 // Test only stuff. 105 this.activeTokens = activeTokens; 106 this.numTokenFetches = 0; 107 108 FxAccountsClient.apply(this); 109 } 110 111 MockFxAccountsClient.prototype = {}; 112 Object.setPrototypeOf( 113 MockFxAccountsClient.prototype, 114 FxAccountsClient.prototype 115 ); 116 117 function MockFxAccounts() { 118 // The FxA "auth" and "oauth" servers both share the same db of tokens, 119 // so we need to simulate the same here in the tests. 120 const activeTokens = new Set(); 121 return new FxAccounts({ 122 fxAccountsClient: new MockFxAccountsClient(activeTokens), 123 newAccountState(credentials) { 124 // we use a real accountState but mocked storage. 125 let storage = new MockStorageManager(); 126 storage.initialize(credentials); 127 return new AccountState(storage); 128 }, 129 _getDeviceName() { 130 return "mock device name"; 131 }, 132 fxaPushService: { 133 registerPushEndpoint() { 134 return new Promise(resolve => { 135 resolve({ 136 endpoint: "http://mochi.test:8888", 137 }); 138 }); 139 }, 140 }, 141 }); 142 } 143 144 async function createMockFxA() { 145 let fxa = new MockFxAccounts(); 146 let credentials = { 147 email: "foo@example.com", 148 uid: "1234@lcip.org", 149 sessionToken: "dead", 150 scopedKeys: { 151 [SCOPE_OLD_SYNC]: { 152 kid: "key id for sync key", 153 k: "key material for sync key", 154 kty: "oct", 155 }, 156 }, 157 verified: true, 158 }; 159 160 await fxa._internal.setSignedInUser(credentials); 161 return fxa; 162 } 163 164 // The tests. 165 166 add_task(async function testRevoke() { 167 let tokenOptions = { scope: "test-scope" }; 168 let fxa = await createMockFxA(); 169 let client = fxa._internal.fxAccountsClient; 170 171 // get our first token and check we hit the mock. 172 let token1 = await fxa.getOAuthToken(tokenOptions); 173 equal(client.numTokenFetches, 1); 174 equal(client.activeTokens.size, 1); 175 ok(token1, "got a token"); 176 equal(token1, "token0"); 177 178 // drop the new token from our cache. 179 await fxa.removeCachedOAuthToken({ token: token1 }); 180 ok(client.oauthDestroy.calledOnce); 181 182 // the revoke should have been successful. 183 equal(client.activeTokens.size, 0); 184 // fetching it again hits the server. 185 let token2 = await fxa.getOAuthToken(tokenOptions); 186 equal(client.numTokenFetches, 2); 187 equal(client.activeTokens.size, 1); 188 ok(token2, "got a token"); 189 notEqual(token1, token2, "got a different token"); 190 }); 191 192 add_task(async function testSignOutDestroysTokens() { 193 let fxa = await createMockFxA(); 194 let client = fxa._internal.fxAccountsClient; 195 196 // get our first token and check we hit the mock. 197 let token1 = await fxa.getOAuthToken({ scope: "test-scope" }); 198 equal(client.numTokenFetches, 1); 199 equal(client.activeTokens.size, 1); 200 ok(token1, "got a token"); 201 202 // get another 203 let token2 = await fxa.getOAuthToken({ scope: "test-scope-2" }); 204 equal(client.numTokenFetches, 2); 205 equal(client.activeTokens.size, 2); 206 ok(token2, "got a token"); 207 notEqual(token1, token2, "got a different token"); 208 209 // FxA fires an observer when the "background" signout is complete. 210 let signoutComplete = promiseNotification("testhelper-fxa-signout-complete"); 211 // now sign out - they should be removed. 212 await fxa.signOut(); 213 await signoutComplete; 214 ok(client.oauthDestroy.calledTwice); 215 // No active tokens left. 216 equal(client.activeTokens.size, 0); 217 }); 218 219 add_task(async function testTokenRaces() { 220 // Here we do 2 concurrent fetches each for 2 different token scopes (ie, 221 // 4 token fetches in total). 222 // This should provoke a potential race in the token fetching but we use 223 // a map of in-flight token fetches, so we should still only perform 2 224 // fetches, but each of the 4 calls should resolve with the correct values. 225 let fxa = await createMockFxA(); 226 let client = fxa._internal.fxAccountsClient; 227 228 let results = await Promise.all([ 229 fxa.getOAuthToken({ scope: "test-scope" }), 230 fxa.getOAuthToken({ scope: "test-scope" }), 231 fxa.getOAuthToken({ scope: "test-scope-2" }), 232 fxa.getOAuthToken({ scope: "test-scope-2" }), 233 ]); 234 235 equal(client.numTokenFetches, 2, "should have fetched 2 tokens."); 236 237 // Should have 2 unique tokens 238 results.sort(); 239 equal(results[0], results[1]); 240 equal(results[2], results[3]); 241 // should be 2 active. 242 equal(client.activeTokens.size, 2); 243 await fxa.removeCachedOAuthToken({ token: results[0] }); 244 equal(client.activeTokens.size, 1); 245 await fxa.removeCachedOAuthToken({ token: results[2] }); 246 equal(client.activeTokens.size, 0); 247 ok(client.oauthDestroy.calledTwice); 248 }); 249 250 add_task(async function testTokenTTL() { 251 // This tests the TTL option passed into the method 252 let fxa = await createMockFxA(); 253 let token = await fxa.getOAuthToken({ scope: "test-ttl", ttl: 1000 }); 254 equal(token, "token0-ttl-1000"); 255 });