Skip to content

Commit c0b19ad

Browse files
authored
Verify token claims prior to expiration (#124)
1 parent f3ea581 commit c0b19ad

File tree

4 files changed

+96
-39
lines changed

4 files changed

+96
-39
lines changed

internal/middleware/auth/auth.go

Lines changed: 41 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -64,16 +64,25 @@ func NewAuthMiddleware(cfg *config.Config, userRepo uRepo.IUserRepo) (*jwt.GinJW
6464
IdentityHandler: func(c *gin.Context) interface{} {
6565
claims := jwt.ExtractClaims(c)
6666

67-
id, ok := claims[auth.IdentityKey].(string)
67+
userIDRaw, ok := claims[auth.IdentityKey].(string)
6868
if !ok {
6969
logging.FromContext(c).Errorw("failed to extract ID from claims")
7070
return nil
7171
}
72-
userID, err := strconv.Atoi(id)
72+
userID, err := strconv.Atoi(userIDRaw)
7373
if err != nil {
7474
return nil
7575
}
7676

77+
var tokenID = 0
78+
appTokenIDRaw, ok := claims[auth.AppTokenKey].(string)
79+
if ok {
80+
tokenID, err = strconv.Atoi(appTokenIDRaw)
81+
if err != nil {
82+
tokenID = 0
83+
}
84+
}
85+
7786
scopesRaw, ok := claims["scopes"].([]interface{})
7887
if !ok {
7988
return nil
@@ -87,13 +96,39 @@ func NewAuthMiddleware(cfg *config.Config, userRepo uRepo.IUserRepo) (*jwt.GinJW
8796
}
8897

8998
return &models.SignedInIdentity{
90-
UserID: userID,
91-
Type: models.IdentityTypeUser,
92-
Scopes: scopes,
99+
UserID: userID,
100+
TokenID: tokenID,
101+
Type: models.IdentityType(claims["type"].(string)),
102+
Scopes: scopes,
93103
}
94104
},
95105
Authorizator: func(data interface{}, c *gin.Context) bool {
96-
if _, ok := data.(*models.SignedInIdentity); ok {
106+
if identity, ok := data.(*models.SignedInIdentity); ok {
107+
if identity.Type == models.IdentityTypeUser {
108+
// Check that the user still exists
109+
_, err := userRepo.GetUser(c.Request.Context(), identity.UserID)
110+
if err != nil {
111+
logging.FromContext(c).Errorw("failed to find user", "err", err)
112+
return false
113+
}
114+
} else if identity.Type == models.IdentityTypeApp {
115+
// An app token id must be present
116+
if identity.TokenID == 0 {
117+
logging.FromContext(c).Errorw("app token ID is nil")
118+
return false
119+
}
120+
121+
// Check that the app token still exists
122+
_, err := userRepo.GetAppTokenByID(c.Request.Context(), identity.TokenID)
123+
if err != nil {
124+
logging.FromContext(c).Errorw("failed to find app token", "err", err)
125+
return false
126+
}
127+
} else {
128+
logging.FromContext(c).Errorw("unknown identity type", "type", identity.Type)
129+
return false
130+
}
131+
97132
return true
98133
}
99134
return false

internal/models/user.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,10 @@ const (
3030
)
3131

3232
type SignedInIdentity struct {
33-
UserID int
34-
Type IdentityType
35-
Scopes []ApiTokenScope
33+
UserID int
34+
TokenID int
35+
Type IdentityType
36+
Scopes []ApiTokenScope
3637
}
3738

3839
type UserPasswordReset struct {

internal/repos/user/user.go

Lines changed: 49 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ type IUserRepo interface {
2121
ActivateAccount(c context.Context, email string, code string) (bool, error)
2222
UpdatePasswordByToken(ctx context.Context, email string, token string, password string) error
2323
CreateAppToken(c context.Context, userID int, name string, scopes []models.ApiTokenScope, days int) (*models.AppToken, error)
24+
GetAppTokenByID(c context.Context, tokenId int) (*models.AppToken, error)
2425
GetAllUserTokens(c context.Context, userID int) ([]*models.AppToken, error)
2526
DeleteAppToken(c context.Context, userID int, tokenID string) error
2627
UpdateNotificationSettings(c context.Context, userID int, provider models.NotificationProvider, triggers models.NotificationTriggerOptions) error
@@ -160,43 +161,62 @@ func convertScopesToStringArray(scopes []models.ApiTokenScope) []string {
160161
}
161162

162163
func (r *UserRepository) CreateAppToken(c context.Context, userID int, name string, scopes []models.ApiTokenScope, days int) (*models.AppToken, error) {
163-
duration := time.Duration(days) * 24 * time.Hour
164-
expiresAt := time.Now().UTC().Add(duration)
165-
jwtToken := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
166-
auth.IdentityKey: fmt.Sprintf("%d", userID),
167-
"exp": expiresAt.Unix(),
168-
"type": "app",
169-
"scopes": scopes,
170-
})
164+
var token *models.AppToken
165+
err := r.db.WithContext(c).Transaction(func(tx *gorm.DB) error {
166+
var nextID int
167+
if err := tx.Raw("SELECT COALESCE(MAX(id), 0)+1 AS next_id FROM app_tokens").Scan(&nextID).Error; err != nil {
168+
return fmt.Errorf("failed to get next token id: %w", err)
169+
}
171170

172-
signedToken, err := jwtToken.SignedString([]byte(r.cfg.Jwt.Secret))
173-
if err != nil {
174-
return nil, fmt.Errorf("failed to sign token: %s", err.Error())
175-
}
171+
for _, scope := range scopes {
172+
if scope == models.ApiTokenScopeUserRead || scope == models.ApiTokenScopeUserWrite {
173+
return fmt.Errorf("user scopes are not allowed")
174+
}
176175

177-
for _, scope := range scopes {
178-
if scope == models.ApiTokenScopeUserRead || scope == models.ApiTokenScopeUserWrite {
179-
return nil, fmt.Errorf("user scopes are not allowed")
176+
if scope == models.ApiTokenScopeTokenWrite {
177+
return fmt.Errorf("token scopes are not allowed")
178+
}
180179
}
181180

182-
if scope == models.ApiTokenScopeTokenWrite {
183-
return nil, fmt.Errorf("token scopes are not allowed")
181+
duration := time.Duration(days) * 24 * time.Hour
182+
expiresAt := time.Now().UTC().Add(duration)
183+
jwtToken := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
184+
auth.AppTokenKey: fmt.Sprintf("%d", nextID),
185+
auth.IdentityKey: fmt.Sprintf("%d", userID),
186+
"exp": expiresAt.Unix(),
187+
"type": "app",
188+
"scopes": scopes,
189+
})
190+
191+
signedToken, err := jwtToken.SignedString([]byte(r.cfg.Jwt.Secret))
192+
if err != nil {
193+
return fmt.Errorf("failed to sign token: %s", err.Error())
184194
}
185-
}
186195

187-
token := &models.AppToken{
188-
UserID: userID,
189-
Name: name,
190-
Token: signedToken,
191-
ExpiresAt: expiresAt,
192-
Scopes: convertScopesToStringArray(scopes),
193-
}
196+
token = &models.AppToken{
197+
ID: nextID,
198+
UserID: userID,
199+
Name: name,
200+
Token: signedToken,
201+
ExpiresAt: expiresAt,
202+
Scopes: convertScopesToStringArray(scopes),
203+
}
194204

195-
if err := r.db.WithContext(c).Create(token).Error; err != nil {
196-
return nil, fmt.Errorf("failed to save token: %s", err.Error())
197-
}
205+
if err := tx.Create(token).Error; err != nil {
206+
return fmt.Errorf("failed to save token: %s", err.Error())
207+
}
208+
return nil
209+
})
198210

199-
return token, nil
211+
return token, err
212+
}
213+
214+
func (r *UserRepository) GetAppTokenByID(c context.Context, tokenId int) (*models.AppToken, error) {
215+
var token models.AppToken
216+
if err := r.db.WithContext(c).Where("id = ?", tokenId).First(&token).Error; err != nil {
217+
return nil, err
218+
}
219+
return &token, nil
200220
}
201221

202222
func (r *UserRepository) GetAllUserTokens(c context.Context, userID int) ([]*models.AppToken, error) {

internal/utils/auth/auth.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ import (
1111
"golang.org/x/crypto/bcrypt"
1212
)
1313

14-
var IdentityKey = "id"
14+
var IdentityKey = "user_id"
15+
var AppTokenKey = "token_id"
1516

1617
func EncodePassword(password string) (string, error) {
1718
bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)

0 commit comments

Comments
 (0)