Summary
In security.provider=AtlasGoogleSecurity (Google Cloud IAP) mode, Atlas force-logs the user out within minutes of every login. The browser console shows:
>>> Checking user token....
>>> Token close to expiring. Refreshing user token....
GET https://<host>/WebAPI/user/refresh 403 (Forbidden) // or 404
Uncaught (in promise) {ok: false, status: 403|404, ...}
…followed by the UI rendering "Application initialization failed" with the Sign In button reappearing.
The underlying cause is that WebAPI/user/refresh is not implemented in AtlasGoogleSecurity — IAP mode never mints a WebAPI JWT, so there is nothing to refresh. Atlas, however, polls that endpoint anyway. Each failure triggers resetAuthParams(), which clears subject/permissions and signs the user out.
This ticket is for the Atlas-side fix.
Why Atlas calls a refresh it doesn't need
Two pieces of existing Atlas behavior, taken together, mean the call is both unnecessary and guaranteed to fire:
-
Atlas already has IAP-aware session keep-alive. In js/services/AuthAPI.js, when loadUserInfo() sees the response header x-auth-provider: AtlasGoogleSecurity, an authProvider.subscribe(...) handler injects a hidden iframe pointing at /_gcp_iap/session_refresher. That iframe keeps Google's IAP session alive without any help from WebAPI.
-
The token-expiry check fires constantly in IAP mode. WebAPI never sets localStorage.bearerToken in IAP mode, so authApi.token() is null and tokenExpirationDate() returns null. The check in js/Application.js then evaluates to:
timeToExpire() = authApi.tokenExpirationDate() - new Date()
≈ 0 - currentEpochMillis
≈ -1.78e12
…which is always less than refreshTokenThreshold (default 4h = 1.44e7), so:
if (authApi.isAuthenticated() && this.timeToExpire() < config.refreshTokenThreshold) {
authApi.refreshToken();
}
…always fires. isAuthenticated() is true because loadUserInfo() set subject(info.login).
So Atlas calls WebAPI/user/refresh on essentially every interaction tick, the call fails (because IAP mode doesn't implement it), and Atlas signs the user out.
Proposed change
Extend the existing early-return in refreshToken in js/services/AuthAPI.js to also cover IAP:
var refreshToken = function() {
- if (!config.userAuthenticationEnabled) {
- return Promise.resolve(true); // no-op if userAuthenticationEnabled == false
+ if (!config.userAuthenticationEnabled || authProvider() === AUTH_PROVIDERS.IAP) {
+ // No-op when auth is disabled, or when WebAPI is fronted by Google IAP.
+ // The IAP session is kept alive by the hidden /_gcp_iap/session_refresher
+ // iframe (see authProvider.subscribe above); WebAPI's AtlasGoogleSecurity
+ // does not expose /user/refresh.
+ return Promise.resolve(true);
}
if (!isPromisePending(refreshTokenPromise)) {
refreshTokenPromise = httpService.doGet(getServiceUrl() + "user/refresh");
refreshTokenPromise.then(({ data, headers }) => {
setAuthParams(headers.get(TOKEN_HEADER), data.permissions);
});
refreshTokenPromise.catch(() => {
resetAuthParams();
});
}
return refreshTokenPromise;
}
Both symbols already exist at module scope in this file:
AUTH_PROVIDERS is defined near the top: const AUTH_PROVIDERS = { IAP: 'AtlasGoogleSecurity' };
authProvider is the ko.observable populated from the x-auth-provider response header in loadUserInfo().
No other files need to change.
Affected versions
Reproduced on v2.15.x. Likely affects every version that contains AUTH_PROVIDERS.IAP and the authProvider observable (i.e. since IAP support was first added).
Reproduction
- Run Atlas against a WebAPI instance configured with
security.provider=AtlasGoogleSecurity and Google IAP in front of WebAPI.
- Sign in via IAP;
/user/me succeeds and the home page loads.
- Move the mouse around / interact with the page until the periodic check at
js/Application.js:154 fires (≥ 30 user-interaction events).
- Observe
GET WebAPI/user/refresh returning 403 or 404 and the UI reverting to the Sign In state with "no sources defined."
- Apply the patch above; verify the call is no longer made and the session remains stable indefinitely.
Tests
If a JS test for AuthAPI exists, add a case asserting that refreshToken() resolves immediately and makes no HTTP call when authProvider() is AUTH_PROVIDERS.IAP. Otherwise, manual verification per the reproduction steps above.
Notes
- The matching WebAPI gap (no filter chain or filters wired for
/user/refresh in AtlasGoogleSecurity) should be tracked and fixed separately so that future changes don't reintroduce a dependency on this endpoint from IAP-mode Atlas.
- Workarounds people may already be using and that this PR makes unnecessary: seeding a
user:refresh:get permission in the WebAPI security schema, or setting ATLAS_REFRESH_TOKEN_THRESHOLD to a large negative number to suppress the call.
Summary
In
security.provider=AtlasGoogleSecurity(Google Cloud IAP) mode, Atlas force-logs the user out within minutes of every login. The browser console shows:…followed by the UI rendering "Application initialization failed" with the Sign In button reappearing.
The underlying cause is that
WebAPI/user/refreshis not implemented inAtlasGoogleSecurity— IAP mode never mints a WebAPI JWT, so there is nothing to refresh. Atlas, however, polls that endpoint anyway. Each failure triggersresetAuthParams(), which clearssubject/permissionsand signs the user out.This ticket is for the Atlas-side fix.
Why Atlas calls a refresh it doesn't need
Two pieces of existing Atlas behavior, taken together, mean the call is both unnecessary and guaranteed to fire:
Atlas already has IAP-aware session keep-alive. In
js/services/AuthAPI.js, whenloadUserInfo()sees the response headerx-auth-provider: AtlasGoogleSecurity, anauthProvider.subscribe(...)handler injects a hidden iframe pointing at/_gcp_iap/session_refresher. That iframe keeps Google's IAP session alive without any help from WebAPI.The token-expiry check fires constantly in IAP mode. WebAPI never sets
localStorage.bearerTokenin IAP mode, soauthApi.token()is null andtokenExpirationDate()returnsnull. The check injs/Application.jsthen evaluates to:…which is always less than
refreshTokenThreshold(default 4h =1.44e7), so:…always fires.
isAuthenticated()is true becauseloadUserInfo()setsubject(info.login).So Atlas calls
WebAPI/user/refreshon essentially every interaction tick, the call fails (because IAP mode doesn't implement it), and Atlas signs the user out.Proposed change
Extend the existing early-return in
refreshTokeninjs/services/AuthAPI.jsto also cover IAP:var refreshToken = function() { - if (!config.userAuthenticationEnabled) { - return Promise.resolve(true); // no-op if userAuthenticationEnabled == false + if (!config.userAuthenticationEnabled || authProvider() === AUTH_PROVIDERS.IAP) { + // No-op when auth is disabled, or when WebAPI is fronted by Google IAP. + // The IAP session is kept alive by the hidden /_gcp_iap/session_refresher + // iframe (see authProvider.subscribe above); WebAPI's AtlasGoogleSecurity + // does not expose /user/refresh. + return Promise.resolve(true); } if (!isPromisePending(refreshTokenPromise)) { refreshTokenPromise = httpService.doGet(getServiceUrl() + "user/refresh"); refreshTokenPromise.then(({ data, headers }) => { setAuthParams(headers.get(TOKEN_HEADER), data.permissions); }); refreshTokenPromise.catch(() => { resetAuthParams(); }); } return refreshTokenPromise; }Both symbols already exist at module scope in this file:
AUTH_PROVIDERSis defined near the top:const AUTH_PROVIDERS = { IAP: 'AtlasGoogleSecurity' };authProvideris theko.observablepopulated from thex-auth-providerresponse header inloadUserInfo().No other files need to change.
Affected versions
Reproduced on
v2.15.x. Likely affects every version that containsAUTH_PROVIDERS.IAPand theauthProviderobservable (i.e. since IAP support was first added).Reproduction
security.provider=AtlasGoogleSecurityand Google IAP in front of WebAPI./user/mesucceeds and the home page loads.js/Application.js:154fires (≥ 30 user-interaction events).GET WebAPI/user/refreshreturning 403 or 404 and the UI reverting to the Sign In state with "no sources defined."Tests
If a JS test for
AuthAPIexists, add a case asserting thatrefreshToken()resolves immediately and makes no HTTP call whenauthProvider()isAUTH_PROVIDERS.IAP. Otherwise, manual verification per the reproduction steps above.Notes
/user/refreshinAtlasGoogleSecurity) should be tracked and fixed separately so that future changes don't reintroduce a dependency on this endpoint from IAP-mode Atlas.user:refresh:getpermission in the WebAPI security schema, or settingATLAS_REFRESH_TOKEN_THRESHOLDto a large negative number to suppress the call.