mirror of
https://github.com/pocket-id/pocket-id.git
synced 2026-10-02 07:49:04 +02:00
feat: add ability to customize session duration of clients (#1641)
This commit is contained in:
@@ -52,10 +52,11 @@ func TestTokenHandlerClientCredentialsGrant(t *testing.T) {
|
||||
hashed, err := bcrypt.GenerateFromPassword([]byte(clientPlain), bcrypt.DefaultCost)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, db.Create(&model.OidcClient{
|
||||
Base: model.Base{ID: clientID},
|
||||
Name: "Client Credentials Client",
|
||||
Secret: string(hashed),
|
||||
IsPublic: false,
|
||||
Base: model.Base{ID: clientID},
|
||||
Name: "Client Credentials Client",
|
||||
Secret: string(hashed),
|
||||
IsPublic: false,
|
||||
AccessTokenDurationMinutes: 2 * 60,
|
||||
}).Error)
|
||||
|
||||
provider, err := newProvider(NewStore(db, nil), nil, testTokenSigner{key: key}, Config{
|
||||
@@ -88,6 +89,8 @@ func TestTokenHandlerClientCredentialsGrant(t *testing.T) {
|
||||
require.Contains(t, jwtAudience(claims), clientID, "access token must be audience-bound to the client")
|
||||
require.Equal(t, "client-"+clientID, claims["sub"])
|
||||
require.Equal(t, clientID, claims["client_id"])
|
||||
require.InDelta(t, 2*time.Hour/time.Second, body["expires_in"], 1)
|
||||
require.InDelta(t, 2*time.Hour/time.Second, claims["exp"].(float64)-claims["iat"].(float64), 1)
|
||||
}
|
||||
|
||||
// TestTokenHandlerClientCredentialsDropsIdentityScopes guards that a machine token never
|
||||
@@ -452,6 +455,46 @@ func TestTokenHandlerRefreshGrantRevalidatesUser(t *testing.T) {
|
||||
require.NotEqual(t, token, body["refresh_token"], "refresh token must be rotated")
|
||||
})
|
||||
|
||||
t.Run("rotation applies the current client lifetimes", func(t *testing.T) {
|
||||
db := testutils.NewDatabaseForTest(t)
|
||||
const clientID, userID = "client-custom-lifetimes", "user-custom-lifetimes"
|
||||
const accessDuration = 2 * time.Hour
|
||||
const refreshDuration = 7 * 24 * time.Hour
|
||||
createClient(t, db, model.OidcClient{Base: model.Base{ID: clientID}, Name: "Client", IsPublic: true})
|
||||
require.NoError(t, db.Create(&model.User{Base: model.Base{ID: userID}, Username: "tim"}).Error)
|
||||
|
||||
token := mintRefreshToken(t, db, clientID, userID)
|
||||
globalSecret, err := DeriveGlobalSecret([]byte(secret))
|
||||
require.NoError(t, err)
|
||||
strategy := compose.NewOAuth2HMACStrategy(&fosite.Config{GlobalSecret: globalSecret})
|
||||
existingSignature := strategy.RefreshTokenSignature(t.Context(), token)
|
||||
var existingBeforeUpdate OAuth2Session
|
||||
require.NoError(t, db.First(&existingBeforeUpdate, "kind = ? AND key = ?", sessionKindRefreshToken, existingSignature).Error)
|
||||
require.NotNil(t, existingBeforeUpdate.ExpiresAt)
|
||||
|
||||
require.NoError(t, db.Model(&model.OidcClient{}).Where("id = ?", clientID).Updates(map[string]any{
|
||||
"access_token_duration_minutes": int64(accessDuration / time.Minute),
|
||||
"refresh_token_duration_minutes": int64(refreshDuration / time.Minute),
|
||||
}).Error)
|
||||
var existingAfterUpdate OAuth2Session
|
||||
require.NoError(t, db.First(&existingAfterUpdate, "kind = ? AND key = ?", sessionKindRefreshToken, existingSignature).Error)
|
||||
require.Equal(t, existingBeforeUpdate.ExpiresAt, existingAfterUpdate.ExpiresAt)
|
||||
|
||||
rotationStartedAt := time.Now().UTC()
|
||||
body := doRefresh(t, db, clientID, token)
|
||||
|
||||
require.NotEmpty(t, body["access_token"], "expected a new access token, got error: %v", body["error"])
|
||||
require.InDelta(t, accessDuration/time.Second, body["expires_in"], 1)
|
||||
claims := decodeJWTPart(t, body["access_token"].(string), 1)
|
||||
require.InDelta(t, accessDuration/time.Second, claims["exp"].(float64)-claims["iat"].(float64), 1)
|
||||
|
||||
rotatedSignature := strategy.RefreshTokenSignature(t.Context(), body["refresh_token"].(string))
|
||||
var stored OAuth2Session
|
||||
require.NoError(t, db.First(&stored, "kind = ? AND key = ?", sessionKindRefreshToken, rotatedSignature).Error)
|
||||
require.NotNil(t, stored.ExpiresAt)
|
||||
require.WithinDuration(t, rotationStartedAt.Add(refreshDuration), time.Time(*stored.ExpiresAt), 2*time.Second)
|
||||
})
|
||||
|
||||
t.Run("disabled user is rejected on refresh", func(t *testing.T) {
|
||||
db := testutils.NewDatabaseForTest(t)
|
||||
const clientID, userID = "client-disabled", "user-disabled"
|
||||
|
||||
Reference in New Issue
Block a user