mirror of
https://github.com/openziti/ziti.git
synced 2026-09-10 08:45:41 +00:00
3e5abf1696
- asserts the current api session certificate list excludes another identity's certificates - asserts scoping is per api session, so a second api session of the same identity is excluded - adds CreateCurrentApiSessionCertificate and ListCurrentApiSessionCertificates client helpers
901 lines
28 KiB
Go
901 lines
28 KiB
Go
package tests
|
|
|
|
import (
|
|
"crypto"
|
|
"crypto/ecdsa"
|
|
"crypto/elliptic"
|
|
"crypto/rand"
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"crypto/x509/pkix"
|
|
"encoding/pem"
|
|
"errors"
|
|
"fmt"
|
|
"math/big"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/dgryski/dgoogauth"
|
|
"github.com/go-openapi/runtime"
|
|
"github.com/go-openapi/strfmt"
|
|
"github.com/golang-jwt/jwt/v5"
|
|
"github.com/google/uuid"
|
|
"github.com/openziti/edge-api/rest_client_api_client"
|
|
clientAuthentication "github.com/openziti/edge-api/rest_client_api_client/authentication"
|
|
clientCurrentApiSession "github.com/openziti/edge-api/rest_client_api_client/current_api_session"
|
|
clientCurrentIdentity "github.com/openziti/edge-api/rest_client_api_client/current_identity"
|
|
clientEnroll "github.com/openziti/edge-api/rest_client_api_client/enroll"
|
|
"github.com/openziti/edge-api/rest_model"
|
|
"github.com/openziti/edge-api/rest_util"
|
|
nfPem "github.com/openziti/foundation/v2/pem"
|
|
"github.com/openziti/identity/certtools"
|
|
edgeApis "github.com/openziti/sdk-golang/v2/edge-apis"
|
|
"github.com/openziti/sdk-golang/v2/ziti"
|
|
"github.com/openziti/ziti/v2/common"
|
|
"github.com/zitadel/oidc/v3/pkg/oidc"
|
|
)
|
|
|
|
type ClientHelperClient struct {
|
|
*edgeApis.ClientApiClient
|
|
testCtx *TestContext
|
|
}
|
|
|
|
func (helper *ClientHelperClient) GetVersion() (*rest_model.Version, error) {
|
|
resp, err := helper.API.Informational.ListVersion(nil)
|
|
if err != nil {
|
|
return nil, rest_util.WrapErr(err)
|
|
}
|
|
return resp.Payload.Data, nil
|
|
}
|
|
|
|
// ListCurrentIdentityEdgeRouters returns the edge routers the authenticated identity can access,
|
|
// via the client current-identity/edge-routers endpoint.
|
|
func (helper *ClientHelperClient) ListCurrentIdentityEdgeRouters() ([]*rest_model.CurrentIdentityEdgeRouterDetail, error) {
|
|
resp, err := helper.API.CurrentIdentity.GetCurrentIdentityEdgeRouters(&clientCurrentIdentity.GetCurrentIdentityEdgeRoutersParams{}, nil)
|
|
if err != nil {
|
|
return nil, rest_util.WrapErr(err)
|
|
}
|
|
return resp.Payload.Data, nil
|
|
}
|
|
|
|
func (helper *ClientHelperClient) CompleteJwtTokenEnrollmentToCertAuth(enrollmentJwt string) (*edgeApis.CertCredentials, error) {
|
|
privateKey, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
template := &x509.CertificateRequest{}
|
|
|
|
newCsr, err := x509.CreateCertificateRequest(rand.Reader, template, privateKey)
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
newCsrPem := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE REQUEST", Bytes: newCsr})
|
|
|
|
params := clientEnroll.NewEnrollTokenParams()
|
|
|
|
params.TokenEnrollmentRequest = &rest_model.TokenEnrollmentRequest{
|
|
ClientCsr: string(newCsrPem),
|
|
}
|
|
params.Authorization = "Bearer " + enrollmentJwt
|
|
|
|
resp, err := helper.API.Enroll.EnrollToken(params)
|
|
|
|
if err != nil {
|
|
return nil, rest_util.WrapErr(err)
|
|
}
|
|
|
|
certs := nfPem.PemStringToCertificates(resp.Payload.Data.Cert)
|
|
|
|
if len(certs) == 0 {
|
|
return nil, fmt.Errorf("no certificates returned from enrollment")
|
|
}
|
|
|
|
creds := edgeApis.NewCertCredentials(certs, privateKey)
|
|
|
|
return creds, nil
|
|
}
|
|
|
|
func (helper *ClientHelperClient) CompleteJwtTokenEnrollmentToTokenAuth(enrollmentJwt string) error {
|
|
_, err := helper.CompleteJwtTokenEnrollmentToTokenAuthWithControllers(enrollmentJwt)
|
|
return err
|
|
}
|
|
|
|
// CompleteJwtTokenEnrollmentToTokenAuthWithControllers performs a token-to-token enrollment and returns
|
|
// the list of controllers carried in the enrollment response.
|
|
func (helper *ClientHelperClient) CompleteJwtTokenEnrollmentToTokenAuthWithControllers(enrollmentJwt string) (rest_model.ControllersList, error) {
|
|
params := clientEnroll.NewEnrollTokenParams()
|
|
|
|
params.TokenEnrollmentRequest = &rest_model.TokenEnrollmentRequest{}
|
|
params.Authorization = "Bearer " + enrollmentJwt
|
|
|
|
resp, err := helper.API.Enroll.EnrollToken(params)
|
|
|
|
if err != nil {
|
|
return nil, rest_util.WrapErr(err)
|
|
}
|
|
|
|
return resp.Payload.Data.Controllers, nil
|
|
}
|
|
|
|
func (helper *ClientHelperClient) CompleteOttEnrollment(enrollmentToken string) (*edgeApis.CertCredentials, error) {
|
|
creds, _, err := helper.CompleteOttEnrollmentWithControllers(enrollmentToken)
|
|
return creds, err
|
|
}
|
|
|
|
// CompleteOttEnrollmentWithControllers performs an OTT enrollment and additionally returns the list of
|
|
// controllers carried in the enrollment response.
|
|
func (helper *ClientHelperClient) CompleteOttEnrollmentWithControllers(enrollmentToken string) (*edgeApis.CertCredentials, rest_model.ControllersList, error) {
|
|
token := enrollmentToken
|
|
|
|
if IsJwt(token) {
|
|
jwtParser := jwt.NewParser()
|
|
enrollmentClaims := &ziti.EnrollmentClaims{}
|
|
_, _, err := jwtParser.ParseUnverified(token, enrollmentClaims)
|
|
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("could not parse enrollment JWT: %w", rest_util.WrapErr(err))
|
|
}
|
|
|
|
token = enrollmentClaims.ID
|
|
}
|
|
|
|
privateKey, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
|
|
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
template := &x509.CertificateRequest{}
|
|
|
|
csr, err := x509.CreateCertificateRequest(rand.Reader, template, privateKey)
|
|
|
|
if err != nil {
|
|
return nil, nil, rest_util.WrapErr(err)
|
|
}
|
|
|
|
csrPem := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE REQUEST", Bytes: csr})
|
|
|
|
params := clientEnroll.NewEnrollOttParams()
|
|
params.OttEnrollmentRequest = &rest_model.OttEnrollmentRequest{
|
|
ClientCsr: string(csrPem),
|
|
Token: token,
|
|
}
|
|
|
|
resp, err := helper.API.Enroll.EnrollOtt(params)
|
|
|
|
if err != nil {
|
|
return nil, nil, rest_util.WrapErr(err)
|
|
}
|
|
|
|
certs := nfPem.PemStringToCertificates(resp.Payload.Data.Cert)
|
|
|
|
if len(certs) == 0 {
|
|
return nil, nil, fmt.Errorf("no certificates returned from enrollment")
|
|
}
|
|
|
|
caCerts := nfPem.PemStringToCertificates(resp.Payload.Data.Ca)
|
|
|
|
var caPool *x509.CertPool
|
|
|
|
if len(caCerts) == 0 {
|
|
caPool = helper.testCtx.ControllerCaPool()
|
|
} else {
|
|
caPool = x509.NewCertPool()
|
|
for _, caCert := range caCerts {
|
|
caPool.AddCert(caCert)
|
|
}
|
|
}
|
|
|
|
return &edgeApis.CertCredentials{
|
|
BaseCredentials: edgeApis.BaseCredentials{
|
|
CaPool: caPool,
|
|
},
|
|
Certs: certs,
|
|
Key: privateKey,
|
|
}, resp.Payload.Data.Controllers, nil
|
|
}
|
|
|
|
func (helper *ClientHelperClient) CompleteOttGenericEnrollment(enrollmentToken string) (*edgeApis.CertCredentials, error) {
|
|
token := enrollmentToken
|
|
|
|
if IsJwt(token) {
|
|
jwtParser := jwt.NewParser()
|
|
enrollmentClaims := &ziti.EnrollmentClaims{}
|
|
_, _, err := jwtParser.ParseUnverified(token, enrollmentClaims)
|
|
|
|
if err != nil {
|
|
return nil, fmt.Errorf("could not parse enrollment JWT: %w", rest_util.WrapErr(err))
|
|
}
|
|
|
|
token = enrollmentClaims.ID
|
|
}
|
|
|
|
privateKey, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
template := &x509.CertificateRequest{}
|
|
|
|
csr, err := x509.CreateCertificateRequest(rand.Reader, template, privateKey)
|
|
|
|
if err != nil {
|
|
return nil, rest_util.WrapErr(err)
|
|
}
|
|
|
|
csrPem := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE REQUEST", Bytes: csr})
|
|
|
|
params := clientEnroll.NewEnrollParams()
|
|
params.Method = ToPtr(rest_model.EnrollmentCreateMethodOtt)
|
|
params.Token = ToPtr(strfmt.UUID(token))
|
|
params.Body = &rest_model.GenericEnroll{
|
|
ClientCsr: string(csrPem),
|
|
}
|
|
|
|
resp, err := helper.API.Enroll.Enroll(params, func(operation *runtime.ClientOperation) {
|
|
operation.ConsumesMediaTypes = []string{"text/plain"}
|
|
})
|
|
|
|
if err != nil {
|
|
return nil, rest_util.WrapErr(err)
|
|
}
|
|
|
|
certs := nfPem.PemStringToCertificates(resp.Payload.Data.Cert)
|
|
|
|
if len(certs) == 0 {
|
|
return nil, fmt.Errorf("no certificates returned from enrollment")
|
|
}
|
|
|
|
return &edgeApis.CertCredentials{
|
|
BaseCredentials: edgeApis.BaseCredentials{},
|
|
Certs: certs,
|
|
Key: privateKey,
|
|
}, nil
|
|
}
|
|
|
|
func (helper *ClientHelperClient) CompleteOttCaEnrollment(enrollmentToken string, clientCert []*x509.Certificate, clientKey crypto.PrivateKey) (*edgeApis.CertCredentials, error) {
|
|
creds, _, err := helper.CompleteOttCaEnrollmentWithControllers(enrollmentToken, clientCert, clientKey)
|
|
return creds, err
|
|
}
|
|
|
|
// CompleteOttCaEnrollmentWithControllers performs an OTTCA enrollment and additionally returns the list
|
|
// of controllers carried in the enrollment response.
|
|
func (helper *ClientHelperClient) CompleteOttCaEnrollmentWithControllers(enrollmentToken string, clientCert []*x509.Certificate, clientKey crypto.PrivateKey) (*edgeApis.CertCredentials, rest_model.ControllersList, error) {
|
|
token := enrollmentToken
|
|
|
|
if len(clientCert) == 0 {
|
|
return nil, nil, fmt.Errorf("no client certificates returned from enrollment")
|
|
}
|
|
|
|
var rawCerts [][]byte
|
|
|
|
for _, cert := range clientCert {
|
|
rawCerts = append(rawCerts, cert.Raw)
|
|
}
|
|
|
|
tlsCerts := []tls.Certificate{
|
|
{
|
|
Certificate: rawCerts,
|
|
PrivateKey: clientKey,
|
|
Leaf: clientCert[0],
|
|
},
|
|
}
|
|
|
|
// Replace the TLS config with a new one that includes the client certs.
|
|
// We must also close idle connections and replace the transport on the
|
|
// HttpClient to ensure no previously established TLS connections (which
|
|
// lack the client cert) are reused. This avoids a race with the background
|
|
// doOnceCacheVersionInfo goroutine which may have opened a connection
|
|
// before the client cert was configured.
|
|
oldTlsConfig := helper.TlsAwareTransport.GetTlsClientConfig()
|
|
newTlsConfig := oldTlsConfig.Clone()
|
|
newTlsConfig.Certificates = tlsCerts
|
|
helper.TlsAwareTransport.CloseIdleConnections()
|
|
newTransport := edgeApis.NewTlsAwareHttpTransport(nil)
|
|
newTransport.SetTlsClientConfig(newTlsConfig)
|
|
helper.TlsAwareTransport = newTransport
|
|
helper.HttpClient.Transport = newTransport
|
|
|
|
if IsJwt(token) {
|
|
jwtParser := jwt.NewParser()
|
|
enrollmentClaims := &ziti.EnrollmentClaims{}
|
|
_, _, err := jwtParser.ParseUnverified(token, enrollmentClaims)
|
|
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("could not parse enrollment JWT: %w", rest_util.WrapErr(err))
|
|
}
|
|
|
|
token = enrollmentClaims.ID
|
|
}
|
|
|
|
params := clientEnroll.NewEnrollOttCaParams()
|
|
params.OttEnrollmentRequest = &rest_model.OttEnrollmentRequest{
|
|
Token: token,
|
|
}
|
|
|
|
certCreds := &edgeApis.CertCredentials{
|
|
BaseCredentials: edgeApis.BaseCredentials{},
|
|
Certs: clientCert,
|
|
Key: clientKey,
|
|
}
|
|
|
|
helper.Credentials = certCreds
|
|
|
|
resp, err := helper.API.Enroll.EnrollOttCa(params)
|
|
|
|
if err != nil {
|
|
return nil, nil, rest_util.WrapErr(err)
|
|
}
|
|
|
|
return certCreds, resp.Payload.Data.Controllers, nil
|
|
}
|
|
|
|
func (helper *ClientHelperClient) CompleteUpdbEnrollment(enrollmentToken string, username string, password string) (*edgeApis.UpdbCredentials, error) {
|
|
creds, _, err := helper.CompleteUpdbEnrollmentWithResponse(enrollmentToken, username, password)
|
|
return creds, err
|
|
}
|
|
|
|
// CompleteUpdbEnrollmentWithResponse performs a UPDB enrollment and additionally returns the enrollment
|
|
// response, which carries the established username and the list of controllers.
|
|
func (helper *ClientHelperClient) CompleteUpdbEnrollmentWithResponse(enrollmentToken string, username string, password string) (*edgeApis.UpdbCredentials, *rest_model.EnrollmentResponseUpdb, error) {
|
|
token := enrollmentToken
|
|
|
|
if IsJwt(token) {
|
|
jwtParser := jwt.NewParser()
|
|
enrollmentClaims := &ziti.EnrollmentClaims{}
|
|
_, _, err := jwtParser.ParseUnverified(token, enrollmentClaims)
|
|
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("could not parse enrollment JWT: %w", rest_util.WrapErr(err))
|
|
}
|
|
|
|
token = enrollmentClaims.ID
|
|
}
|
|
|
|
params := clientEnroll.NewEnrollUpdbParams()
|
|
params.Token = strfmt.UUID(token)
|
|
params.UpdbCredentials = clientEnroll.EnrollUpdbBody{
|
|
Password: rest_model.Password(password),
|
|
Username: rest_model.Username(username),
|
|
}
|
|
|
|
resp, err := helper.API.Enroll.EnrollUpdb(params)
|
|
|
|
if err != nil {
|
|
return nil, nil, rest_util.WrapErr(err)
|
|
}
|
|
|
|
return &edgeApis.UpdbCredentials{
|
|
BaseCredentials: edgeApis.BaseCredentials{},
|
|
Username: username,
|
|
Password: password,
|
|
}, resp.Payload.Data, nil
|
|
}
|
|
|
|
func (helper *ClientHelperClient) ExtendCertsWithAuthenticatorId(authenticatorId string) (*edgeApis.CertCredentials, error) {
|
|
request, err := certtools.NewCertRequest(map[string]string{
|
|
"C": "US", "O": "NetFoundry-API-Test", "CN": uuid.NewString(),
|
|
}, nil)
|
|
|
|
if err != nil {
|
|
return nil, fmt.Errorf("could not create base CSR values: %w", err)
|
|
}
|
|
|
|
newPrivateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("could not generate private key: %w", err)
|
|
}
|
|
|
|
csr, err := x509.CreateCertificateRequest(rand.Reader, request, newPrivateKey)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("could not create CSR: %w", err)
|
|
}
|
|
|
|
csrPem := string(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE REQUEST", Bytes: csr}))
|
|
|
|
extendParams := &clientCurrentApiSession.ExtendCurrentIdentityAuthenticatorParams{
|
|
Extend: &rest_model.IdentityExtendEnrollmentRequest{
|
|
ClientCertCsr: &csrPem,
|
|
},
|
|
ID: authenticatorId,
|
|
}
|
|
|
|
extendResp, err := helper.API.CurrentAPISession.ExtendCurrentIdentityAuthenticator(extendParams, nil)
|
|
|
|
if err != nil {
|
|
return nil, fmt.Errorf("could not extend current identity authenticator: %w", err)
|
|
}
|
|
|
|
caCerts := nfPem.PemStringToCertificates(extendResp.Payload.Data.Ca)
|
|
caPool := x509.NewCertPool()
|
|
|
|
for _, c := range caCerts {
|
|
caPool.AddCert(c)
|
|
}
|
|
|
|
newCerts := nfPem.PemStringToCertificates(extendResp.Payload.Data.ClientCert)
|
|
|
|
newCreds := &edgeApis.CertCredentials{
|
|
BaseCredentials: edgeApis.BaseCredentials{
|
|
CaPool: caPool,
|
|
},
|
|
Certs: newCerts,
|
|
Key: newPrivateKey,
|
|
}
|
|
|
|
verifyParams := &clientCurrentApiSession.ExtendVerifyCurrentIdentityAuthenticatorParams{
|
|
Extend: &rest_model.IdentityExtendValidateEnrollmentRequest{
|
|
ClientCert: &extendResp.Payload.Data.ClientCert,
|
|
},
|
|
ID: authenticatorId,
|
|
}
|
|
|
|
_, err = helper.API.CurrentAPISession.ExtendVerifyCurrentIdentityAuthenticator(verifyParams, nil)
|
|
|
|
if err != nil {
|
|
return nil, fmt.Errorf("could not verify the extension of the authenticator: %w", rest_util.WrapErr(err))
|
|
}
|
|
|
|
return newCreds, nil
|
|
|
|
}
|
|
|
|
func (helper *ClientHelperClient) GetCurrentApiSessionDetail() (*rest_model.CurrentAPISessionDetail, error) {
|
|
params := &clientCurrentApiSession.GetCurrentAPISessionParams{}
|
|
|
|
resp, err := helper.API.CurrentAPISession.GetCurrentAPISession(params, nil)
|
|
|
|
if err != nil {
|
|
return nil, fmt.Errorf("could not get current api session detail: %w", rest_util.WrapErr(err))
|
|
}
|
|
|
|
return resp.Payload.Data, nil
|
|
}
|
|
|
|
// CreateCurrentApiSessionCertificate generates a new key and CSR, then submits it to
|
|
// create an API session certificate for the currently authenticated API Session.
|
|
func (helper *ClientHelperClient) CreateCurrentApiSessionCertificate() (*rest_model.CurrentAPISessionCertificateCreateResponse, error) {
|
|
request, err := certtools.NewCertRequest(map[string]string{
|
|
"C": "US", "O": "NetFoundry-API-Test", "CN": uuid.NewString(),
|
|
}, nil)
|
|
|
|
if err != nil {
|
|
return nil, fmt.Errorf("could not create base CSR values: %w", err)
|
|
}
|
|
|
|
privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("could not generate private key: %w", err)
|
|
}
|
|
|
|
csr, err := x509.CreateCertificateRequest(rand.Reader, request, privateKey)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("could not create CSR: %w", err)
|
|
}
|
|
|
|
csrPem := string(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE REQUEST", Bytes: csr}))
|
|
|
|
params := &clientCurrentApiSession.CreateCurrentAPISessionCertificateParams{
|
|
SessionCertificate: &rest_model.CurrentAPISessionCertificateCreate{
|
|
Csr: &csrPem,
|
|
},
|
|
}
|
|
|
|
resp, err := helper.API.CurrentAPISession.CreateCurrentAPISessionCertificate(params, nil)
|
|
|
|
if err != nil {
|
|
return nil, fmt.Errorf("could not create current api session certificate: %w", rest_util.WrapErr(err))
|
|
}
|
|
|
|
return resp.Payload.Data, nil
|
|
}
|
|
|
|
// ListCurrentApiSessionCertificates returns the API session certificates visible to the
|
|
// currently authenticated API Session.
|
|
func (helper *ClientHelperClient) ListCurrentApiSessionCertificates() (rest_model.CurrentAPISessionCertificateList, error) {
|
|
params := &clientCurrentApiSession.ListCurrentAPISessionCertificatesParams{}
|
|
|
|
resp, err := helper.API.CurrentAPISession.ListCurrentAPISessionCertificates(params, nil)
|
|
|
|
if err != nil {
|
|
return nil, fmt.Errorf("could not list current api session certificates: %w", rest_util.WrapErr(err))
|
|
}
|
|
|
|
return resp.Payload.Data, nil
|
|
}
|
|
|
|
func (helper *ClientHelperClient) GetTotpMfa() (*rest_model.DetailMfa, error) {
|
|
params := &clientCurrentIdentity.DetailMfaParams{}
|
|
|
|
resp, err := helper.API.CurrentIdentity.DetailMfa(params, nil)
|
|
|
|
if err != nil {
|
|
return nil, fmt.Errorf("could not get totp mfa: %w", err)
|
|
}
|
|
|
|
return resp.Payload.Data, nil
|
|
}
|
|
|
|
func (helper *ClientHelperClient) CreateTotpMfaEnrollment() (*rest_model.DetailMfa, error) {
|
|
params := &clientCurrentIdentity.EnrollMfaParams{}
|
|
|
|
_, err := helper.API.CurrentIdentity.EnrollMfa(params, nil)
|
|
|
|
if err != nil {
|
|
return nil, fmt.Errorf("could not create totp mfa enrollment: %w", err)
|
|
}
|
|
|
|
return helper.GetTotpMfa()
|
|
}
|
|
|
|
func (helper *ClientHelperClient) VerifyTotpMfaEnrollment(code string) (*rest_model.DetailMfa, error) {
|
|
params := &clientCurrentIdentity.VerifyMfaParams{
|
|
MfaValidation: &rest_model.MfaCode{
|
|
Code: ToPtr(code),
|
|
},
|
|
Context: nil,
|
|
HTTPClient: nil,
|
|
}
|
|
|
|
_, err := helper.API.CurrentIdentity.VerifyMfa(params, nil)
|
|
|
|
if err != nil {
|
|
return nil, fmt.Errorf("could not verify totp mfa enrollment: %w", err)
|
|
}
|
|
|
|
return helper.GetTotpMfa()
|
|
}
|
|
|
|
func (helper *ClientHelperClient) EnrollTotpMfa() (*TotpProvider, *rest_model.DetailMfa, error) {
|
|
totpMfaEnrollment, err := helper.CreateTotpMfaEnrollment()
|
|
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("could not create totp mfa enrollment: %w", err)
|
|
}
|
|
|
|
totpProvider := &TotpProvider{}
|
|
err = totpProvider.ApplyProvisioningUrl(totpMfaEnrollment.ProvisioningURL)
|
|
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("could not apply provisioning url: %w", err)
|
|
}
|
|
|
|
curCode := totpProvider.Code()
|
|
|
|
totpMfaEnrollment, err = helper.VerifyTotpMfaEnrollment(curCode)
|
|
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("could not verify totp mfa enrollment: %w", err)
|
|
}
|
|
|
|
return totpProvider, totpMfaEnrollment, err
|
|
}
|
|
|
|
func (helper *ClientHelperClient) GetTotpToken(code string) (*rest_model.TotpToken, error) {
|
|
params := clientCurrentApiSession.CreateTotpTokenParams{
|
|
MfaValidation: &rest_model.MfaCode{
|
|
Code: ToPtr(code),
|
|
},
|
|
}
|
|
|
|
resp, err := helper.API.CurrentAPISession.CreateTotpToken(¶ms, nil)
|
|
|
|
if err != nil {
|
|
return nil, fmt.Errorf("could not get totp token: %w", err)
|
|
}
|
|
|
|
return resp.Payload.Data, nil
|
|
}
|
|
|
|
func (helper *ClientHelperClient) QueryCurrentApiSession() (*rest_model.CurrentAPISessionDetail, error) {
|
|
params := &clientCurrentApiSession.GetCurrentAPISessionParams{}
|
|
resp, err := helper.API.CurrentAPISession.GetCurrentAPISession(params, nil)
|
|
|
|
if err != nil {
|
|
return nil, rest_util.WrapErr(err)
|
|
}
|
|
|
|
return resp.Payload.Data, nil
|
|
}
|
|
|
|
// RawOidcAuthRequest performs a raw OIDC PKCE authentication flow with no TOTP providers,
|
|
// returning the resulting tokens, all intermediate HTTP responses, and any error.
|
|
func (helper *ClientHelperClient) RawOidcAuthRequest(credentials edgeApis.Credentials) (*oidc.Tokens[*oidc.IDTokenClaims], *edgeApis.OidcAuthResponses, error) {
|
|
return helper.RawOidcAuthRequestWithProviders(credentials, nil, nil)
|
|
}
|
|
|
|
// RawOidcAuthRequestWithProviders performs a raw OIDC PKCE authentication flow with optional TOTP
|
|
// enrollment and code providers, returning the resulting tokens, all intermediate HTTP responses,
|
|
// and any error. Nil providers are ignored; supply only those needed for the scenario under test.
|
|
func (helper *ClientHelperClient) RawOidcAuthRequestWithProviders(credentials edgeApis.Credentials, enrollProvider edgeApis.TotpEnrollmentProvider, codeProvider edgeApis.TotpCodeProvider) (*oidc.Tokens[*oidc.IDTokenClaims], *edgeApis.OidcAuthResponses, error) {
|
|
httpTransport := &http.Transport{
|
|
TLSClientConfig: &tls.Config{
|
|
RootCAs: credentials.GetCaPool(),
|
|
Certificates: credentials.TlsCerts(),
|
|
},
|
|
Proxy: http.ProxyFromEnvironment,
|
|
ForceAttemptHTTP2: true,
|
|
MaxIdleConns: 100,
|
|
IdleConnTimeout: 90 * time.Second,
|
|
TLSHandshakeTimeout: 10 * time.Second,
|
|
ExpectContinueTimeout: 1 * time.Second,
|
|
}
|
|
|
|
httpClient := &http.Client{
|
|
Transport: httpTransport,
|
|
Timeout: time.Second * 10,
|
|
}
|
|
|
|
clientRuntime := edgeApis.NewRuntime(helper.testCtx.ClientApiUrl(), rest_client_api_client.DefaultSchemes, httpClient)
|
|
|
|
apiClientTransport := &edgeApis.ApiClientTransport{
|
|
ClientTransport: clientRuntime,
|
|
ApiUrl: helper.testCtx.ClientApiUrl(),
|
|
}
|
|
|
|
config := &edgeApis.EdgeOidcAuthConfig{
|
|
ClientTransportPool: &SingularClientTransportPool{ApiClientTransport: apiClientTransport},
|
|
Credentials: credentials,
|
|
HttpClient: httpClient,
|
|
TotpEnrollmentProvider: enrollProvider,
|
|
TotpCodeProvider: codeProvider,
|
|
RedirectUri: edgeApis.DefaultOidcRedirectUri,
|
|
ApiHost: helper.testCtx.ApiHost,
|
|
}
|
|
|
|
return edgeApis.NewEdgeOidcAuthenticator(config).AuthenticateWithResponses()
|
|
}
|
|
|
|
// OidcAuthorize starts an OIDC PKCE authorization flow and returns an OidcAuthorizeResult
|
|
// for use when tests need to make raw HTTP calls to the OP login endpoints. The Exchange
|
|
// function in the result completes the flow by trading an authorization code for OIDC tokens.
|
|
func (helper *ClientHelperClient) OidcAuthorize(credentials edgeApis.Credentials) (*edgeApis.OidcAuthorizeResult, error) {
|
|
httpTransport := &http.Transport{
|
|
TLSClientConfig: &tls.Config{
|
|
RootCAs: credentials.GetCaPool(),
|
|
Certificates: credentials.TlsCerts(),
|
|
},
|
|
Proxy: http.ProxyFromEnvironment,
|
|
ForceAttemptHTTP2: true,
|
|
MaxIdleConns: 100,
|
|
IdleConnTimeout: 90 * time.Second,
|
|
TLSHandshakeTimeout: 10 * time.Second,
|
|
ExpectContinueTimeout: 1 * time.Second,
|
|
}
|
|
|
|
httpClient := &http.Client{
|
|
Transport: httpTransport,
|
|
Timeout: time.Second * 10,
|
|
}
|
|
|
|
clientRuntime := edgeApis.NewRuntime(helper.testCtx.ClientApiUrl(), rest_client_api_client.DefaultSchemes, httpClient)
|
|
|
|
apiClientTransport := &edgeApis.ApiClientTransport{
|
|
ClientTransport: clientRuntime,
|
|
ApiUrl: helper.testCtx.ClientApiUrl(),
|
|
}
|
|
|
|
config := &edgeApis.EdgeOidcAuthConfig{
|
|
ClientTransportPool: &SingularClientTransportPool{ApiClientTransport: apiClientTransport},
|
|
Credentials: credentials,
|
|
HttpClient: httpClient,
|
|
RedirectUri: edgeApis.DefaultOidcRedirectUri,
|
|
ApiHost: helper.testCtx.ApiHost,
|
|
}
|
|
|
|
return edgeApis.NewEdgeOidcAuthenticator(config).Authorize()
|
|
}
|
|
|
|
func (helper *ClientHelperClient) RawLegacyAuthRequest(credentials edgeApis.Credentials) (*clientAuthentication.AuthenticateOK, error) {
|
|
params := clientAuthentication.NewAuthenticateParams()
|
|
params.Method = string(credentials.Method())
|
|
|
|
httpTransport := &http.Transport{
|
|
TLSClientConfig: &tls.Config{
|
|
RootCAs: credentials.GetCaPool(),
|
|
Certificates: credentials.TlsCerts(),
|
|
},
|
|
Proxy: http.ProxyFromEnvironment,
|
|
ForceAttemptHTTP2: true,
|
|
MaxIdleConns: 100,
|
|
IdleConnTimeout: 90 * time.Second,
|
|
TLSHandshakeTimeout: 10 * time.Second,
|
|
ExpectContinueTimeout: 1 * time.Second,
|
|
}
|
|
|
|
httpClient := &http.Client{
|
|
Transport: httpTransport,
|
|
CheckRedirect: nil,
|
|
Timeout: time.Second * 10,
|
|
}
|
|
|
|
params.HTTPClient = httpClient
|
|
|
|
return helper.API.Authentication.Authenticate(params, func(operation *runtime.ClientOperation) {
|
|
operation.AuthInfo = credentials
|
|
})
|
|
}
|
|
|
|
func generateCaSignedClientCert(caCert *x509.Certificate, caSigner crypto.Signer, commonName string) (*x509.Certificate, crypto.Signer, error) {
|
|
id, _ := rand.Int(rand.Reader, big.NewInt(100000000000000000))
|
|
verificationCert := &x509.Certificate{
|
|
SerialNumber: id,
|
|
Subject: pkix.Name{
|
|
CommonName: commonName,
|
|
Organization: []string{"Ziti CLI Generated API Test Cert"},
|
|
},
|
|
NotBefore: time.Now(),
|
|
NotAfter: time.Now().Add(time.Minute * 10),
|
|
|
|
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
|
|
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
|
|
BasicConstraintsValid: true,
|
|
}
|
|
|
|
verificationKey, err := ecdsa.GenerateKey(elliptic.P521(), rand.Reader)
|
|
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("could not generate private key for certificate (%v)", err)
|
|
}
|
|
|
|
signedCertBytes, err := x509.CreateCertificate(rand.Reader, verificationCert, caCert, verificationKey.Public(), caSigner)
|
|
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("could not sign certificate with CA (%v)", err)
|
|
}
|
|
|
|
verificationCert, _ = x509.ParseCertificate(signedCertBytes)
|
|
|
|
return verificationCert, verificationKey, nil
|
|
}
|
|
|
|
func IsJwt(token string) bool {
|
|
if strings.HasPrefix(token, "eY") {
|
|
parts := strings.Split(token, ",")
|
|
return len(parts) == 3 && len(parts[0]) > 0 && len(parts[1]) > 0 && len(parts[2]) > 0
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
type TotpProvider struct {
|
|
Secret string
|
|
Issuer string
|
|
Name string
|
|
ProvisioningUrl string
|
|
}
|
|
|
|
func (t *TotpProvider) FuncProvider() func(chan string) {
|
|
return func(ch chan string) {
|
|
ch <- t.Code()
|
|
}
|
|
}
|
|
|
|
func (t *TotpProvider) ApplyProvisioningUrl(provisioningUrl string) error {
|
|
parsedUrl, err := url.Parse(provisioningUrl)
|
|
|
|
if err != nil {
|
|
return fmt.Errorf("could not parse provisioning url: %w", err)
|
|
}
|
|
|
|
if parsedUrl.Scheme != "otpauth" {
|
|
return fmt.Errorf("provisioning url must be an otpauth url")
|
|
}
|
|
|
|
queryParams, err := url.ParseQuery(parsedUrl.RawQuery)
|
|
if err != nil {
|
|
return fmt.Errorf("could not parse query params from provisioning url: %w", err)
|
|
}
|
|
|
|
secrets, ok := queryParams["secret"]
|
|
|
|
if !ok {
|
|
return errors.New("could not find secret in provisioning url")
|
|
}
|
|
|
|
if len(secrets) != 1 {
|
|
return fmt.Errorf("expected 1 secret in provisioning url, got %d", len(secrets))
|
|
}
|
|
|
|
t.Secret = secrets[0]
|
|
t.ProvisioningUrl = provisioningUrl
|
|
|
|
return nil
|
|
}
|
|
|
|
func (t *TotpProvider) Code() string {
|
|
if t.ProvisioningUrl == "" {
|
|
panic(errors.New("no provisioning url set"))
|
|
}
|
|
|
|
if t.Secret == "" {
|
|
panic(errors.New("secret not set"))
|
|
}
|
|
|
|
now := time.Now().UTC().Unix() / 30
|
|
code := dgoogauth.ComputeCode(t.Secret, now)
|
|
|
|
//pad leading 0s to 6 characters
|
|
return fmt.Sprintf("%06d", code)
|
|
}
|
|
|
|
// SingularClientTransportPool is a transport pool that only allows a single transport to be used, implements ClientTransportPool
|
|
type SingularClientTransportPool struct {
|
|
ApiClientTransport *edgeApis.ApiClientTransport
|
|
}
|
|
|
|
func (s *SingularClientTransportPool) Submit(operation *runtime.ClientOperation) (any, error) {
|
|
return s.ApiClientTransport.Submit(operation)
|
|
}
|
|
|
|
func (s *SingularClientTransportPool) Add(apiUrl *url.URL, transport runtime.ClientTransport) {
|
|
s.ApiClientTransport = &edgeApis.ApiClientTransport{
|
|
ClientTransport: transport,
|
|
ApiUrl: apiUrl,
|
|
}
|
|
}
|
|
|
|
func (s *SingularClientTransportPool) Remove(apiUrl *url.URL) {
|
|
//do nothing
|
|
}
|
|
|
|
func (s *SingularClientTransportPool) GetActiveTransport() *edgeApis.ApiClientTransport {
|
|
return s.ApiClientTransport
|
|
}
|
|
|
|
func (s *SingularClientTransportPool) SetActiveTransport(transport *edgeApis.ApiClientTransport) {
|
|
s.ApiClientTransport = transport
|
|
}
|
|
|
|
func (s *SingularClientTransportPool) GetApiUrls() []*url.URL {
|
|
return []*url.URL{s.ApiClientTransport.ApiUrl}
|
|
}
|
|
|
|
func (s *SingularClientTransportPool) IterateTransportsRandomly() chan<- *edgeApis.ApiClientTransport {
|
|
ch := make(chan *edgeApis.ApiClientTransport, 1)
|
|
|
|
ch <- s.ApiClientTransport
|
|
|
|
return ch
|
|
}
|
|
|
|
func (s *SingularClientTransportPool) TryTransportsForOp(operation *runtime.ClientOperation) (any, error) {
|
|
return s.ApiClientTransport.Submit(operation)
|
|
}
|
|
|
|
func (s *SingularClientTransportPool) TryTransportForF(cb func(*edgeApis.ApiClientTransport) (any, error)) (any, error) {
|
|
return cb(s.ApiClientTransport)
|
|
}
|
|
|
|
// OidcAccessToken authenticates via OIDC with the given credentials and returns
|
|
// the raw access token string plus its parsed claims. The caller must set CaPool
|
|
// on the credentials before calling if the controller uses a self-signed certificate.
|
|
func (helper *ClientHelperClient) OidcAccessToken(credentials edgeApis.Credentials) (string, *common.AccessClaims, error) {
|
|
tokens, _, err := helper.RawOidcAuthRequest(credentials)
|
|
if err != nil {
|
|
return "", nil, err
|
|
}
|
|
if tokens.AccessToken == "" {
|
|
return "", nil, errors.New("empty access token from OIDC auth")
|
|
}
|
|
return parseAccessToken(tokens.AccessToken)
|
|
}
|
|
|
|
// parseAccessToken parses an access token string into AccessClaims without
|
|
// verifying the signature.
|
|
func parseAccessToken(tokenStr string) (string, *common.AccessClaims, error) {
|
|
claims := &common.AccessClaims{}
|
|
parser := jwt.NewParser()
|
|
_, _, err := parser.ParseUnverified(tokenStr, claims)
|
|
if err != nil {
|
|
return "", nil, err
|
|
}
|
|
return tokenStr, claims, nil
|
|
}
|