Skip to content

Commit

Permalink
Merge pull request #2430 from openziti/fix.2354.auth.query.support.oidc
Browse files Browse the repository at this point in the history
fixes #2354 adds oidc AuthQuery support
  • Loading branch information
andrewpmartinez authored Oct 4, 2024
2 parents 7849e06 + c2a62c9 commit e9c8180
Show file tree
Hide file tree
Showing 11 changed files with 306 additions and 48 deletions.
30 changes: 20 additions & 10 deletions controller/env/appenv.go
Original file line number Diff line number Diff line change
Expand Up @@ -602,7 +602,7 @@ func (ae *AppEnv) FillRequestContext(rc *response.RequestContext) error {
func NewAuthQueryZitiMfa() *rest_model.AuthQueryDetail {
provider := rest_model.MfaProvidersZiti
return &rest_model.AuthQueryDetail{
TypeID: "MFA",
TypeID: rest_model.AuthQueryTypeMFA,
Format: rest_model.MfaFormatsAlphaNumeric,
HTTPMethod: http.MethodPost,
HTTPURL: "./authenticate/mfa",
Expand All @@ -612,12 +612,23 @@ func NewAuthQueryZitiMfa() *rest_model.AuthQueryDetail {
}
}

func NewAuthQueryExtJwt(url string) *rest_model.AuthQueryDetail {
func NewAuthQueryExtJwt(signer *model.ExternalJwtSigner) *rest_model.AuthQueryDetail {
provider := rest_model.MfaProvidersURL

if signer == nil {
return &rest_model.AuthQueryDetail{
TypeID: rest_model.AuthQueryTypeEXTDashJWT,
Provider: &provider,
}
}

return &rest_model.AuthQueryDetail{
HTTPURL: url,
TypeID: "EXT-JWT",
HTTPURL: stringz.OrEmpty(signer.ExternalAuthUrl),
TypeID: rest_model.AuthQueryTypeEXTDashJWT,
Provider: &provider,
Scopes: signer.Scopes,
ClientID: stringz.OrEmpty(signer.ClientId),
ID: signer.Id,
}
}

Expand All @@ -642,12 +653,11 @@ func ProcessAuthQueries(ae *AppEnv, rc *response.RequestContext) {

if err != nil || !authResult.IsSuccessful() {
signer, err := ae.Managers.ExternalJwtSigner.Read(*rc.AuthPolicy.Secondary.RequiredExtJwtSigner)
authUrl := ""
if err == nil {
authUrl = stringz.OrEmpty(signer.ExternalAuthUrl)
}

rc.AuthQueries = append(rc.AuthQueries, NewAuthQueryExtJwt(authUrl))
if err != nil {
pfxlog.Logger().Errorf("could not read required external jwt signer: %s: %s", *rc.AuthPolicy.Secondary.RequiredExtJwtSigner, err)
}
rc.AuthQueries = append(rc.AuthQueries, NewAuthQueryExtJwt(signer))

}
}
Expand Down Expand Up @@ -859,7 +869,7 @@ func (ae *AppEnv) getJwtTokenFromRequest(r *http.Request) *jwt.Token {
parsedToken, err := jwt.ParseWithClaims(token, claims, ae.ControllersKeyFunc)

if err != nil {
pfxlog.Logger().WithError(err).Error("error during JWT parsing during API request")
pfxlog.Logger().WithError(err).Debug("JWT provided that did not parse and verify against controller public keys, skipping")
continue
}
if parsedToken.Valid {
Expand Down
1 change: 1 addition & 0 deletions controller/model/authenticator_mod_updb.go
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ func (module *AuthModuleUpdb) Process(context AuthContext) (AuthResult, error) {
authenticator: authenticator,
authenticatorId: authenticator.Id,
env: module.env,
authPolicy: authPolicy,
}, nil
}

Expand Down
88 changes: 71 additions & 17 deletions controller/oidc_auth/login.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"embed"
"encoding/json"
"fmt"
"github.com/go-openapi/swag"
"github.com/openziti/edge-api/rest_model"
"github.com/openziti/foundation/v2/errorz"
"github.com/openziti/ziti/controller/apierror"
Expand Down Expand Up @@ -108,6 +109,7 @@ func newLogin(store Storage, callback func(context.Context, string) string, issu

func (l *login) createRouter(issuerInterceptor *op.IssuerInterceptor) {
l.router = mux.NewRouter()
l.router.Path("/auth-queries").Methods("GET").HandlerFunc(l.listAuthQueries)
l.router.Path("/password").Methods("GET").HandlerFunc(l.loginHandler)
l.router.Path("/password").Methods("POST").HandlerFunc(issuerInterceptor.HandlerFunc(l.authenticate))

Expand Down Expand Up @@ -150,14 +152,14 @@ func (l *login) loginHandler(w http.ResponseWriter, r *http.Request) {
}

func renderLogin(w http.ResponseWriter, id string, err error) {
renderPage(w, loginTemplate, id, err)
renderPage(w, loginTemplate, id, err, nil)
}

func renderTotp(w http.ResponseWriter, id string, err error) {
renderPage(w, totpTemplate, id, err)
func renderTotp(w http.ResponseWriter, id string, err error, additionalData any) {
renderPage(w, totpTemplate, id, err, additionalData)
}

func renderPage(w http.ResponseWriter, pageTemplate *template.Template, id string, err error) {
func renderPage(w http.ResponseWriter, pageTemplate *template.Template, id string, err error, additionalData any) {
w.Header().Set("content-type", "text/html; charset=utf-8")
var errMsg string
errDisplay := "none"
Expand All @@ -166,17 +168,19 @@ func renderPage(w http.ResponseWriter, pageTemplate *template.Template, id strin
errDisplay = "block"
}
data := &struct {
ID string
Error string
ErrorDisplay string
ID string
Error string
ErrorDisplay string
AdditionalData any
}{
ID: id,
Error: errMsg,
ErrorDisplay: errDisplay,
ID: id,
Error: errMsg,
ErrorDisplay: errDisplay,
AdditionalData: additionalData,
}

err = pageTemplate.Execute(w, data)
if err != nil {
templateErr := pageTemplate.Execute(w, data)
if templateErr != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
Expand Down Expand Up @@ -241,13 +245,13 @@ func (l *login) checkTotp(w http.ResponseWriter, r *http.Request) {
})
return
} else {
renderTotp(w, id, verifyErr)
renderTotp(w, id, verifyErr, nil)
return
}
}

if !authRequest.HasAmr(AuthMethodSecondaryTotp) {
renderTotp(w, id, errors.New("TOTP supplied but not enabled or required on identity"))
renderTotp(w, id, errors.New("TOTP supplied but not enabled or required on identity"), nil)
}

callbackUrl := l.callback(r.Context(), id)
Expand Down Expand Up @@ -291,6 +295,7 @@ func (l *login) authenticate(w http.ResponseWriter, r *http.Request) {
invalid := apierror.NewInvalidAuth()
if method == AuthMethodPassword {
renderLogin(w, credentials.AuthRequestId, invalid)
w.WriteHeader(invalid.Status)
return
}

Expand All @@ -302,12 +307,25 @@ func (l *login) authenticate(w http.ResponseWriter, r *http.Request) {
authRequest.EnvInfo = credentials.EnvInfo
authRequest.AuthTime = time.Now()

if authRequest.SecondaryTotpRequired && !authRequest.HasAmr(AuthMethodSecondaryTotp) {
var authQueries []*rest_model.AuthQueryDetail

if !authRequest.HasSecondaryAuth() {
authQueries = authRequest.GetAuthQueries()
}

if authRequest.NeedsTotp() {
w.Header().Set(TotpRequiredHeader, "true")
}

if len(authQueries) > 0 {

if responseType == HtmlContentType {
renderTotp(w, credentials.AuthRequestId, err)
renderTotp(w, credentials.AuthRequestId, err, authQueries)
} else if responseType == JsonContentType {
renderJson(w, http.StatusOK, &rest_model.Empty{})
respBody := JsonMap(map[string]interface{}{
"authQueries": authQueries,
})
renderJson(w, http.StatusOK, &respBody)
}

return
Expand All @@ -317,6 +335,42 @@ func (l *login) authenticate(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, callbackUrl, http.StatusFound)
}

func (l *login) listAuthQueries(w http.ResponseWriter, r *http.Request) {
authRequestId := r.URL.Query().Get("id")

authRequest, err := l.store.GetAuthRequest(authRequestId)

if err != nil {
invalid := apierror.NewInvalidAuth()
http.Error(w, invalid.Message, invalid.Status)
return
}

var authQueries []*rest_model.AuthQueryDetail

if !authRequest.HasSecondaryAuth() {
authQueries = authRequest.GetAuthQueries()
}

if authRequest.NeedsTotp() {
w.Header().Set(TotpRequiredHeader, "true")
}

respBody := JsonMap(map[string]interface{}{
"authQueries": authQueries,
})
renderJson(w, http.StatusOK, &respBody)
}

type JsonMap map[string]any

func (m *JsonMap) MarshalBinary() ([]byte, error) {
if m == nil {
return nil, nil
}
return swag.WriteJSON(m)
}

func (l *login) startEnrollTotp(w http.ResponseWriter, r *http.Request) {
changeCtx := NewHttpChangeCtx(r)

Expand Down
66 changes: 55 additions & 11 deletions controller/oidc_auth/requests.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import (
"crypto/x509"
"fmt"
"github.com/openziti/edge-api/rest_model"
"github.com/openziti/foundation/v2/stringz"
"github.com/openziti/ziti/common"
"github.com/openziti/ziti/controller/model"
"time"

"github.com/zitadel/oidc/v2/pkg/oidc"
Expand All @@ -14,16 +16,15 @@ import (
// AuthRequest represents an OIDC authentication request and implements op.AuthRequest
type AuthRequest struct {
oidc.AuthRequest
Id string
CreationDate time.Time
IdentityId string
AuthTime time.Time
ApiSessionId string
SecondaryTotpRequired bool
SecondaryExtJwtRequired bool
SecondaryExtJwtId string
ConfigTypes []string
Amr map[string]struct{}
Id string
CreationDate time.Time
IdentityId string
AuthTime time.Time
ApiSessionId string
SecondaryTotpRequired bool
SecondaryExtJwtSigner *model.ExternalJwtSigner
ConfigTypes []string
Amr map[string]struct{}

PeerCerts []*x509.Certificate
RequestedMethod string
Expand Down Expand Up @@ -69,7 +70,7 @@ func (a *AuthRequest) HasPrimaryAuth() bool {
// HasSecondaryAuth returns true if all applicable secondary authentications have been passed
func (a *AuthRequest) HasSecondaryAuth() bool {
return (!a.SecondaryTotpRequired || a.HasAmr(AuthMethodSecondaryTotp)) &&
(!a.SecondaryExtJwtRequired || a.HasAmr(AuthMethodSecondaryExtJwt))
(a.SecondaryExtJwtSigner == nil || a.HasAmrExtJwtId(a.SecondaryExtJwtSigner.Id))
}

// HasAmr returns true if the supplied amr is present
Expand All @@ -78,6 +79,10 @@ func (a *AuthRequest) HasAmr(amr string) bool {
return found
}

func (a *AuthRequest) HasAmrExtJwtId(id string) bool {
return a.HasAmr(AuthMethodSecondaryExtJwt + ":" + id)
}

// AddAmr adds the supplied amr
func (a *AuthRequest) AddAmr(amr string) {
if a.Amr == nil {
Expand Down Expand Up @@ -159,6 +164,45 @@ func (a *AuthRequest) GetCertFingerprints() []string {
return prints
}

func (a *AuthRequest) NeedsTotp() bool {
return a.SecondaryTotpRequired && !a.HasAmr(AuthMethodSecondaryTotp)
}

func (a *AuthRequest) NeedsSecondaryExtJwt() bool {
return a.SecondaryExtJwtSigner != nil && !a.HasAmrExtJwtId(a.SecondaryExtJwtSigner.Id)
}

func (a *AuthRequest) GetAuthQueries() []*rest_model.AuthQueryDetail {
var authQueries []*rest_model.AuthQueryDetail

if a.NeedsTotp() {
provider := rest_model.MfaProvidersZiti
authQueries = append(authQueries, &rest_model.AuthQueryDetail{
Format: rest_model.MfaFormatsNumeric,
HTTPMethod: "POST",
HTTPURL: "./oidc/login/totp",
MaxLength: 8,
MinLength: 6,
Provider: &provider,
TypeID: rest_model.AuthQueryTypeTOTP,
})
}

if a.NeedsSecondaryExtJwt() {
provider := rest_model.MfaProvidersURL
authQueries = append(authQueries, &rest_model.AuthQueryDetail{
ClientID: stringz.OrEmpty(a.SecondaryExtJwtSigner.ClientId),
HTTPURL: stringz.OrEmpty(a.SecondaryExtJwtSigner.ExternalAuthUrl),
Scopes: a.SecondaryExtJwtSigner.Scopes,
Provider: &provider,
ID: a.SecondaryExtJwtSigner.Id,
TypeID: rest_model.AuthQueryTypeEXTDashJWT,
})
}

return authQueries
}

// RefreshTokenRequest is a wrapper around RefreshClaims to avoid collisions between go-jwt interface requirements and
// zitadel oidc interface names. Implements zitadel op.RefreshTokenRequest
type RefreshTokenRequest struct {
Expand Down
12 changes: 11 additions & 1 deletion controller/oidc_auth/storage.go
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,17 @@ func (s *HybridStorage) Authenticate(authCtx model.AuthContext, id string, confi
return nil, err
}

authRequest.SecondaryTotpRequired = mfa != nil && mfa.IsVerified
authRequest.SecondaryTotpRequired = (mfa != nil && mfa.IsVerified) || result.AuthPolicy().Secondary.RequireTotp

extJwtSignerId := stringz.OrEmpty(result.AuthPolicy().Secondary.RequiredExtJwtSigner)

if extJwtSignerId != "" {
authRequest.SecondaryExtJwtSigner, err = s.env.GetManagers().ExternalJwtSigner.Read(extJwtSignerId)

if err != nil {
return nil, err
}
}

if authCtx.GetMethod() == AuthMethodCert {
if len(authRequest.PeerCerts) == 0 {
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ require (
github.com/openziti/agent v1.0.18
github.com/openziti/channel/v3 v3.0.5
github.com/openziti/cobra-to-md v1.0.1
github.com/openziti/edge-api v0.26.32
github.com/openziti/edge-api v0.26.33
github.com/openziti/foundation/v2 v2.0.49
github.com/openziti/identity v1.0.85
github.com/openziti/jwks v1.0.6
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -576,8 +576,8 @@ github.com/openziti/cobra-to-md v1.0.1 h1:WRinNoIRmwWUSJm+pSNXMjOrtU48oxXDZgeCYQ
github.com/openziti/cobra-to-md v1.0.1/go.mod h1:FjCpk/yzHF7/r28oSTNr5P57yN5VolpdAtS/g7KNi2c=
github.com/openziti/dilithium v0.3.5 h1:+envGNzxc3OyVPiuvtxivQmCsOjdZjtOMLpQBeMz7eM=
github.com/openziti/dilithium v0.3.5/go.mod h1:XONq1iK6te/WwNzkgZHfIDHordMPqb0hMwJ8bs9EfSk=
github.com/openziti/edge-api v0.26.32 h1:32oJI97cuM/kRJPEOwH2pe9dqwj56IYdQgTjTJaaHaU=
github.com/openziti/edge-api v0.26.32/go.mod h1:sYHVpm26Jr1u7VooNJzTb2b2nGSlmCHMnbGC8XfWSng=
github.com/openziti/edge-api v0.26.33 h1:EjR7D9O9zuZZqBYRD+X9iDkm5yIQ/G/tjIgnL8ioShE=
github.com/openziti/edge-api v0.26.33/go.mod h1:sYHVpm26Jr1u7VooNJzTb2b2nGSlmCHMnbGC8XfWSng=
github.com/openziti/foundation/v2 v2.0.49 h1:aQ5I/lMhkHQ6urhRpLwrWP+7YtoeUitCfY/wub+nOqo=
github.com/openziti/foundation/v2 v2.0.49/go.mod h1:tFk7wg5WE/nDDur5jSVQTROugKDXQkFvmqRSV4pvWp0=
github.com/openziti/identity v1.0.85 h1:jphDHrUCXCJGdbVTMBqsdtS0Ei/vhDH337DMNMYzLro=
Expand Down
Loading

0 comments on commit e9c8180

Please sign in to comment.