mirror of
https://github.com/zitadel/zitadel.git
synced 2024-12-12 11:04:25 +00:00
c2cb84cd24
* backup new protoc plugin * backup * session * backup * initial implementation * change to specific events * implement tests * cleanup * refactor: use new protoc plugin for api v2 * change package * simplify code * cleanup * cleanup * fix merge * start queries * fix tests * improve returned values * add token to projection * tests * test db map * update query * permission checks * fix tests and linting * rework token creation * i18n * refactor token check and fix tests * session to PB test * request to query tests * cleanup proto * test user check * add comment * simplify database map type * Update docs/docs/guides/integrate/access-zitadel-system-api.md Co-authored-by: Tim Möhlmann <tim+github@zitadel.com> * fix test * cleanup * docs --------- Co-authored-by: Tim Möhlmann <tim+github@zitadel.com>
188 lines
5.9 KiB
Go
188 lines
5.9 KiB
Go
package authz
|
|
|
|
import (
|
|
"context"
|
|
"crypto/rsa"
|
|
"encoding/base64"
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/zitadel/oidc/v2/pkg/op"
|
|
"gopkg.in/square/go-jose.v2"
|
|
|
|
"github.com/zitadel/zitadel/internal/crypto"
|
|
caos_errs "github.com/zitadel/zitadel/internal/errors"
|
|
"github.com/zitadel/zitadel/internal/telemetry/tracing"
|
|
)
|
|
|
|
const (
|
|
BearerPrefix = "Bearer "
|
|
SessionTokenFormat = "sess_%s:%s"
|
|
)
|
|
|
|
type TokenVerifier struct {
|
|
authZRepo authZRepo
|
|
clients sync.Map
|
|
authMethods MethodMapping
|
|
systemJWTProfile op.JWTProfileVerifier
|
|
}
|
|
|
|
type MembershipsResolver interface {
|
|
SearchMyMemberships(ctx context.Context, orgID string) ([]*Membership, error)
|
|
}
|
|
|
|
type authZRepo interface {
|
|
VerifyAccessToken(ctx context.Context, token, verifierClientID, projectID string) (userID, agentID, clientID, prefLang, resourceOwner string, err error)
|
|
VerifierClientID(ctx context.Context, name string) (clientID, projectID string, err error)
|
|
SearchMyMemberships(ctx context.Context, orgID string) ([]*Membership, error)
|
|
ProjectIDAndOriginsByClientID(ctx context.Context, clientID string) (projectID string, origins []string, err error)
|
|
ExistsOrg(ctx context.Context, id, domain string) (string, error)
|
|
}
|
|
|
|
func Start(authZRepo authZRepo, issuer string, keys map[string]*SystemAPIUser) (v *TokenVerifier) {
|
|
return &TokenVerifier{
|
|
authZRepo: authZRepo,
|
|
systemJWTProfile: op.NewJWTProfileVerifier(
|
|
&systemJWTStorage{
|
|
keys: keys,
|
|
cachedKeys: make(map[string]*rsa.PublicKey),
|
|
},
|
|
issuer,
|
|
1*time.Hour,
|
|
time.Second,
|
|
),
|
|
}
|
|
}
|
|
|
|
func (v *TokenVerifier) VerifyAccessToken(ctx context.Context, token string, method string) (userID, clientID, agentID, prefLang, resourceOwner string, err error) {
|
|
if strings.HasPrefix(method, "/zitadel.system.v1.SystemService") {
|
|
userID, err := v.verifySystemToken(ctx, token)
|
|
if err != nil {
|
|
return "", "", "", "", "", err
|
|
}
|
|
return userID, "", "", "", "", nil
|
|
}
|
|
userID, agentID, clientID, prefLang, resourceOwner, err = v.authZRepo.VerifyAccessToken(ctx, token, "", GetInstance(ctx).ProjectID())
|
|
return userID, clientID, agentID, prefLang, resourceOwner, err
|
|
}
|
|
|
|
func (v *TokenVerifier) verifySystemToken(ctx context.Context, token string) (string, error) {
|
|
jwtReq, err := op.VerifyJWTAssertion(ctx, token, v.systemJWTProfile)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return jwtReq.Subject, nil
|
|
}
|
|
|
|
type systemJWTStorage struct {
|
|
keys map[string]*SystemAPIUser
|
|
mutex sync.Mutex
|
|
cachedKeys map[string]*rsa.PublicKey
|
|
}
|
|
|
|
type SystemAPIUser struct {
|
|
Path string //if a path is specified, the key will be read from that path
|
|
KeyData []byte //else you can also specify the data directly in the KeyData
|
|
}
|
|
|
|
func (s *SystemAPIUser) readKey() (*rsa.PublicKey, error) {
|
|
if s.Path != "" {
|
|
var err error
|
|
s.KeyData, err = os.ReadFile(s.Path)
|
|
if err != nil {
|
|
return nil, caos_errs.ThrowInternal(err, "AUTHZ-JK31F", "Errors.NotFound")
|
|
}
|
|
}
|
|
return crypto.BytesToPublicKey(s.KeyData)
|
|
}
|
|
|
|
func (s *systemJWTStorage) GetKeyByIDAndClientID(_ context.Context, _, userID string) (*jose.JSONWebKey, error) {
|
|
cachedKey, ok := s.cachedKeys[userID]
|
|
if ok {
|
|
return &jose.JSONWebKey{KeyID: userID, Key: cachedKey}, nil
|
|
}
|
|
key, ok := s.keys[userID]
|
|
if !ok {
|
|
return nil, caos_errs.ThrowNotFound(nil, "AUTHZ-asfd3", "Errors.User.NotFound")
|
|
}
|
|
defer s.mutex.Unlock()
|
|
s.mutex.Lock()
|
|
publicKey, err := key.readKey()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
s.cachedKeys[userID] = publicKey
|
|
return &jose.JSONWebKey{KeyID: userID, Key: publicKey}, nil
|
|
}
|
|
|
|
type client struct {
|
|
id string
|
|
projectID string
|
|
name string
|
|
}
|
|
|
|
func (v *TokenVerifier) RegisterServer(appName, methodPrefix string, mappings MethodMapping) {
|
|
v.clients.Store(methodPrefix, &client{name: appName})
|
|
if v.authMethods == nil {
|
|
v.authMethods = make(map[string]Option)
|
|
}
|
|
for method, option := range mappings {
|
|
v.authMethods[method] = option
|
|
}
|
|
}
|
|
|
|
func (v *TokenVerifier) SearchMyMemberships(ctx context.Context, orgID string) (_ []*Membership, err error) {
|
|
ctx, span := tracing.NewSpan(ctx)
|
|
defer func() { span.EndWithError(err) }()
|
|
return v.authZRepo.SearchMyMemberships(ctx, orgID)
|
|
}
|
|
|
|
func (v *TokenVerifier) ProjectIDAndOriginsByClientID(ctx context.Context, clientID string) (_ string, _ []string, err error) {
|
|
ctx, span := tracing.NewSpan(ctx)
|
|
defer func() { span.EndWithError(err) }()
|
|
|
|
return v.authZRepo.ProjectIDAndOriginsByClientID(ctx, clientID)
|
|
}
|
|
|
|
func (v *TokenVerifier) ExistsOrg(ctx context.Context, id, domain string) (orgID string, err error) {
|
|
ctx, span := tracing.NewSpan(ctx)
|
|
defer func() { span.EndWithError(err) }()
|
|
return v.authZRepo.ExistsOrg(ctx, id, domain)
|
|
}
|
|
|
|
func (v *TokenVerifier) CheckAuthMethod(method string) (Option, bool) {
|
|
authOpt, ok := v.authMethods[method]
|
|
return authOpt, ok
|
|
}
|
|
|
|
func verifyAccessToken(ctx context.Context, token string, t *TokenVerifier, method string) (userID, clientID, agentID, prefLan, resourceOwner string, err error) {
|
|
ctx, span := tracing.NewSpan(ctx)
|
|
defer func() { span.EndWithError(err) }()
|
|
|
|
parts := strings.Split(token, BearerPrefix)
|
|
if len(parts) != 2 {
|
|
return "", "", "", "", "", caos_errs.ThrowUnauthenticated(nil, "AUTH-7fs1e", "invalid auth header")
|
|
}
|
|
return t.VerifyAccessToken(ctx, parts[1], method)
|
|
}
|
|
|
|
func SessionTokenVerifier(algorithm crypto.EncryptionAlgorithm) func(ctx context.Context, sessionToken, sessionID, tokenID string) (err error) {
|
|
return func(ctx context.Context, sessionToken, sessionID, tokenID string) (err error) {
|
|
decodedToken, err := base64.RawURLEncoding.DecodeString(sessionToken)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, spanPasswordComparison := tracing.NewNamedSpan(ctx, "crypto.CompareHash")
|
|
var token string
|
|
token, err = algorithm.DecryptString(decodedToken, algorithm.EncryptionKeyID())
|
|
spanPasswordComparison.EndWithError(err)
|
|
if err != nil || token != fmt.Sprintf(SessionTokenFormat, sessionID, tokenID) {
|
|
return caos_errs.ThrowPermissionDenied(err, "COMMAND-sGr42", "Errors.Session.Token.Invalid")
|
|
}
|
|
return nil
|
|
}
|
|
}
|