diff --git a/authorize/evaluator/evaluator.go b/authorize/evaluator/evaluator.go index e0d5b7026..b6ebe7c58 100644 --- a/authorize/evaluator/evaluator.go +++ b/authorize/evaluator/evaluator.go @@ -292,6 +292,7 @@ var internalPathsNeedingLogin = set.From([]string{ "/.pomerium/webauthn", "/.pomerium/routes", "/.pomerium/api/v1/routes", + "/.pomerium/mcp/authorize", }) func (e *Evaluator) evaluateInternal(_ context.Context, req *Request) (*PolicyResponse, error) { diff --git a/go.mod b/go.mod index 27484a601..db043331d 100644 --- a/go.mod +++ b/go.mod @@ -71,7 +71,6 @@ require ( github.com/tniswong/go.rfcx v0.0.0-20181019234604-07783c52761f github.com/volatiletech/null/v9 v9.0.0 github.com/yuin/gopher-lua v1.1.1 - github.com/zeebo/assert v1.3.1 github.com/zeebo/xxh3 v1.0.2 go.opencensus.io v0.24.0 go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 @@ -233,6 +232,7 @@ require ( github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/yashtewari/glob-intersection v0.2.0 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect + github.com/zeebo/assert v1.3.1 // indirect github.com/zeebo/blake3 v0.2.4 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.opentelemetry.io/contrib/detectors/gcp v1.34.0 // indirect diff --git a/internal/mcp/handler_authorization.go b/internal/mcp/handler_authorization.go index 2fd667ffb..0021d904e 100644 --- a/internal/mcp/handler_authorization.go +++ b/internal/mcp/handler_authorization.go @@ -3,13 +3,16 @@ package mcp import ( "context" "errors" + "fmt" "net/http" "net/url" "time" + "github.com/go-jose/go-jose/v3/jwt" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" + "github.com/pomerium/pomerium/internal/httputil" "github.com/pomerium/pomerium/internal/log" "github.com/pomerium/pomerium/internal/oauth21" oauth21proto "github.com/pomerium/pomerium/internal/oauth21/gen" @@ -24,7 +27,14 @@ func (srv *Handler) Authorize(w http.ResponseWriter, r *http.Request) { ctx := r.Context() - v, err := oauth21.ParseCodeGrantAuthorizeRequest(r) + sessionID, err := getSessionFromRequest(r) + if err != nil { + log.Ctx(ctx).Error().Err(err).Msg("session is not present, this is a misconfigured request") + http.Error(w, "internal server error", http.StatusInternalServerError) + return + } + + v, err := oauth21.ParseCodeGrantAuthorizeRequest(r, sessionID) if err != nil { log.Ctx(ctx).Error().Err(err).Msg("failed to parse authorization request") oauth21.ErrorResponse(w, http.StatusBadRequest, oauth21.InvalidRequest) @@ -94,3 +104,23 @@ func (srv *Handler) AuthorizationResponse( to.RawQuery = q.Encode() http.Redirect(w, r, to.String(), http.StatusFound) } + +func getSessionFromRequest(r *http.Request) (string, error) { + h := r.Header.Get(httputil.HeaderPomeriumJWTAssertion) + if h == "" { + return "", fmt.Errorf("missing %s header", httputil.HeaderPomeriumJWTAssertion) + } + + token, err := jwt.ParseSigned(h) + if err != nil { + return "", fmt.Errorf("failed to parse JWT: %w", err) + } + var m map[string]any + _ = token.UnsafeClaimsWithoutVerification(&m) + sessionID, ok := m["sid"].(string) + if !ok { + return "", fmt.Errorf("missing session ID in JWT") + } + + return sessionID, nil +} diff --git a/internal/oauth21/authorize.go b/internal/oauth21/authorize.go index fce6aceb0..bd0fc0d4d 100644 --- a/internal/oauth21/authorize.go +++ b/internal/oauth21/authorize.go @@ -12,7 +12,7 @@ import ( // ParseCodeGrantAuthorizeRequest parses the authorization request for the code grant flow. // see https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-12#section-4.1.1 // scopes are ignored -func ParseCodeGrantAuthorizeRequest(r *http.Request) (*gen.AuthorizationRequest, error) { +func ParseCodeGrantAuthorizeRequest(r *http.Request, sessionID string) (*gen.AuthorizationRequest, error) { if err := r.ParseForm(); err != nil { return nil, fmt.Errorf("failed to parse form: %w", err) } @@ -24,6 +24,7 @@ func ParseCodeGrantAuthorizeRequest(r *http.Request) (*gen.AuthorizationRequest, State: optionalFormParam(r, "state"), CodeChallenge: r.Form.Get("code_challenge"), CodeChallengeMethod: optionalFormParam(r, "code_challenge_method"), + SessionId: sessionID, } if err := protovalidate.Validate(v); err != nil { diff --git a/internal/oauth21/gen/authorization_request.pb.go b/internal/oauth21/gen/authorization_request.pb.go index 756e88042..5f465c9e7 100644 --- a/internal/oauth21/gen/authorization_request.pb.go +++ b/internal/oauth21/gen/authorization_request.pb.go @@ -48,8 +48,11 @@ type AuthorizationRequest struct { // OPTIONAL, defaults to plain if not present in the request. Code verifier // transformation method is S256 or plain. CodeChallengeMethod *string `protobuf:"bytes,7,opt,name=code_challenge_method,json=codeChallengeMethod,proto3,oneof" json:"code_challenge_method,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // session this authorization request is associated with. + // This is a Pomerium implementation specific field. + SessionId string `protobuf:"bytes,8,opt,name=session_id,json=sessionId,proto3" json:"session_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *AuthorizationRequest) Reset() { @@ -131,6 +134,13 @@ func (x *AuthorizationRequest) GetCodeChallengeMethod() string { return "" } +func (x *AuthorizationRequest) GetSessionId() string { + if x != nil { + return x.SessionId + } + return "" +} + var File_authorization_request_proto protoreflect.FileDescriptor var file_authorization_request_proto_rawDesc = string([]byte{ @@ -138,7 +148,7 @@ var file_authorization_request_proto_rawDesc = string([]byte{ 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x07, 0x6f, 0x61, 0x75, 0x74, 0x68, 0x32, 0x31, 0x1a, 0x1b, 0x62, 0x75, 0x66, 0x2f, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x2f, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x2e, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x22, 0x83, 0x03, 0x0a, 0x14, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, + 0x6f, 0x74, 0x6f, 0x22, 0xaa, 0x03, 0x0a, 0x14, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x23, 0x0a, 0x09, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x06, 0xba, 0x48, 0x03, 0xc8, 0x01, 0x01, 0x52, 0x08, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x49, @@ -159,21 +169,24 @@ var file_authorization_request_proto_rawDesc = string([]byte{ 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x42, 0x12, 0xba, 0x48, 0x0f, 0x72, 0x0d, 0x52, 0x04, 0x53, 0x32, 0x35, 0x36, 0x52, 0x05, 0x70, 0x6c, 0x61, 0x69, 0x6e, 0x48, 0x02, 0x52, 0x13, 0x63, 0x6f, 0x64, 0x65, 0x43, 0x68, 0x61, 0x6c, 0x6c, 0x65, 0x6e, 0x67, 0x65, 0x4d, 0x65, 0x74, 0x68, 0x6f, - 0x64, 0x88, 0x01, 0x01, 0x42, 0x0f, 0x0a, 0x0d, 0x5f, 0x72, 0x65, 0x64, 0x69, 0x72, 0x65, 0x63, - 0x74, 0x5f, 0x75, 0x72, 0x69, 0x42, 0x08, 0x0a, 0x06, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x65, 0x42, - 0x18, 0x0a, 0x16, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x5f, 0x63, 0x68, 0x61, 0x6c, 0x6c, 0x65, 0x6e, - 0x67, 0x65, 0x5f, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x42, 0xac, 0x01, 0x0a, 0x0b, 0x63, 0x6f, - 0x6d, 0x2e, 0x6f, 0x61, 0x75, 0x74, 0x68, 0x32, 0x31, 0x42, 0x19, 0x41, 0x75, 0x74, 0x68, 0x6f, - 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x50, - 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x46, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, - 0x6f, 0x6d, 0x2f, 0x62, 0x75, 0x66, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x2f, 0x62, 0x75, 0x66, 0x2d, - 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x73, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x76, 0x61, - 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x2f, 0x71, 0x75, 0x69, 0x63, 0x6b, 0x73, 0x74, 0x61, 0x72, - 0x74, 0x2d, 0x67, 0x6f, 0x2f, 0x73, 0x74, 0x61, 0x72, 0x74, 0x2f, 0x67, 0x65, 0x6e, 0xa2, 0x02, - 0x03, 0x4f, 0x58, 0x58, 0xaa, 0x02, 0x07, 0x4f, 0x61, 0x75, 0x74, 0x68, 0x32, 0x31, 0xca, 0x02, - 0x07, 0x4f, 0x61, 0x75, 0x74, 0x68, 0x32, 0x31, 0xe2, 0x02, 0x13, 0x4f, 0x61, 0x75, 0x74, 0x68, - 0x32, 0x31, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, - 0x07, 0x4f, 0x61, 0x75, 0x74, 0x68, 0x32, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x64, 0x88, 0x01, 0x01, 0x12, 0x25, 0x0a, 0x0a, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, + 0x69, 0x64, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x42, 0x06, 0xba, 0x48, 0x03, 0xc8, 0x01, 0x01, + 0x52, 0x09, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x42, 0x0f, 0x0a, 0x0d, 0x5f, + 0x72, 0x65, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x5f, 0x75, 0x72, 0x69, 0x42, 0x08, 0x0a, 0x06, + 0x5f, 0x73, 0x74, 0x61, 0x74, 0x65, 0x42, 0x18, 0x0a, 0x16, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x5f, + 0x63, 0x68, 0x61, 0x6c, 0x6c, 0x65, 0x6e, 0x67, 0x65, 0x5f, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, + 0x42, 0xac, 0x01, 0x0a, 0x0b, 0x63, 0x6f, 0x6d, 0x2e, 0x6f, 0x61, 0x75, 0x74, 0x68, 0x32, 0x31, + 0x42, 0x19, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x46, 0x67, + 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x62, 0x75, 0x66, 0x62, 0x75, 0x69, + 0x6c, 0x64, 0x2f, 0x62, 0x75, 0x66, 0x2d, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x73, 0x2f, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x2f, 0x71, 0x75, + 0x69, 0x63, 0x6b, 0x73, 0x74, 0x61, 0x72, 0x74, 0x2d, 0x67, 0x6f, 0x2f, 0x73, 0x74, 0x61, 0x72, + 0x74, 0x2f, 0x67, 0x65, 0x6e, 0xa2, 0x02, 0x03, 0x4f, 0x58, 0x58, 0xaa, 0x02, 0x07, 0x4f, 0x61, + 0x75, 0x74, 0x68, 0x32, 0x31, 0xca, 0x02, 0x07, 0x4f, 0x61, 0x75, 0x74, 0x68, 0x32, 0x31, 0xe2, + 0x02, 0x13, 0x4f, 0x61, 0x75, 0x74, 0x68, 0x32, 0x31, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, + 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x07, 0x4f, 0x61, 0x75, 0x74, 0x68, 0x32, 0x31, 0x62, + 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, }) var ( diff --git a/internal/oauth21/proto/authorization_request.proto b/internal/oauth21/proto/authorization_request.proto index 1402774ea..532167ce2 100644 --- a/internal/oauth21/proto/authorization_request.proto +++ b/internal/oauth21/proto/authorization_request.proto @@ -44,4 +44,8 @@ message AuthorizationRequest { // transformation method is S256 or plain. optional string code_challenge_method = 7 [ (buf.validate.field).string = {in : [ "S256", "plain" ]} ]; + + // session this authorization request is associated with. + // This is a Pomerium implementation specific field. + string session_id = 8 [ (buf.validate.field).required = true ]; } diff --git a/internal/oauth21/validate_client_test.go b/internal/oauth21/validate_client_test.go index 16e0e8010..8b70e359a 100644 --- a/internal/oauth21/validate_client_test.go +++ b/internal/oauth21/validate_client_test.go @@ -3,7 +3,7 @@ package oauth21_test import ( "testing" - "github.com/zeebo/assert" + "github.com/stretchr/testify/assert" "google.golang.org/protobuf/proto" "github.com/pomerium/pomerium/internal/oauth21"