Skip to content

Commit b5928d2

Browse files
committed
chore: logging, remove bootstrap TTL and updated .env examples
1 parent 7e83857 commit b5928d2

9 files changed

Lines changed: 31 additions & 56 deletions

File tree

.env.example

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ NODE_ENV=development
55
VERSION=1.0.0
66
APP_NAME=Seamless Auth Example
77
APP_ID=local-dev
8-
APP_ORIGIN=http://localhost:5001
8+
APP_ORIGIN=http://localhost:3000
99
ISSUER=http://localhost:5312
1010

1111
# "web" for website to auth server, "server" for api server to auth server auth
@@ -40,14 +40,11 @@ JWKS_ACTIVE_KID=dev-main
4040

4141
# WEBAUTHN
4242
RPID=localhost
43-
ORIGINS=http://localhost:5001
43+
ORIGINS=http://localhost:5173,http://localhost:5174
4444

4545
# ADMIN BOOTSTRAP
4646
# Enables bootstrap feature
4747
SEAMLESS_BOOTSTRAP_ENABLED=true
4848

4949
# Secret used to authorize bootstrap invite creation
50-
SEAMLESS_BOOTSTRAP_SECRET=dev-bootstrap-secret-123
51-
52-
# How long the invite (and cookie) is valid
53-
SEAMLESS_BOOTSTRAP_TTL_MINUTES=15
50+
SEAMLESS_BOOTSTRAP_SECRET=dev-bootstrap-secret-123

src/app.ts

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -21,26 +21,15 @@ import getLogger from './utils/logger.js';
2121
const logger = getLogger('app');
2222
const app = express();
2323

24-
const isValidUrl = (str: string) => {
25-
try {
26-
if (str === '*') return true;
27-
new URL(str);
28-
return true;
29-
} catch {
30-
throw new Error('Invalid host provied.');
31-
}
32-
};
33-
34-
const rawOrigin = process.env.APP_ORIGIN?.trim();
35-
const allowedOrigin = rawOrigin && isValidUrl(rawOrigin) ? rawOrigin : '';
24+
const rawOrigin = process.env.APP_ORIGINS!.split(',');
3625

