tangled
alpha
login
or
join now
stream.place
/
streamplace
Live video on the AT Protocol
74
fork
atom
overview
issues
1
pulls
pipelines
checkpoint
Eli Mallon
9 months ago
ddccfef7
ad008cf4
+333
-198
7 changed files
expand all
collapse all
unified
split
js
app
components
login
login.tsx
features
bluesky
blueskySlice.tsx
oauthClient.tsx
pkg
api
api.go
oauth.go
atproto
client_metadata.go
oauth_upstream.go
+35
-20
js/app/components/login/login.tsx
···
68
>
69
<ChangePDS open={open} onOpenChange={onOpenChange} />
70
{/* <Text>{error}</Text> */}
71
-
<H3>Handle</H3>
72
-
<Input
73
-
width="100%"
74
-
placeholder="example.bsky.social"
75
-
value={handle}
76
-
onChangeText={(text) => setHandle(text)}
77
-
/>
78
-
<Button
79
width="100%"
80
-
onPress={async () => {
81
-
// const agent = new AtpBaseClient(url);
82
-
// const res = await agent.place.stream.account.login({
83
-
// handleOrDID: handle,
84
-
// });
85
-
// window.location.href = res.data.redirectUrl;
86
-
await dispatch(login(`https://bsky.social`));
0
0
0
0
0
0
0
87
}}
88
-
margin="$4"
89
-
backgroundColor="$accentColor"
90
-
disabled={loginState.loading}
91
>
92
-
<Text>{loginState.loading ? <Spinner /> : `Log in with ATProto`}</Text>
93
-
</Button>
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
94
</View>
95
);
96
}
···
68
>
69
<ChangePDS open={open} onOpenChange={onOpenChange} />
70
{/* <Text>{error}</Text> */}
71
+
<Form
0
0
0
0
0
0
0
72
width="100%"
73
+
maxWidth={800}
74
+
jc="center"
75
+
ai="center"
76
+
onSubmit={async () => {
77
+
// onPress={async () => {
78
+
// // const agent = new AtpBaseClient(url);
79
+
// // const res = await agent.place.stream.account.login({
80
+
// // handleOrDID: handle,
81
+
// // });
82
+
// // window.location.href = res.data.redirectUrl;
83
+
// await dispatch(login(`https://bsky.social`));
84
+
// }}
85
+
console.log("here we go");
86
+
await dispatch(login(`https://longos.iameli.link`));
87
}}
0
0
0
88
>
89
+
<H3>Handle</H3>
90
+
<Input
91
+
width="100%"
92
+
placeholder="example.bsky.social"
93
+
value={handle}
94
+
onChangeText={(text) => setHandle(text)}
95
+
/>
96
+
<Form.Trigger asChild>
97
+
<Button
98
+
width="100%"
99
+
margin="$4"
100
+
backgroundColor="$accentColor"
101
+
disabled={loginState.loading}
102
+
>
103
+
<Text>
104
+
{loginState.loading ? <Spinner /> : `Log in with ATProto`}
105
+
</Text>
106
+
</Button>
107
+
</Form.Trigger>
108
+
</Form>
109
</View>
110
);
111
}
+3
-1
js/app/features/bluesky/blueskySlice.tsx
···
157
if (!bluesky.client) {
158
throw new Error("No client");
159
}
160
-
const u = await bluesky.client.authorize(pds);
0
161
thunkAPI.dispatch(openLoginLink(u.toString()));
162
// cheeky 500ms delay so you don't see the text flash back
163
await new Promise((resolve) => setTimeout(resolve, 500));
···
183
};
184
},
185
rejected: (state, action) => {
0
186
return {
187
...state,
188
login: {
···
157
if (!bluesky.client) {
158
throw new Error("No client");
159
}
160
+
const u = await bluesky.client.authorize("scumb.ag", {});
161
+
console.log(u);
162
thunkAPI.dispatch(openLoginLink(u.toString()));
163
// cheeky 500ms delay so you don't see the text flash back
164
await new Promise((resolve) => setTimeout(resolve, 500));
···
184
};
185
},
186
rejected: (state, action) => {
187
+
console.error("login rejected", action.error);
188
return {
189
...state,
190
login: {
+73
-61
js/app/features/bluesky/oauthClient.tsx
···
18
throw new Error("streamplaceUrl is required");
19
}
20
let meta: ClientMetadata;
21
-
// if (
22
-
// streamplaceUrl.startsWith("http://localhost") ||
23
-
// streamplaceUrl.startsWith("http://127.0.0.1")
24
-
// ) {
25
-
// const isWeb = Platform.OS === "web";
26
-
// const u = new URL(streamplaceUrl);
27
-
// let hostname = u.hostname;
28
-
// if (hostname == "localhost") {
29
-
// hostname = "127.0.0.1";
30
-
// }
31
-
// let redirect = `${u.protocol}//${hostname}`;
32
-
// if (u.port !== "") {
33
-
// redirect = `${redirect}:${u.port}`;
34
-
// }
35
-
// if (isWeb) {
36
-
// redirect = `${redirect}/login`;
37
-
// } else {
38
-
// const scheme = Constants.expoConfig?.scheme;
39
-
// if (!scheme) {
40
-
// throw new Error("unable to resolve scheme for oauth redirect");
41
-
// }
42
-
// redirect = `${redirect}/app-return/${scheme}`;
43
-
// }
44
-
// const queryParams = new URLSearchParams();
45
-
// queryParams.set("scope", "atproto transition:generic");
46
-
// queryParams.set("redirect_uri", redirect);
47
-
// meta = {
48
-
// client_id: `http://localhost?${queryParams.toString()}`,
49
-
// redirect_uris: [redirect as any],
50
-
// scope: "atproto transition:generic",
51
-
// token_endpoint_auth_method: "none",
52
-
// client_name: "Loopback client",
53
-
// response_types: ["code"],
54
-
// grant_types: ["authorization_code", "refresh_token"],
55
-
// // > There is a special exception for the localhost development workflow [ ... ]
56
-
// // > These clients use web URLs, but have application_type set to native in the generated client metadata.
57
-
// application_type: "native",
58
-
// dpop_bound_access_tokens: true,
59
-
// };
60
-
// } else {
61
-
// const res = await fetch(
62
-
// `${streamplaceUrl}/api/atproto-oauth/${Platform.OS}`,
63
-
// );
64
-
// meta = await res.json();
65
-
// }
66
-
meta = {
67
-
redirect_uris: [
68
-
"https://longos.iameli.link/xrpc/place.stream.account.oauthReturn",
69
-
],
70
-
response_types: ["code"],
71
-
grant_types: ["authorization_code", "refresh_token"],
72
-
scope: "atproto transition:generic",
73
-
token_endpoint_auth_method: "none",
74
-
application_type: "web",
75
-
client_id: "https://longos.iameli.link/api/atproto-oauth/web",
76
-
client_name: "Streamplace",
77
-
client_uri: "https://longos.iameli.link",
78
-
dpop_bound_access_tokens: true,
79
-
};
80
clientMetadataSchema.parse(meta);
81
return new ReactNativeOAuthClient({
82
-
fetch: (input, init) => {
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
83
console.log("!!!!!! fetch", input, init);
84
-
return fetch(input, init);
85
},
86
handleResolver: "https://bsky.social", // backend instances should use a DNS based resolver
87
responseMode: "query", // or "fragment" (frontend only) or "form_post" (backend only)
···
18
throw new Error("streamplaceUrl is required");
19
}
20
let meta: ClientMetadata;
21
+
if (
22
+
streamplaceUrl.startsWith("http://localhost") ||
23
+
streamplaceUrl.startsWith("http://127.0.0.1")
24
+
) {
25
+
const isWeb = Platform.OS === "web";
26
+
const u = new URL(streamplaceUrl);
27
+
let hostname = u.hostname;
28
+
if (hostname == "localhost") {
29
+
hostname = "127.0.0.1";
30
+
}
31
+
let redirect = `${u.protocol}//${hostname}`;
32
+
if (u.port !== "") {
33
+
redirect = `${redirect}:${u.port}`;
34
+
}
35
+
if (isWeb) {
36
+
redirect = `${redirect}/login`;
37
+
} else {
38
+
const scheme = Constants.expoConfig?.scheme;
39
+
if (!scheme) {
40
+
throw new Error("unable to resolve scheme for oauth redirect");
41
+
}
42
+
redirect = `${redirect}/app-return/${scheme}`;
43
+
}
44
+
const queryParams = new URLSearchParams();
45
+
queryParams.set("scope", "atproto transition:generic");
46
+
queryParams.set("redirect_uri", redirect);
47
+
meta = {
48
+
client_id: `http://localhost?${queryParams.toString()}`,
49
+
redirect_uris: [redirect as any],
50
+
scope: "atproto transition:generic",
51
+
token_endpoint_auth_method: "none",
52
+
client_name: "Loopback client",
53
+
response_types: ["code"],
54
+
grant_types: ["authorization_code", "refresh_token"],
55
+
// > There is a special exception for the localhost development workflow [ ... ]
56
+
// > These clients use web URLs, but have application_type set to native in the generated client metadata.
57
+
application_type: "native",
58
+
dpop_bound_access_tokens: true,
59
+
};
60
+
} else {
61
+
const res = await fetch(
62
+
`${streamplaceUrl}/api/atproto-oauth/downstream/${Platform.OS}`,
63
+
);
64
+
meta = await res.json();
65
+
}
0
0
0
0
0
0
0
0
0
0
0
0
0
0
66
clientMetadataSchema.parse(meta);
67
return new ReactNativeOAuthClient({
68
+
fetch: async (input, init) => {
69
+
// Normalize input to a Request object
70
+
let request: Request;
71
+
if (typeof input === "string" || input instanceof URL) {
72
+
request = new Request(input, init);
73
+
} else {
74
+
request = input;
75
+
}
76
+
77
+
// If we're making a request to the PLC directory, use our custom endpoint
78
+
if (request.url.includes("plc.directory")) {
79
+
const res = await fetch(request, init);
80
+
if (!res.ok) {
81
+
return res;
82
+
}
83
+
const data = await res.json();
84
+
const service = data.service.find((s: any) => s.id === "#atproto_pds");
85
+
if (!service) {
86
+
return res;
87
+
}
88
+
service.serviceEndpoint = streamplaceUrl;
89
+
return new Response(JSON.stringify(data), {
90
+
status: res.status,
91
+
headers: res.headers,
92
+
});
93
+
}
94
+
95
console.log("!!!!!! fetch", input, init);
96
+
return fetch(request, init);
97
},
98
handleResolver: "https://bsky.social", // backend instances should use a DNS based resolver
99
responseMode: "query", // or "fragment" (frontend only) or "form_post" (backend only)
+5
-94
pkg/api/api.go
···
12
"net/http/httputil"
13
"net/url"
14
"os"
15
-
"slices"
16
"strings"
17
"sync"
18
"time"
19
20
"github.com/NYTimes/gziphandler"
21
"github.com/bluesky-social/indigo/api/bsky"
22
-
"github.com/haileyok/atproto-oauth-golang/helpers"
23
"github.com/julienschmidt/httprouter"
24
"github.com/rs/cors"
25
sloghttp "github.com/samber/slog-http"
···
121
// api/playback/iame.li/hls/source/stream.m3u8
122
// api/playback/iame.li/hls/source/000000000000.ts
123
124
-
func generateOAuthServerMetadata(host string) map[string]any {
125
-
oauthServerMetadata := map[string]any{
126
-
"issuer": fmt.Sprintf("https://%s", host),
127
-
"request_parameter_supported": true,
128
-
"request_uri_parameter_supported": true,
129
-
"require_request_uri_registration": true,
130
-
"scopes_supported": []string{"atproto", "transition:generic", "transition:chat.bsky"},
131
-
"subject_types_supported": []string{"public"},
132
-
"response_types_supported": []string{"code"},
133
-
"response_modes_supported": []string{"query", "fragment", "form_post"},
134
-
"grant_types_supported": []string{"authorization_code", "refresh_token"},
135
-
"code_challenge_methods_supported": []string{"S256"},
136
-
"ui_locales_supported": []string{"en-US"},
137
-
"display_values_supported": []string{"page", "popup", "touch"},
138
-
"authorization_response_iss_parameter_supported": true,
139
-
"request_object_encryption_alg_values_supported": []string{},
140
-
"request_object_encryption_enc_values_supported": []string{},
141
-
"jwks_uri": fmt.Sprintf("https://%s/oauth/jwks", host),
142
-
"authorization_endpoint": fmt.Sprintf("https://%s/oauth/authorize", host),
143
-
"token_endpoint": fmt.Sprintf("https://%s/oauth/token", host),
144
-
"token_endpoint_auth_methods_supported": []string{"none", "private_key_jwt"},
145
-
"revocation_endpoint": fmt.Sprintf("https://%s/oauth/revoke", host),
146
-
"introspection_endpoint": fmt.Sprintf("https://%s/oauth/introspect", host),
147
-
"pushed_authorization_request_endpoint": fmt.Sprintf("https://%s/oauth/par", host),
148
-
"require_pushed_authorization_requests": true,
149
-
"client_id_metadata_document_supported": true,
150
-
"request_object_signing_alg_values_supported": []string{
151
-
"RS256", "RS384", "RS512", "PS256", "PS384", "PS512",
152
-
"ES256", "ES256K", "ES384", "ES512", "none",
153
-
},
154
-
"token_endpoint_auth_signing_alg_values_supported": []string{
155
-
"RS256", "RS384", "RS512", "PS256", "PS384", "PS512",
156
-
"ES256", "ES256K", "ES384", "ES512",
157
-
},
158
-
"dpop_signing_alg_values_supported": []string{
159
-
"RS256", "RS384", "RS512", "PS256", "PS384", "PS512",
160
-
"ES256", "ES256K", "ES384", "ES512",
161
-
},
162
-
}
163
-
return oauthServerMetadata
164
-
}
165
-
166
func (a *StreamplaceAPI) Handler(ctx context.Context) (http.Handler, error) {
167
xrpc, err := spxrpc.NewServer(a.CLI, a.Model)
168
if err != nil {
···
173
apiRouter.HandlerFunc("POST", "/api/notification", a.HandleNotification(ctx))
174
// old clients
175
router.HandlerFunc("GET", "/app-updates", a.HandleAppUpdates(ctx))
176
-
router.HandlerFunc("GET", "/.well-known/oauth-authorization-server", func(w http.ResponseWriter, r *http.Request) {
177
-
w.Header().Set("Access-Control-Allow-Origin", "*")
178
-
w.Header().Set("Content-Type", "application/json")
179
-
w.WriteHeader(200)
180
-
json.NewEncoder(w).Encode(generateOAuthServerMetadata("longos.iameli.link"))
181
-
})
182
-
router.HandlerFunc("GET", "/.well-known/oauth-protected-resource", func(w http.ResponseWriter, r *http.Request) {
183
-
w.Header().Set("Access-Control-Allow-Origin", "*")
184
-
w.WriteHeader(404)
185
-
})
186
187
// new ones
188
apiRouter.HandlerFunc("GET", "/api/manifest", a.HandleAppUpdates(ctx))
···
211
apiRouter.GET("/api/segment/recent/:repoDID", a.HandleUserRecentSegments(ctx))
212
apiRouter.GET("/api/bluesky/resolve/:handle", a.HandleBlueskyResolve(ctx))
213
for _, platform := range atproto.AllowedPlatforms {
214
-
apiRouter.GET(fmt.Sprintf("/api/atproto-oauth/%s", platform), a.HandleATProtoOAuth(ctx, platform))
0
215
}
216
apiRouter.GET("/api/atproto-oauth/jwks.json", a.HandleJWKPublic(ctx))
217
apiRouter.GET("/api/live-users", a.HandleLiveUsers(ctx))
218
apiRouter.GET("/api/view-count/:user", a.HandleViewCount(ctx))
0
219
apiRouter.NotFound = a.HandleAPI404(ctx)
220
router.Handler("GET", "/api/*resource", apiRouter)
221
router.Handler("POST", "/api/*resource", apiRouter)
···
642
apierrors.WriteHTTPInternalServerError(w, "could not marshal signing keys", err)
643
return
644
}
645
-
w.Write(bs)
646
-
}
647
-
}
648
-
649
-
func (a *StreamplaceAPI) HandleATProtoOAuth(ctx context.Context, platform string) httprouter.Handle {
650
-
return func(w http.ResponseWriter, req *http.Request, params httprouter.Params) {
651
-
host, _, err := net.SplitHostPort(req.Host)
652
-
if err != nil {
653
-
host = req.Host
654
-
}
655
-
if !slices.Contains(atproto.AllowedPlatforms, platform) {
656
-
apierrors.WriteHTTPBadRequest(w, "unsupported platform", nil)
657
-
return
658
-
}
659
-
660
-
meta := atproto.GetMetadata(host, platform, a.CLI.AppBundleID)
661
-
bs, err := json.Marshal(meta)
662
-
if err != nil {
663
-
apierrors.WriteHTTPInternalServerError(w, "could not marshal metadata", err)
664
-
return
665
-
}
666
-
w.Header().Set("Content-Type", "application/json")
667
-
w.Write(bs)
668
-
}
669
-
}
670
-
671
-
func (a *StreamplaceAPI) HandleJWKPublic(ctx context.Context) httprouter.Handle {
672
-
return func(w http.ResponseWriter, req *http.Request, params httprouter.Params) {
673
-
pubKey, err := a.CLI.JWK.PublicKey()
674
-
if err != nil {
675
-
apierrors.WriteHTTPInternalServerError(w, "could not get public key", err)
676
-
return
677
-
}
678
-
bs, err := json.Marshal(helpers.CreateJwksResponseObject(pubKey))
679
-
if err != nil {
680
-
apierrors.WriteHTTPInternalServerError(w, "could not marshal public key", err)
681
-
return
682
-
}
683
-
w.Header().Set("Content-Type", "application/json")
684
w.Write(bs)
685
}
686
}
···
12
"net/http/httputil"
13
"net/url"
14
"os"
0
15
"strings"
16
"sync"
17
"time"
18
19
"github.com/NYTimes/gziphandler"
20
"github.com/bluesky-social/indigo/api/bsky"
0
21
"github.com/julienschmidt/httprouter"
22
"github.com/rs/cors"
23
sloghttp "github.com/samber/slog-http"
···
119
// api/playback/iame.li/hls/source/stream.m3u8
120
// api/playback/iame.li/hls/source/000000000000.ts
121
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
122
func (a *StreamplaceAPI) Handler(ctx context.Context) (http.Handler, error) {
123
xrpc, err := spxrpc.NewServer(a.CLI, a.Model)
124
if err != nil {
···
129
apiRouter.HandlerFunc("POST", "/api/notification", a.HandleNotification(ctx))
130
// old clients
131
router.HandlerFunc("GET", "/app-updates", a.HandleAppUpdates(ctx))
132
+
router.HandlerFunc("GET", "/.well-known/oauth-authorization-server", a.HandleOAuthAuthorizationServer(ctx))
133
+
router.HandlerFunc("GET", "/.well-known/oauth-protected-resource", a.HandleOAuthProtectedResource(ctx))
0
0
0
0
0
0
0
0
134
135
// new ones
136
apiRouter.HandlerFunc("GET", "/api/manifest", a.HandleAppUpdates(ctx))
···
159
apiRouter.GET("/api/segment/recent/:repoDID", a.HandleUserRecentSegments(ctx))
160
apiRouter.GET("/api/bluesky/resolve/:handle", a.HandleBlueskyResolve(ctx))
161
for _, platform := range atproto.AllowedPlatforms {
162
+
apiRouter.GET(fmt.Sprintf("/api/atproto-oauth/upstream/%s", platform), a.HandleATProtoOAuthUpstream(ctx, platform))
163
+
apiRouter.GET(fmt.Sprintf("/api/atproto-oauth/downstream/%s", platform), a.HandleATProtoOAuthDownstream(ctx, platform))
164
}
165
apiRouter.GET("/api/atproto-oauth/jwks.json", a.HandleJWKPublic(ctx))
166
apiRouter.GET("/api/live-users", a.HandleLiveUsers(ctx))
167
apiRouter.GET("/api/view-count/:user", a.HandleViewCount(ctx))
168
+
apiRouter.HandlerFunc("POST", "/api/oauth/par", a.HandleOAuthPAR(ctx))
169
apiRouter.NotFound = a.HandleAPI404(ctx)
170
router.Handler("GET", "/api/*resource", apiRouter)
171
router.Handler("POST", "/api/*resource", apiRouter)
···
592
apierrors.WriteHTTPInternalServerError(w, "could not marshal signing keys", err)
593
return
594
}
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
595
w.Write(bs)
596
}
597
}
+191
pkg/api/oauth.go
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
1
+
package api
2
+
3
+
import (
4
+
"context"
5
+
"encoding/json"
6
+
"fmt"
7
+
"net"
8
+
"net/http"
9
+
"slices"
10
+
11
+
"github.com/haileyok/atproto-oauth-golang/helpers"
12
+
"github.com/julienschmidt/httprouter"
13
+
"stream.place/streamplace/pkg/atproto"
14
+
apierrors "stream.place/streamplace/pkg/errors"
15
+
)
16
+
17
+
func (a *StreamplaceAPI) HandleATProtoOAuthUpstream(ctx context.Context, platform string) httprouter.Handle {
18
+
return func(w http.ResponseWriter, req *http.Request, params httprouter.Params) {
19
+
host, _, err := net.SplitHostPort(req.Host)
20
+
if err != nil {
21
+
host = req.Host
22
+
}
23
+
if !slices.Contains(atproto.AllowedPlatforms, platform) {
24
+
apierrors.WriteHTTPBadRequest(w, "unsupported platform", nil)
25
+
return
26
+
}
27
+
28
+
meta := atproto.GetUpstreamMetadata(host, platform, a.CLI.AppBundleID)
29
+
bs, err := json.Marshal(meta)
30
+
if err != nil {
31
+
apierrors.WriteHTTPInternalServerError(w, "could not marshal metadata", err)
32
+
return
33
+
}
34
+
w.Header().Set("Content-Type", "application/json")
35
+
w.Write(bs)
36
+
}
37
+
}
38
+
39
+
func (a *StreamplaceAPI) HandleATProtoOAuthDownstream(ctx context.Context, platform string) httprouter.Handle {
40
+
return func(w http.ResponseWriter, req *http.Request, params httprouter.Params) {
41
+
host, _, err := net.SplitHostPort(req.Host)
42
+
if err != nil {
43
+
host = req.Host
44
+
}
45
+
if !slices.Contains(atproto.AllowedPlatforms, platform) {
46
+
apierrors.WriteHTTPBadRequest(w, "unsupported platform", nil)
47
+
return
48
+
}
49
+
50
+
meta := atproto.GetDownstreamMetadata(host, platform, a.CLI.AppBundleID)
51
+
bs, err := json.Marshal(meta)
52
+
if err != nil {
53
+
apierrors.WriteHTTPInternalServerError(w, "could not marshal metadata", err)
54
+
return
55
+
}
56
+
w.Header().Set("Content-Type", "application/json")
57
+
w.Write(bs)
58
+
}
59
+
}
60
+
61
+
func (a *StreamplaceAPI) HandleJWKPublic(ctx context.Context) httprouter.Handle {
62
+
return func(w http.ResponseWriter, req *http.Request, params httprouter.Params) {
63
+
pubKey, err := a.CLI.JWK.PublicKey()
64
+
if err != nil {
65
+
apierrors.WriteHTTPInternalServerError(w, "could not get public key", err)
66
+
return
67
+
}
68
+
bs, err := json.Marshal(helpers.CreateJwksResponseObject(pubKey))
69
+
if err != nil {
70
+
apierrors.WriteHTTPInternalServerError(w, "could not marshal public key", err)
71
+
return
72
+
}
73
+
w.Header().Set("Content-Type", "application/json")
74
+
w.Write(bs)
75
+
}
76
+
}
77
+
78
+
func generateOAuthServerMetadata(host string) map[string]any {
79
+
oauthServerMetadata := map[string]any{
80
+
"issuer": fmt.Sprintf("https://%s", host),
81
+
"request_parameter_supported": true,
82
+
"request_uri_parameter_supported": true,
83
+
"require_request_uri_registration": true,
84
+
"scopes_supported": []string{"atproto", "transition:generic", "transition:chat.bsky"},
85
+
"subject_types_supported": []string{"public"},
86
+
"response_types_supported": []string{"code"},
87
+
"response_modes_supported": []string{"query", "fragment", "form_post"},
88
+
"grant_types_supported": []string{"authorization_code", "refresh_token"},
89
+
"code_challenge_methods_supported": []string{"S256"},
90
+
"ui_locales_supported": []string{"en-US"},
91
+
"display_values_supported": []string{"page", "popup", "touch"},
92
+
"authorization_response_iss_parameter_supported": true,
93
+
"request_object_encryption_alg_values_supported": []string{},
94
+
"request_object_encryption_enc_values_supported": []string{},
95
+
"jwks_uri": fmt.Sprintf("https://%s/api/oauth/jwks", host),
96
+
"authorization_endpoint": fmt.Sprintf("https://%s/api/oauth/authorize", host),
97
+
"token_endpoint": fmt.Sprintf("https://%s/api/oauth/token", host),
98
+
"token_endpoint_auth_methods_supported": []string{"none", "private_key_jwt"},
99
+
"revocation_endpoint": fmt.Sprintf("https://%s/api/oauth/revoke", host),
100
+
"introspection_endpoint": fmt.Sprintf("https://%s/api/oauth/introspect", host),
101
+
"pushed_authorization_request_endpoint": fmt.Sprintf("https://%s/api/oauth/par", host),
102
+
"require_pushed_authorization_requests": true,
103
+
"client_id_metadata_document_supported": true,
104
+
"request_object_signing_alg_values_supported": []string{
105
+
"RS256", "RS384", "RS512", "PS256", "PS384", "PS512",
106
+
"ES256", "ES256K", "ES384", "ES512", "none",
107
+
},
108
+
"token_endpoint_auth_signing_alg_values_supported": []string{
109
+
"RS256", "RS384", "RS512", "PS256", "PS384", "PS512",
110
+
"ES256", "ES256K", "ES384", "ES512",
111
+
},
112
+
"dpop_signing_alg_values_supported": []string{
113
+
"RS256", "RS384", "RS512", "PS256", "PS384", "PS512",
114
+
"ES256", "ES256K", "ES384", "ES512",
115
+
},
116
+
}
117
+
return oauthServerMetadata
118
+
}
119
+
120
+
func (a *StreamplaceAPI) HandleOAuthAuthorizationServer(ctx context.Context) http.HandlerFunc {
121
+
return func(w http.ResponseWriter, r *http.Request) {
122
+
w.Header().Set("Access-Control-Allow-Origin", "*")
123
+
w.Header().Set("Content-Type", "application/json")
124
+
w.WriteHeader(200)
125
+
json.NewEncoder(w).Encode(generateOAuthServerMetadata("longos.iameli.link"))
126
+
}
127
+
}
128
+
129
+
func (a *StreamplaceAPI) HandleOAuthProtectedResource(ctx context.Context) http.HandlerFunc {
130
+
return func(w http.ResponseWriter, r *http.Request) {
131
+
w.Header().Set("Access-Control-Allow-Origin", "*")
132
+
w.Header().Set("Content-Type", "application/json")
133
+
w.WriteHeader(200)
134
+
json.NewEncoder(w).Encode(map[string]interface{}{
135
+
"resource": "https://longos.iameli.link",
136
+
"authorization_servers": []string{
137
+
"https://longos.iameli.link",
138
+
},
139
+
"scopes_supported": []string{},
140
+
"bearer_methods_supported": []string{
141
+
"header",
142
+
},
143
+
"resource_documentation": "https://atproto.com",
144
+
})
145
+
}
146
+
}
147
+
148
+
func (a *StreamplaceAPI) HandleOAuthPAR(ctx context.Context) http.HandlerFunc {
149
+
return func(w http.ResponseWriter, r *http.Request) {
150
+
w.Header().Set("Access-Control-Allow-Origin", "*")
151
+
w.Header().Set("Content-Type", "application/json")
152
+
w.WriteHeader(201)
153
+
json.NewEncoder(w).Encode(map[string]any{
154
+
"request_uri": "urn:ietf:params:oauth:request_uri:req-b963542aeeaebe427f8ee5c6dd095ff0",
155
+
"expires_in": 299,
156
+
})
157
+
}
158
+
}
159
+
func (a *StreamplaceAPI) HandlePLC(ctx context.Context) http.HandlerFunc {
160
+
return func(w http.ResponseWriter, r *http.Request) {
161
+
w.Header().Set("Access-Control-Allow-Origin", "*")
162
+
w.Header().Set("Content-Type", "application/json")
163
+
w.WriteHeader(201)
164
+
json.NewEncoder(w).Encode(map[string]any{
165
+
"@context": []string{
166
+
"https://www.w3.org/ns/did/v1",
167
+
"https://w3id.org/security/multikey/v1",
168
+
"https://w3id.org/security/suites/secp256k1-2019/v1",
169
+
},
170
+
"id": "did:plc:dkh4rwafdcda4ko7lewe43ml",
171
+
"alsoKnownAs": []string{
172
+
"at://scumb.ag",
173
+
},
174
+
"verificationMethod": []map[string]string{
175
+
{
176
+
"id": "did:plc:dkh4rwafdcda4ko7lewe43ml#atproto",
177
+
"type": "Multikey",
178
+
"controller": "did:plc:dkh4rwafdcda4ko7lewe43ml",
179
+
"publicKeyMultibase": "zQ3shMdd6GA2eefzDHPoTGmtt1D8tTfbE7MqBzrF9Dv78m5Lr",
180
+
},
181
+
},
182
+
"service": []map[string]string{
183
+
{
184
+
"id": "#atproto_pds",
185
+
"type": "AtprotoPersonalDataServer",
186
+
"serviceEndpoint": "https://milkcap.us-west.host.bsky.network",
187
+
},
188
+
},
189
+
})
190
+
}
191
+
}
+24
-20
pkg/atproto/client_metadata.go
···
42
return &b
43
}
44
45
-
func GetMetadata(host string, platform string, appBundleId string) *OAuthClientMetadata {
46
meta := &OAuthClientMetadata{
47
-
ClientID: fmt.Sprintf("https://%s/api/atproto-oauth/%s", host, platform),
48
JwksURI: fmt.Sprintf("https://%s/api/atproto-oauth/jwks.json", host),
49
ClientURI: fmt.Sprintf("https://%s", host),
50
// RedirectURIs: []string{fmt.Sprintf("https://%s/login", host)},
···
56
DPoPBoundAccessTokens: boolPtr(true),
57
TokenEndpointAuthSigningAlg: "ES256",
58
}
59
-
60
-
// metadata := map[string]any{
61
-
// "client_id": serverMetadataUrl,
62
-
// "client_name": "Atproto Oauth Golang Tester",
63
-
// "client_uri": serverUrlRoot,
64
-
// "logo_uri": fmt.Sprintf("%s/logo.png", serverUrlRoot),
65
-
// "tos_uri": fmt.Sprintf("%s/tos", serverUrlRoot),
66
-
// "policy_url": fmt.Sprintf("%s/policy", serverUrlRoot),
67
-
// "redirect_uris": []string{serverCallbackUrl},
68
-
// "grant_types": []string{"authorization_code", "refresh_token"},
69
-
// "response_types": []string{"code"},
70
-
// "application_type": "web",
71
-
// "dpop_bound_access_tokens": true,
72
-
// "jwks_uri": fmt.Sprintf("%s/oauth/jwks.json", serverUrlRoot),
73
-
// "scope": "atproto transition:generic",
74
-
// "token_endpoint_auth_method": "private_key_jwt",
75
-
// "token_endpoint_auth_signing_alg": "ES256",
76
-
// }
77
78
if platform == "web" {
79
meta.RedirectURIs = []string{fmt.Sprintf("https://%s/xrpc/place.stream.account.oauthReturn", host)}
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
80
meta.ApplicationType = "web"
81
} else {
82
meta.RedirectURIs = []string{fmt.Sprintf("https://%s/api/app-return/%s", host, appBundleId)}
···
42
return &b
43
}
44
45
+
func GetUpstreamMetadata(host string, platform string, appBundleId string) *OAuthClientMetadata {
46
meta := &OAuthClientMetadata{
47
+
ClientID: fmt.Sprintf("https://%s/api/atproto-oauth/upstream/%s", host, platform),
48
JwksURI: fmt.Sprintf("https://%s/api/atproto-oauth/jwks.json", host),
49
ClientURI: fmt.Sprintf("https://%s", host),
50
// RedirectURIs: []string{fmt.Sprintf("https://%s/login", host)},
···
56
DPoPBoundAccessTokens: boolPtr(true),
57
TokenEndpointAuthSigningAlg: "ES256",
58
}
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
59
60
if platform == "web" {
61
meta.RedirectURIs = []string{fmt.Sprintf("https://%s/xrpc/place.stream.account.oauthReturn", host)}
62
+
meta.ApplicationType = "web"
63
+
} else {
64
+
meta.RedirectURIs = []string{fmt.Sprintf("https://%s/api/app-return/%s", host, appBundleId)}
65
+
meta.ApplicationType = "native"
66
+
}
67
+
return meta
68
+
}
69
+
70
+
func GetDownstreamMetadata(host string, platform string, appBundleId string) *OAuthClientMetadata {
71
+
meta := &OAuthClientMetadata{
72
+
ClientID: fmt.Sprintf("https://%s/api/atproto-oauth/downstream/%s", host, platform),
73
+
ClientURI: fmt.Sprintf("https://%s", host),
74
+
// RedirectURIs: []string{fmt.Sprintf("https://%s/login", host)},
75
+
Scope: "atproto transition:generic",
76
+
TokenEndpointAuthMethod: "none",
77
+
ClientName: "Streamplace",
78
+
ResponseTypes: []string{"code"},
79
+
GrantTypes: []string{"authorization_code", "refresh_token"},
80
+
DPoPBoundAccessTokens: boolPtr(true),
81
+
}
82
+
if platform == "web" {
83
+
meta.RedirectURIs = []string{fmt.Sprintf("https://%s/login", host)}
84
meta.ApplicationType = "web"
85
} else {
86
meta.RedirectURIs = []string{fmt.Sprintf("https://%s/api/app-return/%s", host, appBundleId)}
+2
-2
pkg/atproto/oauth.go
pkg/atproto/oauth_upstream.go
···
22
)
23
24
func Login(ctx context.Context, cli *config.CLI, input *streamplace.AccountLogin_Input, mod model.Model) (*streamplace.AccountDefs_LoginResponse, error) {
25
-
meta := GetMetadata("longos.iameli.link", "web", "")
26
oclient, err := oauth.NewClient(oauth.ClientArgs{
27
ClientJwk: cli.JWK,
28
ClientId: meta.ClientID,
···
116
}
117
118
func HandleOauthReturn(ctx context.Context, cli *config.CLI, code string, iss string, state string, mod model.Model) error {
119
-
meta := GetMetadata("longos.iameli.link", "web", "")
120
oclient, err := oauth.NewClient(oauth.ClientArgs{
121
ClientJwk: cli.JWK,
122
ClientId: meta.ClientID,
···
22
)
23
24
func Login(ctx context.Context, cli *config.CLI, input *streamplace.AccountLogin_Input, mod model.Model) (*streamplace.AccountDefs_LoginResponse, error) {
25
+
meta := GetUpstreamMetadata("longos.iameli.link", "web", "")
26
oclient, err := oauth.NewClient(oauth.ClientArgs{
27
ClientJwk: cli.JWK,
28
ClientId: meta.ClientID,
···
116
}
117
118
func HandleOauthReturn(ctx context.Context, cli *config.CLI, code string, iss string, state string, mod model.Model) error {
119
+
meta := GetUpstreamMetadata("longos.iameli.link", "web", "")
120
oclient, err := oauth.NewClient(oauth.ClientArgs{
121
ClientJwk: cli.JWK,
122
ClientId: meta.ClientID,