Live video on the AT Protocol

checkpoint

+333 -198
+35 -20
js/app/components/login/login.tsx
··· 68 68 > 69 69 <ChangePDS open={open} onOpenChange={onOpenChange} /> 70 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 71 + <Form 79 72 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`)); 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 87 }} 88 - margin="$4" 89 - backgroundColor="$accentColor" 90 - disabled={loginState.loading} 91 88 > 92 - <Text>{loginState.loading ? <Spinner /> : `Log in with ATProto`}</Text> 93 - </Button> 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> 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 - const u = await bluesky.client.authorize(pds); 160 + const u = await bluesky.client.authorize("scumb.ag", {}); 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 + 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 - // 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 - }; 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 + } 80 66 clientMetadataSchema.parse(meta); 81 67 return new ReactNativeOAuthClient({ 82 - fetch: (input, init) => { 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 + 83 95 console.log("!!!!!! fetch", input, init); 84 - return fetch(input, init); 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 - "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 - "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 - 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 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 - 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 - }) 132 + router.HandlerFunc("GET", "/.well-known/oauth-authorization-server", a.HandleOAuthAuthorizationServer(ctx)) 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 - apiRouter.GET(fmt.Sprintf("/api/atproto-oauth/%s", platform), a.HandleATProtoOAuth(ctx, platform)) 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)) 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 + 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 - 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 595 w.Write(bs) 685 596 } 686 597 }
+191
pkg/api/oauth.go
··· 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 42 return &b 43 43 } 44 44 45 - func GetMetadata(host string, platform string, appBundleId string) *OAuthClientMetadata { 45 + func GetUpstreamMetadata(host string, platform string, appBundleId string) *OAuthClientMetadata { 46 46 meta := &OAuthClientMetadata{ 47 - ClientID: fmt.Sprintf("https://%s/api/atproto-oauth/%s", host, platform), 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 - 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 59 78 60 if platform == "web" { 79 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)} 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 - meta := GetMetadata("longos.iameli.link", "web", "") 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 - meta := GetMetadata("longos.iameli.link", "web", "") 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,