diff --git a/oauth/dpop/manager.go b/oauth/dpop/manager.go index e6bed93..103e004 100644 --- a/oauth/dpop/manager.go +++ b/oauth/dpop/manager.go @@ -75,7 +75,6 @@ func (dm *Manager) CheckProof(reqMethod, reqUrl string, headers http.Header, acc } proof := extractProof(headers) - if proof == "" { return nil, nil } @@ -197,12 +196,13 @@ func (dm *Manager) CheckProof(reqMethod, reqUrl string, headers http.Header, acc nonce, _ := claims["nonce"].(string) if nonce == "" { - // WARN: this _must_ be `use_dpop_nonce` for clients know they should make another request + // reference impl checks if self.nonce is not null before returning an error, but we always have a + // nonce so we do not bother checking return nil, ErrUseDpopNonce } if nonce != "" && !dm.nonce.Check(nonce) { - // WARN: this _must_ be `use_dpop_nonce` so that clients will fetch a new nonce + // dpop nonce mismatch return nil, ErrUseDpopNonce } @@ -237,7 +237,7 @@ func (dm *Manager) CheckProof(reqMethod, reqUrl string, headers http.Header, acc } func extractProof(headers http.Header) string { - dpopHeaders := headers["Dpop"] + dpopHeaders := headers.Values("dpop") switch len(dpopHeaders) { case 0: return "" diff --git a/oauth/provider/client_auth.go b/oauth/provider/client_auth.go index 0f404ab..1e4a620 100644 --- a/oauth/provider/client_auth.go +++ b/oauth/provider/client_auth.go @@ -19,9 +19,9 @@ type AuthenticateClientOptions struct { } type AuthenticateClientRequestBase struct { - ClientID string `form:"client_id" json:"client_id" validate:"required"` - ClientAssertionType *string `form:"client_assertion_type" json:"client_assertion_type,omitempty"` - ClientAssertion *string `form:"client_assertion" json:"client_assertion,omitempty"` + ClientID string `form:"client_id" json:"client_id" query:"client_id" validate:"required"` + ClientAssertionType *string `form:"client_assertion_type" json:"client_assertion_type,omitempty" query:"client_assertion_type"` + ClientAssertion *string `form:"client_assertion" json:"client_assertion,omitempty" query:"client_assertion"` } func (p *Provider) AuthenticateClient(ctx context.Context, req AuthenticateClientRequestBase, proof *dpop.Proof, opts *AuthenticateClientOptions) (*client.Client, *ClientAuth, error) { diff --git a/oauth/provider/models.go b/oauth/provider/models.go index aec80d0..cf416a7 100644 --- a/oauth/provider/models.go +++ b/oauth/provider/models.go @@ -32,14 +32,15 @@ func (ca ClientAuth) Value() (driver.Value, error) { type ParRequest struct { AuthenticateClientRequestBase - ResponseType string `form:"response_type" json:"response_type" validate:"required"` - CodeChallenge *string `form:"code_challenge" json:"code_challenge" validate:"required"` - CodeChallengeMethod string `form:"code_challenge_method" json:"code_challenge_method" validate:"required"` - State string `form:"state" json:"state" validate:"required"` - RedirectURI string `form:"redirect_uri" json:"redirect_uri" validate:"required"` - Scope string `form:"scope" json:"scope" validate:"required"` - LoginHint *string `form:"login_hint" json:"login_hint,omitempty"` - DpopJkt *string `form:"dpop_jkt" json:"dpop_jkt,omitempty"` + ResponseType string `form:"response_type" json:"response_type" query:"response_type" validate:"required"` + CodeChallenge *string `form:"code_challenge" json:"code_challenge" query:"code_challenge" validate:"required"` + CodeChallengeMethod string `form:"code_challenge_method" json:"code_challenge_method" query:"code_challenge_method" validate:"required"` + State string `form:"state" json:"state" query:"state" validate:"required"` + RedirectURI string `form:"redirect_uri" json:"redirect_uri" query:"redirect_uri" validate:"required"` + Scope string `form:"scope" json:"scope" query:"scope" validate:"required"` + LoginHint *string `form:"login_hint" query:"login_hint" json:"login_hint,omitempty"` + DpopJkt *string `form:"dpop_jkt" query:"dpop_jkt" json:"dpop_jkt,omitempty"` + ResponseMode *string `form:"response_mode" json:"response_mode,omitempty" query:"response_mode"` } func (opr *ParRequest) Scan(value any) error { diff --git a/server/handle_oauth_authorize.go b/server/handle_oauth_authorize.go index ebc180a..2665c7a 100644 --- a/server/handle_oauth_authorize.go +++ b/server/handle_oauth_authorize.go @@ -1,6 +1,7 @@ package server import ( + "fmt" "net/url" "strings" "time" @@ -8,25 +9,91 @@ import ( "github.com/Azure/go-autorest/autorest/to" "github.com/haileyok/cocoon/internal/helpers" "github.com/haileyok/cocoon/oauth" + "github.com/haileyok/cocoon/oauth/constants" "github.com/haileyok/cocoon/oauth/provider" "github.com/labstack/echo/v4" ) +type HandleOauthAuthorizeGetInput struct { + RequestUri string `query:"request_uri"` +} + func (s *Server) handleOauthAuthorizeGet(e echo.Context) error { ctx := e.Request().Context() - reqUri := e.QueryParam("request_uri") - if reqUri == "" { - // render page for logged out dev - if s.config.Version == "dev" { - return e.Render(200, "authorize.html", map[string]any{ - "Scopes": []string{"atproto", "transition:generic"}, - "AppName": "DEV MODE AUTHORIZATION PAGE", - "Handle": "paula.cocoon.social", - "RequestUri": "", - }) + logger := s.logger.With("name", "handleOauthAuthorizeGet") + + var input HandleOauthAuthorizeGetInput + if err := e.Bind(&input); err != nil { + logger.Error("error binding request", "err", err) + return fmt.Errorf("error binding request") + } + + var reqId string + if input.RequestUri != "" { + id, err := oauth.DecodeRequestUri(input.RequestUri) + if err != nil { + logger.Error("no request uri found in input", "url", e.Request().URL.String()) + return helpers.InputError(e, to.StringPtr("no request uri")) + } + reqId = id + } else { + var parRequest provider.ParRequest + if err := e.Bind(&parRequest); err != nil { + s.logger.Error("error binding for standard auth request", "error", err) + return helpers.InputError(e, to.StringPtr("InvalidRequest")) } - return helpers.InputError(e, to.StringPtr("no request uri")) + + if err := e.Validate(parRequest); err != nil { + // render page for logged out dev + if s.config.Version == "dev" && parRequest.ClientID == "" { + return e.Render(200, "authorize.html", map[string]any{ + "Scopes": []string{"atproto", "transition:generic"}, + "AppName": "DEV MODE AUTHORIZATION PAGE", + "Handle": "paula.cocoon.social", + "RequestUri": "", + }) + } + return helpers.InputError(e, to.StringPtr("no request uri and invalid parameters")) + } + + client, clientAuth, err := s.oauthProvider.AuthenticateClient(ctx, parRequest.AuthenticateClientRequestBase, nil, &provider.AuthenticateClientOptions{ + AllowMissingDpopProof: true, + }) + if err != nil { + s.logger.Error("error authenticating client in standard request", "client_id", parRequest.ClientID, "error", err) + return helpers.ServerError(e, to.StringPtr(err.Error())) + } + + if parRequest.DpopJkt == nil { + if client.Metadata.DpopBoundAccessTokens { + } + } else { + if !client.Metadata.DpopBoundAccessTokens { + msg := "dpop bound access tokens are not enabled for this client" + return helpers.InputError(e, &msg) + } + } + + eat := time.Now().Add(constants.ParExpiresIn) + id := oauth.GenerateRequestId() + + authRequest := &provider.OauthAuthorizationRequest{ + RequestId: id, + ClientId: client.Metadata.ClientID, + ClientAuth: *clientAuth, + Parameters: parRequest, + ExpiresAt: eat, + } + + if err := s.db.Create(ctx, authRequest, nil).Error; err != nil { + s.logger.Error("error creating auth request in db", "error", err) + return helpers.ServerError(e, nil) + } + + input.RequestUri = oauth.EncodeRequestUri(id) + reqId = id + } repo, _, err := s.getSessionRepoOrErr(e) @@ -34,11 +101,6 @@ func (s *Server) handleOauthAuthorizeGet(e echo.Context) error { return e.Redirect(303, "/account/signin?"+e.QueryParams().Encode()) } - reqId, err := oauth.DecodeRequestUri(reqUri) - if err != nil { - return helpers.InputError(e, to.StringPtr(err.Error())) - } - var req provider.OauthAuthorizationRequest if err := s.db.Raw(ctx, "SELECT * FROM oauth_authorization_requests WHERE request_id = ?", nil, reqId).Scan(&req).Error; err != nil { return helpers.ServerError(e, to.StringPtr(err.Error())) @@ -60,7 +122,7 @@ func (s *Server) handleOauthAuthorizeGet(e echo.Context) error { data := map[string]any{ "Scopes": scopes, "AppName": appName, - "RequestUri": reqUri, + "RequestUri": input.RequestUri, "QueryParams": e.QueryParams().Encode(), "Handle": repo.Actor.Handle, } @@ -129,8 +191,22 @@ func (s *Server) handleOauthAuthorizePost(e echo.Context) error { q.Set("code", code) hashOrQuestion := "?" - if authReq.ClientAuth.Method != "private_key_jwt" { - hashOrQuestion = "#" + if authReq.Parameters.ResponseMode != nil { + switch *authReq.Parameters.ResponseMode { + case "fragment": + hashOrQuestion = "#" + case "query": + // do nothing + break + default: + if authReq.Parameters.ResponseType != "code" { + hashOrQuestion = "#" + } + } + } else { + if authReq.Parameters.ResponseType != "code" { + hashOrQuestion = "#" + } } return e.Redirect(303, authReq.Parameters.RedirectURI+hashOrQuestion+q.Encode()) diff --git a/server/handle_oauth_par.go b/server/handle_oauth_par.go index c0c64ae..b50c9c9 100644 --- a/server/handle_oauth_par.go +++ b/server/handle_oauth_par.go @@ -42,6 +42,7 @@ func (s *Server) handleOauthPar(e echo.Context) error { e.Response().Header().Set("DPoP-Nonce", nonce) e.Response().Header().Add("access-control-expose-headers", "DPoP-Nonce") } + logger.Error("nonce error: use_dpop_nonce", "headers", e.Request().Header) return e.JSON(400, map[string]string{ "error": "use_dpop_nonce", })