-
Notifications
You must be signed in to change notification settings - Fork 15
/
Copy path+server.ts
196 lines (160 loc) · 5.52 KB
/
+server.ts
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
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
import { getAuthSessionIdFromCookie, setJwtCookie } from '$lib/server/auth/utilities';
import {
type VerifiedAuthenticationResponse,
verifyAuthenticationResponse,
} from '@simplewebauthn/server';
import type { AuthenticationResponseJSON } from '@simplewebauthn/types';
import { type Cookies, error, json, type RequestHandler } from '@sveltejs/kit';
import {
type Authenticator,
findAuthenticatorByIdentifier,
updateAuthenticator,
} from '$lib/server/data/authentication/authenticator';
import {
deleteChallenges,
resolveCurrentChallenge,
} from '$lib/server/data/authentication/challenge';
import jwt from 'jsonwebtoken';
import { env } from '$env/dynamic/private';
import { NoResultError } from 'kysely';
import { resolvePreviousLocation } from '$lib/server/utilities';
import type { Client } from '$lib/server/database';
import { decodeFromBase64 } from '$lib/utilities';
export const POST = async function handler({ url, request, cookies, locals: { database } }) {
// region Resolve Authentication Session
const sessionId = getAuthSessionIdFromCookie(cookies);
if (!sessionId) {
throw error(401, 'Not authenticated');
}
// endregion
// region Resolve current Challenge
let challenge: string;
try {
challenge = await resolveCurrentChallenge(database, sessionId);
} catch (err) {
if (!(err instanceof Error)) {
throw err;
}
throw error(400, `Failed to resolve challenge: ${err.message}`);
}
// endregion
// region Fetch and validate the response
let response: AuthenticationResponseJSON;
try {
response = (await request.json()) as AuthenticationResponseJSON;
} catch (err) {
if (!(err instanceof Error)) {
throw err;
}
await deleteChallenges(database, sessionId);
return error(400, `Invalid request body: ${err.message}`);
}
const userId = response.response.userHandle;
if (!userId) {
await deleteChallenges(database, sessionId);
return error(400, `Invalid payload: Missing user handle`);
}
// endregion
// region Resolve Authenticator
let authenticator: Authenticator;
try {
authenticator = await resolveAuthenticator(database, response, userId);
} catch (reason) {
const { message } = reason instanceof Error ? reason : { message: 'Invalid authenticator' };
await deleteChallenges(database, sessionId);
return error(400, { message });
}
// endregion
// region Verify the authentication response
let verificationResponse: VerifiedAuthenticationResponse;
try {
verificationResponse = await verify(url, response, challenge, authenticator);
} catch (cause) {
const { message } = cause instanceof Error ? cause : { message: 'Failed to verify response' };
await deleteChallenges(database, sessionId);
return error(400, { message });
}
const {
verified,
authenticationInfo: { newCounter: counter },
} = verificationResponse;
if (verified) {
// Update the authenticator's counter in the DB to the newest count in the authentication
await updateAuthenticator(database, response.rawId, {
counter: counter.toString(),
last_used_at: new Date(),
});
}
await deleteChallenges(database, sessionId);
// endregion
// region Issue Access Token
issueAccessToken(cookies, authenticator);
// endregion
// region Resolve previous URL and generate response
const destination = resolvePreviousLocation(cookies, url, '/');
return json({ verified, destination });
// endregion
} satisfies RequestHandler;
async function resolveAuthenticator(
database: Client,
response: AuthenticationResponseJSON,
userId: string,
) {
let authenticator: Authenticator | null;
try {
authenticator = await findAuthenticatorByIdentifier(database, response.rawId);
} catch (cause) {
if (!(cause instanceof NoResultError)) {
throw cause;
}
authenticator = null;
}
if (!authenticator || authenticator.user_id !== userId) {
throw new Error('Authenticator is not registered with this site');
}
return authenticator;
}
async function verify(
url: URL,
response: AuthenticationResponseJSON,
challenge: string,
{ counter, identifier, public_key, transports }: Authenticator,
) {
try {
return await verifyAuthenticationResponse({
response,
requireUserVerification: true,
expectedChallenge: `${challenge}`,
expectedOrigin: url.origin, // <-- TODO: Use origin from RP ID instead
expectedRPID: url.hostname, // <-- TODO: Use hostname from env instead
authenticator: {
credentialPublicKey: decodeFromBase64(public_key),
credentialID: decodeFromBase64(identifier),
counter: Number(counter),
transports,
},
});
} catch (cause) {
if (!(cause instanceof Error)) {
throw cause;
}
throw new Error(`Failed to verify authentication response: ${cause.message}`, {
cause,
});
}
}
function issueAccessToken(cookies: Cookies, { id: authenticator, user_id }: Authenticator) {
console.error('Unexpected token issue', {authenticator, user_id})
// Sign the user token: We have authenticated the user successfully using the passcode, so they
// may use this JWT to create their pass *key*.
const token = jwt.sign({ authenticator }, env.JWT_SECRET, {
subject: user_id,
});
// Set the cookie on the response: It will be included in any requests to the server, including
// for tRPC. This makes for a nice, transparent, and "just works" authentication scheme.
setJwtCookie(cookies, token);
}
export type VerificationResponse = {
verified: boolean;
destination: string;
};