Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ NODE_ENV=development
VERSION=1.0.0
APP_NAME=Seamless Auth Example
APP_ID=local-dev
APP_ORIGIN=http://localhost:5001
APP_ORIGIN=http://localhost:3000
ISSUER=http://localhost:5312

# "web" for website to auth server, "server" for api server to auth server auth
Expand Down Expand Up @@ -40,4 +40,11 @@ JWKS_ACTIVE_KID=dev-main

# WEBAUTHN
RPID=localhost
ORIGINS=http://localhost:5001
ORIGINS=http://localhost:5173,http://localhost:5174

# ADMIN BOOTSTRAP
# Enables bootstrap feature
SEAMLESS_BOOTSTRAP_ENABLED=true

# Secret used to authorize bootstrap invite creation
SEAMLESS_BOOTSTRAP_SECRET=dev-bootstrap-secret-123
15 changes: 2 additions & 13 deletions src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,26 +21,15 @@ import getLogger from './utils/logger.js';
const logger = getLogger('app');
const app = express();

const isValidUrl = (str: string) => {
try {
if (str === '*') return true;
new URL(str);
return true;
} catch {
throw new Error('Invalid host provied.');
}
};

const rawOrigin = process.env.APP_ORIGIN?.trim();
const allowedOrigin = rawOrigin && isValidUrl(rawOrigin) ? rawOrigin : '';
const rawOrigin = process.env.APP_ORIGINS!.split(',');

const corsOptions: CorsOptions = {
origin: (origin, callback) => {
if (!origin) {
return callback(null, true);
}

if (origin === allowedOrigin || origin === 'http://localhost:5174') {
if (rawOrigin.includes(origin)) {
return callback(null, true);
}

Expand Down
73 changes: 73 additions & 0 deletions src/controllers/bootstrap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/*
* Copyright © 2026 Fells Code, LLC
* Licensed under the GNU Affero General Public License v3.0
* See LICENSE file in the project root for full license information
*/

import { Request, Response } from 'express';

import {
assertBootstrapAllowed,
assertBootstrapSecret,
BootstrapError,
createAdminBootstrapInvite,
} from '../services/bootstrapService.js';
import getLogger from '../utils/logger.js';

const logger = getLogger('bootstrapAdminInvite');

function getBearerToken(req: Request): string | undefined {
const auth = req.header('authorization');
if (!auth) return undefined;

const [scheme, token] = auth.split(' ');
if (scheme?.toLowerCase() !== 'bearer') return undefined;

return token;
}

export async function createAdminBootstrapInviteHandler(req: Request, res: Response) {
try {
logger.info('Creating a bootstrap admin invitation');

const bearerToken = getBearerToken(req);

assertBootstrapSecret(bearerToken);
await assertBootstrapAllowed();

const { email } = req.body;

const result = await createAdminBootstrapInvite({
email,
createdIp: req.ip ?? null,
createdUserAgent: req.get('user-agent') ?? null,
});

return res.status(201).json({
success: true,
data: {
url: result.registrationUrl,
expiresAt: result.expiresAt.toISOString(),
token: result.token,
},
});
} catch (error) {
if (error instanceof BootstrapError) {
return res.status(error.status).json({
success: false,
error: {
code: error.code,
message: error.message,
},
});
}

return res.status(500).json({
success: false,
error: {
code: 'BOOTSTRAP_INTERNAL_ERROR',
message: 'An unexpected error occurred.',
},
});
}
}
94 changes: 38 additions & 56 deletions src/controllers/magicLinks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,22 +9,16 @@ import { Request, Response } from 'express';
import { Op } from 'sequelize';

import { getSystemConfig } from '../config/getSystemConfig.js';
import { setAuthCookies } from '../lib/cookie.js';
import { generateRefreshToken, hashRefreshToken, signAccessToken } from '../lib/token.js';
import { AuthEvent } from '../models/authEvents.js';
import { MagicLinkToken } from '../models/magicLinks.js';
import { Session } from '../models/sessions.js';
import { User } from '../models/users.js';
import { AuthEventService } from '../services/authEventService.js';
import { maybePromoteBootstrapAdmin } from '../services/bootstrapPromotionService.js';
import { sendMagicLinkEmail } from '../services/messagingService.js';
import { issueSessionAndRespond } from '../services/sessionIssuance.js';
import { AuthenticatedRequest } from '../types/types.js';
import getLogger from '../utils/logger.js';
import {
computeSessionTimes,
hashDeviceFingerprint,
hashSha256,
parseDurationToSeconds,
} from '../utils/utils.js';
import { hashDeviceFingerprint, hashSha256 } from '../utils/utils.js';

const logger = getLogger('magic-links');

Expand Down Expand Up @@ -193,59 +187,47 @@ export async function pollMagicLinkConfirmation(req: Request, res: Response) {
req,
});

const refreshToken = generateRefreshToken();
const refreshTokenHash = await hashRefreshToken(refreshToken);
const { expiresAt, idleExpiresAt } = computeSessionTimes();

const session = await Session.create({
userId: user.id,
infraId: process.env.APP_ID!,
mode: AUTH_MODE,
refreshTokenHash,
userAgent: req.get('user-agent'),
ipAddress: req.ip,
expiresAt,
idleExpiresAt,
lastUsedAt: undefined,
});

const token = await signAccessToken(session.id, user.id, user.roles);

user.challenge = '';
user.verified = true;

await user.save();

if (token && refreshToken) {
await AuthEvent.create({
user_id: user.id,
type: 'registration_success',
ip_address: req.ip,
user_agent: req.headers['user-agent'],
metadata: {},
});

if (AUTH_MODE === 'web') {
await setAuthCookies(res, { accessToken: token, refreshToken });
res.status(200).json({ message: 'Success' });
return;
}

const { access_token_ttl, refresh_token_ttl } = await getSystemConfig();

return res.status(200).json({
message: 'Success',
token,
refreshToken,
sub: user.id,
roles: user.roles,
const bootstrapResult = await maybePromoteBootstrapAdmin({
user,
req,
completionMethod: 'magic_link_fallback',
});

if (bootstrapResult.promoted) {
logger.info(`Bootstrap admin granted to ${user.email}`);
}

await AuthEvent.create({
user_id: user.id,
type: 'registration_success',
ip_address: req.ip,
user_agent: req.headers['user-agent'],
metadata: {},
});

await issueSessionAndRespond({
user: {
id: user.id,
email: user.email,
phone: user.phone,
ttl: parseDurationToSeconds(access_token_ttl || '15m'),
refreshTtl: parseDurationToSeconds(refresh_token_ttl || '1h'),
});
}
}
roles: user.roles ?? [],
},
req,
res,
authMode: AUTH_MODE,
clearBootstrap: true,
});

user.update({
lastLogin: new Date(),
});

return res.status(204).json({ message: 'Not verified.' });
return;
}
return res.status(204).json({ message: 'Success' });
}
Loading
Loading