Skip to content

Commit 63ef342

Browse files
committed
[jwt] implement tokens revocation
1 parent 6f1f9af commit 63ef342

File tree

12 files changed

+226
-80
lines changed

12 files changed

+226
-80
lines changed

api/requests.http

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -218,12 +218,10 @@ Content-Type: application/json
218218
}
219219

220220
###
221-
POST {{baseUrl}}/3rdparty/v1/auth/token/revoke HTTP/1.1
221+
DELETE {{baseUrl}}/3rdparty/v1/auth/token/w8pxz0a4Fwa4xgzyCvSeC HTTP/1.1
222222
Authorization: Basic {{credentials}}
223223
Content-Type: application/json
224224

225-
{}
226-
227225
###
228226
GET http://localhost:3000/metrics HTTP/1.1
229227

internal/sms-gateway/handlers/thirdparty/auth.go

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package thirdparty
22

33
import (
4+
"errors"
45
"time"
56

67
"github.com/android-sms-gateway/server/internal/sms-gateway/handlers/base"
@@ -33,6 +34,7 @@ func NewAuthHandler(
3334
}
3435

3536
func (h *AuthHandler) Register(router fiber.Router) {
37+
router.Use(h.errorHanlder)
3638
router.Post("/token", permissions.RequireScope(ScopeTokensManage), userauth.WithUser(h.postToken))
3739
router.Delete("/token/:jti", permissions.RequireScope(ScopeTokensManage), userauth.WithUser(h.deleteToken))
3840
}
@@ -71,7 +73,7 @@ func (h *AuthHandler) postToken(user users.User, c *fiber.Ctx) error {
7173
return err
7274
}
7375

74-
token, err := h.jwtSvc.GenerateToken(user.ID, req.Scopes, req.TTL)
76+
token, err := h.jwtSvc.GenerateToken(c.Context(), user.ID, req.Scopes, req.TTL)
7577
if err != nil {
7678
return err
7779
}
@@ -84,6 +86,43 @@ func (h *AuthHandler) postToken(user users.User, c *fiber.Ctx) error {
8486
})
8587
}
8688

89+
// @Summary Revoke token
90+
// @Description Revoke access token with specified jti
91+
// @Security ApiAuth
92+
// @Security JWTAuth
93+
// @Tags User, Auth
94+
// @Success 204 "No Content"
95+
// @Failure 401 {object} smsgateway.ErrorResponse "Unauthorized"
96+
// @Failure 403 {object} smsgateway.ErrorResponse "Forbidden"
97+
// @Failure 500 {object} smsgateway.ErrorResponse "Internal server error"
98+
// @Router /3rdparty/v1/auth/token/{jti} [delete]
99+
//
100+
// Revoke token.
87101
func (h *AuthHandler) deleteToken(user users.User, c *fiber.Ctx) error {
88-
return fiber.ErrNotImplemented
102+
jti := c.Params("jti")
103+
104+
if err := h.jwtSvc.RevokeToken(c.Context(), user.ID, jti); err != nil {
105+
return err
106+
}
107+
108+
return c.SendStatus(fiber.StatusNoContent)
109+
}
110+
111+
func (h *AuthHandler) errorHanlder(c *fiber.Ctx) error {
112+
err := c.Next()
113+
if err == nil {
114+
return nil
115+
}
116+
117+
switch {
118+
case errors.Is(err, jwt.ErrDisabled):
119+
return fiber.NewError(fiber.StatusNotImplemented, "token service disabled, contact your administrator")
120+
121+
case errors.Is(err, jwt.ErrInitFailed):
122+
fallthrough
123+
case errors.Is(err, jwt.ErrInvalidConfig):
124+
return fiber.NewError(fiber.StatusInternalServerError, "token service not configured, contact your administrator")
125+
}
126+
127+
return err
89128
}

internal/sms-gateway/jwt/disabled.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,16 +13,16 @@ func newDisabled() Service {
1313
}
1414

