Skip to content

Commit 8ecc34d

Browse files
committed
[jwt] implement token issuing
1 parent 9f3e7f7 commit 8ecc34d

File tree

6 files changed

+91
-26
lines changed

6 files changed

+91
-26
lines changed

api/requests.http

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,19 @@ POST {{baseUrl}}/3rdparty/v1/auth/token HTTP/1.1
197197
Authorization: Basic {{credentials}}
198198
Content-Type: application/json
199199

200-
{}
200+
{
201+
"scopes": [
202+
"messages:read",
203+
"messages:send",
204+
"devices:read",
205+
"devices:write",
206+
"webhooks:read",
207+
"webhooks:write",
208+
"settings:read",
209+
"settings:write",
210+
"logs:read",
211+
]
212+
}
201213

202214
###
203215
POST {{baseUrl}}/3rdparty/v1/auth/token/revoke HTTP/1.1

configs/config.example.yml

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,15 +38,19 @@ cache: # cache config
3838
url: memory:// # cache url (memory:// or redis://) [CACHE__URL]
3939
pubsub: # pubsub config
4040
url: memory:// # pubsub url (memory:// or redis://) [PUBSUB__URL]
41+
jwt:
42+
secret: # jwt secret [JWT__SECRET]
43+
ttl: 24h # jwt ttl [JWT__TTL]
44+
issuer: # jwt issuer [JWT__ISSUER]
4145

4246
## Worker Config ##
4347

4448
tasks: # tasks config
4549
messages_hashing:
46-
interval: 168h # task execution interval in hours [TASKS__MESSAGES_HASHING__INTERVAL]
50+
interval: 168h # task execution interval [TASKS__MESSAGES_HASHING__INTERVAL]
4751
messages_cleanup:
48-
interval: 24h # task execution interval in hours [TASKS__MESSAGES_CLEANUP__INTERVAL]
49-
max_age: 720h # messages max age in hours [TASKS__MESSAGES_CLEANUP__MAX_AGE]
52+
interval: 24h # task execution interval [TASKS__MESSAGES_CLEANUP__INTERVAL]
53+
max_age: 720h # messages max age [TASKS__MESSAGES_CLEANUP__MAX_AGE]
5054
devices_cleanup:
51-
interval: 24h # task execution interval in hours [TASKS__DEVICES_CLEANUP__INTERVAL]
52-
max_age: 8760h # inactive devices max age in hours [TASKS__DEVICES_CLEANUP__MAX_AGE]
55+
interval: 24h # task execution interval [TASKS__DEVICES_CLEANUP__INTERVAL]
56+
max_age: 8760h # inactive devices max age [TASKS__DEVICES_CLEANUP__MAX_AGE]
Lines changed: 43 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,72 @@
11
package thirdparty
22

33
import (
4+
"time"
5+
46
"github.com/android-sms-gateway/server/internal/sms-gateway/handlers/base"
7+
"github.com/android-sms-gateway/server/internal/sms-gateway/handlers/middlewares/userauth"
8+
"github.com/android-sms-gateway/server/internal/sms-gateway/jwt"
9+
"github.com/android-sms-gateway/server/internal/sms-gateway/models"
510
"github.com/go-playground/validator/v10"
611
"github.com/gofiber/fiber/v2"
712
"go.uber.org/zap"
813
)
914

1015
type AuthHandler struct {
1116
base.Handler
17+
18+
jwtSvc jwt.Service
1219
}
1320

1421
func NewAuthHandler(
22+
jwtSvc jwt.Service,
23+
1524
logger *zap.Logger,
1625
validator *validator.Validate,
1726
) *AuthHandler {
1827
return &AuthHandler{
1928
Handler: base.Handler{Logger: logger, Validator: validator},
29+
30+
jwtSvc: jwtSvc,
2031
}
2132
}
2233

2334
func (h *AuthHandler) Register(router fiber.Router) {
24-
router.Post("/token", h.token)
25-
router.Post("/token/revoke", h.tokenRevoke)
35+
router.Post("/token", userauth.WithUser(h.postToken))
36+
router.Delete("/token/:jti", userauth.WithUser(h.deleteToken))
2637
}
2738

