Skip to content

Commit 03d8844

Browse files
Merge pull request #37 from wagnerdevocelot/codex/implement-database-access-adapter
Implement SQL storage adapter
2 parents 57570aa + 67e0491 commit 03d8844

2 files changed

Lines changed: 241 additions & 1 deletion

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ This example uses hardcoded configuration values and `compose.Compose` for setti
2727
* **Fosite Setup:** Uses `compose.Compose` with `compose.CommonStrategy` (`main.go`).
2828
* **HMAC Secret:** A 32-byte secret (`jwtSecret`) is defined and set in `fositeConfig.GlobalSecret`. The core HMAC strategy (`CoreStrategy`) is configured via `compose.NewOAuth2HMACStrategy(fositeConfig)`, relying on the `GlobalSecret`. **Use a strong, random secret in production.**
2929
* **OIDC Strategy:** Uses RSA keys (generated on startup) configured via `compose.NewOpenIDConnectStrategy(...)` within `CommonStrategy`. **Use persistent, securely stored RSA keys in production.**
30-
* **Storage:** Uses an in-memory store (`storage.go`). All data is lost on restart. A generic `StorageInterface` is now available for implementing persistent storage alternatives. **Replace with persistent storage (SQL, etc.) for production.**
30+
* **Storage:** Uses an in-memory store (`storage.go`). All data is lost on restart. A generic `StorageInterface` is now available for implementing persistent storage alternatives. An example SQL adapter is provided in `db_adapter.go` to demonstrate how legacy tables can be mapped to the interface. **Replace with persistent storage (SQL, etc.) for production.**
3131
* **Example Client:** Client ID `my-test-client`, secret `foobar` (hashed in `storage.go`).
3232
* **Port:** Listens on `:8080` (`main.go`).
3333
* **Session Management:** Basic, insecure in-memory sessions (`handlers.go`). **Replace with robust session handling.**

