Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion internal/api/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,22 @@ func (a *API) parseJWTClaims(bearer string, r *http.Request) (context.Context, e
ctx := r.Context()
config := a.config

p := jwt.NewParser(jwt.WithValidMethods(config.JWT.ValidMethods))
// Check if allow_expired query parameter is set
allowExpired := r.URL.Query().Get("allow_expired") == "true"

// Configure JWT parser based on allowExpired flag
var p *jwt.Parser
if allowExpired {
// Skip claims validation (including exp check) when explicitly requested
p = jwt.NewParser(
jwt.WithValidMethods(config.JWT.ValidMethods),
jwt.WithoutClaimsValidation(),
)
} else {
// Default behavior: validate all claims including expiration
p = jwt.NewParser(jwt.WithValidMethods(config.JWT.ValidMethods))
}

token, err := p.ParseWithClaims(bearer, &AccessTokenClaims{}, func(token *jwt.Token) (interface{}, error) {
if kid, ok := token.Header["kid"]; ok {
if kidStr, ok := kid.(string); ok {
Expand Down
60 changes: 60 additions & 0 deletions internal/api/auth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,66 @@
}
}

func (ts *AuthTestSuite) TestParseJWTClaimsWithAllowExpired() {
// Test that allow_expired query parameter controls JWT expiration validation
u, err := models.FindUserByEmailAndAudience(ts.API.db, "test@example.com", ts.Config.JWT.Aud)
require.NoError(ts.T(), err)

s, err := models.NewSession(u.ID, nil)
require.NoError(ts.T(), err)
require.NoError(ts.T(), ts.API.db.Create(s))

// Create an expired JWT token (exp set to past time)
expiredClaims := &AccessTokenClaims{
RegisteredClaims: jwt.RegisteredClaims{
Subject: u.ID.String(),
ExpiresAt: jwt.NewNumericDate(jwt.Now().Add(-1 * 3600)), // expired 1 hour ago

Check failure on line 176 in internal/api/auth_test.go

View workflow job for this annotation

GitHub Actions / test

undefined: jwt.Now
},
Role: "authenticated",
SessionId: s.ID.String(),
}

expiredJwt, err := jwt.NewWithClaims(jwt.SigningMethodHS256, expiredClaims).SignedString([]byte(ts.Config.JWT.Secret))
require.NoError(ts.T(), err)

ts.Run("Without allow_expired parameter - should reject expired token", func() {
req := httptest.NewRequest(http.MethodGet, "http://localhost/user", nil)
req.Header.Set("Authorization", "Bearer "+expiredJwt)

_, err := ts.API.parseJWTClaims(expiredJwt, req)
require.Error(ts.T(), err)
require.Contains(ts.T(), err.Error(), "token is expired")
})

ts.Run("With allow_expired=false - should reject expired token", func() {
req := httptest.NewRequest(http.MethodGet, "http://localhost/user?allow_expired=false", nil)
req.Header.Set("Authorization", "Bearer "+expiredJwt)

_, err := ts.API.parseJWTClaims(expiredJwt, req)
require.Error(ts.T(), err)
require.Contains(ts.T(), err.Error(), "token is expired")
})

ts.Run("With allow_expired=true - should accept expired token", func() {
req := httptest.NewRequest(http.MethodGet, "http://localhost/user?allow_expired=true", nil)
req.Header.Set("Authorization", "Bearer "+expiredJwt)

ctx, err := ts.API.parseJWTClaims(expiredJwt, req)
require.NoError(ts.T(), err)

// Verify token is stored in context
token := getToken(ctx)
require.NotNil(ts.T(), token)
require.Equal(ts.T(), expiredJwt, token.Raw)

// Verify claims are correctly parsed
claims := getClaims(ctx)
require.NotNil(ts.T(), claims)
require.Equal(ts.T(), u.ID.String(), claims.Subject)
require.Equal(ts.T(), "authenticated", claims.Role)
})
}

func (ts *AuthTestSuite) TestMaybeLoadUserOrSession() {
u, err := models.FindUserByEmailAndAudience(ts.API.db, "test@example.com", ts.Config.JWT.Aud)
require.NoError(ts.T(), err)
Expand Down
Loading