mirror of
https://github.com/HeyPuter/puter.git
synced 2026-10-03 02:18:06 +00:00
fix: check-app reported every app as already authorized
The token grant writes `flag:app-is-authenticated` on the user->app row, but the check asked for `service:<app_uid>:ii:flag:app-is-authenticated`. Those never match, and the parent walk reduces the question to a bare `service` — a root every user holds by default — so the check answered true for any app_uid and handed back an app-under-user token for an app the user had never opened. Read the row the grant writes, through one shared constant so the two cannot drift again, and mint against the resolved uid. A cancelled account picker no longer leaves `popup_signin_consent` set: with no relationship there is no token to flip it.
This commit is contained in:
@@ -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<string, unknown>,
|
||||
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 () => {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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}`,
|
||||
|
||||
@@ -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 &&
|
||||
|
||||
Reference in New Issue
Block a user