Skip to content
Source
Sign inSign up

connectAuth.ts

77 lines2.7 KB

1import { readHashParams } from "./remote.ts";
2​
3const CONNECT_AUTH_STATE_PARAM = "state";
4const CONNECT_AUTH_CHALLENGE_PARAM = "challenge";
5const CONNECT_AUTH_PORT_PARAM = "port";
6const CONNECT_LOOPBACK_CALLBACK_PATH = "/callback";
7​
8const CONNECT_AUTHORIZE_PATH = "/connect";
9​
10export const DEFAULT_HOSTED_APP_URL = "https://app.t3.codes";
11​
12export const CONNECT_OAUTH_SCOPES = ["openid", "profile", "email", "offline_access"] as const;
13​
14export interface ConnectAuthorizeRequest {
15 readonly state: string;
16 readonly challenge: string;
17 readonly loopbackPort: number;
19​
20/** Route through /connect because Clerk's signed-out redirect can lose OAuth query parameters. */
21export function buildConnectAuthorizeRequestUrl(input: {
22 readonly hostedAppUrl: string;
23 readonly state: string;
24 readonly challenge: string;
25 readonly loopbackPort: number;
26}): string {
27 const url = new URL(CONNECT_AUTHORIZE_PATH, input.hostedAppUrl);
28 url.hash = new URLSearchParams([
29 [CONNECT_AUTH_STATE_PARAM, input.state],
30 [CONNECT_AUTH_CHALLENGE_PARAM, input.challenge],
31 [CONNECT_AUTH_PORT_PARAM, String(input.loopbackPort)],
32 ]).toString();
33 return url.toString();
35​
36export function readConnectAuthorizeRequest(url: URL): ConnectAuthorizeRequest | null {
37 const params = readHashParams(url);
38 const state = params.get(CONNECT_AUTH_STATE_PARAM)?.trim() ?? "";
39 const challenge = params.get(CONNECT_AUTH_CHALLENGE_PARAM)?.trim() ?? "";
40 const loopbackPort = parseLoopbackPort(params.get(CONNECT_AUTH_PORT_PARAM)?.trim() ?? "");
41 if (!state || !challenge || loopbackPort === null) {
42 return null;
43 }
44 return { state, challenge, loopbackPort };
46​
47function parseLoopbackPort(value: string): number | null {
48 if (!/^\d{1,5}$/.test(value)) {
49 return null;
50 }
51 const port = Number(value);
52 return port >= 1 && port <= 65535 ? port : null;
54​
55/** Must match the loopback redirect URI registered on the Clerk CLI OAuth app. */
56export function connectLoopbackRedirectUri(port: number): string {
57 return `http://127.0.0.1:${port}${CONNECT_LOOPBACK_CALLBACK_PATH}`;
59​
60export function buildConnectClerkAuthorizeUrl(input: {
61 readonly authorizationEndpoint: string;
62 readonly clientId: string;
63 readonly redirectUri: string;
64 readonly scopes: ReadonlyArray<string>;
65 readonly state: string;
66 readonly challenge: string;
67}): string {
68 const url = new URL(input.authorizationEndpoint);
69 url.searchParams.set("client_id", input.clientId);
70 url.searchParams.set("redirect_uri", input.redirectUri);
71 url.searchParams.set("response_type", "code");
72 url.searchParams.set("scope", input.scopes.join(" "));
73 url.searchParams.set("state", input.state);
74 url.searchParams.set("code_challenge", input.challenge);
75 url.searchParams.set("code_challenge_method", "S256");
76 return url.toString();