Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions edge-apis/clients.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,17 @@ package edge_apis

import (
"crypto/x509"
"net/http"
"net/url"
"strings"
"sync/atomic"

"github.com/go-openapi/runtime"
openapiclient "github.com/go-openapi/runtime/client"
"github.com/go-openapi/strfmt"
"github.com/michaelquigley/pfxlog"
"github.com/openziti/edge-api/rest_client_api_client"
"github.com/openziti/edge-api/rest_management_api_client"
"net/http"
"net/url"
"strings"
"sync/atomic"
)

// ApiType is an interface constraint for generics. The underlying go-swagger types only have fields, which are
Expand Down Expand Up @@ -173,7 +174,7 @@ func (self *BaseClient[A]) ProcessControllers(authEnabledApi AuthEnabledApi) {
list, err := authEnabledApi.ListControllers()

if err != nil {
pfxlog.Logger().WithError(err).Error("error listing controllers, continuing with 1 default configured controller")
pfxlog.Logger().WithError(err).Debug("error listing controllers, continuing with 1 default configured controller")
return
}

Expand Down
7 changes: 7 additions & 0 deletions example/device-auth/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
OIDC Device Code authentication example
---

This sample shows OpenZiti OIDC authentication with device code flow.
Prerequisites:
- your OpenZiti network is configured with an external OIDC provider
- your OIDC provider is configured to allow device code flow
142 changes: 142 additions & 0 deletions example/device-auth/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
package main

import (
"bytes"
"flag"
"fmt"
"log"
"net/http"
"net/url"
"strings"
"time"

"github.com/dgrijalva/jwt-go"
"github.com/openziti/edge-api/rest_model"
"github.com/openziti/edge-api/rest_util"
nfx509 "github.com/openziti/foundation/v2/x509"
"github.com/openziti/sdk-golang/ziti"
"gopkg.in/square/go-jose.v2/json"
)

func die[T interface{}](res T, err error) T {
if err != nil {
log.Fatal(err)
}
return res
}

func main() {
cfg := flag.String("config", "", "path to config file")
openzitiURL := flag.String("ziti", "https://localhost:1280", "URL of the OpenZiti service")
flag.Parse()

var config *ziti.Config
if cfg == nil || *cfg == "" {
config = &ziti.Config{
ZtAPI: *openzitiURL,
}
// warning: this call is insecure and should not be used in production
ca := die(rest_util.GetControllerWellKnownCas(*openzitiURL))
var buf bytes.Buffer
_ = nfx509.MarshalToPem(ca, &buf)
config.ID.CA = buf.String()
} else {
if openzitiURL == nil || *openzitiURL == "" {
log.Fatal("OpenZiti URL must be specified")
}
config = die(ziti.NewConfigFromFile(*cfg))
}
ztx := die(ziti.NewContext(config))

err := ztx.Authenticate()
var provider *rest_model.ClientExternalJWTSignerDetail
if err != nil {
fmt.Println("Try authenticating with external provider")
idps := die(ztx.GetExternalSigners())
for idx, idp := range idps {
fmt.Printf("%d: %s\n", idx, *idp.Name)
}

fmt.Printf("Select provider allowing device code flow.\nEnter number[0-%d] to authenticate: ", len(idps)-1)
var id int
_ = die(fmt.Scanf("%d", &id))

provider = idps[id]
}
if provider == nil {
log.Fatal("No provider found")
}
fmt.Printf("Using %s\n", *provider.Name)

resp := die(http.Get(*provider.ExternalAuthURL + "/.well-known/openid-configuration"))
var oidcConfig map[string]interface{}
_ = json.NewDecoder(resp.Body).Decode(&oidcConfig)

deviceAuth := oidcConfig["device_authorization_endpoint"].(string)
scopes := append(provider.Scopes, "openid")
ss := strings.Join(scopes, " ")
resp = die(http.PostForm(deviceAuth, url.Values{
"client_id": {*provider.ClientID},
"scope": {ss},
"audience": {*provider.Audience},
}))

var deviceCode map[string]interface{}
_ = json.NewDecoder(resp.Body).Decode(&deviceCode)
if completeUrl, ok := deviceCode["verification_uri_complete"]; ok {
fmt.Printf("Open %s in your browser\n", completeUrl.(string))
} else if verifyUrl, ok := deviceCode["verification_uri"]; ok {
fmt.Printf("Open %s in your browser, and use code %s\n",
verifyUrl.(string), deviceCode["user_code"].(string))
} else {
log.Fatal("Unable to determine verification URL")
}

interval := time.Duration(int(deviceCode["interval"].(float64))) * time.Second

var token map[string]interface{}
for {
clear(token)
time.Sleep(interval)

tokenUrl := oidcConfig["token_endpoint"].(string)
resp = die(http.PostForm(tokenUrl, url.Values{
"client_id": {*provider.ClientID},
"device_code": {deviceCode["device_code"].(string)},
"grant_type": {"urn:ietf:params:oauth:grant-type:device_code"},
}))

json.NewDecoder(resp.Body).Decode(&token)
errmsg, hasErr := token["error"]
if !hasErr {
break
}
errormsg := errmsg.(string)
if errormsg == "authorization_pending" {
fmt.Println("Waiting for user to authorize...")
continue
}
log.Fatal(errormsg)
}

accessToken := token["access_token"].(string)
tok, _ := jwt.Parse(accessToken, nil)
if claims, ok := tok.Claims.(jwt.MapClaims); ok {
for k, v := range claims {
fmt.Printf("\t%s: %v\n", k, v)
}
}
ztx.LoginWithJWT(accessToken)

err = ztx.Authenticate()
if err != nil {
log.Fatal(err)
}
fmt.Println("Authenticated")

services, _ := ztx.GetServices()
fmt.Println("Available Services:")
for _, svc := range services {
fmt.Printf("\t%s\n", *svc.Name)
}
}
3 changes: 2 additions & 1 deletion example/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@ replace github.com/openziti/sdk-golang => ../

require (
github.com/Jeffail/gabs v1.4.0
github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/google/uuid v1.6.0
github.com/gorilla/mux v1.8.1
github.com/michaelquigley/pfxlog v0.6.10
github.com/openziti/edge-api v0.26.47
github.com/openziti/foundation/v2 v2.0.73
github.com/openziti/runzmd v1.0.33
github.com/openziti/sdk-golang v0.0.0
Expand Down Expand Up @@ -82,7 +84,6 @@ require (
github.com/oklog/ulid v1.3.1 // indirect
github.com/opentracing/opentracing-go v1.2.0 // indirect
github.com/openziti/channel/v4 v4.2.31 // indirect
github.com/openziti/edge-api v0.26.47 // indirect
github.com/openziti/identity v1.0.112 // indirect
github.com/openziti/metrics v1.4.2 // indirect
github.com/openziti/secretstream v0.1.39 // indirect
Expand Down
2 changes: 2 additions & 0 deletions example/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
github.com/dlclark/regexp2 v1.1.6/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
Expand Down
18 changes: 14 additions & 4 deletions ziti/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ import (
"crypto/x509/pkix"
"encoding/pem"
"fmt"
"strings"
"sync/atomic"

"github.com/go-openapi/strfmt"
"github.com/golang-jwt/jwt/v5"
"github.com/google/uuid"
Expand All @@ -46,8 +49,6 @@ import (
"github.com/openziti/sdk-golang/ziti/edge/posture"
"github.com/openziti/transport/v2"
"github.com/pkg/errors"
"strings"
"sync/atomic"
)

// CtrlClient is a stateful version of ZitiEdgeClient that simplifies operations
Expand All @@ -69,6 +70,15 @@ type CtrlClient struct {
capabilitiesLoaded atomic.Bool
}

func (self *CtrlClient) GetExternalSigners() (rest_model.ClientExternalJWTSignerList, error) {
response, err := self.API.ExternalJWTSigner.ListExternalJWTSigners(nil)
if err != nil {
return nil, err
}

return response.Payload.Data, nil
}

// GetCurrentApiSession returns the current cached ApiSession or nil
func (self *CtrlClient) GetCurrentApiSession() apis.ApiSession {
return self.ClientApiClient.GetCurrentApiSession()
Expand All @@ -92,7 +102,7 @@ func (self *CtrlClient) Refresh() (apis.ApiSession, error) {
}

// IsServiceListUpdateAvailable will contact the controller to determine if a new set of services are available. Service
// updates could entail gaining/losing services access via policy or runtime authorization revocation due to posture
// updates could entail gaining/losing service access via policy or runtime authorization revocation due to posture
// checks.
func (self *CtrlClient) IsServiceListUpdateAvailable() (bool, *strfmt.DateTime, error) {
resp, err := self.API.CurrentAPISession.ListServiceUpdates(current_api_session.NewListServiceUpdatesParams(), self.GetCurrentApiSession())
Expand All @@ -104,7 +114,7 @@ func (self *CtrlClient) IsServiceListUpdateAvailable() (bool, *strfmt.DateTime,
return self.lastServiceUpdate == nil || !resp.Payload.Data.LastChangeAt.Equal(*self.lastServiceUpdate), resp.Payload.Data.LastChangeAt, nil
}

// Authenticate attempts to use authenticate, overwriting any existing ApiSession.
// Authenticate attempts to authenticate, overwriting any existing ApiSession.
func (self *CtrlClient) Authenticate() (apis.ApiSession, error) {
var err error

Expand Down
10 changes: 3 additions & 7 deletions ziti/contexts.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,13 +98,9 @@ func NewContextWithOpts(cfg *Config, options *Options) (Context, error) {
newContext.maxDefaultConnections = 1
}

if cfg.ID.Cert != "" && cfg.ID.Key != "" {
idCredentials := edge_apis.NewIdentityCredentialsFromConfig(cfg.ID)
idCredentials.ConfigTypes = cfg.ConfigTypes
cfg.Credentials = idCredentials
} else if cfg.Credentials == nil {
return nil, errors.New("either cfg.ID or cfg.Credentials must be provided")
}
idCredentials := edge_apis.NewIdentityCredentialsFromConfig(cfg.ID)
idCredentials.ConfigTypes = cfg.ConfigTypes
cfg.Credentials = idCredentials

var apiStrs []string
if len(cfg.ZtAPIs) > 0 {
Expand Down
35 changes: 30 additions & 5 deletions ziti/ziti.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,9 +88,15 @@ type Context interface {
// creation.
Authenticate() error

// GetExternalSigners retrieves a list of external JWT signers with their details.
// Returns an error if the operation fails.
GetExternalSigners() ([]*rest_model.ClientExternalJWTSignerDetail, error)

// SetCredentials sets the credentials used to authenticate against the Edge Client API.
SetCredentials(authenticator apis.Credentials)

LoginWithJWT(jst string)

// GetCredentials returns the currently set credentials used to authenticate against the Edge Client API.
GetCredentials() apis.Credentials

Expand All @@ -107,17 +113,17 @@ type Context interface {
// DialWithOptions performs the same logic as Dial but allows specification of DialOptions.
DialWithOptions(serviceName string, options *DialOptions) (edge.Conn, error)

// DialAddr finds the service for given address and performs a Dial for it.
// DialAddr finds the service for a given address and performs a Dial for it.
DialAddr(network string, addr string) (edge.Conn, error)

// Listen attempts to host a service by the given service name; authenticating as necessary in order to obtain
// a service session, attach to Edge Routers, and bind (host) the service.
Listen(serviceName string) (edge.Listener, error)

// ListenWithOptions performs the same logic as Listen, but allows the specification of ListenOptions.
// ListenWithOptions performs the same logic as Listen but allows the specification of ListenOptions.
ListenWithOptions(serviceName string, options *ListenOptions) (edge.Listener, error)

// GetServiceId will return the id of a specific service by service name. If not found, false, will be returned
// GetServiceId will return the id of a specific service by service name. If not found, false will be returned
// with an empty string.
GetServiceId(serviceName string) (string, bool, error)

Expand All @@ -128,15 +134,15 @@ type Context interface {
// GetService will return the service details of a specific service by service name.
GetService(serviceName string) (*rest_model.ServiceDetail, bool)

// GetServiceForAddr finds the service with intercept that matches best to given address
// GetServiceForAddr finds the service with intercept that matches best to the given address
GetServiceForAddr(network, hostname string, port uint16) (*rest_model.ServiceDetail, int, error)

// RefreshServices forces the context to refresh the list of services the current authenticating identity has access
// to.
RefreshServices() error

// RefreshService forces the context to refresh just the service with the given name. If the given service isn't
// found, a nil will be returned
// found, nil will be returned
RefreshService(serviceName string) (*rest_model.ServiceDetail, error)

// GetServiceTerminators will return a slice of rest_model.TerminatorClientDetail for a specific service name.
Expand Down Expand Up @@ -482,6 +488,20 @@ func (context *ContextImpl) SetCredentials(credentials apis.Credentials) {
context.CtrlClt.Credentials = credentials
}

func (context *ContextImpl) LoginWithJWT(jwt string) {
cred := context.CtrlClt.Credentials
jwtCred := &apis.JwtCredentials{
BaseCredentials: apis.BaseCredentials{
ConfigTypes: cred.Payload().ConfigTypes,
EnvInfo: cred.Payload().EnvInfo,
SdkInfo: cred.Payload().SdkInfo,
CaPool: context.CtrlClt.CaPool,
},
JWT: jwt,
}
context.SetCredentials(jwtCred)
}

func (context *ContextImpl) GetCredentials() apis.Credentials {
return context.CtrlClt.Credentials
}
Expand Down Expand Up @@ -668,6 +688,11 @@ func (context *ContextImpl) refreshSessions() {
}
}

func (context *ContextImpl) GetExternalSigners() ([]*rest_model.ClientExternalJWTSignerDetail, error) {
result, err := context.CtrlClt.GetExternalSigners()
return result, err
}

func (context *ContextImpl) RefreshServices() error {
return context.refreshServices(true, false)
}
Expand Down
Loading