auth.go
127 lines1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
package security
import (
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"net/http"
"strings"
"time"
)
var (
ErrInvalidToken = errors.New("invalid token")
ErrExpiredToken = errors.New("token expired")
)
// CreateToken creates an HS256 JWT with the given claims, secret, and expiry.
func CreateToken(claims map[string]any, secret string, expiry time.Duration) (string, error) {
header := base64URLEncode([]byte(`{"alg":"HS256","typ":"JWT"}`))
if claims == nil {
claims = make(map[string]any)
}
claims["exp"] = time.Now().Add(expiry).Unix()
claims["iat"] = time.Now().Unix()
payload, err := json.Marshal(claims)
if err != nil {
return "", fmt.Errorf("marshal claims: %w", err)
}
payloadEnc := base64URLEncode(payload)
sig := signHS256(header+"."+payloadEnc, secret)
return header + "." + payloadEnc + "." + sig, nil
}
// ValidateToken validates an HS256 JWT and returns its claims.
func ValidateToken(token, secret string) (map[string]any, error) {
parts := strings.SplitN(token, ".", 3)
if len(parts) != 3 {
return nil, ErrInvalidToken
}
// Verify signature
expected := signHS256(parts[0]+"."+parts[1], secret)
if !hmac.Equal([]byte(expected), []byte(parts[2])) {
return nil, ErrInvalidToken
}
// Decode payload
payload, err := base64URLDecode(parts[1])
if err != nil {
return nil, ErrInvalidToken
}
var claims map[string]any
if err := json.Unmarshal(payload, &claims); err != nil {
return nil, ErrInvalidToken
}
// Check expiry
if exp, ok := claims["exp"].(float64); ok {
if time.Now().Unix() > int64(exp) {
return nil, ErrExpiredToken
}
}
return claims, nil
}
// SetSessionCookie sets a secure session cookie with the given token.
func SetSessionCookie(w http.ResponseWriter, name, token string, expiry time.Duration) {
http.SetCookie(w, &http.Cookie{
Name: name,
Value: token,
Path: "/",
MaxAge: int(expiry.Seconds()),
HttpOnly: true,
Secure: true,
SameSite: http.SameSiteLaxMode,
})
}
// ClearSessionCookie removes a session cookie.
func ClearSessionCookie(w http.ResponseWriter, name string) {
http.SetCookie(w, &http.Cookie{
Name: name,
Value: "",
Path: "/",
MaxAge: -1,
HttpOnly: true,
})
}
// SessionFromRequest extracts a session cookie value from the request.
// Returns empty string if not found.
func SessionFromRequest(r *http.Request, name string) string {
cookie, err := r.Cookie(name)
if err != nil {
return ""
}
return cookie.Value
}
func signHS256(data, secret string) string {
mac := hmac.New(sha256.New, []byte(secret))
mac.Write([]byte(data))
return base64URLEncode(mac.Sum(nil))
}
func base64URLEncode(data []byte) string {
return strings.TrimRight(base64.URLEncoding.EncodeToString(data), "=")
}
func base64URLDecode(s string) ([]byte, error) {
// Add padding
switch len(s) % 4 {
case 2:
s += "=="
case 3:
s += "="
}
return base64.URLEncoding.DecodeString(s)
}