Files
ziti/controller/model/authenticator.go
Andrew Martinez fb2034245d fixes openziti/ziti#3356 adds www-authenticate headers (#3561)
* fixes openziti/ziti#3356 adds www-authenticate headers

- www-authenticate headers are returned on 401s from API requests
- www-authenticate headers are returned during authentication to signal
  addtional JWT bearer tokens needed (secondary ext jwt)
- adds support for additional headers on API errors
- adds SecurityTokenCtx for centralized security header processing
  (legacy, jwt, etc.)
- adds SecurityCtx for centralized identity, auth policy, MFA handling
- refactors existing JWT authentication methods (oidc, legacy) to use
  centralized processing where possible
2026-02-24 10:01:36 -05:00

330 lines
8.7 KiB
Go

/*
Copyright NetFoundry Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package model
import (
"crypto/x509"
"encoding/json"
"fmt"
"net/http"
"time"
"github.com/go-viper/mapstructure/v2"
"github.com/michaelquigley/pfxlog"
"github.com/openziti/ziti/v2/common"
"github.com/openziti/ziti/v2/controller/change"
"github.com/openziti/ziti/v2/controller/event"
)
type AuthResult interface {
AuthenticatorId() string
SessionCerts() []*x509.Certificate
Identity() *Identity
Authenticator() *Authenticator
AuthPolicy() *AuthPolicy
IsSuccessful() bool
ImproperClientCertChain() bool
AdditionalHeaders() map[string][]string
}
type AuthProcessor interface {
CanHandle(method string) bool
Process(context AuthContext) (AuthResult, error)
}
type AuthRegistry interface {
Add(method AuthProcessor)
GetByMethod(method string) AuthProcessor
}
type AuthProcessorRegistryImpl struct {
processors []AuthProcessor
}
func (registry *AuthProcessorRegistryImpl) Add(processor AuthProcessor) {
registry.processors = append(registry.processors, processor)
}
func (registry *AuthProcessorRegistryImpl) GetByMethod(method string) AuthProcessor {
for _, processor := range registry.processors {
if processor.CanHandle(method) {
return processor
}
}
return nil
}
type AuthContext interface {
GetMethod() string
GetData() map[string]interface{}
GetCerts() []*x509.Certificate
GetHeaders() Headers
GetChangeContext() *change.Context
GetRemoteAddr() string
GetEnvInfo() *EnvInfo
GetSdkInfo() *SdkInfo
// GetPrimaryIdentity returns the current in context identity, which should be nil for primary and filled for secondary
GetPrimaryIdentity() *Identity
// SetPrimaryIdentity sets the identity already verified by a primary authentication method, used during secondary methods
SetPrimaryIdentity(*Identity)
// GetSecurityTokenCtx returns the security token context associated with this request
GetSecurityTokenCtx() *common.SecurityTokenCtx
// SetSecurityTokenCtx sets the security token context associated with this request
SetSecurityTokenCtx(ctx *common.SecurityTokenCtx)
}
type AuthContextHttp struct {
Method string
Data map[string]interface{}
Certs []*x509.Certificate
Headers Headers
ChangeContext *change.Context
PrimaryIdentity *Identity
RemoteAddr string
SdkInfo *SdkInfo
EnvInfo *EnvInfo
SecurityTokenCtx *common.SecurityTokenCtx
}
func (context *AuthContextHttp) GetSecurityTokenCtx() *common.SecurityTokenCtx {
return context.SecurityTokenCtx
}
func (context *AuthContextHttp) SetSecurityTokenCtx(ctx *common.SecurityTokenCtx) {
context.SecurityTokenCtx = ctx
}
func (context *AuthContextHttp) GetEnvInfo() *EnvInfo {
return context.EnvInfo
}
func (context *AuthContextHttp) GetSdkInfo() *SdkInfo {
return context.SdkInfo
}
func NewAuthContextHttp(request *http.Request, method string, data interface{}, ctx *change.Context) AuthContext {
//TODO: this is a giant hack to not deal w/ removing the AuthContext layer
sigh, _ := json.Marshal(data)
mapData := map[string]interface{}{}
_ = json.Unmarshal(sigh, &mapData)
headers := Headers{}
for h, v := range request.Header {
headers.Set(h, v)
}
sdkInfo, envInfo, err := parseSdkEnvInfo(mapData)
if err != nil {
pfxlog.Logger().WithError(err).Error("unable to parse sdk and env info, continuing with authentication processing but sdk and env info will not be updated")
}
return &AuthContextHttp{
Method: method,
Data: mapData,
Certs: request.TLS.PeerCertificates,
Headers: headers,
ChangeContext: ctx,
RemoteAddr: request.RemoteAddr,
SdkInfo: sdkInfo,
EnvInfo: envInfo,
}
}
func parseSdkEnvInfo(data map[string]any) (*SdkInfo, *EnvInfo, error) {
var sdkInfo *SdkInfo
var envInfo *EnvInfo
if envInfoInterface := data["envInfo"]; envInfoInterface != nil {
if envInfoMap := envInfoInterface.(map[string]interface{}); envInfoMap != nil {
if err := mapstructure.Decode(envInfoMap, &envInfo); err != nil {
return nil, nil, fmt.Errorf("could not decode key [envInfo] of type %T as %T", envInfoMap, envInfo)
}
}
}
if sdkInfoInterface := data["sdkInfo"]; sdkInfoInterface != nil {
if sdkInfoMap := sdkInfoInterface.(map[string]interface{}); sdkInfoMap != nil {
if err := mapstructure.Decode(sdkInfoMap, &sdkInfo); err != nil {
return nil, nil, fmt.Errorf("could not decode key [sdkInfo] of type %T as %T", sdkInfoMap, sdkInfo)
}
}
}
return sdkInfo, envInfo, nil
}
func (context *AuthContextHttp) GetMethod() string {
return context.Method
}
func (context *AuthContextHttp) GetData() map[string]interface{} {
return context.Data
}
func (context *AuthContextHttp) GetHeaders() Headers {
return context.Headers
}
func (context *AuthContextHttp) GetCerts() []*x509.Certificate {
return context.Certs
}
func (context *AuthContextHttp) GetChangeContext() *change.Context {
return context.ChangeContext
}
func (context *AuthContextHttp) GetPrimaryIdentity() *Identity { return context.PrimaryIdentity }
func (context *AuthContextHttp) SetPrimaryIdentity(primaryIdentity *Identity) {
context.PrimaryIdentity = primaryIdentity
}
func (context *AuthContextHttp) GetRemoteAddr() string {
return context.RemoteAddr
}
func (context *AuthContextHttp) SetRemoteAddr(addr string) {
context.RemoteAddr = addr
}
var _ AuthResult = &AuthResultBase{}
type AuthResultBase struct {
identity *Identity
authenticatorId string
authenticator *Authenticator
sessionCerts []*x509.Certificate
authPolicy *AuthPolicy
improperClientCertChain bool
env Env
headers map[string][]string
}
func (a *AuthResultBase) AdditionalHeaders() map[string][]string {
return a.headers
}
func (a *AuthResultBase) AuthenticatorId() string {
return a.authenticatorId
}
func (a *AuthResultBase) SessionCerts() []*x509.Certificate {
return a.sessionCerts
}
func (a *AuthResultBase) Identity() *Identity {
return a.identity
}
func (a *AuthResultBase) Authenticator() *Authenticator {
if a.authenticator == nil {
a.authenticator, _ = a.env.GetManagers().Authenticator.Read(a.authenticatorId)
}
return a.authenticator
}
func (a *AuthResultBase) ImproperClientCertChain() bool {
return a.improperClientCertChain
}
func (a *AuthResultBase) AuthPolicy() *AuthPolicy {
return a.authPolicy
}
func (a *AuthResultBase) IsSuccessful() bool {
return a.identity != nil
}
type AuthBundle struct {
Authenticator *Authenticator
Identity *Identity
AuthPolicy *AuthPolicy
TokenIssuer common.TokenIssuer
ImproperClientCertChain bool
}
func (a *AuthBundle) Apply(event *event.AuthenticationEvent) {
if a.Authenticator != nil {
event.AuthenticatorId = a.Authenticator.Id
// set in scenarios where the identity is not fetched explicitly
event.IdentityId = a.Authenticator.IdentityId
}
if a.Identity != nil {
event.IdentityId = a.Identity.Id
}
if a.AuthPolicy != nil {
event.AuthPolicyId = a.AuthPolicy.Id
}
if a.TokenIssuer != nil {
event.ExternalJwtSignerId = a.TokenIssuer.Id()
}
event.ImproperClientCertChain = a.ImproperClientCertChain
}
type BaseAuthenticator struct {
method string
env Env
}
func (a *BaseAuthenticator) NewAuthEventFailure(authCtx AuthContext, bundle *AuthBundle, reason string) *event.AuthenticationEvent {
result := &event.AuthenticationEvent{
Namespace: event.AuthenticationEventNS,
EventSrcId: a.env.GetId(),
Timestamp: time.Now(),
EventType: event.AuthenticationEventTypeFail,
Method: a.method,
FailureReason: reason,
RemoteAddress: authCtx.GetRemoteAddr(),
}
bundle.Apply(result)
return result
}
func (a *BaseAuthenticator) NewAuthEventSuccess(authCtx AuthContext, bundle *AuthBundle) *event.AuthenticationEvent {
result := &event.AuthenticationEvent{
Namespace: event.AuthenticationEventNS,
EventSrcId: a.env.GetId(),
Timestamp: time.Now(),
EventType: event.AuthenticationEventTypeSuccess,
Method: a.method,
RemoteAddress: authCtx.GetRemoteAddr(),
}
bundle.Apply(result)
return result
}
func (a *BaseAuthenticator) DispatchEvent(event *event.AuthenticationEvent) {
a.env.GetEventDispatcher().AcceptAuthenticationEvent(event)
}