package auth import ( "crypto/aes" "crypto/cipher" "crypto/rand" "crypto/sha256" "encoding/base64" "errors" "io" "time" "github.com/golang-jwt/jwt/v5" ) type GitHubOAuthStateClaims struct { StaffID uint `json:"staffId"` jwt.RegisteredClaims } func IssueGitHubOAuthState(staffID uint) (string, error) { claims := GitHubOAuthStateClaims{ StaffID: staffID, RegisteredClaims: jwt.RegisteredClaims{ ExpiresAt: jwt.NewNumericDate(time.Now().Add(15 * time.Minute)), IssuedAt: jwt.NewNumericDate(time.Now()), }, } t := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) return t.SignedString([]byte(JWTSecret())) } func ParseGitHubOAuthState(state string) (uint, error) { t, err := jwt.ParseWithClaims(state, &GitHubOAuthStateClaims{}, func(t *jwt.Token) (any, error) { return []byte(JWTSecret()), nil }) if err != nil { return 0, err } claims, ok := t.Claims.(*GitHubOAuthStateClaims) if !ok || !t.Valid || claims.StaffID == 0 { return 0, errors.New("invalid oauth state") } return claims.StaffID, nil } func tokenCipher() (cipher.AEAD, error) { sum := sha256.Sum256([]byte(JWTSecret())) block, err := aes.NewCipher(sum[:]) if err != nil { return nil, err } return cipher.NewGCM(block) } func EncryptSecret(plain string) (string, error) { if plain == "" { return "", errors.New("empty secret") } gcm, err := tokenCipher() if err != nil { return "", err } nonce := make([]byte, gcm.NonceSize()) if _, err := io.ReadFull(rand.Reader, nonce); err != nil { return "", err } sealed := gcm.Seal(nonce, nonce, []byte(plain), nil) return base64.StdEncoding.EncodeToString(sealed), nil } func DecryptSecret(encoded string) (string, error) { raw, err := base64.StdEncoding.DecodeString(encoded) if err != nil { return "", err } gcm, err := tokenCipher() if err != nil { return "", err } if len(raw) < gcm.NonceSize() { return "", errors.New("ciphertext too short") } nonce, ciphertext := raw[:gcm.NonceSize()], raw[gcm.NonceSize():] plain, err := gcm.Open(nil, nonce, ciphertext, nil) if err != nil { return "", err } return string(plain), nil }