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

fixes #2354 adds oidc AuthQuery support #2430

Merged
merged 6 commits into from
Oct 4, 2024
Merged
Show file tree
Hide file tree
Changes from 4 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
25 changes: 17 additions & 8 deletions controller/env/appenv.go
Original file line number Diff line number Diff line change
Expand Up @@ -608,12 +608,22 @@ 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: "EXT-JWT",
Provider: &provider,
}
}

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

Expand All @@ -638,12 +648,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 @@ -855,7 +864,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
65 changes: 54 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,44 @@ 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: "TOTP",
})
}

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,
TypeID: a.SecondaryExtJwtSigner.Id,
})
}

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 @@ -223,7 +223,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
10 changes: 5 additions & 5 deletions 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.3
github.com/openziti/cobra-to-md v1.0.1
github.com/openziti/edge-api v0.26.30
github.com/openziti/edge-api v0.26.31
github.com/openziti/foundation/v2 v2.0.49
github.com/openziti/identity v1.0.85
github.com/openziti/jwks v1.0.5
Expand Down Expand Up @@ -185,11 +185,11 @@ require (
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
go.mongodb.org/mongo-driver v1.16.1 // indirect
go.mongodb.org/mongo-driver v1.17.0 // indirect
go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352 // indirect
go.opentelemetry.io/otel v1.29.0 // indirect
go.opentelemetry.io/otel/metric v1.29.0 // indirect
go.opentelemetry.io/otel/trace v1.29.0 // indirect
go.opentelemetry.io/otel v1.30.0 // indirect
go.opentelemetry.io/otel/metric v1.30.0 // indirect
go.opentelemetry.io/otel/trace v1.30.0 // indirect
go.uber.org/atomic v1.9.0 // indirect
go.uber.org/multierr v1.9.0 // indirect
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect
Expand Down
Loading
Loading