forked from tangled.org/core
Monorepo for Tangled

appview: oauth: add dev configuration

setting TANGLED_DEV=true now lets you work on tangled without creating
ngrok/localtunnel tunnels.

authored by oppi.li and committed by Tangled 83a64370 f5da7c02

Changed files
+90 -31
appview
oauth
+1 -2
appview/config.go
··· 15 15 } 16 16 17 17 type OAuthConfig struct { 18 - Jwks string `env:"JWKS"` 19 - ServerMetadataUrl string `env:"SERVER_METADATA_URL"` 18 + Jwks string `env:"JWKS"` 20 19 } 21 20 22 21 type JetstreamConfig struct {
+22 -22
appview/oauth/handler/handler.go
··· 51 51 } 52 52 53 53 func (o *OAuthHandler) clientMetadata(w http.ResponseWriter, r *http.Request) { 54 - metadata := map[string]any{ 55 - "client_id": o.Config.OAuth.ServerMetadataUrl, 56 - "client_name": "Tangled", 57 - "subject_type": "public", 58 - "client_uri": o.Config.Core.AppviewHost, 59 - "redirect_uris": []string{fmt.Sprintf("%s/oauth/callback", o.Config.Core.AppviewHost)}, 60 - "grant_types": []string{"authorization_code", "refresh_token"}, 61 - "response_types": []string{"code"}, 62 - "application_type": "web", 63 - "dpop_bound_access_tokens": true, 64 - "jwks_uri": fmt.Sprintf("%s/oauth/jwks.json", o.Config.Core.AppviewHost), 65 - "scope": "atproto transition:generic", 66 - "token_endpoint_auth_method": "private_key_jwt", 67 - "token_endpoint_auth_signing_alg": "ES256", 68 - } 69 - 70 54 w.Header().Set("Content-Type", "application/json") 71 55 w.WriteHeader(http.StatusOK) 72 - json.NewEncoder(w).Encode(metadata) 56 + json.NewEncoder(w).Encode(o.OAuth.ClientMetadata()) 73 57 } 74 58 75 59 func (o *OAuthHandler) jwks(w http.ResponseWriter, r *http.Request) { ··· 101 85 o.Pages.Notice(w, "login-msg", fmt.Sprintf("\"%s\" is an invalid handle.", handle)) 102 86 return 103 87 } 88 + self := o.OAuth.ClientMetadata() 104 89 oauthClient, err := client.NewClient( 105 - o.Config.OAuth.ServerMetadataUrl, 90 + self.ClientID, 106 91 o.Config.OAuth.Jwks, 107 - fmt.Sprintf("%s/oauth/callback", o.Config.Core.AppviewHost)) 92 + self.RedirectURIs[0], 93 + ) 108 94 109 95 if err != nil { 110 96 log.Println("failed to create oauth client:", err) ··· 164 150 } 165 151 166 152 u, _ := url.Parse(authMeta.AuthorizationEndpoint) 167 - u.RawQuery = fmt.Sprintf("client_id=%s&request_uri=%s", url.QueryEscape(o.Config.OAuth.ServerMetadataUrl), parResp.RequestUri) 153 + query := url.Values{} 154 + query.Add("client_id", self.ClientID) 155 + query.Add("request_uri", parResp.RequestUri) 156 + u.RawQuery = query.Encode() 168 157 o.Pages.HxRedirect(w, u.String()) 169 158 } 170 159 } ··· 186 175 } 187 176 }() 188 177 178 + error := r.FormValue("error") 179 + errorDescription := r.FormValue("error_description") 180 + if error != "" || errorDescription != "" { 181 + log.Printf("error: %s, %s", error, errorDescription) 182 + o.Pages.Notice(w, "login-msg", "Failed to authenticate. Try again later.") 183 + return 184 + } 185 + 189 186 code := r.FormValue("code") 190 187 if code == "" { 191 188 log.Println("missing code for state: ", state) ··· 200 197 return 201 198 } 202 199 200 + self := o.OAuth.ClientMetadata() 201 + 203 202 oauthClient, err := client.NewClient( 204 - o.Config.OAuth.ServerMetadataUrl, 203 + self.ClientID, 205 204 o.Config.OAuth.Jwks, 206 - fmt.Sprintf("%s/oauth/callback", o.Config.Core.AppviewHost)) 205 + self.RedirectURIs[0], 206 + ) 207 207 208 208 if err != nil { 209 209 log.Println("failed to create oauth client:", err)
+62 -2
appview/oauth/oauth.go
··· 4 4 "fmt" 5 5 "log" 6 6 "net/http" 7 + "net/url" 7 8 "time" 8 9 9 10 "github.com/gorilla/sessions" ··· 113 114 if err != nil { 114 115 return nil, false, err 115 116 } 116 - oauthClient, err := client.NewClient(o.Config.OAuth.ServerMetadataUrl, 117 + 118 + self := o.ClientMetadata() 119 + 120 + oauthClient, err := client.NewClient( 121 + self.ClientID, 117 122 o.Config.OAuth.Jwks, 118 - fmt.Sprintf("%s/oauth/callback", o.Config.Core.AppviewHost)) 123 + self.RedirectURIs[0], 124 + ) 119 125 120 126 if err != nil { 121 127 return nil, false, err ··· 206 212 207 213 return xrpcClient, nil 208 214 } 215 + 216 + type ClientMetadata struct { 217 + ClientID string `json:"client_id"` 218 + ClientName string `json:"client_name"` 219 + SubjectType string `json:"subject_type"` 220 + ClientURI string `json:"client_uri"` 221 + RedirectURIs []string `json:"redirect_uris"` 222 + GrantTypes []string `json:"grant_types"` 223 + ResponseTypes []string `json:"response_types"` 224 + ApplicationType string `json:"application_type"` 225 + DpopBoundAccessTokens bool `json:"dpop_bound_access_tokens"` 226 + JwksURI string `json:"jwks_uri"` 227 + Scope string `json:"scope"` 228 + TokenEndpointAuthMethod string `json:"token_endpoint_auth_method"` 229 + TokenEndpointAuthSigningAlg string `json:"token_endpoint_auth_signing_alg"` 230 + } 231 + 232 + func (o *OAuth) ClientMetadata() ClientMetadata { 233 + makeRedirectURIs := func(c string) []string { 234 + return []string{fmt.Sprintf("%s/oauth/callback", c)} 235 + } 236 + 237 + clientURI := o.Config.Core.AppviewHost 238 + clientID := fmt.Sprintf("%s/oauth/client-metadata.json", clientURI) 239 + redirectURIs := makeRedirectURIs(clientURI) 240 + 241 + if o.Config.Core.Dev { 242 + clientURI = fmt.Sprintf("http://127.0.0.1:3000") 243 + redirectURIs = makeRedirectURIs(clientURI) 244 + 245 + query := url.Values{} 246 + query.Add("redirect_uri", redirectURIs[0]) 247 + query.Add("scope", "atproto transition:generic") 248 + clientID = fmt.Sprintf("http://localhost?%s", query.Encode()) 249 + } 250 + 251 + jwksURI := fmt.Sprintf("%s/oauth/jwks.json", clientURI) 252 + 253 + return ClientMetadata{ 254 + ClientID: clientID, 255 + ClientName: "Tangled", 256 + SubjectType: "public", 257 + ClientURI: clientURI, 258 + RedirectURIs: redirectURIs, 259 + GrantTypes: []string{"authorization_code", "refresh_token"}, 260 + ResponseTypes: []string{"code"}, 261 + ApplicationType: "web", 262 + DpopBoundAccessTokens: true, 263 + JwksURI: jwksURI, 264 + Scope: "atproto transition:generic", 265 + TokenEndpointAuthMethod: "private_key_jwt", 266 + TokenEndpointAuthSigningAlg: "ES256", 267 + } 268 + }
+4 -4
flake.lock
··· 64 64 "inter-fonts-src": { 65 65 "flake": false, 66 66 "locked": { 67 - "lastModified": 1731705360, 67 + "lastModified": 1731687360, 68 68 "narHash": "sha256-5vdKKvHAeZi6igrfpbOdhZlDX2/5+UvzlnCQV6DdqoQ=", 69 69 "type": "tarball", 70 70 "url": "https://github.com/rsms/inter/releases/download/v4.1/Inter-4.1.zip" ··· 89 89 }, 90 90 "nixpkgs": { 91 91 "locked": { 92 - "lastModified": 1746663147, 93 - "narHash": "sha256-Ua0drDHawlzNqJnclTJGf87dBmaO/tn7iZ+TCkTRpRc=", 92 + "lastModified": 1746904237, 93 + "narHash": "sha256-3e+AVBczosP5dCLQmMoMEogM57gmZ2qrVSrmq9aResQ=", 94 94 "owner": "nixos", 95 95 "repo": "nixpkgs", 96 - "rev": "dda3dcd3fe03e991015e9a74b22d35950f264a54", 96 + "rev": "d89fc19e405cb2d55ce7cc114356846a0ee5e956", 97 97 "type": "github" 98 98 }, 99 99 "original": {
+1 -1
flake.nix
··· 171 171 air-watcher = name: 172 172 pkgs.writeShellScriptBin "run" 173 173 '' 174 - TANGLED_DEV=true ${pkgs.air}/bin/air -c /dev/null \ 174 + ${pkgs.air}/bin/air -c /dev/null \ 175 175 -build.cmd "${pkgs.go}/bin/go build -o ./out/${name}.out ./cmd/${name}/main.go" \ 176 176 -build.bin "./out/${name}.out" \ 177 177 -build.stop_on_error "true" \