feat: add ability to customize session duration of clients (#1641)

This commit is contained in:
Elias Schneider
2026-08-03 23:23:29 +02:00
committed by GitHub
parent 1238bf0f37
commit 5d43c4aaeb
28 changed files with 752 additions and 77 deletions

View File

@@ -24,6 +24,8 @@ type OidcClientDto struct {
Credentials OidcClientCredentialsDto `json:"credentials"`
IsGroupRestricted bool `json:"isGroupRestricted"`
PkceSupported bool `json:"pkceSupported,omitempty"`
AccessTokenDurationMinutes int64 `json:"accessTokenDurationMinutes"`
RefreshTokenDurationMinutes int64 `json:"refreshTokenDurationMinutes"`
}
type OidcClientWithAllowedUserGroupsDto struct {
@@ -53,6 +55,8 @@ type OidcClientUpdateDto struct {
LogoURL *string `json:"logoUrl"`
DarkLogoURL *string `json:"darkLogoUrl"`
IsGroupRestricted bool `json:"isGroupRestricted"`
AccessTokenDurationMinutes int64 `json:"accessTokenDurationMinutes" binding:"required,token_duration"`
RefreshTokenDurationMinutes int64 `json:"refreshTokenDurationMinutes" binding:"required,token_duration"`
}
type OidcClientCreateDto struct {

View File

@@ -8,6 +8,7 @@ import (
"time"
"github.com/ory/fosite"
"github.com/pocket-id/pocket-id/backend/internal/model"
"github.com/pocket-id/pocket-id/backend/internal/utils"
"github.com/gin-gonic/gin/binding"
@@ -61,6 +62,9 @@ func init() {
"resource_uri": func(fl validator.FieldLevel) bool {
return ValidateResourceURI(fl.Field().String())
},
"token_duration": func(fl validator.FieldLevel) bool {
return model.IsValidTokenDurationMinutes(fl.Field().Int())
},
}
for k, v := range validators {
err := engine.RegisterValidation(k, v)

View File

@@ -3,9 +3,38 @@ package dto
import (
"testing"
"github.com/gin-gonic/gin/binding"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestTokenDurationValidation(t *testing.T) {
type input struct {
Duration int64 `binding:"required,token_duration"`
}
for _, test := range []struct {
name string
value int64
wantErr bool
}{
{name: "omitted", wantErr: true},
{name: "below minimum", value: 0, wantErr: true},
{name: "minimum", value: 1},
{name: "custom duration", value: 90},
{name: "above maximum", value: 365*24*60 + 1, wantErr: true},
} {
t.Run(test.name, func(t *testing.T) {
err := binding.Validator.ValidateStruct(input{Duration: test.value})
if test.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
})
}
}
func TestValidateUsername(t *testing.T) {
tests := []struct {
name string

View File

@@ -25,6 +25,15 @@ type OidcClientType string
const (
OidcClientTypeStandard OidcClientType = "standard"
OidcClientTypeCIMD OidcClientType = "cimd"
// DefaultAccessTokenDurationMinutes is the access-token lifetime used for new clients
DefaultAccessTokenDurationMinutes int64 = 60
// DefaultRefreshTokenDurationMinutes is the refresh-token lifetime used for new clients
DefaultRefreshTokenDurationMinutes int64 = 30 * 24 * 60
// MinTokenDurationMinutes is the shortest configurable token lifetime
MinTokenDurationMinutes int64 = 1
// MaxTokenDurationMinutes is the longest configurable token lifetime
MaxTokenDurationMinutes int64 = 365 * 24 * 60
)
type OidcClient struct {
@@ -49,6 +58,8 @@ type OidcClient struct {
ClientType OidcClientType `gorm:"default:standard" sortable:"true" filterable:"true"`
MetadataExpiresAt *datatype.DateTime
MetadataGrantTypes datatype.StringList
AccessTokenDurationMinutes int64 `gorm:"default:60"`
RefreshTokenDurationMinutes int64 `gorm:"default:43200"`
AllowedUserGroups []UserGroup `gorm:"many2many:oidc_clients_allowed_user_groups;"`
CreatedByID *string
@@ -56,6 +67,11 @@ type OidcClient struct {
UserAuthorizedOidcClients []UserAuthorizedOidcClient `gorm:"foreignKey:ClientID;references:ID"`
}
// IsValidTokenDurationMinutes reports whether a duration is within the configurable range
func IsValidTokenDurationMinutes(minutes int64) bool {
return minutes >= MinTokenDurationMinutes && minutes <= MaxTokenDurationMinutes
}
func (c OidcClient) HasLogo() bool {
return c.ImageType != nil && *c.ImageType != ""
}

View File

@@ -201,12 +201,14 @@ func buildClientFromMetadata(doc *fosite.ClientMetadataDocument, rawURL string)
}
client := model.OidcClient{
Base: model.Base{ID: rawURL},
Name: doc.ClientName,
CallbackURLs: datatype.StringList(doc.RedirectURIs),
LogoutCallbackURLs: datatype.StringList(doc.PostLogoutRedirectURIs),
ClientType: model.OidcClientTypeCIMD,
MetadataGrantTypes: datatype.StringList(grantTypes),
Base: model.Base{ID: rawURL},
Name: doc.ClientName,
CallbackURLs: datatype.StringList(doc.RedirectURIs),
LogoutCallbackURLs: datatype.StringList(doc.PostLogoutRedirectURIs),
ClientType: model.OidcClientTypeCIMD,
MetadataGrantTypes: datatype.StringList(grantTypes),
AccessTokenDurationMinutes: model.DefaultAccessTokenDurationMinutes,
RefreshTokenDurationMinutes: model.DefaultRefreshTokenDurationMinutes,
}
switch doc.TokenEndpointAuthMethod {

View File

@@ -40,6 +40,8 @@ func TestBuildClientFromMetadata(t *testing.T) {
assert.Equal(t, []string{"https://app.example.com/logout"}, []string(c.LogoutCallbackURLs))
assert.Equal(t, []string{"authorization_code"}, []string(c.MetadataGrantTypes))
assert.Empty(t, c.Credentials.FederatedIdentities)
assert.Equal(t, model.DefaultAccessTokenDurationMinutes, c.AccessTokenDurationMinutes)
assert.Equal(t, model.DefaultRefreshTokenDurationMinutes, c.RefreshTokenDurationMinutes)
})
t.Run("authenticated clients are rejected", func(t *testing.T) {
@@ -161,7 +163,16 @@ func TestRefreshMetadataClient(t *testing.T) {
s := newMetadataStore(t, map[string]*http.Response{id: resp})
fresh := datatype.DateTime(time.Now().Add(time.Hour))
seed := model.OidcClient{Base: model.Base{ID: id}, Name: "Old", IsPublic: true, PkceEnabled: true, ClientType: model.OidcClientTypeCIMD, MetadataExpiresAt: &fresh}
seed := model.OidcClient{
Base: model.Base{ID: id},
Name: "Old",
IsPublic: true,
PkceEnabled: true,
ClientType: model.OidcClientTypeCIMD,
MetadataExpiresAt: &fresh,
AccessTokenDurationMinutes: 2 * 60,
RefreshTokenDurationMinutes: 7 * 24 * 60,
}
require.NoError(t, s.db.Create(&seed).Error)
// A normal lookup still returns the cached value.
@@ -174,6 +185,8 @@ func TestRefreshMetadataClient(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, "App", c.Name)
assert.True(t, c.IsMetadataDocument())
assert.Equal(t, int64(2*60), c.AccessTokenDurationMinutes)
assert.Equal(t, int64(7*24*60), c.RefreshTokenDurationMinutes)
})
}

View File

@@ -2,6 +2,7 @@ package oidc
import (
"slices"
"time"
"github.com/ory/fosite"
"github.com/pocket-id/pocket-id/backend/internal/model"
@@ -86,3 +87,36 @@ func (c Client) GetResponseModes() []fosite.ResponseModeType {
fosite.ResponseModeFormPost,
}
}
func (c Client) GetEffectiveLifespan(grantType fosite.GrantType, tokenType fosite.TokenType, fallback time.Duration) time.Duration {
var minutes int64
switch tokenType {
case fosite.AccessToken:
switch grantType {
case fosite.GrantTypeAuthorizationCode, fosite.GrantTypeRefreshToken, fosite.GrantTypeDeviceCode, fosite.GrantTypeClientCredentials:
minutes = c.AccessTokenDurationMinutes
case fosite.GrantTypeImplicit, fosite.GrantTypePassword, fosite.GrantTypeJWTBearer:
return fallback
default:
return fallback
}
case fosite.RefreshToken:
switch grantType {
case fosite.GrantTypeAuthorizationCode, fosite.GrantTypeRefreshToken, fosite.GrantTypeDeviceCode:
minutes = c.RefreshTokenDurationMinutes
case fosite.GrantTypeImplicit, fosite.GrantTypePassword, fosite.GrantTypeClientCredentials, fosite.GrantTypeJWTBearer:
return fallback
default:
return fallback
}
case fosite.AuthorizeCode, fosite.IDToken, fosite.UserCode, fosite.DeviceCode, fosite.PushedAuthorizeRequestContext:
return fallback
default:
return fallback
}
if !model.IsValidTokenDurationMinutes(minutes) {
return fallback
}
return time.Duration(minutes) * time.Minute
}

View File

@@ -1,11 +1,53 @@
package oidc
import (
"testing"
"time"
"github.com/ory/fosite"
"github.com/stretchr/testify/require"
"github.com/pocket-id/pocket-id/backend/internal/model"
)
// Interface assertions
var (
_ fosite.Client = (*Client)(nil)
_ fosite.ResponseModeClient = (*Client)(nil)
_ fosite.Client = (*Client)(nil)
_ fosite.ResponseModeClient = (*Client)(nil)
_ fosite.ClientWithCustomTokenLifespans = (*Client)(nil)
)
func TestClientGetEffectiveLifespan(t *testing.T) {
client := Client{OidcClient: model.OidcClient{
AccessTokenDurationMinutes: 2 * 60,
RefreshTokenDurationMinutes: 7 * 24 * 60,
}}
fallback := 13 * time.Minute
for _, test := range []struct {
name string
grantType fosite.GrantType
tokenType fosite.TokenType
want time.Duration
}{
{name: "authorization code access token", grantType: fosite.GrantTypeAuthorizationCode, tokenType: fosite.AccessToken, want: 2 * time.Hour},
{name: "authorization code refresh token", grantType: fosite.GrantTypeAuthorizationCode, tokenType: fosite.RefreshToken, want: 7 * 24 * time.Hour},
{name: "refresh grant access token", grantType: fosite.GrantTypeRefreshToken, tokenType: fosite.AccessToken, want: 2 * time.Hour},
{name: "refresh grant refresh token", grantType: fosite.GrantTypeRefreshToken, tokenType: fosite.RefreshToken, want: 7 * 24 * time.Hour},
{name: "device grant access token", grantType: fosite.GrantTypeDeviceCode, tokenType: fosite.AccessToken, want: 2 * time.Hour},
{name: "device grant refresh token", grantType: fosite.GrantTypeDeviceCode, tokenType: fosite.RefreshToken, want: 7 * 24 * time.Hour},
{name: "client credentials access token", grantType: fosite.GrantTypeClientCredentials, tokenType: fosite.AccessToken, want: 2 * time.Hour},
{name: "client credentials refresh token falls back", grantType: fosite.GrantTypeClientCredentials, tokenType: fosite.RefreshToken, want: fallback},
{name: "ID token falls back", grantType: fosite.GrantTypeAuthorizationCode, tokenType: fosite.IDToken, want: fallback},
{name: "unsupported grant falls back", grantType: fosite.GrantTypePassword, tokenType: fosite.AccessToken, want: fallback},
} {
t.Run(test.name, func(t *testing.T) {
require.Equal(t, test.want, client.GetEffectiveLifespan(test.grantType, test.tokenType, fallback))
})
}
client.AccessTokenDurationMinutes = 0
client.RefreshTokenDurationMinutes = model.MaxTokenDurationMinutes + 1
require.Equal(t, fallback, client.GetEffectiveLifespan(fosite.GrantTypeAuthorizationCode, fosite.AccessToken, fallback))
require.Equal(t, fallback, client.GetEffectiveLifespan(fosite.GrantTypeAuthorizationCode, fosite.RefreshToken, fallback))
}

View File

@@ -94,12 +94,14 @@ func (b *ClientPreviewBuilder) validatedScopes(ctx context.Context, client model
func (b *ClientPreviewBuilder) newPreviewRequest(ctx context.Context, client model.OidcClient, userID string, scopes fosite.Arguments, authenticationMethod string) *fosite.Request {
now := time.Now().UTC()
runtimeClient := Client{OidcClient: client}
session := NewAuthenticatedSession(userID, authenticationMethod, now, now)
session.SetExpiresAt(fosite.AccessToken, now.Add(b.strategies.config.GetAccessTokenLifespan(ctx)))
accessTokenLifespan := fosite.GetEffectiveLifespan(runtimeClient, fosite.GrantTypeAuthorizationCode, fosite.AccessToken, b.strategies.config.GetAccessTokenLifespan(ctx))
session.SetExpiresAt(fosite.AccessToken, now.Add(accessTokenLifespan))
request := fosite.NewRequest()
request.RequestedAt = now
request.Client = Client{OidcClient: client}
request.Client = runtimeClient
request.RequestedScope = scopes
request.GrantedScope = scopes
request.RequestedAudience = fosite.Arguments{client.ID}

View File

@@ -5,6 +5,7 @@ import (
"crypto/elliptic"
"crypto/rand"
"testing"
"time"
"github.com/stretchr/testify/require"
@@ -40,8 +41,9 @@ func TestClientPreviewBuilderUsesFositeTokenStrategies(t *testing.T) {
}).Error)
preview, err := builder.BuildClientPreview(t.Context(), model.OidcClient{
Base: model.Base{ID: clientID},
Name: "Test Client",
Base: model.Base{ID: clientID},
Name: "Test Client",
AccessTokenDurationMinutes: 2 * 60,
}, userID, []string{"openid", "email"}, "phr")
require.NoError(t, err)
@@ -51,6 +53,11 @@ func TestClientPreviewBuilderUsesFositeTokenStrategies(t *testing.T) {
// The identity scopes add the issuer to the audience so the previewed token would also work at /userinfo
require.ElementsMatch(t, []string{clientID, "https://issuer.example.com"}, stringSliceClaim(t, preview.AccessToken["aud"]))
require.NotContains(t, preview.AccessToken, "type")
issuedAt, ok := preview.AccessToken["iat"].(time.Time)
require.Truef(t, ok, "expected time.Time iat, got %T", preview.AccessToken["iat"])
expiresAt, ok := preview.AccessToken["exp"].(time.Time)
require.Truef(t, ok, "expected time.Time exp, got %T", preview.AccessToken["exp"])
require.Equal(t, 2*time.Hour, expiresAt.Sub(issuedAt))
require.Equal(t, userID, preview.IDToken["sub"])
// ID tokens carry the "type" marker (so the end-session endpoint can reject access tokens

View File

@@ -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"

View File

@@ -32,8 +32,8 @@ const (
GrantTypeDeviceCode = "urn:ietf:params:oauth:grant-type:device_code"
GrantTypeClientCredentials = "client_credentials"
AccessTokenDuration = time.Hour
RefreshTokenDuration = 30 * 24 * time.Hour // 30 days
AccessTokenDuration = time.Duration(model.DefaultAccessTokenDurationMinutes) * time.Minute
RefreshTokenDuration = time.Duration(model.DefaultRefreshTokenDurationMinutes) * time.Minute
)
type OidcService struct {
@@ -148,7 +148,9 @@ func (s *OidcService) CreateClient(ctx context.Context, input dto.OidcClientCrea
Base: model.Base{
ID: input.ID,
},
CreatedByID: new(userID),
CreatedByID: new(userID),
AccessTokenDurationMinutes: model.DefaultAccessTokenDurationMinutes,
RefreshTokenDurationMinutes: model.DefaultRefreshTokenDurationMinutes,
}
updateOIDCClientModelFromDto(&client, &input.OidcClientUpdateDto)
@@ -213,6 +215,8 @@ func (s *OidcService) UpdateClient(ctx context.Context, clientID string, input d
"SkipConsent",
"LaunchURL",
"IsGroupRestricted",
"AccessTokenDurationMinutes",
"RefreshTokenDurationMinutes",
).
Updates(&client).Error
} else {
@@ -253,6 +257,8 @@ func updateOIDCClientModelFromDto(client *model.OidcClient, input *dto.OidcClien
client.SkipConsent = input.SkipConsent
client.LaunchURL = input.LaunchURL
client.IsGroupRestricted = input.IsGroupRestricted
client.AccessTokenDurationMinutes = input.AccessTokenDurationMinutes
client.RefreshTokenDurationMinutes = input.RefreshTokenDurationMinutes
// Preserve fields that are sourced from the client metadata document
if client.IsMetadataDocument() {

View File

@@ -528,9 +528,11 @@ func TestOidcService_CreateClient_withDescription(t *testing.T) {
description := "A test client description"
input := dto.OidcClientCreateDto{
OidcClientUpdateDto: dto.OidcClientUpdateDto{
Name: "Test Client",
Description: description,
CallbackURLs: []string{"https://example.com/callback"},
Name: "Test Client",
Description: description,
CallbackURLs: []string{"https://example.com/callback"},
AccessTokenDurationMinutes: model.DefaultAccessTokenDurationMinutes,
RefreshTokenDurationMinutes: model.DefaultRefreshTokenDurationMinutes,
},
}
@@ -552,8 +554,10 @@ func TestOidcService_CreateClient_withoutDescription(t *testing.T) {
input := dto.OidcClientCreateDto{
OidcClientUpdateDto: dto.OidcClientUpdateDto{
Name: "Test Client",
CallbackURLs: []string{"https://example.com/callback"},
Name: "Test Client",
CallbackURLs: []string{"https://example.com/callback"},
AccessTokenDurationMinutes: model.DefaultAccessTokenDurationMinutes,
RefreshTokenDurationMinutes: model.DefaultRefreshTokenDurationMinutes,
},
}
@@ -606,9 +610,11 @@ func TestOidcService_UpdateClient_description(t *testing.T) {
// Update with a description
description := "Updated description"
input := dto.OidcClientUpdateDto{
Name: "Test Client",
Description: description,
CallbackURLs: []string{"https://example.com/callback"},
Name: "Test Client",
Description: description,
CallbackURLs: []string{"https://example.com/callback"},
AccessTokenDurationMinutes: model.DefaultAccessTokenDurationMinutes,
RefreshTokenDurationMinutes: model.DefaultRefreshTokenDurationMinutes,
}
_, err = s.UpdateClient(t.Context(), client.ID, input)
@@ -654,6 +660,8 @@ func TestOidcService_UpdateClient_CIMDPreservesMetadataFields(t *testing.T) {
require.NoError(t, db.Create(&client).Error)
launchURL := "https://app.example.com"
accessDuration := int64(2 * 60)
refreshDuration := int64(7 * 24 * 60)
input := dto.OidcClientUpdateDto{
Name: "Overridden Client",
Description: "Locally managed description",
@@ -666,6 +674,8 @@ func TestOidcService_UpdateClient_CIMDPreservesMetadataFields(t *testing.T) {
SkipConsent: true,
LaunchURL: &launchURL,
IsGroupRestricted: true,
AccessTokenDurationMinutes: accessDuration,
RefreshTokenDurationMinutes: refreshDuration,
Credentials: dto.OidcClientCredentialsDto{
FederatedIdentities: []dto.OidcClientFederatedIdentityDto{{
Issuer: "https://override.example.com",
@@ -691,6 +701,8 @@ func TestOidcService_UpdateClient_CIMDPreservesMetadataFields(t *testing.T) {
assert.Equal(t, input.SkipConsent, fetched.SkipConsent)
assert.Equal(t, input.LaunchURL, fetched.LaunchURL)
assert.Equal(t, input.IsGroupRestricted, fetched.IsGroupRestricted)
assert.Equal(t, accessDuration, fetched.AccessTokenDurationMinutes)
assert.Equal(t, refreshDuration, fetched.RefreshTokenDurationMinutes)
}
func TestOidcService_UpdateClient_CIMDDoesNotOverwriteConcurrentMetadataRefresh(t *testing.T) {
@@ -715,7 +727,11 @@ func TestOidcService_UpdateClient_CIMDDoesNotOverwriteConcurrentMetadataRefresh(
END;
`).Error)
input := dto.OidcClientUpdateDto{Description: "Locally managed description"}
input := dto.OidcClientUpdateDto{
Description: "Locally managed description",
AccessTokenDurationMinutes: model.DefaultAccessTokenDurationMinutes,
RefreshTokenDurationMinutes: model.DefaultRefreshTokenDurationMinutes,
}
_, err = s.UpdateClient(t.Context(), client.ID, input)
require.NoError(t, err)

View File

@@ -0,0 +1,2 @@
ALTER TABLE oidc_clients DROP COLUMN access_token_duration_minutes;
ALTER TABLE oidc_clients DROP COLUMN refresh_token_duration_minutes;

View File

@@ -0,0 +1,2 @@
ALTER TABLE oidc_clients ADD COLUMN access_token_duration_minutes BIGINT NOT NULL DEFAULT 60;
ALTER TABLE oidc_clients ADD COLUMN refresh_token_duration_minutes BIGINT NOT NULL DEFAULT 43200;

View File

@@ -0,0 +1,2 @@
ALTER TABLE oidc_clients DROP COLUMN access_token_duration_minutes;
ALTER TABLE oidc_clients DROP COLUMN refresh_token_duration_minutes;

View File

@@ -0,0 +1,2 @@
ALTER TABLE oidc_clients ADD COLUMN access_token_duration_minutes INTEGER NOT NULL DEFAULT 60;
ALTER TABLE oidc_clients ADD COLUMN refresh_token_duration_minutes INTEGER NOT NULL DEFAULT 43200;