fix(oidc): IDP and machine user auth methods (#7992)

# Which Problems Are Solved

After https://github.com/zitadel/zitadel/pull/7822 was merged we
discovered that
v2 tokens that where obtained through an IDP using the v1 login, can't
be used for
zitadel API calls.

- Because we used to store the AMR claim on the auth request, but
internally use the domain.UserAuthMethod type. AMR has no notion of an
IDP login, so that "factor" was lost
during conversion. Rendering those v2 tokens invalid on the zitadel API.
- A wrong check on machine user tokens falsly allowed some tokens to be
valid
- The client ID was set to tokens from client credentials and JWT
profile, which made client queries fail in the validation middleware.
The middleware expects client ID unset for machine users.

# How the Problems Are Solved

Store the domain.AuthMethods directly in  the auth requests and session,
instead of using AMR claims with lossy conversion.

- IDPs have seperate auth method, which is not an AMR claim
- Machine users are treated specialy, eg auth methods are not required.
- Do not set the client ID for client credentials and JWT profile

# Additional Changes

Cleaned up mostly unused `oidc.getInfoFromRequest()`.

# Additional Context

- Bugs were introduced in https://github.com/zitadel/zitadel/pull/7822
and not yet part of a release.
- Reported internally.
This commit is contained in:
Tim Möhlmann
2024-05-23 07:35:10 +02:00
committed by GitHub
parent e57a9b57c8
commit f5e9d4f57f
11 changed files with 126 additions and 112 deletions

View File

@@ -12,7 +12,6 @@ import (
"github.com/zitadel/logging"
"github.com/zitadel/oidc/v3/pkg/oidc"
"github.com/zitadel/oidc/v3/pkg/op"
"golang.org/x/text/language"
"github.com/zitadel/zitadel/internal/api/authz"
http_utils "github.com/zitadel/zitadel/internal/api/http"
@@ -199,23 +198,6 @@ func (*OPStorage) panicErr(method string) error {
return fmt.Errorf("OPStorage.%s should not be called anymore. This is a bug. Please report https://github.com/zitadel/zitadel/issues", method)
}
func getInfoFromRequest(req op.TokenRequest) (agentID, clientID, userOrgID string, authTime time.Time, amr []string, preferredLanguage *language.Tag, reason domain.TokenReason, actor *domain.TokenActor) {
switch r := req.(type) {
case *AuthRequest:
return r.AgentID, r.ApplicationID, r.UserOrgID, r.AuthTime, r.GetAMR(), r.PreferredLanguage, domain.TokenReasonAuthRequest, nil
case *RefreshTokenRequest:
return r.UserAgentID, r.ClientID, "", r.AuthTime, r.AuthMethodsReferences, nil, domain.TokenReasonRefresh, r.Actor
case op.IDTokenRequest:
return "", r.GetClientID(), "", r.GetAuthTime(), r.GetAMR(), nil, domain.TokenReasonAuthRequest, nil
case *oidc.JWTTokenRequest:
return "", "", "", r.GetAuthTime(), nil, nil, domain.TokenReasonJWTProfile, nil
case *clientCredentialsRequest:
return "", "", "", time.Time{}, nil, nil, domain.TokenReasonClientCredentials, nil
default:
return "", "", "", time.Time{}, nil, nil, domain.TokenReasonAuthRequest, nil
}
}
func (o *OPStorage) TokenRequestByRefreshToken(ctx context.Context, refreshToken string) (_ op.RefreshTokenRequest, err error) {
panic("TokenRequestByRefreshToken should not be called anymore. This is a bug. Please report https://github.com/zitadel/zitadel/issues")
}
@@ -511,8 +493,8 @@ func implicitFlowComplianceChecker() command.AuthRequestComplianceChecker {
func (s *Server) authorizeCallbackHandler(w http.ResponseWriter, r *http.Request) {
authorizer := s.Provider()
authReq, err := func() (authReq op.AuthRequest, err error) {
ctx, span := tracing.NewSpan(r.Context())
authReq, err := func(ctx context.Context) (authReq *AuthRequest, err error) {
ctx, span := tracing.NewSpan(ctx)
r = r.WithContext(ctx)
defer func() { span.EndWithError(err) }()
@@ -520,7 +502,7 @@ func (s *Server) authorizeCallbackHandler(w http.ResponseWriter, r *http.Request
if err != nil {
return nil, err
}
authReq, err = authorizer.Storage().AuthRequestByID(r.Context(), id)
authReq, err = s.getAuthRequestV1ByID(ctx, id)
if err != nil {
return nil, err
}
@@ -528,13 +510,13 @@ func (s *Server) authorizeCallbackHandler(w http.ResponseWriter, r *http.Request
return authReq, oidc.ErrInteractionRequired().WithDescription("Unfortunately, the user may be not logged in and/or additional interaction is required.")
}
return authReq, s.authResponse(authReq, authorizer, w, r)
}()
}(r.Context())
if err != nil {
op.AuthRequestError(w, r, authReq, err, authorizer)
}
}
func (s *Server) authResponse(authReq op.AuthRequest, authorizer op.Authorizer, w http.ResponseWriter, r *http.Request) (err error) {
func (s *Server) authResponse(authReq *AuthRequest, authorizer op.Authorizer, w http.ResponseWriter, r *http.Request) (err error) {
ctx, span := tracing.NewSpan(r.Context())
r = r.WithContext(ctx)
defer func() { span.EndWithError(err) }()
@@ -551,7 +533,7 @@ func (s *Server) authResponse(authReq op.AuthRequest, authorizer op.Authorizer,
return s.authResponseToken(authReq, authorizer, client, w, r)
}
func (s *Server) authResponseToken(authReq op.AuthRequest, authorizer op.Authorizer, opClient op.Client, w http.ResponseWriter, r *http.Request) (err error) {
func (s *Server) authResponseToken(authReq *AuthRequest, authorizer op.Authorizer, opClient op.Client, w http.ResponseWriter, r *http.Request) (err error) {
ctx, span := tracing.NewSpan(r.Context())
r = r.WithContext(ctx)
defer func() { span.EndWithError(err) }()
@@ -561,23 +543,20 @@ func (s *Server) authResponseToken(authReq op.AuthRequest, authorizer op.Authori
return zerrors.ThrowInternal(nil, "OIDC-waeN6", "Error.Internal")
}
userAgentID, _, userOrgID, authTime, authMethodsReferences, preferredLanguage, reason, actor := getInfoFromRequest(authReq)
scope := authReq.GetScopes()
session, err := s.command.CreateOIDCSession(ctx,
authReq.GetSubject(),
userOrgID,
authReq.UserID,
authReq.UserOrgID,
client.client.ClientID,
scope,
authReq.GetAudience(),
AMRToAuthMethodTypes(authMethodsReferences),
authTime,
authReq.Audience,
authReq.AuthMethods(),
authReq.AuthTime,
authReq.GetNonce(),
preferredLanguage,
&domain.UserAgent{
FingerprintID: &userAgentID,
},
reason,
actor,
authReq.PreferredLanguage,
authReq.BrowserInfo.ToUserAgent(),
domain.TokenReasonAuthRequest,
nil,
slices.Contains(scope, oidc.ScopeOfflineAccess),
)
if err != nil {

View File

@@ -94,7 +94,7 @@ func (a *AuthRequest) oidc() *domain.AuthRequestOIDC {
return a.Request.(*domain.AuthRequestOIDC)
}
func AuthRequestFromBusiness(authReq *domain.AuthRequest) (_ op.AuthRequest, err error) {
func AuthRequestFromBusiness(authReq *domain.AuthRequest) (_ *AuthRequest, err error) {
if _, ok := authReq.Request.(*domain.AuthRequestOIDC); !ok {
return nil, zerrors.ThrowInvalidArgument(nil, "OIDC-Haz7A", "auth request is not of type oidc")
}

View File

@@ -34,7 +34,7 @@ func (s *Server) ClientCredentialsExchange(ctx context.Context, r *op.ClientRequ
session, err := s.command.CreateOIDCSession(ctx,
client.user.ID,
client.user.ResourceOwner,
r.Data.ClientID,
"",
scope,
domain.AddAudScopeToAudience(ctx, nil, r.Data.Scope),
[]domain.UserAuthMethodType{domain.UserAuthMethodTypePassword},

View File

@@ -8,6 +8,7 @@ import (
"github.com/zitadel/oidc/v3/pkg/oidc"
"github.com/zitadel/oidc/v3/pkg/op"
"github.com/zitadel/zitadel/internal/api/http/middleware"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/telemetry/tracing"
@@ -69,36 +70,33 @@ func (s *Server) codeExchangeV1(ctx context.Context, client *Client, req *oidc.A
if req.RedirectURI != authReq.GetRedirectURI() {
return nil, "", oidc.ErrInvalidGrant().WithDescription("redirect_uri does not correspond")
}
userAgentID, _, userOrgID, authTime, authMethodsReferences, preferredLanguage, reason, actor := getInfoFromRequest(authReq)
scope := authReq.GetScopes()
session, err = s.command.CreateOIDCSession(ctx,
authReq.GetSubject(),
userOrgID,
authReq.UserID,
authReq.UserOrgID,
client.client.ClientID,
scope,
authReq.GetAudience(),
AMRToAuthMethodTypes(authMethodsReferences),
authTime,
authReq.Audience,
authReq.AuthMethods(),
authReq.AuthTime,
authReq.GetNonce(),
preferredLanguage,
&domain.UserAgent{
FingerprintID: &userAgentID,
},
reason,
actor,
authReq.PreferredLanguage,
authReq.BrowserInfo.ToUserAgent(),
domain.TokenReasonAuthRequest,
nil,
slices.Contains(scope, oidc.ScopeOfflineAccess),
)
if err != nil {
return nil, "", err
}
return session, authReq.GetState(), s.repo.DeleteAuthRequest(ctx, authReq.GetID())
return session, authReq.TransferState, s.repo.DeleteAuthRequest(ctx, authReq.ID)
}
// getAuthRequestV1ByCode finds the v1 auth request by code.
// code needs to be the encrypted version of the ID,
// this is required by the underlying repo.
func (s *Server) getAuthRequestV1ByCode(ctx context.Context, code string) (op.AuthRequest, error) {
func (s *Server) getAuthRequestV1ByCode(ctx context.Context, code string) (*AuthRequest, error) {
authReq, err := s.repo.AuthRequestByCode(ctx, code)
if err != nil {
return nil, err
@@ -106,6 +104,18 @@ func (s *Server) getAuthRequestV1ByCode(ctx context.Context, code string) (op.Au
return AuthRequestFromBusiness(authReq)
}
func (s *Server) getAuthRequestV1ByID(ctx context.Context, id string) (*AuthRequest, error) {
userAgentID, ok := middleware.UserAgentIDFromCtx(ctx)
if !ok {
return nil, zerrors.ThrowPreconditionFailed(nil, "OIDC-TiTu7", "no user agent id")
}
resp, err := s.repo.AuthRequestByIDCheckLoggedIn(ctx, id, userAgentID)
if err != nil {
return nil, err
}
return AuthRequestFromBusiness(resp)
}
func codeExchangeComplianceChecker(client *Client, req *oidc.AccessTokenRequest) command.AuthRequestComplianceChecker {
return func(ctx context.Context, authReq *command.AuthRequestWriteModel) error {
if authReq.CodeChallenge != nil || client.AuthMethod() == oidc.AuthMethodNone {

View File

@@ -42,15 +42,15 @@ func (s *Server) JWTProfile(ctx context.Context, r *op.Request[oidc.JWTProfileGr
session, err := s.command.CreateOIDCSession(ctx,
user.ID,
user.ResourceOwner,
jwtReq.Subject,
"",
scope,
domain.AddAudScopeToAudience(ctx, nil, r.Data.Scope),
nil,
[]domain.UserAuthMethodType{domain.UserAuthMethodTypePrivateKey},
time.Now(),
"",
nil,
nil,
domain.TokenReasonClientCredentials,
domain.TokenReasonJWTProfile,
nil,
false,
)