package auth import ( "encoding/json" "fmt" "net/http" "strings" "time" "github.com/golang-jwt/jwt/v5" ) // UserInfo represents authenticated user information type UserInfo struct { ID string `json:"sub"` Email string `json:"email,omitempty"` Name string `json:"name,omitempty"` Provider string `json:"iss,omitempty"` } // AuthConfig holds authentication configuration type AuthConfig struct { DevMode bool JWTSecret string OAuthConfigs map[string]OAuthConfig } // OAuthConfig holds OAuth provider configuration type OAuthConfig struct { ClientID string ClientSecret string RedirectURL string Scopes []string } // AuthService handles authentication operations type AuthService struct { config *AuthConfig } // NewAuthService creates a new authentication service func NewAuthService(config *AuthConfig) *AuthService { return &AuthService{config: config} } // ExtractUserFromRequest extracts user information from HTTP request func (a *AuthService) ExtractUserFromRequest(r *http.Request) (*UserInfo, error) { authHeader := r.Header.Get("Authorization") if authHeader == "" { return &UserInfo{ID: "anonymous"}, nil } // Parse Bearer token if !strings.HasPrefix(authHeader, "Bearer ") { return nil, fmt.Errorf("invalid authorization header format") } token := strings.TrimPrefix(authHeader, "Bearer ") // Handle mock tokens in development mode if a.config.DevMode && strings.HasPrefix(token, "mock-") { return a.parseMockToken(token), nil } // Parse real JWT token return a.parseJWT(token) } // parseMockToken handles mock development tokens func (a *AuthService) parseMockToken(token string) *UserInfo { // Mock token format: mock-{user}-{timestamp}-{random} parts := strings.Split(token, "-") if len(parts) >= 2 { return &UserInfo{ ID: parts[1], // user part Email: fmt.Sprintf("%s@localhost", parts[1]), Name: strings.Title(parts[1]), Provider: "insertr-dev", } } return &UserInfo{ID: "anonymous"} } // parseJWT parses and validates a real JWT token func (a *AuthService) parseJWT(tokenString string) (*UserInfo, error) { // Parse the token token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { // Validate signing method if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) } return []byte(a.config.JWTSecret), nil }) if err != nil { return nil, fmt.Errorf("failed to parse JWT: %w", err) } // Extract claims if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid { userInfo := &UserInfo{} // Extract standard JWT claims if sub, ok := claims["sub"].(string); ok { userInfo.ID = sub } if email, ok := claims["email"].(string); ok { userInfo.Email = email } if name, ok := claims["name"].(string); ok { userInfo.Name = name } if iss, ok := claims["iss"].(string); ok { userInfo.Provider = iss } // Fallback to alternative claim names if userInfo.ID == "" { if userID, ok := claims["user_id"].(string); ok { userInfo.ID = userID } } // Default to anonymous if no user ID found if userInfo.ID == "" { userInfo.ID = "anonymous" } return userInfo, nil } return nil, fmt.Errorf("invalid JWT claims") } // CreateMockJWT creates a mock JWT token for development/testing func (a *AuthService) CreateMockJWT(userID, email, name string) (string, error) { if !a.config.DevMode { return "", fmt.Errorf("mock JWT creation only allowed in development mode") } // Create the claims claims := jwt.MapClaims{ "sub": userID, "email": email, "name": name, "iss": "insertr-dev", "iat": time.Now().Unix(), "exp": time.Now().Add(24 * time.Hour).Unix(), } // Create token token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) // Sign and get the complete encoded token as a string tokenString, err := token.SignedString([]byte(a.config.JWTSecret)) if err != nil { return "", fmt.Errorf("failed to sign JWT: %w", err) } return tokenString, nil } // ValidateToken validates a JWT token without extracting user info func (a *AuthService) ValidateToken(tokenString string) error { _, err := a.parseJWT(tokenString) return err } // RefreshToken creates a new token with extended expiration func (a *AuthService) RefreshToken(tokenString string) (string, error) { userInfo, err := a.parseJWT(tokenString) if err != nil { return "", fmt.Errorf("cannot refresh invalid token: %w", err) } // Create new token with same user info but extended expiration claims := jwt.MapClaims{ "sub": userInfo.ID, "email": userInfo.Email, "name": userInfo.Name, "iss": userInfo.Provider, "iat": time.Now().Unix(), "exp": time.Now().Add(24 * time.Hour).Unix(), } token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) return token.SignedString([]byte(a.config.JWTSecret)) } // IsAuthenticated checks if the request has valid authentication func (a *AuthService) IsAuthenticated(r *http.Request) bool { userInfo, err := a.ExtractUserFromRequest(r) return err == nil && userInfo.ID != "anonymous" } // RequireAuth middleware that requires authentication func (a *AuthService) RequireAuth(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { userInfo, err := a.ExtractUserFromRequest(r) if err != nil || userInfo.ID == "anonymous" { http.Error(w, "Authentication required", http.StatusUnauthorized) return } // Add user info to request context for use by handlers ctx := r.Context() ctx = ContextWithUser(ctx, userInfo) r = r.WithContext(ctx) next.ServeHTTP(w, r) }) } // OAuth flow handlers (to be implemented when adding real OAuth) // HandleOAuthLogin initiates OAuth flow func (a *AuthService) HandleOAuthLogin(w http.ResponseWriter, r *http.Request) { provider := r.URL.Query().Get("provider") if provider == "" { provider = "google" } // TODO: Implement OAuth initiation // For now, return mock success in dev mode if a.config.DevMode { response := map[string]interface{}{ "message": "OAuth login not yet implemented", "redirect_url": "/auth/callback?code=mock_code&state=mock_state", "dev_mode": true, } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(response) return } http.Error(w, "OAuth not implemented", http.StatusNotImplemented) } // HandleOAuthCallback handles OAuth provider callback func (a *AuthService) HandleOAuthCallback(w http.ResponseWriter, r *http.Request) { code := r.URL.Query().Get("code") _ = r.URL.Query().Get("state") // TODO: validate state parameter // TODO: Implement OAuth token exchange // For now, return mock token in dev mode if a.config.DevMode && code != "" { mockToken, err := a.CreateMockJWT("dev-user", "dev@localhost", "Development User") if err != nil { http.Error(w, "Failed to create mock token", http.StatusInternalServerError) return } response := map[string]interface{}{ "token": mockToken, "dev_mode": true, "message": "Mock OAuth callback successful", } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(response) return } http.Error(w, "OAuth callback not implemented", http.StatusNotImplemented) }