refactor(auth): split IAM module and add access/refresh session flow
This commit is contained in:
147
internal/iam/token.go
Normal file
147
internal/iam/token.go
Normal file
@@ -0,0 +1,147 @@
|
||||
package iam
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type tokenManager struct {
|
||||
secret []byte
|
||||
ttl time.Duration
|
||||
kind tokenKind
|
||||
}
|
||||
|
||||
func newTokenManager(secret string, ttl time.Duration, kind tokenKind) *tokenManager {
|
||||
return &tokenManager{secret: []byte(secret), ttl: ttl, kind: kind}
|
||||
}
|
||||
|
||||
func (t *tokenManager) generateAccess(userID int64, sessionID, jti string) (string, time.Time, error) {
|
||||
now := time.Now().UTC()
|
||||
expiresAt := now.Add(t.ttl)
|
||||
payload := accessClaims{
|
||||
UserID: userID,
|
||||
SessionID: sessionID,
|
||||
JTI: jti,
|
||||
IssuedAt: now.Unix(),
|
||||
ExpiresAt: expiresAt.Unix(),
|
||||
Type: t.kind,
|
||||
}
|
||||
token, err := t.sign(payload)
|
||||
if err != nil {
|
||||
return "", time.Time{}, err
|
||||
}
|
||||
return token, expiresAt, nil
|
||||
}
|
||||
|
||||
func (t *tokenManager) parseAccess(token string) (accessClaims, error) {
|
||||
var claims accessClaims
|
||||
if err := t.verifyAndDecode(token, &claims); err != nil {
|
||||
return claims, err
|
||||
}
|
||||
if claims.Type != tokenKindAccess {
|
||||
return claims, errInvalidToken
|
||||
}
|
||||
if claims.UserID == 0 || strings.TrimSpace(claims.SessionID) == "" || strings.TrimSpace(claims.JTI) == "" {
|
||||
return claims, errInvalidToken
|
||||
}
|
||||
if time.Now().UTC().Unix() > claims.ExpiresAt {
|
||||
return claims, errTokenExpired
|
||||
}
|
||||
return claims, nil
|
||||
}
|
||||
|
||||
func (t *tokenManager) generateRefresh(userID int64, sessionID, refreshID string) (string, time.Time, error) {
|
||||
now := time.Now().UTC()
|
||||
expiresAt := now.Add(t.ttl)
|
||||
payload := refreshClaims{
|
||||
UserID: userID,
|
||||
SessionID: sessionID,
|
||||
RefreshID: refreshID,
|
||||
IssuedAt: now.Unix(),
|
||||
ExpiresAt: expiresAt.Unix(),
|
||||
Type: t.kind,
|
||||
}
|
||||
token, err := t.sign(payload)
|
||||
if err != nil {
|
||||
return "", time.Time{}, err
|
||||
}
|
||||
return token, expiresAt, nil
|
||||
}
|
||||
|
||||
func (t *tokenManager) parseRefresh(token string) (refreshClaims, error) {
|
||||
var claims refreshClaims
|
||||
if err := t.verifyAndDecode(token, &claims); err != nil {
|
||||
return claims, err
|
||||
}
|
||||
if claims.Type != tokenKindRefresh {
|
||||
return claims, errInvalidToken
|
||||
}
|
||||
if claims.UserID == 0 || strings.TrimSpace(claims.SessionID) == "" || strings.TrimSpace(claims.RefreshID) == "" {
|
||||
return claims, errInvalidToken
|
||||
}
|
||||
if time.Now().UTC().Unix() > claims.ExpiresAt {
|
||||
return claims, errTokenExpired
|
||||
}
|
||||
return claims, nil
|
||||
}
|
||||
|
||||
func (t *tokenManager) sign(v any) (string, error) {
|
||||
raw, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
payload := base64.RawURLEncoding.EncodeToString(raw)
|
||||
sig := t.signPayload(payload)
|
||||
return payload + "." + sig, nil
|
||||
}
|
||||
|
||||
func (t *tokenManager) verifyAndDecode(token string, out any) error {
|
||||
parts := strings.Split(token, ".")
|
||||
if len(parts) != 2 {
|
||||
return errInvalidToken
|
||||
}
|
||||
payload, sig := parts[0], parts[1]
|
||||
if !t.verifyPayload(payload, sig) {
|
||||
return errInvalidToken
|
||||
}
|
||||
raw, err := base64.RawURLEncoding.DecodeString(payload)
|
||||
if err != nil {
|
||||
return errInvalidToken
|
||||
}
|
||||
if err := json.Unmarshal(raw, out); err != nil {
|
||||
return errInvalidToken
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *tokenManager) signPayload(payload string) string {
|
||||
mac := hmac.New(sha256.New, t.secret)
|
||||
mac.Write([]byte(payload))
|
||||
return base64.RawURLEncoding.EncodeToString(mac.Sum(nil))
|
||||
}
|
||||
|
||||
func (t *tokenManager) verifyPayload(payload, signature string) bool {
|
||||
expected := t.signPayload(payload)
|
||||
return hmac.Equal([]byte(signature), []byte(expected))
|
||||
}
|
||||
|
||||
func randomID(prefix string) (string, error) {
|
||||
b := make([]byte, 16)
|
||||
if _, err := rand.Read(b); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return fmt.Sprintf("%s_%s", prefix, hex.EncodeToString(b)), nil
|
||||
}
|
||||
|
||||
func hashRefreshToken(token, pepper string) string {
|
||||
mac := hmac.New(sha256.New, []byte(pepper))
|
||||
mac.Write([]byte(token))
|
||||
return hex.EncodeToString(mac.Sum(nil))
|
||||
}
|
||||
Reference in New Issue
Block a user