@@ -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