feat(oidc): optimize the userinfo endpoint (#7706)

* feat(oidc): optimize the userinfo endpoint

* store project ID in the access token

* query for projectID if not in token

* add scope based tests

* Revert "store project ID in the access token"

This reverts commit 5f0262f239.

* query project role assertion

* use project role assertion setting to return roles

* workaround eventual consistency and handle PAT

* do not append empty project id
This commit is contained in:
Tim Möhlmann
2024-04-09 16:15:35 +03:00
committed by GitHub
parent c8e0b30e17
commit 6a51c4b0f5
25 changed files with 528 additions and 159 deletions

View File

@@ -5,21 +5,63 @@ import (
"encoding/base64"
"encoding/json"
"fmt"
"net/http"
"slices"
"strings"
"github.com/dop251/goja"
"github.com/zitadel/logging"
"github.com/zitadel/oidc/v3/pkg/oidc"
"github.com/zitadel/oidc/v3/pkg/op"
"github.com/zitadel/zitadel/internal/actions"
"github.com/zitadel/zitadel/internal/actions/object"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/query"
"github.com/zitadel/zitadel/internal/telemetry/tracing"
)
func (s *Server) userInfo(ctx context.Context, userID, projectID string, scope, roleAudience []string) (_ *oidc.UserInfo, err error) {
roleAudience, requestedRoles := prepareRoles(ctx, projectID, scope, roleAudience)
func (s *Server) UserInfo(ctx context.Context, r *op.Request[oidc.UserInfoRequest]) (_ *op.Response, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() {
err = oidcError(err)
span.EndWithError(err)
}()
features := authz.GetFeatures(ctx)
if features.LegacyIntrospection {
return s.LegacyServer.UserInfo(ctx, r)
}
if features.TriggerIntrospectionProjections {
query.TriggerOIDCUserInfoProjections(ctx)
}
token, err := s.verifyAccessToken(ctx, r.Data.AccessToken)
if err != nil {
return nil, op.NewStatusError(oidc.ErrAccessDenied().WithDescription("access token invalid").WithParent(err), http.StatusUnauthorized)
}
var (
projectID string
assertion bool
)
if token.clientID != "" {
projectID, assertion, err = s.query.GetOIDCUserinfoClientByID(ctx, token.clientID)
if err != nil {
return nil, err
}
}
userInfo, err := s.userInfo(ctx, token.userID, projectID, assertion, token.scope, nil)
if err != nil {
return nil, err
}
return op.NewResponse(userInfo), nil
}
func (s *Server) userInfo(ctx context.Context, userID, projectID string, projectRoleAssertion bool, scope, roleAudience []string) (_ *oidc.UserInfo, err error) {
roleAudience, requestedRoles := prepareRoles(ctx, projectID, projectRoleAssertion, scope, roleAudience)
qu, err := s.query.GetOIDCUserInfo(ctx, userID, roleAudience)
if err != nil {
return nil, err
@@ -31,15 +73,14 @@ func (s *Server) userInfo(ctx context.Context, userID, projectID string, scope,
// prepareRoles scans the requested scopes, appends to roleAudience and returns the requestedRoles.
//
// Scopes with [ScopeProjectRolePrefix] are added to requestedRoles.
// When [ScopeProjectsRoles] is present and roleAudience was empty,
// project IDs with the [domain.ProjectIDScope] prefix are added to the roleAudience.
//
// Scopes with [ScopeProjectRolePrefix] are added to requestedRoles.
//
// If the resulting requestedRoles or roleAudience are not not empty,
// If projectRoleAssertion is true and the resulting requestedRoles or roleAudience are not empty,
// the current projectID will always be parts or roleAudience.
// Else nil, nil is returned.
func prepareRoles(ctx context.Context, projectID string, scope, roleAudience []string) (ra, requestedRoles []string) {
func prepareRoles(ctx context.Context, projectID string, projectRoleAssertion bool, scope, roleAudience []string) (ra, requestedRoles []string) {
// if all roles are requested take the audience for those from the scopes
if slices.Contains(scope, ScopeProjectsRoles) && len(roleAudience) == 0 {
roleAudience = domain.AddAudScopeToAudience(ctx, roleAudience, scope)
@@ -50,7 +91,7 @@ func prepareRoles(ctx context.Context, projectID string, scope, roleAudience []s
requestedRoles = append(requestedRoles, role)
}
}
if len(requestedRoles) == 0 && len(roleAudience) == 0 {
if !projectRoleAssertion && len(requestedRoles) == 0 && len(roleAudience) == 0 {
return nil, nil
}