-
-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathtoken.ts
More file actions
116 lines (96 loc) · 3.43 KB
/
Copy pathtoken.ts
File metadata and controls
116 lines (96 loc) · 3.43 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
/*
* 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 { hashSync } from 'bcrypt-ts';
import { createHmac, randomBytes } from 'crypto';
import { importPKCS8, SignJWT } from 'jose';
import { getSystemConfig } from '../config/getSystemConfig.js';
import getLogger from '../utils/logger.js';
import { getSigningKey } from '../utils/signingKeyStore.js';
const logger = getLogger('tokens');
// User tokens are signed with `aud` equal to ISSUER. The Seamless adapter verifies
// signed auth responses with `aud === audience`, and the deployment contract requires
// the adopter's `audience` to equal its `authServerUrl`, which is byte-identical to
// this ISSUER. Omitting `aud` makes jose reject every token the adapter checks.
const ISSUER = process.env.ISSUER!;
let warnedAboutDevLookupSecret = false;
function getRefreshTokenLookupSecret() {
const explicitSecret = process.env.REFRESH_TOKEN_LOOKUP_SECRET?.trim();
if (explicitSecret) {
return explicitSecret;
}
const apiServiceSecret = process.env.API_SERVICE_TOKEN?.trim();
if (apiServiceSecret) {
return apiServiceSecret;
}
if (process.env.NODE_ENV !== 'production') {
if (!warnedAboutDevLookupSecret) {
logger.warn(
'REFRESH_TOKEN_LOOKUP_SECRET is not set. Falling back to a development-only secret for refresh token lookup fingerprints.',
);
warnedAboutDevLookupSecret = true;
}
return `dev-refresh-lookup:${process.env.APP_ID ?? 'local'}:${ISSUER}`;
}
throw new Error(
'REFRESH_TOKEN_LOOKUP_SECRET (or API_SERVICE_TOKEN) must be set to derive refresh token lookup fingerprints in production.',
);
}
export async function signAccessToken(
sessionId: string,
userId: string,
roles?: string[],
organizationId?: string | null,
) {
const { kid, privateKeyPem } = await getSigningKey();
const privateKey = await importPKCS8(privateKeyPem, 'RS256');
const { access_token_ttl } = await getSystemConfig();
const jwt = await new SignJWT({
sid: sessionId,
sub: userId,
iss: process.env.ISSUER,
typ: 'access',
roles,
...(organizationId ? { org_id: organizationId } : {}),
})
.setProtectedHeader({ alg: 'RS256', kid })
.setIssuedAt()
.setIssuer(ISSUER)
.setAudience(ISSUER)
.setExpirationTime(access_token_ttl)
.sign(privateKey);
return jwt;
}
export async function signEphemeralToken(userId: string) {
try {
const { kid, privateKeyPem } = await getSigningKey();
const privateKey = await importPKCS8(privateKeyPem, 'RS256');
const jwt = await new SignJWT({
sub: userId,
iss: process.env.ISSUER,
typ: 'ephemeral',
})
.setProtectedHeader({ alg: 'RS256', kid })
.setIssuedAt()
.setIssuer(ISSUER)
.setAudience(ISSUER)
.setExpirationTime('5m')
.sign(privateKey);
return jwt;
} catch (error) {
logger.error(`Failed to create JWT token. Ephemeral. Reason: ${error}.`);
throw new Error('Failed to sign Ephemeral Token', { cause: error });
}
}
export function generateRefreshToken() {
return randomBytes(32).toString('base64url');
}
export async function hashRefreshToken(token: string) {
const saltRounds = 12;
return hashSync(token, saltRounds);
}
export function createRefreshTokenLookup(token: string) {
return createHmac('sha256', getRefreshTokenLookupSecret()).update(token).digest('hex');
}