587 lines
22 KiB
Go
587 lines
22 KiB
Go
package repositories
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"database/sql"
|
|
"encoding/base64"
|
|
|
|
"github.com/creativenoz/aurganize-v62/backend/internal/models"
|
|
"github.com/google/uuid"
|
|
"github.com/jmoiron/sqlx"
|
|
)
|
|
|
|
// SessionRepository handles all database operations related to user sessions.
|
|
// A session represents an authenticated user's connection/login instance.
|
|
//
|
|
// What is a session?
|
|
// - Created when a user logs in
|
|
// - Stores refresh token information
|
|
// - Tracks device/location information
|
|
// - Can be revoked to log out a specific device
|
|
// - Has an expiration date
|
|
//
|
|
// Why track sessions?
|
|
// 1. Security: See all active login locations/devices
|
|
// 2. Control: Revoke specific sessions (e.g., "logout from my phone")
|
|
// 3. Audit: Track when/where users log in
|
|
// 4. Token validation: Verify refresh tokens haven't been revoked
|
|
//
|
|
// Architecture pattern: Repository Pattern
|
|
// - Abstracts database operations
|
|
// - Provides clean interface for data access
|
|
// - Makes testing easier (can mock repository)
|
|
// - Keeps SQL queries separate from business logic
|
|
type SessionRepository struct {
|
|
db *sqlx.DB // sqlx provides enhanced database operations (named queries, struct scanning)
|
|
}
|
|
|
|
// NewSessionRepository creates a new instance of SessionRepository.
|
|
// This constructor follows dependency injection pattern:
|
|
// - Database connection passed in rather than created internally
|
|
// - Makes testing easier (can pass test database)
|
|
// - Keeps repository flexible (works with any sqlx.DB connection)
|
|
//
|
|
// Parameter:
|
|
// - db: The database connection pool to use for all operations
|
|
//
|
|
// Returns:
|
|
// - Initialized SessionRepository ready to perform database operations
|
|
func NewSessionRepository(db *sqlx.DB) *SessionRepository {
|
|
return &SessionRepository{db: db}
|
|
}
|
|
|
|
// Create creates a new session record in the database.
|
|
// This is called when a user logs in to track the authentication session.
|
|
//
|
|
// What happens here:
|
|
// 1. Hashes the refresh token (security - never store raw tokens)
|
|
// 2. Inserts session record with user info, device info, expiration
|
|
// 3. Returns the created session with generated ID and timestamps
|
|
//
|
|
// Why hash the token?
|
|
// - If database is compromised, attackers can't use the tokens directly
|
|
// - Hashing is one-way (can verify but can't recover original)
|
|
// - Similar to password hashing but using SHA-256 instead of bcrypt
|
|
//
|
|
// Token hashing strategy explained:
|
|
// We use SHA-256 instead of bcrypt because:
|
|
// - bcrypt is for passwords (slow, salted, designed for brute-force resistance)
|
|
// - bcrypt generates different hash each time for same input (random salt)
|
|
// - SHA-256 is for tokens (fast, deterministic, allows exact lookup)
|
|
// - SHA-256 always produces same hash for same input (what we need for token lookup)
|
|
//
|
|
// If we used bcrypt:
|
|
// - Each login would generate different hash for same token
|
|
// - We couldn't look up sessions by token (bcrypt needs to compare, not lookup)
|
|
// - Token validation would require scanning all sessions (very slow)
|
|
//
|
|
// Flow:
|
|
// 1. Hash the plaintext refresh token using SHA-256
|
|
// 2. Insert session record with hashed token
|
|
// 3. Database generates ID, timestamps
|
|
// 4. Return complete session object
|
|
//
|
|
// Error handling:
|
|
// - Returns error if database insert fails
|
|
// - Caller should handle errors (usually return 500 to client)
|
|
func (r *SessionRepository) Create(ctx context.Context, input *models.CreateSessionInput) (*models.Session, error) {
|
|
// OLD CODE (commented out) - Why bcrypt doesn't work for tokens:
|
|
// hash, err := bcrypt.GenerateFromPassword([]byte(input.RefreshToken), bcrypt.DefaultCost)
|
|
// if err != nil {
|
|
// return nil, err
|
|
// }
|
|
|
|
// EXPLANATION OF WHY BCRYPT DOESN'T WORK:
|
|
// bcrypt is designed for passwords (slow, with salt, for brute-force protection)
|
|
// For tokens, you should use SHA-256 (fast, deterministic hash)
|
|
//
|
|
// Why this matters:
|
|
// * bcrypt generates a different hash each time for the same input (because of random salt)
|
|
// - Example: Hash("mytoken") could give "$2a$10$abcd..." first time and "$2a$10$xyz..." second time
|
|
// * When you try to verify the token later, bcrypt.CompareHashAndPassword won't work with the plain token
|
|
// - You'd need to store which hash belongs to which token (defeats the purpose)
|
|
// * SHA-256 always produces the same hash for the same input (what you need for token lookup)
|
|
// - Example: Hash("mytoken") always gives "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08"
|
|
//
|
|
// NEW CODE: Hash token using SHA-256 for deterministic lookup
|
|
hash := hashToken(input.RefreshToken)
|
|
|
|
// Prepare session struct to receive database response
|
|
session := &models.Session{}
|
|
|
|
// SQL query to insert new session
|
|
// Uses RETURNING clause to get back the created record in one database round-trip
|
|
// This is PostgreSQL-specific syntax (MySQL would need separate SELECT after INSERT)
|
|
query := `
|
|
INSERT INTO sessions (
|
|
user_id, -- Which user this session belongs to
|
|
refresh_token_hash,-- Hashed refresh token (never store plaintext tokens!)
|
|
user_agent, -- Browser/app information (e.g., "Mozilla/5.0...")
|
|
ip_address, -- IP address user logged in from
|
|
device_name, -- Optional device name (e.g., "John's iPhone")
|
|
device_type, -- Device category: "mobile", "desktop", "web"
|
|
expires_at -- When this session expires (usually 7 days from now)
|
|
) VALUES ($1,$2,$3,$4,$5,$6,$7)
|
|
RETURNING id, user_id, refresh_token_hash, user_agent,
|
|
ip_address, device_name, device_type, expires_at, is_revoked,
|
|
revoked_at, revoked_reason, created_at, last_used_at
|
|
`
|
|
|
|
// Execute query and scan result directly into session struct
|
|
// GetContext:
|
|
// - Executes query with context (supports cancellation/timeout)
|
|
// - Expects exactly one row returned
|
|
// - Maps columns to struct fields by matching db tags
|
|
// - Returns error if query fails or row count != 1
|
|
err := r.db.GetContext(
|
|
ctx,
|
|
session, // Destination struct
|
|
query, // SQL query
|
|
// Parameters matching $1, $2, $3, etc. in query
|
|
input.UserID,
|
|
string(hash), // Store hash, not raw token
|
|
input.UserAgent,
|
|
input.IPAddress,
|
|
input.DeviceName,
|
|
input.DeviceType,
|
|
input.ExpiresAt,
|
|
)
|
|
return session, err
|
|
}
|
|
|
|
// FindBySessionIDAndToken looks up a valid session by session ID and refresh token.
|
|
// This is used to validate refresh tokens during token refresh requests.
|
|
//
|
|
// Why we need both session ID and token:
|
|
// - Session ID comes from JWT claims (identifies which session)
|
|
// - Token is the actual refresh token (proves possession)
|
|
// - Both must match for validation to succeed
|
|
//
|
|
// Security checks performed:
|
|
// 1. Token hash must match stored hash
|
|
// 2. Session ID must match
|
|
// 3. Session must not be revoked (is_revoked = FALSE)
|
|
// 4. Session must not be expired (expires_at > NOW())
|
|
//
|
|
// Why hash the token for lookup?
|
|
// - We never store plaintext tokens in database
|
|
// - Hash the provided token using same algorithm (SHA-256)
|
|
// - Look up by the hash
|
|
// - If database is breached, attackers get hashes, not usable tokens
|
|
//
|
|
// Flow:
|
|
// 1. Hash the provided token
|
|
// 2. Query database for matching session ID and token hash
|
|
// 3. Only return if session is valid (not revoked, not expired)
|
|
// 4. Return nil if not found (not an error, just not found)
|
|
//
|
|
// Return values:
|
|
// - (*Session, nil): Session found and valid
|
|
// - (nil, nil): Session not found (not revoked/expired, or doesn't exist)
|
|
// - (nil, error): Database error occurred
|
|
func (r *SessionRepository) FindBySessionIDAndToken(ctx context.Context, sessionId uuid.UUID, token string) (*models.Session, error) {
|
|
session := &models.Session{}
|
|
|
|
// SQL query with multiple conditions for security
|
|
query := `
|
|
SELECT id, user_id, refresh_token_hash, user_agent,
|
|
ip_address, device_name, device_type, expires_at, is_revoked,
|
|
revoked_at, revoked_reason, created_at, last_used_at
|
|
FROM sessions
|
|
WHERE refresh_token_hash = $1 -- Token must match
|
|
AND id = $2 -- Session ID must match
|
|
AND is_revoked = FALSE -- Session must not be revoked
|
|
AND expires_at > NOW() -- Session must not be expired
|
|
`
|
|
|
|
// Hash the provided token using same algorithm used during creation
|
|
// This allows us to look up the session by the hash
|
|
tokenHash := hashToken(token)
|
|
|
|
// Execute query
|
|
err := r.db.GetContext(
|
|
ctx,
|
|
session,
|
|
query,
|
|
tokenHash, // $1 - Hashed token for lookup
|
|
sessionId, // $2 - Session ID for matching
|
|
)
|
|
|
|
// Handle "not found" case specially
|
|
// sql.ErrNoRows means query executed successfully but returned no rows
|
|
// This is not an error condition - it just means session doesn't exist or is invalid
|
|
if err == sql.ErrNoRows {
|
|
return nil, nil // Return nil session and nil error
|
|
}
|
|
|
|
return session, err
|
|
}
|
|
|
|
// FindById retrieves a session by its ID if it's valid (not revoked, not expired).
|
|
// This is useful for:
|
|
// - Checking session status
|
|
// - Updating session information
|
|
// - Listing user's sessions
|
|
//
|
|
// Validation checks:
|
|
// - Session must exist
|
|
// - Session must not be revoked (is_revoked = FALSE)
|
|
// - Session must not be expired (expires_at > NOW())
|
|
//
|
|
// Unlike FindBySessionIDAndToken, this doesn't verify the token itself,
|
|
// just checks if the session exists and is valid.
|
|
//
|
|
// Return values:
|
|
// - (*Session, nil): Session found and valid
|
|
// - (nil, nil): Session not found or is invalid
|
|
// - (nil, error): Database error occurred
|
|
func (r *SessionRepository) FindById(ctx context.Context, id uuid.UUID) (*models.Session, error) {
|
|
session := &models.Session{}
|
|
|
|
// Query for session by ID with validation checks
|
|
query := `
|
|
SELECT id, user_id, refresh_token_hash, user_agent,
|
|
ip_address, device_name, device_type, expires_at, is_revoked,
|
|
revoked_at, revoked_reason, created_at, last_used_at
|
|
FROM sessions
|
|
WHERE id = $1 -- Match session ID
|
|
AND is_revoked = FALSE -- Must not be revoked
|
|
AND expires_at > NOW() -- Must not be expired
|
|
`
|
|
|
|
err := r.db.GetContext(
|
|
ctx,
|
|
session,
|
|
query,
|
|
id, // $1 - Session ID
|
|
)
|
|
|
|
// Handle "not found" case
|
|
if err == sql.ErrNoRows {
|
|
return nil, nil // Not found is not an error
|
|
}
|
|
return session, err
|
|
}
|
|
|
|
// UpdateLastUsed updates the last_used_at timestamp for a session.
|
|
// This is called whenever a session's refresh token is used to get a new access token.
|
|
//
|
|
// Why track last usage?
|
|
// 1. Security: Identify sessions that haven't been used recently
|
|
// 2. Cleanup: Can remove stale sessions
|
|
// 3. User awareness: Show users which sessions are actively being used
|
|
// 4. Anomaly detection: Unusual usage patterns might indicate compromise
|
|
//
|
|
// Called by:
|
|
// - Token refresh endpoint (every time user gets new access token)
|
|
// - Typically happens every 15 minutes (when access token expires)
|
|
//
|
|
// Updates:
|
|
// - last_used_at: Set to current database time (NOW())
|
|
//
|
|
// Error handling:
|
|
// - Returns error if update fails (database error)
|
|
// - Caller usually ignores this error (not critical for token refresh)
|
|
func (r *SessionRepository) UpdateLastUsed(ctx context.Context, id uuid.UUID) error {
|
|
// SQL update query
|
|
// Uses NOW() for database-consistent timestamp (not Go's time.Now())
|
|
query := `
|
|
UPDATE sessions
|
|
SET last_used_at = NOW() -- Update to current database time
|
|
WHERE id=$1 -- Only update this session
|
|
`
|
|
|
|
// ExecContext executes query that doesn't return rows (UPDATE, DELETE, etc.)
|
|
// Returns:
|
|
// - sql.Result: Contains rows affected, last insert ID, etc.
|
|
// - error: Database error if query fails
|
|
_, err := r.db.ExecContext(
|
|
ctx,
|
|
query,
|
|
id, // $1 - Session ID
|
|
)
|
|
|
|
return err
|
|
}
|
|
|
|
// Revoke marks a session as revoked, preventing its refresh token from being used.
|
|
// This is called during:
|
|
// - User logout (revoke current session)
|
|
// - Security actions (revoke compromised session)
|
|
// - Administrative actions (force logout)
|
|
//
|
|
// What happens:
|
|
// 1. Finds session by token hash
|
|
// 2. Sets is_revoked = TRUE (marks as invalid)
|
|
// 3. Sets revoked_at = NOW() (records when revoked)
|
|
// 4. Sets revoked_reason (why it was revoked)
|
|
//
|
|
// Why track revocation reason?
|
|
// - Audit trail: Know why sessions ended
|
|
// - Analytics: Understand logout patterns
|
|
// - Security: Identify security-related revocations
|
|
// - User awareness: Can show user why session ended
|
|
//
|
|
// Common revocation reasons:
|
|
// - "user_logout": User clicked logout button
|
|
// - "password_change": Password was changed (invalidate all sessions)
|
|
// - "security_breach": Suspected compromise
|
|
// - "admin_action": Administrator revoked session
|
|
// - "device_lost": User reported device lost/stolen
|
|
//
|
|
// Important: Once revoked, the session cannot be un-revoked.
|
|
// User must log in again to create a new session.
|
|
//
|
|
// Error handling:
|
|
// - Returns error if update fails
|
|
// - No error if session doesn't exist (idempotent operation)
|
|
func (r *SessionRepository) Revoke(ctx context.Context, token string, reason string) error {
|
|
// Hash the token to find corresponding session
|
|
// We store hashed tokens, so we must hash to look up
|
|
tokenHash := hashToken(token)
|
|
|
|
// SQL update query to mark session as revoked
|
|
query := `
|
|
UPDATE sessions
|
|
SET is_revoked = TRUE, -- Mark as revoked
|
|
revoked_at = NOW(), -- Record revocation time
|
|
revoked_reason = $2 -- Record why it was revoked
|
|
WHERE refresh_token_hash=$1 -- Find session by token hash
|
|
`
|
|
|
|
// Execute update
|
|
// Note: UPDATE returns success even if no rows matched
|
|
// This makes the operation idempotent (safe to call multiple times)
|
|
_, err := r.db.ExecContext(
|
|
ctx,
|
|
query,
|
|
tokenHash, // $1 - Token hash to find session
|
|
reason, // $2 - Why session is being revoked
|
|
)
|
|
|
|
return err
|
|
}
|
|
|
|
// RevokeByUserId revokes all sessions for a specific user.
|
|
// This is a security feature called "logout everywhere" or "logout all devices".
|
|
//
|
|
// When to use this:
|
|
// 1. Password change: Invalidate all existing sessions (force re-login)
|
|
// 2. Security breach: User reports account compromise
|
|
// 3. Administrative action: Admin needs to force user logout
|
|
// 4. Account deletion: Revoke all sessions before deleting user
|
|
//
|
|
// What it does:
|
|
// - Finds all non-revoked sessions for the user
|
|
// - Marks them all as revoked
|
|
// - Records when and why they were revoked
|
|
//
|
|
// After calling this:
|
|
// - All refresh tokens for this user become invalid
|
|
// - User must log in again on all devices
|
|
// - Current access tokens remain valid until they expire (typically 15 minutes)
|
|
//
|
|
// Note: This doesn't immediately invalidate access tokens because:
|
|
// - Access tokens are stateless (not checked against database)
|
|
// - They expire quickly anyway (15 minutes)
|
|
// - Checking database for every API request would be slow
|
|
// - For immediate invalidation, would need a token blacklist (expensive)
|
|
//
|
|
// Error handling:
|
|
// - Returns error if update fails
|
|
// - No error if user has no sessions (idempotent)
|
|
func (r *SessionRepository) RevokeByUserId(ctx context.Context, userID uuid.UUID, reason string) error {
|
|
// SQL query to revoke all user's sessions
|
|
query := `
|
|
UPDATE sessions
|
|
SET is_revoked = TRUE, -- Mark as revoked
|
|
revoked_at = NOW(), -- Record revocation time
|
|
revoked_reason = $2 -- Record reason
|
|
WHERE user_id = $1 -- All sessions for this user
|
|
AND is_revoked = FALSE -- Only revoke non-revoked sessions (optimization)
|
|
`
|
|
|
|
// Execute update
|
|
// Could affect 0 to many rows depending on how many sessions user has
|
|
_, err := r.db.ExecContext(
|
|
ctx,
|
|
query,
|
|
userID, // $1 - User whose sessions to revoke
|
|
reason, // $2 - Reason for revocation
|
|
)
|
|
|
|
return err
|
|
}
|
|
|
|
// DeleteExpired removes expired and old revoked sessions from database.
|
|
// This is a cleanup/maintenance operation typically run as a scheduled job.
|
|
//
|
|
// What gets deleted:
|
|
// 1. Sessions past their expiration date (expires_at < NOW())
|
|
// 2. Revoked sessions older than 30 days (is_revoked AND revoked_at < 30 days ago)
|
|
//
|
|
// Why delete expired sessions?
|
|
// - Database cleanup: Prevents unlimited growth
|
|
// - Performance: Smaller tables = faster queries
|
|
// - Privacy: No need to keep old session data forever
|
|
// - Compliance: Data retention policies may require deletion
|
|
//
|
|
// Why keep revoked sessions for 30 days?
|
|
// - Audit trail: Need recent history for security investigations
|
|
// - User support: Can check recent logouts for support issues
|
|
// - Analytics: Understand logout patterns
|
|
// - After 30 days: Unlikely to need the data, safe to delete
|
|
//
|
|
// When to run this:
|
|
// - Scheduled job: Daily or weekly (off-peak hours)
|
|
// - Not during request handling: Too slow, not time-critical
|
|
// - Could use database job scheduler or cron job
|
|
//
|
|
// Performance considerations:
|
|
// - Could be slow if millions of sessions
|
|
// - Consider adding indexes on expires_at and revoked_at
|
|
// - Could batch delete (delete 1000 at a time) for very large tables
|
|
// - Consider partitioning sessions table by date for easier cleanup
|
|
//
|
|
// Return value:
|
|
// - Number of rows deleted (for logging/monitoring)
|
|
// - Error if delete fails
|
|
func (r *SessionRepository) DeleteExpired(ctx context.Context) (int64, error) {
|
|
// SQL delete query with two conditions (connected by OR)
|
|
// Deletes sessions that meet EITHER condition
|
|
query := `
|
|
DELETE FROM sessions
|
|
WHERE expires_at < NOW() -- Condition 1: Session expired
|
|
OR (
|
|
is_revoked = TRUE -- Condition 2: Session revoked AND
|
|
AND revoked_at < NOW() - INTERVAL '30 days' -- More than 30 days ago
|
|
)
|
|
`
|
|
|
|
// Execute delete operation
|
|
// DELETE returns number of affected rows
|
|
result, err := r.db.ExecContext(ctx, query)
|
|
|
|
if err != nil {
|
|
return 0, err // Return 0 and error if delete fails
|
|
}
|
|
|
|
// Extract number of rows deleted
|
|
// This is useful for logging: "Deleted 1,234 expired sessions"
|
|
return result.RowsAffected()
|
|
}
|
|
|
|
// ListByUserID retrieves all sessions for a specific user.
|
|
// This is used to show users their active sessions (like Gmail's "devices & activity").
|
|
//
|
|
// What it returns:
|
|
// - ALL sessions for user (both active and revoked)
|
|
// - Includes expired sessions (caller can filter if needed)
|
|
// - Sorted by database order (consider adding ORDER BY created_at DESC)
|
|
//
|
|
// Use cases:
|
|
// 1. Security page: Show user where they're logged in
|
|
// 2. Session management: Let user revoke specific sessions
|
|
// 3. Audit: Show login history with locations/devices
|
|
// 4. Support: Help user understand their login activity
|
|
//
|
|
// What each session shows:
|
|
// - When created (created_at)
|
|
// - Last used (last_used_at)
|
|
// - Device type (mobile/desktop/web)
|
|
// - Location (IP address)
|
|
// - Browser (user agent)
|
|
// - Status (is_revoked, expires_at)
|
|
//
|
|
// Privacy note:
|
|
// - Never expose refresh_token_hash to client
|
|
// - IP addresses may be considered personal data (GDPR)
|
|
// - Consider anonymizing/hashing old IP addresses
|
|
//
|
|
// Performance consideration:
|
|
// - Most users have few sessions (1-5)
|
|
// - Not a concern unless user has 100+ sessions
|
|
// - Consider adding pagination for enterprise users
|
|
//
|
|
// Return values:
|
|
// - Slice of sessions (empty slice if user has no sessions)
|
|
// - Error if query fails
|
|
func (r *SessionRepository) ListByUserID(ctx context.Context, userId uuid.UUID) ([]*models.Session, error) {
|
|
// Slice to hold returned sessions
|
|
var sessions []*models.Session
|
|
|
|
// SQL query to get all user sessions
|
|
// No filtering by is_revoked or expires_at - returns everything
|
|
// Consider adding: ORDER BY created_at DESC for newest first
|
|
query := `
|
|
SELECT id, user_id, refresh_token_hash, user_agent,
|
|
ip_address, device_name, device_type, expires_at, is_revoked,
|
|
revoked_at, revoked_reason, created_at, last_used_at
|
|
FROM sessions
|
|
WHERE user_id=$1 -- All sessions for this user
|
|
`
|
|
|
|
// SelectContext is like GetContext but for multiple rows
|
|
// - Executes query and scans all rows into slice
|
|
// - Maps columns to struct fields by db tags
|
|
// - Returns empty slice if no rows (not an error)
|
|
err := r.db.SelectContext(
|
|
ctx,
|
|
&sessions, // Destination slice (must be pointer to slice)
|
|
query,
|
|
userId, // $1 - User ID
|
|
)
|
|
|
|
return sessions, err
|
|
}
|
|
|
|
// hashToken creates a SHA-256 hash of a token and returns it as a base64-encoded string.
|
|
// This is used for secure token storage in the database.
|
|
//
|
|
// Why hash tokens?
|
|
// 1. Security: If database is breached, attackers get hashes not usable tokens
|
|
// 2. Defense in depth: Multiple layers of security
|
|
// 3. Compliance: Some regulations require token hashing
|
|
// 4. Best practice: Never store sensitive tokens in plaintext
|
|
//
|
|
// Why SHA-256 instead of bcrypt?
|
|
// - SHA-256 is deterministic: Same input always gives same output
|
|
// Example: hashToken("mytoken") always gives same hash
|
|
// Allows database lookup by hash
|
|
// - bcrypt is random: Same input gives different output each time (due to salt)
|
|
// Example: bcrypt("mytoken") gives different hash every time
|
|
// Can't look up by hash, must compare with every stored hash
|
|
// - SHA-256 is fast: Good for tokens that are looked up frequently
|
|
// - bcrypt is slow: Good for passwords to resist brute force
|
|
//
|
|
// Why base64 encode?
|
|
// - SHA-256 produces binary data (32 bytes)
|
|
// - Binary data is hard to store in text fields
|
|
// - base64 converts binary to text (safe for VARCHAR/TEXT columns)
|
|
// - URLEncoding variant avoids special characters (+, /, =)
|
|
//
|
|
// Process:
|
|
// 1. Convert token string to bytes
|
|
// 2. Hash using SHA-256 (produces 32-byte hash)
|
|
// 3. Encode to base64 (produces ~44-character string)
|
|
// 4. Store in database as string
|
|
//
|
|
// Security note:
|
|
// - SHA-256 is one-way: Can't reverse hash to get original token
|
|
// - Can only verify by hashing again and comparing
|
|
// - This means if database is compromised, tokens can't be extracted
|
|
//
|
|
// Return:
|
|
// - Base64-encoded SHA-256 hash as string
|
|
func hashToken(token string) string {
|
|
// Create SHA-256 hash of token bytes
|
|
// sha256.Sum256 returns [32]byte array
|
|
hash := sha256.Sum256([]byte(token))
|
|
|
|
// Encode hash bytes to base64 string
|
|
// URLEncoding uses URL-safe characters (no +, /, =)
|
|
// hash[:] converts [32]byte array to []byte slice
|
|
return base64.URLEncoding.EncodeToString(hash[:])
|
|
}
|