feat: implement precise token logout
This commit is contained in:
@@ -147,6 +147,11 @@ func (ta *authenticator) Authenticate(ctx auth.AuthContext, token []byte) (*auth
|
||||
|
||||
features := auth.Feature(tl.Features)
|
||||
if features&auth.FeatureNoLogin == 0 {
|
||||
if revoked, enabled, err := store.IsLoginTokenRevoked(types.TenantID(tl.TenantID), token); err != nil {
|
||||
return nil, nil, err
|
||||
} else if enabled && revoked {
|
||||
return nil, nil, types.ErrFailed
|
||||
}
|
||||
if store.Security == nil {
|
||||
return nil, nil, types.ErrInternal
|
||||
}
|
||||
|
||||
+31
-4
@@ -6677,14 +6677,19 @@ func authenticateIMHTTPBearer(wrt http.ResponseWriter, req *http.Request) (*imPr
|
||||
}
|
||||
|
||||
func resolveIMHTTPBearer(req *http.Request) (*imPrincipal, error) {
|
||||
principal, _, _, err := resolveIMHTTPBearerToken(req)
|
||||
return principal, err
|
||||
}
|
||||
|
||||
func resolveIMHTTPBearerToken(req *http.Request) (*imPrincipal, []byte, time.Duration, error) {
|
||||
authz := strings.TrimSpace(req.Header.Get("Authorization"))
|
||||
token, ok := strings.CutPrefix(authz, "Bearer ")
|
||||
if !ok || strings.TrimSpace(token) == "" || store.Store == nil {
|
||||
return nil, types.ErrPermissionDenied
|
||||
return nil, nil, 0, types.ErrPermissionDenied
|
||||
}
|
||||
handler := store.Store.GetLogicalAuthHandler("token")
|
||||
if handler == nil || !handler.IsInitialized() {
|
||||
return nil, types.ErrUnsupported
|
||||
return nil, nil, 0, types.ErrUnsupported
|
||||
}
|
||||
secret := []byte(strings.TrimSpace(token))
|
||||
if decoded, err := base64.StdEncoding.DecodeString(strings.TrimSpace(token)); err == nil {
|
||||
@@ -6692,9 +6697,31 @@ func resolveIMHTTPBearer(req *http.Request) (*imPrincipal, error) {
|
||||
}
|
||||
rec, _, err := handler.Authenticate(auth.AuthContext{}, secret)
|
||||
if err != nil || rec == nil || rec.TenantID.IsZero() || rec.Uid.IsZero() {
|
||||
return nil, types.ErrPermissionDenied
|
||||
return nil, nil, 0, types.ErrPermissionDenied
|
||||
}
|
||||
return &imPrincipal{TenantID: rec.TenantID, Uid: rec.Uid}, nil
|
||||
return &imPrincipal{TenantID: rec.TenantID, Uid: rec.Uid}, secret, time.Duration(rec.Lifetime), nil
|
||||
}
|
||||
|
||||
func serveIMAuthLogout(wrt http.ResponseWriter, req *http.Request) {
|
||||
if req.Method != http.MethodPost {
|
||||
writeIMError(wrt, req, http.StatusMethodNotAllowed, 30000, imReasonMethodNotAllowed, "method not allowed", nil)
|
||||
return
|
||||
}
|
||||
principal, token, ttl, err := resolveIMHTTPBearerToken(req)
|
||||
if err != nil {
|
||||
writeIMError(wrt, req, http.StatusUnauthorized, 30043, imReasonAuthRequired, "authentication required", nil)
|
||||
return
|
||||
}
|
||||
if ttl < time.Second {
|
||||
ttl = time.Second
|
||||
}
|
||||
enabled, err := store.RevokeLoginToken(principal.TenantID, token, ttl)
|
||||
if err != nil || !enabled {
|
||||
logs.Warn.Println("im logout: token revocation failed", principal.TenantID, principal.Uid, err)
|
||||
writeIMError(wrt, req, http.StatusInternalServerError, 90000, imReasonInternal, "internal error", nil)
|
||||
return
|
||||
}
|
||||
writeIMSuccess(wrt, req, map[string]any{"status": "ok"})
|
||||
}
|
||||
|
||||
func currentUserPhone(tenantID types.TenantID, uid types.Uid) (string, error) {
|
||||
|
||||
@@ -861,6 +861,7 @@ func main() {
|
||||
mux.HandleFunc(config.ApiPath+"v1/im/auth/verification-codes/", serveIMVerificationCodeVerify)
|
||||
mux.HandleFunc(config.ApiPath+"v1/im/auth/password-reset/challenges/", serveIMPasswordResetVerify)
|
||||
mux.HandleFunc(config.ApiPath+"v1/im/auth/password-reset/confirm", serveIMPasswordResetConfirm)
|
||||
mux.HandleFunc(config.ApiPath+"v1/im/auth/logout", serveIMAuthLogout)
|
||||
mux.HandleFunc(config.ApiPath+"v1/im/security/urls/check", serveIMSecurityURLCheck)
|
||||
mux.HandleFunc(config.ApiPath+"v1/im/security/risk-challenges/", serveIMRiskChallengeConfirm)
|
||||
mux.HandleFunc(config.ApiPath+"v1/im/security/challenges", serveIMSecurityChallenges)
|
||||
|
||||
@@ -2,6 +2,8 @@ package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sort"
|
||||
@@ -78,6 +80,7 @@ type groupMessageRuntime interface {
|
||||
AllowRate(tenantID types.TenantID, scope string, limit int, window time.Duration) (bool, int, time.Duration, error)
|
||||
ClaimShortState(namespace, id string, ttl time.Duration) (bool, error)
|
||||
ClaimShortStateValue(namespace, id, value string, ttl time.Duration) (bool, bool, error)
|
||||
ShortStateExists(namespace, id string) (bool, error)
|
||||
}
|
||||
|
||||
var groupMsgRuntime groupMessageRuntime
|
||||
@@ -146,6 +149,26 @@ func ClaimShortStateValue(namespace, id, value string, ttl time.Duration) (bool,
|
||||
return claimed, sameValue, true, err
|
||||
}
|
||||
|
||||
func RevokeLoginToken(tenantID types.TenantID, token []byte, ttl time.Duration) (bool, error) {
|
||||
if groupMsgRuntime == nil || !tenantID.IsValid() || len(token) == 0 || ttl <= 0 {
|
||||
return false, nil
|
||||
}
|
||||
digest := sha256.Sum256(token)
|
||||
id := strconv.FormatInt(int64(tenantID), 10) + ":" + hex.EncodeToString(digest[:])
|
||||
_, err := groupMsgRuntime.ClaimShortState("revoked_login_token", id, ttl)
|
||||
return true, err
|
||||
}
|
||||
|
||||
func IsLoginTokenRevoked(tenantID types.TenantID, token []byte) (bool, bool, error) {
|
||||
if groupMsgRuntime == nil || !tenantID.IsValid() || len(token) == 0 {
|
||||
return false, false, nil
|
||||
}
|
||||
digest := sha256.Sum256(token)
|
||||
id := strconv.FormatInt(int64(tenantID), 10) + ":" + hex.EncodeToString(digest[:])
|
||||
revoked, err := groupMsgRuntime.ShortStateExists("revoked_login_token", id)
|
||||
return revoked, true, err
|
||||
}
|
||||
|
||||
func RefreshUserPresence(tenantID types.TenantID, uid types.Uid, sid, nodeID string) error {
|
||||
if groupMsgRuntime == nil || !tenantID.IsValid() || uid.IsZero() || sid == "" {
|
||||
return nil
|
||||
@@ -881,6 +904,11 @@ func (r *redisGroupMessageRuntime) ClaimShortStateValue(namespace, id, value str
|
||||
return false, existing == value, nil
|
||||
}
|
||||
|
||||
func (r *redisGroupMessageRuntime) ShortStateExists(namespace, id string) (bool, error) {
|
||||
count, err := r.client.Exists(context.Background(), r.shortStateKey(namespace, id)).Result()
|
||||
return count > 0, err
|
||||
}
|
||||
|
||||
func (r *redisGroupMessageRuntime) hotSeqIDs(tenantID types.TenantID, topic string, opts *types.QueryOpt, limit int) ([]int64, bool, error) {
|
||||
if opts != nil && len(opts.IdRanges) > 0 {
|
||||
seqs := expandRanges(opts.IdRanges, limit)
|
||||
|
||||
@@ -11,13 +11,15 @@ import (
|
||||
type testGroupMessageRuntime struct {
|
||||
claims map[string]*groupMessageUniqueRecord
|
||||
seqs map[string]int64
|
||||
shortStates map[string]bool
|
||||
deletedPrefixes []string
|
||||
}
|
||||
|
||||
func newTestGroupMessageRuntime() *testGroupMessageRuntime {
|
||||
return &testGroupMessageRuntime{
|
||||
claims: make(map[string]*groupMessageUniqueRecord),
|
||||
seqs: make(map[string]int64),
|
||||
claims: make(map[string]*groupMessageUniqueRecord),
|
||||
seqs: make(map[string]int64),
|
||||
shortStates: make(map[string]bool),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -138,7 +140,12 @@ func (r *testGroupMessageRuntime) AllowRate(types.TenantID, string, int, time.Du
|
||||
return true, 0, 0, nil
|
||||
}
|
||||
|
||||
func (r *testGroupMessageRuntime) ClaimShortState(string, string, time.Duration) (bool, error) {
|
||||
func (r *testGroupMessageRuntime) ClaimShortState(namespace, id string, _ time.Duration) (bool, error) {
|
||||
key := namespace + ":" + id
|
||||
if r.shortStates[key] {
|
||||
return false, nil
|
||||
}
|
||||
r.shortStates[key] = true
|
||||
return true, nil
|
||||
}
|
||||
|
||||
@@ -146,6 +153,30 @@ func (r *testGroupMessageRuntime) ClaimShortStateValue(string, string, string, t
|
||||
return true, true, nil
|
||||
}
|
||||
|
||||
func (r *testGroupMessageRuntime) ShortStateExists(namespace, id string) (bool, error) {
|
||||
return r.shortStates[namespace+":"+id], nil
|
||||
}
|
||||
|
||||
func TestLoginTokenRevocation(t *testing.T) {
|
||||
oldRuntime := groupMsgRuntime
|
||||
groupMsgRuntime = newTestGroupMessageRuntime()
|
||||
t.Cleanup(func() { groupMsgRuntime = oldRuntime })
|
||||
|
||||
token := []byte("current-device-token")
|
||||
if revoked, enabled, err := IsLoginTokenRevoked(7, token); err != nil || !enabled || revoked {
|
||||
t.Fatalf("unexpected initial state: revoked=%v enabled=%v err=%v", revoked, enabled, err)
|
||||
}
|
||||
if enabled, err := RevokeLoginToken(7, token, time.Hour); err != nil || !enabled {
|
||||
t.Fatalf("revoke token: enabled=%v err=%v", enabled, err)
|
||||
}
|
||||
if revoked, enabled, err := IsLoginTokenRevoked(7, token); err != nil || !enabled || !revoked {
|
||||
t.Fatalf("revoked token was accepted: revoked=%v enabled=%v err=%v", revoked, enabled, err)
|
||||
}
|
||||
if revoked, _, err := IsLoginTokenRevoked(7, []byte("other-device-token")); err != nil || revoked {
|
||||
t.Fatalf("other device token was revoked: revoked=%v err=%v", revoked, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMessageRuntimeSeqAndClientMidIdempotency(t *testing.T) {
|
||||
oldRuntime := groupMsgRuntime
|
||||
runtime := newTestGroupMessageRuntime()
|
||||
|
||||
Reference in New Issue
Block a user