mirror of
https://github.com/HeyPuter/puter.git
synced 2026-11-09 23:43:16 +00:00
589 lines
19 KiB
JavaScript
589 lines
19 KiB
JavaScript
// METADATA // {"ai-commented":{"service":"openai-completion","model":"gpt-4o"}}
|
|
/*
|
|
* Copyright (C) 2024-present Puter Technologies Inc.
|
|
*
|
|
* This file is part of Puter.
|
|
*
|
|
* Puter is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU Affero General Public License as published
|
|
* by the Free Software Foundation, either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU Affero General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Affero General Public License
|
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
*/
|
|
const { Actor, UserActorType, AppUnderUserActorType, AccessTokenActorType, SiteActorType } = require('./Actor');
|
|
const BaseService = require('../BaseService');
|
|
const { get_user, get_app } = require('../../helpers');
|
|
const { Context } = require('../../util/context');
|
|
const APIError = require('../../api/APIError');
|
|
const { DB_WRITE } = require('../database/consts');
|
|
const { UUIDFPE } = require('../../util/uuidfpe');
|
|
|
|
// This constant defines the namespace used for generating app UUIDs from their origins
|
|
const APP_ORIGIN_UUID_NAMESPACE = '33de3768-8ee0-43e9-9e73-db192b97a5d8';
|
|
|
|
const LegacyTokenError = class extends Error {
|
|
};
|
|
|
|
/**
|
|
* @class AuthService
|
|
* This class is responsible for handling authentication and authorization tasks for the application.
|
|
*/
|
|
class AuthService extends BaseService {
|
|
static MODULES = {
|
|
jwt: require('jsonwebtoken'),
|
|
crypto: require('crypto'),
|
|
uuidv5: require('uuid').v5,
|
|
uuidv4: require('uuid').v4,
|
|
};
|
|
|
|
async _init () {
|
|
this.db = await this.services.get('database').get(DB_WRITE, 'auth');
|
|
this.svc_session = await this.services.get('session');
|
|
|
|
const svc_feature_flag = await this.services.get('feature-flag');
|
|
svc_feature_flag.register('temp-users-disabled', {
|
|
$: 'config-flag',
|
|
value: this.global_config.disable_temp_users ?? false,
|
|
});
|
|
|
|
svc_feature_flag.register('user-signup-disabled', {
|
|
$: 'config-flag',
|
|
value: this.global_config.disable_user_signup ?? false,
|
|
});
|
|
|
|
// "FPE" stands for "Format Preserving Encryption"
|
|
// The `uuid_fpe_key` is a key for creating encrypted alternatives
|
|
// to UUIDs and decrypting them back to the original UUIDs
|
|
//
|
|
// We do this to avoid exposing the internal UUID for sessions.
|
|
const uuid_fpe_key = this.config.uuid_fpe_key
|
|
? UUIDFPE.uuidToBuffer(this.config.uuid_fpe_key)
|
|
: this.modules.crypto.randomBytes(16);
|
|
this.uuid_fpe = new UUIDFPE(uuid_fpe_key);
|
|
|
|
this.sessions = {};
|
|
|
|
const svc_token = await this.services.get('token');
|
|
this.modules.jwt = {
|
|
sign: (payload, _, options) => svc_token.sign('auth', payload, options),
|
|
verify: (token, _) => svc_token.verify('auth', token),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* This method authenticates a user or app using a token.
|
|
* It checks the token's type (session, app-under-user, access-token) and decodes it.
|
|
* Depending on the token type, it returns the corresponding user/app actor.
|
|
* @param {string} token - The token to authenticate.
|
|
* @returns {Promise<Actor>} The authenticated user or app actor.
|
|
*/
|
|
async authenticate_from_token (token) {
|
|
const decoded = this.modules.jwt.verify(token,
|
|
this.global_config.jwt_secret);
|
|
|
|
if ( ! decoded.hasOwnProperty('type') ) {
|
|
throw new LegacyTokenError();
|
|
}
|
|
|
|
if ( decoded.type === 'session' ) {
|
|
const session = await this.get_session_(decoded.uuid);
|
|
|
|
if ( ! session ) {
|
|
throw APIError.create('token_auth_failed');
|
|
}
|
|
|
|
const user = await get_user({ uuid: decoded.user_uid });
|
|
|
|
if ( ! user ) {
|
|
throw APIError.create('user_not_found');
|
|
}
|
|
|
|
const actor_type = new UserActorType({
|
|
user,
|
|
session: session.uuid,
|
|
});
|
|
|
|
return new Actor({
|
|
user_uid: decoded.user_uid,
|
|
type: actor_type,
|
|
});
|
|
}
|
|
|
|
if ( decoded.type === 'app-under-user' ) {
|
|
let session;
|
|
if ( decoded.session ) {
|
|
const session_uuid = this.uuid_fpe.decrypt(decoded.session);
|
|
session = await this.get_session_(session_uuid);
|
|
|
|
if ( ! session ) {
|
|
throw APIError.create('token_auth_failed');
|
|
}
|
|
}
|
|
|
|
const user = await get_user({ uuid: decoded.user_uid });
|
|
if ( ! user ) {
|
|
throw APIError.create('token_auth_failed');
|
|
}
|
|
|
|
const app = await get_app({ uid: decoded.app_uid });
|
|
if ( ! app ) {
|
|
throw APIError.create('token_auth_failed');
|
|
}
|
|
|
|
const actor_type = new AppUnderUserActorType({
|
|
user,
|
|
app,
|
|
session,
|
|
});
|
|
|
|
return new Actor({
|
|
user_uid: decoded.user_uid,
|
|
app_uid: decoded.app_uid,
|
|
type: actor_type,
|
|
});
|
|
}
|
|
|
|
if ( decoded.type === 'access-token' ) {
|
|
const token = decoded.token_uid;
|
|
console.log('DECODED', decoded);
|
|
if ( ! token ) {
|
|
throw APIError.create('token_auth_failed');
|
|
}
|
|
|
|
const user_uid = decoded.user_uid;
|
|
if ( ! user_uid ) {
|
|
throw APIError.create('token_auth_failed');
|
|
}
|
|
|
|
const app_uid = decoded.app_uid;
|
|
|
|
const authorizer = ( user_uid && app_uid )
|
|
? await Actor.create(AppUnderUserActorType, { user_uid, app_uid })
|
|
: await Actor.create(UserActorType, { user_uid });
|
|
|
|
const authorized = Context.get('actor');
|
|
|
|
const actor_type = new AccessTokenActorType({
|
|
token, authorizer, authorized,
|
|
});
|
|
|
|
return new Actor({
|
|
user_uid,
|
|
app_uid,
|
|
type: actor_type,
|
|
});
|
|
}
|
|
|
|
if ( decoded.type === 'actor-site' ) {
|
|
const site_uid = decoded.site_uid;
|
|
const svc_puterSite = this.services.get('puter-site');
|
|
const site =
|
|
await svc_puterSite.get_subdomain_by_uid(site_uid);
|
|
return Actor.create(SiteActorType, {
|
|
site,
|
|
iat: decoded.iat,
|
|
});
|
|
}
|
|
|
|
throw APIError.create('token_auth_failed');
|
|
}
|
|
|
|
get_user_app_token (app_uid) {
|
|
const actor = Context.get('actor');
|
|
const actor_type = actor.type;
|
|
|
|
if ( ! (actor_type instanceof UserActorType) ) {
|
|
throw APIError.create('forbidden');
|
|
}
|
|
|
|
this.log.debug(`generating user-app token for app ${app_uid} and user ${actor_type.user.uuid}`, {
|
|
app_uid,
|
|
user_uid: actor_type.user.uuid,
|
|
});
|
|
|
|
const token = this.modules.jwt.sign({
|
|
type: 'app-under-user',
|
|
version: '0.0.0',
|
|
user_uid: actor_type.user.uuid,
|
|
app_uid,
|
|
...(actor_type.session ? { session: this.uuid_fpe.encrypt(actor_type.session) } : {}),
|
|
},
|
|
this.global_config.jwt_secret);
|
|
|
|
return token;
|
|
}
|
|
|
|
get_site_app_token ({ site_uid }) {
|
|
const token = this.modules.jwt.sign({
|
|
type: 'actor-site',
|
|
version: '0.0.0',
|
|
site_uid,
|
|
},
|
|
this.global_config.jwt_secret,
|
|
{ expiresIn: '1h' });
|
|
|
|
return token;
|
|
}
|
|
|
|
/**
|
|
* Internal method for creating a session.
|
|
*
|
|
* If a request object is provided in the metadata, it will be used to
|
|
* extract information about the requestor and include it in the
|
|
* session's metadata.
|
|
*/
|
|
async create_session_ (user, meta = {}) {
|
|
this.log.debug('CREATING SESSION');
|
|
|
|
if ( meta.req ) {
|
|
const req = meta.req;
|
|
delete meta.req;
|
|
|
|
const ip = this.global_config.fowarded
|
|
? req.headers['x-forwarded-for'] ||
|
|
req.connection.remoteAddress
|
|
: req.connection.remoteAddress
|
|
;
|
|
|
|
meta.ip = ip;
|
|
|
|
meta.server = this.global_config.server_id;
|
|
|
|
if ( req.headers['user-agent'] ) {
|
|
meta.user_agent = req.headers['user-agent'];
|
|
}
|
|
|
|
if ( req.headers['referer'] ) {
|
|
meta.referer = req.headers['referer'];
|
|
}
|
|
|
|
if ( req.headers['origin'] ) {
|
|
const origin = this._origin_from_url(req.headers['origin']);
|
|
if ( origin ) {
|
|
meta.origin = origin;
|
|
}
|
|
}
|
|
|
|
if ( req.headers['host'] ) {
|
|
const host = this._origin_from_url(req.headers['host']);
|
|
if ( host ) {
|
|
meta.host = host;
|
|
}
|
|
}
|
|
}
|
|
|
|
return await this.svc_session.create_session(user, meta);
|
|
}
|
|
|
|
/**
|
|
* Alias to SessionService's get_session method,
|
|
* in case AuthService ever needs to wrap this functionality.
|
|
*/
|
|
async get_session_ (uuid) {
|
|
return await this.svc_session.get_session(uuid);
|
|
}
|
|
|
|
/**
|
|
* Creates a session token using TokenService's sign method
|
|
* with type 'session' using a newly created session for the
|
|
* specified user.
|
|
* @param {*} user
|
|
* @param {*} meta
|
|
* @returns
|
|
*/
|
|
async create_session_token (user, meta) {
|
|
const session = await this.create_session_(user, meta);
|
|
|
|
const token = this.modules.jwt.sign({
|
|
type: 'session',
|
|
version: '0.0.0',
|
|
uuid: session.uuid,
|
|
// meta: session.meta,
|
|
user_uid: user.uuid,
|
|
}, this.global_config.jwt_secret);
|
|
|
|
return { session, token };
|
|
}
|
|
|
|
/**
|
|
* This method checks if the provided session token is valid and returns the associated user and token.
|
|
* If the token is not a valid session token or it does not exist in the database, it returns an empty object.
|
|
*
|
|
* @param {string} cur_token - The session token to be checked.
|
|
* @param {object} meta - Additional metadata associated with the token.
|
|
* @returns {object} Object containing the user and token if the token is valid, otherwise an empty object.
|
|
*/
|
|
async check_session (cur_token, meta) {
|
|
const decoded = this.modules.jwt.verify(cur_token, this.global_config.jwt_secret);
|
|
|
|
console.log('\x1B[36;1mDECODED SESSION', decoded);
|
|
|
|
if ( decoded.type && decoded.type !== 'session' ) {
|
|
return {};
|
|
}
|
|
|
|
const is_legacy = !decoded.type;
|
|
|
|
const user = await get_user({ uuid:
|
|
is_legacy ? decoded.uuid : decoded.user_uid,
|
|
});
|
|
if ( ! user ) {
|
|
return {};
|
|
}
|
|
|
|
if ( ! is_legacy ) {
|
|
// Ensure session exists
|
|
const session = await this.get_session_(decoded.uuid);
|
|
if ( ! session ) {
|
|
return {};
|
|
}
|
|
|
|
// Return the session
|
|
return { user, token: cur_token };
|
|
}
|
|
|
|
this.log.info('UPGRADING SESSION');
|
|
|
|
// Upgrade legacy token
|
|
// TODO: phase this out
|
|
const { session, token } = await this.create_session_token(user, meta);
|
|
|
|
const actor_type = new UserActorType({
|
|
user,
|
|
session,
|
|
});
|
|
|
|
const actor = new Actor({
|
|
user_uid: user.uuid,
|
|
type: actor_type,
|
|
});
|
|
|
|
return { actor, user, token };
|
|
}
|
|
|
|
/**
|
|
* Removes a session with the specified token
|
|
*
|
|
* @param {string} token - The token to be authenticated.
|
|
* @returns {Promise<void>}
|
|
*/
|
|
async remove_session_by_token (token) {
|
|
const decoded = this.modules.jwt.verify(token, this.global_config.jwt_secret);
|
|
|
|
if ( decoded.type !== 'session' ) {
|
|
return;
|
|
}
|
|
|
|
await this.svc_session.remove_session(decoded.uuid);
|
|
}
|
|
|
|
/**
|
|
* This method is used to create an access token for a user or an application.
|
|
*
|
|
* Access tokens aren't currently used by any of Puter's features.
|
|
* The feature is kept here for future-use.
|
|
*
|
|
* @param {1} authorizer - The actor that is creating the access token.
|
|
* @param {*} permissions - The permissions to be granted to the access token.
|
|
* @returns
|
|
*/
|
|
async create_access_token (authorizer, permissions, options) {
|
|
const jwt_obj = {};
|
|
const authorizer_obj = {};
|
|
if ( authorizer.type instanceof UserActorType ) {
|
|
Object.assign(authorizer_obj, {
|
|
authorizer_user_id: authorizer.type.user.id,
|
|
});
|
|
const user = await get_user({ id: authorizer.type.user.id });
|
|
jwt_obj.user_uid = user.uuid;
|
|
}
|
|
else if ( authorizer.type instanceof AppUnderUserActorType ) {
|
|
Object.assign(authorizer_obj, {
|
|
authorizer_user_id: authorizer.type.user.id,
|
|
authorizer_app_id: authorizer.type.app.id,
|
|
});
|
|
const user = await get_user({ id: authorizer.type.user.id });
|
|
jwt_obj.user_uid = user.uuid;
|
|
const app = await get_app({ id: authorizer.type.app.id });
|
|
jwt_obj.app_uid = app.uid;
|
|
}
|
|
else {
|
|
throw APIError.create('forbidden');
|
|
}
|
|
|
|
const uuid = this.modules.uuidv4();
|
|
|
|
const jwt = this.modules.jwt.sign({
|
|
type: 'access-token',
|
|
version: '0.0.0',
|
|
token_uid: uuid,
|
|
...jwt_obj,
|
|
}, this.global_config.jwt_secret, options);
|
|
|
|
for ( const permmission_spec of permissions ) {
|
|
let [permission, extra] = permmission_spec;
|
|
|
|
const svc_permission = await Context.get('services').get('permission');
|
|
permission = await svc_permission._rewrite_permission(permission);
|
|
|
|
const insert_object = {
|
|
token_uid: uuid,
|
|
...authorizer_obj,
|
|
permission,
|
|
extra: JSON.stringify(extra ?? {}),
|
|
};
|
|
const cols = Object.keys(insert_object).join(', ');
|
|
const vals = Object.values(insert_object).map(v => '?').join(', ');
|
|
await this.db.write('INSERT INTO `access_token_permissions` ' +
|
|
`(${cols}) VALUES (${vals})`,
|
|
Object.values(insert_object));
|
|
}
|
|
|
|
return jwt;
|
|
}
|
|
|
|
/**
|
|
* Get the session list for the specified actor.
|
|
*
|
|
* This is primarily used by the `/list-sessions` API endpoint
|
|
* for the Session Manager in Puter's settings window.
|
|
*
|
|
* @param {*} actor - The actor for which to list sessions.
|
|
* @returns {Promise<Array>} - A list of sessions for the actor.
|
|
*/
|
|
async list_sessions (actor) {
|
|
const seen = new Set();
|
|
const sessions = [];
|
|
|
|
const cache_sessions = this.svc_session.get_user_sessions(actor.type.user);
|
|
for ( const session of cache_sessions ) {
|
|
seen.add(session.uuid);
|
|
sessions.push(session);
|
|
}
|
|
|
|
// We won't take the cached sessions here because it's
|
|
// possible the user has sessions on other servers
|
|
const db_sessions = await this.db.read('SELECT uuid, meta FROM `sessions` WHERE `user_id` = ?',
|
|
[actor.type.user.id]);
|
|
|
|
for ( const session of db_sessions ) {
|
|
if ( seen.has(session.uuid) ) {
|
|
continue;
|
|
}
|
|
session.meta = this.db.case({
|
|
mysql: () => session.meta,
|
|
/**
|
|
* This method is responsible for authenticating a user or app using a token. It decodes the token and checks if it's valid, then returns an appropriate actor object based on the token type.
|
|
*
|
|
* @param {string} token - The user or app access token.
|
|
* @returns {Actor} - Actor object representing the authenticated user or app.
|
|
*/
|
|
otherwise: () => JSON.parse(session.meta ?? '{}'),
|
|
})();
|
|
sessions.push(session);
|
|
};
|
|
|
|
for ( const session of sessions ) {
|
|
if ( session.uuid === actor.type.session ) {
|
|
session.current = true;
|
|
}
|
|
}
|
|
|
|
return sessions;
|
|
}
|
|
|
|
/**
|
|
* Revokes a session by UUID. The actor is ignored but should be provided
|
|
* for future use.
|
|
*
|
|
* @param {*} actor
|
|
* @param {*} uuid
|
|
*/
|
|
async revoke_session (actor, uuid) {
|
|
delete this.sessions[uuid];
|
|
this.svc_session.remove_session(uuid);
|
|
}
|
|
|
|
/**
|
|
* This method is used to create or obtain a user-app token deterministically
|
|
* from an origin at which puter.js might be embedded.
|
|
*
|
|
* @param {*} origin - The origin URL at which puter.js is embedded.
|
|
* @returns
|
|
*/
|
|
async get_user_app_token_from_origin (origin) {
|
|
origin = this._origin_from_url(origin);
|
|
const app_uid = await this._app_uid_from_origin(origin);
|
|
|
|
// Determine if the app exists
|
|
const apps = await this.db.read('SELECT * FROM `apps` WHERE `uid` = ? LIMIT 1',
|
|
[app_uid]);
|
|
|
|
if ( apps[0] ) {
|
|
return this.get_user_app_token(app_uid);
|
|
}
|
|
|
|
this.log.info(`creating app ${app_uid} from origin ${origin}`);
|
|
|
|
const name = app_uid;
|
|
const title = app_uid;
|
|
const description = `App created from origin ${origin}`;
|
|
const index_url = origin;
|
|
const owner_user_id = null;
|
|
|
|
// Create the app
|
|
await this.db.write('INSERT INTO `apps` ' +
|
|
'(`uid`, `name`, `title`, `description`, `index_url`, `owner_user_id`) ' +
|
|
'VALUES (?, ?, ?, ?, ?, ?)',
|
|
[app_uid, name, title, description, index_url, owner_user_id]);
|
|
|
|
return this.get_user_app_token(app_uid);
|
|
}
|
|
|
|
/**
|
|
* Generates a deterministic app uuid from an origin
|
|
*
|
|
* @param {*} origin
|
|
* @returns
|
|
*/
|
|
async app_uid_from_origin (origin) {
|
|
origin = this._origin_from_url(origin);
|
|
if ( origin === null ) {
|
|
throw APIError.create('no_origin_for_app');
|
|
}
|
|
return await this._app_uid_from_origin(origin);
|
|
}
|
|
|
|
async _app_uid_from_origin (origin) {
|
|
const event = { origin };
|
|
const svc_event = this.services.get('event');
|
|
await svc_event.emit('app.from-origin', event);
|
|
// UUIDV5
|
|
const uuid = this.modules.uuidv5(event.origin, APP_ORIGIN_UUID_NAMESPACE);
|
|
return `app-${uuid}`;
|
|
}
|
|
|
|
_origin_from_url ( url ) {
|
|
try {
|
|
const parsedUrl = new URL(url);
|
|
// Origin is protocol + hostname + port
|
|
return `${parsedUrl.protocol}//${parsedUrl.hostname}${parsedUrl.port ? `:${parsedUrl.port}` : ''}`;
|
|
} catch ( error ) {
|
|
console.error('Invalid URL:', error.message);
|
|
return null;
|
|
}
|
|
}
|
|
}
|
|
|
|
module.exports = {
|
|
AuthService,
|
|
LegacyTokenError,
|
|
};
|