1515
// GenerateToken implements Service.
16-
func (d *disabled) GenerateToken(userID string, scopes []string, ttl time.Duration) (*TokenInfo, error) {
16+
func (d *disabled) GenerateToken(_ context.Context, _ string, _ []string, _ time.Duration) (*TokenInfo, error) {
1717
return nil, ErrDisabled
1818
}
1919

2020
// ParseToken implements Service.
21-
func (d *disabled) ParseToken(ctx context.Context, token string) (*Claims, error) {
21+
func (d *disabled) ParseToken(_ context.Context, _ string) (*Claims, error) {
2222
return nil, ErrDisabled
2323
}
2424

2525
// RevokeToken implements Service.
26-
func (d *disabled) RevokeToken(ctx context.Context, jti string) error {
26+
func (d *disabled) RevokeToken(_ context.Context, _, _ string) error {
2727
return ErrDisabled
2828
}

internal/sms-gateway/jwt/errors.go

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,9 @@ package jwt
33
import "errors"
44

55
var (
6-
ErrDisabled = errors.New("jwt disabled")
7-
ErrInitFailed = errors.New("failed to initialize jwt")
8-
ErrInvalidConfig = errors.New("invalid config")
9-
ErrInvalidToken = errors.New("invalid token")
10-
ErrTokenRevoked = errors.New("token revoked")
11-
ErrUnexpectedSigningMethod = errors.New("unexpected signing method")
6+
ErrDisabled = errors.New("jwt disabled")
7+
ErrInitFailed = errors.New("failed to initialize jwt")
8+
ErrInvalidConfig = errors.New("invalid config")
9+
ErrInvalidToken = errors.New("invalid token")
10+
ErrTokenRevoked = errors.New("token revoked")
1211
)

internal/sms-gateway/jwt/jwt.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@ import (
88
)
99

1010
type Service interface {
11-
GenerateToken(userID string, scopes []string, ttl time.Duration) (*TokenInfo, error)
11+
GenerateToken(ctx context.Context, userID string, scopes []string, ttl time.Duration) (*TokenInfo, error)
1212
ParseToken(ctx context.Context, token string) (*Claims, error)
13-
RevokeToken(ctx context.Context, jti string) error
13+
RevokeToken(ctx context.Context, userID, jti string) error
1414
}
1515

1616
type Claims struct {

internal/sms-gateway/jwt/models.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package jwt
2+
3+
import (
4+
"fmt"
5+
"time"
6+
7+
"github.com/android-sms-gateway/server/internal/sms-gateway/models"
8+
"gorm.io/gorm"
9+
)
10+
11+
type tokenModel struct {
12+
models.TimedModel
13+
14+
ID string `gorm:"primaryKey;type:char(21)"`
15+
UserID string `gorm:"not null;type:char(21);index:idx_tokens_user_id"`
16+
ExpiresAt time.Time `gorm:"not null"`
17+
RevokedAt *time.Time `gorm:"index:idx_tokens_revoked_at"`
18+
}
19+
20+
func (tokenModel) TableName() string {
21+
return "tokens"
22+
}
23+
24+
func newTokenModel(id, userID string, expiresAt time.Time) *tokenModel {
25+
return &tokenModel{
26+
ID: id,
27+
UserID: userID,
28+
ExpiresAt: expiresAt,
29+
}
30+
}
31+
32+
func Migrate(db *gorm.DB) error {
33+
if err := db.AutoMigrate(new(tokenModel)); err != nil {
34+
return fmt.Errorf("tokens migration failed: %w", err)
35+
}
36+
return nil
37+
}

internal/sms-gateway/jwt/module.go

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package jwt
22

33
import (
44
"github.com/android-sms-gateway/server/internal/sms-gateway/cache"
5+
"github.com/capcom6/go-infra-fx/db"
56
"github.com/go-core-fx/logger"
67
"go.uber.org/fx"
78
)
@@ -13,13 +14,17 @@ func Module() fx.Option {
1314
fx.Provide(func(factory cache.Factory) (cache.Cache, error) {
1415
return factory.New("jwt")
1516
}, fx.Private),
16-
fx.Provide(NewRevokedStorage, fx.Private),
17-
fx.Provide(func(config Config, revoked *RevokedStorage) (Service, error) {
17+
fx.Provide(NewRepository, fx.Private),
18+
fx.Provide(func(config Config, tokens *Repository) (Service, error) {
1819
if config.Secret == "" {
1920
return newDisabled(), nil
2021
}
2122

22-
return New(config, revoked)
23+
return New(config, tokens)
2324
}),
2425
)
2526
}
27+
28+
func init() {
29+
db.RegisterMigration(Migrate)
30+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package jwt
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
"gorm.io/gorm"
8+
)
9+
10+
type Repository struct {
11+
db *gorm.DB
12+
}
13+
14+
func NewRepository(db *gorm.DB) *Repository {
15+
return &Repository{
16+
db: db,
17+
}
18+
}
19+
20+
func (r *Repository) Insert(ctx context.Context, token *tokenModel) error {
21+
if err := r.db.WithContext(ctx).Create(token).Error; err != nil {
22+
return fmt.Errorf("can't create token: %w", err)
23+
}
24+
25+
return nil
26+
}
27+
28+
func (r *Repository) Revoke(ctx context.Context, jti, userID string) error {
29+
if err := r.db.WithContext(ctx).Model((*tokenModel)(nil)).
30+
Where("id = ? and user_id = ? and revoked_at is null", jti, userID).
31+
Update("revoked_at", gorm.Expr("NOW()")).Error; err != nil {
32+
return fmt.Errorf("can't revoke token: %w", err)
33+
}
34+
35+
return nil
36+
}
37+
38+
func (r *Repository) IsRevoked(ctx context.Context, jti string) (bool, error) {
39+
var count int64
40+
if err := r.db.WithContext(ctx).Model((*tokenModel)(nil)).
41+
Where("id = ? and revoked_at is not null", jti).
42+
Count(&count).Error; err != nil {
43+
return false, fmt.Errorf("can't check if token is revoked: %w", err)
44+
}
45+
46+
return count > 0, nil
47+
}

internal/sms-gateway/jwt/revoked.go

Lines changed: 0 additions & 36 deletions
This file was deleted.

internal/sms-gateway/jwt/service.go

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,17 @@ const jtiLength = 21
1414
type service struct {
1515
config Config
1616

17-
revoked *RevokedStorage
17+
tokens *Repository
1818

1919
idFactory func() string
2020
}
2121

22-
func New(config Config, revoked *RevokedStorage) (Service, error) {
22+
func New(config Config, tokens *Repository) (Service, error) {
2323
if err := config.Validate(); err != nil {
2424
return nil, err
2525
}
2626

27-
if revoked == nil {
27+
if tokens == nil {
2828
return nil, fmt.Errorf("%w: revoked storage is required", ErrInitFailed)
2929
}
3030

@@ -36,13 +36,13 @@ func New(config Config, revoked *RevokedStorage) (Service, error) {
3636
return &service{
3737
config: config,
3838

39-
revoked: revoked,
39+
tokens: tokens,
4040

4141
idFactory: idFactory,
4242
}, nil
4343
}
4444

45-
func (s *service) GenerateToken(userID string, scopes []string, ttl time.Duration) (*TokenInfo, error) {
45+
func (s *service) GenerateToken(ctx context.Context, userID string, scopes []string, ttl time.Duration) (*TokenInfo, error) {
4646
if userID == "" {
4747
return nil, fmt.Errorf("%w: user id is required", ErrInvalidConfig)
4848
}
@@ -78,6 +78,10 @@ func (s *service) GenerateToken(userID string, scopes []string, ttl time.Duratio
7878
return nil, fmt.Errorf("failed to sign token: %w", err)
7979
}
8080

81+
if err := s.tokens.Insert(ctx, newTokenModel(claims.ID, claims.UserID, claims.ExpiresAt.Time)); err != nil {
82+
return nil, fmt.Errorf("failed to insert token: %w", err)
83+
}
84+
8185
return &TokenInfo{ID: claims.ID, AccessToken: signedToken, ExpiresAt: claims.ExpiresAt.Time}, nil
8286
}
8387

@@ -102,9 +106,9 @@ func (s *service) ParseToken(ctx context.Context, token string) (*Claims, error)
102106
return nil, ErrInvalidToken
103107
}
104108

105-
revoked, err := s.revoked.IsRevoked(ctx, claims.ID)
109+
revoked, err := s.tokens.IsRevoked(ctx, claims.ID)
106110
if err != nil {
107-
return nil, fmt.Errorf("failed to check if token is revoked: %w", err)
111+
return nil, err
108112
}
109113
if revoked {
110114
return nil, ErrTokenRevoked
@@ -113,6 +117,6 @@ func (s *service) ParseToken(ctx context.Context, token string) (*Claims, error)
113117
return claims, nil
114118
}
115119

116-
func (s *service) RevokeToken(ctx context.Context, jti string) error {
117-
return s.revoked.Revoke(ctx, jti, s.config.TTL)
120+
func (s *service) RevokeToken(ctx context.Context, userID, jti string) error {
121+
return s.tokens.Revoke(ctx, jti, userID)
118122
}

0 commit comments

Comments
 (0)