tangled
alpha
login
or
join now
t1c.dev
/
rocksky
forked from
rocksky.app/rocksky
2
fork
atom
A decentralized music tracking and discovery platform built on AT Protocol 🎵
2
fork
atom
overview
issues
pulls
pipelines
Add CustomOAuthClient with PAR and DPoP support
tsiry-sandratraina.com
1 month ago
f227c89d
243eb205
+109
-5
2 changed files
expand all
collapse all
unified
split
apps
api
src
auth
client.ts
bsky
app.ts
+109
-2
apps/api/src/auth/client.ts
···
1
1
import { JoseKey } from "@atproto/jwk-jose";
2
2
-
import { NodeOAuthClient, type RuntimeLock } from "@atproto/oauth-client-node";
2
2
+
import {
3
3
+
AuthorizeOptions,
4
4
+
NodeOAuthClient,
5
5
+
NodeOAuthClientOptions,
6
6
+
OAuthAuthorizationRequestParameters,
7
7
+
type RuntimeLock,
8
8
+
} from "@atproto/oauth-client-node";
3
9
import Redis from "ioredis";
4
10
import Redlock from "redlock";
5
11
import type { Database } from "../db";
6
12
import { env } from "../lib/env";
7
13
import { SessionStore, StateStore } from "./storage";
8
14
15
15
+
export const FALLBACK_ALG = "ES256";
16
16
+
17
17
+
export class CustomOAuthClient extends NodeOAuthClient {
18
18
+
constructor(options: NodeOAuthClientOptions) {
19
19
+
super(options);
20
20
+
}
21
21
+
22
22
+
async authorize(
23
23
+
input: string,
24
24
+
{ signal, ...options }: AuthorizeOptions = {},
25
25
+
): Promise<URL> {
26
26
+
const redirectUri =
27
27
+
options?.redirect_uri ?? this.clientMetadata.redirect_uris[0];
28
28
+
if (!this.clientMetadata.redirect_uris.includes(redirectUri)) {
29
29
+
// The server will enforce this, but let's catch it early
30
30
+
throw new TypeError("Invalid redirect_uri");
31
31
+
}
32
32
+
33
33
+
const { identity, metadata } = await this.oauthResolver.resolve(input, {
34
34
+
signal,
35
35
+
});
36
36
+
37
37
+
const pkce = await this.runtime.generatePKCE();
38
38
+
const dpopKey = await this.runtime.generateKey(
39
39
+
metadata.dpop_signing_alg_values_supported || [FALLBACK_ALG],
40
40
+
);
41
41
+
42
42
+
const state = await this.runtime.generateNonce();
43
43
+
44
44
+
await this.stateStore.set(state, {
45
45
+
iss: metadata.issuer,
46
46
+
dpopKey,
47
47
+
verifier: pkce.verifier,
48
48
+
appState: options?.state,
49
49
+
});
50
50
+
51
51
+
const parameters: OAuthAuthorizationRequestParameters = {
52
52
+
...options,
53
53
+
54
54
+
client_id: this.clientMetadata.client_id,
55
55
+
redirect_uri: redirectUri,
56
56
+
code_challenge: pkce.challenge,
57
57
+
code_challenge_method: pkce.method,
58
58
+
state,
59
59
+
login_hint: identity && !options.prompt ? input : undefined,
60
60
+
response_mode: this.responseMode,
61
61
+
response_type: "code" as const,
62
62
+
scope: options?.scope ?? this.clientMetadata.scope,
63
63
+
};
64
64
+
65
65
+
const authorizationUrl = new URL(metadata.authorization_endpoint);
66
66
+
67
67
+
// Since the user will be redirected to the authorization_endpoint url using
68
68
+
// a browser, we need to make sure that the url is valid.
69
69
+
if (
70
70
+
authorizationUrl.protocol !== "https:" &&
71
71
+
authorizationUrl.protocol !== "http:"
72
72
+
) {
73
73
+
throw new TypeError(
74
74
+
`Invalid authorization endpoint protocol: ${authorizationUrl.protocol}`,
75
75
+
);
76
76
+
}
77
77
+
78
78
+
if (metadata.pushed_authorization_request_endpoint) {
79
79
+
const server = await this.serverFactory.fromMetadata(metadata, dpopKey);
80
80
+
const parResponse = await server.request(
81
81
+
"pushed_authorization_request",
82
82
+
parameters,
83
83
+
);
84
84
+
85
85
+
authorizationUrl.searchParams.set(
86
86
+
"client_id",
87
87
+
this.clientMetadata.client_id,
88
88
+
);
89
89
+
authorizationUrl.searchParams.set("request_uri", parResponse.request_uri);
90
90
+
return authorizationUrl;
91
91
+
} else if (metadata.require_pushed_authorization_requests) {
92
92
+
throw new Error(
93
93
+
"Server requires pushed authorization requests (PAR) but no PAR endpoint is available",
94
94
+
);
95
95
+
} else {
96
96
+
for (const [key, value] of Object.entries(parameters)) {
97
97
+
if (value) authorizationUrl.searchParams.set(key, String(value));
98
98
+
}
99
99
+
100
100
+
// Length of the URL that will be sent to the server
101
101
+
const urlLength =
102
102
+
authorizationUrl.pathname.length + authorizationUrl.search.length;
103
103
+
if (urlLength < 2048) {
104
104
+
return authorizationUrl;
105
105
+
} else if (!metadata.pushed_authorization_request_endpoint) {
106
106
+
throw new Error("Login URL too long");
107
107
+
}
108
108
+
}
109
109
+
110
110
+
throw new Error(
111
111
+
"Server does not support pushed authorization requests (PAR)",
112
112
+
);
113
113
+
}
114
114
+
}
115
115
+
9
116
export const createClient = async (db: Database) => {
10
117
const publicUrl = env.PUBLIC_URL;
11
118
const url = publicUrl.includes("localhost")
···
25
132
}
26
133
};
27
134
28
28
-
return new NodeOAuthClient({
135
135
+
return new CustomOAuthClient({
29
136
clientMetadata: {
30
137
client_name: "Rocksky",
31
138
client_id: !publicUrl.includes("localhost")
-3
apps/api/src/bsky/app.ts
···
39
39
if (cli) {
40
40
ctx.kv.set(`cli:${handle}`, "1");
41
41
}
42
42
-
if (prompt) {
43
43
-
url.searchParams.delete("login_hint");
44
44
-
}
45
42
return c.redirect(url.toString());
46
43
} catch (e) {
47
44
c.status(500);