fix: puter site middleware host (#2594)

This commit is contained in:
Daniel Salazar
2026-03-03 22:18:47 -08:00
committed by GitHub
parent 8380b28d8a
commit 6b6f9b4743
4 changed files with 55 additions and 2 deletions
@@ -127,6 +127,13 @@ function getSubdomainFromHostedRequest (req) {
return host.split('.')[0] || '';
}
function getRequestedPrivateHost (req) {
const normalizedRequestHost = normalizeConfiguredHostname(req.hostname);
if ( ! normalizedRequestHost ) return undefined;
if ( ! hostMatchesPrivateDomain(normalizedRequestHost) ) return undefined;
return normalizedRequestHost;
}
function buildPrivateHostRedirectUrl (req, app) {
if ( ! app ) {
return null;
@@ -388,6 +395,7 @@ async function resolvePrivateIdentity ({ req, services, appUid }) {
const privateCookieName = authService.getPrivateAssetCookieName();
const privateCookieToken = req.cookies?.[privateCookieName];
const privateAppSubdomain = getSubdomainFromHostedRequest(req) || undefined;
const requestedPrivateHost = getRequestedPrivateHost(req);
const hasPrivateCookie = typeof privateCookieToken === 'string' && !!privateCookieToken;
let hasInvalidPrivateCookie = false;
@@ -396,12 +404,14 @@ async function resolvePrivateIdentity ({ req, services, appUid }) {
const claims = authService.verifyPrivateAssetToken(privateCookieToken, {
expectedAppUid: appUid,
expectedSubdomain: privateAppSubdomain,
expectedPrivateHost: requestedPrivateHost,
});
return {
source: 'private-cookie',
userUid: claims.userUid,
sessionUuid: claims.sessionUuid,
subdomain: claims.subdomain || privateAppSubdomain,
privateHost: claims.privateHost || requestedPrivateHost,
hasValidPrivateCookie: true,
hasPrivateCookie,
hasInvalidPrivateCookie,
@@ -422,6 +432,7 @@ async function resolvePrivateIdentity ({ req, services, appUid }) {
source: 'session-cookie',
...identity,
subdomain: privateAppSubdomain,
privateHost: requestedPrivateHost,
hasValidPrivateCookie: false,
hasPrivateCookie,
hasInvalidPrivateCookie,
@@ -465,6 +476,7 @@ async function resolvePrivateIdentity ({ req, services, appUid }) {
source: 'bootstrap-token',
...identity,
subdomain: privateAppSubdomain,
privateHost: requestedPrivateHost,
hasValidPrivateCookie: false,
hasPrivateCookie,
hasInvalidPrivateCookie,
@@ -487,6 +499,7 @@ async function resolvePrivateIdentity ({ req, services, appUid }) {
userUid: undefined,
sessionUuid: undefined,
subdomain: privateAppSubdomain,
privateHost: requestedPrivateHost,
hasValidPrivateCookie: false,
hasPrivateCookie,
hasInvalidPrivateCookie,
@@ -786,6 +799,7 @@ async function evaluatePrivateAppAccess ({ req, res, services, app, requestPath
userUid: identity.userUid,
sessionUuid: identity.sessionUuid,
subdomain: identity.subdomain,
privateHost: identity.privateHost,
});
res.cookie(
authService.getPrivateAssetCookieName(),
@@ -953,6 +953,7 @@ describe('PuterSiteMiddleware', () => {
userUid: 'user-allow-111',
sessionUuid: 'session-allow-111',
subdomain: 'paid',
privateHost: 'paid.puter.dev',
});
expect(mockRes.cookie).toHaveBeenCalledWith(
'puter.private.asset.token',
+31 -2
View File
@@ -365,7 +365,14 @@ class AuthService extends BaseService {
return normalizedSubdomain || undefined;
}
createPrivateAssetToken ({ appUid, userUid, sessionUuid, subdomain, ttlSeconds } = {}) {
normalizePrivateAssetHost (privateHost) {
if ( typeof privateHost !== 'string' ) return undefined;
const normalizedPrivateHost = privateHost.trim().toLowerCase().replace(/^\./, '');
if ( ! normalizedPrivateHost ) return undefined;
return normalizedPrivateHost;
}
createPrivateAssetToken ({ appUid, userUid, sessionUuid, subdomain, privateHost, ttlSeconds } = {}) {
if ( typeof appUid !== 'string' || !appUid.trim() ) {
throw new Error('appUid is required to create private asset token.');
}
@@ -379,6 +386,10 @@ class AuthService extends BaseService {
if ( subdomain !== undefined && !normalizedSubdomain ) {
throw new Error('subdomain must be a non-empty string when provided.');
}
const normalizedPrivateHost = this.normalizePrivateAssetHost(privateHost);
if ( privateHost !== undefined && !normalizedPrivateHost ) {
throw new Error('privateHost must be a non-empty string when provided.');
}
const effectiveTtlSeconds = this.resolvePositiveInteger(
ttlSeconds,
@@ -392,6 +403,7 @@ class AuthService extends BaseService {
user_uid: userUid.trim(),
...(sessionUuid ? { session: this.uuid_fpe.encrypt(sessionUuid) } : {}),
...(normalizedSubdomain ? { subdomain: normalizedSubdomain } : {}),
...(normalizedPrivateHost ? { private_host: normalizedPrivateHost } : {}),
};
return this.tokenService.sign('auth', payload, {
@@ -401,7 +413,7 @@ class AuthService extends BaseService {
verifyPrivateAssetToken (
token,
{ expectedAppUid, expectedUserUid, expectedSessionUuid, expectedSubdomain } = {},
{ expectedAppUid, expectedUserUid, expectedSessionUuid, expectedSubdomain, expectedPrivateHost } = {},
) {
let decoded;
try {
@@ -440,6 +452,13 @@ class AuthService extends BaseService {
}
subdomain = decoded.subdomain.trim().toLowerCase();
}
let privateHost;
if ( decoded.private_host !== undefined ) {
if ( typeof decoded.private_host !== 'string' || !decoded.private_host.trim() ) {
throw APIError.create('token_auth_failed');
}
privateHost = decoded.private_host.trim().toLowerCase();
}
if ( expectedAppUid && decoded.app_uid !== expectedAppUid ) {
throw APIError.create('token_auth_failed');
@@ -461,12 +480,22 @@ class AuthService extends BaseService {
throw APIError.create('token_auth_failed');
}
}
const normalizedExpectedPrivateHost = this.normalizePrivateAssetHost(expectedPrivateHost);
if ( expectedPrivateHost !== undefined && !normalizedExpectedPrivateHost ) {
throw APIError.create('token_auth_failed');
}
if ( normalizedExpectedPrivateHost ) {
if ( !privateHost || privateHost !== normalizedExpectedPrivateHost ) {
throw APIError.create('token_auth_failed');
}
}
return {
appUid: decoded.app_uid,
userUid: decoded.user_uid,
sessionUuid,
subdomain,
privateHost,
exp: decoded.exp,
iat: decoded.iat,
};
@@ -77,12 +77,14 @@ describe('AuthService private asset token helpers', () => {
const userUid = '4b0cecf8-dd6a-4eb5-bcc4-c76cc7e8d7f0';
const sessionUuid = 'f9000804-2fd3-4da5-819b-afc5296f90f7';
const subdomain = 'beans';
const privateHost = 'beans.puter.dev';
const token = authService.createPrivateAssetToken({
appUid,
userUid,
sessionUuid,
subdomain,
privateHost,
ttlSeconds: 120,
});
@@ -91,12 +93,14 @@ describe('AuthService private asset token helpers', () => {
expectedUserUid: userUid,
expectedSessionUuid: sessionUuid,
expectedSubdomain: subdomain,
expectedPrivateHost: privateHost,
});
expect(claims.appUid).toBe(appUid);
expect(claims.userUid).toBe(userUid);
expect(claims.sessionUuid).toBe(sessionUuid);
expect(claims.subdomain).toBe(subdomain);
expect(claims.privateHost).toBe(privateHost);
expect(typeof claims.exp).toBe('number');
});
@@ -106,6 +110,7 @@ describe('AuthService private asset token helpers', () => {
appUid: 'app-9f1c10e3-9a7f-43fb-8671-af4918e65407',
userUid: '9885b80e-1a14-4c8d-9e3f-4fa5915b1136',
subdomain: 'beans',
privateHost: 'beans.puter.dev',
});
expect(() => authService.verifyPrivateAssetToken(token, {
@@ -119,6 +124,10 @@ describe('AuthService private asset token helpers', () => {
expect(() => authService.verifyPrivateAssetToken(token, {
expectedSubdomain: 'other-app',
})).toThrow();
expect(() => authService.verifyPrivateAssetToken(token, {
expectedPrivateHost: 'other.puter.dev',
})).toThrow();
});
it('rejects non private-asset tokens', () => {