diff --git a/appview/config.go b/appview/config.go index 7d82b281..c4ca3309 100644 --- a/appview/config.go +++ b/appview/config.go @@ -15,8 +15,7 @@ type CoreConfig struct { } type OAuthConfig struct { - Jwks string `env:"JWKS"` - ServerMetadataUrl string `env:"SERVER_METADATA_URL"` + Jwks string `env:"JWKS"` } type JetstreamConfig struct { diff --git a/appview/oauth/handler/handler.go b/appview/oauth/handler/handler.go index bf768b34..dbdfcfcb 100644 --- a/appview/oauth/handler/handler.go +++ b/appview/oauth/handler/handler.go @@ -51,25 +51,9 @@ func (o *OAuthHandler) Router() http.Handler { } func (o *OAuthHandler) clientMetadata(w http.ResponseWriter, r *http.Request) { - metadata := map[string]any{ - "client_id": o.Config.OAuth.ServerMetadataUrl, - "client_name": "Tangled", - "subject_type": "public", - "client_uri": o.Config.Core.AppviewHost, - "redirect_uris": []string{fmt.Sprintf("%s/oauth/callback", o.Config.Core.AppviewHost)}, - "grant_types": []string{"authorization_code", "refresh_token"}, - "response_types": []string{"code"}, - "application_type": "web", - "dpop_bound_access_tokens": true, - "jwks_uri": fmt.Sprintf("%s/oauth/jwks.json", o.Config.Core.AppviewHost), - "scope": "atproto transition:generic", - "token_endpoint_auth_method": "private_key_jwt", - "token_endpoint_auth_signing_alg": "ES256", - } - w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) - json.NewEncoder(w).Encode(metadata) + json.NewEncoder(w).Encode(o.OAuth.ClientMetadata()) } func (o *OAuthHandler) jwks(w http.ResponseWriter, r *http.Request) { @@ -101,10 +85,12 @@ func (o *OAuthHandler) login(w http.ResponseWriter, r *http.Request) { o.Pages.Notice(w, "login-msg", fmt.Sprintf("\"%s\" is an invalid handle.", handle)) return } + self := o.OAuth.ClientMetadata() oauthClient, err := client.NewClient( - o.Config.OAuth.ServerMetadataUrl, + self.ClientID, o.Config.OAuth.Jwks, - fmt.Sprintf("%s/oauth/callback", o.Config.Core.AppviewHost)) + self.RedirectURIs[0], + ) if err != nil { log.Println("failed to create oauth client:", err) @@ -164,7 +150,10 @@ func (o *OAuthHandler) login(w http.ResponseWriter, r *http.Request) { } u, _ := url.Parse(authMeta.AuthorizationEndpoint) - u.RawQuery = fmt.Sprintf("client_id=%s&request_uri=%s", url.QueryEscape(o.Config.OAuth.ServerMetadataUrl), parResp.RequestUri) + query := url.Values{} + query.Add("client_id", self.ClientID) + query.Add("request_uri", parResp.RequestUri) + u.RawQuery = query.Encode() o.Pages.HxRedirect(w, u.String()) } } @@ -186,6 +175,14 @@ func (o *OAuthHandler) callback(w http.ResponseWriter, r *http.Request) { } }() + error := r.FormValue("error") + errorDescription := r.FormValue("error_description") + if error != "" || errorDescription != "" { + log.Printf("error: %s, %s", error, errorDescription) + o.Pages.Notice(w, "login-msg", "Failed to authenticate. Try again later.") + return + } + code := r.FormValue("code") if code == "" { log.Println("missing code for state: ", state) @@ -200,10 +197,13 @@ func (o *OAuthHandler) callback(w http.ResponseWriter, r *http.Request) { return } + self := o.OAuth.ClientMetadata() + oauthClient, err := client.NewClient( - o.Config.OAuth.ServerMetadataUrl, + self.ClientID, o.Config.OAuth.Jwks, - fmt.Sprintf("%s/oauth/callback", o.Config.Core.AppviewHost)) + self.RedirectURIs[0], + ) if err != nil { log.Println("failed to create oauth client:", err) diff --git a/appview/oauth/oauth.go b/appview/oauth/oauth.go index 1cdd190b..ab4e0476 100644 --- a/appview/oauth/oauth.go +++ b/appview/oauth/oauth.go @@ -4,6 +4,7 @@ import ( "fmt" "log" "net/http" + "net/url" "time" "github.com/gorilla/sessions" @@ -113,9 +114,14 @@ func (o *OAuth) GetSession(r *http.Request) (*db.OAuthSession, bool, error) { if err != nil { return nil, false, err } - oauthClient, err := client.NewClient(o.Config.OAuth.ServerMetadataUrl, + + self := o.ClientMetadata() + + oauthClient, err := client.NewClient( + self.ClientID, o.Config.OAuth.Jwks, - fmt.Sprintf("%s/oauth/callback", o.Config.Core.AppviewHost)) + self.RedirectURIs[0], + ) if err != nil { return nil, false, err @@ -206,3 +212,57 @@ func (o *OAuth) AuthorizedClient(r *http.Request) (*xrpc.Client, error) { return xrpcClient, nil } + +type ClientMetadata struct { + ClientID string `json:"client_id"` + ClientName string `json:"client_name"` + SubjectType string `json:"subject_type"` + ClientURI string `json:"client_uri"` + RedirectURIs []string `json:"redirect_uris"` + GrantTypes []string `json:"grant_types"` + ResponseTypes []string `json:"response_types"` + ApplicationType string `json:"application_type"` + DpopBoundAccessTokens bool `json:"dpop_bound_access_tokens"` + JwksURI string `json:"jwks_uri"` + Scope string `json:"scope"` + TokenEndpointAuthMethod string `json:"token_endpoint_auth_method"` + TokenEndpointAuthSigningAlg string `json:"token_endpoint_auth_signing_alg"` +} + +func (o *OAuth) ClientMetadata() ClientMetadata { + makeRedirectURIs := func(c string) []string { + return []string{fmt.Sprintf("%s/oauth/callback", c)} + } + + clientURI := o.Config.Core.AppviewHost + clientID := fmt.Sprintf("%s/oauth/client-metadata.json", clientURI) + redirectURIs := makeRedirectURIs(clientURI) + + if o.Config.Core.Dev { + clientURI = fmt.Sprintf("http://127.0.0.1:3000") + redirectURIs = makeRedirectURIs(clientURI) + + query := url.Values{} + query.Add("redirect_uri", redirectURIs[0]) + query.Add("scope", "atproto transition:generic") + clientID = fmt.Sprintf("http://localhost?%s", query.Encode()) + } + + jwksURI := fmt.Sprintf("%s/oauth/jwks.json", clientURI) + + return ClientMetadata{ + ClientID: clientID, + ClientName: "Tangled", + SubjectType: "public", + ClientURI: clientURI, + RedirectURIs: redirectURIs, + GrantTypes: []string{"authorization_code", "refresh_token"}, + ResponseTypes: []string{"code"}, + ApplicationType: "web", + DpopBoundAccessTokens: true, + JwksURI: jwksURI, + Scope: "atproto transition:generic", + TokenEndpointAuthMethod: "private_key_jwt", + TokenEndpointAuthSigningAlg: "ES256", + } +} diff --git a/flake.lock b/flake.lock index 219e15e6..83ce5a36 100644 --- a/flake.lock +++ b/flake.lock @@ -64,7 +64,7 @@ "inter-fonts-src": { "flake": false, "locked": { - "lastModified": 1731705360, + "lastModified": 1731687360, "narHash": "sha256-5vdKKvHAeZi6igrfpbOdhZlDX2/5+UvzlnCQV6DdqoQ=", "type": "tarball", "url": "https://github.com/rsms/inter/releases/download/v4.1/Inter-4.1.zip" @@ -89,11 +89,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1746663147, - "narHash": "sha256-Ua0drDHawlzNqJnclTJGf87dBmaO/tn7iZ+TCkTRpRc=", + "lastModified": 1746904237, + "narHash": "sha256-3e+AVBczosP5dCLQmMoMEogM57gmZ2qrVSrmq9aResQ=", "owner": "nixos", "repo": "nixpkgs", - "rev": "dda3dcd3fe03e991015e9a74b22d35950f264a54", + "rev": "d89fc19e405cb2d55ce7cc114356846a0ee5e956", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index 76ed17ce..1a307c44 100644 --- a/flake.nix +++ b/flake.nix @@ -171,7 +171,7 @@ air-watcher = name: pkgs.writeShellScriptBin "run" '' - TANGLED_DEV=true ${pkgs.air}/bin/air -c /dev/null \ + ${pkgs.air}/bin/air -c /dev/null \ -build.cmd "${pkgs.go}/bin/go build -o ./out/${name}.out ./cmd/${name}/main.go" \ -build.bin "./out/${name}.out" \ -build.stop_on_error "true" \