-
This commit is contained in:
@ -44,9 +44,13 @@ COPY --from=builder /app/api-key-service /app/api-key-service
|
||||
# Copy migration files
|
||||
COPY --from=builder /app/migrations /app/migrations
|
||||
|
||||
# Copy template files
|
||||
COPY --from=builder /app/templates /app/templates
|
||||
|
||||
# Change ownership to non-root user
|
||||
RUN chown -R appuser:appgroup /app && \
|
||||
chmod -R 755 /app/migrations
|
||||
chmod -R 755 /app/migrations && \
|
||||
chmod -R 755 /app/templates
|
||||
|
||||
# Switch to non-root user
|
||||
USER appuser
|
||||
|
||||
139
SECURITY_RECOMMENDATIONS.md
Normal file
139
SECURITY_RECOMMENDATIONS.md
Normal file
@ -0,0 +1,139 @@
|
||||
# Token Prefix Security Recommendations
|
||||
|
||||
## Current Security Issues
|
||||
|
||||
### 1. Information Disclosure
|
||||
- Prefixes like "KMS", "TEST", "PROD" expose system architecture
|
||||
- Makes it easy for attackers to identify token origins and purposes
|
||||
- Leaks information about internal applications and environments
|
||||
|
||||
### 2. Predictable Token Structure
|
||||
- Static tokens: `PREFIX + "T-" + token_data`
|
||||
- User tokens: `PREFIX + "UT-" + jwt_token`
|
||||
- Highly predictable format aids in token identification and potential attacks
|
||||
|
||||
### 3. Application Fingerprinting
|
||||
- Trivial to map tokens to specific applications
|
||||
- Easy to identify different environments (dev/staging/prod)
|
||||
- Reveals system architecture from leaked tokens
|
||||
|
||||
## Recommended Security Improvements
|
||||
|
||||
### Option 1: Remove Custom Prefixes (Recommended)
|
||||
```go
|
||||
// Use a single, non-descriptive prefix for all tokens
|
||||
const SecureTokenPrefix = "kms_"
|
||||
|
||||
// All tokens would be: kms_<random_data>
|
||||
// No application or type information revealed
|
||||
```
|
||||
|
||||
### Option 2: Opaque Application Identifiers
|
||||
```go
|
||||
// Use random/hashed identifiers instead of descriptive names
|
||||
type Application struct {
|
||||
AppID string `json:"app_id"`
|
||||
TokenPrefix string `json:"token_prefix"` // Random: "A7B2", "X9K5", etc.
|
||||
}
|
||||
|
||||
// Generate random 4-character prefixes
|
||||
func GenerateSecurePrefix() string {
|
||||
const charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||
result := make([]byte, 4)
|
||||
for i := range result {
|
||||
result[i] = charset[rand.Intn(len(charset))]
|
||||
}
|
||||
return string(result)
|
||||
}
|
||||
```
|
||||
|
||||
### Option 3: No Type Indicators
|
||||
```go
|
||||
// Remove "T-" and "UT-" type indicators
|
||||
// Token type should be determined by validation, not structure
|
||||
func GenerateToken(appPrefix string, tokenType string) string {
|
||||
token := appPrefix + randomTokenData // No type suffix
|
||||
return token
|
||||
}
|
||||
```
|
||||
|
||||
### Option 4: Encrypted Token Metadata
|
||||
```go
|
||||
// Encrypt sensitive information within tokens
|
||||
type TokenMetadata struct {
|
||||
AppID string `json:"app_id"`
|
||||
TokenType string `json:"token_type"`
|
||||
IssuedAt time.Time `json:"issued_at"`
|
||||
}
|
||||
|
||||
func CreateSecureToken(metadata TokenMetadata, key []byte) string {
|
||||
// Encrypt metadata and embed in token
|
||||
encrypted := encrypt(metadata, key)
|
||||
return "kms_" + base64.URLEncoding.EncodeToString(encrypted)
|
||||
}
|
||||
```
|
||||
|
||||
## Implementation Priority
|
||||
|
||||
1. **Immediate**: Remove descriptive prefixes ("KMS", "TEST", "PROD")
|
||||
2. **Short-term**: Remove type indicators ("T-", "UT-")
|
||||
3. **Long-term**: Implement encrypted token metadata
|
||||
|
||||
## Security Best Practices
|
||||
|
||||
### Token Storage
|
||||
- Always hash tokens before database storage
|
||||
- Use strong hashing algorithms (bcrypt, scrypt, Argon2)
|
||||
- Never log or expose full tokens
|
||||
|
||||
### Token Validation
|
||||
- Validate tokens by content, not by prefix patterns
|
||||
- Use cryptographic verification for all tokens
|
||||
- Implement proper token revocation mechanisms
|
||||
|
||||
### Monitoring
|
||||
- Monitor for token enumeration attempts
|
||||
- Alert on suspicious prefix-based attacks
|
||||
- Log token validation failures for security analysis
|
||||
|
||||
## Code Changes Required
|
||||
|
||||
### 1. Update Token Generation
|
||||
```go
|
||||
// Before: Predictable structure
|
||||
token := "TESTT-" + tokenData
|
||||
|
||||
// After: Opaque structure
|
||||
token := "kms_" + secureRandomToken()
|
||||
```
|
||||
|
||||
### 2. Update Validation Logic
|
||||
```go
|
||||
// Before: Prefix-based type detection
|
||||
if strings.HasPrefix(token, app.TokenPrefix + "UT-") {
|
||||
return TokenTypeUser
|
||||
}
|
||||
|
||||
// After: Cryptographic validation
|
||||
tokenType, err := validateAndExtractType(token, app.HMACKey)
|
||||
```
|
||||
|
||||
### 3. Database Migration
|
||||
```sql
|
||||
-- Remove descriptive prefixes from existing tokens
|
||||
UPDATE applications
|
||||
SET token_prefix = 'kms_'
|
||||
WHERE token_prefix IN ('KMS', 'TEST', 'PROD', 'DEV');
|
||||
```
|
||||
|
||||
## Risk Assessment
|
||||
|
||||
**Current Risk Level**: HIGH
|
||||
- Token structure reveals sensitive system information
|
||||
- Easy application and environment fingerprinting
|
||||
- Predictable token patterns aid in attacks
|
||||
|
||||
**Mitigated Risk Level**: LOW
|
||||
- Opaque token structure prevents information leakage
|
||||
- No application or environment identification possible
|
||||
- Cryptographic validation ensures security
|
||||
150
TOKEN_LOOKUP_ANALYSIS.md
Normal file
150
TOKEN_LOOKUP_ANALYSIS.md
Normal file
@ -0,0 +1,150 @@
|
||||
# Token Verification: App ID From Token vs Separate Parameter
|
||||
|
||||
## Current Implementation Analysis
|
||||
|
||||
### Current Flow (Requires app_id parameter):
|
||||
```
|
||||
1. Client: POST /api/verify {"token": "TESTT-abc123", "app_id": "test.example.com"}
|
||||
2. System: Get app by app_id → Validate token against app's stored hashes
|
||||
3. Result: Token validated within specific application scope
|
||||
```
|
||||
|
||||
### Proposed Alternative (Extract app_id from token):
|
||||
```
|
||||
1. Client: POST /api/verify {"token": "TESTT-abc123"}
|
||||
2. System: Extract prefix "TEST" → Find app by token_prefix → Validate token
|
||||
3. Result: Token validated without requiring app_id parameter
|
||||
```
|
||||
|
||||
## Security Analysis
|
||||
|
||||
### ✅ **Advantages of Token-Only Verification:**
|
||||
|
||||
#### 1. **Simpler API Usage**
|
||||
```javascript
|
||||
// Current (requires app knowledge)
|
||||
await verify({token: "TESTT-abc123", app_id: "test.example.com"});
|
||||
|
||||
// Proposed (token-only)
|
||||
await verify({token: "TESTT-abc123"});
|
||||
```
|
||||
|
||||
#### 2. **Prevents App ID Mismatches**
|
||||
- Current: Client could provide wrong app_id (though validation would fail)
|
||||
- Proposed: App is determined directly from token - no mismatch possible
|
||||
|
||||
#### 3. **Better Token Portability**
|
||||
- Tokens are self-describing and don't require external context
|
||||
- Useful for tokens shared across different applications/services
|
||||
|
||||
### ❌ **Security Risks of Token-Only Verification:**
|
||||
|
||||
#### 1. **Information Disclosure Through Enumeration**
|
||||
```bash
|
||||
# Attacker can discover applications by trying token prefixes
|
||||
curl -X POST /api/verify -d '{"token": "TESTX-fakehash"}'
|
||||
# Response reveals if "TEST" application exists
|
||||
|
||||
curl -X POST /api/verify -d '{"token": "PRODX-fakehash"}'
|
||||
# Response reveals if "PROD" application exists
|
||||
```
|
||||
|
||||
#### 2. **Reduced Access Control Granularity**
|
||||
```
|
||||
Current: Client must know both token AND which app it belongs to
|
||||
Proposed: Client only needs token - loses "need to know" app context
|
||||
```
|
||||
|
||||
#### 3. **Prefix Collision Risk**
|
||||
```sql
|
||||
-- Multiple apps could theoretically have same prefix
|
||||
INSERT INTO applications (app_id, token_prefix) VALUES
|
||||
('app1.com', 'TEST'),
|
||||
('app2.com', 'TEST'); -- Collision!
|
||||
```
|
||||
|
||||
#### 4. **Weaker Defense Against Token Leakage**
|
||||
- Current: Leaked token + need app_id knowledge = double barrier
|
||||
- Proposed: Leaked token alone is sufficient = single barrier
|
||||
|
||||
## Implementation Feasibility
|
||||
|
||||
### Required Changes for Token-Only Verification:
|
||||
|
||||
```go
|
||||
// 1. Add repository method
|
||||
func (r *ApplicationRepository) GetByTokenPrefix(ctx context.Context, prefix string) (*domain.Application, error)
|
||||
|
||||
// 2. Extract prefix from token
|
||||
func extractTokenPrefix(token string) string {
|
||||
dashIndex := strings.Index(token, "-")
|
||||
if dashIndex >= 3 && dashIndex <= 6 {
|
||||
return token[:dashIndex-1] // Remove "T" or "UT" part
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// 3. Modified verification flow
|
||||
func (s *tokenService) VerifyTokenOnly(ctx context.Context, token string) (*domain.VerifyResponse, error) {
|
||||
prefix := extractTokenPrefix(token)
|
||||
app, err := s.appRepo.GetByTokenPrefix(ctx, prefix)
|
||||
if err != nil {
|
||||
return &domain.VerifyResponse{Valid: false, Error: "Invalid token"}, nil
|
||||
}
|
||||
// Continue with existing verification logic...
|
||||
}
|
||||
```
|
||||
|
||||
### Database Schema Considerations:
|
||||
```sql
|
||||
-- Ensure unique prefixes (recommended)
|
||||
ALTER TABLE applications ADD CONSTRAINT unique_token_prefix
|
||||
UNIQUE (token_prefix) WHERE token_prefix != '';
|
||||
|
||||
-- Index already exists from migration 003
|
||||
-- CREATE INDEX idx_applications_token_prefix ON applications(token_prefix);
|
||||
```
|
||||
|
||||
## Recommendation: **Keep Current Implementation (Require app_id)**
|
||||
|
||||
### Reasoning:
|
||||
|
||||
#### **Security-First Approach** 🛡️
|
||||
1. **Prevents enumeration attacks** - attackers can't discover apps by probing prefixes
|
||||
2. **Maintains access control granularity** - clients must know token + app context
|
||||
3. **Defense in depth** - two required pieces of information instead of one
|
||||
4. **Clear audit trails** - logs show which app context was used for verification
|
||||
|
||||
#### **Architectural Benefits** 🏗️
|
||||
1. **Explicit application scoping** - makes it clear which app owns the token
|
||||
2. **Better error handling** - can distinguish between "invalid app" vs "invalid token"
|
||||
3. **Supports multi-tenancy** - different apps can have isolated token validation
|
||||
4. **Future extensibility** - can add per-app validation rules without breaking changes
|
||||
|
||||
#### **Operational Benefits** 🔧
|
||||
1. **Clear API contracts** - consumers explicitly specify their context
|
||||
2. **Better monitoring** - can track usage per application
|
||||
3. **Simpler debugging** - logs clearly show app context for each verification
|
||||
|
||||
### **Optional: Provide Both Endpoints**
|
||||
|
||||
```go
|
||||
// Existing secure endpoint (recommended for production)
|
||||
POST /api/verify
|
||||
{
|
||||
"token": "TESTT-abc123",
|
||||
"app_id": "test.example.com"
|
||||
}
|
||||
|
||||
// Optional convenience endpoint (for development/testing)
|
||||
POST /api/verify/token-only
|
||||
{
|
||||
"token": "TESTT-abc123"
|
||||
}
|
||||
```
|
||||
|
||||
## Final Answer
|
||||
|
||||
**Keep the current implementation requiring app_id** for security and architectural reasons. The slight inconvenience of requiring the app_id parameter provides significant security benefits and maintains better system architecture.
|
||||
|
||||
The token prefix provides tampering protection (which is working correctly), but requiring separate app_id provides additional security layers that outweigh the convenience of token-only verification.
|
||||
@ -185,7 +185,8 @@ func setupRouter(cfg config.ConfigProvider, logger *zap.Logger, healthHandler *h
|
||||
api := router.Group("/api")
|
||||
{
|
||||
// Authentication endpoints (no prior auth required)
|
||||
api.POST("/login", authHandler.Login)
|
||||
api.GET("/login", authHandler.Login) // HTML page for browser access
|
||||
api.POST("/login", authHandler.Login) // JSON API for programmatic access
|
||||
api.POST("/verify", authHandler.Verify)
|
||||
api.POST("/renew", authHandler.Renew)
|
||||
|
||||
|
||||
@ -5,8 +5,11 @@ import (
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
@ -27,6 +30,18 @@ type AuthHandler struct {
|
||||
config config.ConfigProvider
|
||||
errorHandler *errors.ErrorHandler
|
||||
logger *zap.Logger
|
||||
loginTemplate *template.Template
|
||||
}
|
||||
|
||||
// LoginPageData represents data passed to the login HTML template
|
||||
type LoginPageData struct {
|
||||
Token string
|
||||
TokenJSON template.JS
|
||||
RedirectURLJSON template.JS
|
||||
TokenDeliveryJSON template.JS
|
||||
ExpiresAt string
|
||||
AppID string
|
||||
UserID string
|
||||
}
|
||||
|
||||
// NewAuthHandler creates a new auth handler
|
||||
@ -36,6 +51,14 @@ func NewAuthHandler(
|
||||
config config.ConfigProvider,
|
||||
logger *zap.Logger,
|
||||
) *AuthHandler {
|
||||
// Load login template
|
||||
templatePath := filepath.Join("templates", "login.html")
|
||||
loginTemplate, err := template.ParseFiles(templatePath)
|
||||
if err != nil {
|
||||
logger.Error("Failed to load login template", zap.Error(err), zap.String("path", templatePath))
|
||||
// Template loading failure is not fatal, we'll fall back to JSON
|
||||
}
|
||||
|
||||
return &AuthHandler{
|
||||
authService: authService,
|
||||
tokenService: tokenService,
|
||||
@ -43,21 +66,55 @@ func NewAuthHandler(
|
||||
config: config,
|
||||
errorHandler: errors.NewErrorHandler(logger),
|
||||
logger: logger,
|
||||
loginTemplate: loginTemplate,
|
||||
}
|
||||
}
|
||||
|
||||
// Login handles POST /login
|
||||
// Login handles login requests (both GET for HTML and POST for JSON)
|
||||
func (h *AuthHandler) Login(c *gin.Context) {
|
||||
// Handle GET requests or requests that prefer HTML
|
||||
acceptHeader := c.GetHeader("Accept")
|
||||
contentType := c.GetHeader("Content-Type")
|
||||
|
||||
isJSONRequest := (c.Request.Method == "POST" && (contentType == "application/json" ||
|
||||
(acceptHeader != "" && (acceptHeader == "application/json" ||
|
||||
(acceptHeader != "text/html" && acceptHeader != "*/*")))))
|
||||
|
||||
var req domain.LoginRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
h.errorHandler.HandleValidationError(c, "request_body", "Invalid login request format")
|
||||
return
|
||||
|
||||
if isJSONRequest {
|
||||
// Handle JSON POST request (existing API behavior)
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
h.errorHandler.HandleValidationError(c, "request_body", "Invalid login request format")
|
||||
return
|
||||
}
|
||||
} else {
|
||||
// Handle HTML request (GET or POST with form data)
|
||||
req.AppID = c.Query("app_id")
|
||||
req.RedirectURI = c.Query("redirect_uri")
|
||||
req.TokenDelivery = domain.TokenDeliveryMode(c.DefaultQuery("token_delivery", string(domain.TokenDeliveryQuery)))
|
||||
|
||||
// Parse permissions from query parameter (comma-separated)
|
||||
if perms := c.Query("permissions"); perms != "" {
|
||||
// Simple parsing for comma-separated permissions
|
||||
req.Permissions = []string{perms} // Simplified for this example
|
||||
}
|
||||
|
||||
// If no app_id provided, show error
|
||||
if req.AppID == "" {
|
||||
h.renderLoginError(c, "Missing required parameter: app_id", isJSONRequest)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Validate authentication headers with HMAC signature
|
||||
userContext, err := h.headerValidator.ValidateAuthenticationHeaders(c.Request)
|
||||
if err != nil {
|
||||
h.errorHandler.HandleAuthenticationError(c, err)
|
||||
if isJSONRequest {
|
||||
h.errorHandler.HandleAuthenticationError(c, err)
|
||||
} else {
|
||||
h.renderLoginError(c, "Authentication failed: "+err.Error(), isJSONRequest)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@ -66,12 +123,16 @@ func (h *AuthHandler) Login(c *gin.Context) {
|
||||
// Generate user token
|
||||
token, err := h.tokenService.GenerateUserToken(c.Request.Context(), req.AppID, userContext.UserID, req.Permissions)
|
||||
if err != nil {
|
||||
h.errorHandler.HandleInternalError(c, err)
|
||||
if isJSONRequest {
|
||||
h.errorHandler.HandleInternalError(c, err)
|
||||
} else {
|
||||
h.renderLoginError(c, "Failed to generate token: "+err.Error(), isJSONRequest)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if req.RedirectURI == "" {
|
||||
// If no redirect URI, return token directly via secure response body
|
||||
// For JSON requests without redirect URI, return token directly
|
||||
if isJSONRequest && req.RedirectURI == "" {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"token": token,
|
||||
"user_id": userContext.UserID,
|
||||
@ -81,11 +142,12 @@ func (h *AuthHandler) Login(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// For redirect flows, choose token delivery method
|
||||
// Default to cookie delivery for security
|
||||
// Handle redirect flows
|
||||
tokenDelivery := req.TokenDelivery
|
||||
if tokenDelivery == "" {
|
||||
tokenDelivery = domain.TokenDeliveryCookie
|
||||
// Default to query delivery for redirects (external apps need token in URL)
|
||||
// Only use cookie delivery if explicitly specified
|
||||
tokenDelivery = domain.TokenDeliveryQuery
|
||||
}
|
||||
|
||||
h.logger.Debug("Token delivery mode", zap.String("mode", string(tokenDelivery)))
|
||||
@ -97,7 +159,9 @@ func (h *AuthHandler) Login(c *gin.Context) {
|
||||
switch tokenDelivery {
|
||||
case domain.TokenDeliveryQuery:
|
||||
// Deliver token via query parameter (for integrations like VS Code)
|
||||
redirectURL = req.RedirectURI + "?token=" + token + "&state=" + state
|
||||
if req.RedirectURI != "" {
|
||||
redirectURL = req.RedirectURI + "?token=" + token + "&state=" + state
|
||||
}
|
||||
|
||||
case domain.TokenDeliveryCookie:
|
||||
// Deliver token via secure cookie (default, more secure)
|
||||
@ -119,18 +183,105 @@ func (h *AuthHandler) Login(c *gin.Context) {
|
||||
)
|
||||
|
||||
// Redirect without token in URL for security
|
||||
redirectURL = req.RedirectURI + "?state=" + state
|
||||
if req.RedirectURI != "" {
|
||||
redirectURL = req.RedirectURI + "?state=" + state
|
||||
}
|
||||
|
||||
default:
|
||||
// Invalid delivery mode, default to cookie
|
||||
redirectURL = req.RedirectURI + "?state=" + state
|
||||
if req.RedirectURI != "" {
|
||||
redirectURL = req.RedirectURI + "?state=" + state
|
||||
}
|
||||
}
|
||||
|
||||
response := domain.LoginResponse{
|
||||
RedirectURL: redirectURL,
|
||||
// Return appropriate response format
|
||||
if isJSONRequest {
|
||||
response := domain.LoginResponse{
|
||||
RedirectURL: redirectURL,
|
||||
}
|
||||
c.JSON(http.StatusOK, response)
|
||||
} else {
|
||||
// Render HTML page
|
||||
h.renderLoginPage(c, token, redirectURL, string(tokenDelivery), userContext.UserID, req.AppID)
|
||||
}
|
||||
}
|
||||
|
||||
// renderLoginPage renders the HTML login page with token information
|
||||
func (h *AuthHandler) renderLoginPage(c *gin.Context, token, redirectURL, tokenDelivery, userID, appID string) {
|
||||
if h.loginTemplate == nil {
|
||||
// Fallback to JSON if template not available
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"token": token,
|
||||
"redirect_url": redirectURL,
|
||||
"user_id": userID,
|
||||
"app_id": appID,
|
||||
"message": "Login successful - HTML template not available",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, response)
|
||||
// Prepare template data
|
||||
tokenJSON, _ := json.Marshal(token)
|
||||
redirectURLJSON, _ := json.Marshal(redirectURL)
|
||||
tokenDeliveryJSON, _ := json.Marshal(tokenDelivery)
|
||||
|
||||
data := LoginPageData{
|
||||
Token: token,
|
||||
TokenJSON: template.JS(tokenJSON),
|
||||
RedirectURLJSON: template.JS(redirectURLJSON),
|
||||
TokenDeliveryJSON: template.JS(tokenDeliveryJSON),
|
||||
ExpiresAt: time.Now().Add(7 * 24 * time.Hour).Format("Jan 2, 2006 at 3:04 PM MST"),
|
||||
AppID: appID,
|
||||
UserID: userID,
|
||||
}
|
||||
|
||||
c.Header("Content-Type", "text/html; charset=utf-8")
|
||||
// Override CSP for login page to allow inline styles and scripts
|
||||
c.Header("Content-Security-Policy", "default-src 'self'; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline'")
|
||||
|
||||
if err := h.loginTemplate.Execute(c.Writer, data); err != nil {
|
||||
h.logger.Error("Failed to render login template", zap.Error(err))
|
||||
// Fallback to JSON response
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"token": token,
|
||||
"redirect_url": redirectURL,
|
||||
"user_id": userID,
|
||||
"app_id": appID,
|
||||
"message": "Login successful - template render failed",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// renderLoginError renders an error page or JSON error response
|
||||
func (h *AuthHandler) renderLoginError(c *gin.Context, message string, isJSON bool) {
|
||||
if isJSON {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": "Bad Request",
|
||||
"message": message,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Simple HTML error page
|
||||
c.Header("Content-Type", "text/html; charset=utf-8")
|
||||
// Override CSP for error page to allow inline styles
|
||||
c.Header("Content-Security-Policy", "default-src 'self'; style-src 'self' 'unsafe-inline'")
|
||||
c.String(http.StatusBadRequest, `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Login Error</title>
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; max-width: 600px; margin: 50px auto; padding: 20px; }
|
||||
.error { background: #f8d7da; color: #721c24; padding: 15px; border-radius: 5px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Login Error</h1>
|
||||
<div class="error">%s</div>
|
||||
<p><a href="javascript:history.back()">Go back</a></p>
|
||||
</body>
|
||||
</html>`, message)
|
||||
}
|
||||
|
||||
// generateSecureState generates a secure state parameter for OAuth flows
|
||||
|
||||
241
templates/login.html
Normal file
241
templates/login.html
Normal file
@ -0,0 +1,241 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Login - API Key Management Service</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
}
|
||||
.container {
|
||||
background: white;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 15px 35px rgba(0,0,0,0.1);
|
||||
padding: 40px;
|
||||
max-width: 500px;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
h1 {
|
||||
color: #333;
|
||||
margin-bottom: 30px;
|
||||
font-size: 28px;
|
||||
font-weight: 300;
|
||||
}
|
||||
.token-section {
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
.token-display {
|
||||
background: #2d3748;
|
||||
color: #e2e8f0;
|
||||
padding: 15px;
|
||||
border-radius: 6px;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 14px;
|
||||
word-break: break-all;
|
||||
margin: 15px 0;
|
||||
border: none;
|
||||
width: 100%;
|
||||
min-height: 60px;
|
||||
resize: none;
|
||||
}
|
||||
.copy-button {
|
||||
background: #4299e1;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
margin-top: 10px;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
.copy-button:hover {
|
||||
background: #3182ce;
|
||||
}
|
||||
.status {
|
||||
margin: 20px 0;
|
||||
padding: 15px;
|
||||
border-radius: 6px;
|
||||
font-weight: 500;
|
||||
}
|
||||
.status.success {
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
border: 1px solid #c3e6cb;
|
||||
}
|
||||
.status.error {
|
||||
background: #f8d7da;
|
||||
color: #721c24;
|
||||
border: 1px solid #f5c6cb;
|
||||
}
|
||||
.status.info {
|
||||
background: #d1ecf1;
|
||||
color: #0c5460;
|
||||
border: 1px solid #bee5eb;
|
||||
}
|
||||
.spinner {
|
||||
border: 3px solid #f3f3f3;
|
||||
border-top: 3px solid #4299e1;
|
||||
border-radius: 50%;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
animation: spin 1s linear infinite;
|
||||
margin: 0 auto 15px auto;
|
||||
}
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
.redirect-info {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
margin-top: 15px;
|
||||
}
|
||||
noscript .no-js-message {
|
||||
background: #fff3cd;
|
||||
color: #856404;
|
||||
padding: 15px;
|
||||
border: 1px solid #ffeaa7;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>🔐 Login Success</h1>
|
||||
|
||||
<noscript>
|
||||
<div class="no-js-message">
|
||||
<strong>JavaScript is disabled.</strong> Please copy the token below and paste it into your application.
|
||||
</div>
|
||||
</noscript>
|
||||
|
||||
<div id="loading" class="status info">
|
||||
<div class="spinner"></div>
|
||||
<div>Processing login and redirecting...</div>
|
||||
</div>
|
||||
|
||||
<div id="success-status" class="status success hidden">
|
||||
✅ Login successful! Redirecting to your application...
|
||||
</div>
|
||||
|
||||
<div id="error-status" class="status error hidden">
|
||||
<div id="error-message">❌ Redirect failed. Please copy the token below manually.</div>
|
||||
</div>
|
||||
|
||||
<div class="token-section">
|
||||
<h3>Your Authentication Token</h3>
|
||||
<p>Use this token to authenticate with the API:</p>
|
||||
<textarea id="token-display" class="token-display" readonly>{{.Token}}</textarea>
|
||||
<button id="copy-button" class="copy-button" onclick="copyToken()">📋 Copy Token</button>
|
||||
<div class="redirect-info">
|
||||
<strong>Token expires:</strong> {{.ExpiresAt}}<br>
|
||||
<strong>Application:</strong> {{.AppID}}<br>
|
||||
<strong>User:</strong> {{.UserID}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Token and redirect information from server
|
||||
const token = {{.TokenJSON}};
|
||||
const redirectURL = {{.RedirectURLJSON}};
|
||||
const tokenDelivery = {{.TokenDeliveryJSON}};
|
||||
|
||||
// Elements
|
||||
const loadingDiv = document.getElementById('loading');
|
||||
const successDiv = document.getElementById('success-status');
|
||||
const errorDiv = document.getElementById('error-status');
|
||||
const errorMessage = document.getElementById('error-message');
|
||||
|
||||
function copyToken() {
|
||||
const tokenDisplay = document.getElementById('token-display');
|
||||
tokenDisplay.select();
|
||||
tokenDisplay.setSelectionRange(0, 99999); // For mobile devices
|
||||
|
||||
try {
|
||||
document.execCommand('copy');
|
||||
const copyButton = document.getElementById('copy-button');
|
||||
const originalText = copyButton.textContent;
|
||||
copyButton.textContent = '✅ Copied!';
|
||||
copyButton.style.background = '#48bb78';
|
||||
|
||||
setTimeout(() => {
|
||||
copyButton.textContent = originalText;
|
||||
copyButton.style.background = '#4299e1';
|
||||
}, 2000);
|
||||
} catch (err) {
|
||||
console.error('Failed to copy token:', err);
|
||||
}
|
||||
}
|
||||
|
||||
function performRedirect() {
|
||||
if (!redirectURL) {
|
||||
// No redirect URL provided, just show success message
|
||||
loadingDiv.classList.add('hidden');
|
||||
successDiv.innerHTML = '✅ Login successful! You can now close this window and use the token above.';
|
||||
successDiv.classList.remove('hidden');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Show success status
|
||||
loadingDiv.classList.add('hidden');
|
||||
successDiv.classList.remove('hidden');
|
||||
|
||||
// Perform the redirect after a short delay
|
||||
setTimeout(() => {
|
||||
window.location.href = redirectURL;
|
||||
}, 1500);
|
||||
|
||||
// Set a backup timer in case redirect fails
|
||||
setTimeout(() => {
|
||||
if (window.location.href.indexOf(redirectURL) === -1) {
|
||||
// Redirect failed, show error
|
||||
successDiv.classList.add('hidden');
|
||||
errorDiv.classList.remove('hidden');
|
||||
errorMessage.textContent = '❌ Automatic redirect failed. Please copy the token above and paste it into your application.';
|
||||
}
|
||||
}, 5000);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Redirect failed:', error);
|
||||
loadingDiv.classList.add('hidden');
|
||||
errorDiv.classList.remove('hidden');
|
||||
errorMessage.textContent = '❌ Redirect failed: ' + error.message + '. Please copy the token above manually.';
|
||||
}
|
||||
}
|
||||
|
||||
// Start the redirect process when page loads
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Small delay to let the user see the page loaded
|
||||
setTimeout(performRedirect, 1000);
|
||||
});
|
||||
|
||||
// Handle cookie-based token delivery
|
||||
if (tokenDelivery === 'cookie') {
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const message = document.querySelector('.redirect-info');
|
||||
message.innerHTML += '<br><strong>Delivery method:</strong> Secure cookie (auth_token)';
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user