Change authcode using redis, authtoken use client secret to sign jwt
Signed-off-by: Asai Neko <sugar@sne.moe>
This commit is contained in:
@@ -1,53 +1,68 @@
|
|||||||
package authcode
|
package authcode
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"sync"
|
"nixcn-cms/data"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Token struct {
|
type Token struct {
|
||||||
Email string
|
ClientId string
|
||||||
ExpiresAt time.Time
|
Email string
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
func NewAuthCode(clientId string, email string) (string, error) {
|
||||||
store = sync.Map{}
|
ctx := context.Background()
|
||||||
)
|
|
||||||
|
|
||||||
// Generate magic token
|
// generate random code
|
||||||
func NewAuthCode(email string) (string, error) {
|
|
||||||
b := make([]byte, 32)
|
b := make([]byte, 32)
|
||||||
if _, err := rand.Read(b); err != nil {
|
if _, err := rand.Read(b); err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
code := base64.RawURLEncoding.EncodeToString(b)
|
code := base64.RawURLEncoding.EncodeToString(b)
|
||||||
|
key := "auth_code:" + code
|
||||||
|
|
||||||
store.Store(code, Token{
|
ttl := viper.GetDuration("ttl.auth_code_ttl")
|
||||||
Email: email,
|
|
||||||
ExpiresAt: time.Now().Add(viper.GetDuration("ttl.auth_code_ttl")),
|
// store auth code metadata in Redis
|
||||||
})
|
if err := data.Redis.HSet(
|
||||||
|
ctx,
|
||||||
|
key,
|
||||||
|
map[string]any{
|
||||||
|
"client_id": clientId,
|
||||||
|
"email": email,
|
||||||
|
},
|
||||||
|
).Err(); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// set expiration (one-time auth code)
|
||||||
|
if err := data.Redis.Expire(ctx, key, ttl).Err(); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
return code, nil
|
return code, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify magic token
|
func VerifyAuthCode(code string) (*Token, bool) {
|
||||||
func VerifyAuthCode(code string) (string, bool) {
|
ctx := context.Background()
|
||||||
val, ok := store.Load(code)
|
key := "auth_code:" + code
|
||||||
if !ok {
|
|
||||||
return "", false
|
// Read auth code payload
|
||||||
|
dataMap, err := data.Redis.HGetAll(ctx, key).Result()
|
||||||
|
if err != nil || len(dataMap) == 0 {
|
||||||
|
return nil, false
|
||||||
}
|
}
|
||||||
|
|
||||||
t := val.(Token)
|
// Delete auth code immediately (one-time use)
|
||||||
if time.Now().After(t.ExpiresAt) {
|
_ = data.Redis.Del(ctx, key).Err()
|
||||||
store.Delete(code)
|
|
||||||
return "", false
|
|
||||||
}
|
|
||||||
|
|
||||||
store.Delete(code)
|
return &Token{
|
||||||
return t.Email, true
|
ClientId: dataMap["client_id"],
|
||||||
|
Email: dataMap["email"],
|
||||||
|
}, true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,14 +20,16 @@ type Token struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type JwtClaims struct {
|
type JwtClaims struct {
|
||||||
UserID uuid.UUID `json:"user_id"`
|
ClientId string `json:"client_id"`
|
||||||
|
UserID uuid.UUID `json:"user_id"`
|
||||||
jwt.RegisteredClaims
|
jwt.RegisteredClaims
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate jwt clames
|
// Generate jwt clames
|
||||||
func (self *Token) NewClaims(userId uuid.UUID) JwtClaims {
|
func (self *Token) NewClaims(clientId string, userId uuid.UUID) JwtClaims {
|
||||||
return JwtClaims{
|
return JwtClaims{
|
||||||
UserID: userId,
|
ClientId: clientId,
|
||||||
|
UserID: userId,
|
||||||
RegisteredClaims: jwt.RegisteredClaims{
|
RegisteredClaims: jwt.RegisteredClaims{
|
||||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(viper.GetDuration("ttl.access_ttl"))),
|
ExpiresAt: jwt.NewNumericDate(time.Now().Add(viper.GetDuration("ttl.access_ttl"))),
|
||||||
IssuedAt: jwt.NewNumericDate(time.Now()),
|
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||||||
@@ -37,10 +39,20 @@ func (self *Token) NewClaims(userId uuid.UUID) JwtClaims {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Generate access token
|
// Generate access token
|
||||||
func (self *Token) GenerateAccessToken(userId uuid.UUID) (string, error) {
|
func (self *Token) GenerateAccessToken(clientId string, userId uuid.UUID) (string, error) {
|
||||||
claims := self.NewClaims(userId)
|
claims := self.NewClaims(clientId, userId)
|
||||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||||
secret := viper.GetString("secrets.jwt_secret")
|
|
||||||
|
clientData, err := new(data.Client).GetClientByClientId(clientId)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("error getting client data: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
secret, err := clientData.GetDecryptedSecret()
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("error getting decrypted secret: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
signedToken, err := token.SignedString([]byte(secret))
|
signedToken, err := token.SignedString([]byte(secret))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("error signing token: %v", err)
|
return "", fmt.Errorf("error signing token: %v", err)
|
||||||
@@ -58,59 +70,73 @@ func (self *Token) GenerateRefreshToken() (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Issue both access and refresh token
|
// Issue both access and refresh token
|
||||||
func (self *Token) IssueTokens(userId uuid.UUID) (string, string, error) {
|
func (self *Token) IssueTokens(clientId string, userId uuid.UUID) (string, string, error) {
|
||||||
// Gen atk
|
// access token
|
||||||
access, err := self.GenerateAccessToken(userId)
|
access, err := self.GenerateAccessToken(clientId, userId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", "", err
|
return "", "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Gen rtk
|
// refresh token
|
||||||
refresh, err := self.GenerateRefreshToken()
|
refresh, err := self.GenerateRefreshToken()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", "", err
|
return "", "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store to redis
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
ttl := viper.GetDuration("ttl.refresh_ttl")
|
ttl := viper.GetDuration("ttl.refresh_ttl")
|
||||||
|
|
||||||
// refresh -> user
|
refreshKey := "refresh:" + refresh
|
||||||
if err := data.Redis.Set(
|
|
||||||
|
// refresh -> user + client
|
||||||
|
if err := data.Redis.HSet(
|
||||||
ctx,
|
ctx,
|
||||||
"refresh:"+refresh,
|
refreshKey,
|
||||||
userId.String(),
|
map[string]any{
|
||||||
ttl,
|
"user_id": userId.String(),
|
||||||
|
"client_id": clientId,
|
||||||
|
},
|
||||||
).Err(); err != nil {
|
).Err(); err != nil {
|
||||||
return "", "", err
|
return "", "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := data.Redis.Expire(ctx, refreshKey, ttl).Err(); err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
|
||||||
// user -> refresh tokens
|
// user -> refresh tokens
|
||||||
userSetKey := "user:" + userId.String() + ":refresh_tokens"
|
userSetKey := "user:" + userId.String() + ":refresh_tokens"
|
||||||
|
|
||||||
if err := data.Redis.SAdd(
|
if err := data.Redis.SAdd(ctx, userSetKey, refresh).Err(); err != nil {
|
||||||
ctx,
|
|
||||||
userSetKey,
|
|
||||||
refresh,
|
|
||||||
).Err(); err != nil {
|
|
||||||
return "", "", err
|
return "", "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
// set user ttl >= all refresh token
|
|
||||||
_ = data.Redis.Expire(ctx, userSetKey, ttl).Err()
|
_ = data.Redis.Expire(ctx, userSetKey, ttl).Err()
|
||||||
|
|
||||||
|
// client -> refresh tokens
|
||||||
|
clientSetKey := "client:" + clientId + ":refresh_tokens"
|
||||||
|
_ = data.Redis.SAdd(ctx, clientSetKey, refresh).Err()
|
||||||
|
_ = data.Redis.Expire(ctx, clientSetKey, ttl).Err()
|
||||||
|
|
||||||
return access, refresh, nil
|
return access, refresh, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Refresh access token
|
// Refresh access token
|
||||||
func (self *Token) RefreshAccessToken(refreshToken string) (string, error) {
|
func (self *Token) RefreshAccessToken(refreshToken string) (string, error) {
|
||||||
// Read rtk:userid from redis
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
key := "refresh:" + refreshToken
|
key := "refresh:" + refreshToken
|
||||||
|
|
||||||
userIdStr, err := data.Redis.Get(ctx, key).Result()
|
// read refresh token bind data
|
||||||
if err != nil {
|
dataMap, err := data.Redis.HGetAll(ctx, key).Result()
|
||||||
return "", err
|
if err != nil || len(dataMap) == 0 {
|
||||||
|
return "", errors.New("invalid refresh token")
|
||||||
|
}
|
||||||
|
|
||||||
|
userIdStr := dataMap["user_id"]
|
||||||
|
clientId := dataMap["client_id"]
|
||||||
|
|
||||||
|
if userIdStr == "" || clientId == "" {
|
||||||
|
return "", errors.New("refresh token corrupted")
|
||||||
}
|
}
|
||||||
|
|
||||||
userId, err := uuid.Parse(userIdStr)
|
userId, err := uuid.Parse(userIdStr)
|
||||||
@@ -118,75 +144,105 @@ func (self *Token) RefreshAccessToken(refreshToken string) (string, error) {
|
|||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate access token
|
// Generate new access token
|
||||||
return self.GenerateAccessToken(userId)
|
return self.GenerateAccessToken(clientId, userId)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (self *Token) RenewRefreshToken(refreshToken string) (string, error) {
|
func (self *Token) RenewRefreshToken(refreshToken string) (string, error) {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
ttl := viper.GetDuration("ttl.refresh_ttl")
|
ttl := viper.GetDuration("ttl.refresh_ttl")
|
||||||
|
|
||||||
key := "refresh:" + refreshToken
|
oldKey := "refresh:" + refreshToken
|
||||||
userIdStr, err := data.Redis.Get(ctx, key).Result()
|
|
||||||
|
// read old refresh token bind data
|
||||||
|
dataMap, err := data.Redis.HGetAll(ctx, oldKey).Result()
|
||||||
|
if err != nil || len(dataMap) == 0 {
|
||||||
|
return "", errors.New("invalid refresh token")
|
||||||
|
}
|
||||||
|
|
||||||
|
userIdStr := dataMap["user_id"]
|
||||||
|
clientId := dataMap["client_id"]
|
||||||
|
|
||||||
|
if userIdStr == "" || clientId == "" {
|
||||||
|
return "", errors.New("refresh token corrupted")
|
||||||
|
}
|
||||||
|
|
||||||
|
// generate new refresh token
|
||||||
|
newRefresh, err := self.GenerateRefreshToken()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
refresh, err := self.GenerateRefreshToken()
|
// revoke old refresh token
|
||||||
if err != nil {
|
if err := self.RevokeRefreshToken(refreshToken); err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = self.RevokeRefreshToken(refreshToken)
|
newKey := "refresh:" + newRefresh
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
// refresh -> user
|
// refresh -> user + client
|
||||||
if err := data.Redis.Set(
|
if err := data.Redis.HSet(
|
||||||
ctx,
|
ctx,
|
||||||
"refresh:"+refresh,
|
newKey,
|
||||||
userIdStr,
|
map[string]any{
|
||||||
ttl,
|
"user_id": userIdStr,
|
||||||
|
"client_id": clientId,
|
||||||
|
},
|
||||||
).Err(); err != nil {
|
).Err(); err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := data.Redis.Expire(ctx, newKey, ttl).Err(); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
// user -> refresh tokens
|
// user -> refresh tokens
|
||||||
userSetKey := "user:" + userIdStr + ":refresh_tokens"
|
userSetKey := "user:" + userIdStr + ":refresh_tokens"
|
||||||
|
if err := data.Redis.SAdd(ctx, userSetKey, newRefresh).Err(); err != nil {
|
||||||
if err := data.Redis.SAdd(
|
|
||||||
ctx,
|
|
||||||
userSetKey,
|
|
||||||
refresh,
|
|
||||||
).Err(); err != nil {
|
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
// set user ttl >= all refresh token
|
|
||||||
_ = data.Redis.Expire(ctx, userSetKey, ttl).Err()
|
_ = data.Redis.Expire(ctx, userSetKey, ttl).Err()
|
||||||
|
|
||||||
return refresh, nil
|
// client -> refresh tokens
|
||||||
|
clientSetKey := "client:" + clientId + ":refresh_tokens"
|
||||||
|
_ = data.Redis.SAdd(ctx, clientSetKey, newRefresh).Err()
|
||||||
|
_ = data.Redis.Expire(ctx, clientSetKey, ttl).Err()
|
||||||
|
|
||||||
|
return newRefresh, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (self *Token) RevokeRefreshToken(refreshToken string) error {
|
func (self *Token) RevokeRefreshToken(refreshToken string) error {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
key := "refresh:" + refreshToken
|
refreshKey := "refresh:" + refreshToken
|
||||||
|
|
||||||
userIDStr, err := data.Redis.Get(ctx, key).Result()
|
// read refresh token metadata (user_id, client_id)
|
||||||
if err != nil {
|
dataMap, err := data.Redis.HGetAll(ctx, refreshKey).Result()
|
||||||
|
if err != nil || len(dataMap) == 0 {
|
||||||
|
// Token already revoked or not found
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
userSetKey := "user:" + userIDStr + ":refresh_tokens"
|
userID := dataMap["user_id"]
|
||||||
|
clientID := dataMap["client_id"]
|
||||||
|
|
||||||
// Delete rtk from redis
|
// build index keys
|
||||||
|
userSetKey := "user:" + userID + ":refresh_tokens"
|
||||||
|
clientSetKey := "client:" + clientID + ":refresh_tokens"
|
||||||
|
|
||||||
|
// remove refresh token and all related indexes atomically
|
||||||
pipe := data.Redis.TxPipeline()
|
pipe := data.Redis.TxPipeline()
|
||||||
pipe.Del(ctx, key) // rtk:userid index
|
|
||||||
pipe.SRem(ctx, userSetKey, refreshToken) // userid:rtk index
|
|
||||||
_, err = pipe.Exec(ctx)
|
|
||||||
|
|
||||||
|
// remove main refresh token record
|
||||||
|
pipe.Del(ctx, refreshKey)
|
||||||
|
|
||||||
|
// remove refresh token from user's active refresh token set
|
||||||
|
pipe.SRem(ctx, userSetKey, refreshToken)
|
||||||
|
|
||||||
|
// remove refresh token from client's active refresh token set
|
||||||
|
pipe.SRem(ctx, clientSetKey, refreshToken)
|
||||||
|
|
||||||
|
_, err = pipe.Exec(ctx)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -195,7 +251,6 @@ func (self *Token) HeaderVerify(header string) (string, error) {
|
|||||||
return "", nil
|
return "", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
jwtSecret := []byte(viper.GetString("secrets.jwt_secret"))
|
|
||||||
// Split header to 2
|
// Split header to 2
|
||||||
parts := strings.SplitN(header, " ", 2)
|
parts := strings.SplitN(header, " ", 2)
|
||||||
if len(parts) != 2 || parts[0] != "Bearer" {
|
if len(parts) != 2 || parts[0] != "Bearer" {
|
||||||
@@ -210,7 +265,25 @@ func (self *Token) HeaderVerify(header string) (string, error) {
|
|||||||
tokenStr,
|
tokenStr,
|
||||||
claims,
|
claims,
|
||||||
func(token *jwt.Token) (any, error) {
|
func(token *jwt.Token) (any, error) {
|
||||||
return jwtSecret, nil
|
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||||
|
return nil, errors.New("unexpected signing method")
|
||||||
|
}
|
||||||
|
|
||||||
|
if claims.ClientId == "" {
|
||||||
|
return nil, errors.New("client_id missing in token")
|
||||||
|
}
|
||||||
|
|
||||||
|
clientData, err := new(data.Client).GetClientByClientId(claims.ClientId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error getting client data: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
secret, err := clientData.GetDecryptedSecret()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error getting decrypted secret: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return secret, nil
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ func Magic(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
code, err := authcode.NewAuthCode(req.Email)
|
code, err := authcode.NewAuthCode(req.ClientId, req.Email)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(500, gin.H{"status": "code gen failed"})
|
c.JSON(500, gin.H{"status": "code gen failed"})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ func Redirect(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
code, err := authcode.NewAuthCode(user.Email)
|
code, err := authcode.NewAuthCode(clientId, user.Email)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(500, gin.H{"status": "code gen failed"})
|
c.JSON(500, gin.H{"status": "code gen failed"})
|
||||||
return
|
return
|
||||||
@@ -109,7 +109,7 @@ func Redirect(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
newCode, err := authcode.NewAuthCode(email)
|
newCode, err := authcode.NewAuthCode(clientId, email)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(500, gin.H{"status": "internal server error"})
|
c.JSON(500, gin.H{"status": "internal server error"})
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -22,14 +22,14 @@ func Token(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
email, ok := authcode.VerifyAuthCode(req.Code)
|
authCode, ok := authcode.VerifyAuthCode(req.Code)
|
||||||
if !ok {
|
if !ok {
|
||||||
c.JSON(403, gin.H{"status": "invalid or expired token"})
|
c.JSON(403, gin.H{"status": "invalid or expired token"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
userData := new(data.User)
|
userData := new(data.User)
|
||||||
user, err := userData.GetByEmail(email)
|
user, err := userData.GetByEmail(authCode.Email)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(500, gin.H{"status": "internal server error"})
|
c.JSON(500, gin.H{"status": "internal server error"})
|
||||||
return
|
return
|
||||||
@@ -39,7 +39,7 @@ func Token(c *gin.Context) {
|
|||||||
JwtTool := authtoken.Token{
|
JwtTool := authtoken.Token{
|
||||||
Application: viper.GetString("server.application"),
|
Application: viper.GetString("server.application"),
|
||||||
}
|
}
|
||||||
accessToken, refreshToken, err := JwtTool.IssueTokens(user.UserId)
|
accessToken, refreshToken, err := JwtTool.IssueTokens(authCode.ClientId, user.UserId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(500, gin.H{"status": "error generating tokens"})
|
c.JSON(500, gin.H{"status": "error generating tokens"})
|
||||||
return
|
return
|
||||||
|
|||||||
Reference in New Issue
Block a user