db_adapter.go

Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"database/sql"
6+
"errors"
7+
"fmt"
8+
"strings"
9+
"time"
10+
11+
"github.com/ory/fosite"
12+
)
13+
14+
// LegacyDBAdapter implements StorageInterface using a legacy SQL database.
15+
// It demonstrates how one could adapt a pre-existing schema to the
16+
// StorageInterface expected by the OAuth2 service.
17+
//
18+
// This adapter is intentionally simple and does not rely on any ORM. It uses
19+
// database/sql directly so that it can map legacy tables to the fosite models.
20+
type LegacyDBAdapter struct {
21+
DB *sql.DB
22+
}
23+
24+
// NewLegacyDBAdapter returns a new adapter instance using the given sql.DB.
25+
func NewLegacyDBAdapter(db *sql.DB) *LegacyDBAdapter {
26+
return &LegacyDBAdapter{DB: db}
27+
}
28+
29+
// ---- Entity Mapping Structures ----
30+
// These structs represent how data is stored in the legacy database.
31+
// They are converted to/from fosite models when interacting with the service.
32+
33+
type legacyClient struct {
34+
ID string
35+
Secret string
36+
RedirectURIs string // comma separated
37+
Scopes string // comma separated
38+
IsPublic bool
39+
}
40+
41+
func (lc legacyClient) toFosite() fosite.Client {
42+
return &fosite.DefaultClient{
43+
ID: lc.ID,
44+
Secret: []byte(lc.Secret),
45+
RedirectURIs: splitAndTrim(lc.RedirectURIs),
46+
Scopes: fosite.Arguments(splitAndTrim(lc.Scopes)),
47+
Public: lc.IsPublic,
48+
}
49+
}
50+
51+
func splitAndTrim(s string) []string {
52+
if s == "" {
53+
return nil
54+
}
55+
var res []string
56+
for _, part := range strings.Split(s, ",") {
57+
p := strings.TrimSpace(part)
58+
if p != "" {
59+
res = append(res, p)
60+
}
61+
}
62+
return res
63+
}
64+
65+
// ---- StorageInterface Implementation ----
66+
67+
func (a *LegacyDBAdapter) GetClient(ctx context.Context, id string) (fosite.Client, error) {
68+
row := a.DB.QueryRowContext(ctx, `SELECT id, secret, redirect_uris, scopes, is_public FROM clients WHERE id = ?`, id)
69+
var lc legacyClient
70+
if err := row.Scan(&lc.ID, &lc.Secret, &lc.RedirectURIs, &lc.Scopes, &lc.IsPublic); err != nil {
71+
if errors.Is(err, sql.ErrNoRows) {
72+
return nil, fmt.Errorf("client not found: %w", fosite.ErrNotFound)
73+
}
74+
return nil, fmt.Errorf("db query failed: %w", err)
75+
}
76+
return lc.toFosite(), nil
77+
}
78+
79+
func (a *LegacyDBAdapter) CreateClient(ctx context.Context, client fosite.Client) error {
80+
c, ok := client.(*fosite.DefaultClient)
81+
if !ok {
82+
return fmt.Errorf("unsupported client type %T", client)
83+
}
84+
_, err := a.DB.ExecContext(ctx,
85+
`INSERT INTO clients (id, secret, redirect_uris, scopes, is_public) VALUES (?, ?, ?, ?, ?)`,
86+
c.ID,
87+
string(c.Secret),
88+
strings.Join(c.RedirectURIs, ","),
89+
strings.Join(c.Scopes, ","),
90+
c.Public,
91+
)
92+
if err != nil {
93+
return fmt.Errorf("failed to insert client: %w", err)
94+
}
95+
return nil
96+
}
97+
98+
func (a *LegacyDBAdapter) UpdateClient(ctx context.Context, client fosite.Client) error {
99+
c, ok := client.(*fosite.DefaultClient)
100+
if !ok {
101+
return fmt.Errorf("unsupported client type %T", client)
102+
}
103+
_, err := a.DB.ExecContext(ctx,
104+
`UPDATE clients SET secret=?, redirect_uris=?, scopes=?, is_public=? WHERE id=?`,
105+
string(c.Secret),
106+
strings.Join(c.RedirectURIs, ","),
107+
strings.Join(c.Scopes, ","),
108+
c.Public,
109+
c.ID,
110+
)
111+
if err != nil {
112+
return fmt.Errorf("failed to update client: %w", err)
113+
}
114+
return nil
115+
}
116+
117+
func (a *LegacyDBAdapter) DeleteClient(ctx context.Context, id string) error {
118+
_, err := a.DB.ExecContext(ctx, `DELETE FROM clients WHERE id=?`, id)
119+
if err != nil {
120+
return fmt.Errorf("failed to delete client: %w", err)
121+
}
122+
return nil
123+
}
124+
125+
// Token operations in the legacy DB use a generic tokens table. Each token type
126+
// is mapped by the token_type column. Only minimal fields required by fosite are
127+
// stored here.
128+
type legacyToken struct {
129+
Signature string
130+
ClientID string
131+
TokenType string
132+
Data []byte
133+
}
134+
135+
func (a *LegacyDBAdapter) CreateToken(ctx context.Context, tokenType, signature, clientID string, data interface{}) error {
136+
// data is expected to be encoded outside. We assume caller provides []byte.
137+
b, ok := data.([]byte)
138+
if !ok {
139+
return fmt.Errorf("invalid token payload type %T", data)
140+
}
141+
_, err := a.DB.ExecContext(ctx,
142+
`INSERT INTO tokens (signature, client_id, token_type, data) VALUES (?, ?, ?, ?)`,
143+
signature, clientID, tokenType, b)
144+
if err != nil {
145+
return fmt.Errorf("failed to create token: %w", err)
146+
}
147+
return nil
148+
}
149+
150+
func (a *LegacyDBAdapter) GetToken(ctx context.Context, tokenType, signature string) (interface{}, error) {
151+
row := a.DB.QueryRowContext(ctx, `SELECT data FROM tokens WHERE signature=? AND token_type=?`, signature, tokenType)
152+
var data []byte
153+
if err := row.Scan(&data); err != nil {
154+
if errors.Is(err, sql.ErrNoRows) {
155+
return nil, fmt.Errorf("%w: token not found", fosite.ErrNotFound)
156+
}
157+
return nil, fmt.Errorf("db query failed: %w", err)
158+
}
159+
return data, nil
160+
}
161+
162+
func (a *LegacyDBAdapter) DeleteToken(ctx context.Context, tokenType, signature string) error {
163+
_, err := a.DB.ExecContext(ctx, `DELETE FROM tokens WHERE signature=? AND token_type=?`, signature, tokenType)
164+
if err != nil {
165+
return fmt.Errorf("failed to delete token: %w", err)
166+
}
167+
return nil
168+
}
169+
170+
func (a *LegacyDBAdapter) RevokeToken(ctx context.Context, tokenType, signature string) error {
171+
// Mark token as revoked using a revoked_at column; create the column if not present.
172+
_, err := a.DB.ExecContext(ctx,
173+
`UPDATE tokens SET revoked_at=? WHERE signature=? AND token_type=?`,
174+
time.Now().UTC(), signature, tokenType)
175+
if err != nil {
176+
return fmt.Errorf("failed to revoke token: %w", err)
177+
}
178+
return nil
179+
}
180+
181+
// Session operations map directly to a sessions table. The "session_type"
182+
// column differentiates between openid, pkce, etc.
183+
func (a *LegacyDBAdapter) CreateSession(ctx context.Context, sessionType, id string, data interface{}) error {
184+
b, ok := data.([]byte)
185+
if !ok {
186+
return fmt.Errorf("invalid session payload type %T", data)
187+
}
188+
_, err := a.DB.ExecContext(ctx,
189+
`INSERT INTO sessions (id, session_type, data) VALUES (?, ?, ?)`,
190+
id, sessionType, b,
191+
)
192+
if err != nil {
193+
return fmt.Errorf("failed to create session: %w", err)
194+
}
195+
return nil
196+
}
197+
198+
func (a *LegacyDBAdapter) GetSession(ctx context.Context, sessionType, id string) (interface{}, error) {
199+
row := a.DB.QueryRowContext(ctx, `SELECT data FROM sessions WHERE id=? AND session_type=?`, id, sessionType)
200+
var data []byte
201+
if err := row.Scan(&data); err != nil {
202+
if errors.Is(err, sql.ErrNoRows) {
203+
return nil, fmt.Errorf("%w: session not found", fosite.ErrNotFound)
204+
}
205+
return nil, fmt.Errorf("db query failed: %w", err)
206+
}
207+
return data, nil
208+
}
209+
210+
func (a *LegacyDBAdapter) DeleteSession(ctx context.Context, sessionType, id string) error {
211+
_, err := a.DB.ExecContext(ctx, `DELETE FROM sessions WHERE id=? AND session_type=?`, id, sessionType)
212+
if err != nil {
213+
return fmt.Errorf("failed to delete session: %w", err)
214+
}
215+
return nil
216+
}
217+
218+
func (a *LegacyDBAdapter) ValidateJWT(ctx context.Context, jti string) error {
219+
row := a.DB.QueryRowContext(ctx, `SELECT EXISTS(SELECT 1 FROM used_jtis WHERE jti=?)`, jti)
220+
var exists bool
221+
if err := row.Scan(&exists); err != nil {
222+
return fmt.Errorf("db query failed: %w", err)
223+
}
224+
if exists {
225+
return fosite.ErrJTIKnown
226+
}
227+
return nil
228+
}
229+
230+
func (a *LegacyDBAdapter) MarkJWTAsUsed(ctx context.Context, jti string, exp time.Time) error {
231+
_, err := a.DB.ExecContext(ctx,
232+
`INSERT INTO used_jtis (jti, expires_at) VALUES (?, ?)`, jti, exp.UTC())
233+
if err != nil {
234+
return fmt.Errorf("failed to mark jti used: %w", err)
235+
}
236+
return nil
237+
}
238+
239+
// Ensure interface compliance
240+
var _ StorageInterface = (*LegacyDBAdapter)(nil)

0 commit comments

Comments
 (0)