diff --git a/server/handle_oauth_authorize.go b/server/handle_oauth_authorize.go index 5941942..034e4e8 100644 --- a/server/handle_oauth_authorize.go +++ b/server/handle_oauth_authorize.go @@ -12,6 +12,7 @@ import ( "github.com/haileyok/cocoon/internal/helpers" "github.com/haileyok/cocoon/oauth" "github.com/haileyok/cocoon/oauth/constants" + "github.com/haileyok/cocoon/oauth/dpop" "github.com/haileyok/cocoon/oauth/provider" "github.com/labstack/echo-contrib/session" "github.com/labstack/echo/v4" @@ -63,7 +64,24 @@ func (s *Server) handleOauthAuthorizeGet(e echo.Context) error { return helpers.InputError(e, to.StringPtr("no request uri and invalid parameters")) } - client, clientAuth, err := s.oauthProvider.AuthenticateClient(ctx, parRequest.AuthenticateClientRequestBase, nil, &provider.AuthenticateClientOptions{ + dpopProof, err := s.oauthProvider.DpopManager.CheckProof(e.Request().Method, "https://"+s.config.Hostname+e.Request().URL.String(), e.Request().Header, nil) + if err != nil { + if errors.Is(err, dpop.ErrUseDpopNonce) { + nonce := s.oauthProvider.NextNonce() + if nonce != "" { + e.Response().Header().Set("DPoP-Nonce", nonce) + e.Response().Header().Add("access-control-expose-headers", "DPoP-Nonce") + } + s.logger.Error("nonce error: use_dpop_nonce", "headers", e.Request().Header) + return e.JSON(400, map[string]string{ + "error": "use_dpop_nonce", + }) + } + s.logger.Error("error getting dpop proof", "error", err) + return helpers.InputError(e, nil) + } + + client, clientAuth, err := s.oauthProvider.AuthenticateClient(ctx, parRequest.AuthenticateClientRequestBase, dpopProof, &provider.AuthenticateClientOptions{ AllowMissingDpopProof: true, }) if err != nil { @@ -85,13 +103,18 @@ func (s *Server) handleOauthAuthorizeGet(e echo.Context) error { } if parRequest.DpopJkt == nil { - if client.Metadata.DpopBoundAccessTokens { + if client.Metadata.DpopBoundAccessTokens && dpopProof != nil { + parRequest.DpopJkt = to.StringPtr(dpopProof.JKT) } } else { if !client.Metadata.DpopBoundAccessTokens { msg := "dpop bound access tokens are not enabled for this client" return helpers.InputError(e, &msg) } + if dpopProof != nil && dpopProof.JKT != *parRequest.DpopJkt { + msg := "supplied dpop jkt does not match header dpop jkt" + return helpers.InputError(e, &msg) + } } eat := time.Now().Add(constants.ParExpiresIn) diff --git a/server/handle_oauth_par.go b/server/handle_oauth_par.go index 0ea74ed..f78f6fc 100644 --- a/server/handle_oauth_par.go +++ b/server/handle_oauth_par.go @@ -75,7 +75,7 @@ func (s *Server) handleOauthPar(e echo.Context) error { } if parRequest.DpopJkt == nil { - if client.Metadata.DpopBoundAccessTokens { + if client.Metadata.DpopBoundAccessTokens && dpopProof != nil { parRequest.DpopJkt = to.StringPtr(dpopProof.JKT) } } else { @@ -85,7 +85,7 @@ func (s *Server) handleOauthPar(e echo.Context) error { return helpers.InputError(e, &msg) } - if dpopProof.JKT != *parRequest.DpopJkt { + if dpopProof != nil && dpopProof.JKT != *parRequest.DpopJkt { msg := "supplied dpop jkt does not match header dpop jkt" logger.Error(msg) return helpers.InputError(e, &msg) diff --git a/server/handle_oauth_token.go b/server/handle_oauth_token.go index 476ae74..7e2eb73 100644 --- a/server/handle_oauth_token.go +++ b/server/handle_oauth_token.go @@ -133,8 +133,19 @@ func (s *Server) handleOauthToken(e echo.Context) error { "client_id": authReq.ClientId, } + // Determine the effective DPoP JKT: prefer the value bound in the auth + // request (from PAR or the dpop_jkt query param), but fall back to the + // proof presented at the token endpoint. This handles clients that use + // the standard (non-PAR) authorize flow without sending dpop_jkt. + effectiveJkt := "" if authReq.Parameters.DpopJkt != nil { - accessClaims["cnf"] = *authReq.Parameters.DpopJkt + effectiveJkt = *authReq.Parameters.DpopJkt + } else if proof != nil { + effectiveJkt = proof.JKT + } + + if effectiveJkt != "" { + accessClaims["cnf"] = map[string]string{"jkt": effectiveJkt} } accessToken := jwt.NewWithClaims(jwt.SigningMethodES256, accessClaims) @@ -144,10 +155,18 @@ func (s *Server) handleOauthToken(e echo.Context) error { return err } + // Ensure the stored token parameters reflect the effective DPoP binding + // so that subsequent requests (middleware JKT check, refresh) are + // consistent with the JWT's cnf claim. + tokenParams := authReq.Parameters + if effectiveJkt != "" && tokenParams.DpopJkt == nil { + tokenParams.DpopJkt = to.StringPtr(effectiveJkt) + } + if err := s.db.Create(ctx, &provider.OauthToken{ ClientId: authReq.ClientId, ClientAuth: *clientAuth, - Parameters: authReq.Parameters, + Parameters: tokenParams, ExpiresAt: eat, DeviceId: "", Sub: repo.Repo.Did, @@ -160,9 +179,8 @@ func (s *Server) handleOauthToken(e echo.Context) error { return helpers.ServerError(e, nil) } - // prob not needed tokenType := "Bearer" - if authReq.Parameters.DpopJkt != nil { + if effectiveJkt != "" { tokenType = "DPoP" } @@ -233,7 +251,7 @@ func (s *Server) handleOauthToken(e echo.Context) error { } if oauthToken.Parameters.DpopJkt != nil { - accessClaims["cnf"] = *&oauthToken.Parameters.DpopJkt + accessClaims["cnf"] = map[string]string{"jkt": *oauthToken.Parameters.DpopJkt} } accessToken := jwt.NewWithClaims(jwt.SigningMethodES256, accessClaims) diff --git a/server/handle_well_known.go b/server/handle_well_known.go index a1c4456..cf8207c 100644 --- a/server/handle_well_known.go +++ b/server/handle_well_known.go @@ -16,6 +16,12 @@ var ( "transition:email", "transition:generic", "transition:chat.bsky", + "repo", + "rpc", + "blob", + "account", + "identity", + "include", } ) diff --git a/server/middleware.go b/server/middleware.go index 187cbb0..44f0585 100644 --- a/server/middleware.go +++ b/server/middleware.go @@ -286,8 +286,8 @@ func (s *Server) handleOauthSessionMiddleware(next echo.HandlerFunc) echo.Handle }) } - if *oauthToken.Parameters.DpopJkt != proof.JKT { - logger.Error("jkt mismatch", "token", oauthToken.Parameters.DpopJkt, "proof", proof.JKT) + if !dpopJktMatches(oauthToken.Parameters.DpopJkt, proof) { + logger.Error("jkt mismatch", "token", oauthToken.Parameters.DpopJkt, "proof", proof) return helpers.InputError(e, to.StringPtr("dpop jkt mismatch")) } diff --git a/server/server.go b/server/server.go index 6896eb2..5d65a25 100644 --- a/server/server.go +++ b/server/server.go @@ -293,8 +293,13 @@ func New(args *Args) (*Server, error) { e.Use(echo_session.Middleware(sessions.NewCookieStore([]byte(args.SessionSecret)))) e.Use(echoprometheus.NewMiddleware("cocoon")) e.Use(middleware.CORSWithConfig(middleware.CORSConfig{ - AllowOrigins: []string{"*"}, - AllowHeaders: []string{"*"}, + // Use AllowOriginFunc instead of AllowOrigins: ["*"] so that the + // specific Origin is echoed back. When AllowCredentials is true, + // browsers reject "Access-Control-Allow-Origin: *" and + // "Access-Control-Allow-Headers: *" for credentialed requests. + // This was silently breaking OAuth token exchanges from browser-based + // clients (e.g. pckt.blog) that send credentials with their fetch. + AllowOriginFunc: func(origin string) (bool, error) { return true, nil }, AllowMethods: []string{"*"}, AllowCredentials: true, MaxAge: 100_000_000,