Skip to content

Commit 8fedb27

Browse files
committed
New approach
1 parent 0b3fbc1 commit 8fedb27

File tree

8 files changed

+896
-79
lines changed

8 files changed

+896
-79
lines changed

internal/auth/browser.go

Lines changed: 26 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ package auth
33
import (
44
"context"
55
"encoding/json"
6-
"fmt"
76
"net/http"
87
"time"
98

@@ -19,15 +18,25 @@ type SessionData struct {
1918
Expires time.Time `json:"expires"`
2019
}
2120

21+
// BrowserState represents the state parameter data for browser SSO
22+
type BrowserState struct {
23+
Nonce string `json:"nonce"`
24+
ReturnURL string `json:"return_url"`
25+
}
26+
2227
// SSOMiddleware creates middleware for browser-based SSO authentication
2328
func (s *Server) SSOMiddleware() func(http.Handler) http.Handler {
2429
return func(next http.Handler) http.Handler {
2530
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
2631
// Check for session cookie
2732
sessionValue, err := cookie.GetSession(r)
2833
if err != nil {
29-
// No cookie, redirect directly to Google OAuth
34+
// No cookie, redirect directly to OAuth
3035
state := s.generateBrowserState(r.URL.String())
36+
if state == "" {
37+
jsonwriter.WriteInternalServerError(w, "Failed to generate authentication state")
38+
return
39+
}
3140
googleURL := s.authService.GoogleAuthURL(state)
3241
http.Redirect(w, r, googleURL, http.StatusFound)
3342
return
@@ -56,11 +65,14 @@ func (s *Server) SSOMiddleware() func(http.Handler) http.Handler {
5665

5766
// Check expiration
5867
if time.Now().After(sessionData.Expires) {
59-
// Expired session
6068
log.LogDebug("Session expired for user %s", sessionData.Email)
6169
cookie.ClearSession(w)
6270
// Redirect directly to Google OAuth
6371
state := s.generateBrowserState(r.URL.String())
72+
if state == "" {
73+
jsonwriter.WriteInternalServerError(w, "Failed to generate authentication state")
74+
return
75+
}
6476
googleURL := s.authService.GoogleAuthURL(state)
6577
http.Redirect(w, r, googleURL, http.StatusFound)
6678
return
@@ -75,14 +87,16 @@ func (s *Server) SSOMiddleware() func(http.Handler) http.Handler {
7587

7688
// generateBrowserState creates a secure state parameter for browser SSO
7789
func (s *Server) generateBrowserState(returnURL string) string {
78-
// Generate random nonce
79-
nonce := crypto.GenerateSecureToken()
80-
81-
// Create signed CSRF token: nonce + HMAC(nonce + returnURL)
82-
// This ensures the token is tied to the specific return URL
83-
data := nonce + ":" + returnURL
84-
signature := crypto.SignData(data, []byte(s.config.EncryptionKey))
90+
state := BrowserState{
91+
Nonce: crypto.GenerateSecureToken(),
92+
ReturnURL: returnURL,
93+
}
8594

86-
// Format: "browser:nonce:signature:returnURL"
87-
return fmt.Sprintf("browser:%s:%s:%s", nonce, signature, returnURL)
95+
token, err := s.browserStateToken.Sign(state)
96+
if err != nil {
97+
log.LogError("Failed to sign browser state: %v", err)
98+
// Return empty string to trigger auth failure - middleware will handle it
99+
return ""
100+
}
101+
return "browser:" + token
88102
}

internal/auth/server.go

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,12 @@ func GetUserContextKey() contextKey {
3333

3434
// Server wraps fosite.OAuth2Provider with clean architecture
3535
type Server struct {
36-
provider fosite.OAuth2Provider
37-
storage storage.Storage
38-
authService *authService
39-
config Config
40-
sessionEncryptor crypto.Encryptor // Created once for browser SSO performance
36+
provider fosite.OAuth2Provider
37+
storage storage.Storage
38+
authService *authService
39+
config Config
40+
sessionEncryptor crypto.Encryptor
41+
browserStateToken *crypto.SignedToken
4142
}
4243

4344
// Config holds OAuth server configuration
@@ -132,11 +133,12 @@ func NewServer(config Config, store storage.Storage) (*Server, error) {
132133
}
133134

134135
return &Server{
135-
provider: provider,
136-
storage: store,
137-
authService: authService,
138-
config: config,
139-
sessionEncryptor: sessionEncryptor,
136+
provider: provider,
137+
storage: store,
138+
authService: authService,
139+
config: config,
140+
sessionEncryptor: sessionEncryptor,
141+
browserStateToken: crypto.NewSignedToken(key, 10*time.Minute),
140142
}, nil
141143
}
142144

internal/crypto/signed_token.go

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
package crypto
2+
3+
import (
4+
"encoding/base64"
5+
"encoding/json"
6+
"fmt"
7+
"strings"
8+
"time"
9+
)
10+
11+
// SignedToken provides HMAC-signed JSON tokens with optional expiry
12+
type SignedToken struct {
13+
signingKey []byte
14+
ttl time.Duration
15+
}
16+
17+
// NewSignedToken creates a new signed token handler
18+
func NewSignedToken(signingKey []byte, ttl time.Duration) *SignedToken {
19+
return &SignedToken{
20+
signingKey: signingKey,
21+
ttl: ttl,
22+
}
23+
}
24+
25+
// TokenData wraps user data with metadata
26+
type TokenData struct {
27+
Data json.RawMessage `json:"data"`
28+
ExpiresAt time.Time `json:"expires_at,omitempty"`
29+
}
30+
31+
// Sign marshals data to JSON, signs it with HMAC, and returns a base64-encoded token
32+
func (st *SignedToken) Sign(v any) (string, error) {
33+
// Marshal user data
34+
userData, err := json.Marshal(v)
35+
if err != nil {
36+
return "", fmt.Errorf("failed to marshal data: %w", err)
37+
}
38+
39+
// Wrap with metadata
40+
tokenData := TokenData{
41+
Data: userData,
42+
}
43+
if st.ttl > 0 {
44+
tokenData.ExpiresAt = time.Now().Add(st.ttl)
45+
}
46+
47+
// Marshal complete token
48+
jsonData, err := json.Marshal(tokenData)
49+
if err != nil {
50+
return "", fmt.Errorf("failed to marshal token data: %w", err)
51+
}
52+
53+
// Create signature
54+
signature := SignData(string(jsonData), st.signingKey)
55+
56+
// Combine data and signature
57+
combined := fmt.Sprintf("%s.%s", base64.URLEncoding.EncodeToString(jsonData), signature)
58+
return combined, nil
59+
}
60+
61+
// Verify validates the signature, checks expiry, and unmarshals the data
62+
func (st *SignedToken) Verify(token string, v any) error {
63+
// Split data and signature
64+
parts := strings.Split(token, ".")
65+
if len(parts) != 2 {
66+
return fmt.Errorf("invalid token format")
67+
}
68+
69+
// Decode JSON data
70+
jsonData, err := base64.URLEncoding.DecodeString(parts[0])
71+
if err != nil {
72+
return fmt.Errorf("failed to decode token data: %w", err)
73+
}
74+
75+
// Verify signature
76+
signature := parts[1]
77+
if !ValidateSignedData(string(jsonData), signature, st.signingKey) {
78+
return fmt.Errorf("invalid signature")
79+
}
80+
81+
// Unmarshal token data
82+
var tokenData TokenData
83+
if err := json.Unmarshal(jsonData, &tokenData); err != nil {
84+
return fmt.Errorf("failed to unmarshal token data: %w", err)
85+
}
86+
87+
// Check expiry
88+
if !tokenData.ExpiresAt.IsZero() && time.Now().After(tokenData.ExpiresAt) {
89+
return fmt.Errorf("token expired")
90+
}
91+
92+
// Unmarshal user data
93+
if err := json.Unmarshal(tokenData.Data, v); err != nil {
94+
return fmt.Errorf("failed to unmarshal user data: %w", err)
95+
}
96+
97+
return nil
98+
}

0 commit comments

Comments
 (0)