fix(apiserver): revoke the token on logout (bug-1946)
Cause: Logout only deleted tokens/<user>/token, a file that token validation (ValidateHandler / GetUsernameFromToken) never reads, so a logged-out token stayed valid until its expiry (24 h by default). Fix: an in-memory revocation set (controllers/token_revocation.go) keyed by the token's jti (generateJWT now sets a random uuid jti, so two logons in the same second give different tokens) or, for tokens without a jti, by SHA-256 of the token; each entry is kept until the token's expiry and expired entries are purged on every revocation. Logout revokes the token; the shared ParseTokenClaims (used by ValidateHandler, RequirePermission and handlers via GetUsernameFromToken) rejects revoked tokens. A missing token file no longer fails logout. Scope: per process, not persisted and not shared between apiservers. Tests: TestLogout_RevokesToken (middleware), TestTokenRevocations_*, TestIntegration_PostLogoutTokenRejected (replaces the design-gap test). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
19a1a6f023
commit
4ce4f0b798
6 changed files with 256 additions and 34 deletions
|
|
@ -16,6 +16,7 @@ import (
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/golang-jwt/jwt/v4"
|
"github.com/golang-jwt/jwt/v4"
|
||||||
|
"github.com/google/uuid"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -48,6 +49,8 @@ func generateJWT(username string, IP string) (string, error) {
|
||||||
Username: username,
|
Username: username,
|
||||||
IP: IP,
|
IP: IP,
|
||||||
RegisteredClaims: jwt.RegisteredClaims{
|
RegisteredClaims: jwt.RegisteredClaims{
|
||||||
|
// unique token id: logout revokes by it (bug-1946)
|
||||||
|
ID: uuid.NewString(),
|
||||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Duration(utils.AuthenticationYamlConfig.Token.TokenExpireTime) * time.Second)),
|
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Duration(utils.AuthenticationYamlConfig.Token.TokenExpireTime) * time.Second)),
|
||||||
IssuedAt: jwt.NewNumericDate(time.Now()),
|
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||||||
NotBefore: jwt.NewNumericDate(time.Now()),
|
NotBefore: jwt.NewNumericDate(time.Now()),
|
||||||
|
|
@ -99,11 +102,13 @@ func storeToken(token string, username string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// removeToken removes the JWT file for a user.
|
// removeToken removes the JWT file for a user. A missing file is not an
|
||||||
|
// error: the file only mirrors the last token issued, and revocation does
|
||||||
|
// not depend on it.
|
||||||
func removeToken(username string) error {
|
func removeToken(username string) error {
|
||||||
tokenDir := filepath.Join(utils.Global_TopDir, "tokens", username)
|
tokenDir := filepath.Join(utils.Global_TopDir, "tokens", username)
|
||||||
tokenFile := filepath.Join(tokenDir, "token")
|
tokenFile := filepath.Join(tokenDir, "token")
|
||||||
if err := os.Remove(tokenFile); err != nil {
|
if err := os.Remove(tokenFile); err != nil && !os.IsNotExist(err) {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
// Remove user directory if empty
|
// Remove user directory if empty
|
||||||
|
|
@ -175,15 +180,22 @@ func (a AuthController) Logout(ctx *gin.Context) {
|
||||||
token := ctx.GetHeader("Authorization")
|
token := ctx.GetHeader("Authorization")
|
||||||
|
|
||||||
// get username from token
|
// get username from token
|
||||||
var err error
|
claims, err := ParseTokenClaims(token)
|
||||||
username, err := GetUsernameFromToken(strings.TrimPrefix(token, "Bearer "))
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error(common.ERROR_GET_USERNAME_MSG, zap.Error(err))
|
logger.Error(common.ERROR_GET_USERNAME_MSG, zap.Error(err))
|
||||||
ReturnError(ctx, common.ERROR_CODE, common.ERROR_GET_USERNAME_MSG+": "+err.Error(), nil)
|
ReturnError(ctx, common.ERROR_CODE, common.ERROR_GET_USERNAME_MSG+": "+err.Error(), nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
username := claims.Username
|
||||||
logger.Debug("username", zap.String("username", username))
|
logger.Debug("username", zap.String("username", username))
|
||||||
|
|
||||||
|
// Revoke the token until its expiry: validation rejects it from now on.
|
||||||
|
expiresAt := time.Now().Add(time.Duration(utils.AuthenticationYamlConfig.Token.TokenExpireTime) * time.Second)
|
||||||
|
if claims.ExpiresAt != nil {
|
||||||
|
expiresAt = claims.ExpiresAt.Time
|
||||||
|
}
|
||||||
|
RevokeToken(token, claims.ID, expiresAt)
|
||||||
|
|
||||||
// Remove token file from server-managed directory
|
// Remove token file from server-managed directory
|
||||||
if err := removeToken(username); err != nil {
|
if err := removeToken(username); err != nil {
|
||||||
logger.Error(common.ERROR_REMOVE_TOKEN_MSG, zap.Error(err))
|
logger.Error(common.ERROR_REMOVE_TOKEN_MSG, zap.Error(err))
|
||||||
|
|
@ -198,19 +210,33 @@ func (a AuthController) Logout(ctx *gin.Context) {
|
||||||
ReturnSuccess(ctx, common.SUCCESS_CODE, common.SUCCESS_MSG, response)
|
ReturnSuccess(ctx, common.SUCCESS_CODE, common.SUCCESS_MSG, response)
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetUsernameFromToken(tokenString string) (string, error) {
|
// ParseTokenClaims parses and verifies a token ("Bearer " prefix optional)
|
||||||
token, err := jwt.Parse(strings.TrimPrefix(tokenString, "Bearer "), JWTKeyFunc, JWTValidMethods())
|
// and rejects a token revoked by logout.
|
||||||
|
func ParseTokenClaims(tokenString string) (*models.JWTClaims, error) {
|
||||||
|
tokenString = strings.TrimPrefix(tokenString, "Bearer ")
|
||||||
|
claims := &models.JWTClaims{}
|
||||||
|
token, err := jwt.ParseWithClaims(tokenString, claims, JWTKeyFunc, JWTValidMethods())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if !token.Valid {
|
||||||
|
return nil, fmt.Errorf("invalid token")
|
||||||
|
}
|
||||||
|
if IsTokenRevoked(tokenString, claims.ID) {
|
||||||
|
return nil, fmt.Errorf("token has been revoked")
|
||||||
|
}
|
||||||
|
return claims, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetUsernameFromToken(tokenString string) (string, error) {
|
||||||
|
claims, err := ParseTokenClaims(tokenString)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
if claims.Username == "" {
|
||||||
if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
|
return "", fmt.Errorf("invalid token")
|
||||||
if username, ok := claims["username"].(string); ok {
|
|
||||||
return username, nil
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return "", fmt.Errorf("invalid token")
|
return claims.Username, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// getAccessQueues returns the list of queues the user can access for the given permission.
|
// getAccessQueues returns the list of queues the user can access for the given permission.
|
||||||
|
|
|
||||||
|
|
@ -591,9 +591,10 @@ users:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- 6.7 P1: Post-logout token reuse (design gap documentation) ---
|
// --- 6.7 P1: Post-logout token reuse is rejected (bug-1946) ---
|
||||||
// JWTs are stateless — no JTI blacklist. A valid token remains usable until expiry.
|
// Logout revokes the token until its expiry (in-memory, per process). This
|
||||||
func TestIntegration_DesignGap_PostLogoutTokenReuse(t *testing.T) {
|
// token has no jti, so it is revoked by its hash.
|
||||||
|
func TestIntegration_PostLogoutTokenRejected(t *testing.T) {
|
||||||
resetIntegrationState()
|
resetIntegrationState()
|
||||||
initTestAuthConfig()
|
initTestAuthConfig()
|
||||||
|
|
||||||
|
|
@ -602,14 +603,17 @@ func TestIntegration_DesignGap_PostLogoutTokenReuse(t *testing.T) {
|
||||||
ctx.JSON(http.StatusOK, gin.H{"status": "ok"})
|
ctx.JSON(http.StatusOK, gin.H{"status": "ok"})
|
||||||
})
|
})
|
||||||
|
|
||||||
// Create a valid long-lived token
|
expiresAt := time.Now().Add(time.Hour)
|
||||||
token := signTestJWT("user1", "basic_user", time.Now().Add(time.Hour))
|
token := signTestJWT("user1", "basic_user", expiresAt)
|
||||||
|
// No RBAC roles are loaded here, so the token passes authentication and
|
||||||
|
// fails authorization (403); only the 401 matters.
|
||||||
|
if w := doRequest(router, "GET", "/xce/v1/hosts", token); w.Code == http.StatusUnauthorized {
|
||||||
|
t.Fatalf("before logout: status = 401, the token must authenticate")
|
||||||
|
}
|
||||||
|
|
||||||
// "Logout" is a no-op in stateless JWT — the token is still valid
|
RevokeToken(token, "", expiresAt)
|
||||||
w := doRequest(router, "GET", "/xce/v1/hosts", token)
|
if w := doRequest(router, "GET", "/xce/v1/hosts", token); w.Code != http.StatusUnauthorized {
|
||||||
if w.Code == http.StatusUnauthorized {
|
t.Errorf("after logout: status = %d, want 401", w.Code)
|
||||||
t.Error("post-logout token reuse: got 401, but JWTs are stateless and no blacklist exists — " +
|
|
||||||
"if this fails, a JTI blacklist has been implemented and this test should be updated")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
73
src/frontend/apiserver/controllers/token_revocation.go
Normal file
73
src/frontend/apiserver/controllers/token_revocation.go
Normal file
|
|
@ -0,0 +1,73 @@
|
||||||
|
package controllers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/hex"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// tokenRevocations is the in-memory set of logged-out tokens (bug-1946).
|
||||||
|
// Each entry is kept until the token's own expiry, after which the token is
|
||||||
|
// rejected anyway; expired entries are purged on every revocation, so the set
|
||||||
|
// holds at most the tokens logged out within one token lifetime.
|
||||||
|
//
|
||||||
|
// Scope: per process. The set is not persisted and not shared, so a revoked
|
||||||
|
// token is accepted again by another apiserver sharing XCE_JWT_SECRET, or by
|
||||||
|
// this one after a restart with the same XCE_JWT_SECRET.
|
||||||
|
type tokenRevocations struct {
|
||||||
|
mu sync.Mutex
|
||||||
|
expires map[string]time.Time // revocation key -> token expiry
|
||||||
|
}
|
||||||
|
|
||||||
|
var revokedTokens = &tokenRevocations{expires: make(map[string]time.Time)}
|
||||||
|
|
||||||
|
// tokenRevocationKey identifies a token: its jti when present (every token
|
||||||
|
// generateJWT issues has one), otherwise a SHA-256 of the raw token string.
|
||||||
|
func tokenRevocationKey(tokenString, jti string) string {
|
||||||
|
if jti != "" {
|
||||||
|
return "jti:" + jti
|
||||||
|
}
|
||||||
|
sum := sha256.Sum256([]byte(strings.TrimPrefix(tokenString, "Bearer ")))
|
||||||
|
return "sha256:" + hex.EncodeToString(sum[:])
|
||||||
|
}
|
||||||
|
|
||||||
|
// revoke records key as revoked until expiresAt and purges expired entries.
|
||||||
|
func (r *tokenRevocations) revoke(key string, expiresAt time.Time, now time.Time) {
|
||||||
|
r.mu.Lock()
|
||||||
|
defer r.mu.Unlock()
|
||||||
|
for k, exp := range r.expires {
|
||||||
|
if !exp.After(now) {
|
||||||
|
delete(r.expires, k)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if expiresAt.After(now) {
|
||||||
|
r.expires[key] = expiresAt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// isRevoked reports whether key was revoked and has not expired yet.
|
||||||
|
func (r *tokenRevocations) isRevoked(key string, now time.Time) bool {
|
||||||
|
r.mu.Lock()
|
||||||
|
defer r.mu.Unlock()
|
||||||
|
exp, ok := r.expires[key]
|
||||||
|
return ok && exp.After(now)
|
||||||
|
}
|
||||||
|
|
||||||
|
// size returns the number of entries kept (for tests).
|
||||||
|
func (r *tokenRevocations) size() int {
|
||||||
|
r.mu.Lock()
|
||||||
|
defer r.mu.Unlock()
|
||||||
|
return len(r.expires)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RevokeToken revokes a token until its expiry.
|
||||||
|
func RevokeToken(tokenString, jti string, expiresAt time.Time) {
|
||||||
|
revokedTokens.revoke(tokenRevocationKey(tokenString, jti), expiresAt, time.Now())
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsTokenRevoked reports whether a token was revoked by logout.
|
||||||
|
func IsTokenRevoked(tokenString, jti string) bool {
|
||||||
|
return revokedTokens.isRevoked(tokenRevocationKey(tokenString, jti), time.Now())
|
||||||
|
}
|
||||||
45
src/frontend/apiserver/controllers/token_revocation_test.go
Normal file
45
src/frontend/apiserver/controllers/token_revocation_test.go
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
package controllers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// bug-1946: revocation entries live until the token's expiry and are purged
|
||||||
|
// afterwards, so memory stays bounded.
|
||||||
|
func TestTokenRevocations_ExpiryAndPurge(t *testing.T) {
|
||||||
|
r := &tokenRevocations{expires: make(map[string]time.Time)}
|
||||||
|
now := time.Unix(1_000_000, 0)
|
||||||
|
|
||||||
|
r.revoke("a", now.Add(time.Hour), now)
|
||||||
|
r.revoke("b", now.Add(2*time.Hour), now)
|
||||||
|
r.revoke("already-expired", now.Add(-time.Second), now)
|
||||||
|
if r.size() != 2 {
|
||||||
|
t.Fatalf("size = %d, want 2 (an already expired token is not stored)", r.size())
|
||||||
|
}
|
||||||
|
if !r.isRevoked("a", now) || !r.isRevoked("b", now) {
|
||||||
|
t.Fatal("revoked tokens must be reported as revoked")
|
||||||
|
}
|
||||||
|
if r.isRevoked("c", now) {
|
||||||
|
t.Error("a token never revoked must not be reported as revoked")
|
||||||
|
}
|
||||||
|
|
||||||
|
later := now.Add(90 * time.Minute)
|
||||||
|
if r.isRevoked("a", later) {
|
||||||
|
t.Error("an entry past its token expiry must not count")
|
||||||
|
}
|
||||||
|
r.revoke("c", later.Add(time.Hour), later)
|
||||||
|
if r.size() != 2 {
|
||||||
|
t.Errorf("size = %d, want 2: the expired entry a must be purged", r.size())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTokenRevocationKey(t *testing.T) {
|
||||||
|
if got := tokenRevocationKey("tok", "id-1"); got != "jti:id-1" {
|
||||||
|
t.Errorf("with jti: %q", got)
|
||||||
|
}
|
||||||
|
a, b := tokenRevocationKey("Bearer tok", ""), tokenRevocationKey("tok", "")
|
||||||
|
if a != b || a == "" || a == "jti:" {
|
||||||
|
t.Errorf("without jti the key must be the token hash, Bearer prefix ignored: %q %q", a, b)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -3,7 +3,6 @@ package middleware
|
||||||
import (
|
import (
|
||||||
"apiserver/common"
|
"apiserver/common"
|
||||||
"apiserver/controllers"
|
"apiserver/controllers"
|
||||||
"apiserver/models"
|
|
||||||
logger "apiserver/utils"
|
logger "apiserver/utils"
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
|
@ -13,7 +12,6 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/golang-jwt/jwt/v4"
|
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -98,20 +96,14 @@ func validateHeader() gin.HandlerFunc {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// isValidToken verifies the token (signature, HS256 only, expiry, not
|
||||||
|
// revoked by logout) and its IP binding.
|
||||||
func isValidToken(tokenString string, IP string) (bool, error) {
|
func isValidToken(tokenString string, IP string) (bool, error) {
|
||||||
token, err := jwt.ParseWithClaims(tokenString, &models.JWTClaims{},
|
claims, err := controllers.ParseTokenClaims(tokenString)
|
||||||
controllers.JWTKeyFunc, controllers.JWTValidMethods())
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
return claims.IP == IP, nil
|
||||||
if claims, ok := token.Claims.(*models.JWTClaims); ok && token.Valid {
|
|
||||||
if claims.IP == IP {
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateRequestBody(ctx *gin.Context) (bool, error) {
|
func validateRequestBody(ctx *gin.Context) (bool, error) {
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,18 @@
|
||||||
package middleware
|
package middleware
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"apiserver/backend/auth"
|
||||||
|
"apiserver/controllers"
|
||||||
"apiserver/models"
|
"apiserver/models"
|
||||||
"apiserver/utils"
|
"apiserver/utils"
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/golang-jwt/jwt/v4"
|
"github.com/golang-jwt/jwt/v4"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
@ -45,6 +52,81 @@ func signTestToken(t *testing.T, method jwt.SigningMethod, username, ip string)
|
||||||
return signed
|
return signed
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// newAuthFlowRouter wires logon, logout and one protected route behind
|
||||||
|
// ValidateHandler, with a config_file provider holding alice/s3cret.
|
||||||
|
func newAuthFlowRouter(t *testing.T) *gin.Engine {
|
||||||
|
t.Helper()
|
||||||
|
setTestAuthConfig(t)
|
||||||
|
prevTop, prevProvider := utils.Global_TopDir, auth.GlobalProvider
|
||||||
|
t.Cleanup(func() { utils.Global_TopDir, auth.GlobalProvider = prevTop, prevProvider })
|
||||||
|
utils.Global_TopDir = t.TempDir()
|
||||||
|
provider, err := auth.NewConfigFileProvider([]byte(
|
||||||
|
"provider: config_file\nconfig_file:\n users:\n - username: alice\n password: s3cret\n"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("NewConfigFileProvider: %v", err)
|
||||||
|
}
|
||||||
|
auth.GlobalProvider = provider
|
||||||
|
|
||||||
|
gin.SetMode(gin.TestMode)
|
||||||
|
r := gin.New()
|
||||||
|
r.Use(ValidateHandler())
|
||||||
|
authHandler := &controllers.AuthController{}
|
||||||
|
r.POST("/xce/v1/auth/logon", authHandler.Logon)
|
||||||
|
r.POST("/xce/v1/auth/logout", authHandler.Logout)
|
||||||
|
r.GET("/xce/v1/hosts", func(ctx *gin.Context) {
|
||||||
|
controllers.ReturnSuccess(ctx, http.StatusOK, "success", nil)
|
||||||
|
})
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func serve(r *gin.Engine, method, path, token, body string) *httptest.ResponseRecorder {
|
||||||
|
req := httptest.NewRequest(method, path, strings.NewReader(body))
|
||||||
|
req.RemoteAddr = "10.0.0.1:4000"
|
||||||
|
if body != "" {
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
}
|
||||||
|
if token != "" {
|
||||||
|
req.Header.Set("Authorization", "Bearer "+token)
|
||||||
|
}
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
r.ServeHTTP(w, req)
|
||||||
|
return w
|
||||||
|
}
|
||||||
|
|
||||||
|
func logon(t *testing.T, r *gin.Engine) string {
|
||||||
|
t.Helper()
|
||||||
|
w := serve(r, http.MethodPost, "/xce/v1/auth/logon", "", `{"username":"alice","password":"s3cret"}`)
|
||||||
|
var resp struct {
|
||||||
|
Data models.LogonResponse `json:"data"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil || resp.Data.Token == "" {
|
||||||
|
t.Fatalf("logon failed: status %d body %s", w.Code, w.Body.String())
|
||||||
|
}
|
||||||
|
return resp.Data.Token
|
||||||
|
}
|
||||||
|
|
||||||
|
// bug-1946: after logout the token is rejected; a new logon (even in the
|
||||||
|
// same second) gets a working token.
|
||||||
|
func TestLogout_RevokesToken(t *testing.T) {
|
||||||
|
r := newAuthFlowRouter(t)
|
||||||
|
|
||||||
|
token := logon(t, r)
|
||||||
|
if w := serve(r, http.MethodGet, "/xce/v1/hosts", token, ""); w.Code != http.StatusOK {
|
||||||
|
t.Fatalf("before logout: status = %d, want 200 (%s)", w.Code, w.Body.String())
|
||||||
|
}
|
||||||
|
if w := serve(r, http.MethodPost, "/xce/v1/auth/logout", token, ""); w.Code != http.StatusOK {
|
||||||
|
t.Fatalf("logout: status = %d, want 200 (%s)", w.Code, w.Body.String())
|
||||||
|
}
|
||||||
|
if w := serve(r, http.MethodGet, "/xce/v1/hosts", token, ""); w.Code != http.StatusUnauthorized {
|
||||||
|
t.Errorf("after logout: status = %d, want 401 (%s)", w.Code, w.Body.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
fresh := logon(t, r)
|
||||||
|
if w := serve(r, http.MethodGet, "/xce/v1/hosts", fresh, ""); w.Code != http.StatusOK {
|
||||||
|
t.Errorf("token from a new logon: status = %d, want 200 (%s)", w.Code, w.Body.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// bug-1950: only the HMAC algorithm used for signing (HS256) is accepted.
|
// bug-1950: only the HMAC algorithm used for signing (HS256) is accepted.
|
||||||
func TestIsValidToken_PinsSigningAlgorithm(t *testing.T) {
|
func TestIsValidToken_PinsSigningAlgorithm(t *testing.T) {
|
||||||
setTestAuthConfig(t)
|
setTestAuthConfig(t)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue