mirror of
https://github.com/pocket-id/pocket-id.git
synced 2026-09-02 09:11:28 +02:00
feat: add ability to customize session duration of clients (#1641)
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 != ""
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE oidc_clients DROP COLUMN access_token_duration_minutes;
|
||||
ALTER TABLE oidc_clients DROP COLUMN refresh_token_duration_minutes;
|
||||
@@ -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;
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE oidc_clients DROP COLUMN access_token_duration_minutes;
|
||||
ALTER TABLE oidc_clients DROP COLUMN refresh_token_duration_minutes;
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user