Skip to content

Commit f6b3f0e

Browse files
committed
feat: add Redis-backed storage and unify server store interface
- Add support for Redis-backed persistent storage alongside in-memory storage - Implement a factory pattern for selecting and initializing the storage backend - Refactor server initialization to use the unified store interface - Add comprehensive documentation for OAuth MCP server and storage options - Introduce extensive tests and examples for the new factory and Redis store implementations - Add Redis client dependency to project configuration Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>
1 parent f010663 commit f6b3f0e

File tree

11 files changed

+2688
-8
lines changed

11 files changed

+2688
-8
lines changed
Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
1+
# OAuth MCP Server
2+
3+
OAuth 2.0 authorization server with MCP integration, supporting multiple OAuth providers and storage backends.
4+
5+
## Features
6+
7+
- **Multiple OAuth Providers**: GitHub, Gitea, GitLab
8+
- **Flexible Storage**: Choose between in-memory or Redis-backed storage
9+
- **OAuth 2.0 with PKCE**: Full authorization code flow support
10+
- **MCP Protocol**: Authenticated MCP tools integration
11+
12+
## Quick Start
13+
14+
### Basic Usage (Memory Store)
15+
16+
```bash
17+
go run . -client_id=<your-client-id> -client_secret=<your-client-secret>
18+
```
19+
20+
This will start the server on port 8095 using in-memory storage.
21+
22+
### Using Redis Store
23+
24+
```bash
25+
# Start Redis (using Docker)
26+
docker run -d -p 6379:6379 redis:alpine
27+
28+
# Run server with Redis
29+
go run . \
30+
-client_id=<your-client-id> \
31+
-client_secret=<your-client-secret> \
32+
-store redis \
33+
-redis-addr localhost:6379
34+
```
35+
36+
### Using Different Providers
37+
38+
```bash
39+
# GitHub (default)
40+
go run . -client_id=<id> -client_secret=<secret> -provider github
41+
42+
# GitLab
43+
go run . -client_id=<id> -client_secret=<secret> -provider gitlab
44+
45+
# Gitea
46+
go run . -client_id=<id> -client_secret=<secret> -provider gitea -gitea-host https://gitea.com
47+
```
48+
49+
## Command-Line Flags
50+
51+
### Required Flags
52+
53+
- `-client_id`: OAuth 2.0 Client ID from your OAuth provider
54+
- `-client_secret`: OAuth 2.0 Client Secret from your OAuth provider
55+
56+
### Optional Flags
57+
58+
#### Server Configuration
59+
60+
- `-addr`: Server address (default: `:8095`)
61+
- `-log-level`: Logging level: DEBUG, INFO, WARN, ERROR (default: DEBUG)
62+
63+
#### OAuth Provider
64+
65+
- `-provider`: OAuth provider name (default: `github`)
66+
- Supported: `github`, `gitea`, `gitlab`
67+
- `-gitea-host`: Gitea host URL (default: `https://gitea.com`)
68+
- `-gitlab-host`: GitLab host URL (default: `https://gitlab.com`)
69+
70+
#### Storage Backend
71+
72+
- `-store`: Storage type (default: `memory`)
73+
- `memory`: In-memory storage (data lost on restart)
74+
- `redis`: Redis-backed persistent storage
75+
- `-redis-addr`: Redis server address (default: `localhost:6379`)
76+
- Only used when `-store=redis`
77+
- `-redis-password`: Redis password (optional)
78+
- Only used when `-store=redis`
79+
- `-redis-db`: Redis database number (default: 0)
80+
- Only used when `-store=redis`
81+
82+
## Storage Options
83+
84+
### Memory Store
85+
86+
**Pros:**
87+
88+
- No external dependencies
89+
- Fast access
90+
- Simple setup
91+
- Good for development/testing
92+
93+
**Cons:**
94+
95+
- Data lost on restart
96+
- Not suitable for production with multiple instances
97+
- Limited by available memory
98+
99+
**Usage:**
100+
101+
```bash
102+
go run . -client_id=<id> -client_secret=<secret> -store memory
103+
```
104+
105+
### Redis Store
106+
107+
**Pros:**
108+
109+
- Persistent storage
110+
- Suitable for production
111+
- Supports multiple server instances
112+
- Automatic expiration handling
113+
114+
**Cons:**
115+
116+
- Requires Redis server
117+
- Network dependency
118+
- Additional operational complexity
119+
120+
**Usage:**
121+
122+
```bash
123+
# Basic Redis connection
124+
go run . -client_id=<id> -client_secret=<secret> -store redis
125+
126+
# Redis with authentication
127+
go run . \
128+
-client_id=<id> \
129+
-client_secret=<secret> \
130+
-store redis \
131+
-redis-addr localhost:6379 \
132+
-redis-password mypassword \
133+
-redis-db 1
134+
```
135+
136+
## Endpoints
137+
138+
- `GET /.well-known/oauth-authorization-server`: OAuth authorization server metadata
139+
- `GET /.well-known/oauth-protected-resource`: Protected resource metadata
140+
- `GET /authorize`: OAuth authorization endpoint
141+
- `POST /token`: OAuth token endpoint
142+
- `POST /register`: Dynamic client registration endpoint
143+
- `POST /mcp`: MCP protocol endpoint (requires authentication)
144+
- `GET /mcp`: MCP protocol SSE endpoint (requires authentication)
145+
- `DELETE /mcp`: MCP protocol endpoint (requires authentication)
146+
147+
## Examples
148+
149+
### Development Setup
150+
151+
```bash
152+
# Terminal 1: Start server with memory store
153+
go run . -client_id=test-id -client_secret=test-secret
154+
155+
# Terminal 2: Run OAuth client example
156+
cd ../oauth-client
157+
go run .
158+
```
159+
160+
### Production Setup with Redis
161+
162+
```bash
163+
# 1. Start Redis
164+
docker run -d --name redis \
165+
-p 6379:6379 \
166+
redis:alpine
167+
168+
# 2. Start OAuth server
169+
go run . \
170+
-client_id=<production-client-id> \
171+
-client_secret=<production-client-secret> \
172+
-provider github \
173+
-store redis \
174+
-redis-addr localhost:6379 \
175+
-log-level INFO \
176+
-addr :8095
177+
```
178+
179+
### Using Redis Cluster
180+
181+
```bash
182+
go run . \
183+
-client_id=<id> \
184+
-client_secret=<secret> \
185+
-store redis \
186+
-redis-addr "node1:7000,node2:7001,node3:7002"
187+
```
188+
189+
## Environment Variables
190+
191+
You can also configure Redis connection using environment variables in your code:
192+
193+
```go
194+
// Example: Load from environment
195+
store, err := store.NewRedisStoreFromOptions(store.RedisOptions{
196+
Addr: os.Getenv("REDIS_ADDR"), // default: "localhost:6379"
197+
Password: os.Getenv("REDIS_PASSWORD"), // default: ""
198+
DB: 0, // or parse from env
199+
})
200+
```
201+
202+
## Testing
203+
204+
```bash
205+
# Test with curl
206+
curl http://localhost:8095/.well-known/oauth-authorization-server
207+
208+
# Register a client
209+
curl -X POST http://localhost:8095/register \
210+
-H "Content-Type: application/json" \
211+
-d '{
212+
"redirect_uris": ["http://localhost:8080/callback"],
213+
"grant_types": ["authorization_code"],
214+
"response_types": ["code"]
215+
}'
216+
```
217+
218+
## Troubleshooting
219+
220+
### Redis Connection Issues
221+
222+
If you see "failed to create Redis store" error:
223+
224+
1. Verify Redis is running:
225+
226+
```bash
227+
redis-cli ping
228+
# Should return: PONG
229+
```
230+
231+
2. Check Redis connection:
232+
233+
```bash
234+
redis-cli -h localhost -p 6379
235+
```
236+
237+
3. Verify network connectivity:
238+
239+
```bash
240+
telnet localhost 6379
241+
```
242+
243+
### Memory Store Issues
244+
245+
If authorization codes expire too quickly, they are automatically cleaned up. Default expiration is 10 minutes.
246+
247+
## See Also
248+
249+
- [Store Package Documentation](../../pkg/store/README.md)
250+
- [OAuth Client Example](../oauth-client/)
251+
- [MCP Protocol](https://github.com/mark3labs/mcp-go)

03-oauth-mcp/oauth-server/server.go

Lines changed: 41 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -82,13 +82,21 @@ func main() {
8282
var giteaHost string
8383
var gitlabHost string
8484
var logLevel string
85+
var storeType string
86+
var redisAddr string
87+
var redisPassword string
88+
var redisDB int
8589
flag.StringVar(&externalClientID, "client_id", "", "OAuth 2.0 Client ID")
8690
flag.StringVar(&externalClientSecret, "client_secret", "", "OAuth 2.0 Client Secret")
8791
flag.StringVar(&addr, "addr", ":8095", "address to listen on")
8892
flag.StringVar(&providerName, "provider", "github", "OAuth provider: github, gitea, or gitlab")
8993
flag.StringVar(&giteaHost, "gitea-host", "https://gitea.com", "Gitea host")
9094
flag.StringVar(&gitlabHost, "gitlab-host", "https://gitlab.com", "GitLab host")
9195
flag.StringVar(&logLevel, "log-level", "", "Log level (DEBUG, INFO, WARN, ERROR). Defaults to DEBUG in development, INFO in production")
96+
flag.StringVar(&storeType, "store", "memory", "Store type: memory or redis")
97+
flag.StringVar(&redisAddr, "redis-addr", "localhost:6379", "Redis address (only used when store=redis)")
98+
flag.StringVar(&redisPassword, "redis-password", "", "Redis password (only used when store=redis)")
99+
flag.IntVar(&redisDB, "redis-db", 0, "Redis database (only used when store=redis)")
92100
flag.Parse()
93101

94102
// Initialize logger with the specified log level
@@ -116,8 +124,33 @@ func main() {
116124
os.Exit(1)
117125
}
118126

119-
// Initialize Memory store
120-
memoryStore := store.NewMemoryStore()
127+
// Initialize store using factory pattern
128+
storeConfig := store.Config{
129+
Type: store.ParseStoreType(storeType),
130+
Redis: store.RedisOptions{
131+
Addr: redisAddr,
132+
Password: redisPassword,
133+
DB: redisDB,
134+
},
135+
}
136+
137+
oauthStore, err := store.NewStore(storeConfig)
138+
if err != nil {
139+
slog.Error("Failed to create store", "type", storeType, "error", err)
140+
os.Exit(1)
141+
}
142+
143+
// Log the store type being used
144+
switch storeConfig.Type {
145+
case store.StoreTypeMemory:
146+
slog.Info("Using in-memory store")
147+
case store.StoreTypeRedis:
148+
slog.Info("Using Redis store", "addr", redisAddr, "db", redisDB)
149+
// Ensure Redis connection is closed on shutdown
150+
if redisStore, ok := oauthStore.(*store.RedisStore); ok {
151+
defer redisStore.Close()
152+
}
153+
}
121154

122155
mcpServer := NewMCPServer()
123156
router := gin.Default()
@@ -187,7 +220,7 @@ func main() {
187220
}
188221

189222
// Get client
190-
client, err := memoryStore.GetClient(c.Request.Context(), clientID)
223+
client, err := oauthStore.GetClient(c.Request.Context(), clientID)
191224
if err != nil {
192225
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid client_id"})
193226
return
@@ -228,7 +261,7 @@ func main() {
228261
CreatedAt: time.Now().Unix(),
229262
ExpiresAt: time.Now().Add(10 * time.Minute).Unix(),
230263
}
231-
if err := memoryStore.SaveAuthorizationCode(c.Request.Context(), authCode); err != nil {
264+
if err := oauthStore.SaveAuthorizationCode(c.Request.Context(), authCode); err != nil {
232265
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
233266
return
234267
}
@@ -268,7 +301,7 @@ func main() {
268301
}
269302

270303
// Validate client credentials
271-
client, err := memoryStore.GetClient(c.Request.Context(), clientID)
304+
client, err := oauthStore.GetClient(c.Request.Context(), clientID)
272305
if err != nil {
273306
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid client_id"})
274307
return
@@ -281,7 +314,7 @@ func main() {
281314
}
282315

283316
// Get authorization code
284-
authCode, err := memoryStore.GetAuthorizationCode(c.Request.Context(), clientID)
317+
authCode, err := oauthStore.GetAuthorizationCode(c.Request.Context(), clientID)
285318
if err != nil || authCode == nil {
286319
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid code"})
287320
return
@@ -345,7 +378,7 @@ func main() {
345378
)
346379

347380
// Delete authorization code
348-
if err := memoryStore.DeleteAuthorizationCode(c.Request.Context(), clientID); err != nil {
381+
if err := oauthStore.DeleteAuthorizationCode(c.Request.Context(), clientID); err != nil {
349382
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to delete authorization code", "details": err.Error()})
350383
return
351384
}
@@ -394,7 +427,7 @@ func main() {
394427
ClientSecretExpiresAt: time.Now().Add(1 * time.Minute).Unix(),
395428
}
396429

397-
err := memoryStore.CreateClient(context.Background(), client)
430+
err := oauthStore.CreateClient(context.Background(), client)
398431
if err != nil {
399432
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create client", "details": err.Error()})
400433
return

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ require (
88
github.com/gin-gonic/gin v1.11.0
99
github.com/google/uuid v1.6.0
1010
github.com/mark3labs/mcp-go v0.40.0
11+
github.com/redis/rueidis v1.0.66
1112
go.opentelemetry.io/otel v1.38.0
1213
go.opentelemetry.io/otel/trace v1.38.0
1314
golang.org/x/oauth2 v0.31.0

go.sum

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w
5757
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
5858
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
5959
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
60+
github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8=
61+
github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY=
6062
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
6163
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
6264
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
@@ -65,6 +67,8 @@ github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
6567
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
6668
github.com/quic-go/quic-go v0.54.1 h1:4ZAWm0AhCb6+hE+l5Q1NAL0iRn/ZrMwqHRGQiFwj2eg=
6769
github.com/quic-go/quic-go v0.54.1/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY=
70+
github.com/redis/rueidis v1.0.66 h1:7rvyrl0vL/cAEkE97+L5v3MJ3Vg8IKz+KIxUTfT+yJk=
71+
github.com/redis/rueidis v1.0.66/go.mod h1:Lkhr2QTgcoYBhxARU7kJRO8SyVlgUuEkcJO1Y8MCluA=
6872
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
6973
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
7074
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=

0 commit comments

Comments
 (0)