auth: issuer mix-up mitigation (#859)
This PR functions as a Reference implementation of [SEP-2468](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2468) / [RFC9207](https://datatracker.ietf.org/doc/rfc9207/). This PR hardens the MCP OAuth Client functionality against Mix-Up attacks: > Mix-up attacks aim to steal an authorization code or access token by > tricking the client into sending the authorization code or access > token to the attacker instead of the honest authorization or resource > server This PR hardens the client by adding support for a new `iss` parameter in authorization responses: - Authorization Servers broadcast support for the `iss` parameter via the `authorization_response_iss_parameter_supported` metadata parameter - If the parameter is supported, clients expect to receive the `iss` parameter in the authorization response - Clients compare the `iss` parameter in the authorization response to the `Issuer` parameter in the authorization metadata. The two must match exactly for the response to be processed. Fixes #941
This commit is contained in:
parent
4cbdd6a514
commit
2d47cc9664
11 changed files with 151 additions and 9 deletions
|
|
@ -50,6 +50,12 @@ type AuthorizationResult struct {
|
||||||
Code string
|
Code string
|
||||||
// State string returned by the authorization server.
|
// State string returned by the authorization server.
|
||||||
State string
|
State string
|
||||||
|
// Iss is the issuer identifier returned by the authorization server in the
|
||||||
|
// authorization response per [RFC 9207]. The AuthorizationCodeFetcher should
|
||||||
|
// populate this from the "iss" query parameter in the redirect URI if present.
|
||||||
|
//
|
||||||
|
// [RFC 9207]: https://www.rfc-editor.org/rfc/rfc9207
|
||||||
|
Iss string
|
||||||
}
|
}
|
||||||
|
|
||||||
// AuthorizationArgs is the input to [AuthorizationCodeFetcher].
|
// AuthorizationArgs is the input to [AuthorizationCodeFetcher].
|
||||||
|
|
@ -318,6 +324,9 @@ func (h *AuthorizationCodeHandler) Authorize(ctx context.Context, req *http.Requ
|
||||||
// Purposefully leaving the error unwrappable so it can be handled by the caller.
|
// Purposefully leaving the error unwrappable so it can be handled by the caller.
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if err := validateIssuerResponse(authRes.Iss, asm.Issuer, asm.AuthorizationResponseIssParameterSupported); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
err = h.exchangeAuthorizationCode(ctx, cfg, authRes, prm.Resource)
|
err = h.exchangeAuthorizationCode(ctx, cfg, authRes, prm.Resource)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -560,6 +569,27 @@ func (h *AuthorizationCodeHandler) getAuthorizationCode(ctx context.Context, cfg
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// validateIssuerResponse validates the "iss" parameter in an authorization response
|
||||||
|
// per [RFC 9207].
|
||||||
|
//
|
||||||
|
// [RFC 9207]: https://www.rfc-editor.org/rfc/rfc9207
|
||||||
|
func validateIssuerResponse(iss, expectedIssuer string, issParameterSupported bool) error {
|
||||||
|
if issParameterSupported {
|
||||||
|
if iss == "" {
|
||||||
|
return fmt.Errorf("authorization server advertises RFC 9207 iss parameter support but none was received in the authorization response")
|
||||||
|
}
|
||||||
|
if iss != expectedIssuer {
|
||||||
|
return fmt.Errorf("authorization response issuer %q does not match expected issuer %q", iss, expectedIssuer)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if iss != "" {
|
||||||
|
return fmt.Errorf("authorization server does not advertise RFC 9207 iss parameter support but iss was received in the authorization response")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// exchangeAuthorizationCode exchanges the authorization code for a token
|
// exchangeAuthorizationCode exchanges the authorization code for a token
|
||||||
// and stores it in a token source.
|
// and stores it in a token source.
|
||||||
func (h *AuthorizationCodeHandler) exchangeAuthorizationCode(ctx context.Context, cfg *oauth2.Config, authResult *authResult, resourceURL string) error {
|
func (h *AuthorizationCodeHandler) exchangeAuthorizationCode(ctx context.Context, cfg *oauth2.Config, authResult *authResult, resourceURL string) error {
|
||||||
|
|
|
||||||
|
|
@ -78,6 +78,7 @@ func TestAuthorize(t *testing.T) {
|
||||||
return &AuthorizationResult{
|
return &AuthorizationResult{
|
||||||
Code: location.Query().Get("code"),
|
Code: location.Query().Get("code"),
|
||||||
State: location.Query().Get("state"),
|
State: location.Query().Get("state"),
|
||||||
|
Iss: location.Query().Get("iss"),
|
||||||
}, nil
|
}, nil
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
@ -176,6 +177,7 @@ func TestAuthorize_ScopeAccumulation(t *testing.T) {
|
||||||
return &AuthorizationResult{
|
return &AuthorizationResult{
|
||||||
Code: loc.Query().Get("code"),
|
Code: loc.Query().Get("code"),
|
||||||
State: loc.Query().Get("state"),
|
State: loc.Query().Get("state"),
|
||||||
|
Iss: loc.Query().Get("iss"),
|
||||||
}, nil
|
}, nil
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
@ -736,6 +738,59 @@ func TestDynamicRegistration(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestValidateIssuerResponse(t *testing.T) {
|
||||||
|
const expectedIssuer = "https://auth.example.com"
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
iss string
|
||||||
|
issSupported bool
|
||||||
|
wantErr bool
|
||||||
|
wantErrContains string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "ValidIss",
|
||||||
|
iss: expectedIssuer,
|
||||||
|
issSupported: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "WrongIss",
|
||||||
|
iss: "https://attacker.example.com",
|
||||||
|
issSupported: true,
|
||||||
|
wantErr: true,
|
||||||
|
wantErrContains: "does not match expected issuer",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "MissingIssWhenRequired",
|
||||||
|
iss: "",
|
||||||
|
issSupported: true,
|
||||||
|
wantErr: true,
|
||||||
|
wantErrContains: "RFC 9207",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "MissingIssWhenNotRequired",
|
||||||
|
iss: "",
|
||||||
|
issSupported: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
err := validateIssuerResponse(tt.iss, expectedIssuer, tt.issSupported)
|
||||||
|
if tt.wantErr {
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("validateIssuerResponse() = nil, want error containing %q", tt.wantErrContains)
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), tt.wantErrContains) {
|
||||||
|
t.Errorf("validateIssuerResponse() error = %q, want it to contain %q", err.Error(), tt.wantErrContains)
|
||||||
|
}
|
||||||
|
} else if err != nil {
|
||||||
|
t.Fatalf("validateIssuerResponse() unexpected error = %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestInferApplicationType(t *testing.T) {
|
func TestInferApplicationType(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
|
|
|
||||||
|
|
@ -66,6 +66,11 @@ func init() {
|
||||||
"auth/token-endpoint-auth-basic",
|
"auth/token-endpoint-auth-basic",
|
||||||
"auth/token-endpoint-auth-post",
|
"auth/token-endpoint-auth-post",
|
||||||
"auth/token-endpoint-auth-none",
|
"auth/token-endpoint-auth-none",
|
||||||
|
"auth/iss-supported",
|
||||||
|
"auth/iss-not-advertised",
|
||||||
|
"auth/iss-supported-missing",
|
||||||
|
"auth/iss-wrong-issuer",
|
||||||
|
"auth/iss-unexpected",
|
||||||
}
|
}
|
||||||
for _, scenario := range authScenarios {
|
for _, scenario := range authScenarios {
|
||||||
registerScenario(scenario, runAuthClient)
|
registerScenario(scenario, runAuthClient)
|
||||||
|
|
@ -232,6 +237,7 @@ func fetchAuthorizationCodeAndState(ctx context.Context, args *auth.Authorizatio
|
||||||
return &auth.AuthorizationResult{
|
return &auth.AuthorizationResult{
|
||||||
Code: locURL.Query().Get("code"),
|
Code: locURL.Query().Get("code"),
|
||||||
State: locURL.Query().Get("state"),
|
State: locURL.Query().Get("state"),
|
||||||
|
Iss: locURL.Query().Get("iss"),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@
|
||||||
1. [Token Passthrough](#token-passthrough)
|
1. [Token Passthrough](#token-passthrough)
|
||||||
1. [Server-Side Request Forgery](#server-side-request-forgery)
|
1. [Server-Side Request Forgery](#server-side-request-forgery)
|
||||||
1. [Session Hijacking](#session-hijacking)
|
1. [Session Hijacking](#session-hijacking)
|
||||||
|
1. [Issuer Mix-Up](#issuer-mix-up)
|
||||||
1. [Utilities](#utilities)
|
1. [Utilities](#utilities)
|
||||||
1. [Cancellation](#cancellation)
|
1. [Cancellation](#cancellation)
|
||||||
1. [Ping](#ping)
|
1. [Ping](#ping)
|
||||||
|
|
@ -322,6 +323,7 @@ This handler supports:
|
||||||
- [Client ID Metadata Documents](https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization#client-id-metadata-documents)
|
- [Client ID Metadata Documents](https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization#client-id-metadata-documents)
|
||||||
- [Pre-registered clients](https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization#preregistration)
|
- [Pre-registered clients](https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization#preregistration)
|
||||||
- [Dynamic Client Registration](https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization#dynamic-client-registration)
|
- [Dynamic Client Registration](https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization#dynamic-client-registration)
|
||||||
|
- [RFC 9207](https://www.rfc-editor.org/rfc/rfc9207) Authorization Server Issuer Identification
|
||||||
|
|
||||||
To use it, configure the handler and assign it to the transport:
|
To use it, configure the handler and assign it to the transport:
|
||||||
|
|
||||||
|
|
@ -333,11 +335,12 @@ authHandler, _ := auth.NewAuthorizationCodeHandler(&auth.AuthorizationCodeHandle
|
||||||
// PreregisteredClientConfig: ...
|
// PreregisteredClientConfig: ...
|
||||||
// DynamicClientRegistrationConfig: ...
|
// DynamicClientRegistrationConfig: ...
|
||||||
AuthorizationCodeFetcher: func(ctx context.Context, args *auth.AuthorizationArgs) (*auth.AuthorizationResult, error) {
|
AuthorizationCodeFetcher: func(ctx context.Context, args *auth.AuthorizationArgs) (*auth.AuthorizationResult, error) {
|
||||||
// Open the args.URL in a browser and return the resulting code and state.
|
// Open the args.URL in a browser and return the resulting code, state, and iss.
|
||||||
// See full example in examples/auth/client/main.go.
|
// See full example in examples/auth/client/main.go.
|
||||||
code := ...
|
code := ...
|
||||||
state := ...
|
state := ...
|
||||||
return &auth.AuthorizationResult{Code: code, State: state}, nil
|
iss := ... // "iss" query parameter from the redirect URI (RFC 9207)
|
||||||
|
return &auth.AuthorizationResult{Code: code, State: state, Iss: iss}, nil
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -490,6 +493,22 @@ sets `UserID` on the returned `TokenInfo`, the streamable transport will:
|
||||||
`TokenInfo.UserID` to enable this protection. This prevents an attacker with a valid
|
`TokenInfo.UserID` to enable this protection. This prevents an attacker with a valid
|
||||||
token from hijacking another user's session by guessing or obtaining their session ID.
|
token from hijacking another user's session by guessing or obtaining their session ID.
|
||||||
|
|
||||||
|
### Issuer Mix-Up
|
||||||
|
|
||||||
|
The [mitigation](https://www.rfc-editor.org/rfc/rfc9207) against issuer mix-up attacks is
|
||||||
|
implemented per [RFC 9207](https://www.rfc-editor.org/rfc/rfc9207). The SDK client validates
|
||||||
|
the `iss` parameter in authorization responses to ensure they originated from the expected
|
||||||
|
authorization server:
|
||||||
|
|
||||||
|
- If `iss` is present in the redirect URI, the SDK verifies it matches the issuer from the
|
||||||
|
authorization server's metadata. A mismatch results in an error.
|
||||||
|
- If `iss` is absent but the authorization server advertises
|
||||||
|
`authorization_response_iss_parameter_supported: true` in its [RFC 8414](https://www.rfc-editor.org/rfc/rfc8414)
|
||||||
|
metadata, the SDK rejects the response with an error.
|
||||||
|
|
||||||
|
The `AuthorizationCodeFetcher` is responsible for extracting the `iss` query parameter from
|
||||||
|
the redirect URI and returning it in [`AuthorizationResult.Iss`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/auth#AuthorizationResult).
|
||||||
|
|
||||||
## Utilities
|
## Utilities
|
||||||
|
|
||||||
### Cancellation
|
### Cancellation
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,7 @@ func (r *codeReceiver) serveRedirectHandler(listener net.Listener) {
|
||||||
r.authChan <- &auth.AuthorizationResult{
|
r.authChan <- &auth.AuthorizationResult{
|
||||||
Code: req.URL.Query().Get("code"),
|
Code: req.URL.Query().Get("code"),
|
||||||
State: req.URL.Query().Get("state"),
|
State: req.URL.Query().Get("state"),
|
||||||
|
Iss: req.URL.Query().Get("iss"),
|
||||||
}
|
}
|
||||||
fmt.Fprint(w, "Authentication successful. You can close this window.")
|
fmt.Fprint(w, "Authentication successful. You can close this window.")
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,16 @@
|
||||||
module auth-middleware-example
|
module auth-middleware-example
|
||||||
|
|
||||||
go 1.23.0
|
go 1.25.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/golang-jwt/jwt/v5 v5.2.2
|
github.com/golang-jwt/jwt/v5 v5.3.1
|
||||||
github.com/modelcontextprotocol/go-sdk v0.3.0
|
github.com/modelcontextprotocol/go-sdk v0.3.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/google/jsonschema-go v0.4.2 // indirect
|
github.com/google/jsonschema-go v0.4.2 // indirect
|
||||||
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
|
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
|
||||||
golang.org/x/oauth2 v0.30.0 // indirect
|
golang.org/x/oauth2 v0.35.0 // indirect
|
||||||
)
|
)
|
||||||
|
|
||||||
replace github.com/modelcontextprotocol/go-sdk => ../../../
|
replace github.com/modelcontextprotocol/go-sdk => ../../../
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
|
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
|
||||||
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
github.com/google/jsonschema-go v0.3.0 h1:6AH2TxVNtk3IlvkkhjrtbUc4S8AvO0Xii0DxIygDg+Q=
|
github.com/google/jsonschema-go v0.3.0 h1:6AH2TxVNtk3IlvkkhjrtbUc4S8AvO0Xii0DxIygDg+Q=
|
||||||
|
|
@ -9,5 +10,6 @@ github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zI
|
||||||
github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
|
github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
|
||||||
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
|
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
|
||||||
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
|
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
|
||||||
|
golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||||
golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo=
|
golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo=
|
||||||
golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg=
|
golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg=
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
module github.com/modelcontextprotocol/go-sdk/examples/rate-limiting
|
module github.com/modelcontextprotocol/go-sdk/examples/rate-limiting
|
||||||
|
|
||||||
go 1.23.0
|
go 1.25.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/modelcontextprotocol/go-sdk v0.3.0
|
github.com/modelcontextprotocol/go-sdk v0.3.0
|
||||||
|
|
|
||||||
|
|
@ -247,6 +247,7 @@ This handler supports:
|
||||||
- [Client ID Metadata Documents](https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization#client-id-metadata-documents)
|
- [Client ID Metadata Documents](https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization#client-id-metadata-documents)
|
||||||
- [Pre-registered clients](https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization#preregistration)
|
- [Pre-registered clients](https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization#preregistration)
|
||||||
- [Dynamic Client Registration](https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization#dynamic-client-registration)
|
- [Dynamic Client Registration](https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization#dynamic-client-registration)
|
||||||
|
- [RFC 9207](https://www.rfc-editor.org/rfc/rfc9207) Authorization Server Issuer Identification
|
||||||
|
|
||||||
To use it, configure the handler and assign it to the transport:
|
To use it, configure the handler and assign it to the transport:
|
||||||
|
|
||||||
|
|
@ -258,11 +259,12 @@ authHandler, _ := auth.NewAuthorizationCodeHandler(&auth.AuthorizationCodeHandle
|
||||||
// PreregisteredClientConfig: ...
|
// PreregisteredClientConfig: ...
|
||||||
// DynamicClientRegistrationConfig: ...
|
// DynamicClientRegistrationConfig: ...
|
||||||
AuthorizationCodeFetcher: func(ctx context.Context, args *auth.AuthorizationArgs) (*auth.AuthorizationResult, error) {
|
AuthorizationCodeFetcher: func(ctx context.Context, args *auth.AuthorizationArgs) (*auth.AuthorizationResult, error) {
|
||||||
// Open the args.URL in a browser and return the resulting code and state.
|
// Open the args.URL in a browser and return the resulting code, state, and iss.
|
||||||
// See full example in examples/auth/client/main.go.
|
// See full example in examples/auth/client/main.go.
|
||||||
code := ...
|
code := ...
|
||||||
state := ...
|
state := ...
|
||||||
return &auth.AuthorizationResult{Code: code, State: state}, nil
|
iss := ... // "iss" query parameter from the redirect URI (RFC 9207)
|
||||||
|
return &auth.AuthorizationResult{Code: code, State: state, Iss: iss}, nil
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -415,6 +417,22 @@ sets `UserID` on the returned `TokenInfo`, the streamable transport will:
|
||||||
`TokenInfo.UserID` to enable this protection. This prevents an attacker with a valid
|
`TokenInfo.UserID` to enable this protection. This prevents an attacker with a valid
|
||||||
token from hijacking another user's session by guessing or obtaining their session ID.
|
token from hijacking another user's session by guessing or obtaining their session ID.
|
||||||
|
|
||||||
|
### Issuer Mix-Up
|
||||||
|
|
||||||
|
The [mitigation](https://www.rfc-editor.org/rfc/rfc9207) against issuer mix-up attacks is
|
||||||
|
implemented per [RFC 9207](https://www.rfc-editor.org/rfc/rfc9207). The SDK client validates
|
||||||
|
the `iss` parameter in authorization responses to ensure they originated from the expected
|
||||||
|
authorization server:
|
||||||
|
|
||||||
|
- If `iss` is present in the redirect URI, the SDK verifies it matches the issuer from the
|
||||||
|
authorization server's metadata. A mismatch results in an error.
|
||||||
|
- If `iss` is absent but the authorization server advertises
|
||||||
|
`authorization_response_iss_parameter_supported: true` in its [RFC 8414](https://www.rfc-editor.org/rfc/rfc8414)
|
||||||
|
metadata, the SDK rejects the response with an error.
|
||||||
|
|
||||||
|
The `AuthorizationCodeFetcher` is responsible for extracting the `iss` query parameter from
|
||||||
|
the redirect URI and returning it in [`AuthorizationResult.Iss`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/auth#AuthorizationResult).
|
||||||
|
|
||||||
## Utilities
|
## Utilities
|
||||||
|
|
||||||
### Cancellation
|
### Cancellation
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ import (
|
||||||
"maps"
|
"maps"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
|
"net/url"
|
||||||
"slices"
|
"slices"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
|
@ -178,6 +179,8 @@ func (s *FakeAuthorizationServer) handleMetadata(w http.ResponseWriter, r *http.
|
||||||
CodeChallengeMethodsSupported: []string{"S256"},
|
CodeChallengeMethodsSupported: []string{"S256"},
|
||||||
ClientIDMetadataDocumentSupported: cimdSupported,
|
ClientIDMetadataDocumentSupported: cimdSupported,
|
||||||
TokenEndpointAuthMethodsSupported: []string{"client_secret_post", "client_secret_basic"},
|
TokenEndpointAuthMethodsSupported: []string{"client_secret_post", "client_secret_basic"},
|
||||||
|
// Advertise RFC 9207 support: the authorize endpoint includes "iss" in responses.
|
||||||
|
AuthorizationResponseIssParameterSupported: true,
|
||||||
}
|
}
|
||||||
// Set CORS headers for cross-origin client discovery.
|
// Set CORS headers for cross-origin client discovery.
|
||||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||||
|
|
@ -267,8 +270,9 @@ func (s *FakeAuthorizationServer) handleAuthorize(w http.ResponseWriter, r *http
|
||||||
}
|
}
|
||||||
|
|
||||||
state := r.URL.Query().Get("state")
|
state := r.URL.Query().Get("state")
|
||||||
|
issuer := s.URL() + s.config.IssuerPath
|
||||||
|
|
||||||
redirectURL := fmt.Sprintf("%s?code=%s&state=%s", redirectURI, code, state)
|
redirectURL := fmt.Sprintf("%s?code=%s&state=%s&iss=%s", redirectURI, code, state, url.QueryEscape(issuer))
|
||||||
http.Redirect(w, r, redirectURL, http.StatusFound)
|
http.Redirect(w, r, redirectURL, http.StatusFound)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -114,6 +114,13 @@ type AuthServerMeta struct {
|
||||||
// ClientIDMetadataDocumentSupported is a boolean indicating whether the authorization server
|
// ClientIDMetadataDocumentSupported is a boolean indicating whether the authorization server
|
||||||
// supports client ID metadata documents.
|
// supports client ID metadata documents.
|
||||||
ClientIDMetadataDocumentSupported bool `json:"client_id_metadata_document_supported,omitempty"`
|
ClientIDMetadataDocumentSupported bool `json:"client_id_metadata_document_supported,omitempty"`
|
||||||
|
|
||||||
|
// AuthorizationResponseIssParameterSupported indicates whether the authorization server
|
||||||
|
// provides the "iss" parameter in authorization responses per [RFC 9207].
|
||||||
|
// When true, clients must verify the "iss" parameter is present and matches the Issuer field.
|
||||||
|
//
|
||||||
|
// [RFC 9207]: https://www.rfc-editor.org/rfc/rfc9207
|
||||||
|
AuthorizationResponseIssParameterSupported bool `json:"authorization_response_iss_parameter_supported,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetAuthServerMeta issues a GET request to retrieve authorization server metadata
|
// GetAuthServerMeta issues a GET request to retrieve authorization server metadata
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue