Skip to content

Commit fe2657d

Browse files
update redirect URI handling, ensure default uses ported value
Older controller code only supported hostname:port values for default redirect.
1 parent 6032172 commit fe2657d

File tree

5 files changed

+143
-52
lines changed

5 files changed

+143
-52
lines changed

edge-apis/client_base.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,12 @@ func (self *BaseClient[A]) SetAllowOidcDynamicallyEnabled(allow bool) {
8787
apiType.SetAllowOidcDynamicallyEnabled(allow)
8888
}
8989

90+
func (self *BaseClient[A]) SetOidcRedirectUri(redirectUri string) {
91+
v := any(self.API)
92+
apiType := v.(OidcEnabledApi)
93+
apiType.SetOidcRedirectUri(redirectUri)
94+
}
95+
9096
// Authenticate authenticates using provided credentials, updating the TLS configuration based on the credential's CA pool.
9197
// On success, stores the session and processes controller endpoints for HA failover.
9298
// On failure, clears the session and credentials.

edge-apis/client_edge_client.go

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ import (
1919
"github.com/zitadel/oidc/v3/pkg/oidc"
2020
)
2121

22+
var _ OidcEnabledApi = (*ZitiEdgeClient)(nil)
23+
var _ AuthEnabledApi = (*ZitiEdgeClient)(nil)
24+
2225
// ClientApiClient provides access to the Ziti Edge Client API for identity operations.
2326
type ClientApiClient struct {
2427
BaseClient[ZitiEdgeClient]
@@ -96,6 +99,11 @@ type ZitiEdgeClient struct {
9699

97100
TotpCodeProvider TotpCodeProvider
98101
ClientTransportPool ClientTransportPool
102+
OidcRedirectUri string
103+
}
104+
105+
func (self *ZitiEdgeClient) SetOidcRedirectUri(redirectUri string) {
106+
self.OidcRedirectUri = redirectUri
99107
}
100108

101109
// GetClientTransportPool returns the transport pool managing multiple controller endpoints for failover.
@@ -167,7 +175,16 @@ func (self *ZitiEdgeClient) legacyAuth(credentials Credentials, configTypes []st
167175

168176
// oidcAuth performs OIDC OAuth flow based authentication.
169177
func (self *ZitiEdgeClient) oidcAuth(credentials Credentials, configTypeOverrides []string, httpClient *http.Client) (ApiSession, error) {
170-
return oidcAuth(self.ClientTransportPool, credentials, configTypeOverrides, httpClient, self.TotpCodeProvider)
178+
config := &EdgeOidcAuthConfig{
179+
ClientTransportPool: self.ClientTransportPool,
180+
Credentials: credentials,
181+
ConfigTypeOverrides: configTypeOverrides,
182+
HttpClient: httpClient,
183+
TotpCodeProvider: self.TotpCodeProvider,
184+
RedirectUri: self.OidcRedirectUri,
185+
}
186+
187+
return oidcAuth(config)
171188
}
172189

173190
// SetUseOidc forces OIDC mode (true) or legacy mode (false), overriding automatic detection.

edge-apis/client_edge_management.go

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ func NewManagementApiClientWithConfig(config *ApiClientConfig) *ManagementApiCli
7878
}
7979

8080
var _ AuthEnabledApi = (*ZitiEdgeManagement)(nil)
81+
var _ OidcEnabledApi = (*ZitiEdgeManagement)(nil)
8182

8283
// ZitiEdgeManagement is an alias of the go-swagger generated client that allows this package to add additional
8384
// functionality to the alias type to implement the AuthEnabledApi interface.
@@ -97,6 +98,11 @@ type ZitiEdgeManagement struct {
9798

9899
TotpCodeProvider TotpCodeProvider
99100
ClientTransportPool ClientTransportPool
101+
OidcRedirectUri string
102+
}
103+
104+
func (self *ZitiEdgeManagement) SetOidcRedirectUri(redirectUri string) {
105+
self.OidcRedirectUri = redirectUri
100106
}
101107

102108
// SetClientTransportPool sets the transport pool.
@@ -170,7 +176,15 @@ func (self *ZitiEdgeManagement) legacyAuth(credentials Credentials, configTypes
170176

171177
// oidcAuth performs OIDC OAuth flow based authentication.
172178
func (self *ZitiEdgeManagement) oidcAuth(credentials Credentials, configTypeOverrides []string, httpClient *http.Client) (ApiSession, error) {
173-
return oidcAuth(self.ClientTransportPool, credentials, configTypeOverrides, httpClient, self.TotpCodeProvider)
179+
config := &EdgeOidcAuthConfig{
180+
ClientTransportPool: self.ClientTransportPool,
181+
Credentials: credentials,
182+
ConfigTypeOverrides: configTypeOverrides,
183+
HttpClient: httpClient,
184+
TotpCodeProvider: self.TotpCodeProvider,
185+
RedirectUri: self.OidcRedirectUri,
186+
}
187+
return oidcAuth(config)
174188
}
175189

176190
// SetUseOidc forces OIDC mode (true) or legacy mode (false), overriding automatic detection.

edge-apis/clients_shared.go

Lines changed: 84 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import (
2525
"encoding/json"
2626
"errors"
2727
"fmt"
28+
"io"
2829
"net/http"
2930
"net/url"
3031
"strings"
@@ -40,6 +41,11 @@ import (
4041
"golang.org/x/oauth2"
4142
)
4243

44+
// DefaultOidcRedirectUri is the default redirect URI for the OIDC PKCE flow that satisfies the default OIDC redirects
45+
// for the Ziti Edge OIDC API. It is not an actual server, rather an intercepted redirect URI that is used to extract
46+
// the resulting OIDC tokens.
47+
const DefaultOidcRedirectUri = "http://localhost:8080/auth/callback"
48+
4349
// ApiType is an interface constraint for generics. The underlying go-swagger types only have fields, which are
4450
// insufficient to attempt to make a generic type from. Instead, this constraint is used that points at the
4551
// aliased types.
@@ -55,6 +61,22 @@ type OidcEnabledApi interface {
5561
// SetAllowOidcDynamicallyEnabled sets whether clients will check the controller for OIDC support or not. If supported
5662
// OIDC is favored over legacy authentication.
5763
SetAllowOidcDynamicallyEnabled(allow bool)
64+
65+
// SetOidcRedirectUri sets the redirect URI for the OIDC PKCE flow. The default value is used if not set.
66+
// Should only be necessary to call for custom redirect controller configurations.
67+
SetOidcRedirectUri(redirectUri string)
68+
}
69+
70+
// EdgeOidcAuthConfig represents the options necessary to complete an OAuth 2.0 PKCE authentication flow against an
71+
// OpenZiti controller.
72+
type EdgeOidcAuthConfig struct {
73+
ClientTransportPool ClientTransportPool
74+
Credentials Credentials
75+
ConfigTypeOverrides []string
76+
HttpClient *http.Client
77+
TotpCodeProvider TotpCodeProvider
78+
RedirectUri string
79+
ApiHost string
5880
}
5981

6082
// ApiClientConfig contains configuration options for creating API clients.
@@ -221,15 +243,15 @@ func (a *authPayload) toValues() url.Values {
221243

222244
// oidcAuth performs OIDC authentication using OAuth flow with PKCE.
223245
// It handles TOTP if required and returns an OIDC session with tokens.
224-
func oidcAuth(clientTransportPool ClientTransportPool, credentials Credentials, configTypeOverrides []string, httpClient *http.Client, totpCodeProvider TotpCodeProvider) (ApiSession, error) {
225-
if credentials.Method() == AuthMethodEmpty {
246+
func oidcAuth(config *EdgeOidcAuthConfig) (ApiSession, error) {
247+
if config.Credentials.Method() == AuthMethodEmpty {
226248
return nil, fmt.Errorf("auth method %s cannot be used for authentication, please provide alternate credentials", AuthMethodEmpty)
227249
}
228250

229-
certificates := credentials.TlsCerts()
251+
certificates := config.Credentials.TlsCerts()
230252

231253
if len(certificates) != 0 {
232-
if transport, ok := httpClient.Transport.(TlsAwareTransport); ok {
254+
if transport, ok := config.HttpClient.Transport.(TlsAwareTransport); ok {
233255
tlsClientConf := transport.GetTlsClientConfig()
234256
tlsClientConf.Certificates = certificates
235257
transport.CloseIdleConnections()
@@ -238,12 +260,12 @@ func oidcAuth(clientTransportPool ClientTransportPool, credentials Credentials,
238260

239261
var outTokens *oidc.Tokens[*oidc.IDTokenClaims]
240262

241-
_, err := clientTransportPool.TryTransportForF(func(transport *ApiClientTransport) (any, error) {
242-
243-
edgeOidcAuth := newEdgeOidcAuthenticator(transport, httpClient)
263+
_, err := config.ClientTransportPool.TryTransportForF(func(transport *ApiClientTransport) (any, error) {
264+
config.ApiHost = transport.ApiUrl.Host
265+
edgeOidcAuth := NewEdgeOidcAuthenticator(config)
244266

245267
var err error
246-
outTokens, err = edgeOidcAuth.Authenticate(credentials, totpCodeProvider, configTypeOverrides)
268+
outTokens, err = edgeOidcAuth.Authenticate()
247269

248270
if err != nil {
249271
return nil, err
@@ -258,50 +280,52 @@ func oidcAuth(clientTransportPool ClientTransportPool, credentials Credentials,
258280

259281
return &ApiSessionOidc{
260282
OidcTokens: outTokens,
261-
RequestHeaders: credentials.GetRequestHeaders(),
283+
RequestHeaders: config.Credentials.GetRequestHeaders(),
262284
}, nil
263285
}
264286

265-
// edgeOidcAuthenticator handles the OAuth 2.0 PKCE authentication flow for the Ziti Edge API.
287+
// EdgeOidcAuthenticator handles the OAuth 2.0 PKCE authentication flow for the Ziti Edge API.
266288
// It submits user credentials to the authorization endpoint, handles optional TOTP verification,
267289
// and exchanges the authorization code for OIDC tokens. The HTTP client follows redirects
268290
// during the authorization flow and extracts the authorization code from the final redirect.
269-
type edgeOidcAuthenticator struct {
270-
httpClient *http.Client
271-
configTypeOverrides []string
272-
client *resty.Client
273-
apiHost string
274-
redirectUri string
291+
type EdgeOidcAuthenticator struct {
292+
*EdgeOidcAuthConfig
293+
restyClient *resty.Client
275294
}
276295

277-
// newEdgeOidcAuthenticator creates a new edgeOidcAuthenticator configured for PKCE authentication.
296+
// NewEdgeOidcAuthenticator creates a new EdgeOidcAuthenticator configured for PKCE authentication.
278297
// It sets up an HTTP client with a custom redirect policy that follows redirects during the
279298
// authorization flow but stops when the callback redirect URI is reached, allowing code extraction
280299
// from the redirect URL. The redirectUri parameter defines where the authorization server will
281300
// redirect with the authorization code in the query parameters.
282-
func newEdgeOidcAuthenticator(transport *ApiClientTransport, httpClient *http.Client) *edgeOidcAuthenticator {
283-
const DefaultOidcRedirectUri = "http://localhost/auth/callback"
301+
func NewEdgeOidcAuthenticator(config *EdgeOidcAuthConfig) *EdgeOidcAuthenticator {
302+
client := resty.NewWithClient(config.HttpClient)
284303

285-
client := resty.NewWithClient(httpClient)
304+
if config.RedirectUri == "" {
305+
config.RedirectUri = DefaultOidcRedirectUri
306+
}
286307

287308
// allows resty to follow redirects for us during the OAuth flow, but not for the end PKCE callback
288309
// there is no server running for that redirect to hit, as it is this code
289310
client.SetRedirectPolicy(RedirectUntilUrlPrefix(DefaultOidcRedirectUri))
290311

291-
apiHost := transport.ApiUrl.Host
292-
293-
return &edgeOidcAuthenticator{
294-
httpClient: httpClient,
295-
client: client,
296-
apiHost: apiHost,
297-
redirectUri: DefaultOidcRedirectUri,
312+
return &EdgeOidcAuthenticator{
313+
EdgeOidcAuthConfig: config,
314+
restyClient: client,
298315
}
299316
}
300317

318+
// SetRedirectUri sets the redirect URI for the authorization server. The default value is
319+
// included in the default Edge OIDC controller configuration, but if it has been set to custom
320+
// values, this function can be used to reflect that configuration.
321+
func (e *EdgeOidcAuthenticator) SetRedirectUri(redirectUri string) {
322+
e.RedirectUri = redirectUri
323+
}
324+
301325
// Authenticate performs the complete OAuth 2.0 PKCE authentication flow. It initiates authorization
302326
// with PKCE parameters, submits credentials and handles optional TOTP verification, then exchanges
303327
// the resulting authorization code for OIDC tokens.
304-
func (e *edgeOidcAuthenticator) Authenticate(credentials Credentials, totpCodeProvider TotpCodeProvider, configTypeOverrides []string) (*oidc.Tokens[*oidc.IDTokenClaims], error) {
328+
func (e *EdgeOidcAuthenticator) Authenticate() (*oidc.Tokens[*oidc.IDTokenClaims], error) {
305329
pkceParams, err := newPkceParameters()
306330
if err != nil {
307331
return nil, fmt.Errorf("failed to generate PKCE parameters: %w", err)
@@ -313,7 +337,7 @@ func (e *edgeOidcAuthenticator) Authenticate(credentials Credentials, totpCodePr
313337
return nil, fmt.Errorf("failed to initiate authorization flow: %w", err)
314338
}
315339

316-
redirectResp, err := e.handlePrimaryAndSecondaryAuth(verificationParams, credentials, totpCodeProvider, configTypeOverrides)
340+
redirectResp, err := e.handlePrimaryAndSecondaryAuth(verificationParams)
317341
if err != nil {
318342
return nil, err
319343
}
@@ -329,7 +353,7 @@ func (e *edgeOidcAuthenticator) Authenticate(credentials Credentials, totpCodePr
329353
// finishOAuthFlow extracts the authorization code from the callback redirect and exchanges it for tokens.
330354
// The authorization server returns the code as a query parameter in the Location header of the redirect response.
331355
// The code is then used with the PKCE verifier to obtain OIDC tokens via the token endpoint.
332-
func (e *edgeOidcAuthenticator) finishOAuthFlow(redirectResp *resty.Response, verificationParams *verificationParameters, pkceParams *pkceParameters) (*oidc.Tokens[*oidc.IDTokenClaims], error) {
356+
func (e *EdgeOidcAuthenticator) finishOAuthFlow(redirectResp *resty.Response, verificationParams *verificationParameters, pkceParams *pkceParameters) (*oidc.Tokens[*oidc.IDTokenClaims], error) {
333357
if redirectResp.StatusCode() != http.StatusFound {
334358
return nil, fmt.Errorf("authentication failed, expected a 302, got %d", redirectResp.StatusCode())
335359
}
@@ -368,24 +392,24 @@ func (e *edgeOidcAuthenticator) finishOAuthFlow(redirectResp *resty.Response, ve
368392
}
369393

370394
// handlePrimaryAndSecondaryAuth submits credentials to the authorization endpoint and handles optional TOTP.
371-
func (e *edgeOidcAuthenticator) handlePrimaryAndSecondaryAuth(verificationParams *verificationParameters, credentials Credentials, totpCodeProvider TotpCodeProvider, configTypeOverrides []string) (*resty.Response, error) {
372-
loginUri := "https://" + e.apiHost + "/oidc/login/" + string(credentials.Method())
373-
totpUri := "https://" + e.apiHost + "/oidc/login/totp"
395+
func (e *EdgeOidcAuthenticator) handlePrimaryAndSecondaryAuth(verificationParams *verificationParameters) (*resty.Response, error) {
396+
loginUri := "https://" + e.ApiHost + "/oidc/login/" + string(e.Credentials.Method())
397+
totpUri := "https://" + e.ApiHost + "/oidc/login/totp"
374398

375399
payload := &authPayload{
376-
Authenticate: credentials.Payload(),
400+
Authenticate: e.Credentials.Payload(),
377401
AuthRequestId: verificationParams.AuthRequestId,
378402
}
379403

380-
if e.configTypeOverrides != nil {
381-
payload.ConfigTypes = e.configTypeOverrides
404+
if e.ConfigTypeOverrides != nil {
405+
payload.ConfigTypes = e.ConfigTypeOverrides
382406
}
383407

384408
formData := payload.toValues()
385-
req := e.client.R()
386-
clientRequest := asClientRequest(req, e.client)
409+
req := e.restyClient.R()
410+
clientRequest := asClientRequest(req, e.restyClient)
387411

388-
err := credentials.AuthenticateRequest(clientRequest, strfmt.Default)
412+
err := e.Credentials.AuthenticateRequest(clientRequest, strfmt.Default)
389413
if err != nil {
390414
return nil, err
391415
}
@@ -410,11 +434,11 @@ func (e *edgeOidcAuthenticator) handlePrimaryAndSecondaryAuth(verificationParams
410434
return nil, errors.New("response was not a redirect and TOTP is not required, unknown additional authentication steps are required but unsupported")
411435
}
412436

413-
if totpCodeProvider == nil {
437+
if e.TotpCodeProvider == nil {
414438
return nil, errors.New("totp is required but no totp callback was defined")
415439
}
416440

417-
totpCodeResultCh := totpCodeProvider.GetTotpCode()
441+
totpCodeResultCh := e.TotpCodeProvider.GetTotpCode()
418442
var totpCode string
419443

420444
select {
@@ -427,7 +451,7 @@ func (e *edgeOidcAuthenticator) handlePrimaryAndSecondaryAuth(verificationParams
427451
return nil, fmt.Errorf("timeout waiting for totp code provider")
428452
}
429453

430-
resp, err = e.client.R().SetBody(&totpCodePayload{
454+
resp, err = e.restyClient.R().SetBody(&totpCodePayload{
431455
MfaCode: rest_model.MfaCode{
432456
Code: &totpCode,
433457
},
@@ -451,29 +475,39 @@ func (e *edgeOidcAuthenticator) handlePrimaryAndSecondaryAuth(verificationParams
451475
}
452476

453477
// initOAuthFlow initiates the OAuth authorization request with PKCE parameters and returns the authorization request ID.
454-
func (e *edgeOidcAuthenticator) initOAuthFlow(pkceParams *pkceParameters) (*verificationParameters, error) {
478+
func (e *EdgeOidcAuthenticator) initOAuthFlow(pkceParams *pkceParameters) (*verificationParameters, error) {
455479
verificationParams := &verificationParameters{
456480
State: generateRandomState(),
457481
Nonce: generateNonce(),
458482
}
459483

460-
authUrl := "https://" + e.apiHost + "/oidc/authorize?" + url.Values{
484+
authUrl := "https://" + e.ApiHost + "/oidc/authorize?" + url.Values{
461485
"client_id": []string{"native"},
462486
"response_type": []string{"code"},
463487
"scope": []string{"openid offline_access"},
464488
"state": []string{verificationParams.State},
465489
"code_challenge": []string{pkceParams.Challenge},
466490
"code_challenge_method": []string{pkceParams.Method},
467-
"redirect_uri": []string{e.redirectUri},
491+
"redirect_uri": []string{e.RedirectUri},
468492
"nonce": []string{verificationParams.Nonce},
469493
}.Encode()
470494

471-
resp, err := e.client.R().SetDoNotParseResponse(true).Get(authUrl)
495+
resp, err := e.restyClient.R().SetDoNotParseResponse(true).Get(authUrl)
472496
if err != nil {
473497
return nil, err
474498
}
475499
defer func() { _ = resp.RawResponse.Body.Close() }()
476500

501+
if resp.StatusCode() != http.StatusOK {
502+
body, _ := io.ReadAll(resp.RawResponse.Body)
503+
504+
if len(body) == 0 {
505+
body = []byte("<body was empty>")
506+
}
507+
508+
return nil, fmt.Errorf("authentication request start failed with status %d, either a misconfigured request was sent or the expected redirect URL (%s) is not allowed: %s", resp.StatusCode(), e.RedirectUri, body)
509+
}
510+
477511
verificationParams.AuthRequestId = resp.Header().Get(AuthRequestIdHeader)
478512
if verificationParams.AuthRequestId == "" {
479513
return nil, errors.New("could not find auth request id header from authorize endpoint")
@@ -498,15 +532,15 @@ func RedirectUntilUrlPrefix(urlPrefixToStopAt ...string) resty.RedirectPolicy {
498532
}
499533

500534
// exchangeAuthorizationCodeForTokens exchanges an authorization code and PKCE verifier for OIDC tokens.
501-
func (e *edgeOidcAuthenticator) exchangeAuthorizationCodeForTokens(code string, pkceParams *pkceParameters) (*oidc.Tokens[*oidc.IDTokenClaims], error) {
502-
tokenEndpoint := "https://" + e.apiHost + "/oidc/oauth/token"
535+
func (e *EdgeOidcAuthenticator) exchangeAuthorizationCodeForTokens(code string, pkceParams *pkceParameters) (*oidc.Tokens[*oidc.IDTokenClaims], error) {
536+
tokenEndpoint := "https://" + e.ApiHost + "/oidc/oauth/token"
503537

504-
tokenResp, err := e.client.R().SetFormData(map[string]string{
538+
tokenResp, err := e.restyClient.R().SetFormData(map[string]string{
505539
"grant_type": "authorization_code",
506540
"client_id": "native",
507541
"code_verifier": pkceParams.Verifier,
508542
"code": code,
509-
"redirect_uri": "http://localhost/auth/callback",
543+
"redirect_uri": DefaultOidcRedirectUri,
510544
}).Post(tokenEndpoint)
511545

512546
if err != nil {

0 commit comments

Comments
 (0)