-
Notifications
You must be signed in to change notification settings - Fork 326
/
Copy pathauthenticateRequest.ts
137 lines (116 loc) · 4.85 KB
/
authenticateRequest.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
import type { RequestState } from '@clerk/backend/internal';
import { AuthStatus, createClerkRequest } from '@clerk/backend/internal';
import { isDevelopmentFromSecretKey } from '@clerk/shared/keys';
import { isHttpOrHttps, isProxyUrlRelative, isValidProxyUrl } from '@clerk/shared/proxy';
import { handleValueOrFn } from '@clerk/shared/utils';
import type { RequestHandler, Response } from 'express';
import type { IncomingMessage } from 'http';
import { clerkClient as defaultClerkClient } from './clerkClient';
import { satelliteAndMissingProxyUrlAndDomain, satelliteAndMissingSignInUrl } from './errors';
import type { AuthenticateRequestParams, ClerkMiddlewareOptions, ExpressRequestWithAuth } from './types';
import { loadApiEnv, loadClientEnv } from './utils';
export const authenticateRequest = (opts: AuthenticateRequestParams) => {
const { clerkClient, request, options } = opts;
const { jwtKey, authorizedParties, audience } = options || {};
const clerkRequest = createClerkRequest(incomingMessageToRequest(request));
const env = { ...loadApiEnv(), ...loadClientEnv() };
const secretKey = options?.secretKey || env.secretKey;
const publishableKey = options?.publishableKey || env.publishableKey;
const isSatellite = handleValueOrFn(options?.isSatellite, clerkRequest.clerkUrl, env.isSatellite);
const domain = handleValueOrFn(options?.domain, clerkRequest.clerkUrl) || env.domain;
const signInUrl = options?.signInUrl || env.signInUrl;
const proxyUrl = absoluteProxyUrl(
handleValueOrFn(options?.proxyUrl, clerkRequest.clerkUrl, env.proxyUrl),
clerkRequest.clerkUrl.toString(),
);
if (isSatellite && !proxyUrl && !domain) {
throw new Error(satelliteAndMissingProxyUrlAndDomain);
}
if (isSatellite && !isHttpOrHttps(signInUrl) && isDevelopmentFromSecretKey(secretKey || '')) {
throw new Error(satelliteAndMissingSignInUrl);
}
return clerkClient.authenticateRequest(clerkRequest, {
audience,
secretKey,
publishableKey,
jwtKey,
authorizedParties,
proxyUrl,
isSatellite,
domain,
signInUrl,
});
};
const incomingMessageToRequest = (req: IncomingMessage): Request => {
const headers = Object.keys(req.headers).reduce((acc, key) => Object.assign(acc, { [key]: req?.headers[key] }), {});
// @ts-ignore Optimistic attempt to get the protocol in case
// req extends IncomingMessage in a useful way. No guarantee
// it'll work.
const protocol = req.connection?.encrypted ? 'https' : 'http';
const dummyOriginReqUrl = new URL(req.url || '', `${protocol}://clerk-dummy`);
return new Request(dummyOriginReqUrl, {
method: req.method,
headers: new Headers(headers),
});
};
const setResponseHeaders = (requestState: RequestState, res: Response): Error | undefined => {
if (requestState.headers) {
requestState.headers.forEach((value, key) => res.appendHeader(key, value));
}
return setResponseForHandshake(requestState, res);
};
/**
* Depending on the auth state of the request, handles applying redirects and validating that a handshake state was properly handled.
*
* Returns an error if state is handshake without a redirect, otherwise returns undefined. res.writableEnded should be checked after this method is called.
*/
const setResponseForHandshake = (requestState: RequestState, res: Response): Error | undefined => {
const hasLocationHeader = requestState.headers.get('location');
if (hasLocationHeader) {
// triggering a handshake redirect
res.status(307).end();
return;
}
if (requestState.status === AuthStatus.Handshake) {
return new Error('Clerk: unexpected handshake without redirect');
}
return;
};
const absoluteProxyUrl = (relativeOrAbsoluteUrl: string, baseUrl: string): string => {
if (!relativeOrAbsoluteUrl || !isValidProxyUrl(relativeOrAbsoluteUrl) || !isProxyUrlRelative(relativeOrAbsoluteUrl)) {
return relativeOrAbsoluteUrl;
}
return new URL(relativeOrAbsoluteUrl, baseUrl).toString();
};
export const authenticateAndDecorateRequest = (options: ClerkMiddlewareOptions = {}): RequestHandler => {
const clerkClient = options.clerkClient || defaultClerkClient;
const enableHandshake = options.enableHandshake ?? true;
// eslint-disable-next-line @typescript-eslint/no-misused-promises
const middleware: RequestHandler = async (request, response, next) => {
if ((request as ExpressRequestWithAuth).auth) {
return next();
}
try {
const requestState = await authenticateRequest({
clerkClient,
request,
options,
});
if (enableHandshake) {
const err = setResponseHeaders(requestState, response);
if (err) {
return next(err);
}
if (response.writableEnded) {
return;
}
}
const auth = requestState.toAuth();
Object.assign(request, { auth });
next();
} catch (err) {
next(err);
}
};
return middleware;
};