28-
func (h *AuthHandler) token(c *fiber.Ctx) error {
29-
return fiber.ErrNotImplemented
39+
type tokenRequest struct {
40+
TTL time.Duration `json:"ttl,omitempty"`
41+
Scopes []string `json:"scopes" validate:"required,min=1,dive,required"`
42+
}
43+
44+
type tokenResponse struct {
45+
ID string `json:"id"`
46+
TokenType string `json:"token_type"`
47+
AccessToken string `json:"access_token"`
48+
ExpiresAt time.Time `json:"expires_at"`
49+
}
50+
51+
func (h *AuthHandler) postToken(user models.User, c *fiber.Ctx) error {
52+
req := new(tokenRequest)
53+
if err := h.BodyParserValidator(c, req); err != nil {
54+
return err
55+
}
56+
57+
token, err := h.jwtSvc.GenerateToken(user.ID, req.Scopes, req.TTL)
58+
if err != nil {
59+
return err
60+
}
61+
62+
return c.Status(fiber.StatusCreated).JSON(tokenResponse{
63+
ID: token.ID,
64+
TokenType: "Bearer",
65+
AccessToken: token.AccessToken,
66+
ExpiresAt: token.ExpiresAt,
67+
})
3068
}
3169

32-
func (h *AuthHandler) tokenRevoke(c *fiber.Ctx) error {
70+
func (h *AuthHandler) deleteToken(user models.User, c *fiber.Ctx) error {
3371
return fiber.ErrNotImplemented
3472
}

internal/sms-gateway/jwt/disabled.go

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

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

2020
// ParseToken implements Service.

internal/sms-gateway/jwt/jwt.go

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,27 @@
11
package jwt
22

3-
import "github.com/golang-jwt/jwt/v5"
3+
import (
4+
"context"
5+
"time"
6+
7+
"github.com/golang-jwt/jwt/v5"
8+
)
9+
10+
type Service interface {
11+
GenerateToken(userID string, scopes []string, ttl time.Duration) (*TokenInfo, error)
12+
ParseToken(ctx context.Context, token string) (*Claims, error)
13+
RevokeToken(ctx context.Context, jti string) error
14+
}
415

516
type Claims struct {
617
jwt.RegisteredClaims
718

819
UserID string `json:"user_id"`
920
Scopes []string `json:"scopes"`
1021
}
22+
23+
type TokenInfo struct {
24+
ID string
25+
AccessToken string
26+
ExpiresAt time.Time
27+
}

internal/sms-gateway/jwt/service.go

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,6 @@ import (
1111

1212
const jtiLength = 21
1313

14-
type Service interface {
15-
GenerateToken(userID string, scopes []string, ttl time.Duration) (string, error)
16-
ParseToken(ctx context.Context, token string) (*Claims, error)
17-
RevokeToken(ctx context.Context, jti string) error
18-
}
19-
2014
type service struct {
2115
config Config
2216

@@ -48,17 +42,17 @@ func New(config Config, revoked *RevokedStorage) (Service, error) {
4842
}, nil
4943
}
5044

51-
func (s *service) GenerateToken(userID string, scopes []string, ttl time.Duration) (string, error) {
45+
func (s *service) GenerateToken(userID string, scopes []string, ttl time.Duration) (*TokenInfo, error) {
5246
if userID == "" {
53-
return "", fmt.Errorf("%w: user id is required", ErrInvalidConfig)
47+
return nil, fmt.Errorf("%w: user id is required", ErrInvalidConfig)
5448
}
5549

5650
if len(scopes) == 0 {
57-
return "", fmt.Errorf("%w: scopes are required", ErrInvalidConfig)
51+
return nil, fmt.Errorf("%w: scopes are required", ErrInvalidConfig)
5852
}
5953

6054
if ttl < 0 {
61-
return "", fmt.Errorf("%w: ttl must be non-negative", ErrInvalidConfig)
55+
return nil, fmt.Errorf("%w: ttl must be non-negative", ErrInvalidConfig)
6256
}
6357

6458
if ttl == 0 {
@@ -81,10 +75,10 @@ func (s *service) GenerateToken(userID string, scopes []string, ttl time.Duratio
8175
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
8276
signedToken, err := token.SignedString([]byte(s.config.Secret))
8377
if err != nil {
84-
return "", fmt.Errorf("failed to sign token: %w", err)
78+
return nil, fmt.Errorf("failed to sign token: %w", err)
8579
}
8680

87-
return signedToken, nil
81+
return &TokenInfo{ID: claims.ID, AccessToken: signedToken, ExpiresAt: claims.ExpiresAt.Time}, nil
8882
}
8983

9084
func (s *service) ParseToken(ctx context.Context, token string) (*Claims, error) {

0 commit comments

Comments
 (0)