dev(backend): OIDC continued [1]

This commit is rather monolithic. An attempt to split it up into smaller
changes proved too difficult (as well as frustrating) and I realized it
would absolutely increase the chance of having a broken commit (making
bisects more difficult) unless a lot of testing effort between commits
was performed, which would have very little benefit.

The changes in this commit include:
- Outcome utility used by SignupService for error handling
- SignupService, whichs implements re-usable create_user function
- Signup method in OIDCService
- flow-specific callbacks in OIDC (separates login from signup)
- **SEPARATE SESSION COOKIE AND GUI COOKIE**
  - this change "rocks the boat" the most and has the highest likelihood
    of causing problems
This commit is contained in:
KernelDeimos
2026-02-10 13:12:01 -05:00
parent 47b133d512
commit 7c8f0d5572
27 changed files with 868 additions and 220 deletions
+3
View File
@@ -272,6 +272,9 @@ const install = async ({ context, services, app, useapi, modapi }) => {
const { OIDCService } = require('./services/auth/OIDCService');
services.registerService('oidc', OIDCService);
const { SignupService } = require('./services/auth/SignupService');
services.registerService('signup', SignupService);
const { UserProtectedEndpointsService } = require('./services/web/UserProtectedEndpointsService');
services.registerService('__user-protected-endpoints', UserProtectedEndpointsService);
+4
View File
@@ -453,6 +453,10 @@ class APIError {
status: 403,
message: 'This endpoint must be requested with a user session',
},
'session_required': {
status: 403,
message: 'This endpoint requires a full session (e.g. change password cannot be done with a GUI token).',
},
'temporary_accounts_not_allowed': {
status: 403,
message: 'Temporary accounts cannot perform this action',
+10
View File
@@ -30,6 +30,7 @@ const { NodeUIDSelector } = require('./filesystem/node/selectors');
const { redisClient } = require('./clients/redis/redisSingleton');
const { kv } = require('./util/kvSingleton');
const { APP_ICONS_SUBDOMAIN } = require('./consts/app-icons.js');
const { generate_identifier } = require('./util/identifier');
const identifying_uuid = require('uuid').v4();
@@ -1479,6 +1480,14 @@ async function username_exists (username) {
}
}
async function generate_random_username () {
let username;
do {
username = generate_identifier();
} while ( await username_exists(username) );
return username;
}
async function app_name_exists (name) {
/** @type BaseDatabaseAccessService */
const db = _servicesHolder.services.get('database').get(DB_READ, 'filesystem');
@@ -2070,6 +2079,7 @@ module.exports = {
suggestedAppForFsEntry,
df,
username_exists,
generate_random_username,
uuid2fsentry,
validate_fsentry_name,
validate_signature_auth,
@@ -134,7 +134,8 @@ const configurable_auth = options => async (req, res, next) => {
throw APIError.create('forbidden');
}
res.cookie(config.cookie_name, new_info.token, {
// Use session token in cookie so cookie-based requests have hasHttpPowers; client gets GUI token in response
res.cookie(config.cookie_name, new_info.session_token ?? new_info.token, {
sameSite: 'none',
secure: true,
httpOnly: true,
+93 -67
View File
@@ -20,32 +20,61 @@
const express = require('express');
const router = new express.Router();
const config = require('../../config');
const { get_user } = require('../../helpers');
const complete_ = async ({ req, res, user }) => {
/** If Accept includes text/html, set session cookie and redirect to app; otherwise send JSON. */
const finishOidcSuccess_ = async (req, res, user, stateDecoded) => {
console.log('okay finishOidSuccess_ is happening');
const svc_auth = req.services.get('auth');
const { token } = await svc_auth.create_session_token(user, { req });
res.cookie(config.cookie_name, token, {
const { session, token: session_token } = await svc_auth.create_session_token(user, { req });
res.cookie(config.cookie_name, session_token, {
sameSite: 'none',
secure: true,
httpOnly: true,
});
return res.send({
proceed: true,
next_step: 'complete',
token,
user: {
username: user.username,
uuid: user.uuid,
email: user.email,
email_confirmed: user.email_confirmed,
is_temp: (user.password === null && user.email === null),
},
console.log('what are these values?', {
stateDecoded,
});
let target = stateDecoded.redirect_uri || config.origin || '/';
const origin = config.origin || '';
console.log('okay what\'s target though?', { target, origin });
if ( target && origin && !target.startsWith(origin) ) {
target = origin;
}
return res.redirect(302, target);
};
/** Exchange code for tokens, get userinfo; returns { provider, userinfo, stateDecoded } or sends error and returns null. */
const oidcCallbackPreamble_ = async (req, res, callbackRedirectUri) => {
const svc_oidc = req.services.get('oidc');
const code = req.query.code;
const state = req.query.state;
if ( !code || !state ) {
res.status(400).send('Missing code or state.');
return null;
}
const stateDecoded = svc_oidc.verifyState(state);
if ( !stateDecoded || !stateDecoded.provider ) {
res.status(400).send('Invalid or expired state.');
return null;
}
const provider = stateDecoded.provider;
const tokens = await svc_oidc.exchangeCodeForTokens(provider, code, callbackRedirectUri);
if ( !tokens || !tokens.access_token ) {
res.status(401).send('Token exchange failed.');
return null;
}
const userinfo = await svc_oidc.getUserInfo(provider, tokens.access_token);
if ( !userinfo || !userinfo.sub ) {
res.status(401).send('Could not get user info.');
return null;
}
return { provider, userinfo, stateDecoded };
};
// GET /auth/oidc/providers - list enabled provider ids for frontend
router.get('/auth/oidc/providers', async (req, res) => {
if ( require('../../helpers').subdomain(req) !== 'api' && require('../../helpers').subdomain(req) !== '' ) {
if ( require('../../helpers').subdomain(req) !== 'api' ) {
return res.status(404).end();
}
const svc_oidc = req.services.get('oidc');
@@ -55,7 +84,7 @@ router.get('/auth/oidc/providers', async (req, res) => {
// GET /auth/oidc/:provider/start - redirect to IdP authorization
router.get('/auth/oidc/:provider/start', async (req, res) => {
if ( require('../../helpers').subdomain(req) !== 'api' && require('../../helpers').subdomain(req) !== '' ) {
if ( require('../../helpers').subdomain(req) !== '' ) {
return res.status(404).end();
}
const svc_edgeRateLimit = req.services.get('edge-rate-limit');
@@ -68,74 +97,71 @@ router.get('/auth/oidc/:provider/start', async (req, res) => {
if ( ! cfg ) {
return res.status(404).send('Provider not configured.');
}
const redirectUri = req.query.redirect_uri ? String(req.query.redirect_uri) : undefined;
const statePayload = { provider, redirect_uri: redirectUri };
const flow = req.query.flow ? String(req.query.flow) : undefined;
const flowRedirects = {
login: config.origin || '/',
signup: config.origin || '/',
};
const appRedirectUri = (flow && flowRedirects[flow]) ? flowRedirects[flow] : (config.origin || '/');
const statePayload = { provider, redirect_uri: appRedirectUri };
const state = svc_oidc.signState(statePayload);
const url = await svc_oidc.getAuthorizationUrl(provider, state, redirectUri ? undefined : undefined);
const url = await svc_oidc.getAuthorizationUrl(provider, state, flow);
if ( ! url ) {
return res.status(502).send('Could not build authorization URL.');
}
return res.redirect(302, url);
});
// GET /auth/oidc/callback - handle IdP redirect (code + state)
router.get('/auth/oidc/callback', async (req, res) => {
if ( require('../../helpers').subdomain(req) !== 'api' && require('../../helpers').subdomain(req) !== '' ) {
// GET /auth/oidc/callback/login - login only: existing account or abort. Never creates a user.
router.get('/auth/oidc/callback/login', async (req, res) => {
if ( require('../../helpers').subdomain(req) !== '' ) {
return res.status(404).end();
}
const svc_edgeRateLimit = req.services.get('edge-rate-limit');
if ( ! svc_edgeRateLimit.check('login') ) {
return res.status(429).send('Too many requests.');
}
const code = req.query.code;
const state = req.query.state;
if ( !code || !state ) {
return res.status(400).send('Missing code or state.');
const svc_oidc = req.services.get('oidc');
const callbackRedirectUri = svc_oidc.getCallbackUrlForFlow('login');
const preamble = await oidcCallbackPreamble_(req, res, callbackRedirectUri);
if ( ! preamble ) return;
const { provider, userinfo, stateDecoded } = preamble;
const user = await svc_oidc.findUserByProviderSub(provider, userinfo.sub);
if ( ! user ) {
return res.status(400).send('No account found. Sign up first.');
}
if ( user.suspended ) {
return res.status(401).send('This account is suspended.');
}
return await finishOidcSuccess_(req, res, user, stateDecoded);
});
// GET /auth/oidc/callback/signup - signup only: create new account or abort. Never logs in to existing account.
router.get('/auth/oidc/callback/signup', async (req, res) => {
if ( require('../../helpers').subdomain(req) !== '' ) {
return res.status(404).end();
}
const svc_edgeRateLimit = req.services.get('edge-rate-limit');
if ( ! svc_edgeRateLimit.check('login') ) {
return res.status(429).send('Too many requests.');
}
const svc_oidc = req.services.get('oidc');
const stateDecoded = svc_oidc.verifyState(state);
if ( !stateDecoded || !stateDecoded.provider ) {
return res.status(400).send('Invalid or expired state.');
const callbackRedirectUri = svc_oidc.getCallbackUrlForFlow('signup');
const preamble = await oidcCallbackPreamble_(req, res, callbackRedirectUri);
if ( ! preamble ) return;
const { provider, userinfo, stateDecoded } = preamble;
const existingUser = await svc_oidc.findUserByProviderSub(provider, userinfo.sub);
if ( existingUser ) {
return res.status(400).send('Account already exists. Log in instead.');
}
const provider = stateDecoded.provider;
const redirectUri = `${config.api_base_url}/auth/oidc/callback`;
const tokens = await svc_oidc.exchangeCodeForTokens(provider, code, redirectUri);
if ( !tokens || !tokens.access_token ) {
return res.status(401).send('Token exchange failed.');
const outcome = await svc_oidc.createUserFromOIDC(provider, userinfo);
if ( outcome.failed ) {
console.log('it looks like the outcome failed...');
return res.status(400).send(outcome.userMessage);
}
const userinfo = await svc_oidc.getUserInfo(provider, tokens.access_token);
if ( !userinfo || !userinfo.sub ) {
return res.status(401).send('Could not get user info.');
}
let user = await svc_oidc.findUserByProviderSub(provider, userinfo.sub);
if ( user ) {
if ( user.suspended ) {
return res.status(401).send('This account is suspended.');
}
return await complete_({ req, res, user });
}
user = await svc_oidc.createUserFromOIDC(provider, userinfo);
if ( ! user ) {
return res.status(400).send('Email already registered. Please log in with your password and link your Google account, or use a different email.');
}
const accept = req.headers.accept || '';
const wantsRedirect = accept.includes('text/html');
if ( wantsRedirect ) {
const svc_auth = req.services.get('auth');
const { token } = await svc_auth.create_session_token(user, { req });
res.cookie(config.cookie_name, token, {
sameSite: 'none',
secure: true,
httpOnly: true,
});
let target = stateDecoded.redirect_uri || config.origin || '/';
const origin = config.origin || '';
if ( target && origin && !target.startsWith(origin) ) {
target = origin;
}
return res.redirect(302, target);
}
return await complete_({ req, res, user });
const user = await get_user({ id: outcome.infoObject.user_id });
console.log('got user????', user);
return await finishOidcSuccess_(req, res, user, stateDecoded);
});
module.exports = router;
+6 -6
View File
@@ -26,21 +26,21 @@ const { requireCaptcha } = require('../modules/captcha/middleware/captcha-middle
const complete_ = async ({ req, res, user }) => {
const svc_auth = req.services.get('auth');
const { token } = await svc_auth.create_session_token(user, { req });
const { session, token: session_token } = await svc_auth.create_session_token(user, { req });
const gui_token = svc_auth.create_gui_token(user, session);
//set cookie
// res.cookie(config.cookie_name, token);
res.cookie(config.cookie_name, token, {
// HTTP-only cookie gets session token (cookie-based requests have hasHttpPowers)
res.cookie(config.cookie_name, session_token, {
sameSite: 'none',
secure: true,
httpOnly: true,
});
// send response
// response body: GUI token only (client never gets session token)
return res.send({
proceed: true,
next_step: 'complete',
token: token,
token: gui_token,
user: {
username: user.username,
uuid: user.uuid,
+2
View File
@@ -51,7 +51,9 @@ router.post('/logout', auth, express.json(), async (req, res, next) => {
//---------------------------------------------------------
// DANGER ZONE: delete temp user and all its data
//---------------------------------------------------------
console.log('wait... what are these?', req.user.password, req.user.email);
if ( req.user.password === null && req.user.email === null ) {
console.log('ACTUALLY DELETING A USER');
const { deleteUser } = require('../helpers');
deleteUser(req.user.id);
}
+6 -5
View File
@@ -208,9 +208,10 @@ router.post('/save_account', auth, express.json(), async (req, res, next) => {
}
}
// create token for login
// create token for login: session token for cookie, GUI token for client
const svc_auth = req.services.get('auth');
const { token } = await svc_auth.create_session_token(req.user, { req });
const { session, token: session_token } = await svc_auth.create_session_token(req.user, { req });
const gui_token = svc_auth.create_gui_token(req.user, session);
// user id
// todo if pseudo user, assign directly no need to do another DB lookup
@@ -220,8 +221,8 @@ router.post('/save_account', auth, express.json(), async (req, res, next) => {
// todo send LINK-based verification email
//set cookie
res.cookie(config.cookie_name, token);
// HTTP-only cookie gets session token (cookie-based requests have hasHttpPowers)
res.cookie(config.cookie_name, session_token);
{
const svc_event = req.services.get('event');
@@ -230,7 +231,7 @@ router.post('/save_account', auth, express.json(), async (req, res, next) => {
// return results
return res.send({
token: token,
token: gui_token,
user: {
username: user.username,
uuid: user.uuid,
+7 -6
View File
@@ -421,11 +421,12 @@ module.exports = eggspress(['/signup'], {
const [user] = await db.pread('SELECT * FROM `user` WHERE `id` = ? LIMIT 1',
[user_id]);
// create token for login
const { token } = await svc_auth.create_session_token(user, {
// create token for login: session token for cookie, GUI token for client
const { session, token: session_token } = await svc_auth.create_session_token(user, {
req,
});
// jwt.sign({uuid: user_uuid}, config.jwt_secret);
const gui_token = svc_auth.create_gui_token(user, session);
// jwt.sign({uuid: user_uuid}, config.jwt_secret);
//-------------------------------------------------------------
// email confirmation
@@ -457,8 +458,8 @@ module.exports = eggspress(['/signup'], {
const svc_user = Context.get('services').get('user');
await svc_user.generate_default_fsentries({ user });
//set cookie
res.cookie(config.cookie_name, token, {
// HTTP-only cookie gets session token (cookie-based requests have hasHttpPowers)
res.cookie(config.cookie_name, session_token, {
sameSite: 'none',
secure: true,
httpOnly: true,
@@ -472,7 +473,7 @@ module.exports = eggspress(['/signup'], {
// return results
return res.send({
token: token,
token: gui_token,
user: {
username: user.username,
uuid: user.uuid,
@@ -0,0 +1,127 @@
/*
* 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/>.
*/
'use strict';
const config = require('../config');
const { DB_WRITE } = require('../services/database/consts');
const { generate_identifier } = require('../util/identifier');
const { v4: uuidv4 } = require('uuid');
/**
* Create a new user for signup. Common behavior shared by POST /signup and OIDC signup.
* Form-signup path is still handled in signup.js; this handles OIDC and will support form signup after refactor.
*
* @param {object} services - Backend services (from req.services)
* @param {object} options - Creation options. For OIDC: { providerId, userinfo }. For form signup: TBD (to be refactored from signup.js).
* @returns {Promise<object|null>} The created user, or null on failure (e.g. email already registered).
*/
async function signup_create_new_user (services, options) {
const { providerId, userinfo } = options;
if ( !providerId || !userinfo ) {
// Form signup: to be refactored from signup.js; not implemented here yet.
return null;
}
const db = await services.get('database').get(DB_WRITE, 'auth');
const svc_group = services.get('group');
const svc_user = services.get('user');
const svc_oidc = services.get('oidc');
if ( ! svc_oidc ) return null;
const claims = userinfo;
let username = (claims.name || claims.email || '').toString().trim();
if ( username ) {
username = username.replace(/\s+/g, '_').replace(/[^a-zA-Z0-9_-]/g, '');
if ( username.length > 45 ) username = username.slice(0, 45);
}
if ( !username || !/^\w+$/.test(username) ) {
let candidate;
do {
candidate = generate_identifier();
const [r] = await db.pread('SELECT 1 FROM user WHERE username = ? LIMIT 1', [candidate]);
if ( ! r ) username = candidate;
} while ( !username );
} else {
const [existing] = await db.pread('SELECT 1 FROM user WHERE username = ? LIMIT 1', [username]);
if ( existing ) {
let suffix = 1;
while ( true ) {
const candidate = `${username}${suffix}`;
const [r] = await db.pread('SELECT 1 FROM user WHERE username = ? LIMIT 1', [candidate]);
if ( ! r ) {
username = candidate; break;
}
suffix++;
}
}
}
const email = (claims.email || '').toString().trim() || null;
const clean_email = email ? email.toLowerCase().trim() : null;
if ( clean_email ) {
const [existingEmail] = await db.pread('SELECT 1 FROM user WHERE clean_email = ? LIMIT 1', [clean_email]);
if ( existingEmail ) {
return null; // email already registered; caller should return error
}
}
const user_uuid = uuidv4();
const email_confirm_code = String(Math.floor(100000 + Math.random() * 900000));
const email_confirm_token = uuidv4();
await db.write(`INSERT INTO user (
username, email, clean_email, password, uuid, referrer,
email_confirm_code, email_confirm_token, free_storage,
referred_by, email_confirmed, requires_email_confirmation
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[
username,
email,
clean_email,
null,
user_uuid,
null,
email_confirm_code,
email_confirm_token,
config.storage_capacity,
null,
1,
0,
]);
const [inserted] = await db.pread('SELECT id FROM user WHERE uuid = ? LIMIT 1', [user_uuid]);
const user_id = inserted.id;
await svc_oidc.linkProviderToUser(user_id, providerId, claims.sub, null);
await svc_group.add_users({
uid: config.default_user_group,
users: [username],
});
const [user] = await db.pread('SELECT * FROM user WHERE id = ? LIMIT 1', [user_id]);
if ( user && user.metadata && typeof user.metadata === 'string' ) {
user.metadata = JSON.parse(user.metadata);
} else if ( user && !user.metadata ) {
user.metadata = {};
}
await svc_user.generate_default_fsentries({ user });
return user;
}
module.exports = signup_create_new_user;
+16 -4
View File
@@ -1,9 +1,15 @@
import type { ServerHealthService } from '../modules/core/ServerHealthService';
import { SqliteDatabaseAccessService } from './database/SqliteDatabaseAccessService';
import { MeteringServiceWrapper } from './MeteringService/MeteringServiceWrapper.mjs';
import { DDBClient } from '../clients/dynamodb/DDBClient';
import { DynamoKVStore } from '../clients/dynamodb/DynamoKVStore/DynamoKVStore';
import type { ServerHealthService } from '../modules/core/ServerHealthService';
import { GroupService } from './auth/GroupService';
import SignupService from './auth/SignupService';
import { CleanEmailService } from './CleanEmailService';
import { SqliteDatabaseAccessService } from './database/SqliteDatabaseAccessService';
import { EventService } from './EventService';
import { FeatureFlagService } from './FeatureFlagService';
import { MeteringServiceWrapper } from './MeteringService/MeteringServiceWrapper.mjs';
import type { SUService } from './SUService';
import { UserService } from './UserService';
export interface ServiceResources {
services: {
@@ -13,7 +19,13 @@ export interface ServiceResources {
get (name: 'server-health'): ServerHealthService;
get (name: 'su'): SUService;
get (name: 'dynamo'): DDBClient;
get (name: string): any;
get (name: 'user'): UserService;
get (name: 'event'): EventService;
get (name: 'signup'): SignupService;
get (name: 'group'): GroupService;
get (name: 'feature-flag'): FeatureFlagService;
get (name: 'clean-email'): CleanEmailService;
get (name: string): unknown;
};
config: Record<string, any> & { services?: Record<string, any>; server_id?: string };
name?: string;
@@ -70,10 +70,12 @@ class FeatureFlagService extends BaseService {
/**
* checks is a feature flag is enabled for the current user
* @return {boolean} - true if the feature flag is enabled, false otherwise
* @return {boolean} true if the feature flag is enabled, false otherwise
*
* Usage:
* check({ actor }, 'flag-name')
* @example <caption>with a specified actor</caption>
* check({ actor }, 'flag-name');
* @example <caption>with actor in context</caption>
* check('flag-name');
*/
async check (...a) {
// allows binding call with multiple options objects;
+6 -1
View File
@@ -22,6 +22,9 @@ const { invalidate_cached_user } = require('../helpers');
const BaseService = require('./BaseService');
const { DB_WRITE } = require('./database/consts');
/**
* Lorem ipsum dolor sit amet
*/
class UserService extends BaseService {
static MODULES = {
uuidv4: require('uuid').v4,
@@ -54,7 +57,9 @@ class UserService extends BaseService {
return this.dir_system;
}
// used to be called: generate_system_fsentries
/**
* This used to be called `generate_system_fsentries`
*/
async generate_default_fsentries ({ user }) {
// Note: The comment below is outdated as we now do parallel writes for
+3 -1
View File
@@ -12,8 +12,10 @@ export class SystemActorType {
}
export class UserActorType {
constructor (params: { user: IUser });
constructor (params: { user: IUser; session?: { uuid: string }; hasHttpPowers?: boolean });
user: IUser;
/** When true, this actor can access user-protected HTTP endpoints (e.g. change password). GUI tokens set this false. */
hasHttpPowers: boolean;
get uid (): string;
get_related_type (type_class: unknown): UserActorType;
}
+12 -5
View File
@@ -16,12 +16,12 @@
* 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/>.
*/
import { AdvancedBase } from '../../../../putility/index.js';
import { Context } from '../../util/context.js';
import { get_user, get_app } from '../../helpers.js';
import * as config from '../../config.js';
import { v5 as uuidv5 } from 'uuid';
import crypto from 'crypto';
import { v5 as uuidv5 } from 'uuid';
import { AdvancedBase } from '../../../../putility/index.js';
import * as config from '../../config.js';
import { get_app, get_user } from '../../helpers.js';
import { Context } from '../../util/context.js';
// TODO: add these to configuration; production deployments should change these!
const PRIVATE_UID_NAMESPACE = config.private_uid_namespace
@@ -222,6 +222,13 @@ export class Actor extends AdvancedBase {
* user actors and define how they relate to other types of actors within the system.
*/
export class UserActorType extends ActorType {
constructor (o) {
super(o);
if ( this.hasHttpPowers === undefined ) {
this.hasHttpPowers = false;
}
}
/**
* Gets the unique identifier for the user actor.
*
+94 -8
View File
@@ -107,6 +107,32 @@ class AuthService extends BaseService {
const actor_type = new UserActorType({
user,
session: session.uuid,
hasHttpPowers: true,
});
return new Actor({
user_uid: decoded.user_uid,
type: actor_type,
});
}
if ( decoded.type === 'gui' ) {
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,
hasHttpPowers: false,
});
return new Actor({
@@ -310,6 +336,25 @@ class AuthService extends BaseService {
return { session, token };
}
/**
* Creates a GUI token bound to the same session as the given session object.
* GUI tokens create a UserActorType with hasHttpPowers false, so they cannot
* access user-protected HTTP endpoints (e.g. change password). The GUI receives
* only this token, not the full session token.
*
* @param {*} user - User object (must have .uuid).
* @param {{ uuid: string }} session - Session object (must have .uuid).
* @returns {string} JWT GUI token.
*/
create_gui_token (user, session) {
return this.modules.jwt.sign({
type: 'gui',
version: '0.0.0',
uuid: session.uuid,
user_uid: user.uuid,
}, this.global_config.jwt_secret);
}
/**
* 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.
@@ -323,7 +368,7 @@ class AuthService extends BaseService {
console.log('\x1B[36;1mDECODED SESSION', decoded);
if ( decoded.type && decoded.type !== 'session' ) {
if ( decoded.type && decoded.type !== 'session' && decoded.type !== 'gui' ) {
return {};
}
@@ -343,19 +388,24 @@ class AuthService extends BaseService {
return {};
}
// Return the session
return { user, token: cur_token };
// Return GUI token to client (if they sent session token, exchange for GUI token)
const gui_token = decoded.type === 'gui'
? cur_token
: this.create_gui_token(user, session);
return { user, token: gui_token };
}
this.log.info('UPGRADING SESSION');
// Upgrade legacy token
// TODO: phase this out
const { session, token } = await this.create_session_token(user, meta);
const { session, token: session_token } = await this.create_session_token(user, meta);
const gui_token = this.create_gui_token(user, session);
const actor_type = new UserActorType({
user,
session,
hasHttpPowers: true,
});
const actor = new Actor({
@@ -363,7 +413,8 @@ class AuthService extends BaseService {
type: actor_type,
});
return { actor, user, token };
// token = GUI token for client (response body); session_token = for HTTP-only cookie
return { actor, user, token: gui_token, session_token };
}
/**
@@ -375,7 +426,7 @@ class AuthService extends BaseService {
async remove_session_by_token (token) {
const decoded = this.modules.jwt.verify(token, this.global_config.jwt_secret);
if ( decoded.type !== 'session' ) {
if ( decoded.type !== 'session' && decoded.type !== 'gui' ) {
return;
}
@@ -469,12 +520,12 @@ class AuthService extends BaseService {
} else {
token_uid = tokenOrUuid;
}
/* eslint-disable */
await this.db.write(
'DELETE FROM `access_token_permissions` WHERE `token_uid` = ?',
[token_uid],
);
/* eslint-enable */
const svc_permission = this.services.get('permission');
svc_permission.invalidate_permission_scan_cache_for_access_token(token_uid);
}
@@ -610,6 +661,41 @@ class AuthService extends BaseService {
return null;
}
}
/**
* Registers GET /get-gui-token. Must be called from the GUI origin (no api. subdomain)
* so the HTTP-only session cookie is sent. Returns the GUI token for use in Authorization headers.
*/
['__on_install.routes'] () {
const { app } = this.services.get('web-server');
const config = require('../../config');
const { subdomain } = require('../../helpers');
const configurable_auth = require('../../middleware/configurable_auth');
const { Endpoint } = require('../../util/expressutil');
const svc_auth = this;
Endpoint({
route: '/get-gui-token',
methods: ['GET'],
mw: [configurable_auth()],
handler: async (req, res) => {
if ( ! req.user ) {
return res.status(401).json({});
}
const actor = Context.get('actor');
if ( ! (actor.type instanceof UserActorType) ) {
return res.status(403).json({});
}
if ( ! actor.type.session ) {
return res.status(400).json({ error: 'No session bound to this actor' });
}
const gui_token = svc_auth.create_gui_token(actor.type.user, { uuid: actor.type.session });
return res.json({ token: gui_token });
},
}).attach(app);
}
}
module.exports = {
+47 -92
View File
@@ -17,21 +17,33 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
'use strict';
const BaseService = require('../BaseService');
const { DB_WRITE } = require('../database/consts');
const { generate_identifier } = require('../../util/identifier');
import jwt from 'jsonwebtoken';
import { username_exists } from '../../helpers.js';
import { generate_identifier } from '../../util/identifier.js';
import BaseService from '../BaseService.js';
import { DB_WRITE } from '../database/consts.js';
const GOOGLE_DISCOVERY_URL = 'https://accounts.google.com/.well-known/openid-configuration';
const GOOGLE_SCOPES = 'openid email profile';
const STATE_EXPIRY_SEC = 600; // 10 minutes
const VALID_OIDC_FLOWS = ['login', 'signup'];
async function generate_random_username () {
let username;
do {
username = generate_identifier();
} while ( await username_exists(username) );
return username;
}
/**
* OIDC/OAuth2 service for sign-in with Google (and extensible to other providers).
* Uses config.oidc.providers only; no environment variables.
*/
class OIDCService extends BaseService {
export class OIDCService extends BaseService {
static MODULES = {
jwt: require('jsonwebtoken'),
jwt,
};
async _init () {
@@ -86,12 +98,25 @@ class OIDCService extends BaseService {
}
/**
* Build authorization URL for the provider. redirect_uri is our callback URL.
* Return the OAuth callback URL for a given flow. Structure: /auth/oidc/callback/<flow>
* @param {string} flow - e.g. 'login' or 'signup'
* @returns {string|null} Full callback URL, or null if flow is invalid
*/
async getAuthorizationUrl (providerId, state, redirectUri) {
getCallbackUrlForFlow (flow) {
if ( !flow || !VALID_OIDC_FLOWS.includes(flow) ) return null;
const base = this.global_config.origin || '';
const callback_url = `${base.replace(/\/$/, '')}/auth/oidc/callback/${flow}`;
this.log.noticeme('CALLBACK URL???', { callback_url });
return callback_url;
}
/**
* Build authorization URL for the provider. Callback URL is /auth/oidc/callback/<flow> when flow is provided.
*/
async getAuthorizationUrl (providerId, state, flow) {
const config = await this.getProviderConfig(providerId);
if ( ! config ) return null;
const base = redirectUri ?? `${this.global_config.api_base_url}/auth/oidc/callback`;
const base = this.getCallbackUrlForFlow(flow) ?? `${this.global_config.api_base_url}/auth/oidc/callback`;
const params = new URLSearchParams({
client_id: config.client_id,
redirect_uri: base,
@@ -120,7 +145,7 @@ class OIDCService extends BaseService {
}
/**
* Exchange authorization code for tokens.
* Exchange authorization code for tokens. redirectUri must match the URL used in getAuthorizationUrl (e.g. /auth/oidc/callback/:flow).
*/
async exchangeCodeForTokens (providerId, code, redirectUri) {
const config = await this.getProviderConfig(providerId);
@@ -187,91 +212,23 @@ class OIDCService extends BaseService {
}
/**
* Create a new Puter user from OIDC claims and link the provider. Reuses signup patterns (groups, default fs).
* Create a new Puter user from OIDC claims and link the provider. Delegates to signup_create_new_user.
*/
async createUserFromOIDC (providerId, claims) {
const db = this.db;
const svc_group = this.services.get('group');
const svc_user = this.services.get('user');
const { v4: uuidv4 } = require('uuid');
let username = (claims.name || claims.email || '').toString().trim();
if ( username ) {
username = username.replace(/\s+/g, '_').replace(/[^a-zA-Z0-9_-]/g, '');
if ( username.length > 45 ) username = username.slice(0, 45);
}
if ( !username || !/^\w+$/.test(username) ) {
let candidate;
do {
candidate = generate_identifier();
const [r] = await db.pread('SELECT 1 FROM user WHERE username = ? LIMIT 1', [candidate]);
if ( ! r ) username = candidate;
} while ( !username );
} else {
const [existing] = await db.pread('SELECT 1 FROM user WHERE username = ? LIMIT 1', [username]);
if ( existing ) {
let suffix = 1;
while ( true ) {
const candidate = `${username}${suffix}`;
const [r] = await db.pread('SELECT 1 FROM user WHERE username = ? LIMIT 1', [candidate]);
if ( ! r ) {
username = candidate; break;
}
suffix++;
}
}
}
const email = (claims.email || '').toString().trim() || null;
const clean_email = email ? email.toLowerCase().trim() : null;
if ( clean_email ) {
const [existingEmail] = await db.pread('SELECT 1 FROM user WHERE clean_email = ? LIMIT 1', [clean_email]);
if ( existingEmail ) {
return null; // email already registered; caller should return error
}
}
const user_uuid = uuidv4();
const email_confirm_code = String(Math.floor(100000 + Math.random() * 900000));
const email_confirm_token = uuidv4();
await db.write(`INSERT INTO user (
username, email, clean_email, password, uuid, referrer,
email_confirm_code, email_confirm_token, free_storage,
referred_by, email_confirmed, requires_email_confirmation
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[
username,
email,
clean_email,
null,
user_uuid,
null,
email_confirm_code,
email_confirm_token,
this.global_config.storage_capacity,
null,
1,
0,
]);
const [inserted] = await db.pread('SELECT id FROM user WHERE uuid = ? LIMIT 1', [user_uuid]);
const user_id = inserted.id;
await this.linkProviderToUser(user_id, providerId, claims.sub, null);
await svc_group.add_users({
uid: this.global_config.default_user_group,
users: [username],
const svc_signup = this.services.get('signup');
const outcome = await svc_signup.create_new_user({
username: await generate_random_username(),
email: claims?.email ?? null,
password: null,
oidc_only: true,
});
const [user] = await db.pread('SELECT * FROM user WHERE id = ? LIMIT 1', [user_id]);
if ( user && user.metadata && typeof user.metadata === 'string' ) {
user.metadata = JSON.parse(user.metadata);
} else if ( user && !user.metadata ) {
user.metadata = {};
const { user_id } = outcome.infoObject;
console.log('user_id?', user_id);
if ( outcome.success )
{
await this.linkProviderToUser(user_id, providerId, claims.sub, null);
}
await svc_user.generate_default_fsentries({ user });
return user;
return outcome;
}
/**
@@ -287,5 +244,3 @@ class OIDCService extends BaseService {
return ids;
}
}
module.exports = { OIDCService };
@@ -0,0 +1,270 @@
//@ts-check
import bcrypt from 'bcrypt';
import { v4 as uuidv4 } from 'uuid';
import { generate_random_username, send_email_verification_code, send_email_verification_token, username_exists } from '../../helpers.js';
import { OutcomeObject } from '../../util/outcomeutil.js';
import { validate_nonEmpty_string } from '../../util/validutil.js';
import BaseService from '../BaseService.js';
import { DB_WRITE } from '../database/consts.js';
export class CreatedUserOutcome {
/**
* @type {number|null}
*/
user_id = null;
}
export class SignupService extends BaseService {
/**
* Creates a new user.
* @async
* @param {object} params - The parameters for creating a new user.
* @param {object} [params.req] - The request object (if applicable).
* @param {boolean} [params.temporary] - Whether the user is a temporary user.
* @param {boolean} [params.oidc_only] - Whether the user created with OIDC
* @param {boolean} [params.send_confirmation_code] - Whether to send a confirmation code instead of a token by email
* @param {string|null} params.username - The username of the user.
* @param {string|null} params.email - The email of the user.
* @param {string|null} params.password - The password of the user.
* @returns {Promise<OutcomeObject<CreatedUserOutcome>>} The outcome of the user creation.
*/
async create_new_user ({
req,
temporary = false,
oidc_only = false,
send_confirmation_code = false,
username = null,
email = null,
password = null,
}) {
const outcome = new OutcomeObject(new CreatedUserOutcome());
let raw_email = email;
if ( ! username ) {
throw new TypeError('username is a required parameter of create_new_user');
}
if ( !temporary && !validate_nonEmpty_string(email) ) {
throw new TypeError('email is a required parameter of create_new_user');
}
// Temp users get default values; they cannot have emails or passwords
if ( temporary ) {
username = username ?? await generate_random_username();
email = email ?? `${username}@nonexis.com`;
password = 'login-is-not-enabled'; // arbitrary, but accurate
}
// Some installations of Puter are configured to disable
// signup or temporary users. In these cases, we will specify
// a failure message and abort creating a user.
{
const svc_featureFlag = this.services.get('feature-flag');
const is_temp_users_disabled =
await svc_featureFlag.check('temp-users-disabled');
const is_user_signup_disabled =
await svc_featureFlag.check('user-signup-disabled');
if ( is_user_signup_disabled && is_temp_users_disabled ) {
return outcome.fail(
'User signup and Temporary users are disabled.',
'signup.signup_and_temp_users_disabled',
);
}
if ( temporary && is_temp_users_disabled ) {
return outcome.fail(
'Temporary users are disabled.',
'signup.temp_users_disabled',
);
}
if ( !temporary && is_user_signup_disabled ) {
return outcome.fail(
'User signup is disabled.',
'signup.user_signup_disabled',
);
}
}
// Emit the `puter.signup` event
// NOTICE: conditional early return
{
const svc_event = this.services.get('event');
const event = { allow: true, outcome };
if ( req ) {
event.ip = req.headers?.['x-forwarded-for'] ||
req.connection?.remoteAddress;
event.user_agent = req.headers?.['user-agent'];
event.body = req.body;
}
await svc_event.emit('puter.signup', event);
if ( ! event.allow ) {
outcome.log('disallowed by a puter.signup listener');
return outcome;
}
}
if ( await username_exists(username) ) {
return outcome.fail(
'Username already exists',
'username_already_exists',
);
}
// These checks are required for non-temporary users
if ( ! temporary ) {
const db = this.services.get('database').get(DB_WRITE, 'create-user:not-temp-checks');
const svc_cleanEmail = this.services.get('clean-email');
raw_email = email;
if ( ! email ) {
return outcome.fail(
'An email address is required',
'email_required',
);
}
email = svc_cleanEmail.clean(email);
if ( ! await svc_cleanEmail.validate(email) ) {
return outcome.fail(
'This email does not seem to be valid',
'email_invalid',
);
}
let rows2 = await db.read(`SELECT EXISTS(
SELECT 1 FROM user WHERE (email=? OR clean_email=?) AND email_confirmed=1 AND password IS NOT NULL
) AS email_exists`, [raw_email, email]);
if ( rows2[0].email_exists )
{
return outcome.fail(
'Email is already verified for another account',
'email_already_exists',
);
}
}
// TODO: this is where referral goes. We might drop
// referral, so I'm leaving it out here for now.
const user_uuid = uuidv4();
const email_confirm_token = uuidv4();
// TODO: `Math.random()` is not crypto-secure
const email_confirm_code = `${Math.floor(100000 + Math.random() * 900000)}`;
const audit_metadata = {};
if ( req ) {
audit_metadata.ip = req.connection.remoteAddress;
audit_metadata.ip_fwd = req.headers['x-forwarded-for'];
audit_metadata.user_agent = req.headers['user-agent'];
audit_metadata.origin = req.headers['origin'];
audit_metadata.server = this.global_config.server_id;
}
{
const db = this.services.get('database').get(DB_WRITE, 'create-user:main-insert');
const insert_res = await db.write(`INSERT INTO user
(
username, email, clean_email, password, uuid, referrer,
email_confirm_code, email_confirm_token, free_storage,
referred_by, audit_metadata, signup_ip, signup_ip_forwarded,
signup_user_agent, signup_origin, signup_server
)
VALUES
(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[
// username
username,
// email
temporary ? null : raw_email,
// normalized email
temporary ? null : email,
// password
(temporary || oidc_only) ? null : await bcrypt.hash(password, 8),
// uuid
user_uuid,
// referrer
req?.body?.referrer ?? null,
// email_confirm_code
email_confirm_code,
// email_confirm_token
email_confirm_token,
// free_storage
this.global_config.storage_capacity,
// referred_by
// TODO: we might remove referalls so I'mm leaving out
// the value for the `referred_by` field for now
null,
// audit_metadata
JSON.stringify(audit_metadata),
// signup_ip
req?.connection?.remoteAddress ?? null,
// signup_ip_fwd
req?.headers?.['x-forwarded-for'] ?? null,
// signup_user_agent
req?.headers?.['user-agent'] ?? null,
// signup_origin
req?.headers?.['origin'] ?? null,
// signup_server
this.global_config.server_id ?? null,
]);
// record activity (asynchronously)
db.write(
'UPDATE `user` SET `last_activity_ts` = now() WHERE id=? LIMIT 1',
[insert_res.insertId],
);
// TODO: it would be VERY NICE if this was a calculated
// group membership instead of something we store in the DB
const svc_group = this.services.get('group');
await svc_group.add_users({
uid: temporary
? this.global_config.default_temp_group
: this.global_config.default_user_group,
users: [username],
});
const user_id = insert_res.insertId;
outcome.infoObject.user_id = user_id;
const [user] = await db.pread(
'SELECT * FROM `user` WHERE `id` = ? LIMIT 1',
[user_id]);
// TODO(???): should user login happen here or by caller?
{
// const { token } = await svc_auth.create_session_token(user, {
// req,
// });
}
if ( send_confirmation_code ) {
send_email_verification_code(email_confirm_code, email);
} else {
send_email_verification_token(email_confirm_token, email, user_uuid);
}
// TODO: This is where sending the referral code would
// usually happen but we might remove referral so I'm
// leaving it out for now.
const svc_user = this.services.get('user');
await svc_user.generate_default_fsentries({ user });
// NOTE: `res.cookie` happens here in @signup.js but this
// should be handled by the caller over here.
{
const svc_event = this.services.get('event');
svc_event.emit('user.save_account', { user });
}
return outcome.success();
}
}
}
@@ -58,7 +58,7 @@ class BaseDatabaseAccessService extends BaseService {
*
* @returns {BaseDatabaseAccessService} The current instance of the service.
*/
get () {
get (_accessLevel, _scope) {
return this;
}
@@ -74,7 +74,7 @@ class UserProtectedEndpointsService extends BaseService {
// Require authenticated session
router.use(configurable_auth({ no_options_auth: true }));
// Only allow user sessions, not API tokens for apps
// Only allow user sessions with HTTP powers (session token), not GUI tokens or API tokens
router.use((req, res, next) => {
if ( req.method === 'OPTIONS' ) return next();
@@ -82,6 +82,9 @@ class UserProtectedEndpointsService extends BaseService {
if ( ! (actor.type instanceof UserActorType) ) {
return APIError.create('user_tokens_only').write(res);
}
if ( ! actor.type.hasHttpPowers ) {
return APIError.create('session_required').write(res);
}
next();
});
@@ -97,7 +100,7 @@ class UserProtectedEndpointsService extends BaseService {
router.use(async (req, res, next) => {
if ( req.method === 'OPTIONS' ) return next();
if ( req.user.password === null ) {
if ( req.user.password === null && req.user.email === null ) {
return APIError.create('temporary_account').write(res);
}
next();
+32
View File
@@ -0,0 +1,32 @@
export class OutcomeObject {
userMessage = null;
userMessageKey = null;
userMessageFields = {};
failed = false;
messages = [];
fields = {};
ended = false;
infoObject;
constructor (infoObject) {
this.failed = true;
this.userMessageFields = {};
this.infoObject = infoObject;
}
log (text, fields) {
this.messages.push({ text, fields });
}
fail (message, i18nKey, fields = {}) {
this.userMessage = message;
this.userMessageKey = i18nKey;
this.userMessageFields = fields;
this.ended = true;
this.failed = true;
return this;
}
success () {
this.ended = true;
this.failed = false;
return this;
}
}
//# sourceMappingURL=outcomeutil.js.map
+77
View File
@@ -0,0 +1,77 @@
/**
* Represents the outcome of a task that might fail or succeed.
*/
export class OutcomeObject<T> {
/**
* If the task was not successful, this will be the message a user
* sees.
*/
userMessage = null;
/**
* If the task was not successful, this will be the i18n key for
* the message a user sees.
*/
userMessageKey = null;
/**
* If the task was not successful, this will be values used for
* a message template that is identified using `userMessageKey`.
*/
userMessageFields = {};
/**
* If the task being performed failed
*/
failed = false;
messages: Record<string, string>[] = [];
fields = {};
/**
* Whether the task being performed has ended,
* either successfully or unsuccessfully.
*/
ended = false;
infoObject: T;
constructor (infoObject: T) {
this.failed = true;
this.userMessageFields = {};
this.infoObject = infoObject;
}
log (text, fields) {
this.messages.push({ text, fields });
}
/**
* Records a failure message.
* Returns the outcome object for chaining with a return statement.
*
* @example
* return outcome.fail(
* 'User already exists',
* 'signup.user_already_exists',
* { username: 'john_doe' }
* );
*
* @param {*} message - message the user sees without i18n
* @param {*} i18nKey - i18n key for the message
* @param {*} fields - fields for i18n-key-identified template
*/
fail (message, i18nKey, fields = {}) {
this.userMessage = message;
this.userMessageKey = i18nKey;
this.userMessageFields = fields;
this.ended = true;
this.failed = true;
return this;
}
success () {
this.ended = true;
this.failed = false;
return this;
}
}
+14 -3
View File
@@ -1,4 +1,4 @@
const APIError = require("../api/APIError");
const APIError = require('../api/APIError');
/*
* Copyright (C) 2024-present Puter Technologies Inc.
@@ -31,7 +31,7 @@ const valid_file_size = v => {
const validate_fields = (fields, values) => {
// First, check for missing fields (undefined)
const missing_fields = Object.keys(fields).filter(field => ! fields[field].optional && values[field] === undefined);
const missing_fields = Object.keys(fields).filter(field => !fields[field].optional && values[field] === undefined);
if ( missing_fields.length > 0 ) {
throw APIError.create('fields_missing', null, { keys: missing_fields });
}
@@ -54,9 +54,20 @@ const validate_fields = (fields, values) => {
})),
});
}
}
};
const validate_nonEmpty_string = value => {
if ( typeof value !== 'string' ) {
return false;
}
if ( value.length === 0 ) {
return false;
}
return true;
};
module.exports = {
valid_file_size,
validate_nonEmpty_string,
validate_fields,
};
@@ -108,12 +108,12 @@ async function UIWindowChangeEmail (options) {
$(el_window).find('.new-email').attr('disabled', true);
$.ajax({
url: `${window.api_origin }/user-protected/change-email`,
url: `${window.gui_origin}/user-protected/change-email`,
type: 'POST',
async: true,
headers: {
'Authorization': `Bearer ${window.auth_token}`,
},
// headers: {
// 'Authorization': `Bearer ${window.auth_token}`,
// },
contentType: 'application/json',
data: JSON.stringify({
new_email: new_email,
+2 -4
View File
@@ -154,15 +154,13 @@ async function UIWindowLogin (options) {
(async () => {
try {
const origin = window.gui_origin || window.location.origin;
const res = await fetch(`${origin}/auth/oidc/providers`);
const res = await fetch(`${window.api_origin}/auth/oidc/providers`);
if ( ! res.ok ) return;
const data = await res.json();
if ( data.providers && data.providers.includes('google') ) {
$(el_window).find('.oidc-providers-wrapper').show();
$(el_window).find('.oidc-google-btn').on('click', function () {
const redirectUri = encodeURIComponent(window.location.origin + (window.location.pathname || '/'));
window.location.href = `${origin}/auth/oidc/google/start?redirect_uri=${redirectUri}`;
window.location.href = `${window.gui_origin}/auth/oidc/google/start?flow=login`;
});
}
} catch (_) {
+2 -4
View File
@@ -162,15 +162,13 @@ function UIWindowSignup (options) {
(async () => {
try {
const origin = window.gui_origin || window.location.origin;
const res = await fetch(`${origin}/auth/oidc/providers`);
const res = await fetch(`${window.api_origin}/auth/oidc/providers`);
if ( ! res.ok ) return;
const data = await res.json();
if ( data.providers && data.providers.includes('google') ) {
$(el_window).find('.oidc-providers-wrapper').show();
$(el_window).find('.oidc-google-btn').on('click', function () {
const redirectUri = encodeURIComponent(window.location.origin + (window.location.pathname || '/'));
window.location.href = `${origin}/auth/oidc/google/start?redirect_uri=${redirectUri}`;
window.location.href = `${window.gui_origin}/auth/oidc/google/start?flow=signup`;
});
}
} catch (_) {
+18 -3
View File
@@ -17,11 +17,14 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import UIDashboard from './UI/Dashboard/UIDashboard.js';
import UIAlert from './UI/UIAlert.js';
import UIComponentWindow from './UI/UIComponentWindow.js';
import UIDesktop from './UI/UIDesktop.js';
import UIWindow from './UI/UIWindow.js';
import UIWindowAuthMe from './UI/UIWindowAuthMe.js';
import UIWindowChangeUsername from './UI/UIWindowChangeUsername.js';
import UIWindowCopyToken from './UI/UIWindowCopyToken.js';
import UIWindowEmailConfirmationRequired from './UI/UIWindowEmailConfirmationRequired.js';
import UIWindowLogin from './UI/UIWindowLogin.js';
import UIWindowLoginInProgress from './UI/UIWindowLoginInProgress.js';
@@ -30,8 +33,6 @@ import UIWindowRequestPermission from './UI/UIWindowRequestPermission.js';
import UIWindowSaveAccount from './UI/UIWindowSaveAccount.js';
import UIWindowSessionList from './UI/UIWindowSessionList.js';
import UIWindowSignup from './UI/UIWindowSignup.js';
import UIWindowCopyToken from './UI/UIWindowCopyToken.js';
import UIWindowAuthMe from './UI/UIWindowAuthMe.js';
import { PROCESS_RUNNING } from './definitions.js';
import item_icon from './helpers/item_icon.js';
import update_last_touch_coordinates from './helpers/update_last_touch_coordinates.js';
@@ -49,7 +50,6 @@ import { ProcessService } from './services/ProcessService.js';
import { SettingsService } from './services/SettingsService.js';
import { ThemeService } from './services/ThemeService.js';
import { privacy_aware_path } from './util/desktop.js';
import UIDashboard from './UI/Dashboard/UIDashboard.js';
const launch_services = async function (options) {
// === Services Data Structures ===
@@ -393,6 +393,21 @@ window.initgui = async function (options) {
// Launch services before any UI is rendered
await launch_services(options);
// If no token in storage but we have a session cookie (e.g. after OIDC redirect), fetch GUI token
if ( !localStorage.getItem('auth_token') && window.auth_token == null ) {
try {
const r = await fetch(`${window.gui_origin}/get-gui-token`, { credentials: 'include' });
if ( r.ok ) {
const { token } = await r.json();
window.auth_token = token;
localStorage.setItem('auth_token', token);
if ( typeof puter !== 'undefined' ) puter.setAuthToken(token, window.api_origin);
}
} catch (e) {
// ignore
}
}
//--------------------------------------------------------------------------------------
// Is attempt_temp_user_creation?
// i.e. https://puter.com/?attempt_temp_user_creation=true