diff --git a/pkg/client/auth.go b/pkg/client/auth.go index 5b0ef493..a0bcc109 100644 --- a/pkg/client/auth.go +++ b/pkg/client/auth.go @@ -96,3 +96,15 @@ func ScopeProjectID(projectID string) string { func ScopeZitadelAPI() string { return ScopeProjectID(scopeZITADELProjectID) } + +// PreSignedJWT allows using a pre-signed JWT token for authorization. +// This is useful when you already have a valid JWT token and don't want the client +// to generate and sign a new one. +func PreSignedJWT(token string) TokenSourceInitializer { + return func(ctx context.Context, _ string) (oauth2.TokenSource, error) { + return oauth2.StaticTokenSource(&oauth2.Token{ + AccessToken: token, + TokenType: oidc.BearerToken, + }), nil + } +} diff --git a/pkg/client/middleware/auth.go b/pkg/client/middleware/auth.go index f95aa86b..14a0a686 100644 --- a/pkg/client/middleware/auth.go +++ b/pkg/client/middleware/auth.go @@ -23,6 +23,7 @@ type AuthInterceptor struct { } type JWTProfileTokenSource func(issuer string, scopes []string) (oauth2.TokenSource, error) +type JWTDirectTokenSource func() (oauth2.TokenSource, error) func JWTProfileFromPath(ctx context.Context, keyPath string) JWTProfileTokenSource { return func(issuer string, scopes []string) (oauth2.TokenSource, error) { @@ -55,6 +56,16 @@ func NewAuthenticator(issuer string, jwtProfileTokenSource JWTProfileTokenSource }, nil } +func NewPresignedJWTAuthenticator(jwtDirectTokenSource JWTDirectTokenSource) (*AuthInterceptor, error) { + ts, err := jwtDirectTokenSource() + if err != nil { + return nil, err + } + return &AuthInterceptor{ + TokenSource: oauth2.ReuseTokenSource(nil, ts), + }, nil +} + func (interceptor *AuthInterceptor) Unary() grpc.UnaryClientInterceptor { return func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error { authCtx, err := interceptor.setToken(ctx) diff --git a/pkg/client/zitadel/client.go b/pkg/client/zitadel/client.go index ba713f45..b5eb51ce 100644 --- a/pkg/client/zitadel/client.go +++ b/pkg/client/zitadel/client.go @@ -5,6 +5,9 @@ import ( "crypto/x509" "strings" + "github.com/zitadel/oidc/v3/pkg/oidc" + "golang.org/x/oauth2" + "google.golang.org/grpc" "google.golang.org/grpc/credentials" @@ -15,6 +18,7 @@ type Connection struct { issuer string api string jwtProfileTokenSource middleware.JWTProfileTokenSource + jwtDirectTokenSource middleware.JWTDirectTokenSource scopes []string orgID string insecure bool @@ -66,7 +70,13 @@ func NewConnection(ctx context.Context, issuer, api string, scopes []string, opt } func (c *Connection) setInterceptors(issuer, orgID string, scopes []string, jwtProfileTokenSource middleware.JWTProfileTokenSource) error { - auth, err := middleware.NewAuthenticator(issuer, jwtProfileTokenSource, scopes...) + var auth *middleware.AuthInterceptor + var err error + if c.jwtDirectTokenSource != nil { + auth, err = middleware.NewPresignedJWTAuthenticator(c.jwtDirectTokenSource) + } else { + auth, err = middleware.NewAuthenticator(issuer, jwtProfileTokenSource, scopes...) + } if err != nil { return err } @@ -125,6 +135,19 @@ func WithJWTProfileTokenSource(provider middleware.JWTProfileTokenSource) func(* } } +// Use a pre-signed JWT for authentication +func WithJWTDirectTokenSource(jwt string) func(*Connection) error { + return func(client *Connection) error { + client.jwtDirectTokenSource = func() (oauth2.TokenSource, error) { + return oauth2.StaticTokenSource(&oauth2.Token{ + AccessToken: jwt, + TokenType: oidc.BearerToken, + }), nil + } + return nil + } +} + // WithOrgID sets the organization context (where the api calls are executed) // if not set the resource owner (organization) of the calling user will be used func WithOrgID(orgID string) func(*Connection) error {