diff --git a/src/backend/controllers/auth/AuthController.test.ts b/src/backend/controllers/auth/AuthController.test.ts index 7d468b6af..f59e47c0d 100644 --- a/src/backend/controllers/auth/AuthController.test.ts +++ b/src/backend/controllers/auth/AuthController.test.ts @@ -2970,6 +2970,35 @@ describe('AuthController.handleGetUserAppToken + handleCheckApp', () => { expect(typeof body.token).toBe('string'); }); + // What decides whether a cancelled "Sign in with Puter" still hands over a token. + it('check-app reports an app the user never opened as unauthorized, with no token', async () => { + const untouched = await ( + server.stores.app.create as unknown as ( + fields: Record, + opts: { ownerUserId: number }, + ) => Promise<{ uid: string; id: number }> + )( + { + name: `ca-${uuidv4()}`, + title: 'Never opened', + index_url: 'https://never-opened.example.test/index.html', + }, + { ownerUserId: user.id }, + ); + + const res = makeRes(); + await inCtx(actor, () => + controller.handleCheckApp( + makeReq({ app_uid: untouched.uid }, { actor }), + res, + ), + ); + expect(res.body).toEqual({ + app_uid: untouched.uid, + authenticated: false, + }); + }); + it('check-app returns the {app_uid, authenticated} envelope shape', async () => { // Create a brand-new actor with no app-related history so the // permission scan can't cache-hit anything from prior tests, AND @@ -3032,16 +3061,7 @@ describe('AuthController.handleGetUserAppToken + handleCheckApp', () => { authenticated: boolean; token?: string; }; - expect(body.app_uid).toBe(otherApp.uid); - expect(typeof body.authenticated).toBe('boolean'); - // Whether `authenticated` is true depends on the user's full - // permission set (default group, owned-app implicits, etc.) — this - // test only pins the response *shape*, since the substantive case - // (`authenticated: true` after a paired get-user-app-token) is - // covered by the test above. - if (!body.authenticated) { - expect(body.token).toBeUndefined(); - } + expect(body).toEqual({ app_uid: otherApp.uid, authenticated: false }); }); it('falls back to origin → app_uid resolution and bootstraps a new app row', async () => { diff --git a/src/backend/controllers/auth/AuthController.ts b/src/backend/controllers/auth/AuthController.ts index 2acdc7ff8..26e53d739 100644 --- a/src/backend/controllers/auth/AuthController.ts +++ b/src/backend/controllers/auth/AuthController.ts @@ -109,6 +109,8 @@ const FINGERPRINT_MAX_LENGTH = 128; // crafted request from turning a single grant call into a bulk write. const MAX_PERMISSIONS_PER_REQUEST = 16; const DISPATCH_ID_MAX_LENGTH = 128; +// One name for the flag, so the write and the read cannot drift apart. +const APP_AUTHENTICATED_FLAG = 'flag:app-is-authenticated'; // -- Post-login route limits ----------------------------------------- // @@ -3871,7 +3873,7 @@ export class AuthController extends PuterController { this.services.permission.grantUserAppPermission( req.actor!, app_uid, - 'flag:app-is-authenticated', + APP_AUTHENTICATED_FLAG, {}, {}, ); @@ -3955,13 +3957,15 @@ export class AuthController extends PuterController { legacyCode: 'bad_request', }); - // Check if the app is authenticated for this user - const authenticated = await this.services.permission - .check( - req.actor!, - `service:${app_uid}:ii:flag:app-is-authenticated`, - ) - .catch(() => false); + // The exact row, not a scan: a `service:`-shaped check falls open on the `service` root. + const app = await this.stores.app.resolveApp(app_uid); + const userId = req.actor!.user?.id; + const authenticated = + !!app?.id && + !!userId && + (await this.stores.permission + .hasUserAppPerm(userId, app.id, APP_AUTHENTICATED_FLAG) + .catch(() => false)); const result: { app_uid: string; @@ -3969,9 +3973,10 @@ export class AuthController extends PuterController { token?: string; } = { app_uid, authenticated }; if (authenticated) { + // The resolved uid: the token carries it as the app's identity. result.token = await this.services.auth.getUserAppToken( req.actor!, - app_uid, + app!.uid, ); } res.json(result); diff --git a/src/gui/src/helpers.js b/src/gui/src/helpers.js index 43aa9d4b2..e7e1d5aaa 100644 --- a/src/gui/src/helpers.js +++ b/src/gui/src/helpers.js @@ -2573,7 +2573,7 @@ window.getUserAppToken = async function (origin) { window.checkUserSiteRelationship = async function (origin) { try { - const response = await fetch(`${window.api_origin }/auth/check-app `, { + const response = await fetch(`${window.api_origin }/auth/check-app`, { headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${ window.auth_token}`, diff --git a/src/gui/src/initgui.js b/src/gui/src/initgui.js index 5a45cb21a..6e16c1f64 100644 --- a/src/gui/src/initgui.js +++ b/src/gui/src/initgui.js @@ -1679,7 +1679,7 @@ window.initgui = async function (options) { let response = await window.checkUserSiteRelationship( window.openerOrigin, ); - window.userAppToken = response.token; + window.userAppToken = response?.token; if ( !picked_a_user_for_sdk_login &&