Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

adds HA auth to the golang SDK #448

Merged
merged 10 commits into from
Jan 3, 2024
Merged
925 changes: 925 additions & 0 deletions .merge_file_FAcDjN

Large diffs are not rendered by default.

463 changes: 452 additions & 11 deletions edge-apis/authwrapper.go

Large diffs are not rendered by default.

39 changes: 24 additions & 15 deletions edge-apis/clients.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ import (
"github.com/go-openapi/strfmt"
"github.com/openziti/edge-api/rest_client_api_client"
"github.com/openziti/edge-api/rest_management_api_client"
"github.com/openziti/edge-api/rest_model"
"github.com/pkg/errors"
"net/url"
"sync/atomic"
Expand All @@ -40,26 +39,26 @@ type ApiType interface {
type BaseClient[A ApiType] struct {
API *A
Components
AuthInfoWriter runtime.ClientAuthInfoWriter
CurrentAPISessionDetail atomic.Pointer[rest_model.CurrentAPISessionDetail]
Credentials Credentials
AuthInfoWriter runtime.ClientAuthInfoWriter
ApiSession atomic.Pointer[ApiSession]
Credentials Credentials
}

// GetCurrentApiSession returns the ApiSession that is being used to authenticate requests.
func (self *BaseClient[A]) GetCurrentApiSession() *rest_model.CurrentAPISessionDetail {
return self.CurrentAPISessionDetail.Load()
func (self *BaseClient[A]) GetCurrentApiSession() *ApiSession {
return self.ApiSession.Load()
}

// Authenticate will attempt to use the provided credentials to authenticate via the underlying ApiType. On success
// the API Session details will be returned and the current client will make authenticated requests on future
// calls. On an error the API Session in use will be cleared and subsequent requests will become/continue to be
// made in an unauthenticated fashion.
func (self *BaseClient[A]) Authenticate(credentials Credentials, configTypes []string) (*rest_model.CurrentAPISessionDetail, error) {
func (self *BaseClient[A]) Authenticate(credentials Credentials, configTypes []string) (*ApiSession, error) {
//casting to `any` works around golang error that happens when type asserting a generic typed field
myAny := any(self.API)
if a, ok := myAny.(AuthEnabledApi); ok {
self.Credentials = nil
self.CurrentAPISessionDetail.Store(nil)
self.ApiSession.Store(nil)

if credCaPool := credentials.GetCaPool(); credCaPool != nil {
self.HttpTransport.TLSClientConfig.RootCAs = credCaPool
Expand All @@ -74,11 +73,12 @@ func (self *BaseClient[A]) Authenticate(credentials Credentials, configTypes []s
}

self.Credentials = credentials
self.CurrentAPISessionDetail.Store(apiSession)
self.ApiSession.Store(apiSession)

self.Runtime.DefaultAuthentication = runtime.ClientAuthInfoWriterFunc(func(request runtime.ClientRequest, registry strfmt.Registry) error {
if currentSession := self.CurrentAPISessionDetail.Load(); currentSession != nil && currentSession.Token != nil && *currentSession.Token != "" {
if err := request.SetHeaderParam("zt-session", *currentSession.Token); err != nil {
currentSession := self.ApiSession.Load()
if currentSession != nil && currentSession.GetToken() != nil {
if err := currentSession.AuthenticateRequest(request, registry); err != nil {
return err
}
}
Expand Down Expand Up @@ -131,13 +131,18 @@ type ManagementApiClient struct {
// For OpenZiti instances not using publicly signed certificates, `ziti.GetControllerWellKnownCaPool()` can be used
// to obtain and verify the target controllers CAs. Tools should allow users to verify and accept new controllers
// that have not been verified from an outside secret (such as an enrollment token).
func NewManagementApiClient(apiUrl *url.URL, caPool *x509.CertPool) *ManagementApiClient {
func NewManagementApiClient(apiUrl *url.URL, caPool *x509.CertPool, totpCallback func(chan string)) *ManagementApiClient {
ret := &ManagementApiClient{}

ret.initializeComponents(apiUrl, rest_management_api_client.DefaultSchemes, ret, caPool)

newApi := rest_management_api_client.New(ret.Components.Runtime, nil)
api := ZitiEdgeManagement(*newApi)
api := ZitiEdgeManagement{
ZitiEdgeManagement: newApi,
apiUrl: apiUrl,
TotpCallback: totpCallback,
}

ret.API = &api

return ret
Expand All @@ -158,13 +163,17 @@ type ClientApiClient struct {
// For OpenZiti instances not using publicly signed certificates, `ziti.GetControllerWellKnownCaPool()` can be used
// to obtain and verify the target controllers CAs. Tools should allow users to verify and accept new controllers
// that have not been verified from an outside secret (such as an enrollment token).
func NewClientApiClient(apiUrl *url.URL, caPool *x509.CertPool) *ClientApiClient {
func NewClientApiClient(apiUrl *url.URL, caPool *x509.CertPool, totpCallback func(chan string)) *ClientApiClient {
ret := &ClientApiClient{}

ret.initializeComponents(apiUrl, rest_client_api_client.DefaultSchemes, ret, caPool)

newApi := rest_client_api_client.New(ret.Components.Runtime, nil)
api := ZitiEdgeClient(*newApi)
api := ZitiEdgeClient{
ZitiEdgeClient: newApi,
apiUrl: apiUrl,
TotpCallback: totpCallback,
}
ret.API = &api

return ret
Expand Down
4 changes: 1 addition & 3 deletions edge-apis/credentials.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,6 @@ type Credentials interface {
// provided token should be the base64 encoded version of the token.
AddJWT(string)

// AuthenticateRequest authenticates an outgoing request.
AuthenticateRequest(runtime.ClientRequest, strfmt.Registry) error

// ClientAuthInfoWriter is used to pass a Credentials instance to the openapi runtime to authenticate outgoing
//requests.
runtime.ClientAuthInfoWriter
Expand Down Expand Up @@ -71,6 +68,7 @@ func getClientAuthInfoOp(credentials Credentials, client *http.Client) func(*run
operation.AuthInfo = credentials

certs := credentials.TlsCerts()

if len(certs) != 0 {
operation.Client = client
if transport, ok := operation.Client.Transport.(*http.Transport); ok {
Expand Down
165 changes: 165 additions & 0 deletions edge-apis/oidc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
package edge_apis

import (
"context"
"crypto/rand"
"crypto/tls"
"fmt"
"github.com/golang-jwt/jwt/v5"
"github.com/google/uuid"
"github.com/michaelquigley/pfxlog"
"github.com/zitadel/oidc/v2/pkg/client/rp"
httphelper "github.com/zitadel/oidc/v2/pkg/http"
"github.com/zitadel/oidc/v2/pkg/oidc"
"net"
"net/http"
"net/http/cookiejar"
"time"
)

const JwtTokenPrefix = "ey"

type ServiceAccessClaims struct {
jwt.RegisteredClaims
ApiSessionId string `json:"z_asid"`
IdentityId string `json:"z_iid"`
TokenType string `json:"z_t"`
Type string `json:"z_st"`
}

type localRpServer struct {
Server *http.Server
Port string
Listener net.Listener
TokenChan chan *oidc.Tokens[*oidc.IDTokenClaims]
CallbackPath string
CallbackUri string
LoginUri string
}

func (t *localRpServer) Stop() {
_ = t.Server.Shutdown(context.Background())
close(t.TokenChan)
}

func (t *localRpServer) Start() {
go func() {
_ = t.Server.Serve(t.Listener)
}()

started := make(chan struct{})

go func() {
client := &http.Client{
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
}
end := time.Now().Add(11 * time.Second)
for {
if time.Now().After(end) {
break
}

time.Sleep(100 * time.Millisecond)

_, err := client.Get(t.LoginUri)

if err == nil {
break
}
}
close(started)
}()
select {
case <-started:
case <-time.After(10 * time.Second):
pfxlog.Logger().Warn("local relying party server did not start within 10s")
}
}

func newLocalRpServer(apiHost string, authMethod string) (*localRpServer, error) {
tokenOutChan := make(chan *oidc.Tokens[*oidc.IDTokenClaims], 1)
result := &localRpServer{
CallbackPath: "/auth/callback",
TokenChan: tokenOutChan,
}
var err error

result.Listener, err = net.Listen("tcp", ":0")

if err != nil {
return nil, fmt.Errorf("could not listen on a random port: %w", err)
}

_, result.Port, _ = net.SplitHostPort(result.Listener.Addr().String())

result.LoginUri = "http://127.0.0.1:" + result.Port + "/login"

key := make([]byte, 32)
_, err = rand.Read(key)
if err != nil {
return nil, fmt.Errorf("could not generate secure cookie key: %w", err)
}

urlBase := "https://" + apiHost
issuer := urlBase + "/oidc"
clientID := "native"
clientSecret := ""
scopes := []string{"openid", "offline_access"}
result.CallbackUri = "http://127.0.0.1:" + result.Port + result.CallbackPath

cookieHandler := httphelper.NewCookieHandler(key, key, httphelper.WithUnsecure())
jar, _ := cookiejar.New(&cookiejar.Options{})
httpClient := &http.Client{

Transport: &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
Proxy: http.ProxyFromEnvironment,
ForceAttemptHTTP2: true,
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
},
CheckRedirect: nil,
Jar: jar,
Timeout: 10 * time.Second,
}

options := []rp.Option{
rp.WithHTTPClient(httpClient),
rp.WithPKCE(cookieHandler),
}

provider, err := rp.NewRelyingPartyOIDC(issuer, clientID, clientSecret, result.CallbackUri, scopes, options...)

if err != nil {
return nil, fmt.Errorf("could not create rp OIDC: %w", err)
}

state := func() string {
return uuid.New().String()
}
serverMux := http.NewServeMux()

authHandler := rp.AuthURLHandler(state, provider, rp.WithPromptURLParam("Welcome back!"), rp.WithURLParam("method", authMethod))
loginHandler := http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
authHandler.ServeHTTP(writer, request)
})

serverMux.Handle("/login", loginHandler)

marshalToken := func(w http.ResponseWriter, r *http.Request, tokens *oidc.Tokens[*oidc.IDTokenClaims], state string, relyingParty rp.RelyingParty) {
tokenOutChan <- tokens
_, _ = w.Write([]byte("done!"))
}

serverMux.Handle(result.CallbackPath, rp.CodeExchangeHandler(marshalToken, provider))

result.Server = &http.Server{Handler: serverMux}

return result, nil
}
9 changes: 5 additions & 4 deletions example/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@ go 1.19

require (
github.com/Jeffail/gabs v1.4.0
github.com/google/uuid v1.4.0
github.com/gorilla/mux v1.8.1
github.com/michaelquigley/pfxlog v0.6.10
github.com/openziti/foundation/v2 v2.0.33
github.com/openziti/runzmd v1.0.33
Expand Down Expand Up @@ -48,6 +46,7 @@ require (
github.com/go-openapi/strfmt v0.21.7 // indirect
github.com/go-openapi/swag v0.22.4 // indirect
github.com/go-openapi/validate v0.22.1 // indirect
github.com/go-resty/resty/v2 v2.10.0 // indirect
github.com/golang-jwt/jwt/v5 v5.1.0 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/gomarkdown/markdown v0.0.0-20230922112808-5421fefb8386 // indirect
Expand All @@ -70,6 +69,7 @@ require (
github.com/miekg/pkcs11 v1.1.1 // indirect
github.com/mitchellh/go-ps v1.0.0 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/muhlemmer/gu v0.3.1 // indirect
github.com/oklog/ulid v1.3.1 // indirect
github.com/opentracing/opentracing-go v1.2.0 // indirect
github.com/openziti/channel/v2 v2.0.105 // indirect
Expand Down Expand Up @@ -101,16 +101,17 @@ require (
github.com/valyala/fasttemplate v1.2.2 // indirect
github.com/yusufpapurcu/wmi v1.2.3 // indirect
github.com/zitadel/logging v0.3.4 // indirect
github.com/zitadel/oidc/v2 v2.11.0 // indirect
go.mongodb.org/mongo-driver v1.13.0 // indirect
go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352 // indirect
go.opentelemetry.io/otel v1.19.0 // indirect
go.opentelemetry.io/otel/metric v1.19.0 // indirect
go.opentelemetry.io/otel/trace v1.19.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/crypto v0.14.0 // indirect
golang.org/x/crypto v0.15.0 // indirect
golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect
golang.org/x/image v0.14.0 // indirect
golang.org/x/net v0.17.0 // indirect
golang.org/x/net v0.18.0 // indirect
golang.org/x/oauth2 v0.13.0 // indirect
golang.org/x/sys v0.14.0 // indirect
golang.org/x/term v0.14.0 // indirect
Expand Down
Loading
Loading