Skip to content

Commit 6bd3764

Browse files
committed
internal/testing: fake auth server
This is a fake OAuth authentication server, for use in testing.
1 parent 87f2224 commit 6bd3764

File tree

3 files changed

+168
-0
lines changed

3 files changed

+168
-0
lines changed

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ module github.com/modelcontextprotocol/go-sdk
33
go 1.23.0
44

55
require (
6+
github.com/golang-jwt/jwt/v5 v5.2.1
67
github.com/google/go-cmp v0.7.0
78
github.com/google/jsonschema-go v0.2.0
89
github.com/yosida95/uritemplate/v3 v3.0.2

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
2+
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
13
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
24
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
35
github.com/google/jsonschema-go v0.2.0 h1:Uh19091iHC56//WOsAd1oRg6yy1P9BpSvpjOL6RcjLQ=
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
package testing
2+
3+
import (
4+
"crypto/sha256"
5+
"encoding/base64"
6+
"encoding/json"
7+
"fmt"
8+
"log"
9+
"net/http"
10+
"time"
11+
12+
"github.com/golang-jwt/jwt/v5"
13+
)
14+
15+
const (
16+
authServerPort = ":8080"
17+
issuer = "http://localhost" + authServerPort
18+
tokenExpiry = time.Hour
19+
)
20+
21+
var jwtSigningKey = []byte("fake-secret-key")
22+
23+
type authCodeInfo struct {
24+
codeChallenge string
25+
redirectURI string
26+
}
27+
28+
// FakeAuthServer is a fake OAuth2 authorization server.
29+
type FakeAuthServer struct {
30+
server *http.Server
31+
authCodes map[string]authCodeInfo
32+
}
33+
34+
func NewFakeAuthServer() *FakeAuthServer {
35+
server := &FakeAuthServer{
36+
authCodes: make(map[string]authCodeInfo),
37+
}
38+
mux := http.NewServeMux()
39+
mux.HandleFunc("/.well-known/oauth-authorization-server", server.handleMetadata)
40+
mux.HandleFunc("/authorize", server.handleAuthorize)
41+
mux.HandleFunc("/token", server.handleToken)
42+
server.server = &http.Server{
43+
Addr: authServerPort,
44+
Handler: mux,
45+
}
46+
return server
47+
}
48+
49+
func (s *FakeAuthServer) Start() {
50+
go func() {
51+
if err := s.server.ListenAndServe(); err != http.ErrServerClosed {
52+
log.Fatalf("ListenAndServe(): %v", err)
53+
}
54+
}()
55+
}
56+
57+
func (s *FakeAuthServer) Stop() {
58+
if err := s.server.Close(); err != nil {
59+
log.Printf("Failed to stop server: %v", err)
60+
}
61+
}
62+
63+
func (s *FakeAuthServer) handleMetadata(w http.ResponseWriter, r *http.Request) {
64+
metadata := map[string]any{
65+
"issuer": issuer,
66+
"authorization_endpoint": issuer + "/authorize",
67+
"token_endpoint": issuer + "/token",
68+
"jwks_uri": issuer + "/.well-known/jwks.json",
69+
"scopes_supported": []string{"openid", "profile", "email"},
70+
"response_types_supported": []string{"code"},
71+
"grant_types_supported": []string{"authorization_code"},
72+
"token_endpoint_auth_methods_supported": []string{"none"},
73+
"code_challenge_methods_supported": []string{"S256"},
74+
}
75+
w.Header().Set("Content-Type", "application/json")
76+
json.NewEncoder(w).Encode(metadata)
77+
}
78+
79+
func (s *FakeAuthServer) handleAuthorize(w http.ResponseWriter, r *http.Request) {
80+
query := r.URL.Query()
81+
responseType := query.Get("response_type")
82+
redirectURI := query.Get("redirect_uri")
83+
codeChallenge := query.Get("code_challenge")
84+
codeChallengeMethod := query.Get("code_challenge_method")
85+
86+
if responseType != "code" {
87+
http.Error(w, "unsupported_response_type", http.StatusBadRequest)
88+
return
89+
}
90+
if redirectURI == "" {
91+
http.Error(w, "invalid_request", http.StatusBadRequest)
92+
return
93+
}
94+
if codeChallenge == "" || codeChallengeMethod != "S256" {
95+
http.Error(w, "invalid_request", http.StatusBadRequest)
96+
return
97+
}
98+
99+
authCode := "fake-auth-code-" + fmt.Sprintf("%d", time.Now().UnixNano())
100+
s.authCodes[authCode] = authCodeInfo{
101+
codeChallenge: codeChallenge,
102+
redirectURI: redirectURI,
103+
}
104+
105+
redirectURL := fmt.Sprintf("%s?code=%s&state=%s", redirectURI, authCode, query.Get("state"))
106+
http.Redirect(w, r, redirectURL, http.StatusFound)
107+
}
108+
109+
func (s *FakeAuthServer) handleToken(w http.ResponseWriter, r *http.Request) {
110+
r.ParseForm()
111+
grantType := r.Form.Get("grant_type")
112+
code := r.Form.Get("code")
113+
redirectURI := r.Form.Get("redirect_uri")
114+
codeVerifier := r.Form.Get("code_verifier")
115+
116+
if grantType != "authorization_code" {
117+
http.Error(w, "unsupported_grant_type", http.StatusBadRequest)
118+
return
119+
}
120+
121+
authCodeInfo, ok := s.authCodes[code]
122+
if !ok {
123+
http.Error(w, "invalid_grant", http.StatusBadRequest)
124+
return
125+
}
126+
delete(s.authCodes, code)
127+
128+
if authCodeInfo.redirectURI != redirectURI {
129+
http.Error(w, "invalid_grant", http.StatusBadRequest)
130+
return
131+
}
132+
133+
// PKCE verification
134+
hasher := sha256.New()
135+
hasher.Write([]byte(codeVerifier))
136+
calculatedChallenge := base64.RawURLEncoding.EncodeToString(hasher.Sum(nil))
137+
if calculatedChallenge != authCodeInfo.codeChallenge {
138+
http.Error(w, "invalid_grant", http.StatusBadRequest)
139+
return
140+
}
141+
142+
// Issue JWT
143+
now := time.Now()
144+
claims := jwt.MapClaims{
145+
"iss": issuer,
146+
"sub": "fake-user-id",
147+
"aud": "fake-client-id",
148+
"exp": now.Add(tokenExpiry).Unix(),
149+
"iat": now.Unix(),
150+
}
151+
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
152+
accessToken, err := token.SignedString(jwtSigningKey)
153+
if err != nil {
154+
http.Error(w, "server_error", http.StatusInternalServerError)
155+
return
156+
}
157+
158+
tokenResponse := map[string]any{
159+
"access_token": accessToken,
160+
"token_type": "Bearer",
161+
"expires_in": int(tokenExpiry.Seconds()),
162+
}
163+
w.Header().Set("Content-Type", "application/json")
164+
json.NewEncoder(w).Encode(tokenResponse)
165+
}

0 commit comments

Comments
 (0)