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