22 * License, v. 2.0. If a copy of the MPL was not distributed with this
33 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
44
5- const ENTERPRISE_LOCKING_ENABLED_PREF = "enterprise.session.locking.enabled" ;
6-
75const lazy = { } ;
86
97ChromeUtils . defineESModuleGetters ( lazy , {
@@ -18,10 +16,6 @@ ChromeUtils.defineLazyGetter(lazy, "log", () => {
1816 return lazy . createEnterpriseLogger ( "FeltLocking" ) ;
1917} ) ;
2018
21- function lockingEnabled ( ) {
22- return Services . prefs . getBoolPref ( ENTERPRISE_LOCKING_ENABLED_PREF , false ) ;
23- }
24-
2519/**
2620 * The email of the currently signed-in user, used as the key under which a
2721 * locked session's refresh token is stored. Read from the cached value rather
@@ -42,19 +36,11 @@ function currentEmail() {
4236 * @returns {Promise<void> }
4337 */
4438async function storeToken ( email , token ) {
45- // Choke point: never persist a token while the feature is disabled.
46- if ( ! lockingEnabled ( ) ) {
47- return ;
48- }
4939 const encryptedRefreshToken = await lazy . OSKeyStore . encrypt ( token ) ;
5040 lazy . FeltStorage . setLockingToken ( email , encryptedRefreshToken ) ;
5141}
5242
5343export const FeltLocking = {
54- get enabled ( ) {
55- return lockingEnabled ( ) ;
56- } ,
57-
5844 /**
5945 * Attempt to resume a previously locked session for the given user. Requires
6046 * OS-level authentication and a stored, still-valid refresh token.
@@ -64,87 +50,81 @@ export const FeltLocking = {
6450 * @returns {Promise<boolean> } Whether the session was successfully unlocked.
6551 */
6652 tryUnlock : async ( email , browser ) => {
67- if ( lockingEnabled ( ) ) {
68- const token = lazy . FeltStorage . getLockingToken ( email ) ;
69- if ( token ) {
70- const { authenticated } = await lazy . OSKeyStore . ensureLoggedIn (
71- "Trying to unlock existing session" ,
72- "Firefox Enterprise"
73- ) ;
74- if ( authenticated ) {
75- let refreshToken ;
76- try {
77- refreshToken = await lazy . OSKeyStore . decrypt ( token , "" , false ) ;
78- } catch ( err ) {
53+ // A stored token exists only because a browser-authorized lock created it,
54+ // so its presence is the authorization to resume: the locking pref lives in
55+ // the browser process, which the Felt UI process cannot read.
56+ const token = lazy . FeltStorage . getLockingToken ( email ) ;
57+ if ( token ) {
58+ const { authenticated } = await lazy . OSKeyStore . ensureLoggedIn (
59+ "Trying to unlock existing session" ,
60+ "Firefox Enterprise"
61+ ) ;
62+ if ( authenticated ) {
63+ let refreshToken ;
64+ try {
65+ refreshToken = await lazy . OSKeyStore . decrypt ( token , "" , false ) ;
66+ } catch ( err ) {
67+ lazy . log . warn (
68+ `tryUnlock: decrypt failed, falling back to sign-in: ${ err } `
69+ ) ;
70+ }
71+ if ( ! refreshToken ) {
72+ Services . felt . setTokens ( "" , "" , 0 ) ;
73+ lazy . FeltStorage . clearLockingToken ( email ) ;
74+ return false ;
75+ }
76+ // Only the refresh token is available here; force a refresh below.
77+ Services . felt . setTokens ( "" , refreshToken , 0 ) ;
78+ try {
79+ const { access_token, refresh_token, expires_at } =
80+ await lazy . ConsoleClient . refreshTokens ( ) ;
81+ Services . felt . setTokens ( access_token , refresh_token , expires_at ) ;
82+
83+ await storeToken ( email , refresh_token ) ;
84+ } catch ( err ) {
85+ Services . felt . setTokens ( "" , "" , 0 ) ;
86+ if ( err ?. name === "ReauthRequiredError" ) {
87+ // The refresh token is genuinely invalid/revoked: drop it so we
88+ // fall back to a full SSO sign-in.
89+ lazy . FeltStorage . clearLockingToken ( email ) ;
90+ } else {
91+ // Transient failure (offline, server error, ...): keep the stored
92+ // token so the session can still be unlocked later.
7993 lazy . log . warn (
80- `tryUnlock: decrypt failed, falling back to sign-in : ${ err } `
94+ `tryUnlock: transient failure resuming from token, keeping it : ${ err } `
8195 ) ;
8296 }
83- if ( ! refreshToken ) {
84- Services . felt . setTokens ( "" , "" , 0 ) ;
85- lazy . FeltStorage . clearLockingToken ( email ) ;
86- return false ;
87- }
88- // Only the refresh token is available here; force a refresh below.
89- Services . felt . setTokens ( "" , refreshToken , 0 ) ;
90- try {
91- const { access_token, refresh_token, expires_at } =
92- await lazy . ConsoleClient . refreshTokens ( ) ;
93- Services . felt . setTokens ( access_token , refresh_token , expires_at ) ;
94-
95- await storeToken ( email , refresh_token ) ;
96- } catch ( err ) {
97- Services . felt . setTokens ( "" , "" , 0 ) ;
98- if ( err ?. name === "ReauthRequiredError" ) {
99- // The refresh token is genuinely invalid/revoked: drop it so we
100- // fall back to a full SSO sign-in.
101- lazy . FeltStorage . clearLockingToken ( email ) ;
102- } else {
103- // Transient failure (offline, server error, ...): keep the stored
104- // token so the session can still be unlocked later.
105- lazy . log . warn (
106- `tryUnlock: transient failure resuming from token, keeping it: ${ err } `
107- ) ;
108- }
109- return false ;
110- }
111-
112- // Tokens are committed; from here a failure is a launch failure, not
113- // a reason to fall back to SSO, so let it propagate to the caller.
114- const parentActor =
115- browser . browsingContext . currentWindowGlobal . domProcess . getActor (
116- "FeltProcess"
117- ) ;
118- await parentActor . receiveMessage ( {
119- name : "FeltChild:StartFirefox" ,
120- data : { } ,
121- } ) ;
122- return true ;
97+ return false ;
12398 }
99+
100+ // Tokens are committed; from here a failure is a launch failure, not
101+ // a reason to fall back to SSO, so let it propagate to the caller.
102+ const parentActor =
103+ browser . browsingContext . currentWindowGlobal . domProcess . getActor (
104+ "FeltProcess"
105+ ) ;
106+ await parentActor . receiveMessage ( {
107+ name : "FeltChild:StartFirefox" ,
108+ data : { } ,
109+ } ) ;
110+ return true ;
124111 }
125112 }
126113 return false ;
127114 } ,
128115
129116 /**
130- * Reconcile the persisted locked-session token with the current refresh token
131- * and the enabled pref: when locking is enabled, store the (encrypted) token
132- * so the session can later be unlocked; when it is disabled, drop any stale
133- * token so flipping the pref off can never leave a credential behind.
117+ * Persist the (encrypted) refresh token so the session can later be unlocked.
118+ * Only ever reached via an explicit, browser-authorized lock, so it does not
119+ * consult the locking pref (which the Felt UI process cannot read).
134120 *
135121 * @param {string } refresh_token
136- * @throws {Error } If locking is enabled but no signed-in user is known, so the
137- * caller can fall back to signing out instead of locking.
122+ * @throws {Error } If no signed-in user is known, so the caller can fall back
123+ * to signing out instead of locking.
138124 * @returns {Promise<void> }
139125 */
140126 store : async refresh_token => {
141127 const email = currentEmail ( ) ;
142- if ( ! lockingEnabled ( ) ) {
143- if ( email ) {
144- lazy . FeltStorage . clearLockingToken ( email ) ;
145- }
146- return ;
147- }
148128 if ( ! email ) {
149129 throw new Error (
150130 "store: no signed-in user known, cannot persist locked session"
@@ -153,6 +133,22 @@ export const FeltLocking = {
153133 await storeToken ( email , refresh_token ) ;
154134 } ,
155135
136+ /**
137+ * Keep an already-persisted token in sync with a rotated refresh token. Never
138+ * creates one: persistence is authorized only by an explicit lock, so a token
139+ * refresh must not turn a non-locking session into a lockable one.
140+ *
141+ * @param {string } refresh_token
142+ * @returns {Promise<void> }
143+ */
144+ updateStoredToken : async refresh_token => {
145+ const email = currentEmail ( ) ;
146+ if ( ! email || ! lazy . FeltStorage . getLockingToken ( email ) ) {
147+ return ;
148+ }
149+ await storeToken ( email , refresh_token ) ;
150+ } ,
151+
156152 /**
157153 * Remove any stored locked-session token for the current user. Always runs
158154 * (even when locking is disabled) so signing out can never leave a credential
0 commit comments