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
- Learn about REST APIs
- Explore GraphQL
- Study WebSockets