3726
const corsOptions: CorsOptions = {
3827
origin: (origin, callback) => {
3928
if (!origin) {
4029
return callback(null, true);
4130
}
4231

43-
if (origin === allowedOrigin || origin === 'http://localhost:5174') {
32+
if (rawOrigin.includes(origin)) {
4433
return callback(null, true);
4534
}
4635

src/controllers/bootstrap.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ import {
1212
BootstrapError,
1313
createAdminBootstrapInvite,
1414
} from '../services/bootstrapService.js';
15+
import getLogger from '../utils/logger.js';
16+
17+
const logger = getLogger('bootstrapAdminInvite');
1518

1619
function getBearerToken(req: Request): string | undefined {
1720
const auth = req.header('authorization');
@@ -25,6 +28,8 @@ function getBearerToken(req: Request): string | undefined {
2528

2629
export async function createAdminBootstrapInviteHandler(req: Request, res: Response) {
2730
try {
31+
logger.info('Creating a bootstrap admin invitation');
32+
2833
const bearerToken = getBearerToken(req);
2934

3035
assertBootstrapSecret(bearerToken);

src/lib/bootstrapCookie.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,23 +6,31 @@
66

77
import { Request, Response } from 'express';
88

9+
import getLogger from '../utils/logger.js';
10+
911
const COOKIE_NAME = 'seamless_bootstrap_token';
1012

13+
const logger = getLogger('bootstrapCookie');
14+
1115
export function setBootstrapCookie(res: Response, token: string) {
1216
res.cookie(COOKIE_NAME, token, {
1317
httpOnly: true,
1418
secure: process.env.NODE_ENV === 'production',
1519
sameSite: process.env.NODE_ENV === 'production' ? 'none' : 'lax',
16-
maxAge: 15 * 60 * 1000, // 15 minutes
20+
maxAge: 15 * 60 * 1000,
1721
path: '/',
1822
});
1923
}
2024

2125
export function getBootstrapCookie(req: Request): string | null {
26+
logger.debug(
27+
`Checking for bootstrap cookie. Cookie value: ${req.cookies?.[COOKIE_NAME] ?? null}`,
28+
);
2229
return req.cookies?.[COOKIE_NAME] ?? null;
2330
}
2431

2532
export function clearBootstrapCookie(res: Response) {
33+
logger.debug(`Clearing bootstrap cookie.`);
2634
res.clearCookie(COOKIE_NAME, {
2735
httpOnly: true,
2836
secure: process.env.NODE_ENV === 'production',

src/middleware/verifyBearerAuth.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ const logger = getLogger('verifyBearerAuth');
1515
export async function verifyBearerAuth(req: Request, res: Response, next: NextFunction) {
1616
const auth = req.headers.authorization;
1717
if (!auth?.startsWith('Bearer ')) {
18-
logger.error('Missing beartoken for authentication request');
18+
logger.error('Missing bearer token for authentication request');
1919
return res.status(401).json({ error: 'missing bearer token' });
2020
}
2121

src/services/bootstrapPromotionService.ts

Lines changed: 6 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ export async function maybePromoteBootstrapAdmin(params: {
6666
logger.debug('checking for promotion');
6767

6868
async function logSkip(reason: string) {
69+
logger.info(`Skipped bootstrap for ${reason}`);
6970
await AuthEventService.log({
7071
userId: user.id,
7172
type: 'bootstrap_admin_check_skipped',
@@ -75,30 +76,18 @@ export async function maybePromoteBootstrapAdmin(params: {
7576
}
7677

7778
if (!isBootstrapEnabled()) {
78-
logger.info('Bootstrap is not enabled');
79-
await AuthEventService.log({
80-
userId: user.id,
81-
type: 'bootstrap_admin_check_skipped',
82-
req,
83-
metadata: { reason: 'bootstrap disabled', completionMethod },
84-
});
79+
logSkip('disabled');
8580
return { promoted: false, reason: 'bootstrap_disabled' };
8681
}
8782

8883
if (userHasAdminRole(user)) {
89-
logger.info('User is already and admin');
90-
await AuthEventService.log({
91-
userId: user.id,
92-
type: 'bootstrap_admin_check_skipped',
93-
req,
94-
metadata: { reason: 'User was already an admin', completionMethod },
95-
});
84+
logSkip('bootstrap_admin_check_skipped');
9685
return { promoted: false, reason: 'already_admin' };
9786
}
9887

9988
const rawToken = getBootstrapCookie(req);
10089
if (!rawToken) {
101-
logger.info('Missing token');
90+
logSkip('Missing token');
10291
return { promoted: false, reason: 'missing_token' };
10392
}
10493

@@ -168,6 +157,8 @@ export async function maybePromoteBootstrapAdmin(params: {
168157
},
169158
});
170159

160+
logger.info('User promoted to admin');
161+
171162
return { promoted: true, reason: 'success' } as PromotionResult;
172163
});
173164
}

src/services/bootstrapService.ts

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ import getLogger from '../utils/logger.js';
1414
import { sendBootstrapEmail } from './messagingService.js';
1515

1616
const logger = getLogger('adminBootstrapService');
17-
const DEFAULT_BOOTSTRAP_TTL_MINUTES = 15;
1817

1918
export class BootstrapError extends Error {
2019
code: string;
@@ -27,11 +26,6 @@ export class BootstrapError extends Error {
2726
}
2827
}
2928

30-
function getBootstrapTtlMinutes(): number {
31-
const raw = Number(process.env.SEAMLESS_BOOTSTRAP_TTL_MINUTES ?? DEFAULT_BOOTSTRAP_TTL_MINUTES);
32-
return Number.isFinite(raw) && raw > 0 ? raw : DEFAULT_BOOTSTRAP_TTL_MINUTES;
33-
}
34-
3529
export function isBootstrapEnabled(): boolean {
3630
return process.env.SEAMLESS_BOOTSTRAP_ENABLED === 'true';
3731
}
@@ -50,6 +44,7 @@ export function getBootstrapSecret(): string {
5044

5145
export function assertBootstrapSecret(provided: string | undefined): void {
5246
if (!provided) {
47+
logger.error('Nothing provided for bootstrap secret');
5348
throw new BootstrapError('BOOTSTRAP_UNAUTHORIZED', 'Unauthorized.', 401);
5449
}
5550

@@ -62,6 +57,7 @@ export function assertBootstrapSecret(provided: string | undefined): void {
6257
providedBuf.length !== expectedBuf.length ||
6358
!crypto.timingSafeEqual(providedBuf, expectedBuf)
6459
) {
60+
logger.error('Incorrect bootstrap secret');
6561
throw new BootstrapError('BOOTSTRAP_UNAUTHORIZED', 'Unauthorized.', 401);
6662
}
6763
}
@@ -120,9 +116,7 @@ export async function createAdminBootstrapInvite(params: {
120116

121117
const rawToken = generateRawToken();
122118
const tokenHash = hashToken(rawToken);
123-
124-
const ttlMinutes = getBootstrapTtlMinutes();
125-
const expiresAt = new Date(Date.now() + ttlMinutes * 60 * 1000);
119+
const expiresAt = new Date(Date.now() + 15 * 60 * 1000);
126120

127121
await BootstrapInvite.create({
128122
email: params.email.toLowerCase(),

tests/setup/mocks.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { vi } from 'vitest';
22

3+
vi.stubEnv('APP_ORIGINS', 'http://localhost:5137');
4+
35
export let mockUser: any = {
46
id: 'user-1',
57
email: 'test@example.com',

tests/unit/services/bootstrap.spec.ts

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,6 @@ beforeEach(() => {
4848

4949
process.env.SEAMLESS_BOOTSTRAP_ENABLED = 'true';
5050
process.env.SEAMLESS_BOOTSTRAP_SECRET = 'test-secret';
51-
process.env.SEAMLESS_BOOTSTRAP_TTL_MINUTES = '15';
5251
process.env.NODE_ENV = 'test';
5352

5453
(User.count as any).mockResolvedValue(0);
@@ -150,16 +149,6 @@ it('stores email in lowercase', async () => {
150149
);
151150
});
152151

153-
it('falls back to default TTL when invalid env', async () => {
154-
process.env.SEAMLESS_BOOTSTRAP_TTL_MINUTES = 'invalid';
155-
156-
const result = await createAdminBootstrapInvite({
157-
email: 'test@example.com',
158-
});
159-
160-
expect(result.expiresAt).toBeInstanceOf(Date);
161-
});
162-
163152
it('hashes token consistently', () => {
164153
const hash1 = hashBootstrapToken('abc');
165154
const hash2 = hashBootstrapToken('abc');

0 commit comments

Comments
 (0)