An atproto PDS written in Go

Compare changes

Choose any two refs to compare.

+4 -4
oauth/dpop/manager.go
··· 75 75 } 76 76 77 77 proof := extractProof(headers) 78 - 79 78 if proof == "" { 80 79 return nil, nil 81 80 } ··· 197 196 198 197 nonce, _ := claims["nonce"].(string) 199 198 if nonce == "" { 200 - // WARN: this _must_ be `use_dpop_nonce` for clients know they should make another request 199 + // reference impl checks if self.nonce is not null before returning an error, but we always have a 200 + // nonce so we do not bother checking 201 201 return nil, ErrUseDpopNonce 202 202 } 203 203 204 204 if nonce != "" && !dm.nonce.Check(nonce) { 205 - // WARN: this _must_ be `use_dpop_nonce` so that clients will fetch a new nonce 205 + // dpop nonce mismatch 206 206 return nil, ErrUseDpopNonce 207 207 } 208 208 ··· 237 237 } 238 238 239 239 func extractProof(headers http.Header) string { 240 - dpopHeaders := headers["Dpop"] 240 + dpopHeaders := headers.Values("dpop") 241 241 switch len(dpopHeaders) { 242 242 case 0: 243 243 return ""
+3 -3
oauth/provider/client_auth.go
··· 19 19 } 20 20 21 21 type AuthenticateClientRequestBase struct { 22 - ClientID string `form:"client_id" json:"client_id" validate:"required"` 23 - ClientAssertionType *string `form:"client_assertion_type" json:"client_assertion_type,omitempty"` 24 - ClientAssertion *string `form:"client_assertion" json:"client_assertion,omitempty"` 22 + ClientID string `form:"client_id" json:"client_id" query:"client_id" validate:"required"` 23 + ClientAssertionType *string `form:"client_assertion_type" json:"client_assertion_type,omitempty" query:"client_assertion_type"` 24 + ClientAssertion *string `form:"client_assertion" json:"client_assertion,omitempty" query:"client_assertion"` 25 25 } 26 26 27 27 func (p *Provider) AuthenticateClient(ctx context.Context, req AuthenticateClientRequestBase, proof *dpop.Proof, opts *AuthenticateClientOptions) (*client.Client, *ClientAuth, error) {
+9 -8
oauth/provider/models.go
··· 32 32 33 33 type ParRequest struct { 34 34 AuthenticateClientRequestBase 35 - ResponseType string `form:"response_type" json:"response_type" validate:"required"` 36 - CodeChallenge *string `form:"code_challenge" json:"code_challenge" validate:"required"` 37 - CodeChallengeMethod string `form:"code_challenge_method" json:"code_challenge_method" validate:"required"` 38 - State string `form:"state" json:"state" validate:"required"` 39 - RedirectURI string `form:"redirect_uri" json:"redirect_uri" validate:"required"` 40 - Scope string `form:"scope" json:"scope" validate:"required"` 41 - LoginHint *string `form:"login_hint" json:"login_hint,omitempty"` 42 - DpopJkt *string `form:"dpop_jkt" json:"dpop_jkt,omitempty"` 35 + ResponseType string `form:"response_type" json:"response_type" query:"response_type" validate:"required"` 36 + CodeChallenge *string `form:"code_challenge" json:"code_challenge" query:"code_challenge" validate:"required"` 37 + CodeChallengeMethod string `form:"code_challenge_method" json:"code_challenge_method" query:"code_challenge_method" validate:"required"` 38 + State string `form:"state" json:"state" query:"state" validate:"required"` 39 + RedirectURI string `form:"redirect_uri" json:"redirect_uri" query:"redirect_uri" validate:"required"` 40 + Scope string `form:"scope" json:"scope" query:"scope" validate:"required"` 41 + LoginHint *string `form:"login_hint" query:"login_hint" json:"login_hint,omitempty"` 42 + DpopJkt *string `form:"dpop_jkt" query:"dpop_jkt" json:"dpop_jkt,omitempty"` 43 + ResponseMode *string `form:"response_mode" json:"response_mode,omitempty" query:"response_mode"` 43 44 } 44 45 45 46 func (opr *ParRequest) Scan(value any) error {
+95 -19
server/handle_oauth_authorize.go
··· 1 1 package server 2 2 3 3 import ( 4 + "fmt" 4 5 "net/url" 5 6 "strings" 6 7 "time" ··· 8 9 "github.com/Azure/go-autorest/autorest/to" 9 10 "github.com/haileyok/cocoon/internal/helpers" 10 11 "github.com/haileyok/cocoon/oauth" 12 + "github.com/haileyok/cocoon/oauth/constants" 11 13 "github.com/haileyok/cocoon/oauth/provider" 12 14 "github.com/labstack/echo/v4" 13 15 ) 14 16 17 + type HandleOauthAuthorizeGetInput struct { 18 + RequestUri string `query:"request_uri"` 19 + } 20 + 15 21 func (s *Server) handleOauthAuthorizeGet(e echo.Context) error { 16 22 ctx := e.Request().Context() 17 23 18 - reqUri := e.QueryParam("request_uri") 19 - if reqUri == "" { 20 - // render page for logged out dev 21 - if s.config.Version == "dev" { 22 - return e.Render(200, "authorize.html", map[string]any{ 23 - "Scopes": []string{"atproto", "transition:generic"}, 24 - "AppName": "DEV MODE AUTHORIZATION PAGE", 25 - "Handle": "paula.cocoon.social", 26 - "RequestUri": "", 27 - }) 24 + logger := s.logger.With("name", "handleOauthAuthorizeGet") 25 + 26 + var input HandleOauthAuthorizeGetInput 27 + if err := e.Bind(&input); err != nil { 28 + logger.Error("error binding request", "err", err) 29 + return fmt.Errorf("error binding request") 30 + } 31 + 32 + var reqId string 33 + if input.RequestUri != "" { 34 + id, err := oauth.DecodeRequestUri(input.RequestUri) 35 + if err != nil { 36 + logger.Error("no request uri found in input", "url", e.Request().URL.String()) 37 + return helpers.InputError(e, to.StringPtr("no request uri")) 38 + } 39 + reqId = id 40 + } else { 41 + var parRequest provider.ParRequest 42 + if err := e.Bind(&parRequest); err != nil { 43 + s.logger.Error("error binding for standard auth request", "error", err) 44 + return helpers.InputError(e, to.StringPtr("InvalidRequest")) 45 + } 46 + 47 + if err := e.Validate(parRequest); err != nil { 48 + // render page for logged out dev 49 + if s.config.Version == "dev" && parRequest.ClientID == "" { 50 + return e.Render(200, "authorize.html", map[string]any{ 51 + "Scopes": []string{"atproto", "transition:generic"}, 52 + "AppName": "DEV MODE AUTHORIZATION PAGE", 53 + "Handle": "paula.cocoon.social", 54 + "RequestUri": "", 55 + }) 56 + } 57 + return helpers.InputError(e, to.StringPtr("no request uri and invalid parameters")) 28 58 } 29 - return helpers.InputError(e, to.StringPtr("no request uri")) 59 + 60 + client, clientAuth, err := s.oauthProvider.AuthenticateClient(ctx, parRequest.AuthenticateClientRequestBase, nil, &provider.AuthenticateClientOptions{ 61 + AllowMissingDpopProof: true, 62 + }) 63 + if err != nil { 64 + s.logger.Error("error authenticating client in standard request", "client_id", parRequest.ClientID, "error", err) 65 + return helpers.ServerError(e, to.StringPtr(err.Error())) 66 + } 67 + 68 + if parRequest.DpopJkt == nil { 69 + if client.Metadata.DpopBoundAccessTokens { 70 + } 71 + } else { 72 + if !client.Metadata.DpopBoundAccessTokens { 73 + msg := "dpop bound access tokens are not enabled for this client" 74 + return helpers.InputError(e, &msg) 75 + } 76 + } 77 + 78 + eat := time.Now().Add(constants.ParExpiresIn) 79 + id := oauth.GenerateRequestId() 80 + 81 + authRequest := &provider.OauthAuthorizationRequest{ 82 + RequestId: id, 83 + ClientId: client.Metadata.ClientID, 84 + ClientAuth: *clientAuth, 85 + Parameters: parRequest, 86 + ExpiresAt: eat, 87 + } 88 + 89 + if err := s.db.Create(ctx, authRequest, nil).Error; err != nil { 90 + s.logger.Error("error creating auth request in db", "error", err) 91 + return helpers.ServerError(e, nil) 92 + } 93 + 94 + input.RequestUri = oauth.EncodeRequestUri(id) 95 + reqId = id 96 + 30 97 } 31 98 32 99 repo, _, err := s.getSessionRepoOrErr(e) 33 100 if err != nil { 34 101 return e.Redirect(303, "/account/signin?"+e.QueryParams().Encode()) 35 - } 36 - 37 - reqId, err := oauth.DecodeRequestUri(reqUri) 38 - if err != nil { 39 - return helpers.InputError(e, to.StringPtr(err.Error())) 40 102 } 41 103 42 104 var req provider.OauthAuthorizationRequest ··· 60 122 data := map[string]any{ 61 123 "Scopes": scopes, 62 124 "AppName": appName, 63 - "RequestUri": reqUri, 125 + "RequestUri": input.RequestUri, 64 126 "QueryParams": e.QueryParams().Encode(), 65 127 "Handle": repo.Actor.Handle, 66 128 } ··· 129 191 q.Set("code", code) 130 192 131 193 hashOrQuestion := "?" 132 - if authReq.ClientAuth.Method != "private_key_jwt" { 133 - hashOrQuestion = "#" 194 + if authReq.Parameters.ResponseMode != nil { 195 + switch *authReq.Parameters.ResponseMode { 196 + case "fragment": 197 + hashOrQuestion = "#" 198 + case "query": 199 + // do nothing 200 + break 201 + default: 202 + if authReq.Parameters.ResponseType != "code" { 203 + hashOrQuestion = "#" 204 + } 205 + } 206 + } else { 207 + if authReq.Parameters.ResponseType != "code" { 208 + hashOrQuestion = "#" 209 + } 134 210 } 135 211 136 212 return e.Redirect(303, authReq.Parameters.RedirectURI+hashOrQuestion+q.Encode())
+1
server/handle_oauth_par.go
··· 42 42 e.Response().Header().Set("DPoP-Nonce", nonce) 43 43 e.Response().Header().Add("access-control-expose-headers", "DPoP-Nonce") 44 44 } 45 + logger.Error("nonce error: use_dpop_nonce", "headers", e.Request().Header) 45 46 return e.JSON(400, map[string]string{ 46 47 "error": "use_dpop_nonce", 47 48 })
+28 -8
server/handle_sync_subscribe_repos.go
··· 12 12 ) 13 13 14 14 func (s *Server) handleSyncSubscribeRepos(e echo.Context) error { 15 - ctx := e.Request().Context() 15 + ctx, cancel := context.WithCancel(e.Request().Context()) 16 + defer cancel() 17 + 16 18 logger := s.logger.With("component", "subscribe-repos-websocket") 17 19 18 20 conn, err := websocket.Upgrade(e.Response().Writer, e.Request(), e.Response().Header(), 1<<10, 1<<10) ··· 30 32 metrics.RelaysConnected.WithLabelValues(ident).Dec() 31 33 }() 32 34 33 - evts, cancel, err := s.evtman.Subscribe(ctx, ident, func(evt *events.XRPCStreamEvent) bool { 35 + evts, evtManCancel, err := s.evtman.Subscribe(ctx, ident, func(evt *events.XRPCStreamEvent) bool { 34 36 return true 35 37 }, nil) 36 38 if err != nil { 37 39 return err 38 40 } 39 - defer cancel() 41 + defer evtManCancel() 42 + 43 + // drop the connection whenever a subscriber disconnects from the socket, we should get errors 44 + go func() { 45 + for { 46 + select { 47 + case <-ctx.Done(): 48 + return 49 + default: 50 + if _, _, err := conn.ReadMessage(); err != nil { 51 + logger.Warn("websocket error", "err", err) 52 + cancel() 53 + return 54 + } 55 + } 56 + } 57 + }() 40 58 41 59 header := events.EventHeader{Op: events.EvtKindMessage} 42 60 for evt := range evts { ··· 97 115 98 116 // we should tell the relay to request a new crawl at this point if we got disconnected 99 117 // use a new context since the old one might be cancelled at this point 100 - ctx, cancel = context.WithTimeout(context.Background(), 10*time.Second) 101 - defer cancel() 102 - if err := s.requestCrawl(ctx); err != nil { 103 - logger.Error("error requesting crawls", "err", err) 104 - } 118 + go func() { 119 + retryCtx, retryCancel := context.WithTimeout(context.Background(), 10*time.Second) 120 + defer retryCancel() 121 + if err := s.requestCrawl(retryCtx); err != nil { 122 + logger.Error("error requesting crawls", "err", err) 123 + } 124 + }() 105 125 106 126 return nil 107 127 }