1. go
  2. /web
  3. /authentication

Implementing Authentication in Go Web Applications

Authentication is a critical component of web applications. This guide covers different authentication methods and their implementation in Go.

Basic Authentication

Password-Based Authentication

type User struct {
    ID           string
    Email        string
    PasswordHash string
}

type UserService struct {
    db *sql.DB
}

func (s *UserService) Authenticate(email, password string) (*User, error) {
    user := &User{}
    err := s.db.QueryRow(
        "SELECT id, email, password_hash FROM users WHERE email = $1",
        email,
    ).Scan(&user.ID, &user.Email, &user.PasswordHash)
    
    if err != nil {
        if err == sql.ErrNoRows {
            return nil, errors.New("invalid credentials")
        }
        return nil, err
    }
    
    // Verify password
    if err := bcrypt.CompareHashAndPassword(
        []byte(user.PasswordHash),
        []byte(password),
    ); err != nil {
        return nil, errors.New("invalid credentials")
    }
    
    return user, nil
}

JWT Authentication

type Claims struct {
    UserID string `json:"user_id"`
    Email  string `json:"email"`
    jwt.StandardClaims
}

type JWTService struct {
    secretKey []byte
}

func (s *JWTService) GenerateToken(user *User) (string, error) {
    claims := &Claims{
        UserID: user.ID,
        Email:  user.Email,
        StandardClaims: jwt.StandardClaims{
            ExpiresAt: time.Now().Add(24 * time.Hour).Unix(),
            IssuedAt:  time.Now().Unix(),
        },
    }
    
    token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
    return token.SignedString(s.secretKey)
}

func (s *JWTService) ValidateToken(tokenString string) (*Claims, error) {
    token, err := jwt.ParseWithClaims(
        tokenString,
        &Claims{},
        func(token *jwt.Token) (interface{}, error) {
            return s.secretKey, nil
        },
    )
    
    if err != nil {
        return nil, err
    }
    
    if claims, ok := token.Claims.(*Claims); ok && token.Valid {
        return claims, nil
    }
    
    return nil, errors.New("invalid token")
}

OAuth2 Integration

OAuth2 Client

type OAuth2Config struct {
    ClientID     string
    ClientSecret string
    RedirectURL  string
    Scopes       []string
}

type OAuth2Service struct {
    config *oauth2.Config
}

func NewOAuth2Service(cfg OAuth2Config) *OAuth2Service {
    config := &oauth2.Config{
        ClientID:     cfg.ClientID,
        ClientSecret: cfg.ClientSecret,
        RedirectURL:  cfg.RedirectURL,
        Scopes:       cfg.Scopes,
        Endpoint:     google.Endpoint, // Or other provider endpoints
    }
    
    return &OAuth2Service{config: config}
}

func (s *OAuth2Service) GetAuthURL(state string) string {
    return s.config.AuthCodeURL(state)
}

func (s *OAuth2Service) Exchange(ctx context.Context, code string) (*oauth2.Token, error) {
    return s.config.Exchange(ctx, code)
}

func (s *OAuth2Service) GetUserInfo(ctx context.Context, token *oauth2.Token) (*UserInfo, error) {
    client := s.config.Client(ctx, token)
    resp, err := client.Get("https://www.googleapis.com/oauth2/v2/userinfo")
    if err != nil {
        return nil, err
    }
    defer resp.Body.Close()
    
    var userInfo UserInfo
    if err := json.NewDecoder(resp.Body).Decode(&userInfo); err != nil {
        return nil, err
    }
    
    return &userInfo, nil
}

Authentication Middleware

JWT Middleware

func JWTMiddleware(jwtService *JWTService) func(http.Handler) http.Handler {
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            // Get token from header
            authHeader := r.Header.Get("Authorization")
            if authHeader == "" {
                http.Error(w, "Authorization header required", http.StatusUnauthorized)
                return
            }
            
            // Parse token
            tokenString := strings.TrimPrefix(authHeader, "Bearer ")
            claims, err := jwtService.ValidateToken(tokenString)
            if err != nil {
                http.Error(w, "Invalid token", http.StatusUnauthorized)
                return
            }
            
            // Add claims to context
            ctx := context.WithValue(r.Context(), "claims", claims)
            next.ServeHTTP(w, r.WithContext(ctx))
        })
    }
}

Role-Based Authorization

type Role string

const (
    RoleAdmin  Role = "admin"
    RoleUser   Role = "user"
    RoleGuest  Role = "guest"
)

func RequireRole(roles ...Role) func(http.Handler) http.Handler {
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            claims := r.Context().Value("claims").(*Claims)
            
            // Check if user has required role
            userRole := Role(claims.Role)
            hasRole := false
            for _, role := range roles {
                if userRole == role {
                    hasRole = true
                    break
                }
            }
            
            if !hasRole {
                http.Error(w, "Forbidden", http.StatusForbidden)
                return
            }
            
            next.ServeHTTP(w, r)
        })
    }
}

Best Practices

1. Password Security

type PasswordService struct {
    minLength int
    maxLength int
}

