pomerium/internal/rfc7591/format.go
Denis Mishin 777b3b12d2
mcp: client registration/token fixes (#5649)
## Summary

Fixes to MCP code registration and token requests. 

1. ease some requirements on fields that are RECOMMENDED 
2. fill in defaults
3. store both request and response in the client registration
4. check client secret in the /token request

## Related issues

- Fixes
https://linear.app/pomerium/issue/ENG-2462/mcp-ignore-unknown-grant-types-in-the-client-registration
- Fixes
https://linear.app/pomerium/issue/ENG-2461/mcp-support-client-secret-in-dynamic-client-registration
 
## User Explanation

<!-- How would you explain this change to the user? If this
change doesn't create any user-facing changes, you can leave
this blank. If filled out, add the `docs` label -->

## Checklist

- [x] reference any related issues
- [x] updated unit tests
- [x] add appropriate label (`enhancement`, `bug`, `breaking`,
`dependencies`, `ci`)
- [ ] ready for review
2025-06-11 11:28:24 -04:00

103 lines
2.6 KiB
Go

package rfc7591v1
import (
"encoding/json"
"fmt"
"io"
"github.com/bufbuild/protovalidate-go"
"google.golang.org/protobuf/encoding/protojson"
"google.golang.org/protobuf/proto"
)
const (
TokenEndpointAuthMethodNone = "none"
TokenEndpointAuthMethodClientSecretBasic = "client_secret_basic"
TokenEndpointAuthMethodClientSecretPost = "client_secret_post"
GrantTypesAuthorizationCode = "authorization_code"
GrantTypesImplicit = "implicit"
GrantTypesPassword = "password"
GrantTypesClientCredentials = "client_credentials"
GrantTypesRefreshToken = "refresh_token"
GrantTypesJWTBearer = "urn:ietf:params:oauth:grant-type:jwt-bearer" //nolint:gosec
GrantTypesSAML2Bearer = "urn:ietf:params:oauth:grant-type:saml2-bearer" //nolint:gosec
GrantTypesDeviceCode = "urn:ietf:params:oauth:grant-type:device_code" //nolint:gosec
ResponseTypesCode = "code"
ResponseTypeToken = "token"
)
func (v *Metadata) SetDefaults() {
if v.TokenEndpointAuthMethod == nil {
v.TokenEndpointAuthMethod = proto.String(TokenEndpointAuthMethodClientSecretBasic)
}
if len(v.GrantTypes) == 0 {
v.GrantTypes = []string{GrantTypesAuthorizationCode}
}
if len(v.ResponseTypes) == 0 {
v.ResponseTypes = []string{ResponseTypesCode}
}
}
func (v *Metadata) Validate() error {
return protovalidate.Validate(v)
}
func ParseMetadata(
data []byte,
) (*Metadata, error) {
v := new(Metadata)
err := protojson.UnmarshalOptions{
AllowPartial: false,
DiscardUnknown: true,
}.Unmarshal(data, v)
if err != nil {
return nil, err
}
return v, nil
}
func WriteRegistrationResponse(
w io.Writer,
clientID string,
clientSecret *ClientSecret,
metadata *Metadata,
) error {
var metadataJSON map[string]any
if metadata == nil {
return fmt.Errorf("metadata cannot be nil")
}
metadataBytes, err := protojson.MarshalOptions{
UseProtoNames: true,
EmitUnpopulated: false,
}.Marshal(metadata)
if err != nil {
return err
}
if err := json.Unmarshal(metadataBytes, &metadataJSON); err != nil {
return err
}
metadataJSON["client_id"] = clientID
if clientSecret != nil {
metadataJSON["client_secret"] = clientSecret.Value
if clientSecret.CreatedAt != nil {
metadataJSON["client_id_issued_at"] = clientSecret.CreatedAt.Seconds
}
// Per RFC 7591: client_secret_expires_at is REQUIRED if client_secret is issued
// Value should be 0 if the secret doesn't expire
if clientSecret.ExpiresAt != nil {
metadataJSON["client_secret_expires_at"] = clientSecret.ExpiresAt.Seconds
} else {
metadataJSON["client_secret_expires_at"] = int64(0)
}
}
return json.NewEncoder(w).Encode(metadataJSON)
}