From c3a6bdb0bb738601f98192ffd6db83fb89f5fbc5 Mon Sep 17 00:00:00 2001 From: Wim Van Laer Date: Tue, 17 Mar 2026 12:54:27 +0100 Subject: [PATCH] Merge commit from fork * fix: add `Scopes` to `Request` interface so that scopes can be validated on all requests * feat: assert org from scope exists when authorizing requests * fix: check all scopes * comments * check org on callback creation * fix tests * clarifications * fix scope marshaling * fix: enfore organization for authrequest in initiation * fix: filter sessions on the accounts page by organization scope * add integration tests --------- Co-authored-by: Livio Spring Co-authored-by: Max Peintner --- apps/login/src/app/(login)/accounts/page.tsx | 23 +- apps/login/src/lib/server/flow-initiation.ts | 4 +- apps/login/src/lib/session.ts | 8 +- .../oidc/v2/integration_test/oidc_test.go | 112 +++++++- internal/api/oidc/auth_request.go | 55 +++- internal/api/oidc/auth_request_converter.go | 14 +- internal/api/oidc/device_auth.go | 4 +- .../integration_test/auth_request_test.go | 15 ++ internal/command/auth_request.go | 40 +-- internal/command/auth_request_model.go | 2 + internal/command/auth_request_test.go | 199 +++++++++++++- internal/command/device_auth.go | 9 +- internal/command/device_auth_model.go | 2 + internal/command/device_auth_test.go | 251 ++++++++++++++++-- internal/command/oidc_session_test.go | 6 + internal/domain/auth_request.go | 18 +- internal/domain/request.go | 39 +++ internal/query/device_auth.go | 17 +- internal/query/device_auth_test.go | 18 +- .../repository/authrequest/auth_request.go | 5 +- internal/repository/deviceauth/device_auth.go | 3 + 21 files changed, 729 insertions(+), 115 deletions(-) diff --git a/apps/login/src/app/(login)/accounts/page.tsx b/apps/login/src/app/(login)/accounts/page.tsx index 4a3501590..3ecc7c4f4 100644 --- a/apps/login/src/app/(login)/accounts/page.tsx +++ b/apps/login/src/app/(login)/accounts/page.tsx @@ -17,12 +17,27 @@ export async function generateMetadata(): Promise { return { title: t("title") }; } -async function loadSessions({ serviceConfig }: { serviceConfig: ServiceConfig }) { +async function loadSessions({ + serviceConfig, + organization, +}: { + serviceConfig: ServiceConfig; + organization?: string; +}) { const cookieIds = await getAllSessionCookieIds(); if (cookieIds && cookieIds.length) { - const response = await listSessions({ serviceConfig, ids: cookieIds.filter((id) => !!id) as string[] }); - return response?.sessions ?? []; + const response = await listSessions({ + serviceConfig, + ids: cookieIds.filter((id) => !!id) as string[], + }); + + let sessions = response?.sessions ?? []; + if (organization) { + sessions = sessions.filter((s) => s.factors?.user?.organizationId === organization); + } + + return sessions; } else { console.info("No session cookie found."); return []; @@ -46,7 +61,7 @@ export default async function Page(props: { searchParams: Promise { - const sessionsWithHint = sessions.filter((s) => { + let sessionsWithHint = sessions.filter((s) => { if (authRequest && authRequest.hintUserId) { return s.factors?.user?.id === authRequest.hintUserId; } @@ -163,6 +165,10 @@ export async function findValidSession({ return true; }); + if (organization) { + sessionsWithHint = sessionsWithHint.filter((s) => s.factors?.user?.organizationId === organization); + } + if (sessionsWithHint.length === 0) { return undefined; } diff --git a/internal/api/grpc/oidc/v2/integration_test/oidc_test.go b/internal/api/grpc/oidc/v2/integration_test/oidc_test.go index 6771cb18a..eefdc27b4 100644 --- a/internal/api/grpc/oidc/v2/integration_test/oidc_test.go +++ b/internal/api/grpc/oidc/v2/integration_test/oidc_test.go @@ -15,6 +15,7 @@ import ( "github.com/zitadel/oidc/v3/pkg/oidc" "google.golang.org/protobuf/types/known/timestamppb" + "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/integration" "github.com/zitadel/zitadel/pkg/grpc/app" filter "github.com/zitadel/zitadel/pkg/grpc/filter/v2beta" @@ -163,6 +164,25 @@ func TestServer_CreateCallback(t *testing.T) { }, wantErr: true, }, + { + name: "invalid organization", + ctx: CTXLoginClient, + req: &oidc_pb.CreateCallbackRequest{ + AuthRequestId: func() string { + org := Instance.CreateOrganization(IAMCTX, integration.OrganizationName(), integration.Email()) + _, authRequestID, err := Instance.CreateOIDCAuthRequest(CTXLoginClient, client.GetClientId(), Instance.Users.Get(integration.UserTypeLogin).ID, redirectURI, oidc.ScopeOpenID, domain.OrgIDScope+org.OrganizationId) + require.NoError(t, err) + return authRequestID + }(), + CallbackKind: &oidc_pb.CreateCallbackRequest_Session{ + Session: &oidc_pb.Session{ + SessionId: sessionResp.GetSessionId(), + SessionToken: sessionResp.GetSessionToken(), + }, + }, + }, + wantErr: true, + }, { name: "fail callback", ctx: CTXLoginClient, @@ -356,6 +376,31 @@ func TestServer_CreateCallback(t *testing.T) { }, wantErr: false, }, + { + name: "callback with required organization", + ctx: CTXLoginClient, + req: &oidc_pb.CreateCallbackRequest{ + AuthRequestId: func() string { + _, authRequestID, err := Instance.CreateOIDCAuthRequest(CTXLoginClient, client.GetClientId(), Instance.Users.Get(integration.UserTypeLogin).ID, redirectURI, oidc.ScopeOpenID, domain.OrgIDScope+Instance.DefaultOrg.Id) + require.NoError(t, err) + return authRequestID + }(), + CallbackKind: &oidc_pb.CreateCallbackRequest_Session{ + Session: &oidc_pb.Session{ + SessionId: sessionResp.GetSessionId(), + SessionToken: sessionResp.GetSessionToken(), + }, + }, + }, + want: &oidc_pb.CreateCallbackResponse{ + CallbackUrl: `oidcintegrationtest:\/\/callback\?code=(.*)&state=state`, + Details: &object.Details{ + ChangeDate: timestamppb.Now(), + ResourceOwner: Instance.ID(), + }, + }, + wantErr: false, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -749,7 +794,7 @@ func TestServer_AuthorizeOrDenyDeviceAuthorization(t *testing.T) { ctx: CTXLoginClient, req: &oidc_pb.AuthorizeOrDenyDeviceAuthorizationRequest{ DeviceAuthorizationId: func() string { - req, err := Instance.CreateDeviceAuthorizationRequest(CTXLoginClient, client.GetClientId(), "openid") + req, err := Instance.CreateDeviceAuthorizationRequest(CTXLoginClient, client.GetClientId(), oidc.ScopeOpenID) require.NoError(t, err) var id string retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTXLoginClient, time.Minute) @@ -776,7 +821,7 @@ func TestServer_AuthorizeOrDenyDeviceAuthorization(t *testing.T) { ctx: CTXLoginClient, req: &oidc_pb.AuthorizeOrDenyDeviceAuthorizationRequest{ DeviceAuthorizationId: func() string { - req, err := Instance.CreateDeviceAuthorizationRequest(CTXLoginClient, client.GetClientId(), "openid") + req, err := Instance.CreateDeviceAuthorizationRequest(CTXLoginClient, client.GetClientId(), oidc.ScopeOpenID) require.NoError(t, err) var id string retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTXLoginClient, time.Minute) @@ -798,12 +843,40 @@ func TestServer_AuthorizeOrDenyDeviceAuthorization(t *testing.T) { }, wantErr: true, }, + { + name: "invalid organization", + ctx: CTXLoginClient, + req: &oidc_pb.AuthorizeOrDenyDeviceAuthorizationRequest{ + DeviceAuthorizationId: func() string { + org := Instance.CreateOrganization(IAMCTX, integration.OrganizationName(), integration.Email()) + req, err := Instance.CreateDeviceAuthorizationRequest(CTXLoginClient, client.GetClientId(), oidc.ScopeOpenID, domain.OrgIDScope+org.OrganizationId) + require.NoError(t, err) + var id string + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTXLoginClient, time.Minute) + require.EventuallyWithT(t, func(collectT *assert.CollectT) { + resp, err := Instance.Client.OIDCv2.GetDeviceAuthorizationRequest(CTXLoginClient, &oidc_pb.GetDeviceAuthorizationRequestRequest{ + UserCode: req.UserCode, + }) + assert.NoError(collectT, err) + id = resp.GetDeviceAuthorizationRequest().GetId() + }, retryDuration, tick) + return id + }(), + Decision: &oidc_pb.AuthorizeOrDenyDeviceAuthorizationRequest_Session{ + Session: &oidc_pb.Session{ + SessionId: sessionResp.GetSessionId(), + SessionToken: sessionResp.GetSessionToken(), + }, + }, + }, + wantErr: true, + }, { name: "deny device authorization", ctx: CTXLoginClient, req: &oidc_pb.AuthorizeOrDenyDeviceAuthorizationRequest{ DeviceAuthorizationId: func() string { - req, err := Instance.CreateDeviceAuthorizationRequest(CTXLoginClient, client.GetClientId(), "openid") + req, err := Instance.CreateDeviceAuthorizationRequest(CTXLoginClient, client.GetClientId(), oidc.ScopeOpenID) require.NoError(t, err) var id string retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTXLoginClient, time.Minute) @@ -826,7 +899,7 @@ func TestServer_AuthorizeOrDenyDeviceAuthorization(t *testing.T) { ctx: CTX, req: &oidc_pb.AuthorizeOrDenyDeviceAuthorizationRequest{ DeviceAuthorizationId: func() string { - req, err := Instance.CreateDeviceAuthorizationRequest(CTXLoginClient, client.GetClientId(), "openid") + req, err := Instance.CreateDeviceAuthorizationRequest(CTXLoginClient, client.GetClientId(), oidc.ScopeOpenID) require.NoError(t, err) var id string retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTXLoginClient, time.Minute) @@ -849,7 +922,7 @@ func TestServer_AuthorizeOrDenyDeviceAuthorization(t *testing.T) { ctx: CTX, req: &oidc_pb.AuthorizeOrDenyDeviceAuthorizationRequest{ DeviceAuthorizationId: func() string { - req, err := Instance.CreateDeviceAuthorizationRequest(CTXLoginClient, client.GetClientId(), "openid") + req, err := Instance.CreateDeviceAuthorizationRequest(CTXLoginClient, client.GetClientId(), oidc.ScopeOpenID) require.NoError(t, err) var id string retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTXLoginClient, time.Minute) @@ -876,7 +949,34 @@ func TestServer_AuthorizeOrDenyDeviceAuthorization(t *testing.T) { ctx: CTXLoginClient, req: &oidc_pb.AuthorizeOrDenyDeviceAuthorizationRequest{ DeviceAuthorizationId: func() string { - req, err := Instance.CreateDeviceAuthorizationRequest(CTXLoginClient, client.GetClientId(), "openid") + req, err := Instance.CreateDeviceAuthorizationRequest(CTXLoginClient, client.GetClientId(), oidc.ScopeOpenID) + require.NoError(t, err) + var id string + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTXLoginClient, time.Minute) + require.EventuallyWithT(t, func(collectT *assert.CollectT) { + resp, err := Instance.Client.OIDCv2.GetDeviceAuthorizationRequest(CTXLoginClient, &oidc_pb.GetDeviceAuthorizationRequestRequest{ + UserCode: req.UserCode, + }) + assert.NoError(collectT, err) + id = resp.GetDeviceAuthorizationRequest().GetId() + }, retryDuration, tick) + return id + }(), + Decision: &oidc_pb.AuthorizeOrDenyDeviceAuthorizationRequest_Session{ + Session: &oidc_pb.Session{ + SessionId: sessionResp.GetSessionId(), + SessionToken: sessionResp.GetSessionToken(), + }, + }, + }, + wantErr: false, + }, + { + name: "authorize, with organization scope", + ctx: CTXLoginClient, + req: &oidc_pb.AuthorizeOrDenyDeviceAuthorizationRequest{ + DeviceAuthorizationId: func() string { + req, err := Instance.CreateDeviceAuthorizationRequest(CTXLoginClient, client.GetClientId(), oidc.ScopeOpenID, domain.OrgIDScope+Instance.DefaultOrg.GetId()) require.NoError(t, err) var id string retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTXLoginClient, time.Minute) diff --git a/internal/api/oidc/auth_request.go b/internal/api/oidc/auth_request.go index e40a5778c..ced95a427 100644 --- a/internal/api/oidc/auth_request.go +++ b/internal/api/oidc/auth_request.go @@ -80,25 +80,31 @@ func (o *OPStorage) CreateAuthRequest(ctx context.Context, req *oidc.AuthRequest } } -func (o *OPStorage) createAuthRequestScopeAndAudience(ctx context.Context, clientID string, reqScope []string) (scope, audience []string, err error) { +func (o *OPStorage) createAuthRequestScopeAndAudience(ctx context.Context, clientID string, reqScope []string) (scope, audience []string, orgID string, err error) { project, err := o.query.ProjectByClientID(ctx, clientID) if err != nil { - return nil, nil, err + return nil, nil, "", err } + + orgID, err = o.assertOrgScope(ctx, reqScope) + if err != nil { + return nil, nil, "", err + } + scope, err = o.assertProjectRoleScopesByProject(ctx, project, reqScope) if err != nil { - return nil, nil, err + return nil, nil, "", err } audience, err = o.audienceFromProjectID(ctx, project.ID) audience = domain.AddAudScopeToAudience(ctx, audience, scope) if err != nil { - return nil, nil, err + return nil, nil, "", err } - return scope, audience, nil + return scope, audience, orgID, nil } func (o *OPStorage) createAuthRequestLoginClient(ctx context.Context, req *oidc.AuthRequest, hintUserID, loginClient string) (op.AuthRequest, error) { - scope, audience, err := o.createAuthRequestScopeAndAudience(ctx, req.ClientID, req.Scopes) + scope, audience, orgID, err := o.createAuthRequestScopeAndAudience(ctx, req.ClientID, req.Scopes) if err != nil { return nil, err } @@ -118,6 +124,7 @@ func (o *OPStorage) createAuthRequestLoginClient(ctx context.Context, req *oidc. UILocales: UILocalesToBusiness(req.UILocales), MaxAge: MaxAgeToBusiness(req.MaxAge), Issuer: o.contextToIssuer(ctx), + OrganizationID: orgID, } if req.LoginHint != "" { authRequest.LoginHint = &req.LoginHint @@ -138,7 +145,8 @@ func (o *OPStorage) createAuthRequest(ctx context.Context, req *oidc.AuthRequest if !ok { return nil, zerrors.ThrowPreconditionFailed(nil, "OIDC-sd436", "no user agent id") } - scope, audience, err := o.createAuthRequestScopeAndAudience(ctx, req.ClientID, req.Scopes) + // we do not need to handle the orgID for the v1 login, since it handles it already + scope, audience, _, err := o.createAuthRequestScopeAndAudience(ctx, req.ClientID, req.Scopes) if err != nil { return nil, err } @@ -530,6 +538,39 @@ func (o *OPStorage) GetRefreshTokenInfo(ctx context.Context, clientID string, to return refreshToken.UserID, refreshToken.ID, nil } +// assertOrgScope checks the scopes for organization scopes and returns the orgID if exactly one org scope is found. +// If multiple org scopes are found or if the org scope is invalid, an error is returned. +// For backwards compatibility, we support both orgID and orgDomain scopes, but they need to be consistent, +// meaning that if both are provided, they need to belong to the same organization. +func (o *OPStorage) assertOrgScope(ctx context.Context, scopes []string) (string, error) { + var id string + for _, scope := range scopes { + if orgID, ok := strings.CutPrefix(scope, domain.OrgIDScope); ok { + org, err := o.query.OrgByID(ctx, orgID) + if err != nil { + return "", err + } + if id != "" && id != org.ID { + return "", oidc.ErrInvalidScope().WithDescription("Only one organization scope may be provided") + } + id = org.ID + continue + } + + if orgDomain, ok := strings.CutPrefix(scope, domain.OrgDomainPrimaryScope); ok { + org, err := o.query.OrgByPrimaryDomain(ctx, orgDomain) + if err != nil { + return "", err + } + if id != "" && id != org.ID { + return "", oidc.ErrInvalidScope().WithDescription("Only one organization scope may be provided") + } + id = org.ID + } + } + return id, nil +} + func (o *OPStorage) assertProjectRoleScopesByProject(ctx context.Context, project *query.Project, scopes []string) ([]string, error) { for _, scope := range scopes { if strings.HasPrefix(scope, ScopeProjectRolePrefix) { diff --git a/internal/api/oidc/auth_request_converter.go b/internal/api/oidc/auth_request_converter.go index 064af20de..98928fb17 100644 --- a/internal/api/oidc/auth_request_converter.go +++ b/internal/api/oidc/auth_request_converter.go @@ -119,13 +119,13 @@ func CreateAuthRequestToBusiness(ctx context.Context, authReq *oidc.AuthRequest, UserID: userID, InstanceID: authz.GetInstance(ctx).InstanceID(), Audience: audience, - Request: &domain.AuthRequestOIDC{ - Scopes: authReq.Scopes, - ResponseType: ResponseTypeToBusiness(authReq.ResponseType), - ResponseMode: ResponseModeToBusiness(authReq.ResponseMode), - Nonce: authReq.Nonce, - CodeChallenge: CodeChallengeToBusiness(authReq.CodeChallenge, authReq.CodeChallengeMethod), - }, + Request: domain.NewAuthRequestOIDC( + authReq.Scopes, + ResponseTypeToBusiness(authReq.ResponseType), + ResponseModeToBusiness(authReq.ResponseMode), + authReq.Nonce, + CodeChallengeToBusiness(authReq.CodeChallenge, authReq.CodeChallengeMethod), + ), } } diff --git a/internal/api/oidc/device_auth.go b/internal/api/oidc/device_auth.go index 8912ad173..4eadc66be 100644 --- a/internal/api/oidc/device_auth.go +++ b/internal/api/oidc/device_auth.go @@ -76,11 +76,11 @@ func (o *OPStorage) StoreDeviceAuthorization(ctx context.Context, clientID, devi logger.OnError(err).Error(logMsg) span.EndWithError(err) }() - scope, audience, err := o.createAuthRequestScopeAndAudience(ctx, clientID, scope) + scope, audience, orgID, err := o.createAuthRequestScopeAndAudience(ctx, clientID, scope) if err != nil { return err } - details, err := o.command.AddDeviceAuth(ctx, clientID, deviceCode, userCode, expires, scope, audience, slices.Contains(scope, oidc.ScopeOfflineAccess)) + details, err := o.command.AddDeviceAuth(ctx, clientID, deviceCode, userCode, orgID, expires, scope, audience, slices.Contains(scope, oidc.ScopeOfflineAccess)) if err == nil { logger.SetFields("details", details).Debug(logMsg) } diff --git a/internal/api/oidc/integration_test/auth_request_test.go b/internal/api/oidc/integration_test/auth_request_test.go index 75c436464..cc6d023e2 100644 --- a/internal/api/oidc/integration_test/auth_request_test.go +++ b/internal/api/oidc/integration_test/auth_request_test.go @@ -17,6 +17,7 @@ import ( http_utils "github.com/zitadel/zitadel/internal/api/http" oidc_api "github.com/zitadel/zitadel/internal/api/oidc" "github.com/zitadel/zitadel/internal/command" + "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/integration" oidc_pb "github.com/zitadel/zitadel/pkg/grpc/oidc/v2" "github.com/zitadel/zitadel/pkg/grpc/session/v2" @@ -36,6 +37,20 @@ func TestOPStorage_CreateAuthRequest(t *testing.T) { id2 := createAuthRequestNoLoginClientHeader(t, Instance, clientIDV2, redirectURI) require.Contains(t, id2, command.IDPrefixV2) + + // valid org scope must succeed + _, _, err := Instance.CreateOIDCAuthRequest(CTX, clientID, Instance.Users.Get(integration.UserTypeLogin).ID, redirectURI, oidc.ScopeOpenID, domain.OrgIDScope+Instance.DefaultOrg.Id) + require.NoError(t, err) + + _, _, err = Instance.CreateOIDCAuthRequest(CTX, clientID, Instance.Users.Get(integration.UserTypeLogin).ID, redirectURI, oidc.ScopeOpenID, domain.OrgIDScope+Instance.DefaultOrg.Id) + require.NoError(t, err) + + // invalid org scope must fail + _, _, err = Instance.CreateOIDCAuthRequest(CTX, clientID, Instance.Users.Get(integration.UserTypeLogin).ID, redirectURI, oidc.ScopeOpenID, domain.OrgIDScope+"invalid") + require.Error(t, err) + + _, _, err = Instance.CreateOIDCAuthRequest(CTX, clientID, Instance.Users.Get(integration.UserTypeLogin).ID, redirectURI, oidc.ScopeOpenID, domain.OrgIDScope+"invalid") + require.Error(t, err) } func TestOPStorage_CreateAccessToken_code(t *testing.T) { diff --git a/internal/command/auth_request.go b/internal/command/auth_request.go index 0ce8741b3..ba78281db 100644 --- a/internal/command/auth_request.go +++ b/internal/command/auth_request.go @@ -30,6 +30,7 @@ type AuthRequest struct { HintUserID *string NeedRefreshToken bool Issuer string + OrganizationID string } type CurrentAuthRequest struct { @@ -75,6 +76,7 @@ func (c *Commands) AddAuthRequest(ctx context.Context, authRequest *AuthRequest) authRequest.HintUserID, authRequest.NeedRefreshToken, authRequest.Issuer, + authRequest.OrganizationID, )) if err != nil { return nil, err @@ -116,6 +118,9 @@ func (c *Commands) LinkSessionToAuthRequest(ctx context.Context, id, sessionID, return nil, nil, err } } + if writeModel.OrganizationID != "" && writeModel.OrganizationID != sessionWriteModel.UserResourceOwner { + return nil, nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-59ljd", "Errors.User.NotAllowedOrg") + } if err := c.pushAppendAndReduce(ctx, writeModel, authrequest.NewSessionLinkedEvent( ctx, &authrequest.NewAggregate(id, authz.GetInstance(ctx).InstanceID()).Aggregate, @@ -171,23 +176,24 @@ func (c *Commands) AddAuthRequestCode(ctx context.Context, authRequestID, code s func authRequestWriteModelToCurrentAuthRequest(writeModel *AuthRequestWriteModel) (_ *CurrentAuthRequest) { return &CurrentAuthRequest{ AuthRequest: &AuthRequest{ - ID: writeModel.AggregateID, - LoginClient: writeModel.LoginClient, - ClientID: writeModel.ClientID, - RedirectURI: writeModel.RedirectURI, - State: writeModel.State, - Nonce: writeModel.Nonce, - Scope: writeModel.Scope, - Audience: writeModel.Audience, - ResponseType: writeModel.ResponseType, - ResponseMode: writeModel.ResponseMode, - CodeChallenge: writeModel.CodeChallenge, - Prompt: writeModel.Prompt, - UILocales: writeModel.UILocales, - MaxAge: writeModel.MaxAge, - LoginHint: writeModel.LoginHint, - HintUserID: writeModel.HintUserID, - Issuer: writeModel.Issuer, + ID: writeModel.AggregateID, + LoginClient: writeModel.LoginClient, + ClientID: writeModel.ClientID, + RedirectURI: writeModel.RedirectURI, + State: writeModel.State, + Nonce: writeModel.Nonce, + Scope: writeModel.Scope, + Audience: writeModel.Audience, + ResponseType: writeModel.ResponseType, + ResponseMode: writeModel.ResponseMode, + CodeChallenge: writeModel.CodeChallenge, + Prompt: writeModel.Prompt, + UILocales: writeModel.UILocales, + MaxAge: writeModel.MaxAge, + LoginHint: writeModel.LoginHint, + HintUserID: writeModel.HintUserID, + Issuer: writeModel.Issuer, + OrganizationID: writeModel.OrganizationID, }, SessionID: writeModel.SessionID, UserID: writeModel.UserID, diff --git a/internal/command/auth_request_model.go b/internal/command/auth_request_model.go index 0e8d88fd1..555d1bfe1 100644 --- a/internal/command/auth_request_model.go +++ b/internal/command/auth_request_model.go @@ -37,6 +37,7 @@ type AuthRequestWriteModel struct { AuthRequestState domain.AuthRequestState NeedRefreshToken bool Issuer string + OrganizationID string } func NewAuthRequestWriteModel(ctx context.Context, id string) *AuthRequestWriteModel { @@ -70,6 +71,7 @@ func (m *AuthRequestWriteModel) Reduce() error { m.AuthRequestState = domain.AuthRequestStateAdded m.NeedRefreshToken = e.NeedRefreshToken m.Issuer = e.Issuer + m.OrganizationID = e.OrganizationID case *authrequest.SessionLinkedEvent: m.SessionID = e.SessionID m.UserID = e.UserID diff --git a/internal/command/auth_request_test.go b/internal/command/auth_request_test.go index 2d3c72b08..250b2c6da 100644 --- a/internal/command/auth_request_test.go +++ b/internal/command/auth_request_test.go @@ -63,6 +63,7 @@ func TestCommands_AddAuthRequest(t *testing.T) { nil, false, "issuer", + "", ), ), ), @@ -103,6 +104,7 @@ func TestCommands_AddAuthRequest(t *testing.T) { gu.Ptr("hintUserID"), false, "issuer", + "organizationID", ), ), ), @@ -124,12 +126,13 @@ func TestCommands_AddAuthRequest(t *testing.T) { Challenge: "challenge", Method: domain.CodeChallengeMethodS256, }, - Prompt: []domain.Prompt{domain.PromptNone}, - UILocales: []string{"en", "de"}, - MaxAge: gu.Ptr(time.Duration(0)), - LoginHint: gu.Ptr("loginHint"), - HintUserID: gu.Ptr("hintUserID"), - Issuer: "issuer", + Prompt: []domain.Prompt{domain.PromptNone}, + UILocales: []string{"en", "de"}, + MaxAge: gu.Ptr(time.Duration(0)), + LoginHint: gu.Ptr("loginHint"), + HintUserID: gu.Ptr("hintUserID"), + Issuer: "issuer", + OrganizationID: "organizationID", }, }, &CurrentAuthRequest{ @@ -148,12 +151,13 @@ func TestCommands_AddAuthRequest(t *testing.T) { Challenge: "challenge", Method: domain.CodeChallengeMethodS256, }, - Prompt: []domain.Prompt{domain.PromptNone}, - UILocales: []string{"en", "de"}, - MaxAge: gu.Ptr(time.Duration(0)), - LoginHint: gu.Ptr("loginHint"), - HintUserID: gu.Ptr("hintUserID"), - Issuer: "issuer", + Prompt: []domain.Prompt{domain.PromptNone}, + UILocales: []string{"en", "de"}, + MaxAge: gu.Ptr(time.Duration(0)), + LoginHint: gu.Ptr("loginHint"), + HintUserID: gu.Ptr("hintUserID"), + Issuer: "issuer", + OrganizationID: "organizationID", }, }, nil, @@ -239,6 +243,7 @@ func TestCommands_LinkSessionToAuthRequest(t *testing.T) { nil, true, "issuer", + "", ), ), eventFromEventPusher( @@ -282,6 +287,7 @@ func TestCommands_LinkSessionToAuthRequest(t *testing.T) { nil, true, "issuer", + "", ), ), ), @@ -324,6 +330,7 @@ func TestCommands_LinkSessionToAuthRequest(t *testing.T) { nil, true, "issuer", + "", ), ), ), @@ -364,6 +371,7 @@ func TestCommands_LinkSessionToAuthRequest(t *testing.T) { nil, true, "issuer", + "", ), ), ), @@ -427,6 +435,7 @@ func TestCommands_LinkSessionToAuthRequest(t *testing.T) { nil, true, "issuer", + "", ), ), ), @@ -455,6 +464,71 @@ func TestCommands_LinkSessionToAuthRequest(t *testing.T) { wantErr: zerrors.ThrowPermissionDenied(nil, "COMMAND-sGr42", "Errors.Session.Token.Invalid"), }, }, + { + "invalid organization", + fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + authrequest.NewAddedEvent(mockCtx, &authrequest.NewAggregate("V2_id", "instanceID").Aggregate, + "loginClient", + "clientID", + "redirectURI", + "state", + "nonce", + []string{"openid"}, + []string{"audience"}, + domain.OIDCResponseTypeCode, + domain.OIDCResponseModeQuery, + nil, + nil, + nil, + nil, + nil, + nil, + true, + "issuer", + "organizationID", + ), + ), + ), + expectFilter( + eventFromEventPusher( + session.NewAddedEvent(mockCtx, + &session.NewAggregate("sessionID", "instance1").Aggregate, + &domain.UserAgent{ + FingerprintID: gu.Ptr("fp1"), + IP: net.ParseIP("1.2.3.4"), + Description: gu.Ptr("firefox"), + Header: http.Header{"foo": []string{"bar"}}, + }, + )), + eventFromEventPusher( + session.NewUserCheckedEvent(mockCtx, &session.NewAggregate("sessionID", "instance1").Aggregate, + "userID", "org1", testNow, &language.Afrikaans), + ), + eventFromEventPusher( + session.NewPasswordCheckedEvent(mockCtx, &session.NewAggregate("sessionID", "instance1").Aggregate, + testNow), + ), + eventFromEventPusherWithCreationDateNow( + session.NewLifetimeSetEvent(mockCtx, &session.NewAggregate("sessionID", "instance1").Aggregate, + 2*time.Minute), + ), + ), + ), + tokenVerifier: newMockTokenVerifierValid(), + }, + args{ + ctx: mockCtx, + id: "V2_id", + sessionID: "sessionID", + sessionToken: "token", + }, + res{ + wantErr: zerrors.ThrowPreconditionFailed(nil, "COMMAND-59ljd", "Errors.User.NotAllowedOrg"), + }, + }, { "linked", fields{ @@ -479,6 +553,7 @@ func TestCommands_LinkSessionToAuthRequest(t *testing.T) { nil, true, "issuer", + "", ), ), ), @@ -545,6 +620,98 @@ func TestCommands_LinkSessionToAuthRequest(t *testing.T) { }, }, }, + { + "linked with organization check", + fields{ + eventstore: expectEventstore( + expectFilter( + eventFromEventPusher( + authrequest.NewAddedEvent(mockCtx, &authrequest.NewAggregate("V2_id", "instanceID").Aggregate, + "loginClient", + "clientID", + "redirectURI", + "state", + "nonce", + []string{"openid"}, + []string{"audience"}, + domain.OIDCResponseTypeCode, + domain.OIDCResponseModeQuery, + nil, + nil, + nil, + nil, + nil, + nil, + true, + "issuer", + "org1", + ), + ), + ), + expectFilter( + eventFromEventPusher( + session.NewAddedEvent(mockCtx, + &session.NewAggregate("sessionID", "instance1").Aggregate, + &domain.UserAgent{ + FingerprintID: gu.Ptr("fp1"), + IP: net.ParseIP("1.2.3.4"), + Description: gu.Ptr("firefox"), + Header: http.Header{"foo": []string{"bar"}}, + }, + )), + eventFromEventPusher( + session.NewUserCheckedEvent(mockCtx, &session.NewAggregate("sessionID", "instance1").Aggregate, + "userID", "org1", testNow, &language.Afrikaans), + ), + eventFromEventPusher( + session.NewPasswordCheckedEvent(mockCtx, &session.NewAggregate("sessionID", "instance1").Aggregate, + testNow), + ), + eventFromEventPusherWithCreationDateNow( + session.NewLifetimeSetEvent(mockCtx, &session.NewAggregate("sessionID", "instance1").Aggregate, + 2*time.Minute), + ), + ), + expectPush( + authrequest.NewSessionLinkedEvent(mockCtx, &authrequest.NewAggregate("V2_id", "instanceID").Aggregate, + "sessionID", + "userID", + testNow, + []domain.UserAuthMethodType{domain.UserAuthMethodTypePassword}, + ), + ), + ), + tokenVerifier: newMockTokenVerifierValid(), + }, + args{ + ctx: mockCtx, + id: "V2_id", + sessionID: "sessionID", + sessionToken: "token", + }, + res{ + details: &domain.ObjectDetails{ResourceOwner: "instanceID"}, + authReq: &CurrentAuthRequest{ + AuthRequest: &AuthRequest{ + ID: "V2_id", + LoginClient: "loginClient", + ClientID: "clientID", + RedirectURI: "redirectURI", + State: "state", + Nonce: "nonce", + Scope: []string{"openid"}, + Audience: []string{"audience"}, + ResponseType: domain.OIDCResponseTypeCode, + ResponseMode: domain.OIDCResponseModeQuery, + Issuer: "issuer", + OrganizationID: "org1", + }, + SessionID: "sessionID", + UserID: "userID", + AuthMethods: []domain.UserAuthMethodType{domain.UserAuthMethodTypePassword}, + }, + }, + }, { "linked with login client check", fields{ @@ -569,6 +736,7 @@ func TestCommands_LinkSessionToAuthRequest(t *testing.T) { nil, true, "issuer", + "", ), ), ), @@ -660,6 +828,7 @@ func TestCommands_LinkSessionToAuthRequest(t *testing.T) { nil, true, "issuer", + "", ), ), ), @@ -752,6 +921,7 @@ func TestCommands_LinkSessionToAuthRequest(t *testing.T) { nil, true, "issuer", + "", ), ), ), @@ -845,6 +1015,7 @@ func TestCommands_LinkSessionToAuthRequest(t *testing.T) { nil, true, "issuer", + "", ), ), ), @@ -970,6 +1141,7 @@ func TestCommands_FailAuthRequest(t *testing.T) { nil, true, "issuer", + "", ), ), ), @@ -1009,6 +1181,7 @@ func TestCommands_FailAuthRequest(t *testing.T) { nil, true, "issuer", + "", ), ), ), @@ -1113,6 +1286,7 @@ func TestCommands_AddAuthRequestCode(t *testing.T) { gu.Ptr("hintUserID"), true, "issuer", + "", ), ), ), @@ -1152,6 +1326,7 @@ func TestCommands_AddAuthRequestCode(t *testing.T) { gu.Ptr("hintUserID"), true, "issuer", + "", ), ), eventFromEventPusher( diff --git a/internal/command/device_auth.go b/internal/command/device_auth.go index 7d4a0f207..ca3e2e2cc 100644 --- a/internal/command/device_auth.go +++ b/internal/command/device_auth.go @@ -15,7 +15,7 @@ import ( "github.com/zitadel/zitadel/internal/zerrors" ) -func (c *Commands) AddDeviceAuth(ctx context.Context, clientID, deviceCode, userCode string, expires time.Time, scopes, audience []string, needRefreshToken bool) (*domain.ObjectDetails, error) { +func (c *Commands) AddDeviceAuth(ctx context.Context, clientID, deviceCode, userCode, orgID string, expires time.Time, scopes, audience []string, needRefreshToken bool) (*domain.ObjectDetails, error) { aggr := deviceauth.NewAggregate(deviceCode, authz.GetInstance(ctx).InstanceID()) model := NewDeviceAuthWriteModel(deviceCode, aggr.ResourceOwner) @@ -29,6 +29,7 @@ func (c *Commands) AddDeviceAuth(ctx context.Context, clientID, deviceCode, user scopes, audience, needRefreshToken, + orgID, )) if err != nil { return nil, err @@ -62,6 +63,9 @@ func (c *Commands) ApproveDeviceAuth( if model.State != domain.DeviceAuthStateInitiated { return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-GEJL3", "Errors.DeviceAuth.AlreadyHandled") } + if model.OrganizationID != "" && model.OrganizationID != userOrgID { + return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-3tgws", "Errors.User.NotAllowedOrg") + } pushedEvents, err := c.eventstore.Push(ctx, deviceauth.NewApprovedEvent(ctx, model.aggregate, userID, userOrgID, authMethods, authTime, preferredLanguage, userAgent, sessionID)) if err != nil { return nil, err @@ -105,6 +109,9 @@ func (c *Commands) ApproveDeviceAuthWithSession( if err := c.sessionTokenVerifier(ctx, sessionToken, sessionWriteModel.AggregateID, sessionWriteModel.TokenID); err != nil { return nil, err } + if model.OrganizationID != "" && model.OrganizationID != sessionWriteModel.UserResourceOwner { + return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-D2j34", "Errors.User.NotAllowedOrg") + } pushedEvents, err := c.eventstore.Push(ctx, deviceauth.NewApprovedEvent( ctx, diff --git a/internal/command/device_auth_model.go b/internal/command/device_auth_model.go index 28833ae89..e8e927154 100644 --- a/internal/command/device_auth_model.go +++ b/internal/command/device_auth_model.go @@ -29,6 +29,7 @@ type DeviceAuthWriteModel struct { UserAgent *domain.UserAgent NeedRefreshToken bool SessionID string + OrganizationID string } func NewDeviceAuthWriteModel(deviceCode, resourceOwner string) *DeviceAuthWriteModel { @@ -53,6 +54,7 @@ func (m *DeviceAuthWriteModel) Reduce() error { m.Audience = e.Audience m.State = e.State m.NeedRefreshToken = e.NeedRefreshToken + m.OrganizationID = e.OrganizationID case *deviceauth.ApprovedEvent: m.State = domain.DeviceAuthStateApproved m.UserID = e.UserID diff --git a/internal/command/device_auth_test.go b/internal/command/device_auth_test.go index 140148250..8c8faac7b 100644 --- a/internal/command/device_auth_test.go +++ b/internal/command/device_auth_test.go @@ -49,6 +49,7 @@ func TestCommands_AddDeviceAuth(t *testing.T) { scopes []string audience []string needRefreshToken bool + organizationID string } tests := []struct { name string @@ -66,7 +67,7 @@ func TestCommands_AddDeviceAuth(t *testing.T) { deviceauth.NewAggregate("123", "instance1"), "client_id", "123", "456", now, []string{"a", "b", "c"}, - []string{"projectID", "clientID"}, true, + []string{"projectID", "clientID"}, true, "orgID", ), )), }, @@ -79,6 +80,7 @@ func TestCommands_AddDeviceAuth(t *testing.T) { scopes: []string{"a", "b", "c"}, audience: []string{"projectID", "clientID"}, needRefreshToken: true, + organizationID: "orgID", }, wantDetails: &domain.ObjectDetails{ ResourceOwner: "instance1", @@ -93,7 +95,7 @@ func TestCommands_AddDeviceAuth(t *testing.T) { deviceauth.NewAggregate("123", "instance1"), "client_id", "123", "456", now, []string{"a", "b", "c"}, - []string{"projectID", "clientID"}, false, + []string{"projectID", "clientID"}, false, "orgID", )), ), }, @@ -106,6 +108,7 @@ func TestCommands_AddDeviceAuth(t *testing.T) { scopes: []string{"a", "b", "c"}, audience: []string{"projectID", "clientID"}, needRefreshToken: false, + organizationID: "orgID", }, wantErr: pushErr, }, @@ -115,7 +118,17 @@ func TestCommands_AddDeviceAuth(t *testing.T) { c := &Commands{ eventstore: tt.fields.eventstore(t), } - gotDetails, err := c.AddDeviceAuth(tt.args.ctx, tt.args.clientID, tt.args.deviceCode, tt.args.userCode, tt.args.expires, tt.args.scopes, tt.args.audience, tt.args.needRefreshToken) + gotDetails, err := c.AddDeviceAuth( + tt.args.ctx, + tt.args.clientID, + tt.args.deviceCode, + tt.args.userCode, + tt.args.organizationID, + tt.args.expires, + tt.args.scopes, + tt.args.audience, + tt.args.needRefreshToken, + ) require.ErrorIs(t, err, tt.wantErr) assertObjectDetails(t, tt.wantDetails, gotDetails) }) @@ -168,6 +181,35 @@ func TestCommands_ApproveDeviceAuth(t *testing.T) { }, wantErr: zerrors.ThrowNotFound(nil, "COMMAND-Hief9", "Errors.DeviceAuth.NotFound"), }, + { + name: "invalid org", + fields: fields{ + eventstore: expectEventstore( + expectFilter(eventFromEventPusherWithInstanceID( + "instance1", + deviceauth.NewAddedEvent( + ctx, + deviceauth.NewAggregate("123", "instance1"), + "client_id", "123", "456", now, + []string{"a", "b", "c"}, + []string{"projectID", "clientID"}, true, "orgID2", + ), + )), + ), + }, + args: args{ + ctx, "123", "subj", "orgID", + []domain.UserAuthMethodType{domain.UserAuthMethodTypePassword}, + time.Unix(123, 456), &language.Afrikaans, &domain.UserAgent{ + FingerprintID: gu.Ptr("fp1"), + IP: net.ParseIP("1.2.3.4"), + Description: gu.Ptr("firefox"), + Header: http.Header{"foo": []string{"bar"}}, + }, + "sessionID", + }, + wantErr: zerrors.ThrowPreconditionFailed(nil, "COMMAND-3tgws", "Errors.User.NotAllowedOrg"), + }, { name: "push error", fields: fields{ @@ -179,7 +221,7 @@ func TestCommands_ApproveDeviceAuth(t *testing.T) { deviceauth.NewAggregate("123", "instance1"), "client_id", "123", "456", now, []string{"a", "b", "c"}, - []string{"projectID", "clientID"}, true, + []string{"projectID", "clientID"}, true, "", ), )), expectPushFailed(pushErr, @@ -221,7 +263,51 @@ func TestCommands_ApproveDeviceAuth(t *testing.T) { deviceauth.NewAggregate("123", "instance1"), "client_id", "123", "456", now, []string{"a", "b", "c"}, - []string{"projectID", "clientID"}, true, + []string{"projectID", "clientID"}, true, "", + ), + )), + expectPush( + deviceauth.NewApprovedEvent( + ctx, deviceauth.NewAggregate("123", "instance1"), "subj", "orgID", + []domain.UserAuthMethodType{domain.UserAuthMethodTypePassword}, + time.Unix(123, 456), &language.Afrikaans, &domain.UserAgent{ + FingerprintID: gu.Ptr("fp1"), + IP: net.ParseIP("1.2.3.4"), + Description: gu.Ptr("firefox"), + Header: http.Header{"foo": []string{"bar"}}, + }, + "sessionID", + ), + ), + ), + }, + args: args{ + ctx, "123", "subj", "orgID", + []domain.UserAuthMethodType{domain.UserAuthMethodTypePassword}, + time.Unix(123, 456), &language.Afrikaans, &domain.UserAgent{ + FingerprintID: gu.Ptr("fp1"), + IP: net.ParseIP("1.2.3.4"), + Description: gu.Ptr("firefox"), + Header: http.Header{"foo": []string{"bar"}}, + }, + "sessionID", + }, + wantDetails: &domain.ObjectDetails{ + ResourceOwner: "instance1", + }, + }, + { + name: "success with organizationID in device auth", + fields: fields{ + eventstore: expectEventstore( + expectFilter(eventFromEventPusherWithInstanceID( + "instance1", + deviceauth.NewAddedEvent( + ctx, + deviceauth.NewAggregate("123", "instance1"), + "client_id", "123", "456", now, + []string{"a", "b", "c"}, + []string{"projectID", "clientID"}, true, "orgID", ), )), expectPush( @@ -317,7 +403,7 @@ func TestCommands_ApproveDeviceAuthFromSession(t *testing.T) { deviceauth.NewAggregate("deviceCode", "instance1"), "client_id", "deviceCode", "456", now, []string{"a", "b", "c"}, - []string{"projectID", "clientID"}, true, + []string{"projectID", "clientID"}, true, "", ), ), eventFromEventPusherWithInstanceID( @@ -349,7 +435,7 @@ func TestCommands_ApproveDeviceAuthFromSession(t *testing.T) { deviceauth.NewAggregate("deviceCode", "instance1"), "client_id", "deviceCode", "456", now, []string{"a", "b", "c"}, - []string{"projectID", "clientID"}, true, + []string{"projectID", "clientID"}, true, "", ), )), ), @@ -374,7 +460,7 @@ func TestCommands_ApproveDeviceAuthFromSession(t *testing.T) { deviceauth.NewAggregate("deviceCode", "instance1"), "client_id", "deviceCode", "456", now, []string{"a", "b", "c"}, - []string{"projectID", "clientID"}, true, + []string{"projectID", "clientID"}, true, "", ), )), expectFilter(), @@ -400,7 +486,7 @@ func TestCommands_ApproveDeviceAuthFromSession(t *testing.T) { deviceauth.NewAggregate("deviceCode", "instance1"), "client_id", "deviceCode", "456", now, []string{"a", "b", "c"}, - []string{"projectID", "clientID"}, true, + []string{"projectID", "clientID"}, true, "", ), )), expectFilter(eventFromEventPusherWithInstanceID( @@ -426,6 +512,57 @@ func TestCommands_ApproveDeviceAuthFromSession(t *testing.T) { }, wantErr: zerrors.ThrowPermissionDenied(nil, "COMMAND-sGr42", "Errors.Session.Token.Invalid"), }, + { + name: "invalid organization, error", + fields: fields{ + eventstore: expectEventstore( + expectFilter(eventFromEventPusherWithInstanceID( + "instance1", + deviceauth.NewAddedEvent( + ctx, + deviceauth.NewAggregate("deviceCode", "instance1"), + "client_id", "deviceCode", "456", now, + []string{"a", "b", "c"}, + []string{"projectID", "clientID"}, true, "orgID2", + ), + )), + expectFilter( + eventFromEventPusherWithInstanceID( + "instance1", + session.NewAddedEvent(ctx, + &session.NewAggregate("sessionID", "instance1").Aggregate, + &domain.UserAgent{ + FingerprintID: gu.Ptr("fp1"), + IP: net.ParseIP("1.2.3.4"), + Description: gu.Ptr("firefox"), + Header: http.Header{"foo": []string{"bar"}}, + }, + )), + eventFromEventPusher( + session.NewUserCheckedEvent(ctx, &session.NewAggregate("sessionID", "instance1").Aggregate, + "userID", "orgID", testNow, &language.Afrikaans), + ), + eventFromEventPusher( + session.NewPasswordCheckedEvent(ctx, &session.NewAggregate("sessionID", "instance1").Aggregate, + testNow), + ), + eventFromEventPusherWithCreationDateNow( + session.NewLifetimeSetEvent(ctx, &session.NewAggregate("sessionID", "instance1").Aggregate, + 2*time.Minute), + ), + ), + ), + tokenVerifier: newMockTokenVerifierValid(), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx, + "deviceCode", + "sessionID", + "sessionToken", + }, + wantErr: zerrors.ThrowPreconditionFailed(nil, "COMMAND-D2j34", "Errors.User.NotAllowedOrg"), + }, { name: "push error", fields: fields{ @@ -437,7 +574,7 @@ func TestCommands_ApproveDeviceAuthFromSession(t *testing.T) { deviceauth.NewAggregate("deviceCode", "instance1"), "client_id", "deviceCode", "456", now, []string{"a", "b", "c"}, - []string{"projectID", "clientID"}, true, + []string{"projectID", "clientID"}, true, "", ), )), expectFilter( @@ -501,7 +638,73 @@ func TestCommands_ApproveDeviceAuthFromSession(t *testing.T) { deviceauth.NewAggregate("deviceCode", "instance1"), "client_id", "deviceCode", "456", now, []string{"a", "b", "c"}, - []string{"projectID", "clientID"}, true, + []string{"projectID", "clientID"}, true, "", + ), + )), + expectFilter( + eventFromEventPusher( + session.NewAddedEvent(ctx, + &session.NewAggregate("sessionID", "instance1").Aggregate, + &domain.UserAgent{ + FingerprintID: gu.Ptr("fp1"), + IP: net.ParseIP("1.2.3.4"), + Description: gu.Ptr("firefox"), + Header: http.Header{"foo": []string{"bar"}}, + }, + ), + ), + eventFromEventPusher( + session.NewUserCheckedEvent(ctx, &session.NewAggregate("sessionID", "instance1").Aggregate, + "userID", "orgID", testNow, &language.Afrikaans), + ), + eventFromEventPusher( + session.NewPasswordCheckedEvent(ctx, &session.NewAggregate("sessionID", "instance1").Aggregate, + testNow), + ), + eventFromEventPusherWithCreationDateNow( + session.NewLifetimeSetEvent(ctx, &session.NewAggregate("sessionID", "instance1").Aggregate, + 2*time.Minute), + ), + ), + expectPush( + deviceauth.NewApprovedEvent( + ctx, deviceauth.NewAggregate("deviceCode", "instance1"), "userID", "orgID", + []domain.UserAuthMethodType{domain.UserAuthMethodTypePassword}, + testNow, &language.Afrikaans, &domain.UserAgent{ + FingerprintID: gu.Ptr("fp1"), + IP: net.ParseIP("1.2.3.4"), + Description: gu.Ptr("firefox"), + Header: http.Header{"foo": []string{"bar"}}, + }, + "sessionID", + ), + ), + ), + tokenVerifier: newMockTokenVerifierValid(), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + ctx, + "deviceCode", + "sessionID", + "sessionToken", + }, + wantDetails: &domain.ObjectDetails{ + ResourceOwner: "instance1", + }, + }, + { + name: "authorized with organizationID in device auth", + fields: fields{ + eventstore: expectEventstore( + expectFilter(eventFromEventPusherWithInstanceID( + "instance1", + deviceauth.NewAddedEvent( + ctx, + deviceauth.NewAggregate("deviceCode", "instance1"), + "client_id", "deviceCode", "456", now, + []string{"a", "b", "c"}, + []string{"projectID", "clientID"}, true, "orgID", ), )), expectFilter( @@ -613,7 +816,7 @@ func TestCommands_CancelDeviceAuth(t *testing.T) { deviceauth.NewAggregate("123", "instance1"), "client_id", "123", "456", now, []string{"a", "b", "c"}, - []string{"projectID", "clientID"}, true, + []string{"projectID", "clientID"}, true, "", ), )), ), @@ -633,7 +836,7 @@ func TestCommands_CancelDeviceAuth(t *testing.T) { deviceauth.NewAggregate("123", "instance1"), "client_id", "123", "456", now, []string{"a", "b", "c"}, - []string{"projectID", "clientID"}, true, + []string{"projectID", "clientID"}, true, "", ), )), expectPushFailed(pushErr, @@ -659,7 +862,7 @@ func TestCommands_CancelDeviceAuth(t *testing.T) { deviceauth.NewAggregate("123", "instance1"), "client_id", "123", "456", now, []string{"a", "b", "c"}, - []string{"projectID", "clientID"}, true, + []string{"projectID", "clientID"}, true, "", ), )), expectPush( @@ -687,7 +890,7 @@ func TestCommands_CancelDeviceAuth(t *testing.T) { deviceauth.NewAggregate("123", "instance1"), "client_id", "123", "456", now, []string{"a", "b", "c"}, - []string{"projectID", "clientID"}, true, + []string{"projectID", "clientID"}, true, "", ), )), expectPush( @@ -767,7 +970,7 @@ func TestCommands_CreateOIDCSessionFromDeviceAuth(t *testing.T) { deviceauth.NewAggregate("123", "instance1"), "clientID", "123", "456", time.Now().Add(time.Minute), []string{"openid", "offline_access"}, - []string{"audience"}, false, + []string{"audience"}, false, "", ), ), ), @@ -806,7 +1009,7 @@ func TestCommands_CreateOIDCSessionFromDeviceAuth(t *testing.T) { deviceauth.NewAggregate("123", "instance1"), "clientID", "123", "456", time.Now().Add(-time.Minute), []string{"openid", "offline_access"}, - []string{"audience"}, false, + []string{"audience"}, false, "", ), ), ), @@ -835,7 +1038,7 @@ func TestCommands_CreateOIDCSessionFromDeviceAuth(t *testing.T) { deviceauth.NewAggregate("123", "instance1"), "clientID", "123", "456", time.Now().Add(-time.Minute), []string{"openid", "offline_access"}, - []string{"audience"}, false, + []string{"audience"}, false, "", ), ), eventFromEventPusherWithInstanceID( @@ -867,7 +1070,7 @@ func TestCommands_CreateOIDCSessionFromDeviceAuth(t *testing.T) { deviceauth.NewAggregate("123", "instance1"), "clientID", "123", "456", time.Now().Add(-time.Minute), []string{"openid", "offline_access"}, - []string{"audience"}, false, + []string{"audience"}, false, "", ), ), eventFromEventPusherWithInstanceID( @@ -899,7 +1102,7 @@ func TestCommands_CreateOIDCSessionFromDeviceAuth(t *testing.T) { deviceauth.NewAggregate("123", "instance1"), "clientID", "123", "456", time.Now().Add(-time.Minute), []string{"openid", "offline_access"}, - []string{"audience"}, false, + []string{"audience"}, false, "", ), ), eventFromEventPusherWithInstanceID( @@ -937,7 +1140,7 @@ func TestCommands_CreateOIDCSessionFromDeviceAuth(t *testing.T) { deviceauth.NewAggregate("123", "instance1"), "clientID", "123", "456", time.Now().Add(-time.Minute), []string{"openid", "offline_access"}, - []string{"audience"}, false, + []string{"audience"}, false, "", ), ), eventFromEventPusherWithInstanceID( @@ -1001,7 +1204,7 @@ func TestCommands_CreateOIDCSessionFromDeviceAuth(t *testing.T) { deviceauth.NewAggregate("123", "instance1"), "clientID", "123", "456", time.Now().Add(-time.Minute), []string{"openid", "offline_access"}, - []string{"audience"}, false, + []string{"audience"}, false, "", ), ), eventFromEventPusherWithInstanceID( @@ -1098,7 +1301,7 @@ func TestCommands_CreateOIDCSessionFromDeviceAuth(t *testing.T) { deviceauth.NewAggregate("123", "instance1"), "clientID", "123", "456", time.Now().Add(-time.Minute), []string{"openid", "offline_access"}, - []string{"audience"}, false, + []string{"audience"}, false, "", ), ), eventFromEventPusherWithInstanceID( @@ -1202,7 +1405,7 @@ func TestCommands_CreateOIDCSessionFromDeviceAuth(t *testing.T) { deviceauth.NewAggregate("123", "instance1"), "clientID", "123", "456", time.Now().Add(-time.Minute), []string{"openid", "offline_access"}, - []string{"audience"}, true, + []string{"audience"}, true, "", ), ), eventFromEventPusherWithInstanceID( diff --git a/internal/command/oidc_session_test.go b/internal/command/oidc_session_test.go index f66c6dffa..4ee95cb7f 100644 --- a/internal/command/oidc_session_test.go +++ b/internal/command/oidc_session_test.go @@ -138,6 +138,7 @@ func TestCommands_CreateOIDCSessionFromAuthRequest(t *testing.T) { gu.Ptr("hintUserID"), true, "issuer", + "", ), ), eventFromEventPusher( @@ -183,6 +184,7 @@ func TestCommands_CreateOIDCSessionFromAuthRequest(t *testing.T) { gu.Ptr("hintUserID"), true, "issuer", + "", ), ), eventFromEventPusher( @@ -236,6 +238,7 @@ func TestCommands_CreateOIDCSessionFromAuthRequest(t *testing.T) { gu.Ptr("hintUserID"), true, "issuer", + "", ), ), eventFromEventPusher( @@ -334,6 +337,7 @@ func TestCommands_CreateOIDCSessionFromAuthRequest(t *testing.T) { gu.Ptr("hintUserID"), true, "issuer", + "", ), ), eventFromEventPusher( @@ -468,6 +472,7 @@ func TestCommands_CreateOIDCSessionFromAuthRequest(t *testing.T) { gu.Ptr("hintUserID"), true, "issuer", + "", ), ), eventFromEventPusher( @@ -611,6 +616,7 @@ func TestCommands_CreateOIDCSessionFromAuthRequest(t *testing.T) { gu.Ptr("hintUserID"), false, "issuer", + "", ), ), eventFromEventPusher( diff --git a/internal/domain/auth_request.go b/internal/domain/auth_request.go index ea746a383..cd6594a30 100644 --- a/internal/domain/auth_request.go +++ b/internal/domain/auth_request.go @@ -230,24 +230,18 @@ func (a *AuthRequest) AppendAudIfNotExisting(aud string) { } func (a *AuthRequest) GetScopeOrgPrimaryDomain() string { - switch request := a.Request.(type) { - case *AuthRequestOIDC: - for _, scope := range request.Scopes { - if strings.HasPrefix(scope, OrgDomainPrimaryScope) { - return strings.TrimPrefix(scope, OrgDomainPrimaryScope) - } + for _, scope := range a.Request.GetScopes() { + if strings.HasPrefix(scope, OrgDomainPrimaryScope) { + return strings.TrimPrefix(scope, OrgDomainPrimaryScope) } } return "" } func (a *AuthRequest) GetScopeOrgID() string { - switch request := a.Request.(type) { - case *AuthRequestOIDC: - for _, scope := range request.Scopes { - if strings.HasPrefix(scope, OrgIDScope) { - return strings.TrimPrefix(scope, OrgIDScope) - } + for _, scope := range a.Request.GetScopes() { + if strings.HasPrefix(scope, OrgIDScope) { + return strings.TrimPrefix(scope, OrgIDScope) } } return "" diff --git a/internal/domain/request.go b/internal/domain/request.go index 92e45c0d2..e3da8e5c2 100644 --- a/internal/domain/request.go +++ b/internal/domain/request.go @@ -17,6 +17,7 @@ const ( type Request interface { Type() AuthRequestType IsValid() bool + GetScopes() []string } type AuthRequestType int32 @@ -35,6 +36,18 @@ type AuthRequestOIDC struct { CodeChallenge *OIDCCodeChallenge } +func NewAuthRequestOIDC( + scopes []string, + responseType OIDCResponseType, + responseMode OIDCResponseMode, + nonce string, + codeChallenge *OIDCCodeChallenge, +) *AuthRequestOIDC { + return &AuthRequestOIDC{ + scopes, responseType, responseMode, nonce, codeChallenge, + } +} + func (a *AuthRequestOIDC) Type() AuthRequestType { return AuthRequestTypeOIDC } @@ -44,6 +57,10 @@ func (a *AuthRequestOIDC) IsValid() bool { a.CodeChallenge == nil || a.CodeChallenge != nil && a.CodeChallenge.IsValid() } +func (a *AuthRequestOIDC) GetScopes() []string { + return a.Scopes +} + type AuthRequestSAML struct { ID string BindingType string @@ -61,6 +78,10 @@ func (a *AuthRequestSAML) IsValid() bool { return true } +func (*AuthRequestSAML) GetScopes() []string { + return nil +} + type AuthRequestDevice struct { ClientID string DeviceCode string @@ -71,6 +92,20 @@ type AuthRequestDevice struct { ProjectName string } +func NewAuthRequestDevice( + clientID string, + deviceCode string, + userCode string, + scopes []string, + audience []string, + appName string, + projectName string, +) *AuthRequestDevice { + return &AuthRequestDevice{ + clientID, deviceCode, userCode, scopes, audience, appName, projectName, + } +} + func (*AuthRequestDevice) Type() AuthRequestType { return AuthRequestTypeDevice } @@ -78,3 +113,7 @@ func (*AuthRequestDevice) Type() AuthRequestType { func (a *AuthRequestDevice) IsValid() bool { return a.DeviceCode != "" && a.UserCode != "" } + +func (a *AuthRequestDevice) GetScopes() []string { + return a.Scopes +} diff --git a/internal/query/device_auth.go b/internal/query/device_auth.go index d2f86a44a..a67091c2c 100644 --- a/internal/query/device_auth.go +++ b/internal/query/device_auth.go @@ -98,8 +98,10 @@ func prepareDeviceAuthQuery() (sq.SelectBuilder, func(*sql.Row) (*domain.AuthReq LeftJoin(join(ProjectColumnID, AppColumnProjectID)). PlaceholderFormat(sq.Dollar), func(row *sql.Row) (*domain.AuthRequestDevice, error) { - dst := new(domain.AuthRequestDevice) var ( + clientID string + deviceCode string + userCode string scopes database.TextArray[string] audience database.TextArray[string] appName sql.NullString @@ -107,9 +109,9 @@ func prepareDeviceAuthQuery() (sq.SelectBuilder, func(*sql.Row) (*domain.AuthReq ) err := row.Scan( - &dst.ClientID, - &dst.DeviceCode, - &dst.UserCode, + &clientID, + &deviceCode, + &userCode, &scopes, &audience, &appName, @@ -121,11 +123,6 @@ func prepareDeviceAuthQuery() (sq.SelectBuilder, func(*sql.Row) (*domain.AuthReq if err != nil { return nil, zerrors.ThrowInternal(err, "QUERY-Voo3o", "Errors.Internal") } - dst.Scopes = scopes - dst.Audience = audience - dst.AppName = appName.String - dst.ProjectName = projectName.String - - return dst, nil + return domain.NewAuthRequestDevice(clientID, deviceCode, userCode, scopes, audience, appName.String, projectName.String), nil } } diff --git a/internal/query/device_auth_test.go b/internal/query/device_auth_test.go index 52ac50abb..627a791cf 100644 --- a/internal/query/device_auth_test.go +++ b/internal/query/device_auth_test.go @@ -52,15 +52,15 @@ var ( "appName", "projectName", } - expectedDeviceAuth = &domain.AuthRequestDevice{ - ClientID: "client-id", - DeviceCode: "device1", - UserCode: "user-code", - Scopes: []string{"a", "b", "c"}, - Audience: []string{"projectID", "clientID"}, - AppName: "appName", - ProjectName: "projectName", - } + expectedDeviceAuth = domain.NewAuthRequestDevice( + "client-id", + "device1", + "user-code", + []string{"a", "b", "c"}, + []string{"projectID", "clientID"}, + "appName", + "projectName", + ) ) func TestQueries_DeviceAuthRequestByUserCode(t *testing.T) { diff --git a/internal/repository/authrequest/auth_request.go b/internal/repository/authrequest/auth_request.go index 75624e3a2..0b2ae61e0 100644 --- a/internal/repository/authrequest/auth_request.go +++ b/internal/repository/authrequest/auth_request.go @@ -39,6 +39,7 @@ type AddedEvent struct { HintUserID *string `json:"hint_user_id,omitempty"` NeedRefreshToken bool `json:"need_refresh_token,omitempty"` Issuer string `json:"issuer,omitempty"` + OrganizationID string `json:"organization_id,omitempty"` } func (e *AddedEvent) Payload() interface{} { @@ -67,7 +68,8 @@ func NewAddedEvent(ctx context.Context, loginHint, hintUserID *string, needRefreshToken bool, - issuer string, + issuer, + organizationID string, ) *AddedEvent { return &AddedEvent{ BaseEvent: *eventstore.NewBaseEventForPush( @@ -92,6 +94,7 @@ func NewAddedEvent(ctx context.Context, HintUserID: hintUserID, NeedRefreshToken: needRefreshToken, Issuer: issuer, + OrganizationID: organizationID, } } diff --git a/internal/repository/deviceauth/device_auth.go b/internal/repository/deviceauth/device_auth.go index 7d96d8c2c..e60ef4bcb 100644 --- a/internal/repository/deviceauth/device_auth.go +++ b/internal/repository/deviceauth/device_auth.go @@ -29,6 +29,7 @@ type AddedEvent struct { Audience []string State domain.DeviceAuthState NeedRefreshToken bool + OrganizationID string } func (e *AddedEvent) SetBaseEvent(b *eventstore.BaseEvent) { @@ -53,6 +54,7 @@ func NewAddedEvent( scopes []string, audience []string, needRefreshToken bool, + organizationID string, ) *AddedEvent { return &AddedEvent{ eventstore.NewBaseEventForPush( @@ -60,6 +62,7 @@ func NewAddedEvent( ), clientID, deviceCode, userCode, expires, scopes, audience, domain.DeviceAuthStateInitiated, needRefreshToken, + organizationID, } }