func (s *PasswordService) HashPassword(password string) (string, error) {
    // Validate password
    if err := s.ValidatePassword(password); err != nil {
        return "", err
    }
    
    // Generate hash
    hash, err := bcrypt.GenerateFromPassword(
        []byte(password),
        bcrypt.DefaultCost,
    )
    if err != nil {
        return "", err
    }
    
    return string(hash), nil
}

func (s *PasswordService) ValidatePassword(password string) error {
    if len(password) < s.minLength {
        return fmt.Errorf("password must be at least %d characters", s.minLength)
    }
    if len(password) > s.maxLength {
        return fmt.Errorf("password must be less than %d characters", s.maxLength)
    }
    
    // Check for required character types
    var (
        hasUpper   bool
        hasLower   bool
        hasNumber  bool
        hasSpecial bool
    )
    
    for _, char := range password {
        switch {
        case unicode.IsUpper(char):
            hasUpper = true
        case unicode.IsLower(char):
            hasLower = true
        case unicode.IsNumber(char):
            hasNumber = true
        case unicode.IsPunct(char) || unicode.IsSymbol(char):
            hasSpecial = true
        }
    }
    
    if !hasUpper || !hasLower || !hasNumber || !hasSpecial {
        return errors.New("password must contain uppercase, lowercase, number, and special character")
    }
    
    return nil
}

2. Token Management

type TokenManager struct {
    store      TokenStore
    expiration time.Duration
}

func (tm *TokenManager) CreateToken(userID string) (*Token, error) {
    token := &Token{
        ID:        generateSecureID(),
        UserID:    userID,
        CreatedAt: time.Now(),
        ExpiresAt: time.Now().Add(tm.expiration),
    }
    
    if err := tm.store.Save(token); err != nil {
        return nil, err
    }
    
    return token, nil
}

func (tm *TokenManager) RevokeToken(tokenID string) error {
    return tm.store.Delete(tokenID)
}

func (tm *TokenManager) RefreshToken(oldToken *Token) (*Token, error) {
    // Validate old token
    if oldToken.IsExpired() {
        return nil, errors.New("token expired")
    }
    
    // Create new token
    newToken, err := tm.CreateToken(oldToken.UserID)
    if err != nil {
        return nil, err
    }
    
    // Revoke old token
    if err := tm.RevokeToken(oldToken.ID); err != nil {
        return nil, err
    }
    
    return newToken, nil
}

3. Session Management

type AuthSession struct {
    UserID       string
    AccessToken  string
    RefreshToken string
    ExpiresAt    time.Time
}

type AuthManager struct {
    sessions    SessionStore
    tokens      TokenManager
    expiration  time.Duration
}

func (am *AuthManager) CreateSession(user *User) (*AuthSession, error) {
    // Create access token
    accessToken, err := am.tokens.CreateToken(user.ID)
    if err != nil {
        return nil, err
    }
    
    // Create refresh token
    refreshToken, err := am.tokens.CreateToken(user.ID)
    if err != nil {
        return nil, err
    }
    
    // Create session
    session := &AuthSession{
        UserID:       user.ID,
        AccessToken:  accessToken.ID,
        RefreshToken: refreshToken.ID,
        ExpiresAt:    time.Now().Add(am.expiration),
    }
    
    if err := am.sessions.Save(session); err != nil {
        return nil, err
    }
    
    return session, nil
}

Common Patterns

1. Multi-Factor Authentication

type MFAProvider interface {
    GenerateSecret() (string, error)
    ValidateCode(secret, code string) bool
}

type TOTPProvider struct {
    window uint
}

func (p *TOTPProvider) GenerateSecret() (string, error) {
    secret := make([]byte, 20)
    if _, err := rand.Read(secret); err != nil {
        return "", err
    }
    return base32.StdEncoding.EncodeToString(secret), nil
}

func (p *TOTPProvider) ValidateCode(secret, code string) bool {
    secretBytes, err := base32.StdEncoding.DecodeString(secret)
    if err != nil {
        return false
    }
    
    return totp.ValidateCustom(
        code,
        string(secretBytes),
        time.Now(),
        totp.ValidateOpts{
            Period:    30,
            Skew:     p.window,
            Digits:    6,
            Algorithm: otp.AlgorithmSHA1,
        },
    )
}

2. API Key Authentication

type APIKey struct {
    ID        string
    Key       string
    UserID    string
    CreatedAt time.Time
    ExpiresAt time.Time
}

type APIKeyService struct {
    store APIKeyStore
}

func (s *APIKeyService) CreateKey(userID string) (*APIKey, error) {
    key := &APIKey{
        ID:        generateSecureID(),
        Key:       generateAPIKey(),
        UserID:    userID,
        CreatedAt: time.Now(),
        ExpiresAt: time.Now().AddDate(1, 0, 0), // 1 year expiration
    }
    
    if err := s.store.Save(key); err != nil {
        return nil, err
    }
    
    return key, nil
}

func (s *APIKeyService) ValidateKey(keyString string) (*APIKey, error) {
    key, err := s.store.Get(keyString)
    if err != nil {
        return nil, err
    }
    
    if key.IsExpired() {
        return nil, errors.New("api key expired")
    }
    
    return key, nil
}

Next Steps