mirror of
https://github.com/zitadel/zitadel.git
synced 2024-12-04 23:45:07 +00:00
feat(API): support V2 token and session token usage (#6180)
This PR adds support for userinfo and introspection of V2 tokens. Further V2 access tokens and session tokens can be used for authentication on the ZITADEL API (like the current access tokens).
This commit is contained in:
parent
4589ddad4a
commit
80961125a7
2
.github/workflows/integration.yml
vendored
2
.github/workflows/integration.yml
vendored
@ -22,7 +22,7 @@ jobs:
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: 1.19
|
||||
go-version: '1.20'
|
||||
- name: Source checkout
|
||||
uses: actions/checkout@v3
|
||||
- name: Set up Docker Buildx
|
||||
|
2
.github/workflows/test-code.yml
vendored
2
.github/workflows/test-code.yml
vendored
@ -17,7 +17,7 @@ jobs:
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: 1.19
|
||||
go-version: '1.20'
|
||||
- name: Source checkout
|
||||
uses: actions/checkout@v3
|
||||
- name: Set up QEMU
|
||||
|
2
.github/workflows/zitadel.yml
vendored
2
.github/workflows/zitadel.yml
vendored
@ -19,7 +19,7 @@ jobs:
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: 1.19
|
||||
go-version: '1.20'
|
||||
- name: Source checkout
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
|
@ -1,4 +1,4 @@
|
||||
ARG GO_VERSION=1.19
|
||||
ARG GO_VERSION=1.20
|
||||
|
||||
#######################
|
||||
## Go dependencies
|
||||
|
2
go.mod
2
go.mod
@ -1,6 +1,6 @@
|
||||
module github.com/zitadel/zitadel
|
||||
|
||||
go 1.19
|
||||
go 1.20
|
||||
|
||||
require (
|
||||
cloud.google.com/go/storage v1.30.1
|
||||
|
@ -20,7 +20,8 @@ import (
|
||||
|
||||
const (
|
||||
BearerPrefix = "Bearer "
|
||||
SessionTokenFormat = "sess_%s:%s"
|
||||
SessionTokenPrefix = "sess_"
|
||||
SessionTokenFormat = SessionTokenPrefix + "%s:%s"
|
||||
)
|
||||
|
||||
type TokenVerifier struct {
|
||||
|
@ -49,7 +49,9 @@ func TestMain(m *testing.M) {
|
||||
}
|
||||
|
||||
func TestServer_GetAuthRequest(t *testing.T) {
|
||||
client, err := Tester.CreateOIDCNativeClient(CTX, redirectURI)
|
||||
project, err := Tester.CreateProject(CTX)
|
||||
require.NoError(t, err)
|
||||
client, err := Tester.CreateOIDCNativeClient(CTX, redirectURI, project.GetId())
|
||||
require.NoError(t, err)
|
||||
authRequestID, err := Tester.CreateOIDCAuthRequest(client.GetClientId(), Tester.Users[integration.FirstInstanceUsersKey][integration.OrgOwner].ID, redirectURI)
|
||||
require.NoError(t, err)
|
||||
@ -91,7 +93,9 @@ func TestServer_GetAuthRequest(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestServer_CreateCallback(t *testing.T) {
|
||||
client, err := Tester.CreateOIDCNativeClient(CTX, redirectURI)
|
||||
project, err := Tester.CreateProject(CTX)
|
||||
require.NoError(t, err)
|
||||
client, err := Tester.CreateOIDCNativeClient(CTX, redirectURI, project.GetId())
|
||||
require.NoError(t, err)
|
||||
sessionResp, err := Tester.Client.SessionV2.CreateSession(CTX, &session.CreateSessionRequest{
|
||||
Checks: &session.Checks{
|
||||
|
@ -156,10 +156,11 @@ func userFactorToPb(factor query.SessionUserFactor) *session.UserFactor {
|
||||
return nil
|
||||
}
|
||||
return &session.UserFactor{
|
||||
VerifiedAt: timestamppb.New(factor.UserCheckedAt),
|
||||
Id: factor.UserID,
|
||||
LoginName: factor.LoginName,
|
||||
DisplayName: factor.DisplayName,
|
||||
VerifiedAt: timestamppb.New(factor.UserCheckedAt),
|
||||
Id: factor.UserID,
|
||||
LoginName: factor.LoginName,
|
||||
DisplayName: factor.DisplayName,
|
||||
OrganisationId: factor.ResourceOwner,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -4,12 +4,15 @@ package session_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/muhlemmer/gu"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/grpc/metadata"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/integration"
|
||||
object "github.com/zitadel/zitadel/pkg/grpc/object/v2alpha"
|
||||
@ -35,6 +38,7 @@ func TestMain(m *testing.M) {
|
||||
|
||||
CTX, _ = Tester.WithAuthorization(ctx, integration.OrgOwner), errCtx
|
||||
User = Tester.CreateHumanUser(CTX)
|
||||
Tester.SetUserPassword(CTX, User.GetUserId(), integration.UserPassword)
|
||||
Tester.RegisterUserPasskey(CTX, User.GetUserId())
|
||||
return m.Run()
|
||||
}())
|
||||
@ -377,3 +381,51 @@ func TestServer_SetSession_flow(t *testing.T) {
|
||||
verifyCurrentSession(t, createResp.GetSessionId(), resp.GetSessionToken(), resp.GetDetails().GetSequence(), time.Minute, nil, wantFactors...)
|
||||
})
|
||||
}
|
||||
|
||||
func Test_ZITADEL_API_missing_authentication(t *testing.T) {
|
||||
// create new, empty session
|
||||
createResp, err := Client.CreateSession(CTX, &session.CreateSessionRequest{Domain: Tester.Config.ExternalDomain})
|
||||
require.NoError(t, err)
|
||||
|
||||
ctx := metadata.AppendToOutgoingContext(context.Background(), "Authorization", fmt.Sprintf("Bearer %s", createResp.GetSessionToken()))
|
||||
sessionResp, err := Tester.Client.SessionV2.GetSession(ctx, &session.GetSessionRequest{SessionId: createResp.GetSessionId()})
|
||||
require.Error(t, err)
|
||||
require.Nil(t, sessionResp)
|
||||
}
|
||||
|
||||
func Test_ZITADEL_API_missing_mfa(t *testing.T) {
|
||||
id, token, _, _ := Tester.CreatePasswordSession(t, CTX, User.GetUserId(), integration.UserPassword)
|
||||
|
||||
ctx := metadata.AppendToOutgoingContext(context.Background(), "Authorization", fmt.Sprintf("Bearer %s", token))
|
||||
sessionResp, err := Tester.Client.SessionV2.GetSession(ctx, &session.GetSessionRequest{SessionId: id})
|
||||
require.Error(t, err)
|
||||
require.Nil(t, sessionResp)
|
||||
}
|
||||
|
||||
func Test_ZITADEL_API_success(t *testing.T) {
|
||||
id, token, _, _ := Tester.CreatePasskeySession(t, CTX, User.GetUserId())
|
||||
|
||||
ctx := metadata.AppendToOutgoingContext(context.Background(), "Authorization", fmt.Sprintf("Bearer %s", token))
|
||||
sessionResp, err := Tester.Client.SessionV2.GetSession(ctx, &session.GetSessionRequest{SessionId: id})
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, id, sessionResp.GetSession().GetFactors().GetPasskey().GetVerifiedAt().AsTime())
|
||||
}
|
||||
|
||||
func Test_ZITADEL_API_session_not_found(t *testing.T) {
|
||||
id, token, _, _ := Tester.CreatePasskeySession(t, CTX, User.GetUserId())
|
||||
|
||||
// test session token works
|
||||
ctx := metadata.AppendToOutgoingContext(context.Background(), "Authorization", fmt.Sprintf("Bearer %s", token))
|
||||
_, err := Tester.Client.SessionV2.GetSession(ctx, &session.GetSessionRequest{SessionId: id})
|
||||
require.NoError(t, err)
|
||||
|
||||
//terminate the session and test it does not work anymore
|
||||
_, err = Tester.Client.SessionV2.DeleteSession(CTX, &session.DeleteSessionRequest{
|
||||
SessionId: id,
|
||||
SessionToken: gu.Ptr(token),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
ctx = metadata.AppendToOutgoingContext(context.Background(), "Authorization", fmt.Sprintf("Bearer %s", token))
|
||||
_, err = Tester.Client.SessionV2.GetSession(ctx, &session.GetSessionRequest{SessionId: id})
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
@ -46,6 +46,7 @@ func Test_sessionsToPb(t *testing.T) {
|
||||
UserCheckedAt: past,
|
||||
LoginName: "donald",
|
||||
DisplayName: "donald duck",
|
||||
ResourceOwner: "org1",
|
||||
},
|
||||
Metadata: map[string][]byte{"hello": []byte("world")},
|
||||
},
|
||||
@ -62,6 +63,7 @@ func Test_sessionsToPb(t *testing.T) {
|
||||
UserCheckedAt: past,
|
||||
LoginName: "donald",
|
||||
DisplayName: "donald duck",
|
||||
ResourceOwner: "org1",
|
||||
},
|
||||
PasswordFactor: query.SessionPasswordFactor{
|
||||
PasswordCheckedAt: past,
|
||||
@ -81,6 +83,7 @@ func Test_sessionsToPb(t *testing.T) {
|
||||
UserCheckedAt: past,
|
||||
LoginName: "donald",
|
||||
DisplayName: "donald duck",
|
||||
ResourceOwner: "org1",
|
||||
},
|
||||
PasskeyFactor: query.SessionPasskeyFactor{
|
||||
PasskeyCheckedAt: past,
|
||||
@ -105,10 +108,11 @@ func Test_sessionsToPb(t *testing.T) {
|
||||
Sequence: 123,
|
||||
Factors: &session.Factors{
|
||||
User: &session.UserFactor{
|
||||
VerifiedAt: timestamppb.New(past),
|
||||
Id: "345",
|
||||
LoginName: "donald",
|
||||
DisplayName: "donald duck",
|
||||
VerifiedAt: timestamppb.New(past),
|
||||
Id: "345",
|
||||
LoginName: "donald",
|
||||
DisplayName: "donald duck",
|
||||
OrganisationId: "org1",
|
||||
},
|
||||
},
|
||||
Metadata: map[string][]byte{"hello": []byte("world")},
|
||||
@ -120,10 +124,11 @@ func Test_sessionsToPb(t *testing.T) {
|
||||
Sequence: 123,
|
||||
Factors: &session.Factors{
|
||||
User: &session.UserFactor{
|
||||
VerifiedAt: timestamppb.New(past),
|
||||
Id: "345",
|
||||
LoginName: "donald",
|
||||
DisplayName: "donald duck",
|
||||
VerifiedAt: timestamppb.New(past),
|
||||
Id: "345",
|
||||
LoginName: "donald",
|
||||
DisplayName: "donald duck",
|
||||
OrganisationId: "org1",
|
||||
},
|
||||
Password: &session.PasswordFactor{
|
||||
VerifiedAt: timestamppb.New(past),
|
||||
@ -138,10 +143,11 @@ func Test_sessionsToPb(t *testing.T) {
|
||||
Sequence: 123,
|
||||
Factors: &session.Factors{
|
||||
User: &session.UserFactor{
|
||||
VerifiedAt: timestamppb.New(past),
|
||||
Id: "345",
|
||||
LoginName: "donald",
|
||||
DisplayName: "donald duck",
|
||||
VerifiedAt: timestamppb.New(past),
|
||||
Id: "345",
|
||||
LoginName: "donald",
|
||||
DisplayName: "donald duck",
|
||||
OrganisationId: "org1",
|
||||
},
|
||||
Passkey: &session.PasskeyFactor{
|
||||
VerifiedAt: timestamppb.New(past),
|
||||
|
@ -5,7 +5,6 @@ package oidc_test
|
||||
import (
|
||||
"context"
|
||||
"net/url"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@ -17,63 +16,14 @@ import (
|
||||
|
||||
oidc_api "github.com/zitadel/zitadel/internal/api/oidc"
|
||||
"github.com/zitadel/zitadel/internal/command"
|
||||
"github.com/zitadel/zitadel/internal/integration"
|
||||
oidc_pb "github.com/zitadel/zitadel/pkg/grpc/oidc/v2alpha"
|
||||
user "github.com/zitadel/zitadel/pkg/grpc/user/v2alpha"
|
||||
)
|
||||
|
||||
var (
|
||||
CTX context.Context
|
||||
CTXLOGIN context.Context
|
||||
Tester *integration.Tester
|
||||
User *user.AddHumanUserResponse
|
||||
armPasskey = []string{oidc_api.UserPresence, oidc_api.MFA}
|
||||
armPassword = []string{oidc_api.PWD}
|
||||
)
|
||||
|
||||
const (
|
||||
redirectURI = "oidcIntegrationTest://callback"
|
||||
redirectURIImplicit = "http://localhost:9999/callback"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
os.Exit(func() int {
|
||||
ctx, errCtx, cancel := integration.Contexts(5 * time.Minute)
|
||||
defer cancel()
|
||||
|
||||
Tester = integration.NewTester(ctx)
|
||||
defer Tester.Done()
|
||||
|
||||
CTX, _ = Tester.WithAuthorization(ctx, integration.OrgOwner), errCtx
|
||||
User = Tester.CreateHumanUser(CTX)
|
||||
Tester.RegisterUserPasskey(CTX, User.GetUserId())
|
||||
CTXLOGIN, _ = Tester.WithAuthorization(ctx, integration.Login), errCtx
|
||||
return m.Run()
|
||||
}())
|
||||
}
|
||||
|
||||
func createClient(t testing.TB) string {
|
||||
app, err := Tester.CreateOIDCNativeClient(CTX, redirectURI)
|
||||
require.NoError(t, err)
|
||||
return app.GetClientId()
|
||||
}
|
||||
|
||||
func createImplicitClient(t testing.TB) string {
|
||||
app, err := Tester.CreateOIDCImplicitFlowClient(CTX, redirectURIImplicit)
|
||||
require.NoError(t, err)
|
||||
return app.GetClientId()
|
||||
}
|
||||
|
||||
func createAuthRequest(t testing.TB, clientID, redirectURI string, scope ...string) string {
|
||||
redURL, err := Tester.CreateOIDCAuthRequest(clientID, Tester.Users[integration.FirstInstanceUsersKey][integration.Login].ID, redirectURI, scope...)
|
||||
require.NoError(t, err)
|
||||
return redURL
|
||||
}
|
||||
|
||||
func createAuthRequestImplicit(t testing.TB, clientID, redirectURI string, scope ...string) string {
|
||||
redURL, err := Tester.CreateOIDCAuthRequestImplicit(clientID, Tester.Users[integration.FirstInstanceUsersKey][integration.Login].ID, redirectURI, scope...)
|
||||
require.NoError(t, err)
|
||||
return redURL
|
||||
}
|
||||
|
||||
func TestOPStorage_CreateAuthRequest(t *testing.T) {
|
||||
clientID := createClient(t)
|
||||
|
||||
@ -101,7 +51,7 @@ func TestOPStorage_CreateAccessToken_code(t *testing.T) {
|
||||
tokens, err := exchangeTokens(t, clientID, code)
|
||||
require.NoError(t, err)
|
||||
assertTokens(t, tokens, false)
|
||||
assertTokenClaims(t, tokens.IDTokenClaims, startTime, changeTime)
|
||||
assertIDTokenClaims(t, tokens.IDTokenClaims, armPasskey, startTime, changeTime)
|
||||
|
||||
// callback on a succeeded request must fail
|
||||
linkResp, err = Tester.Client.OIDCv2.CreateCallback(CTXLOGIN, &oidc_pb.CreateCallbackRequest{
|
||||
@ -155,7 +105,7 @@ func TestOPStorage_CreateAccessToken_implicit(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
claims, err := rp.VerifyTokens[*oidc.IDTokenClaims](context.Background(), accessToken, idToken, provider.IDTokenVerifier())
|
||||
require.NoError(t, err)
|
||||
assertTokenClaims(t, claims, startTime, changeTime)
|
||||
assertIDTokenClaims(t, claims, armPasskey, startTime, changeTime)
|
||||
|
||||
// callback on a succeeded request must fail
|
||||
linkResp, err = Tester.Client.OIDCv2.CreateCallback(CTXLOGIN, &oidc_pb.CreateCallbackRequest{
|
||||
@ -190,7 +140,7 @@ func TestOPStorage_CreateAccessAndRefreshTokens_code(t *testing.T) {
|
||||
tokens, err := exchangeTokens(t, clientID, code)
|
||||
require.NoError(t, err)
|
||||
assertTokens(t, tokens, true)
|
||||
assertTokenClaims(t, tokens.IDTokenClaims, startTime, changeTime)
|
||||
assertIDTokenClaims(t, tokens.IDTokenClaims, armPasskey, startTime, changeTime)
|
||||
}
|
||||
|
||||
func TestOPStorage_CreateAccessAndRefreshTokens_refresh(t *testing.T) {
|
||||
@ -215,7 +165,7 @@ func TestOPStorage_CreateAccessAndRefreshTokens_refresh(t *testing.T) {
|
||||
tokens, err := exchangeTokens(t, clientID, code)
|
||||
require.NoError(t, err)
|
||||
assertTokens(t, tokens, true)
|
||||
assertTokenClaims(t, tokens.IDTokenClaims, startTime, changeTime)
|
||||
assertIDTokenClaims(t, tokens.IDTokenClaims, armPasskey, startTime, changeTime)
|
||||
|
||||
// test actual refresh grant
|
||||
newTokens, err := refreshTokens(t, clientID, tokens.RefreshToken)
|
||||
@ -227,7 +177,7 @@ func TestOPStorage_CreateAccessAndRefreshTokens_refresh(t *testing.T) {
|
||||
claims, err := rp.VerifyTokens[*oidc.IDTokenClaims](context.Background(), newTokens.AccessToken, idToken, provider.IDTokenVerifier())
|
||||
require.NoError(t, err)
|
||||
// auth time must still be the initial
|
||||
assertTokenClaims(t, claims, startTime, changeTime)
|
||||
assertIDTokenClaims(t, claims, armPasskey, startTime, changeTime)
|
||||
|
||||
// refresh with an old refresh_token must fail
|
||||
_, err = rp.RefreshAccessToken(provider, tokens.RefreshToken, "", "")
|
||||
@ -268,8 +218,8 @@ func assertTokens(t *testing.T, tokens *oidc.Tokens[*oidc.IDTokenClaims], requir
|
||||
}
|
||||
}
|
||||
|
||||
func assertTokenClaims(t *testing.T, claims *oidc.IDTokenClaims, sessionStart, sessionChange time.Time) {
|
||||
func assertIDTokenClaims(t *testing.T, claims *oidc.IDTokenClaims, arm []string, sessionStart, sessionChange time.Time) {
|
||||
assert.Equal(t, User.GetUserId(), claims.Subject)
|
||||
assert.Equal(t, []string{oidc_api.UserPresence, oidc_api.MFA}, claims.AuthenticationMethodsReferences)
|
||||
assert.WithinRange(t, claims.AuthTime.AsTime().UTC(), sessionStart.Add(-1*time.Second), sessionChange.Add(1*time.Second))
|
||||
assert.Equal(t, arm, claims.AuthenticationMethodsReferences)
|
||||
assertOIDCTimeRange(t, claims.AuthTime, sessionStart, sessionChange)
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/dop251/goja"
|
||||
"github.com/zitadel/logging"
|
||||
@ -17,6 +18,7 @@ import (
|
||||
"github.com/zitadel/zitadel/internal/actions/object"
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
api_http "github.com/zitadel/zitadel/internal/api/http"
|
||||
"github.com/zitadel/zitadel/internal/command"
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/errors"
|
||||
@ -119,18 +121,26 @@ func (o *OPStorage) AuthorizeClientIDSecret(ctx context.Context, id string, secr
|
||||
func (o *OPStorage) SetUserinfoFromToken(ctx context.Context, userInfo *oidc.UserInfo, tokenID, subject, origin string) (err error) {
|
||||
ctx, span := tracing.NewSpan(ctx)
|
||||
defer func() { span.EndWithError(err) }()
|
||||
|
||||
if strings.HasPrefix(tokenID, command.IDPrefixV2) {
|
||||
token, err := o.query.ActiveAccessTokenByToken(ctx, tokenID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = o.isOriginAllowed(ctx, token.ClientID, origin); err != nil {
|
||||
return err
|
||||
}
|
||||
return o.setUserinfo(ctx, userInfo, token.UserID, token.ClientID, token.Scope, nil)
|
||||
}
|
||||
|
||||
token, err := o.repo.TokenByIDs(ctx, subject, tokenID)
|
||||
if err != nil {
|
||||
return errors.ThrowPermissionDenied(nil, "OIDC-Dsfb2", "token is not valid or has expired")
|
||||
}
|
||||
if token.ApplicationID != "" {
|
||||
app, err := o.query.AppByOIDCClientID(ctx, token.ApplicationID, false)
|
||||
if err != nil {
|
||||
if err = o.isOriginAllowed(ctx, token.ApplicationID, origin); err != nil {
|
||||
return err
|
||||
}
|
||||
if origin != "" && !api_http.IsOriginAllowed(app.OIDCConfig.AllowedOrigins, origin) {
|
||||
return errors.ThrowPermissionDenied(nil, "OIDC-da1f3", "origin is not allowed")
|
||||
}
|
||||
}
|
||||
return o.setUserinfo(ctx, userInfo, token.UserID, token.ApplicationID, token.Scopes, nil)
|
||||
}
|
||||
@ -154,6 +164,24 @@ func (o *OPStorage) SetUserinfoFromScopes(ctx context.Context, userInfo *oidc.Us
|
||||
}
|
||||
|
||||
func (o *OPStorage) SetIntrospectionFromToken(ctx context.Context, introspection *oidc.IntrospectionResponse, tokenID, subject, clientID string) (err error) {
|
||||
ctx, span := tracing.NewSpan(ctx)
|
||||
defer func() { span.EndWithError(err) }()
|
||||
|
||||
if strings.HasPrefix(tokenID, command.IDPrefixV2) {
|
||||
token, err := o.query.ActiveAccessTokenByToken(ctx, tokenID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
projectID, err := o.query.ProjectIDFromClientID(ctx, clientID, false)
|
||||
if err != nil {
|
||||
return errors.ThrowPermissionDenied(nil, "OIDC-Adfg5", "client not found")
|
||||
}
|
||||
return o.introspect(ctx, introspection,
|
||||
tokenID, token.UserID, token.ClientID, projectID,
|
||||
token.Audience, token.Scope,
|
||||
token.AccessTokenCreation, token.AccessTokenExpiration)
|
||||
}
|
||||
|
||||
token, err := o.repo.TokenByIDs(ctx, subject, tokenID)
|
||||
if err != nil {
|
||||
return errors.ThrowPermissionDenied(nil, "OIDC-Dsfb2", "token is not valid or has expired")
|
||||
@ -168,27 +196,10 @@ func (o *OPStorage) SetIntrospectionFromToken(ctx context.Context, introspection
|
||||
return errors.ThrowPreconditionFailed(err, "OIDC-AGefw", "Errors.Internal")
|
||||
}
|
||||
}
|
||||
for _, aud := range token.Audience {
|
||||
if aud == clientID || aud == projectID {
|
||||
userInfo := new(oidc.UserInfo)
|
||||
err := o.setUserinfo(ctx, userInfo, subject, clientID, token.Scopes, []string{projectID}) // always
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
introspection.SetUserInfo(userInfo)
|
||||
introspection.Scope = token.Scopes
|
||||
introspection.ClientID = token.ApplicationID
|
||||
introspection.TokenType = oidc.BearerToken
|
||||
introspection.Expiration = oidc.FromTime(token.Expiration)
|
||||
introspection.IssuedAt = oidc.FromTime(token.CreationDate)
|
||||
introspection.NotBefore = oidc.FromTime(token.CreationDate)
|
||||
introspection.Audience = token.Audience
|
||||
introspection.Issuer = op.IssuerFromContext(ctx)
|
||||
introspection.JWTID = token.ID
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return errors.ThrowPermissionDenied(nil, "OIDC-sdg3G", "token is not valid for this client")
|
||||
return o.introspect(ctx, introspection,
|
||||
token.ID, token.UserID, token.ApplicationID, projectID,
|
||||
token.Audience, token.Scopes,
|
||||
token.CreationDate, token.Expiration)
|
||||
}
|
||||
|
||||
func (o *OPStorage) ClientCredentialsTokenRequest(ctx context.Context, clientID string, scope []string) (op.TokenRequest, error) {
|
||||
@ -230,6 +241,55 @@ func (o *OPStorage) ClientCredentials(ctx context.Context, clientID, clientSecre
|
||||
}, nil
|
||||
}
|
||||
|
||||
// isOriginAllowed checks whether a call by the client to the endpoint is allowed from the provided origin
|
||||
// if no origin is provided, no error will be returned
|
||||
func (o *OPStorage) isOriginAllowed(ctx context.Context, clientID, origin string) error {
|
||||
if origin == "" {
|
||||
return nil
|
||||
}
|
||||
app, err := o.query.AppByOIDCClientID(ctx, clientID, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if api_http.IsOriginAllowed(app.OIDCConfig.AllowedOrigins, origin) {
|
||||
return nil
|
||||
}
|
||||
return errors.ThrowPermissionDenied(nil, "OIDC-da1f3", "origin is not allowed")
|
||||
}
|
||||
|
||||
func (o *OPStorage) introspect(
|
||||
ctx context.Context,
|
||||
introspection *oidc.IntrospectionResponse,
|
||||
tokenID, subject, clientID, projectID string,
|
||||
audience, scope []string,
|
||||
tokenCreation, tokenExpiration time.Time,
|
||||
) (err error) {
|
||||
ctx, span := tracing.NewSpan(ctx)
|
||||
defer func() { span.EndWithError(err) }()
|
||||
|
||||
for _, aud := range audience {
|
||||
if aud == clientID || aud == projectID {
|
||||
userInfo := new(oidc.UserInfo)
|
||||
err = o.setUserinfo(ctx, userInfo, subject, clientID, scope, []string{projectID}) // always
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
introspection.SetUserInfo(userInfo)
|
||||
introspection.Scope = scope
|
||||
introspection.ClientID = clientID
|
||||
introspection.TokenType = oidc.BearerToken
|
||||
introspection.Expiration = oidc.FromTime(tokenExpiration)
|
||||
introspection.IssuedAt = oidc.FromTime(tokenCreation)
|
||||
introspection.NotBefore = oidc.FromTime(tokenCreation)
|
||||
introspection.Audience = audience
|
||||
introspection.Issuer = op.IssuerFromContext(ctx)
|
||||
introspection.JWTID = tokenID
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return errors.ThrowPermissionDenied(nil, "OIDC-sdg3G", "token is not valid for this client")
|
||||
}
|
||||
|
||||
func (o *OPStorage) checkOrgScopes(ctx context.Context, user *query.User, scopes []string) ([]string, error) {
|
||||
for i := len(scopes) - 1; i >= 0; i-- {
|
||||
scope := scopes[i]
|
||||
|
135
internal/api/oidc/client_integration_test.go
Normal file
135
internal/api/oidc/client_integration_test.go
Normal file
@ -0,0 +1,135 @@
|
||||
//go:build integration
|
||||
|
||||
package oidc_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/zitadel/oidc/v2/pkg/client/rp"
|
||||
"github.com/zitadel/oidc/v2/pkg/client/rs"
|
||||
"github.com/zitadel/oidc/v2/pkg/oidc"
|
||||
|
||||
"github.com/zitadel/zitadel/pkg/grpc/authn"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/management"
|
||||
oidc_pb "github.com/zitadel/zitadel/pkg/grpc/oidc/v2alpha"
|
||||
)
|
||||
|
||||
func TestOPStorage_SetUserinfoFromToken(t *testing.T) {
|
||||
clientID := createClient(t)
|
||||
authRequestID := createAuthRequest(t, clientID, redirectURI, oidc.ScopeOpenID, oidc.ScopeProfile, oidc.ScopeEmail, oidc.ScopeOfflineAccess)
|
||||
sessionID, sessionToken, startTime, changeTime := Tester.CreatePasskeySession(t, CTXLOGIN, User.GetUserId())
|
||||
linkResp, err := Tester.Client.OIDCv2.CreateCallback(CTXLOGIN, &oidc_pb.CreateCallbackRequest{
|
||||
AuthRequestId: authRequestID,
|
||||
CallbackKind: &oidc_pb.CreateCallbackRequest_Session{
|
||||
Session: &oidc_pb.Session{
|
||||
SessionId: sessionID,
|
||||
SessionToken: sessionToken,
|
||||
},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// code exchange
|
||||
code := assertCodeResponse(t, linkResp.GetCallbackUrl())
|
||||
tokens, err := exchangeTokens(t, clientID, code)
|
||||
require.NoError(t, err)
|
||||
assertTokens(t, tokens, true)
|
||||
assertIDTokenClaims(t, tokens.IDTokenClaims, armPasskey, startTime, changeTime)
|
||||
|
||||
// test actual userinfo
|
||||
provider, err := Tester.CreateRelyingParty(clientID, redirectURI)
|
||||
require.NoError(t, err)
|
||||
userinfo, err := rp.Userinfo(tokens.AccessToken, tokens.TokenType, tokens.IDTokenClaims.Subject, provider)
|
||||
require.NoError(t, err)
|
||||
assertUserinfo(t, userinfo)
|
||||
}
|
||||
|
||||
func TestOPStorage_SetIntrospectionFromToken(t *testing.T) {
|
||||
project, err := Tester.CreateProject(CTX)
|
||||
require.NoError(t, err)
|
||||
app, err := Tester.CreateOIDCNativeClient(CTX, redirectURI, project.GetId())
|
||||
require.NoError(t, err)
|
||||
api, err := Tester.CreateAPIClient(CTX, project.GetId())
|
||||
require.NoError(t, err)
|
||||
keyResp, err := Tester.Client.Mgmt.AddAppKey(CTX, &management.AddAppKeyRequest{
|
||||
ProjectId: project.GetId(),
|
||||
AppId: api.GetAppId(),
|
||||
Type: authn.KeyType_KEY_TYPE_JSON,
|
||||
ExpirationDate: nil,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
resourceServer, err := Tester.CreateResourceServer(keyResp.GetKeyDetails())
|
||||
require.NoError(t, err)
|
||||
|
||||
scope := []string{oidc.ScopeOpenID, oidc.ScopeProfile, oidc.ScopeEmail, oidc.ScopeOfflineAccess}
|
||||
authRequestID := createAuthRequest(t, app.GetClientId(), redirectURI, scope...)
|
||||
sessionID, sessionToken, startTime, changeTime := Tester.CreatePasskeySession(t, CTXLOGIN, User.GetUserId())
|
||||
linkResp, err := Tester.Client.OIDCv2.CreateCallback(CTXLOGIN, &oidc_pb.CreateCallbackRequest{
|
||||
AuthRequestId: authRequestID,
|
||||
CallbackKind: &oidc_pb.CreateCallbackRequest_Session{
|
||||
Session: &oidc_pb.Session{
|
||||
SessionId: sessionID,
|
||||
SessionToken: sessionToken,
|
||||
},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// code exchange
|
||||
code := assertCodeResponse(t, linkResp.GetCallbackUrl())
|
||||
tokens, err := exchangeTokens(t, app.GetClientId(), code)
|
||||
require.NoError(t, err)
|
||||
assertTokens(t, tokens, true)
|
||||
assertIDTokenClaims(t, tokens.IDTokenClaims, armPasskey, startTime, changeTime)
|
||||
|
||||
// test actual introspection
|
||||
introspection, err := rs.Introspect(context.Background(), resourceServer, tokens.AccessToken)
|
||||
require.NoError(t, err)
|
||||
assertIntrospection(t, introspection,
|
||||
Tester.OIDCIssuer(), app.GetClientId(),
|
||||
scope, []string{app.GetClientId(), api.GetClientId(), project.GetId()},
|
||||
tokens.Expiry, tokens.Expiry.Add(-12*time.Hour))
|
||||
}
|
||||
|
||||
func assertUserinfo(t *testing.T, userinfo *oidc.UserInfo) {
|
||||
assert.Equal(t, User.GetUserId(), userinfo.Subject)
|
||||
assert.Equal(t, "Mickey", userinfo.GivenName)
|
||||
assert.Equal(t, "Mouse", userinfo.FamilyName)
|
||||
assert.Equal(t, "Mickey Mouse", userinfo.Name)
|
||||
assert.NotEmpty(t, userinfo.PreferredUsername)
|
||||
assert.Equal(t, userinfo.PreferredUsername, userinfo.Email)
|
||||
assert.False(t, bool(userinfo.EmailVerified))
|
||||
assertOIDCTime(t, userinfo.UpdatedAt, User.GetDetails().GetChangeDate().AsTime())
|
||||
}
|
||||
|
||||
func assertIntrospection(
|
||||
t *testing.T,
|
||||
introspection *oidc.IntrospectionResponse,
|
||||
issuer, clientID string,
|
||||
scope, audience []string,
|
||||
expiration, creation time.Time,
|
||||
) {
|
||||
assert.True(t, introspection.Active)
|
||||
assert.Equal(t, scope, []string(introspection.Scope))
|
||||
assert.Equal(t, clientID, introspection.ClientID)
|
||||
assert.Equal(t, oidc.BearerToken, introspection.TokenType)
|
||||
assertOIDCTime(t, introspection.Expiration, expiration)
|
||||
assertOIDCTime(t, introspection.IssuedAt, creation)
|
||||
assertOIDCTime(t, introspection.NotBefore, creation)
|
||||
assert.Equal(t, User.GetUserId(), introspection.Subject)
|
||||
assert.ElementsMatch(t, audience, introspection.Audience)
|
||||
assert.Equal(t, issuer, introspection.Issuer)
|
||||
assert.NotEmpty(t, introspection.JWTID)
|
||||
assert.NotEmpty(t, introspection.Username)
|
||||
assert.Equal(t, introspection.Username, introspection.PreferredUsername)
|
||||
assert.Equal(t, "Mickey", introspection.GivenName)
|
||||
assert.Equal(t, "Mouse", introspection.FamilyName)
|
||||
assert.Equal(t, "Mickey Mouse", introspection.Name)
|
||||
assert.Equal(t, introspection.Username, introspection.Email)
|
||||
assert.False(t, bool(introspection.EmailVerified))
|
||||
assertOIDCTime(t, introspection.UpdatedAt, User.GetDetails().GetChangeDate().AsTime())
|
||||
}
|
248
internal/api/oidc/oidc_integration_test.go
Normal file
248
internal/api/oidc/oidc_integration_test.go
Normal file
@ -0,0 +1,248 @@
|
||||
//go:build integration
|
||||
|
||||
package oidc_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/zitadel/oidc/v2/pkg/oidc"
|
||||
"google.golang.org/grpc/metadata"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/integration"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/auth"
|
||||
oidc_pb "github.com/zitadel/zitadel/pkg/grpc/oidc/v2alpha"
|
||||
session "github.com/zitadel/zitadel/pkg/grpc/session/v2alpha"
|
||||
user "github.com/zitadel/zitadel/pkg/grpc/user/v2alpha"
|
||||
)
|
||||
|
||||
var (
|
||||
CTX context.Context
|
||||
CTXLOGIN context.Context
|
||||
Tester *integration.Tester
|
||||
User *user.AddHumanUserResponse
|
||||
)
|
||||
|
||||
const (
|
||||
redirectURI = "oidcIntegrationTest://callback"
|
||||
redirectURIImplicit = "http://localhost:9999/callback"
|
||||
zitadelAudienceScope = domain.ProjectIDScope + domain.ProjectIDScopeZITADEL + domain.AudSuffix
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
os.Exit(func() int {
|
||||
ctx, errCtx, cancel := integration.Contexts(5 * time.Minute)
|
||||
defer cancel()
|
||||
|
||||
Tester = integration.NewTester(ctx)
|
||||
defer Tester.Done()
|
||||
|
||||
CTX, _ = Tester.WithAuthorization(ctx, integration.OrgOwner), errCtx
|
||||
User = Tester.CreateHumanUser(CTX)
|
||||
Tester.SetUserPassword(CTX, User.GetUserId(), integration.UserPassword)
|
||||
Tester.RegisterUserPasskey(CTX, User.GetUserId())
|
||||
CTXLOGIN, _ = Tester.WithAuthorization(ctx, integration.Login), errCtx
|
||||
return m.Run()
|
||||
}())
|
||||
}
|
||||
|
||||
func Test_ZITADEL_API_missing_audience_scope(t *testing.T) {
|
||||
clientID := createClient(t)
|
||||
authRequestID := createAuthRequest(t, clientID, redirectURI, oidc.ScopeOpenID)
|
||||
sessionID, sessionToken, startTime, changeTime := Tester.CreatePasskeySession(t, CTXLOGIN, User.GetUserId())
|
||||
linkResp, err := Tester.Client.OIDCv2.CreateCallback(CTXLOGIN, &oidc_pb.CreateCallbackRequest{
|
||||
AuthRequestId: authRequestID,
|
||||
CallbackKind: &oidc_pb.CreateCallbackRequest_Session{
|
||||
Session: &oidc_pb.Session{
|
||||
SessionId: sessionID,
|
||||
SessionToken: sessionToken,
|
||||
},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// code exchange
|
||||
code := assertCodeResponse(t, linkResp.GetCallbackUrl())
|
||||
tokens, err := exchangeTokens(t, clientID, code)
|
||||
require.NoError(t, err)
|
||||
assertTokens(t, tokens, false)
|
||||
assertIDTokenClaims(t, tokens.IDTokenClaims, armPasskey, startTime, changeTime)
|
||||
|
||||
ctx := metadata.AppendToOutgoingContext(context.Background(), "Authorization", fmt.Sprintf("%s %s", tokens.TokenType, tokens.AccessToken))
|
||||
|
||||
myUserResp, err := Tester.Client.Auth.GetMyUser(ctx, &auth.GetMyUserRequest{})
|
||||
require.Error(t, err)
|
||||
require.Nil(t, myUserResp)
|
||||
}
|
||||
|
||||
func Test_ZITADEL_API_missing_authentication(t *testing.T) {
|
||||
clientID := createClient(t)
|
||||
authRequestID := createAuthRequest(t, clientID, redirectURI, oidc.ScopeOpenID, zitadelAudienceScope)
|
||||
createResp, err := Tester.Client.SessionV2.CreateSession(CTX, &session.CreateSessionRequest{
|
||||
Checks: &session.Checks{
|
||||
User: &session.CheckUser{
|
||||
Search: &session.CheckUser_UserId{UserId: User.GetUserId()},
|
||||
},
|
||||
},
|
||||
Domain: Tester.Config.ExternalDomain,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
linkResp, err := Tester.Client.OIDCv2.CreateCallback(CTXLOGIN, &oidc_pb.CreateCallbackRequest{
|
||||
AuthRequestId: authRequestID,
|
||||
CallbackKind: &oidc_pb.CreateCallbackRequest_Session{
|
||||
Session: &oidc_pb.Session{
|
||||
SessionId: createResp.GetSessionId(),
|
||||
SessionToken: createResp.GetSessionToken(),
|
||||
},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// code exchange
|
||||
code := assertCodeResponse(t, linkResp.GetCallbackUrl())
|
||||
tokens, err := exchangeTokens(t, clientID, code)
|
||||
require.NoError(t, err)
|
||||
|
||||
ctx := metadata.AppendToOutgoingContext(context.Background(), "Authorization", fmt.Sprintf("%s %s", tokens.TokenType, tokens.AccessToken))
|
||||
|
||||
myUserResp, err := Tester.Client.Auth.GetMyUser(ctx, &auth.GetMyUserRequest{})
|
||||
require.Error(t, err)
|
||||
require.Nil(t, myUserResp)
|
||||
}
|
||||
|
||||
func Test_ZITADEL_API_missing_mfa(t *testing.T) {
|
||||
clientID := createClient(t)
|
||||
authRequestID := createAuthRequest(t, clientID, redirectURI, oidc.ScopeOpenID, zitadelAudienceScope)
|
||||
sessionID, sessionToken, startTime, changeTime := Tester.CreatePasswordSession(t, CTX, User.GetUserId(), integration.UserPassword)
|
||||
linkResp, err := Tester.Client.OIDCv2.CreateCallback(CTXLOGIN, &oidc_pb.CreateCallbackRequest{
|
||||
AuthRequestId: authRequestID,
|
||||
CallbackKind: &oidc_pb.CreateCallbackRequest_Session{
|
||||
Session: &oidc_pb.Session{
|
||||
SessionId: sessionID,
|
||||
SessionToken: sessionToken,
|
||||
},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// code exchange
|
||||
code := assertCodeResponse(t, linkResp.GetCallbackUrl())
|
||||
tokens, err := exchangeTokens(t, clientID, code)
|
||||
require.NoError(t, err)
|
||||
assertIDTokenClaims(t, tokens.IDTokenClaims, armPassword, startTime, changeTime)
|
||||
|
||||
ctx := metadata.AppendToOutgoingContext(context.Background(), "Authorization", fmt.Sprintf("%s %s", tokens.TokenType, tokens.AccessToken))
|
||||
|
||||
myUserResp, err := Tester.Client.Auth.GetMyUser(ctx, &auth.GetMyUserRequest{})
|
||||
require.Error(t, err)
|
||||
require.Nil(t, myUserResp)
|
||||
}
|
||||
|
||||
func Test_ZITADEL_API_success(t *testing.T) {
|
||||
clientID := createClient(t)
|
||||
authRequestID := createAuthRequest(t, clientID, redirectURI, oidc.ScopeOpenID, zitadelAudienceScope)
|
||||
sessionID, sessionToken, startTime, changeTime := Tester.CreatePasskeySession(t, CTXLOGIN, User.GetUserId())
|
||||
linkResp, err := Tester.Client.OIDCv2.CreateCallback(CTXLOGIN, &oidc_pb.CreateCallbackRequest{
|
||||
AuthRequestId: authRequestID,
|
||||
CallbackKind: &oidc_pb.CreateCallbackRequest_Session{
|
||||
Session: &oidc_pb.Session{
|
||||
SessionId: sessionID,
|
||||
SessionToken: sessionToken,
|
||||
},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// code exchange
|
||||
code := assertCodeResponse(t, linkResp.GetCallbackUrl())
|
||||
tokens, err := exchangeTokens(t, clientID, code)
|
||||
require.NoError(t, err)
|
||||
assertTokens(t, tokens, false)
|
||||
assertIDTokenClaims(t, tokens.IDTokenClaims, armPasskey, startTime, changeTime)
|
||||
|
||||
ctx := metadata.AppendToOutgoingContext(context.Background(), "Authorization", fmt.Sprintf("%s %s", tokens.TokenType, tokens.AccessToken))
|
||||
|
||||
myUserResp, err := Tester.Client.Auth.GetMyUser(ctx, &auth.GetMyUserRequest{})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, User.GetUserId(), myUserResp.GetUser().GetId())
|
||||
}
|
||||
|
||||
func Test_ZITADEL_API_inactive_access_token(t *testing.T) {
|
||||
clientID := createClient(t)
|
||||
authRequestID := createAuthRequest(t, clientID, redirectURI, oidc.ScopeOpenID, oidc.ScopeOfflineAccess, zitadelAudienceScope)
|
||||
sessionID, sessionToken, startTime, changeTime := Tester.CreatePasskeySession(t, CTXLOGIN, User.GetUserId())
|
||||
linkResp, err := Tester.Client.OIDCv2.CreateCallback(CTXLOGIN, &oidc_pb.CreateCallbackRequest{
|
||||
AuthRequestId: authRequestID,
|
||||
CallbackKind: &oidc_pb.CreateCallbackRequest_Session{
|
||||
Session: &oidc_pb.Session{
|
||||
SessionId: sessionID,
|
||||
SessionToken: sessionToken,
|
||||
},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// code exchange
|
||||
code := assertCodeResponse(t, linkResp.GetCallbackUrl())
|
||||
tokens, err := exchangeTokens(t, clientID, code)
|
||||
require.NoError(t, err)
|
||||
assertTokens(t, tokens, true)
|
||||
assertIDTokenClaims(t, tokens.IDTokenClaims, armPasskey, startTime, changeTime)
|
||||
|
||||
// make sure token works
|
||||
ctx := metadata.AppendToOutgoingContext(context.Background(), "Authorization", fmt.Sprintf("%s %s", tokens.TokenType, tokens.AccessToken))
|
||||
myUserResp, err := Tester.Client.Auth.GetMyUser(ctx, &auth.GetMyUserRequest{})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, User.GetUserId(), myUserResp.GetUser().GetId())
|
||||
|
||||
// refresh token
|
||||
newTokens, err := refreshTokens(t, clientID, tokens.RefreshToken)
|
||||
require.NoError(t, err)
|
||||
assert.NotEmpty(t, newTokens.AccessToken)
|
||||
|
||||
// use invalidated token
|
||||
ctx = metadata.AppendToOutgoingContext(context.Background(), "Authorization", fmt.Sprintf("%s %s", tokens.TokenType, tokens.AccessToken))
|
||||
myUserResp, err = Tester.Client.Auth.GetMyUser(ctx, &auth.GetMyUserRequest{})
|
||||
require.Error(t, err)
|
||||
require.Nil(t, myUserResp)
|
||||
}
|
||||
|
||||
func createClient(t testing.TB) string {
|
||||
project, err := Tester.CreateProject(CTX)
|
||||
require.NoError(t, err)
|
||||
app, err := Tester.CreateOIDCNativeClient(CTX, redirectURI, project.GetId())
|
||||
require.NoError(t, err)
|
||||
return app.GetClientId()
|
||||
}
|
||||
|
||||
func createImplicitClient(t testing.TB) string {
|
||||
app, err := Tester.CreateOIDCImplicitFlowClient(CTX, redirectURIImplicit)
|
||||
require.NoError(t, err)
|
||||
return app.GetClientId()
|
||||
}
|
||||
|
||||
func createAuthRequest(t testing.TB, clientID, redirectURI string, scope ...string) string {
|
||||
redURL, err := Tester.CreateOIDCAuthRequest(clientID, Tester.Users[integration.FirstInstanceUsersKey][integration.Login].ID, redirectURI, scope...)
|
||||
require.NoError(t, err)
|
||||
return redURL
|
||||
}
|
||||
|
||||
func createAuthRequestImplicit(t testing.TB, clientID, redirectURI string, scope ...string) string {
|
||||
redURL, err := Tester.CreateOIDCAuthRequestImplicit(clientID, Tester.Users[integration.FirstInstanceUsersKey][integration.Login].ID, redirectURI, scope...)
|
||||
require.NoError(t, err)
|
||||
return redURL
|
||||
}
|
||||
|
||||
func assertOIDCTime(t *testing.T, actual oidc.Time, expected time.Time) {
|
||||
assertOIDCTimeRange(t, actual, expected, expected)
|
||||
}
|
||||
|
||||
func assertOIDCTimeRange(t *testing.T, actual oidc.Time, expectedStart, expectedEnd time.Time) {
|
||||
assert.WithinRange(t, actual.AsTime(), expectedStart.Add(-1*time.Second), expectedEnd.Add(1*time.Second))
|
||||
}
|
@ -15,7 +15,9 @@ import (
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
http_util "github.com/zitadel/zitadel/internal/api/http"
|
||||
"github.com/zitadel/zitadel/internal/authz/repository/eventsourcing/view"
|
||||
"github.com/zitadel/zitadel/internal/command"
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
caos_errs "github.com/zitadel/zitadel/internal/errors"
|
||||
v1 "github.com/zitadel/zitadel/internal/eventstore/v1"
|
||||
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
|
||||
@ -28,7 +30,6 @@ import (
|
||||
|
||||
type TokenVerifierRepo struct {
|
||||
TokenVerificationKey crypto.EncryptionAlgorithm
|
||||
IAMID string
|
||||
Eventstore v1.Eventstore
|
||||
View *view.View
|
||||
Query *query.Queries
|
||||
@ -92,6 +93,21 @@ func (repo *TokenVerifierRepo) VerifyAccessToken(ctx context.Context, tokenStrin
|
||||
if !ok {
|
||||
return "", "", "", "", "", caos_errs.ThrowUnauthenticated(nil, "APP-Reb32", "invalid token")
|
||||
}
|
||||
if strings.HasPrefix(tokenID, command.IDPrefixV2) {
|
||||
userID, clientID, resourceOwner, err = repo.verifyAccessTokenV2(ctx, tokenID, verifierClientID, projectID)
|
||||
return
|
||||
}
|
||||
if sessionID, ok := strings.CutPrefix(tokenID, authz.SessionTokenPrefix); ok {
|
||||
userID, clientID, resourceOwner, err = repo.verifySessionToken(ctx, sessionID, tokenString)
|
||||
return
|
||||
}
|
||||
return repo.verifyAccessTokenV1(ctx, tokenID, subject, verifierClientID, projectID)
|
||||
}
|
||||
|
||||
func (repo *TokenVerifierRepo) verifyAccessTokenV1(ctx context.Context, tokenID, subject, verifierClientID, projectID string) (userID string, agentID string, clientID, prefLang, resourceOwner string, err error) {
|
||||
ctx, span := tracing.NewSpan(ctx)
|
||||
defer func() { span.EndWithError(err) }()
|
||||
|
||||
_, tokenSpan := tracing.NewNamedSpan(ctx, "token")
|
||||
token, err := repo.tokenByID(ctx, tokenID, subject)
|
||||
tokenSpan.EndWithError(err)
|
||||
@ -104,12 +120,89 @@ func (repo *TokenVerifierRepo) VerifyAccessToken(ctx context.Context, tokenStrin
|
||||
if token.IsPAT {
|
||||
return token.UserID, "", "", "", token.ResourceOwner, nil
|
||||
}
|
||||
for _, aud := range token.Audience {
|
||||
if verifierClientID == aud || projectID == aud {
|
||||
return token.UserID, token.UserAgentID, token.ApplicationID, token.PreferredLanguage, token.ResourceOwner, nil
|
||||
}
|
||||
if err = verifyAudience(token.Audience, verifierClientID, projectID); err != nil {
|
||||
return "", "", "", "", "", err
|
||||
}
|
||||
return "", "", "", "", "", caos_errs.ThrowUnauthenticated(nil, "APP-Zxfako", "invalid audience")
|
||||
return token.UserID, token.UserAgentID, token.ApplicationID, token.PreferredLanguage, token.ResourceOwner, nil
|
||||
}
|
||||
|
||||
func (repo *TokenVerifierRepo) verifyAccessTokenV2(ctx context.Context, token, verifierClientID, projectID string) (userID, clientID, resourceOwner string, err error) {
|
||||
ctx, span := tracing.NewSpan(ctx)
|
||||
defer func() { span.EndWithError(err) }()
|
||||
|
||||
activeToken, err := repo.Query.ActiveAccessTokenByToken(ctx, token)
|
||||
if err != nil {
|
||||
return "", "", "", err
|
||||
}
|
||||
if err = verifyAudience(activeToken.Audience, verifierClientID, projectID); err != nil {
|
||||
return "", "", "", err
|
||||
}
|
||||
if err = repo.checkAuthentication(ctx, activeToken.AuthMethods, activeToken.UserID); err != nil {
|
||||
return "", "", "", err
|
||||
}
|
||||
return activeToken.UserID, activeToken.ClientID, activeToken.ResourceOwner, nil
|
||||
}
|
||||
|
||||
func (repo *TokenVerifierRepo) verifySessionToken(ctx context.Context, sessionID, token string) (userID, clientID, resourceOwner string, err error) {
|
||||
ctx, span := tracing.NewSpan(ctx)
|
||||
defer func() { span.EndWithError(err) }()
|
||||
|
||||
session, err := repo.Query.SessionByID(ctx, false, sessionID, token)
|
||||
if err != nil {
|
||||
return "", "", "", err
|
||||
}
|
||||
if err = repo.checkAuthentication(ctx, authMethodsFromSession(session), session.UserFactor.UserID); err != nil {
|
||||
return "", "", "", err
|
||||
}
|
||||
return session.UserFactor.UserID, "", session.UserFactor.ResourceOwner, nil
|
||||
}
|
||||
|
||||
// checkAuthentication ensures the session or token was authenticated (at least a single [domain.UserAuthMethodType]).
|
||||
// It will also check if there was a multi factor authentication, if either MFA is forced by the login policy or if the user has set up any
|
||||
func (repo *TokenVerifierRepo) checkAuthentication(ctx context.Context, authMethods []domain.UserAuthMethodType, userID string) error {
|
||||
if len(authMethods) == 0 {
|
||||
return caos_errs.ThrowPermissionDenied(nil, "AUTHZ-Kl3p0", "authentication required")
|
||||
}
|
||||
if domain.HasMFA(authMethods) {
|
||||
return nil
|
||||
}
|
||||
availableAuthMethods, forceMFA, err := repo.Query.ListUserAuthMethodTypesRequired(setCallerCtx(ctx, userID), userID, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if forceMFA || domain.HasMFA(availableAuthMethods) {
|
||||
return caos_errs.ThrowPermissionDenied(nil, "AUTHZ-Kl3p0", "mfa required")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func authMethodsFromSession(session *query.Session) []domain.UserAuthMethodType {
|
||||
types := make([]domain.UserAuthMethodType, 0, domain.UserAuthMethodTypeIDP)
|
||||
if !session.PasswordFactor.PasswordCheckedAt.IsZero() {
|
||||
types = append(types, domain.UserAuthMethodTypePassword)
|
||||
}
|
||||
if !session.PasskeyFactor.PasskeyCheckedAt.IsZero() {
|
||||
types = append(types, domain.UserAuthMethodTypePasswordless)
|
||||
}
|
||||
if !session.IntentFactor.IntentCheckedAt.IsZero() {
|
||||
types = append(types, domain.UserAuthMethodTypeIDP)
|
||||
}
|
||||
// TODO: add checks with https://github.com/zitadel/zitadel/issues/5477
|
||||
/*
|
||||
if !session.TOTPFactor.TOTPCheckedAt.IsZero() {
|
||||
types = append(types, domain.UserAuthMethodTypeOTP)
|
||||
}
|
||||
if !session.U2FFactor.U2FCheckedAt.IsZero() {
|
||||
types = append(types, domain.UserAuthMethodTypeU2F)
|
||||
}
|
||||
*/
|
||||
return types
|
||||
}
|
||||
|
||||
func setCallerCtx(ctx context.Context, userID string) context.Context {
|
||||
ctxData := authz.GetCtxData(ctx)
|
||||
ctxData.UserID = userID
|
||||
return authz.SetCtxData(ctx, ctxData)
|
||||
}
|
||||
|
||||
func (repo *TokenVerifierRepo) ProjectIDAndOriginsByClientID(ctx context.Context, clientID string) (projectID string, origins []string, err error) {
|
||||
@ -184,6 +277,15 @@ func (repo *TokenVerifierRepo) decryptAccessToken(token string) (string, error)
|
||||
return tokenIDSubject, nil
|
||||
}
|
||||
|
||||
func verifyAudience(audience []string, verifierClientID, projectID string) error {
|
||||
for _, aud := range audience {
|
||||
if verifierClientID == aud || projectID == aud {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return caos_errs.ThrowUnauthenticated(nil, "APP-Zxfako", "invalid audience")
|
||||
}
|
||||
|
||||
type openIDKeySet struct {
|
||||
*query.Queries
|
||||
}
|
||||
|
@ -14,6 +14,7 @@ import (
|
||||
"github.com/zitadel/zitadel/internal/id"
|
||||
"github.com/zitadel/zitadel/internal/repository/authrequest"
|
||||
"github.com/zitadel/zitadel/internal/repository/oidcsession"
|
||||
"github.com/zitadel/zitadel/internal/repository/user"
|
||||
)
|
||||
|
||||
// AddOIDCSessionAccessToken creates a new OIDC Session, creates an access token and returns its id and expiration.
|
||||
@ -101,6 +102,10 @@ func (c *Commands) newOIDCSessionAddEvents(ctx context.Context, authRequestID st
|
||||
if sessionWriteModel.State != domain.SessionStateActive {
|
||||
return nil, caos_errs.ThrowPreconditionFailed(nil, "OIDCS-sjkl3", "Errors.Session.Terminated")
|
||||
}
|
||||
resourceOwner, err := c.getResourceOwnerOfSessionUser(ctx, sessionWriteModel.UserID, sessionWriteModel.InstanceID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
accessTokenLifetime, refreshTokenLifeTime, refreshTokenIdleLifetime, err := c.tokenTokenLifetimes(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -114,7 +119,7 @@ func (c *Commands) newOIDCSessionAddEvents(ctx context.Context, authRequestID st
|
||||
eventstore: c.eventstore,
|
||||
idGenerator: c.idGenerator,
|
||||
encryptionAlg: c.keyAlgorithm,
|
||||
oidcSessionWriteModel: NewOIDCSessionWriteModel(sessionID, authz.GetInstance(ctx).InstanceID()),
|
||||
oidcSessionWriteModel: NewOIDCSessionWriteModel(sessionID, resourceOwner),
|
||||
sessionWriteModel: sessionWriteModel,
|
||||
authRequestWriteModel: authRequestWriteModel,
|
||||
accessTokenLifetime: accessTokenLifetime,
|
||||
@ -123,6 +128,22 @@ func (c *Commands) newOIDCSessionAddEvents(ctx context.Context, authRequestID st
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *Commands) getResourceOwnerOfSessionUser(ctx context.Context, userID, instanceID string) (string, error) {
|
||||
events, err := c.eventstore.Filter(ctx, eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
|
||||
InstanceID(instanceID).
|
||||
AllowTimeTravel().
|
||||
OrderAsc().
|
||||
Limit(1).
|
||||
AddQuery().
|
||||
AggregateTypes(user.AggregateType).
|
||||
AggregateIDs(userID).
|
||||
Builder())
|
||||
if err != nil || len(events) != 1 {
|
||||
return "", caos_errs.ThrowInternal(err, "OIDCS-sferh", "Errors.Internal")
|
||||
}
|
||||
return events[0].Aggregate().ResourceOwner, nil
|
||||
}
|
||||
|
||||
func (c *Commands) decryptRefreshToken(refreshToken string) (refreshTokenID string, err error) {
|
||||
decoded, err := base64.RawURLEncoding.DecodeString(refreshToken)
|
||||
if err != nil {
|
||||
@ -144,7 +165,7 @@ func (c *Commands) newOIDCSessionUpdateEvents(ctx context.Context, oidcSessionID
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sessionWriteModel := NewOIDCSessionWriteModel(oidcSessionID, authz.GetInstance(ctx).InstanceID())
|
||||
sessionWriteModel := NewOIDCSessionWriteModel(oidcSessionID, "")
|
||||
if err = c.eventstore.FilterToQueryReducer(ctx, sessionWriteModel); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -20,6 +20,7 @@ type OIDCSessionWriteModel struct {
|
||||
AuthMethods []domain.UserAuthMethodType
|
||||
AuthTime time.Time
|
||||
State domain.OIDCSessionState
|
||||
AccessTokenCreation time.Time
|
||||
AccessTokenExpiration time.Time
|
||||
RefreshTokenID string
|
||||
RefreshTokenExpiration time.Time
|
||||
@ -82,6 +83,11 @@ func (wm *OIDCSessionWriteModel) reduceAdded(e *oidcsession.AddedEvent) {
|
||||
wm.AuthMethods = e.AuthMethods
|
||||
wm.AuthTime = e.AuthTime
|
||||
wm.State = domain.OIDCSessionStateActive
|
||||
// the write model might be initialized without resource owner,
|
||||
// so update the aggregate
|
||||
if wm.ResourceOwner == "" {
|
||||
wm.aggregate = &oidcsession.NewAggregate(wm.AggregateID, e.Aggregate().ResourceOwner).Aggregate
|
||||
}
|
||||
}
|
||||
|
||||
func (wm *OIDCSessionWriteModel) reduceAccessTokenAdded(e *oidcsession.AccessTokenAddedEvent) {
|
||||
|
@ -9,6 +9,7 @@ import (
|
||||
"github.com/muhlemmer/gu"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/text/language"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
@ -21,6 +22,7 @@ import (
|
||||
"github.com/zitadel/zitadel/internal/repository/authrequest"
|
||||
"github.com/zitadel/zitadel/internal/repository/oidcsession"
|
||||
"github.com/zitadel/zitadel/internal/repository/session"
|
||||
"github.com/zitadel/zitadel/internal/repository/user"
|
||||
)
|
||||
|
||||
var (
|
||||
@ -108,7 +110,7 @@ func TestCommands_AddOIDCSessionAccessToken(t *testing.T) {
|
||||
authrequest.NewCodeExchangedEvent(context.Background(), &authrequest.NewAggregate("V2_authRequestID", "instanceID").Aggregate),
|
||||
),
|
||||
),
|
||||
expectFilter(),
|
||||
expectFilter(), // inactive session
|
||||
),
|
||||
},
|
||||
args{
|
||||
@ -173,15 +175,22 @@ func TestCommands_AddOIDCSessionAccessToken(t *testing.T) {
|
||||
testNow),
|
||||
),
|
||||
),
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
user.NewHumanAddedEvent(context.Background(), &user.NewAggregate("userID", "org1").Aggregate,
|
||||
"username", "firstName", "lastName", "", "", language.English, domain.GenderUnspecified, "", false,
|
||||
),
|
||||
),
|
||||
),
|
||||
expectFilter(), // token lifetime
|
||||
expectPush(
|
||||
[]*repository.Event{
|
||||
eventFromEventPusherWithInstanceID("instanceID",
|
||||
oidcsession.NewAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "instanceID").Aggregate,
|
||||
oidcsession.NewAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate,
|
||||
"userID", "sessionID", "clientID", []string{"audience"}, []string{"openid"}, []domain.UserAuthMethodType{domain.UserAuthMethodTypePassword}, testNow),
|
||||
),
|
||||
eventFromEventPusherWithInstanceID("instanceID",
|
||||
oidcsession.NewAccessTokenAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "instanceID").Aggregate,
|
||||
oidcsession.NewAccessTokenAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate,
|
||||
"accessTokenID", []string{"openid"}, time.Hour),
|
||||
),
|
||||
eventFromEventPusherWithInstanceID("instanceID",
|
||||
@ -302,7 +311,7 @@ func TestCommands_AddOIDCSessionRefreshAndAccessToken(t *testing.T) {
|
||||
authrequest.NewCodeExchangedEvent(context.Background(), &authrequest.NewAggregate("V2_authRequestID", "instanceID").Aggregate),
|
||||
),
|
||||
),
|
||||
expectFilter(),
|
||||
expectFilter(), // inactive session
|
||||
),
|
||||
},
|
||||
args{
|
||||
@ -367,19 +376,26 @@ func TestCommands_AddOIDCSessionRefreshAndAccessToken(t *testing.T) {
|
||||
testNow),
|
||||
),
|
||||
),
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
user.NewHumanAddedEvent(context.Background(), &user.NewAggregate("userID", "org1").Aggregate,
|
||||
"username", "firstName", "lastName", "", "", language.English, domain.GenderUnspecified, "", false,
|
||||
),
|
||||
),
|
||||
),
|
||||
expectFilter(), // token lifetime
|
||||
expectPush(
|
||||
[]*repository.Event{
|
||||
eventFromEventPusherWithInstanceID("instanceID",
|
||||
oidcsession.NewAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "instanceID").Aggregate,
|
||||
oidcsession.NewAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate,
|
||||
"userID", "sessionID", "clientID", []string{"audience"}, []string{"openid", "offline_access"}, []domain.UserAuthMethodType{domain.UserAuthMethodTypePassword}, testNow),
|
||||
),
|
||||
eventFromEventPusherWithInstanceID("instanceID",
|
||||
oidcsession.NewAccessTokenAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "instanceID").Aggregate,
|
||||
oidcsession.NewAccessTokenAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate,
|
||||
"accessTokenID", []string{"openid", "offline_access"}, time.Hour),
|
||||
),
|
||||
eventFromEventPusherWithInstanceID("instanceID",
|
||||
oidcsession.NewRefreshTokenAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "instanceID").Aggregate,
|
||||
oidcsession.NewRefreshTokenAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate,
|
||||
"refreshTokenID", 7*24*time.Hour, 24*time.Hour),
|
||||
),
|
||||
eventFromEventPusherWithInstanceID("instanceID",
|
||||
@ -489,11 +505,11 @@ func TestCommands_ExchangeOIDCSessionRefreshAndAccessToken(t *testing.T) {
|
||||
eventstore: eventstoreExpect(t,
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
oidcsession.NewAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "instanceID").Aggregate,
|
||||
oidcsession.NewAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate,
|
||||
"userID", "sessionID", "clientID", []string{"audience"}, []string{"openid", "profile", "offline_access"}, []domain.UserAuthMethodType{domain.UserAuthMethodTypePassword}, testNow),
|
||||
),
|
||||
eventFromEventPusher(
|
||||
oidcsession.NewAccessTokenAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "instanceID").Aggregate,
|
||||
oidcsession.NewAccessTokenAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate,
|
||||
"accessTokenID", []string{"openid", "profile", "offline_access"}, time.Hour),
|
||||
),
|
||||
),
|
||||
@ -515,15 +531,15 @@ func TestCommands_ExchangeOIDCSessionRefreshAndAccessToken(t *testing.T) {
|
||||
eventstore: eventstoreExpect(t,
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
oidcsession.NewAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "instanceID").Aggregate,
|
||||
oidcsession.NewAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate,
|
||||
"userID", "sessionID", "clientID", []string{"audience"}, []string{"openid", "profile", "offline_access"}, []domain.UserAuthMethodType{domain.UserAuthMethodTypePassword}, testNow),
|
||||
),
|
||||
eventFromEventPusher(
|
||||
oidcsession.NewAccessTokenAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "instanceID").Aggregate,
|
||||
oidcsession.NewAccessTokenAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate,
|
||||
"accessTokenID", []string{"openid", "profile", "offline_access"}, time.Hour),
|
||||
),
|
||||
eventFromEventPusher(
|
||||
oidcsession.NewRefreshTokenAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "instanceID").Aggregate,
|
||||
oidcsession.NewRefreshTokenAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate,
|
||||
"refreshTokenID", 7*24*time.Hour, 24*time.Hour),
|
||||
),
|
||||
),
|
||||
@ -545,15 +561,15 @@ func TestCommands_ExchangeOIDCSessionRefreshAndAccessToken(t *testing.T) {
|
||||
eventstore: eventstoreExpect(t,
|
||||
expectFilter(
|
||||
eventFromEventPusherWithCreationDateNow(
|
||||
oidcsession.NewAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "instanceID").Aggregate,
|
||||
oidcsession.NewAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate,
|
||||
"userID", "sessionID", "clientID", []string{"audience"}, []string{"openid", "profile", "offline_access"}, []domain.UserAuthMethodType{domain.UserAuthMethodTypePassword}, testNow),
|
||||
),
|
||||
eventFromEventPusherWithCreationDateNow(
|
||||
oidcsession.NewAccessTokenAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "instanceID").Aggregate,
|
||||
oidcsession.NewAccessTokenAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate,
|
||||
"accessTokenID", []string{"openid", "profile", "offline_access"}, time.Hour),
|
||||
),
|
||||
eventFromEventPusherWithCreationDateNow(
|
||||
oidcsession.NewRefreshTokenAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "instanceID").Aggregate,
|
||||
oidcsession.NewRefreshTokenAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate,
|
||||
"refreshTokenID", 7*24*time.Hour, 24*time.Hour),
|
||||
),
|
||||
),
|
||||
@ -561,11 +577,11 @@ func TestCommands_ExchangeOIDCSessionRefreshAndAccessToken(t *testing.T) {
|
||||
expectPush(
|
||||
[]*repository.Event{
|
||||
eventFromEventPusherWithInstanceID("instanceID",
|
||||
oidcsession.NewAccessTokenAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "instanceID").Aggregate,
|
||||
oidcsession.NewAccessTokenAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate,
|
||||
"accessTokenID", []string{"openid", "offline_access"}, time.Hour),
|
||||
),
|
||||
eventFromEventPusherWithInstanceID("instanceID",
|
||||
oidcsession.NewRefreshTokenRenewedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "instanceID").Aggregate,
|
||||
oidcsession.NewRefreshTokenRenewedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate,
|
||||
"refreshTokenID2", 24*time.Hour),
|
||||
),
|
||||
},
|
||||
@ -668,11 +684,11 @@ func TestCommands_OIDCSessionByRefreshToken(t *testing.T) {
|
||||
eventstore: eventstoreExpect(t,
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
oidcsession.NewAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "instanceID").Aggregate,
|
||||
oidcsession.NewAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate,
|
||||
"userID", "sessionID", "clientID", []string{"audience"}, []string{"openid", "profile", "offline_access"}, []domain.UserAuthMethodType{domain.UserAuthMethodTypePassword}, testNow),
|
||||
),
|
||||
eventFromEventPusher(
|
||||
oidcsession.NewAccessTokenAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "instanceID").Aggregate,
|
||||
oidcsession.NewAccessTokenAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate,
|
||||
"accessTokenID", []string{"openid", "profile", "offline_access"}, time.Hour),
|
||||
),
|
||||
),
|
||||
@ -693,15 +709,15 @@ func TestCommands_OIDCSessionByRefreshToken(t *testing.T) {
|
||||
eventstore: eventstoreExpect(t,
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
oidcsession.NewAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "instanceID").Aggregate,
|
||||
oidcsession.NewAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate,
|
||||
"userID", "sessionID", "clientID", []string{"audience"}, []string{"openid", "profile", "offline_access"}, []domain.UserAuthMethodType{domain.UserAuthMethodTypePassword}, testNow),
|
||||
),
|
||||
eventFromEventPusher(
|
||||
oidcsession.NewAccessTokenAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "instanceID").Aggregate,
|
||||
oidcsession.NewAccessTokenAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate,
|
||||
"accessTokenID", []string{"openid", "profile", "offline_access"}, time.Hour),
|
||||
),
|
||||
eventFromEventPusher(
|
||||
oidcsession.NewRefreshTokenAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "instanceID").Aggregate,
|
||||
oidcsession.NewRefreshTokenAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate,
|
||||
"refreshTokenID", 7*24*time.Hour, 24*time.Hour),
|
||||
),
|
||||
),
|
||||
@ -722,15 +738,15 @@ func TestCommands_OIDCSessionByRefreshToken(t *testing.T) {
|
||||
eventstore: eventstoreExpect(t,
|
||||
expectFilter(
|
||||
eventFromEventPusherWithCreationDateNow(
|
||||
oidcsession.NewAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "instanceID").Aggregate,
|
||||
oidcsession.NewAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate,
|
||||
"userID", "sessionID", "clientID", []string{"audience"}, []string{"openid", "profile", "offline_access"}, []domain.UserAuthMethodType{domain.UserAuthMethodTypePassword}, testNow),
|
||||
),
|
||||
eventFromEventPusherWithCreationDateNow(
|
||||
oidcsession.NewAccessTokenAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "instanceID").Aggregate,
|
||||
oidcsession.NewAccessTokenAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate,
|
||||
"accessTokenID", []string{"openid", "profile", "offline_access"}, time.Hour),
|
||||
),
|
||||
eventFromEventPusherWithCreationDateNow(
|
||||
oidcsession.NewRefreshTokenAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "instanceID").Aggregate,
|
||||
oidcsession.NewRefreshTokenAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate,
|
||||
"refreshTokenID", 7*24*time.Hour, 24*time.Hour),
|
||||
),
|
||||
),
|
||||
|
@ -60,6 +60,31 @@ func (f UserAuthMethodType) Valid() bool {
|
||||
return f >= 0 && f < userAuthMethodTypeCount
|
||||
}
|
||||
|
||||
// HasMFA checks whether the user authenticated with multiple auth factors.
|
||||
// This can either be true if the list contains a [UserAuthMethodType] which by itself is MFA (e.g. [UserAuthMethodTypePasswordless])
|
||||
// or if multiple factors were used (e.g. [UserAuthMethodTypePassword] and [UserAuthMethodTypeU2F])
|
||||
func HasMFA(methods []UserAuthMethodType) bool {
|
||||
var factors int
|
||||
for _, method := range methods {
|
||||
switch method {
|
||||
case UserAuthMethodTypePassword:
|
||||
factors++
|
||||
case UserAuthMethodTypePasswordless:
|
||||
return true
|
||||
case UserAuthMethodTypeU2F:
|
||||
factors++
|
||||
case UserAuthMethodTypeOTP:
|
||||
factors++
|
||||
case UserAuthMethodTypeIDP:
|
||||
factors++
|
||||
case UserAuthMethodTypeUnspecified,
|
||||
userAuthMethodTypeCount:
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
return factors > 1
|
||||
}
|
||||
|
||||
type PersonalAccessTokenState int32
|
||||
|
||||
const (
|
||||
|
@ -17,6 +17,7 @@ import (
|
||||
openid "github.com/zitadel/zitadel/internal/idp/providers/oidc"
|
||||
"github.com/zitadel/zitadel/internal/repository/idp"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/admin"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/auth"
|
||||
mgmt "github.com/zitadel/zitadel/pkg/grpc/management"
|
||||
object "github.com/zitadel/zitadel/pkg/grpc/object/v2alpha"
|
||||
oidc_pb "github.com/zitadel/zitadel/pkg/grpc/oidc/v2alpha"
|
||||
@ -29,6 +30,7 @@ type Client struct {
|
||||
CC *grpc.ClientConn
|
||||
Admin admin.AdminServiceClient
|
||||
Mgmt mgmt.ManagementServiceClient
|
||||
Auth auth.AuthServiceClient
|
||||
UserV2 user.UserServiceClient
|
||||
SessionV2 session.SessionServiceClient
|
||||
OIDCv2 oidc_pb.OIDCServiceClient
|
||||
@ -40,6 +42,7 @@ func newClient(cc *grpc.ClientConn) Client {
|
||||
CC: cc,
|
||||
Admin: admin.NewAdminServiceClient(cc),
|
||||
Mgmt: mgmt.NewManagementServiceClient(cc),
|
||||
Auth: auth.NewAuthServiceClient(cc),
|
||||
UserV2: user.NewUserServiceClient(cc),
|
||||
SessionV2: session.NewSessionServiceClient(cc),
|
||||
OIDCv2: oidc_pb.NewOIDCServiceClient(cc),
|
||||
@ -134,6 +137,14 @@ func (s *Tester) RegisterUserPasskey(ctx context.Context, userID string) {
|
||||
logging.OnError(err).Fatal("create user passkey")
|
||||
}
|
||||
|
||||
func (s *Tester) SetUserPassword(ctx context.Context, userID, password string) {
|
||||
_, err := s.Client.UserV2.SetPassword(ctx, &user.SetPasswordRequest{
|
||||
UserId: userID,
|
||||
NewPassword: &user.Password{Password: password},
|
||||
})
|
||||
logging.OnError(err).Fatal("set user password")
|
||||
}
|
||||
|
||||
func (s *Tester) AddGenericOAuthProvider(t *testing.T) string {
|
||||
ctx := authz.WithInstance(context.Background(), s.Instance)
|
||||
id, _, err := s.Commands.AddOrgGenericOAuthProvider(ctx, s.Organisation.ID, command.GenericOAuthProvider{
|
||||
@ -219,3 +230,20 @@ func (s *Tester) CreatePasskeySession(t *testing.T, ctx context.Context, userID
|
||||
return createResp.GetSessionId(), updateResp.GetSessionToken(),
|
||||
createResp.GetDetails().GetChangeDate().AsTime(), updateResp.GetDetails().GetChangeDate().AsTime()
|
||||
}
|
||||
|
||||
func (s *Tester) CreatePasswordSession(t *testing.T, ctx context.Context, userID, password string) (id, token string, start, change time.Time) {
|
||||
createResp, err := s.Client.SessionV2.CreateSession(ctx, &session.CreateSessionRequest{
|
||||
Checks: &session.Checks{
|
||||
User: &session.CheckUser{
|
||||
Search: &session.CheckUser_UserId{UserId: userID},
|
||||
},
|
||||
Password: &session.CheckPassword{
|
||||
Password: password,
|
||||
},
|
||||
},
|
||||
Domain: s.Config.ExternalDomain,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
return createResp.GetSessionId(), createResp.GetSessionToken(),
|
||||
createResp.GetDetails().GetChangeDate().AsTime(), createResp.GetDetails().GetChangeDate().AsTime()
|
||||
}
|
||||
|
@ -64,6 +64,7 @@ const (
|
||||
|
||||
const (
|
||||
FirstInstanceUsersKey = "first"
|
||||
UserPassword = "VeryS3cret!"
|
||||
)
|
||||
|
||||
// User information with a Personal Access Token.
|
||||
|
@ -8,25 +8,20 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/zitadel/oidc/v2/pkg/client"
|
||||
"github.com/zitadel/oidc/v2/pkg/client/rp"
|
||||
"github.com/zitadel/oidc/v2/pkg/client/rs"
|
||||
"github.com/zitadel/oidc/v2/pkg/oidc"
|
||||
|
||||
http_util "github.com/zitadel/zitadel/internal/api/http"
|
||||
oidc_internal "github.com/zitadel/zitadel/internal/api/oidc"
|
||||
"github.com/zitadel/zitadel/internal/command"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/app"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/management"
|
||||
)
|
||||
|
||||
func (s *Tester) CreateOIDCNativeClient(ctx context.Context, redirectURI string) (*management.AddOIDCAppResponse, error) {
|
||||
project, err := s.Client.Mgmt.AddProject(ctx, &management.AddProjectRequest{
|
||||
Name: fmt.Sprintf("project-%d", time.Now().UnixNano()),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
func (s *Tester) CreateOIDCNativeClient(ctx context.Context, redirectURI, projectID string) (*management.AddOIDCAppResponse, error) {
|
||||
return s.Client.Mgmt.AddOIDCApp(ctx, &management.AddOIDCAppRequest{
|
||||
ProjectId: project.GetId(),
|
||||
ProjectId: projectID,
|
||||
Name: fmt.Sprintf("app-%d", time.Now().UnixNano()),
|
||||
RedirectUris: []string{redirectURI},
|
||||
ResponseTypes: []app.OIDCResponseType{app.OIDCResponseType_OIDC_RESPONSE_TYPE_CODE},
|
||||
@ -74,6 +69,20 @@ func (s *Tester) CreateOIDCImplicitFlowClient(ctx context.Context, redirectURI s
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Tester) CreateProject(ctx context.Context) (*management.AddProjectResponse, error) {
|
||||
return s.Client.Mgmt.AddProject(ctx, &management.AddProjectRequest{
|
||||
Name: fmt.Sprintf("project-%d", time.Now().UnixNano()),
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Tester) CreateAPIClient(ctx context.Context, projectID string) (*management.AddAPIAppResponse, error) {
|
||||
return s.Client.Mgmt.AddAPIApp(ctx, &management.AddAPIAppRequest{
|
||||
ProjectId: projectID,
|
||||
Name: fmt.Sprintf("api-%d", time.Now().UnixNano()),
|
||||
AuthMethodType: app.APIAuthMethodType_API_AUTH_METHOD_TYPE_PRIVATE_KEY_JWT,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Tester) CreateOIDCAuthRequest(clientID, loginClient, redirectURI string, scope ...string) (authRequestID string, err error) {
|
||||
provider, err := s.CreateRelyingParty(clientID, redirectURI, scope...)
|
||||
if err != nil {
|
||||
@ -123,12 +132,23 @@ func (s *Tester) CreateOIDCAuthRequestImplicit(clientID, loginClient, redirectUR
|
||||
return strings.TrimPrefix(loc.String(), prefixWithHost), nil
|
||||
}
|
||||
|
||||
func (s *Tester) OIDCIssuer() string {
|
||||
return http_util.BuildHTTP(s.Config.ExternalDomain, s.Config.Port, s.Config.ExternalSecure)
|
||||
}
|
||||
|
||||
func (s *Tester) CreateRelyingParty(clientID, redirectURI string, scope ...string) (rp.RelyingParty, error) {
|
||||
issuer := http_util.BuildHTTP(s.Config.ExternalDomain, s.Config.Port, s.Config.ExternalSecure)
|
||||
if len(scope) == 0 {
|
||||
scope = []string{oidc.ScopeOpenID}
|
||||
}
|
||||
return rp.NewRelyingPartyOIDC(issuer, clientID, "", redirectURI, scope)
|
||||
return rp.NewRelyingPartyOIDC(s.OIDCIssuer(), clientID, "", redirectURI, scope)
|
||||
}
|
||||
|
||||
func (s *Tester) CreateResourceServer(keyFileData []byte) (rs.ResourceServer, error) {
|
||||
keyFile, err := client.ConfigFromKeyFileData(keyFileData)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return rs.NewResourceServerJWTProfile(s.OIDCIssuer(), keyFile.ClientID, keyFile.KeyID, []byte(keyFile.Key))
|
||||
}
|
||||
|
||||
func CheckRedirect(url string, headers map[string]string) (*url.URL, error) {
|
||||
@ -153,11 +173,3 @@ func CheckRedirect(url string, headers map[string]string) (*url.URL, error) {
|
||||
|
||||
return resp.Location()
|
||||
}
|
||||
|
||||
func (s *Tester) CreateSession(ctx context.Context, userID string) (string, string, error) {
|
||||
session, err := s.Commands.CreateSession(ctx, []command.SessionCommand{command.CheckUser(userID)}, "domain.tld", nil)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
return session.ID, session.NewToken, nil
|
||||
}
|
||||
|
113
internal/query/access_token.go
Normal file
113
internal/query/access_token.go
Normal file
@ -0,0 +1,113 @@
|
||||
package query
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
caos_errs "github.com/zitadel/zitadel/internal/errors"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/repository/oidcsession"
|
||||
"github.com/zitadel/zitadel/internal/telemetry/tracing"
|
||||
)
|
||||
|
||||
type OIDCSessionAccessTokenReadModel struct {
|
||||
eventstore.WriteModel
|
||||
|
||||
UserID string
|
||||
SessionID string
|
||||
ClientID string
|
||||
Audience []string
|
||||
Scope []string
|
||||
AuthMethods []domain.UserAuthMethodType
|
||||
AuthTime time.Time
|
||||
State domain.OIDCSessionState
|
||||
AccessTokenID string
|
||||
AccessTokenCreation time.Time
|
||||
AccessTokenExpiration time.Time
|
||||
}
|
||||
|
||||
func newOIDCSessionAccessTokenWriteModel(id string) *OIDCSessionAccessTokenReadModel {
|
||||
return &OIDCSessionAccessTokenReadModel{
|
||||
WriteModel: eventstore.WriteModel{
|
||||
AggregateID: id,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (wm *OIDCSessionAccessTokenReadModel) Reduce() error {
|
||||
for _, event := range wm.Events {
|
||||
switch e := event.(type) {
|
||||
case *oidcsession.AddedEvent:
|
||||
wm.reduceAdded(e)
|
||||
case *oidcsession.AccessTokenAddedEvent:
|
||||
wm.reduceAccessTokenAdded(e)
|
||||
}
|
||||
}
|
||||
return wm.WriteModel.Reduce()
|
||||
}
|
||||
|
||||
func (wm *OIDCSessionAccessTokenReadModel) Query() *eventstore.SearchQueryBuilder {
|
||||
return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
|
||||
AllowTimeTravel().
|
||||
AddQuery().
|
||||
AggregateTypes(oidcsession.AggregateType).
|
||||
AggregateIDs(wm.AggregateID).
|
||||
EventTypes(
|
||||
oidcsession.AddedType,
|
||||
oidcsession.AccessTokenAddedType,
|
||||
).
|
||||
Builder()
|
||||
}
|
||||
|
||||
func (wm *OIDCSessionAccessTokenReadModel) reduceAdded(e *oidcsession.AddedEvent) {
|
||||
wm.UserID = e.UserID
|
||||
wm.SessionID = e.SessionID
|
||||
wm.ClientID = e.ClientID
|
||||
wm.Audience = e.Audience
|
||||
wm.Scope = e.Scope
|
||||
wm.AuthMethods = e.AuthMethods
|
||||
wm.AuthTime = e.AuthTime
|
||||
wm.State = domain.OIDCSessionStateActive
|
||||
}
|
||||
|
||||
func (wm *OIDCSessionAccessTokenReadModel) reduceAccessTokenAdded(e *oidcsession.AccessTokenAddedEvent) {
|
||||
wm.AccessTokenID = e.ID
|
||||
wm.AccessTokenCreation = e.CreationDate()
|
||||
wm.AccessTokenExpiration = e.CreationDate().Add(e.Lifetime)
|
||||
}
|
||||
|
||||
// ActiveAccessTokenByToken will check if the token is active by retrieving the OIDCSession events from the eventstore.
|
||||
// refreshed or expired tokens will return an error
|
||||
func (q *Queries) ActiveAccessTokenByToken(ctx context.Context, token string) (model *OIDCSessionAccessTokenReadModel, err error) {
|
||||
ctx, span := tracing.NewSpan(ctx)
|
||||
defer func() { span.EndWithError(err) }()
|
||||
|
||||
split := strings.Split(token, "-")
|
||||
if len(split) != 2 {
|
||||
return nil, caos_errs.ThrowPermissionDenied(nil, "QUERY-SAhtk", "Errors.OIDCSession.Token.Invalid")
|
||||
}
|
||||
model, err = q.accessTokenByOIDCSessionAndTokenID(ctx, split[0], split[1])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !model.AccessTokenExpiration.After(time.Now()) {
|
||||
return nil, caos_errs.ThrowPermissionDenied(nil, "QUERY-SAF3rf", "Errors.OIDCSession.Token.Expired")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (q *Queries) accessTokenByOIDCSessionAndTokenID(ctx context.Context, oidcSessionID, tokenID string) (model *OIDCSessionAccessTokenReadModel, err error) {
|
||||
ctx, span := tracing.NewSpan(ctx)
|
||||
defer func() { span.EndWithError(err) }()
|
||||
|
||||
model = newOIDCSessionAccessTokenWriteModel(oidcSessionID)
|
||||
if err = q.eventstore.FilterToQueryReducer(ctx, model); err != nil {
|
||||
return nil, caos_errs.ThrowPermissionDenied(err, "QUERY-ASfe2", "Errors.OIDCSession.Token.Invalid")
|
||||
}
|
||||
if model.AccessTokenID != tokenID {
|
||||
return nil, caos_errs.ThrowPermissionDenied(nil, "QUERY-M2u9w", "Errors.OIDCSession.Token.Invalid")
|
||||
}
|
||||
return model, nil
|
||||
}
|
@ -40,6 +40,7 @@ type Session struct {
|
||||
|
||||
type SessionUserFactor struct {
|
||||
UserID string
|
||||
ResourceOwner string
|
||||
UserCheckedAt time.Time
|
||||
LoginName string
|
||||
DisplayName string
|
||||
@ -225,6 +226,7 @@ func prepareSessionQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuil
|
||||
SessionColumnUserCheckedAt.identifier(),
|
||||
LoginNameNameCol.identifier(),
|
||||
HumanDisplayNameCol.identifier(),
|
||||
UserResourceOwnerCol.identifier(),
|
||||
SessionColumnPasswordCheckedAt.identifier(),
|
||||
SessionColumnIntentCheckedAt.identifier(),
|
||||
SessionColumnPasskeyCheckedAt.identifier(),
|
||||
@ -232,7 +234,8 @@ func prepareSessionQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuil
|
||||
SessionColumnToken.identifier(),
|
||||
).From(sessionsTable.identifier()).
|
||||
LeftJoin(join(LoginNameUserIDCol, SessionColumnUserID)).
|
||||
LeftJoin(join(HumanUserIDCol, SessionColumnUserID) + db.Timetravel(call.Took(ctx))).
|
||||
LeftJoin(join(HumanUserIDCol, SessionColumnUserID)).
|
||||
LeftJoin(join(UserIDCol, SessionColumnUserID) + db.Timetravel(call.Took(ctx))).
|
||||
PlaceholderFormat(sq.Dollar), func(row *sql.Row) (*Session, string, error) {
|
||||
session := new(Session)
|
||||
|
||||
@ -241,6 +244,7 @@ func prepareSessionQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuil
|
||||
userCheckedAt sql.NullTime
|
||||
loginName sql.NullString
|
||||
displayName sql.NullString
|
||||
userResourceOwner sql.NullString
|
||||
passwordCheckedAt sql.NullTime
|
||||
intentCheckedAt sql.NullTime
|
||||
passkeyCheckedAt sql.NullTime
|
||||
@ -262,6 +266,7 @@ func prepareSessionQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuil
|
||||
&userCheckedAt,
|
||||
&loginName,
|
||||
&displayName,
|
||||
&userResourceOwner,
|
||||
&passwordCheckedAt,
|
||||
&intentCheckedAt,
|
||||
&passkeyCheckedAt,
|
||||
@ -281,6 +286,7 @@ func prepareSessionQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuil
|
||||
session.UserFactor.UserCheckedAt = userCheckedAt.Time
|
||||
session.UserFactor.LoginName = loginName.String
|
||||
session.UserFactor.DisplayName = displayName.String
|
||||
session.UserFactor.ResourceOwner = userResourceOwner.String
|
||||
session.PasswordFactor.PasswordCheckedAt = passwordCheckedAt.Time
|
||||
session.IntentFactor.IntentCheckedAt = intentCheckedAt.Time
|
||||
session.PasskeyFactor.PasskeyCheckedAt = passkeyCheckedAt.Time
|
||||
@ -304,6 +310,7 @@ func prepareSessionsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBui
|
||||
SessionColumnUserCheckedAt.identifier(),
|
||||
LoginNameNameCol.identifier(),
|
||||
HumanDisplayNameCol.identifier(),
|
||||
UserResourceOwnerCol.identifier(),
|
||||
SessionColumnPasswordCheckedAt.identifier(),
|
||||
SessionColumnIntentCheckedAt.identifier(),
|
||||
SessionColumnPasskeyCheckedAt.identifier(),
|
||||
@ -311,7 +318,8 @@ func prepareSessionsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBui
|
||||
countColumn.identifier(),
|
||||
).From(sessionsTable.identifier()).
|
||||
LeftJoin(join(LoginNameUserIDCol, SessionColumnUserID)).
|
||||
LeftJoin(join(HumanUserIDCol, SessionColumnUserID) + db.Timetravel(call.Took(ctx))).
|
||||
LeftJoin(join(HumanUserIDCol, SessionColumnUserID)).
|
||||
LeftJoin(join(UserIDCol, SessionColumnUserID) + db.Timetravel(call.Took(ctx))).
|
||||
PlaceholderFormat(sq.Dollar), func(rows *sql.Rows) (*Sessions, error) {
|
||||
sessions := &Sessions{Sessions: []*Session{}}
|
||||
|
||||
@ -323,6 +331,7 @@ func prepareSessionsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBui
|
||||
userCheckedAt sql.NullTime
|
||||
loginName sql.NullString
|
||||
displayName sql.NullString
|
||||
userResourceOwner sql.NullString
|
||||
passwordCheckedAt sql.NullTime
|
||||
intentCheckedAt sql.NullTime
|
||||
passkeyCheckedAt sql.NullTime
|
||||
@ -343,6 +352,7 @@ func prepareSessionsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBui
|
||||
&userCheckedAt,
|
||||
&loginName,
|
||||
&displayName,
|
||||
&userResourceOwner,
|
||||
&passwordCheckedAt,
|
||||
&intentCheckedAt,
|
||||
&passkeyCheckedAt,
|
||||
@ -358,6 +368,7 @@ func prepareSessionsQuery(ctx context.Context, db prepareDatabase) (sq.SelectBui
|
||||
session.UserFactor.UserCheckedAt = userCheckedAt.Time
|
||||
session.UserFactor.LoginName = loginName.String
|
||||
session.UserFactor.DisplayName = displayName.String
|
||||
session.UserFactor.ResourceOwner = userResourceOwner.String
|
||||
session.PasswordFactor.PasswordCheckedAt = passwordCheckedAt.Time
|
||||
session.IntentFactor.IntentCheckedAt = intentCheckedAt.Time
|
||||
session.PasskeyFactor.PasskeyCheckedAt = passkeyCheckedAt.Time
|
||||
|
@ -29,6 +29,7 @@ var (
|
||||
` projections.sessions3.user_checked_at,` +
|
||||
` projections.login_names2.login_name,` +
|
||||
` projections.users8_humans.display_name,` +
|
||||
` projections.users8.resource_owner,` +
|
||||
` projections.sessions3.password_checked_at,` +
|
||||
` projections.sessions3.intent_checked_at,` +
|
||||
` projections.sessions3.passkey_checked_at,` +
|
||||
@ -37,6 +38,7 @@ var (
|
||||
` FROM projections.sessions3` +
|
||||
` LEFT JOIN projections.login_names2 ON projections.sessions3.user_id = projections.login_names2.user_id AND projections.sessions3.instance_id = projections.login_names2.instance_id` +
|
||||
` LEFT JOIN projections.users8_humans ON projections.sessions3.user_id = projections.users8_humans.user_id AND projections.sessions3.instance_id = projections.users8_humans.instance_id` +
|
||||
` LEFT JOIN projections.users8 ON projections.sessions3.user_id = projections.users8.id AND projections.sessions3.instance_id = projections.users8.instance_id` +
|
||||
` AS OF SYSTEM TIME '-1 ms'`)
|
||||
expectedSessionsQuery = regexp.QuoteMeta(`SELECT projections.sessions3.id,` +
|
||||
` projections.sessions3.creation_date,` +
|
||||
@ -50,6 +52,7 @@ var (
|
||||
` projections.sessions3.user_checked_at,` +
|
||||
` projections.login_names2.login_name,` +
|
||||
` projections.users8_humans.display_name,` +
|
||||
` projections.users8.resource_owner,` +
|
||||
` projections.sessions3.password_checked_at,` +
|
||||
` projections.sessions3.intent_checked_at,` +
|
||||
` projections.sessions3.passkey_checked_at,` +
|
||||
@ -58,6 +61,7 @@ var (
|
||||
` FROM projections.sessions3` +
|
||||
` LEFT JOIN projections.login_names2 ON projections.sessions3.user_id = projections.login_names2.user_id AND projections.sessions3.instance_id = projections.login_names2.instance_id` +
|
||||
` LEFT JOIN projections.users8_humans ON projections.sessions3.user_id = projections.users8_humans.user_id AND projections.sessions3.instance_id = projections.users8_humans.instance_id` +
|
||||
` LEFT JOIN projections.users8 ON projections.sessions3.user_id = projections.users8.id AND projections.sessions3.instance_id = projections.users8.instance_id` +
|
||||
` AS OF SYSTEM TIME '-1 ms'`)
|
||||
|
||||
sessionCols = []string{
|
||||
@ -73,6 +77,7 @@ var (
|
||||
"user_checked_at",
|
||||
"login_name",
|
||||
"display_name",
|
||||
"user_resource_owner",
|
||||
"password_checked_at",
|
||||
"intent_checked_at",
|
||||
"passkey_checked_at",
|
||||
@ -93,6 +98,7 @@ var (
|
||||
"user_checked_at",
|
||||
"login_name",
|
||||
"display_name",
|
||||
"user_resource_owner",
|
||||
"password_checked_at",
|
||||
"intent_checked_at",
|
||||
"passkey_checked_at",
|
||||
@ -145,6 +151,7 @@ func Test_SessionsPrepare(t *testing.T) {
|
||||
testNow,
|
||||
"login-name",
|
||||
"display-name",
|
||||
"resourceOwner",
|
||||
testNow,
|
||||
testNow,
|
||||
testNow,
|
||||
@ -172,6 +179,7 @@ func Test_SessionsPrepare(t *testing.T) {
|
||||
UserCheckedAt: testNow,
|
||||
LoginName: "login-name",
|
||||
DisplayName: "display-name",
|
||||
ResourceOwner: "resourceOwner",
|
||||
},
|
||||
PasswordFactor: SessionPasswordFactor{
|
||||
PasswordCheckedAt: testNow,
|
||||
@ -210,6 +218,7 @@ func Test_SessionsPrepare(t *testing.T) {
|
||||
testNow,
|
||||
"login-name",
|
||||
"display-name",
|
||||
"resourceOwner",
|
||||
testNow,
|
||||
testNow,
|
||||
testNow,
|
||||
@ -228,6 +237,7 @@ func Test_SessionsPrepare(t *testing.T) {
|
||||
testNow,
|
||||
"login-name2",
|
||||
"display-name2",
|
||||
"resourceOwner",
|
||||
testNow,
|
||||
testNow,
|
||||
testNow,
|
||||
@ -255,6 +265,7 @@ func Test_SessionsPrepare(t *testing.T) {
|
||||
UserCheckedAt: testNow,
|
||||
LoginName: "login-name",
|
||||
DisplayName: "display-name",
|
||||
ResourceOwner: "resourceOwner",
|
||||
},
|
||||
PasswordFactor: SessionPasswordFactor{
|
||||
PasswordCheckedAt: testNow,
|
||||
@ -283,6 +294,7 @@ func Test_SessionsPrepare(t *testing.T) {
|
||||
UserCheckedAt: testNow,
|
||||
LoginName: "login-name2",
|
||||
DisplayName: "display-name2",
|
||||
ResourceOwner: "resourceOwner",
|
||||
},
|
||||
PasswordFactor: SessionPasswordFactor{
|
||||
PasswordCheckedAt: testNow,
|
||||
@ -374,6 +386,7 @@ func Test_SessionPrepare(t *testing.T) {
|
||||
testNow,
|
||||
"login-name",
|
||||
"display-name",
|
||||
"resourceOwner",
|
||||
testNow,
|
||||
testNow,
|
||||
testNow,
|
||||
@ -396,6 +409,7 @@ func Test_SessionPrepare(t *testing.T) {
|
||||
UserCheckedAt: testNow,
|
||||
LoginName: "login-name",
|
||||
DisplayName: "display-name",
|
||||
ResourceOwner: "resourceOwner",
|
||||
},
|
||||
PasswordFactor: SessionPasswordFactor{
|
||||
PasswordCheckedAt: testNow,
|
||||
|
@ -79,6 +79,12 @@ var (
|
||||
name: "count",
|
||||
table: userIDPsCountTable,
|
||||
}
|
||||
|
||||
forceMFATable = loginPolicyTable.setAlias("auth_methods_force_mfa")
|
||||
forceMFAInstanceID = LoginPolicyColumnInstanceID.setTable(forceMFATable)
|
||||
forceMFAOrgID = LoginPolicyColumnOrgID.setTable(forceMFATable)
|
||||
forceMFAIsDefault = LoginPolicyColumnIsDefault.setTable(forceMFATable)
|
||||
forceMFAForce = LoginPolicyColumnForceMFA.setTable(forceMFATable)
|
||||
)
|
||||
|
||||
type AuthMethods struct {
|
||||
@ -170,6 +176,36 @@ func (q *Queries) ListActiveUserAuthMethodTypes(ctx context.Context, userID stri
|
||||
return userAuthMethodTypes, err
|
||||
}
|
||||
|
||||
func (q *Queries) ListUserAuthMethodTypesRequired(ctx context.Context, userID string, withOwnerRemoved bool) (userAuthMethodTypes []domain.UserAuthMethodType, forceMFA bool, err error) {
|
||||
ctxData := authz.GetCtxData(ctx)
|
||||
if ctxData.UserID != userID {
|
||||
if err := q.checkPermission(ctx, domain.PermissionUserRead, ctxData.OrgID, userID); err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
}
|
||||
ctx, span := tracing.NewSpan(ctx)
|
||||
defer func() { span.EndWithError(err) }()
|
||||
|
||||
query, scan := prepareUserAuthMethodTypesRequiredQuery(ctx, q.client)
|
||||
eq := sq.Eq{
|
||||
UserIDCol.identifier(): userID,
|
||||
UserInstanceIDCol.identifier(): authz.GetInstance(ctx).InstanceID(),
|
||||
}
|
||||
if !withOwnerRemoved {
|
||||
eq[UserOwnerRemovedCol.identifier()] = false
|
||||
}
|
||||
stmt, args, err := query.Where(eq).ToSql()
|
||||
if err != nil {
|
||||
return nil, false, errors.ThrowInvalidArgument(err, "QUERY-E5ut4", "Errors.Query.InvalidRequest")
|
||||
}
|
||||
|
||||
rows, err := q.client.QueryContext(ctx, stmt, args...)
|
||||
if err != nil || rows.Err() != nil {
|
||||
return nil, false, errors.ThrowInternal(err, "QUERY-Dun75", "Errors.Internal")
|
||||
}
|
||||
return scan(rows)
|
||||
}
|
||||
|
||||
func NewUserAuthMethodUserIDSearchQuery(value string) (SearchQuery, error) {
|
||||
return NewTextQuery(UserAuthMethodColumnUserID, value, TextEquals)
|
||||
}
|
||||
@ -311,26 +347,11 @@ func prepareUserAuthMethodsQuery(ctx context.Context, db prepareDatabase) (sq.Se
|
||||
}
|
||||
|
||||
func prepareActiveUserAuthMethodTypesQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*AuthMethodTypes, error)) {
|
||||
authMethodsQuery, authMethodsArgs, err := sq.Select(
|
||||
"DISTINCT("+authMethodTypeTypes.identifier()+")",
|
||||
authMethodTypeUserID.identifier(),
|
||||
authMethodTypeInstanceID.identifier()).
|
||||
From(authMethodTypeTable.identifier()).
|
||||
Where(sq.Eq{authMethodTypeState.identifier(): domain.MFAStateReady}).
|
||||
ToSql()
|
||||
authMethodsQuery, authMethodsArgs, err := prepareAuthMethodQuery()
|
||||
if err != nil {
|
||||
return sq.SelectBuilder{}, nil
|
||||
}
|
||||
idpsQuery, _, err := sq.Select(
|
||||
userIDPsCountUserID.identifier(),
|
||||
userIDPsCountInstanceID.identifier(),
|
||||
"COUNT("+userIDPsCountUserID.identifier()+") AS "+userIDPsCountCount.name).
|
||||
From(userIDPsCountTable.identifier()).
|
||||
GroupBy(
|
||||
userIDPsCountUserID.identifier(),
|
||||
userIDPsCountInstanceID.identifier(),
|
||||
).
|
||||
ToSql()
|
||||
idpsQuery, err := prepareAuthMethodsIDPsQuery()
|
||||
if err != nil {
|
||||
return sq.SelectBuilder{}, nil
|
||||
}
|
||||
@ -386,3 +407,106 @@ func prepareActiveUserAuthMethodTypesQuery(ctx context.Context, db prepareDataba
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func prepareUserAuthMethodTypesRequiredQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) ([]domain.UserAuthMethodType, bool, error)) {
|
||||
loginPolicyQuery, err := prepareAuthMethodsForceMFAQuery()
|
||||
if err != nil {
|
||||
return sq.SelectBuilder{}, nil
|
||||
}
|
||||
authMethodsQuery, authMethodsArgs, err := prepareAuthMethodQuery()
|
||||
if err != nil {
|
||||
return sq.SelectBuilder{}, nil
|
||||
}
|
||||
idpsQuery, err := prepareAuthMethodsIDPsQuery()
|
||||
if err != nil {
|
||||
return sq.SelectBuilder{}, nil
|
||||
}
|
||||
return sq.Select(
|
||||
NotifyPasswordSetCol.identifier(),
|
||||
authMethodTypeTypes.identifier(),
|
||||
userIDPsCountCount.identifier(),
|
||||
forceMFAForce.identifier()).
|
||||
From(userTable.identifier()).
|
||||
LeftJoin(join(NotifyUserIDCol, UserIDCol)).
|
||||
LeftJoin("("+authMethodsQuery+") AS "+authMethodTypeTable.alias+" ON "+
|
||||
authMethodTypeUserID.identifier()+" = "+UserIDCol.identifier()+" AND "+
|
||||
authMethodTypeInstanceID.identifier()+" = "+UserInstanceIDCol.identifier(),
|
||||
authMethodsArgs...).
|
||||
LeftJoin("(" + idpsQuery + ") AS " + userIDPsCountTable.alias + " ON " +
|
||||
userIDPsCountUserID.identifier() + " = " + UserIDCol.identifier() + " AND " +
|
||||
userIDPsCountInstanceID.identifier() + " = " + UserInstanceIDCol.identifier()).
|
||||
LeftJoin("(" + loginPolicyQuery + ") AS " + forceMFATable.alias + " ON " +
|
||||
"(" + forceMFAOrgID.identifier() + " = " + UserInstanceIDCol.identifier() + " OR " + forceMFAOrgID.identifier() + " = " + UserResourceOwnerCol.identifier() + ") AND " +
|
||||
forceMFAInstanceID.identifier() + " = " + UserInstanceIDCol.identifier() + db.Timetravel(call.Took(ctx))).
|
||||
PlaceholderFormat(sq.Dollar),
|
||||
func(rows *sql.Rows) ([]domain.UserAuthMethodType, bool, error) {
|
||||
userAuthMethodTypes := make([]domain.UserAuthMethodType, 0)
|
||||
var passwordSet sql.NullBool
|
||||
var idp sql.NullInt64
|
||||
var forceMFA sql.NullBool
|
||||
for rows.Next() {
|
||||
var authMethodType sql.NullInt16
|
||||
err := rows.Scan(
|
||||
&passwordSet,
|
||||
&authMethodType,
|
||||
&idp,
|
||||
&forceMFA,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
if authMethodType.Valid {
|
||||
userAuthMethodTypes = append(userAuthMethodTypes, domain.UserAuthMethodType(authMethodType.Int16))
|
||||
}
|
||||
}
|
||||
if passwordSet.Valid && passwordSet.Bool {
|
||||
userAuthMethodTypes = append(userAuthMethodTypes, domain.UserAuthMethodTypePassword)
|
||||
}
|
||||
if idp.Valid && idp.Int64 > 0 {
|
||||
logging.Error("IDP", idp.Int64)
|
||||
userAuthMethodTypes = append(userAuthMethodTypes, domain.UserAuthMethodTypeIDP)
|
||||
}
|
||||
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, false, errors.ThrowInternal(err, "QUERY-W4zje", "Errors.Query.CloseRows")
|
||||
}
|
||||
|
||||
return userAuthMethodTypes, forceMFA.Bool, nil
|
||||
}
|
||||
}
|
||||
|
||||
func prepareAuthMethodsIDPsQuery() (string, error) {
|
||||
idpsQuery, _, err := sq.Select(
|
||||
userIDPsCountUserID.identifier(),
|
||||
userIDPsCountInstanceID.identifier(),
|
||||
"COUNT("+userIDPsCountUserID.identifier()+") AS "+userIDPsCountCount.name).
|
||||
From(userIDPsCountTable.identifier()).
|
||||
GroupBy(
|
||||
userIDPsCountUserID.identifier(),
|
||||
userIDPsCountInstanceID.identifier(),
|
||||
).
|
||||
ToSql()
|
||||
return idpsQuery, err
|
||||
}
|
||||
|
||||
func prepareAuthMethodQuery() (string, []interface{}, error) {
|
||||
return sq.Select(
|
||||
"DISTINCT("+authMethodTypeTypes.identifier()+")",
|
||||
authMethodTypeUserID.identifier(),
|
||||
authMethodTypeInstanceID.identifier()).
|
||||
From(authMethodTypeTable.identifier()).
|
||||
Where(sq.Eq{authMethodTypeState.identifier(): domain.MFAStateReady}).
|
||||
ToSql()
|
||||
}
|
||||
|
||||
func prepareAuthMethodsForceMFAQuery() (string, error) {
|
||||
loginPolicyQuery, _, err := sq.Select(
|
||||
forceMFAForce.identifier(),
|
||||
forceMFAInstanceID.identifier(),
|
||||
forceMFAOrgID.identifier(),
|
||||
).
|
||||
From(forceMFATable.identifier()).
|
||||
OrderBy(forceMFAIsDefault.identifier()).
|
||||
ToSql()
|
||||
return loginPolicyQuery, err
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
package query
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"database/sql/driver"
|
||||
"errors"
|
||||
@ -8,6 +9,8 @@ import (
|
||||
"regexp"
|
||||
"testing"
|
||||
|
||||
sq "github.com/Masterminds/squirrel"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
)
|
||||
|
||||
@ -53,6 +56,28 @@ var (
|
||||
"method_type",
|
||||
"idps_count",
|
||||
}
|
||||
prepareAuthMethodTypesRequiredStmt = `SELECT projections.users8_notifications.password_set,` +
|
||||
` auth_method_types.method_type,` +
|
||||
` user_idps_count.count,` +
|
||||
` auth_methods_force_mfa.force_mfa` +
|
||||
` FROM projections.users8` +
|
||||
` LEFT JOIN projections.users8_notifications ON projections.users8.id = projections.users8_notifications.user_id AND projections.users8.instance_id = projections.users8_notifications.instance_id` +
|
||||
` LEFT JOIN (SELECT DISTINCT(auth_method_types.method_type), auth_method_types.user_id, auth_method_types.instance_id FROM projections.user_auth_methods4 AS auth_method_types` +
|
||||
` WHERE auth_method_types.state = $1) AS auth_method_types` +
|
||||
` ON auth_method_types.user_id = projections.users8.id AND auth_method_types.instance_id = projections.users8.instance_id` +
|
||||
` LEFT JOIN (SELECT user_idps_count.user_id, user_idps_count.instance_id, COUNT(user_idps_count.user_id) AS count FROM projections.idp_user_links3 AS user_idps_count` +
|
||||
` GROUP BY user_idps_count.user_id, user_idps_count.instance_id) AS user_idps_count` +
|
||||
` ON user_idps_count.user_id = projections.users8.id AND user_idps_count.instance_id = projections.users8.instance_id` +
|
||||
` LEFT JOIN (SELECT auth_methods_force_mfa.force_mfa, auth_methods_force_mfa.instance_id, auth_methods_force_mfa.aggregate_id FROM projections.login_policies4 AS auth_methods_force_mfa ORDER BY auth_methods_force_mfa.is_default) AS auth_methods_force_mfa` +
|
||||
` ON (auth_methods_force_mfa.aggregate_id = projections.users8.instance_id OR auth_methods_force_mfa.aggregate_id = projections.users8.resource_owner) AND auth_methods_force_mfa.instance_id = projections.users8.instance_id` +
|
||||
` AS OF SYSTEM TIME '-1 ms
|
||||
`
|
||||
prepareAuthMethodTypesRequiredCols = []string{
|
||||
"password_set",
|
||||
"method_type",
|
||||
"idps_count",
|
||||
"force_mfa",
|
||||
}
|
||||
)
|
||||
|
||||
func Test_UserAuthMethodPrepares(t *testing.T) {
|
||||
@ -288,6 +313,131 @@ func Test_UserAuthMethodPrepares(t *testing.T) {
|
||||
},
|
||||
object: nil,
|
||||
},
|
||||
{
|
||||
name: "prepareUserAuthMethodTypesRequiredQuery no result",
|
||||
prepare: func(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*testUserAuthMethodTypesRequired, error)) {
|
||||
builder, scan := prepareUserAuthMethodTypesRequiredQuery(ctx, db)
|
||||
return builder, func(rows *sql.Rows) (*testUserAuthMethodTypesRequired, error) {
|
||||
authMethods, forceMFA, err := scan(rows)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &testUserAuthMethodTypesRequired{authMethods: authMethods, forceMFA: forceMFA}, nil
|
||||
}
|
||||
},
|
||||
want: want{
|
||||
sqlExpectations: mockQueries(
|
||||
regexp.QuoteMeta(prepareAuthMethodTypesRequiredStmt),
|
||||
nil,
|
||||
nil,
|
||||
),
|
||||
},
|
||||
object: &testUserAuthMethodTypesRequired{authMethods: []domain.UserAuthMethodType{}, forceMFA: false},
|
||||
},
|
||||
{
|
||||
name: "prepareUserAuthMethodTypesRequiredQuery one second factor",
|
||||
prepare: func(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*testUserAuthMethodTypesRequired, error)) {
|
||||
builder, scan := prepareUserAuthMethodTypesRequiredQuery(ctx, db)
|
||||
return builder, func(rows *sql.Rows) (*testUserAuthMethodTypesRequired, error) {
|
||||
authMethods, forceMFA, err := scan(rows)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &testUserAuthMethodTypesRequired{authMethods: authMethods, forceMFA: forceMFA}, nil
|
||||
}
|
||||
},
|
||||
want: want{
|
||||
sqlExpectations: mockQueries(
|
||||
regexp.QuoteMeta(prepareAuthMethodTypesRequiredStmt),
|
||||
prepareAuthMethodTypesRequiredCols,
|
||||
[][]driver.Value{
|
||||
{
|
||||
true,
|
||||
domain.UserAuthMethodTypePasswordless,
|
||||
1,
|
||||
true,
|
||||
},
|
||||
},
|
||||
),
|
||||
},
|
||||
object: &testUserAuthMethodTypesRequired{
|
||||
authMethods: []domain.UserAuthMethodType{
|
||||
domain.UserAuthMethodTypePasswordless,
|
||||
domain.UserAuthMethodTypePassword,
|
||||
domain.UserAuthMethodTypeIDP,
|
||||
},
|
||||
forceMFA: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "prepareUserAuthMethodTypesRequiredQuery multiple second factors",
|
||||
prepare: func(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*testUserAuthMethodTypesRequired, error)) {
|
||||
builder, scan := prepareUserAuthMethodTypesRequiredQuery(ctx, db)
|
||||
return builder, func(rows *sql.Rows) (*testUserAuthMethodTypesRequired, error) {
|
||||
authMethods, forceMFA, err := scan(rows)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &testUserAuthMethodTypesRequired{authMethods: authMethods, forceMFA: forceMFA}, nil
|
||||
}
|
||||
},
|
||||
want: want{
|
||||
sqlExpectations: mockQueries(
|
||||
regexp.QuoteMeta(prepareAuthMethodTypesRequiredStmt),
|
||||
prepareAuthMethodTypesRequiredCols,
|
||||
[][]driver.Value{
|
||||
{
|
||||
true,
|
||||
domain.UserAuthMethodTypePasswordless,
|
||||
1,
|
||||
true,
|
||||
},
|
||||
{
|
||||
true,
|
||||
domain.UserAuthMethodTypeOTP,
|
||||
1,
|
||||
true,
|
||||
},
|
||||
},
|
||||
),
|
||||
},
|
||||
|
||||
object: &testUserAuthMethodTypesRequired{
|
||||
authMethods: []domain.UserAuthMethodType{
|
||||
domain.UserAuthMethodTypePasswordless,
|
||||
domain.UserAuthMethodTypeOTP,
|
||||
domain.UserAuthMethodTypePassword,
|
||||
domain.UserAuthMethodTypeIDP,
|
||||
},
|
||||
forceMFA: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "prepareUserAuthMethodTypesRequiredQuery sql err",
|
||||
prepare: func(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*testUserAuthMethodTypesRequired, error)) {
|
||||
builder, scan := prepareUserAuthMethodTypesRequiredQuery(ctx, db)
|
||||
return builder, func(rows *sql.Rows) (*testUserAuthMethodTypesRequired, error) {
|
||||
authMethods, forceMFA, err := scan(rows)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &testUserAuthMethodTypesRequired{authMethods: authMethods, forceMFA: forceMFA}, nil
|
||||
}
|
||||
},
|
||||
want: want{
|
||||
sqlExpectations: mockQueryErr(
|
||||
regexp.QuoteMeta(prepareAuthMethodTypesRequiredStmt),
|
||||
sql.ErrConnDone,
|
||||
),
|
||||
err: func(err error) (error, bool) {
|
||||
if !errors.Is(err, sql.ErrConnDone) {
|
||||
return fmt.Errorf("err should be sql.ErrConnDone got: %w", err), false
|
||||
}
|
||||
return nil, true
|
||||
},
|
||||
},
|
||||
object: nil,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
@ -295,3 +445,9 @@ func Test_UserAuthMethodPrepares(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// testUserAuthMethodTypesRequired is required as assetPrepare is only able to return a single object from scan
|
||||
type testUserAuthMethodTypesRequired struct {
|
||||
authMethods []domain.UserAuthMethodType
|
||||
forceMFA bool
|
||||
}
|
||||
|
@ -514,6 +514,9 @@ Errors:
|
||||
WrongLoginClient: Auth Request, създаден от друг клиент за влизане
|
||||
OIDCSession:
|
||||
RefreshTokenInvalid: Токенът за опресняване е невалиден
|
||||
Token:
|
||||
Invalid: Токенът е невалиден
|
||||
Expired: Токенът е изтекъл
|
||||
|
||||
AggregateTypes:
|
||||
action: Действие
|
||||
|
@ -496,6 +496,10 @@ Errors:
|
||||
WrongLoginClient: Auth Request wurde von einem anderen Login-Client erstellt
|
||||
OIDCSession:
|
||||
RefreshTokenInvalid: Refresh Token ist ungültig
|
||||
Token:
|
||||
Invalid: Token ist ungültig
|
||||
Expired: Token ist abgelaufen
|
||||
|
||||
AggregateTypes:
|
||||
action: Action
|
||||
instance: Instanz
|
||||
|
@ -496,6 +496,9 @@ Errors:
|
||||
WrongLoginClient: Auth Request created by other login client
|
||||
OIDCSession:
|
||||
RefreshTokenInvalid: Refresh Token is invalid
|
||||
Token:
|
||||
Invalid: Token is invalid
|
||||
Expired: Token is expired
|
||||
|
||||
AggregateTypes:
|
||||
action: Action
|
||||
|
@ -496,6 +496,9 @@ Errors:
|
||||
WrongLoginClient: Auth Request creado por otro cliente de inicio de sesión
|
||||
OIDCSession:
|
||||
RefreshTokenInvalid: El token de refresco no es válido
|
||||
Token:
|
||||
Invalid: El token no es válido
|
||||
Expired: El token ha caducado
|
||||
|
||||
AggregateTypes:
|
||||
action: Acción
|
||||
|
@ -496,6 +496,9 @@ Errors:
|
||||
WrongLoginClient: Auth Request créé par un autre client de connexion
|
||||
OIDCSession:
|
||||
RefreshTokenInvalid: Le jeton de rafraîchissement n'est pas valide
|
||||
Token:
|
||||
Invalid: Le jeton n'est pas valide
|
||||
Expired: Le jeton est expiré
|
||||
|
||||
AggregateTypes:
|
||||
action: Action
|
||||
|
@ -496,6 +496,9 @@ Errors:
|
||||
WrongLoginClient: Auth Request creato da un altro client di accesso
|
||||
OIDCSession:
|
||||
RefreshTokenInvalid: Refresh Token non è valido
|
||||
Token:
|
||||
Invalid: Token non è valido
|
||||
Expired: Token è scaduto
|
||||
|
||||
AggregateTypes:
|
||||
action: Azione
|
||||
|
@ -485,6 +485,9 @@ Errors:
|
||||
WrongLoginClient: 他のログインクライアントによって作成された AuthRequest
|
||||
OIDCSession:
|
||||
RefreshTokenInvalid: 無効なリフレッシュトークンです
|
||||
Token:
|
||||
Invalid: トークンが無効です
|
||||
Expired: トークンの有効期限が切れている
|
||||
|
||||
AggregateTypes:
|
||||
action: アクション
|
||||
|
@ -490,6 +490,15 @@ Errors:
|
||||
TokenCreationFailed: Неуспешно креирање на токен
|
||||
InvalidToken: Токенот за намера е невалиден
|
||||
OtherUser: Намерата е за друг корисник
|
||||
AuthRequest:
|
||||
AlreadyExists: Барањето за автентикација веќе постои
|
||||
NotExisting: Барањето за автентикација не постои
|
||||
WrongLoginClient: Барањето за автификација беше креирано од друг клиент за најавување
|
||||
OIDCSession:
|
||||
RefreshTokenInvalid: Токенот за освежување е неважечки
|
||||
Token:
|
||||
Invalid: токенот е неважечки
|
||||
Expired: токенот е истечен
|
||||
|
||||
AggregateTypes:
|
||||
action: Акција
|
||||
|
@ -496,6 +496,9 @@ Errors:
|
||||
WrongLoginClient: Auth Request utworzony przez innego klienta logowania
|
||||
OIDCSession:
|
||||
RefreshTokenInvalid: Refresh Token jest nieprawidłowy
|
||||
Token:
|
||||
Invalid: Token jest nieprawidłowy
|
||||
Expired: Token wygasł
|
||||
|
||||
AggregateTypes:
|
||||
action: Działanie
|
||||
|
@ -496,6 +496,9 @@ Errors:
|
||||
WrongLoginClient: 其他登录客户端创建的AuthRequest
|
||||
OIDCSession:
|
||||
RefreshTokenInvalid: Refresh Token 无效
|
||||
Token:
|
||||
Invalid: 令牌无效
|
||||
Expired: 令牌已过期
|
||||
|
||||
AggregateTypes:
|
||||
action: 动作
|
||||
|
@ -74,6 +74,11 @@ message UserFactor {
|
||||
description: "\"display name of the checked user\"";
|
||||
}
|
||||
];
|
||||
string organisation_id = 5 [
|
||||
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
|
||||
description: "\"organisation id of the checked user\"";
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
message PasswordFactor {
|
||||
|
Loading…
Reference in New Issue
Block a user