mirror of
https://github.com/pocket-id/pocket-id.git
synced 2026-07-18 16:59:53 +00:00
1137 lines
44 KiB
Go
1137 lines
44 KiB
Go
package oidc
|
|
|
|
import (
|
|
"context"
|
|
"net/url"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/ory/fosite"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/pocket-id/pocket-id/backend/internal/common"
|
|
"github.com/pocket-id/pocket-id/backend/internal/model"
|
|
datatype "github.com/pocket-id/pocket-id/backend/internal/model/types"
|
|
testutils "github.com/pocket-id/pocket-id/backend/internal/utils/testing"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
type fakeAuditLogger struct {
|
|
events []model.AuditLogEvent
|
|
data []model.AuditLogData
|
|
}
|
|
|
|
func (f *fakeAuditLogger) Create(_ context.Context, event model.AuditLogEvent, _, _, _ string, data model.AuditLogData, _ *gorm.DB) (model.AuditLog, bool) {
|
|
f.events = append(f.events, event)
|
|
f.data = append(f.data, data)
|
|
return model.AuditLog{}, true
|
|
}
|
|
|
|
func TestAuthorizationServiceAuthorizeLogsClientAuthorization(t *testing.T) {
|
|
db := testutils.NewDatabaseForTest(t)
|
|
auditLogger := &fakeAuditLogger{}
|
|
service := newAuthorizationService(db, newInteractionSessionService(db), newClaimsService(db, nil, "", nil), nil, auditLogger, nil)
|
|
|
|
const (
|
|
userID = "test-user"
|
|
clientID = "test-client"
|
|
)
|
|
|
|
require.NoError(t, db.Create(&model.User{
|
|
Base: model.Base{ID: userID},
|
|
}).Error)
|
|
require.NoError(t, db.Create(&model.OidcClient{
|
|
Base: model.Base{ID: clientID},
|
|
Name: "Test Client",
|
|
}).Error)
|
|
require.NoError(t, db.Create(&model.UserAuthorizedOidcClient{
|
|
UserID: userID,
|
|
ClientID: clientID,
|
|
Scope: datatype.StringList{"openid"},
|
|
}).Error)
|
|
|
|
authorization, err := service.authorize(t.Context(), authorizeInput{
|
|
userID: userID,
|
|
authenticationTime: time.Now().UTC(),
|
|
requester: newTestAuthorizeRequester("audit-request", clientID, ""),
|
|
meta: requestMeta{IPAddress: "203.0.113.1", UserAgent: "test-agent"},
|
|
})
|
|
require.NoError(t, err)
|
|
require.False(t, authorization.RequiresInteraction)
|
|
|
|
require.Equal(t, []model.AuditLogEvent{model.AuditLogEventClientAuthorization}, auditLogger.events)
|
|
require.Equal(t, model.AuditLogData{"clientName": "Test Client"}, auditLogger.data[0])
|
|
}
|
|
|
|
func TestAuthorizationServiceRejectsCustomScopeWithoutResource(t *testing.T) {
|
|
db := testutils.NewDatabaseForTest(t)
|
|
service := newAuthorizationService(db, newInteractionSessionService(db), newClaimsService(db, nil, "", nil), nil, nil, nil)
|
|
|
|
const (
|
|
userID = "test-user"
|
|
clientID = "test-client"
|
|
)
|
|
require.NoError(t, db.Create(&model.User{Base: model.Base{ID: userID}}).Error)
|
|
require.NoError(t, db.Create(&model.OidcClient{Base: model.Base{ID: clientID}, Name: "Test Client"}).Error)
|
|
|
|
// A custom permission requested with no resource must be rejected at the
|
|
// authorize endpoint, not displayed on the consent screen.
|
|
requester := newTestAuthorizeRequesterWithForm("bad-scope-request", clientID, url.Values{})
|
|
requester.(*fosite.AuthorizeRequest).RequestedScope = fosite.Arguments{"openid", "users:read"}
|
|
|
|
_, err := service.authorize(t.Context(), authorizeInput{
|
|
userID: userID,
|
|
authenticationTime: time.Now().UTC(),
|
|
requester: requester,
|
|
meta: requestMeta{},
|
|
})
|
|
require.Error(t, err)
|
|
|
|
var rfcErr *fosite.RFC6749Error
|
|
require.ErrorAs(t, err, &rfcErr)
|
|
require.Equal(t, "invalid_scope", rfcErr.ErrorField)
|
|
}
|
|
|
|
// TestAuthorizationServiceCollapsesResourceErrorsBeforeAuthentication guards the pre-authentication resource validation against API enumeration.
|
|
// An unknown API and an API the client is simply not granted are different backend states, but the authorize endpoint runs before the user logs in, so both must surface the exact same generic error; otherwise anyone holding a public client_id could diff the responses to map which API audiences exist and which ones the client may request.
|
|
func TestAuthorizationServiceCollapsesResourceErrorsBeforeAuthentication(t *testing.T) {
|
|
const (
|
|
clientID = "test-client"
|
|
knownAPI = "https://api.orders.example.com"
|
|
unknownAPI = "https://api.unknown.example.com"
|
|
)
|
|
|
|
authorizeWithResource := func(t *testing.T, apiAccess APIAccessProvider, resource string) error {
|
|
t.Helper()
|
|
db := testutils.NewDatabaseForTest(t)
|
|
require.NoError(t, db.Create(&model.OidcClient{Base: model.Base{ID: clientID}, Name: "Test Client"}).Error)
|
|
service := newAuthorizationService(db, newInteractionSessionService(db), newClaimsService(db, nil, "", nil), nil, nil, apiAccess)
|
|
|
|
requester := newTestAuthorizeRequesterWithForm("resource-probe", clientID, url.Values{"resource": {resource}})
|
|
_, err := service.authorize(t.Context(), authorizeInput{
|
|
userID: "test-user",
|
|
authenticationTime: time.Now().UTC(),
|
|
requester: requester,
|
|
})
|
|
return err
|
|
}
|
|
|
|
// The targeted API does not exist at all
|
|
unknownErr := authorizeWithResource(t, userAccess(map[string][]string{}), unknownAPI)
|
|
// The targeted API exists but this client has been granted none of its permissions
|
|
ungrantedErr := authorizeWithResource(t, userAccess(map[string][]string{knownAPI: {}}), knownAPI)
|
|
|
|
require.Error(t, unknownErr)
|
|
require.Error(t, ungrantedErr)
|
|
|
|
var unknownRFC, ungrantedRFC *fosite.RFC6749Error
|
|
require.ErrorAs(t, unknownErr, &unknownRFC)
|
|
require.ErrorAs(t, ungrantedErr, &ungrantedRFC)
|
|
|
|
// Both states collapse to the identical generic error, so an anonymous caller cannot tell an unknown API from an ungranted one
|
|
require.Equal(t, "invalid_request", unknownRFC.ErrorField)
|
|
require.Equal(t, unknownRFC.ErrorField, ungrantedRFC.ErrorField)
|
|
require.Equal(t, unknownRFC.GetDescription(), ungrantedRFC.GetDescription())
|
|
// The ungranted case must not leak fosite's access_denied, which would reveal that the API exists
|
|
require.NotEqual(t, "access_denied", ungrantedRFC.ErrorField)
|
|
}
|
|
|
|
func TestAuthorizationServiceConsentStepLogsNewClientAuthorization(t *testing.T) {
|
|
db := testutils.NewDatabaseForTest(t)
|
|
auditLogger := &fakeAuditLogger{}
|
|
service := newAuthorizationService(db, newInteractionSessionService(db), newClaimsService(db, nil, "", nil), nil, auditLogger, nil)
|
|
|
|
const (
|
|
userID = "test-user"
|
|
clientID = "test-client"
|
|
interactionID = "test-interaction"
|
|
)
|
|
|
|
require.NoError(t, db.Create(&model.User{
|
|
Base: model.Base{ID: userID},
|
|
}).Error)
|
|
require.NoError(t, db.Create(&model.OidcClient{
|
|
Base: model.Base{ID: clientID},
|
|
Name: "Test Client",
|
|
}).Error)
|
|
require.NoError(t, db.Create(&InteractionSession{
|
|
Base: model.Base{ID: interactionID},
|
|
Scopes: datatype.StringList{"openid"},
|
|
ClientID: clientID,
|
|
ConsentRequired: true,
|
|
RequestedAt: datatype.DateTime(time.Now().UTC()),
|
|
Parameters: map[string]string{},
|
|
}).Error)
|
|
|
|
response, err := service.completeInteractionStep(t.Context(), interactionID, userID, interactionStepConsent, "", time.Now().UTC(), requestMeta{})
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, response.RedirectURL)
|
|
|
|
require.Equal(t, []model.AuditLogEvent{model.AuditLogEventNewClientAuthorization}, auditLogger.events)
|
|
require.Equal(t, model.AuditLogData{"clientName": "Test Client"}, auditLogger.data[0])
|
|
}
|
|
|
|
func TestAuthorizationServiceConsentMergesAudienceQualifiedScopeKeys(t *testing.T) {
|
|
db := testutils.NewDatabaseForTest(t)
|
|
auditLogger := &fakeAuditLogger{}
|
|
|
|
const (
|
|
userID = "test-user"
|
|
clientID = "test-client"
|
|
apiA = "https://api-a.example.com"
|
|
apiB = "https://api-b.example.com"
|
|
)
|
|
|
|
service := newAuthorizationService(db, newInteractionSessionService(db), newClaimsService(db, nil, "", nil), nil, auditLogger, userAccess(map[string][]string{
|
|
apiA: {"read"},
|
|
apiB: {"read"},
|
|
}))
|
|
|
|
require.NoError(t, db.Create(&model.User{Base: model.Base{ID: userID}}).Error)
|
|
require.NoError(t, db.Create(&model.OidcClient{
|
|
Base: model.Base{ID: clientID},
|
|
Name: "Test Client",
|
|
}).Error)
|
|
|
|
apiAConsent := consentScopeKeys(apiA, []string{"openid", "read"})
|
|
apiBConsent := consentScopeKeys(apiB, []string{"openid", "read"})
|
|
|
|
hasAlreadyAuthorized, err := service.consent(t.Context(), userID, clientID, apiAConsent)
|
|
require.NoError(t, err)
|
|
require.False(t, hasAlreadyAuthorized)
|
|
|
|
hasAlreadyAuthorized, err = service.consent(t.Context(), userID, clientID, apiBConsent)
|
|
require.NoError(t, err)
|
|
require.False(t, hasAlreadyAuthorized)
|
|
|
|
var authorizedClient model.UserAuthorizedOidcClient
|
|
require.NoError(t, db.First(&authorizedClient, "user_id = ? AND client_id = ?", userID, clientID).Error)
|
|
require.ElementsMatch(t, []string{"openid", consentScopeKey(apiA, "read"), consentScopeKey(apiB, "read")}, authorizedClient.Scope)
|
|
|
|
hasAuthorizedAPIA, err := service.hasAuthorizedClient(t.Context(), clientID, userID, apiAConsent)
|
|
require.NoError(t, err)
|
|
require.True(t, hasAuthorizedAPIA)
|
|
hasAuthorizedAPIB, err := service.hasAuthorizedClient(t.Context(), clientID, userID, apiBConsent)
|
|
require.NoError(t, err)
|
|
require.True(t, hasAuthorizedAPIB)
|
|
|
|
form := url.Values{
|
|
"prompt": {"none"},
|
|
"resource": {apiA},
|
|
}
|
|
requester := newTestAuthorizeRequesterWithForm("silent-api-a-request", clientID, form)
|
|
requester.(*fosite.AuthorizeRequest).RequestedScope = fosite.Arguments{"openid", "read"}
|
|
|
|
authorization, err := service.authorize(t.Context(), authorizeInput{
|
|
userID: userID,
|
|
authenticationTime: time.Now().UTC(),
|
|
requester: requester,
|
|
meta: requestMeta{IPAddress: "203.0.113.1", UserAgent: "test-agent"},
|
|
})
|
|
require.NoError(t, err)
|
|
require.False(t, authorization.RequiresInteraction)
|
|
require.Equal(t, []model.AuditLogEvent{model.AuditLogEventClientAuthorization}, auditLogger.events)
|
|
}
|
|
|
|
func TestAuthorizationServiceAuthorizeConsumesInteractionSession(t *testing.T) {
|
|
db := testutils.NewDatabaseForTest(t)
|
|
service := newAuthorizationService(db, newInteractionSessionService(db), newClaimsService(db, nil, "", nil), nil, nil, nil)
|
|
|
|
const (
|
|
userID = "test-user"
|
|
clientID = "test-client"
|
|
interactionID = "test-interaction"
|
|
)
|
|
|
|
require.NoError(t, db.Create(&model.User{
|
|
Base: model.Base{ID: userID},
|
|
}).Error)
|
|
require.NoError(t, db.Create(&model.OidcClient{
|
|
Base: model.Base{ID: clientID},
|
|
Name: "Test Client",
|
|
}).Error)
|
|
require.NoError(t, db.Create(&model.UserAuthorizedOidcClient{
|
|
UserID: userID,
|
|
ClientID: clientID,
|
|
Scope: datatype.StringList{"openid"},
|
|
}).Error)
|
|
require.NoError(t, db.Create(&InteractionSession{
|
|
Base: model.Base{ID: interactionID},
|
|
Scopes: datatype.StringList{"openid"},
|
|
ClientID: clientID,
|
|
RequestedAt: datatype.DateTime(time.Now().UTC()),
|
|
Parameters: map[string]string{},
|
|
}).Error)
|
|
|
|
authorize := func() (authorizationResult, error) {
|
|
return service.authorize(t.Context(), authorizeInput{
|
|
userID: userID,
|
|
authenticationTime: time.Now().UTC(),
|
|
requester: newTestAuthorizeRequester("consume-request", clientID, ""),
|
|
interactionID: interactionID,
|
|
})
|
|
}
|
|
|
|
authorization, err := authorize()
|
|
require.NoError(t, err)
|
|
require.False(t, authorization.RequiresInteraction)
|
|
|
|
// The interaction session is consumed together with the grant and must be single-use
|
|
var count int64
|
|
require.NoError(t, db.Model(&InteractionSession{}).Where("id = ?", interactionID).Count(&count).Error)
|
|
require.Zero(t, count)
|
|
|
|
_, err = authorize()
|
|
require.ErrorIs(t, err, fosite.ErrInvalidRequest)
|
|
}
|
|
|
|
func TestInteractionSessionServiceGetRejectsExpiredSession(t *testing.T) {
|
|
db := testutils.NewDatabaseForTest(t)
|
|
service := newInteractionSessionService(db)
|
|
|
|
const (
|
|
clientID = "test-client"
|
|
interactionID = "test-interaction"
|
|
)
|
|
|
|
require.NoError(t, db.Create(&model.OidcClient{
|
|
Base: model.Base{ID: clientID},
|
|
Name: "Test Client",
|
|
}).Error)
|
|
require.NoError(t, db.Create(&InteractionSession{
|
|
Base: model.Base{ID: interactionID},
|
|
Scopes: datatype.StringList{"openid"},
|
|
ClientID: clientID,
|
|
RequestedAt: datatype.DateTime(time.Now().UTC()),
|
|
Parameters: map[string]string{},
|
|
}).Error)
|
|
|
|
_, err := service.get(t.Context(), interactionID)
|
|
require.NoError(t, err)
|
|
|
|
// Backdate the session past its lifetime; BeforeCreate stamps CreatedAt, so update directly
|
|
expiredCreatedAt := datatype.DateTime(time.Now().Add(-interactionSessionLifetime - time.Minute))
|
|
require.NoError(t, db.Model(&InteractionSession{}).Where("id = ?", interactionID).Update("created_at", expiredCreatedAt).Error)
|
|
|
|
_, err = service.get(t.Context(), interactionID)
|
|
require.ErrorIs(t, err, gorm.ErrRecordNotFound)
|
|
}
|
|
|
|
func TestAuthorizationServiceAuthorizeBindsScopesToInteractionSession(t *testing.T) {
|
|
db := testutils.NewDatabaseForTest(t)
|
|
service := newAuthorizationService(db, newInteractionSessionService(db), newClaimsService(db, nil, "", nil), nil, nil, nil)
|
|
|
|
const (
|
|
userID = "test-user"
|
|
clientID = "test-client"
|
|
)
|
|
|
|
require.NoError(t, db.Create(&model.User{
|
|
Base: model.Base{ID: userID},
|
|
}).Error)
|
|
require.NoError(t, db.Create(&model.OidcClient{
|
|
Base: model.Base{ID: clientID},
|
|
Name: "Test Client",
|
|
}).Error)
|
|
|
|
newInteractionSession := func(id string) {
|
|
require.NoError(t, db.Create(&InteractionSession{
|
|
Base: model.Base{ID: id},
|
|
Scopes: datatype.StringList{"openid"},
|
|
ClientID: clientID,
|
|
RequestedAt: datatype.DateTime(time.Now().UTC()),
|
|
Parameters: map[string]string{},
|
|
}).Error)
|
|
}
|
|
|
|
authorize := func(interactionID string, scopes ...string) (authorizationResult, error) {
|
|
requester := newTestAuthorizeRequester("scope-binding-request", clientID, "")
|
|
requester.(*fosite.AuthorizeRequest).RequestedScope = fosite.Arguments(scopes)
|
|
return service.authorize(t.Context(), authorizeInput{
|
|
userID: userID,
|
|
authenticationTime: time.Now().UTC(),
|
|
requester: requester,
|
|
interactionID: interactionID,
|
|
})
|
|
}
|
|
|
|
// Scopes matching the consented interaction session are granted
|
|
newInteractionSession("matching-interaction")
|
|
authorization, err := authorize("matching-interaction", "openid")
|
|
require.NoError(t, err)
|
|
require.False(t, authorization.RequiresInteraction)
|
|
|
|
// Scopes added to the URL after consent must not be granted silently
|
|
newInteractionSession("escalated-interaction")
|
|
_, err = authorize("escalated-interaction", "openid", "profile", "email", "groups")
|
|
require.ErrorIs(t, err, fosite.ErrInvalidRequest)
|
|
|
|
// The user must not have been recorded as having authorized the escalated scopes
|
|
var authorizedClient model.UserAuthorizedOidcClient
|
|
require.NoError(t, db.First(&authorizedClient, "user_id = ? AND client_id = ?", userID, clientID).Error)
|
|
require.Equal(t, datatype.StringList{"openid"}, authorizedClient.Scope)
|
|
}
|
|
|
|
func TestAuthorizationServiceAuthorizeRejectsInteractionSessionOfOtherClient(t *testing.T) {
|
|
db := testutils.NewDatabaseForTest(t)
|
|
service := newAuthorizationService(db, newInteractionSessionService(db), newClaimsService(db, nil, "", nil), nil, nil, nil)
|
|
|
|
const (
|
|
userID = "test-user"
|
|
clientID = "test-client"
|
|
otherClientID = "other-client"
|
|
interactionID = "test-interaction"
|
|
)
|
|
|
|
require.NoError(t, db.Create(&model.User{
|
|
Base: model.Base{ID: userID},
|
|
}).Error)
|
|
require.NoError(t, db.Create(&model.OidcClient{
|
|
Base: model.Base{ID: clientID},
|
|
Name: "Test Client",
|
|
}).Error)
|
|
require.NoError(t, db.Create(&model.OidcClient{
|
|
Base: model.Base{ID: otherClientID},
|
|
Name: "Other Client",
|
|
}).Error)
|
|
require.NoError(t, db.Create(&InteractionSession{
|
|
Base: model.Base{ID: interactionID},
|
|
Scopes: datatype.StringList{"openid"},
|
|
ClientID: clientID,
|
|
RequestedAt: datatype.DateTime(time.Now().UTC()),
|
|
Parameters: map[string]string{},
|
|
}).Error)
|
|
|
|
_, err := service.authorize(t.Context(), authorizeInput{
|
|
userID: userID,
|
|
authenticationTime: time.Now().UTC(),
|
|
requester: newTestAuthorizeRequester("other-client-request", otherClientID, ""),
|
|
interactionID: interactionID,
|
|
})
|
|
require.ErrorIs(t, err, fosite.ErrInvalidRequest)
|
|
}
|
|
|
|
func TestAuthorizationServiceAuthorizeSwitchesUserAndResetsRequirements(t *testing.T) {
|
|
db := testutils.NewDatabaseForTest(t)
|
|
service := newAuthorizationService(db, newInteractionSessionService(db), newClaimsService(db, nil, "", nil), nil, nil, nil)
|
|
|
|
const (
|
|
userID = "test-user"
|
|
otherUserID = "other-user"
|
|
clientID = "test-client"
|
|
interactionID = "test-interaction"
|
|
)
|
|
|
|
require.NoError(t, db.Create(&model.User{
|
|
Base: model.Base{ID: userID},
|
|
Username: "test-user",
|
|
}).Error)
|
|
require.NoError(t, db.Create(&model.User{
|
|
Base: model.Base{ID: otherUserID},
|
|
Username: "other-user",
|
|
}).Error)
|
|
require.NoError(t, db.Create(&model.OidcClient{
|
|
Base: model.Base{ID: clientID},
|
|
Name: "Test Client",
|
|
}).Error)
|
|
require.NoError(t, db.Create(&model.UserAuthorizedOidcClient{
|
|
UserID: userID,
|
|
ClientID: clientID,
|
|
Scope: datatype.StringList{"openid"},
|
|
}).Error)
|
|
require.NoError(t, db.Create(&InteractionSession{
|
|
Base: model.Base{ID: interactionID},
|
|
Scopes: datatype.StringList{"openid"},
|
|
ClientID: clientID,
|
|
UserID: stringPointer(userID),
|
|
ReauthenticatedAt: new(datatype.DateTime(time.Now().Add(-time.Minute).UTC())),
|
|
ReauthenticationRequired: false,
|
|
RequestedAt: datatype.DateTime(time.Now().UTC()),
|
|
Parameters: map[string]string{
|
|
"prompt": "login",
|
|
},
|
|
}).Error)
|
|
|
|
authorization, err := service.authorize(t.Context(), authorizeInput{
|
|
userID: otherUserID,
|
|
authenticationTime: time.Now().UTC(),
|
|
requester: newTestAuthorizeRequester("other-user-request", clientID, ""),
|
|
interactionID: interactionID,
|
|
})
|
|
require.NoError(t, err)
|
|
require.True(t, authorization.RequiresInteraction)
|
|
require.Equal(t, interactionID, authorization.InteractionID)
|
|
|
|
var interactionSession InteractionSession
|
|
require.NoError(t, db.First(&interactionSession, "id = ?", interactionID).Error)
|
|
require.NotNil(t, interactionSession.UserID)
|
|
require.Equal(t, otherUserID, *interactionSession.UserID)
|
|
require.True(t, interactionSession.ConsentRequired)
|
|
require.True(t, interactionSession.ReauthenticationRequired)
|
|
require.Nil(t, interactionSession.ReauthenticatedAt)
|
|
}
|
|
|
|
func TestAuthorizationServiceAuthorizeRequiresLoginForUserBoundInteraction(t *testing.T) {
|
|
db := testutils.NewDatabaseForTest(t)
|
|
service := newAuthorizationService(db, newInteractionSessionService(db), newClaimsService(db, nil, "", nil), nil, nil, nil)
|
|
|
|
const (
|
|
userID = "test-user"
|
|
clientID = "test-client"
|
|
interactionID = "test-interaction"
|
|
)
|
|
|
|
require.NoError(t, db.Create(&model.User{
|
|
Base: model.Base{ID: userID},
|
|
}).Error)
|
|
require.NoError(t, db.Create(&model.OidcClient{
|
|
Base: model.Base{ID: clientID},
|
|
Name: "Test Client",
|
|
}).Error)
|
|
require.NoError(t, db.Create(&InteractionSession{
|
|
Base: model.Base{ID: interactionID},
|
|
Scopes: datatype.StringList{"openid"},
|
|
ClientID: clientID,
|
|
UserID: stringPointer(userID),
|
|
RequestedAt: datatype.DateTime(time.Now().UTC()),
|
|
Parameters: map[string]string{},
|
|
}).Error)
|
|
|
|
_, err := service.authorize(t.Context(), authorizeInput{
|
|
requester: newTestAuthorizeRequester("unauthenticated-bound-request", clientID, ""),
|
|
interactionID: interactionID,
|
|
})
|
|
require.ErrorIs(t, err, fosite.ErrLoginRequired)
|
|
}
|
|
|
|
func TestAuthorizationServiceCompleteInteractionBindsUserToSession(t *testing.T) {
|
|
db := testutils.NewDatabaseForTest(t)
|
|
service := newAuthorizationService(db, newInteractionSessionService(db), newClaimsService(db, nil, "", nil), nil, nil, nil)
|
|
|
|
const (
|
|
userID = "test-user"
|
|
clientID = "test-client"
|
|
interactionID = "test-interaction"
|
|
)
|
|
|
|
require.NoError(t, db.Create(&model.User{
|
|
Base: model.Base{ID: userID},
|
|
}).Error)
|
|
require.NoError(t, db.Create(&model.OidcClient{
|
|
Base: model.Base{ID: clientID},
|
|
Name: "Test Client",
|
|
}).Error)
|
|
require.NoError(t, db.Create(&InteractionSession{
|
|
Base: model.Base{ID: interactionID},
|
|
Scopes: datatype.StringList{"openid"},
|
|
ClientID: clientID,
|
|
ConsentRequired: true,
|
|
RequestedAt: datatype.DateTime(time.Now().UTC()),
|
|
Parameters: map[string]string{},
|
|
}).Error)
|
|
|
|
response, err := service.completeInteractionStep(t.Context(), interactionID, userID, interactionStepConsent, "", time.Now().UTC(), requestMeta{})
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, response.RedirectURL)
|
|
|
|
var interactionSession InteractionSession
|
|
require.NoError(t, db.First(&interactionSession, "id = ?", interactionID).Error)
|
|
require.NotNil(t, interactionSession.UserID)
|
|
require.Equal(t, userID, *interactionSession.UserID)
|
|
}
|
|
|
|
func TestAuthorizationServiceCompleteInteractionSwitchesUserAndResetsRequirements(t *testing.T) {
|
|
db := testutils.NewDatabaseForTest(t)
|
|
service := newAuthorizationService(db, newInteractionSessionService(db), newClaimsService(db, nil, "", nil), nil, nil, nil)
|
|
|
|
const (
|
|
userID = "test-user"
|
|
otherUserID = "other-user"
|
|
clientID = "test-client"
|
|
interactionID = "test-interaction"
|
|
)
|
|
|
|
require.NoError(t, db.Create(&model.User{
|
|
Base: model.Base{ID: userID},
|
|
Username: "test-user",
|
|
}).Error)
|
|
require.NoError(t, db.Create(&model.User{
|
|
Base: model.Base{ID: otherUserID},
|
|
Username: "other-user",
|
|
}).Error)
|
|
require.NoError(t, db.Create(&model.OidcClient{
|
|
Base: model.Base{ID: clientID},
|
|
Name: "Test Client",
|
|
}).Error)
|
|
require.NoError(t, db.Create(&InteractionSession{
|
|
Base: model.Base{ID: interactionID},
|
|
Scopes: datatype.StringList{"openid"},
|
|
ClientID: clientID,
|
|
UserID: stringPointer(userID),
|
|
AccountSelectionRequired: true,
|
|
RequestedAt: datatype.DateTime(time.Now().UTC()),
|
|
Parameters: map[string]string{
|
|
"prompt": "select_account",
|
|
},
|
|
}).Error)
|
|
|
|
response, err := service.completeInteractionStep(t.Context(), interactionID, otherUserID, interactionStepSelectAccount, "", time.Now().UTC(), requestMeta{})
|
|
require.NoError(t, err)
|
|
require.Empty(t, response.RedirectURL)
|
|
require.NotNil(t, response.Interaction)
|
|
require.Equal(t, interactionStepConsent, response.Interaction.CurrentStep)
|
|
|
|
var interactionSession InteractionSession
|
|
require.NoError(t, db.First(&interactionSession, "id = ?", interactionID).Error)
|
|
require.NotNil(t, interactionSession.UserID)
|
|
require.Equal(t, otherUserID, *interactionSession.UserID)
|
|
require.False(t, interactionSession.AccountSelectionRequired)
|
|
require.True(t, interactionSession.ConsentRequired)
|
|
}
|
|
|
|
// TestAuthorizationServiceSelectAccountRecomputesConsentForSelectedUser ensures consent is
|
|
// evaluated for the FINAL selected user. When an already-consented user starts
|
|
// prompt=select_account and a different, not-yet-consented user is selected, consent must
|
|
// still be required for that user rather than being inherited from the initiator.
|
|
func TestAuthorizationServiceSelectAccountRecomputesConsentForSelectedUser(t *testing.T) {
|
|
db := testutils.NewDatabaseForTest(t)
|
|
service := newAuthorizationService(db, newInteractionSessionService(db), newClaimsService(db, nil, "", nil), nil, nil, nil)
|
|
|
|
const (
|
|
initiatorID = "initiator-user"
|
|
switchedID = "switched-user"
|
|
clientID = "test-client"
|
|
)
|
|
|
|
require.NoError(t, db.Create(&model.User{Base: model.Base{ID: initiatorID}, Username: "initiator"}).Error)
|
|
require.NoError(t, db.Create(&model.User{Base: model.Base{ID: switchedID}, Username: "switched"}).Error)
|
|
require.NoError(t, db.Create(&model.OidcClient{Base: model.Base{ID: clientID}, Name: "Test Client"}).Error)
|
|
|
|
// The initiator has previously consented to the client; the switched-to user has not.
|
|
require.NoError(t, db.Create(&model.UserAuthorizedOidcClient{
|
|
UserID: initiatorID,
|
|
ClientID: clientID,
|
|
Scope: datatype.StringList{"openid"},
|
|
}).Error)
|
|
|
|
authorization, err := service.authorize(t.Context(), authorizeInput{
|
|
userID: initiatorID,
|
|
authenticationTime: time.Now().UTC(),
|
|
requester: newTestAuthorizeRequester("select-account-request", clientID, "select_account"),
|
|
requestParams: map[string]string{"prompt": "select_account"},
|
|
})
|
|
require.NoError(t, err)
|
|
require.True(t, authorization.RequiresInteraction)
|
|
|
|
response, err := service.completeInteractionStep(t.Context(), authorization.InteractionID, switchedID, interactionStepSelectAccount, "", time.Now().UTC(), requestMeta{})
|
|
require.NoError(t, err)
|
|
|
|
// The flow must not be granted yet: consent is still pending for the switched-to user.
|
|
require.Empty(t, response.RedirectURL)
|
|
require.NotNil(t, response.Interaction)
|
|
require.Equal(t, interactionStepConsent, response.Interaction.CurrentStep)
|
|
|
|
// No authorization record may have been silently created for the switched-to user.
|
|
var count int64
|
|
require.NoError(t, db.Model(&model.UserAuthorizedOidcClient{}).
|
|
Where("user_id = ? AND client_id = ?", switchedID, clientID).
|
|
Count(&count).Error)
|
|
require.Zero(t, count)
|
|
}
|
|
|
|
func TestValidateClientPKCERequirement(t *testing.T) {
|
|
pkceClient := Client{OidcClient: model.OidcClient{Base: model.Base{ID: "c"}, PkceEnabled: true}}
|
|
plainClient := Client{OidcClient: model.OidcClient{Base: model.Base{ID: "c"}}}
|
|
|
|
withChallenge := newTestAuthorizeRequesterWithForm("with-challenge", "c", url.Values{"code_challenge": {"abc123"}})
|
|
withoutChallenge := newTestAuthorizeRequesterWithForm("without-challenge", "c", url.Values{})
|
|
|
|
require.NoError(t, validateClientPKCERequirement(plainClient, withoutChallenge))
|
|
require.NoError(t, validateClientPKCERequirement(pkceClient, withChallenge))
|
|
|
|
err := validateClientPKCERequirement(pkceClient, withoutChallenge)
|
|
require.ErrorIs(t, err, fosite.ErrInvalidRequest)
|
|
}
|
|
|
|
// TestAuthorizationServiceAuthorizeEnforcesPerClientPKCE proves the per-client PkceEnabled
|
|
// flag is honored for confidential clients (fosite only enforces PKCE for public clients).
|
|
func TestAuthorizationServiceAuthorizeEnforcesPerClientPKCE(t *testing.T) {
|
|
db := testutils.NewDatabaseForTest(t)
|
|
service := newAuthorizationService(db, newInteractionSessionService(db), newClaimsService(db, nil, "", nil), nil, nil, nil)
|
|
|
|
const (
|
|
userID = "test-user"
|
|
clientID = "pkce-client"
|
|
)
|
|
require.NoError(t, db.Create(&model.User{Base: model.Base{ID: userID}, Username: "test-user"}).Error)
|
|
require.NoError(t, db.Create(&model.OidcClient{Base: model.Base{ID: clientID}, Name: "PKCE Client", PkceEnabled: true}).Error)
|
|
require.NoError(t, db.Create(&model.UserAuthorizedOidcClient{UserID: userID, ClientID: clientID, Scope: datatype.StringList{"openid"}}).Error)
|
|
|
|
pkceClient := Client{OidcClient: model.OidcClient{Base: model.Base{ID: clientID}, Name: "PKCE Client", PkceEnabled: true}}
|
|
|
|
// Without a code_challenge the request is rejected before any interaction.
|
|
missing := newTestAuthorizeRequesterWithForm("authz-no-pkce", clientID, url.Values{})
|
|
missing.(*fosite.AuthorizeRequest).Client = pkceClient
|
|
_, err := service.authorize(t.Context(), authorizeInput{
|
|
userID: userID,
|
|
authenticationTime: time.Now().UTC(),
|
|
requester: missing,
|
|
})
|
|
require.ErrorIs(t, err, fosite.ErrInvalidRequest)
|
|
|
|
// With a code_challenge the PKCE gate passes and the (already-consented) request is granted.
|
|
withChallenge := newTestAuthorizeRequesterWithForm("authz-pkce", clientID, url.Values{"code_challenge": {"abc123"}})
|
|
withChallenge.(*fosite.AuthorizeRequest).Client = pkceClient
|
|
result, err := service.authorize(t.Context(), authorizeInput{
|
|
userID: userID,
|
|
authenticationTime: time.Now().UTC(),
|
|
requester: withChallenge,
|
|
})
|
|
require.NoError(t, err)
|
|
require.False(t, result.RequiresInteraction)
|
|
}
|
|
|
|
func TestAuthorizationServiceAuthorizePARRequiredClient(t *testing.T) {
|
|
db := testutils.NewDatabaseForTest(t)
|
|
service := newAuthorizationService(db, newInteractionSessionService(db), newClaimsService(db, nil, "", nil), nil, nil, nil)
|
|
|
|
const (
|
|
userID = "test-user"
|
|
clientID = "test-client"
|
|
interactionID = "test-interaction"
|
|
)
|
|
|
|
require.NoError(t, db.Create(&model.User{
|
|
Base: model.Base{ID: userID},
|
|
}).Error)
|
|
require.NoError(t, db.Create(&model.OidcClient{
|
|
Base: model.Base{ID: clientID},
|
|
Name: "Test Client",
|
|
RequiresPushedAuthorizationRequests: true,
|
|
}).Error)
|
|
require.NoError(t, db.Create(&model.UserAuthorizedOidcClient{
|
|
UserID: userID,
|
|
ClientID: clientID,
|
|
Scope: datatype.StringList{"openid"},
|
|
}).Error)
|
|
|
|
authorize := func(interactionID string, hasPushedAuthorizationRequest bool) (authorizationResult, error) {
|
|
requester := newTestAuthorizeRequester("par-request", clientID, "")
|
|
requester.(*fosite.AuthorizeRequest).Client = Client{
|
|
OidcClient: model.OidcClient{
|
|
Base: model.Base{ID: clientID},
|
|
Name: "Test Client",
|
|
RequiresPushedAuthorizationRequests: true,
|
|
},
|
|
}
|
|
return service.authorize(t.Context(), authorizeInput{
|
|
userID: userID,
|
|
authenticationTime: time.Now().UTC(),
|
|
requester: requester,
|
|
hasPushedAuthorizationRequest: hasPushedAuthorizationRequest,
|
|
interactionID: interactionID,
|
|
})
|
|
}
|
|
|
|
// Without a pushed authorization request the client must be rejected
|
|
_, err := authorize("", false)
|
|
var parRequiredError *common.OidcPARRequiredError
|
|
require.ErrorAs(t, err, &parRequiredError)
|
|
|
|
// Re-entry after a completed interaction carries no request_uri, but the bound
|
|
// interaction session proves the original request was PAR-validated
|
|
require.NoError(t, db.Create(&InteractionSession{
|
|
Base: model.Base{ID: interactionID},
|
|
Scopes: datatype.StringList{"openid"},
|
|
ClientID: clientID,
|
|
RequestedAt: datatype.DateTime(time.Now().UTC()),
|
|
Parameters: map[string]string{},
|
|
}).Error)
|
|
|
|
authorization, err := authorize(interactionID, false)
|
|
require.NoError(t, err)
|
|
require.False(t, authorization.RequiresInteraction)
|
|
|
|
// An unknown interaction ID must not satisfy the PAR requirement
|
|
_, err = authorize("nonexistent", false)
|
|
require.ErrorIs(t, err, fosite.ErrInvalidRequest)
|
|
}
|
|
|
|
func TestAuthorizationServiceInteractionRequestQuery(t *testing.T) {
|
|
db := testutils.NewDatabaseForTest(t)
|
|
service := newAuthorizationService(db, newInteractionSessionService(db), newClaimsService(db, nil, "", nil), nil, nil, nil)
|
|
|
|
const (
|
|
clientID = "test-client"
|
|
interactionID = "test-interaction"
|
|
)
|
|
|
|
require.NoError(t, db.Create(&model.OidcClient{
|
|
Base: model.Base{ID: clientID},
|
|
Name: "Test Client",
|
|
}).Error)
|
|
require.NoError(t, db.Create(&InteractionSession{
|
|
Base: model.Base{ID: interactionID},
|
|
Scopes: datatype.StringList{"openid"},
|
|
ClientID: clientID,
|
|
RequestedAt: datatype.DateTime(time.Now().UTC()),
|
|
Parameters: map[string]string{
|
|
"client_id": clientID,
|
|
"response_type": "code",
|
|
"scope": "openid",
|
|
"redirect_uri": "https://client.example/callback",
|
|
"state": "test-state",
|
|
},
|
|
}).Error)
|
|
|
|
query, err := service.interactionRequestQuery(t.Context(), interactionID)
|
|
require.NoError(t, err)
|
|
require.Equal(t, clientID, query.Get("client_id"))
|
|
require.Equal(t, "code", query.Get("response_type"))
|
|
require.Equal(t, "openid", query.Get("scope"))
|
|
require.Equal(t, "https://client.example/callback", query.Get("redirect_uri"))
|
|
require.Equal(t, "test-state", query.Get("state"))
|
|
require.Equal(t, interactionID, query.Get("interaction"))
|
|
|
|
_, err = service.interactionRequestQuery(t.Context(), "nonexistent")
|
|
require.ErrorIs(t, err, fosite.ErrInvalidRequest)
|
|
}
|
|
|
|
func TestAuthorizationServiceAuthorizeUsesLoginAuthenticationTime(t *testing.T) {
|
|
db := testutils.NewDatabaseForTest(t)
|
|
service := newAuthorizationService(db, newInteractionSessionService(db), newClaimsService(db, nil, "", nil), nil, nil, nil)
|
|
|
|
const (
|
|
userID = "test-user"
|
|
clientID = "test-client"
|
|
)
|
|
loginTime := time.Now().Add(-10 * time.Minute).UTC().Truncate(time.Second)
|
|
|
|
require.NoError(t, db.Create(&model.User{
|
|
Base: model.Base{ID: userID},
|
|
}).Error)
|
|
require.NoError(t, db.Create(&model.OidcClient{
|
|
Base: model.Base{ID: clientID},
|
|
Name: "Test Client",
|
|
}).Error)
|
|
require.NoError(t, db.Create(&model.UserAuthorizedOidcClient{
|
|
UserID: userID,
|
|
ClientID: clientID,
|
|
Scope: datatype.StringList{"openid"},
|
|
}).Error)
|
|
|
|
firstAuthorization, err := service.authorize(t.Context(), authorizeInput{
|
|
userID: userID,
|
|
authenticationTime: loginTime,
|
|
requester: newTestAuthorizeRequester("first-request", clientID, ""),
|
|
})
|
|
require.NoError(t, err)
|
|
require.False(t, firstAuthorization.RequiresInteraction)
|
|
require.Equal(t, loginTime, firstAuthorization.Session.IDTokenClaims().AuthTime)
|
|
|
|
secondAuthorization, err := service.authorize(t.Context(), authorizeInput{
|
|
userID: userID,
|
|
authenticationTime: loginTime,
|
|
requester: newTestAuthorizeRequester("second-request", clientID, "none"),
|
|
})
|
|
require.NoError(t, err)
|
|
require.False(t, secondAuthorization.RequiresInteraction)
|
|
require.Equal(t, loginTime, secondAuthorization.Session.IDTokenClaims().AuthTime)
|
|
}
|
|
|
|
func TestAuthorizationServiceAuthorizeRequiresReauthenticationWhenMaxAgeExceeded(t *testing.T) {
|
|
db := testutils.NewDatabaseForTest(t)
|
|
service := newAuthorizationService(db, newInteractionSessionService(db), newClaimsService(db, nil, "", nil), nil, nil, nil)
|
|
|
|
const (
|
|
userID = "test-user"
|
|
clientID = "test-client"
|
|
)
|
|
loginTime := time.Now().Add(-2 * time.Minute).UTC()
|
|
|
|
require.NoError(t, db.Create(&model.User{
|
|
Base: model.Base{ID: userID},
|
|
}).Error)
|
|
require.NoError(t, db.Create(&model.OidcClient{
|
|
Base: model.Base{ID: clientID},
|
|
Name: "Test Client",
|
|
}).Error)
|
|
require.NoError(t, db.Create(&model.UserAuthorizedOidcClient{
|
|
UserID: userID,
|
|
ClientID: clientID,
|
|
Scope: datatype.StringList{"openid"},
|
|
}).Error)
|
|
|
|
authorization, err := service.authorize(t.Context(), authorizeInput{
|
|
userID: userID,
|
|
authenticationTime: loginTime,
|
|
requester: newTestAuthorizeRequesterWithForm("max-age-request", clientID, url.Values{"max_age": {"1"}}),
|
|
requestParams: map[string]string{"max_age": "1"},
|
|
})
|
|
require.NoError(t, err)
|
|
require.True(t, authorization.RequiresInteraction)
|
|
|
|
interaction, err := service.getInteractionSession(t.Context(), authorization.InteractionID)
|
|
require.NoError(t, err)
|
|
require.Equal(t, interactionStepReauthenticate, interaction.CurrentStep)
|
|
}
|
|
|
|
func TestAuthorizationServiceAuthorizeUsesCompletedReauthenticationTime(t *testing.T) {
|
|
db := testutils.NewDatabaseForTest(t)
|
|
service := newAuthorizationService(db, newInteractionSessionService(db), newClaimsService(db, nil, "", nil), nil, nil, nil)
|
|
|
|
const (
|
|
userID = "test-user"
|
|
clientID = "test-client"
|
|
interactionID = "test-interaction"
|
|
)
|
|
loginTime := time.Now().Add(-2 * time.Minute).UTC()
|
|
reauthenticatedAt := time.Now().Add(-1 * time.Second).UTC().Truncate(time.Second)
|
|
|
|
require.NoError(t, db.Create(&model.User{
|
|
Base: model.Base{ID: userID},
|
|
}).Error)
|
|
require.NoError(t, db.Create(&model.OidcClient{
|
|
Base: model.Base{ID: clientID},
|
|
Name: "Test Client",
|
|
}).Error)
|
|
require.NoError(t, db.Create(&model.UserAuthorizedOidcClient{
|
|
UserID: userID,
|
|
ClientID: clientID,
|
|
Scope: datatype.StringList{"openid"},
|
|
}).Error)
|
|
require.NoError(t, db.Create(&InteractionSession{
|
|
Base: model.Base{ID: interactionID},
|
|
Scopes: datatype.StringList{"openid"},
|
|
ClientID: clientID,
|
|
ReauthenticationRequired: false,
|
|
ReauthenticatedAt: new(datatype.DateTime(reauthenticatedAt)),
|
|
Parameters: map[string]string{
|
|
"max_age": "1",
|
|
},
|
|
}).Error)
|
|
|
|
authorization, err := service.authorize(t.Context(), authorizeInput{
|
|
userID: userID,
|
|
authenticationTime: loginTime,
|
|
requester: newTestAuthorizeRequesterWithForm("final-request", clientID, url.Values{"max_age": {"1"}}),
|
|
interactionID: interactionID,
|
|
requestParams: map[string]string{"max_age": "1"},
|
|
})
|
|
require.NoError(t, err)
|
|
require.False(t, authorization.RequiresInteraction)
|
|
require.Equal(t, reauthenticatedAt, authorization.Session.IDTokenClaims().AuthTime)
|
|
}
|
|
|
|
func TestAuthorizationServiceAuthorizeUsesOriginalInteractionRequestTime(t *testing.T) {
|
|
db := testutils.NewDatabaseForTest(t)
|
|
service := newAuthorizationService(db, newInteractionSessionService(db), newClaimsService(db, nil, "", nil), nil, nil, nil)
|
|
|
|
const (
|
|
userID = "test-user"
|
|
clientID = "test-client"
|
|
interactionID = "test-interaction"
|
|
)
|
|
originalRequestedAt := time.Now().Add(-10 * time.Second).UTC().Truncate(time.Second)
|
|
reauthenticatedAt := originalRequestedAt.Add(5 * time.Second)
|
|
continuationRequestedAt := reauthenticatedAt.Add(5 * time.Second)
|
|
|
|
require.NoError(t, db.Create(&model.User{
|
|
Base: model.Base{ID: userID},
|
|
}).Error)
|
|
require.NoError(t, db.Create(&model.OidcClient{
|
|
Base: model.Base{ID: clientID},
|
|
Name: "Test Client",
|
|
}).Error)
|
|
require.NoError(t, db.Create(&model.UserAuthorizedOidcClient{
|
|
UserID: userID,
|
|
ClientID: clientID,
|
|
Scope: datatype.StringList{"openid"},
|
|
}).Error)
|
|
require.NoError(t, db.Create(&InteractionSession{
|
|
Base: model.Base{ID: interactionID},
|
|
Scopes: datatype.StringList{"openid"},
|
|
ClientID: clientID,
|
|
ReauthenticationRequired: false,
|
|
RequestedAt: datatype.DateTime(originalRequestedAt),
|
|
ReauthenticatedAt: new(datatype.DateTime(reauthenticatedAt)),
|
|
Parameters: map[string]string{
|
|
"prompt": "login",
|
|
},
|
|
}).Error)
|
|
|
|
requester := newTestAuthorizeRequesterWithForm(
|
|
"final-request",
|
|
clientID,
|
|
url.Values{"prompt": {"login"}},
|
|
)
|
|
requester.(*fosite.AuthorizeRequest).RequestedAt = continuationRequestedAt
|
|
|
|
authorization, err := service.authorize(t.Context(), authorizeInput{
|
|
userID: userID,
|
|
authenticationTime: originalRequestedAt.Add(-time.Minute),
|
|
requester: requester,
|
|
interactionID: interactionID,
|
|
requestParams: map[string]string{"prompt": "login"},
|
|
})
|
|
require.NoError(t, err)
|
|
require.False(t, authorization.RequiresInteraction)
|
|
require.Equal(t, originalRequestedAt, authorization.Session.IDTokenClaims().RequestedAt)
|
|
require.Equal(t, reauthenticatedAt, authorization.Session.IDTokenClaims().AuthTime)
|
|
require.False(t, authorization.Session.IDTokenClaims().AuthTime.Before(authorization.Session.IDTokenClaims().RequestedAt))
|
|
}
|
|
|
|
func TestInteractionSessionServiceSavePersistsParameters(t *testing.T) {
|
|
db := testutils.NewDatabaseForTest(t)
|
|
service := newInteractionSessionService(db)
|
|
|
|
const (
|
|
clientID = "test-client"
|
|
interactionID = "test-interaction"
|
|
)
|
|
|
|
require.NoError(t, db.Create(&model.OidcClient{
|
|
Base: model.Base{ID: clientID},
|
|
Name: "Test Client",
|
|
}).Error)
|
|
|
|
interactionSession, err := service.create(t.Context(), InteractionSession{
|
|
Base: model.Base{ID: interactionID},
|
|
Scopes: datatype.StringList{"openid"},
|
|
ClientID: clientID,
|
|
ReauthenticationRequired: true,
|
|
RequestedAt: datatype.DateTime(time.Now().UTC()),
|
|
Parameters: map[string]string{
|
|
"max_age": "1",
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
reauthenticatedAt := datatype.DateTime(time.Now().UTC().Truncate(time.Second))
|
|
interactionSession.ReauthenticationRequired = false
|
|
interactionSession.ReauthenticatedAt = &reauthenticatedAt
|
|
require.NoError(t, service.update(t.Context(), interactionSession))
|
|
|
|
storedInteractionSession, err := service.get(t.Context(), interactionID)
|
|
require.NoError(t, err)
|
|
require.False(t, storedInteractionSession.ReauthenticationRequired)
|
|
require.Equal(t, "1", storedInteractionSession.Parameters["max_age"])
|
|
require.NotNil(t, storedInteractionSession.ReauthenticatedAt)
|
|
require.Equal(t, reauthenticatedAt.UTC(), storedInteractionSession.ReauthenticatedAt.UTC())
|
|
}
|
|
|
|
func newTestAuthorizeRequester(requestID, clientID, prompt string) fosite.AuthorizeRequester {
|
|
form := url.Values{}
|
|
if prompt != "" {
|
|
form.Set("prompt", prompt)
|
|
}
|
|
return newTestAuthorizeRequesterWithForm(requestID, clientID, form)
|
|
}
|
|
|
|
func newTestAuthorizeRequesterWithForm(requestID, clientID string, form url.Values) fosite.AuthorizeRequester {
|
|
requester := fosite.NewAuthorizeRequest()
|
|
requester.ID = requestID
|
|
requester.RequestedAt = time.Now().UTC()
|
|
requester.Client = Client{
|
|
OidcClient: model.OidcClient{
|
|
Base: model.Base{ID: clientID},
|
|
Name: "Test Client",
|
|
},
|
|
}
|
|
requester.RequestedScope = fosite.Arguments{"openid"}
|
|
requester.ResponseTypes = fosite.Arguments{"code"}
|
|
requester.RedirectURI = &url.URL{Scheme: "https", Host: "client.example", Path: "/callback"}
|
|
requester.Form = form
|
|
return requester
|
|
}
|
|
|
|
func stringPointer(value string) *string {
|
|
return &value
|
|
}
|
|
|
|
func TestConsentRequired(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
hasAlreadyAuthorized bool
|
|
skipConsent bool
|
|
prompt string
|
|
want bool
|
|
}{
|
|
{"new client without skip requires consent", false, false, "", true},
|
|
{"returning client without skip skips consent", true, false, "", false},
|
|
{"new client with skip skips consent", false, true, "", false},
|
|
{"returning client with skip skips consent", true, true, "", false},
|
|
{"prompt=consent forces consent despite skip", false, true, "consent", true},
|
|
{"prompt=consent forces consent for returning client", true, false, "consent", true},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := consentRequired(tt.hasAlreadyAuthorized, tt.skipConsent, newPromptValues(tt.prompt))
|
|
require.Equal(t, tt.want, got)
|
|
})
|
|
}
|
|
}
|
|
|
|
// A client with SkipConsent must be granted without a consent interaction even on the first authorization, while consent is still recorded so the user can later see and revoke the client
|
|
func TestAuthorizationServiceSkipConsentGrantsWithoutInteraction(t *testing.T) {
|
|
db := testutils.NewDatabaseForTest(t)
|
|
auditLogger := &fakeAuditLogger{}
|
|
service := newAuthorizationService(db, newInteractionSessionService(db), newClaimsService(db, nil, "", nil), nil, auditLogger, nil)
|
|
|
|
const (
|
|
userID = "test-user"
|
|
clientID = "test-client"
|
|
)
|
|
require.NoError(t, db.Create(&model.User{Base: model.Base{ID: userID}}).Error)
|
|
require.NoError(t, db.Create(&model.OidcClient{Base: model.Base{ID: clientID}, Name: "Test Client", SkipConsent: true}).Error)
|
|
|
|
// No prior UserAuthorizedOidcClient record exists, so a client without skip-consent would require the consent screen here
|
|
requester := newTestAuthorizeRequester("skip-consent-request", clientID, "")
|
|
requester.(*fosite.AuthorizeRequest).Client = Client{OidcClient: model.OidcClient{Base: model.Base{ID: clientID}, Name: "Test Client", SkipConsent: true}}
|
|
|
|
authorization, err := service.authorize(t.Context(), authorizeInput{
|
|
userID: userID,
|
|
authenticationTime: time.Now().UTC(),
|
|
requester: requester,
|
|
meta: requestMeta{IPAddress: "203.0.113.1", UserAgent: "test-agent"},
|
|
})
|
|
require.NoError(t, err)
|
|
require.False(t, authorization.RequiresInteraction)
|
|
require.NotNil(t, authorization.Session)
|
|
|
|
var count int64
|
|
require.NoError(t, db.Model(&model.UserAuthorizedOidcClient{}).Where("user_id = ? AND client_id = ?", userID, clientID).Count(&count).Error)
|
|
require.Equal(t, int64(1), count)
|
|
|
|
require.Equal(t, []model.AuditLogEvent{model.AuditLogEventNewClientAuthorization}, auditLogger.events)
|
|
}
|
|
|
|
// A client with SkipConsent must still show the consent screen when the request explicitly asks for it with prompt=consent
|
|
func TestAuthorizationServiceSkipConsentHonorsPromptConsent(t *testing.T) {
|
|
db := testutils.NewDatabaseForTest(t)
|
|
service := newAuthorizationService(db, newInteractionSessionService(db), newClaimsService(db, nil, "", nil), nil, nil, nil)
|
|
|
|
const (
|
|
userID = "test-user"
|
|
clientID = "test-client"
|
|
)
|
|
require.NoError(t, db.Create(&model.User{Base: model.Base{ID: userID}}).Error)
|
|
require.NoError(t, db.Create(&model.OidcClient{Base: model.Base{ID: clientID}, Name: "Test Client", SkipConsent: true}).Error)
|
|
|
|
requester := newTestAuthorizeRequester("skip-consent-prompt-request", clientID, "consent")
|
|
requester.(*fosite.AuthorizeRequest).Client = Client{OidcClient: model.OidcClient{Base: model.Base{ID: clientID}, Name: "Test Client", SkipConsent: true}}
|
|
|
|
authorization, err := service.authorize(t.Context(), authorizeInput{
|
|
userID: userID,
|
|
authenticationTime: time.Now().UTC(),
|
|
requester: requester,
|
|
meta: requestMeta{},
|
|
})
|
|
require.NoError(t, err)
|
|
require.True(t, authorization.RequiresInteraction)
|
|
|
|
interactionSession, err := newInteractionSessionService(db).get(t.Context(), authorization.InteractionID)
|
|
require.NoError(t, err)
|
|
require.True(t, interactionSession.ConsentRequired)
|
|
}
|