mirror of
https://github.com/openziti/ziti.git
synced 2026-10-26 13:39:58 +00:00
949de99ee4
* fixes #3809 support CSR submission during OIDC authentication - accepts an optional CSR during OIDC login (all auth methods) and signs it into a session-bound certificate with a SPIFFE ID derived from the identity and API session - returns the signed certificate PEM as a top-level "session_cert" field in the token endpoint JSON response (CodeExchange, RefreshToken, TokenExchange) - adds cert-binding verification on RefreshToken and TokenExchange: if z_cfs is present the peer cert fingerprint must match (strict), otherwise falls back to SPIFFE ID verification - supports cert rotation via csr_pem form parameter on refresh and token exchange; replaces the session cert fingerprint while preserving the authenticating cert fingerprint - adds AuthCertFingerprints (z_acfs) claim to track permanent auth cert fingerprints separately from rotatable session cert fingerprints - only the leaf certificate fingerprint is added to z_cfs and z_acfs, intermediates are never included - invalid CSR returns 400 Bad Request in OIDC error format - propagates updated CustomClaims (including CertFingerprints) from access token to renewed refresh token so rotated fingerprints are enforced on subsequent refreshes - adds CertGenerated field to ApiSessionEvent - advertises OIDC_AUTH_WITH_CSR controller capability when OIDC is enabled - adds unit tests for verifyCertBinding (fingerprint, SPIFFE ID, edge cases) and CsrPem field parsing - adds integration tests for initial CSR auth (updb, cert, ext-jwt), cert-binding on refresh/exchange, CSR rotation with cert auth, SPIFFE fallback and z_cfs transition, and CSR property forging resistance * address pr concerns * add z_cfs len tests on junk chain certs * strip csr subject info, replace w/ santized values * fix csr rotation rejection/paths during token exchange/refresh
284 lines
8.5 KiB
Go
284 lines
8.5 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 oidc_auth
|
|
|
|
import (
|
|
"crypto/sha1"
|
|
"crypto/x509"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/openziti/edge-api/rest_model"
|
|
"github.com/openziti/foundation/v2/stringz"
|
|
"github.com/openziti/ziti/v2/common"
|
|
"github.com/openziti/ziti/v2/controller/model"
|
|
|
|
"github.com/zitadel/oidc/v3/pkg/oidc"
|
|
)
|
|
|
|
// 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
|
|
IsTotpEnrolled bool
|
|
SecondaryTotpRequired bool
|
|
SecondaryExtJwtSigner *model.ExternalJwtSigner
|
|
ConfigTypes []string
|
|
Amr map[string]struct{}
|
|
|
|
PeerCerts []*x509.Certificate
|
|
RequestedMethod string
|
|
BearerTokenDetected bool
|
|
SdkInfo *rest_model.SdkInfo
|
|
EnvInfo *rest_model.EnvInfo
|
|
RemoteAddress string
|
|
IsCertExtendable bool
|
|
CertAllowExpired bool
|
|
IsCertExtendRequested bool
|
|
IsCertKeyRollRequested bool
|
|
AuthenticatorId string
|
|
ImproperClientCertChain bool
|
|
CsrPem string
|
|
}
|
|
|
|
// GetID returns an AuthRequest's ID and implements op.AuthRequest
|
|
func (a *AuthRequest) GetID() string {
|
|
return a.Id
|
|
}
|
|
|
|
// GetACR returns the authentication class reference provided by client and implements oidc.AuthRequest
|
|
// All ACRs are currently ignored.
|
|
func (a *AuthRequest) GetACR() string {
|
|
return ""
|
|
}
|
|
|
|
// GetAMR returns the authentication method references the authentication has undergone and implements op.AuthRequest
|
|
func (a *AuthRequest) GetAMR() []string {
|
|
result := make([]string, len(a.Amr))
|
|
i := 0
|
|
for k := range a.Amr {
|
|
result[i] = k
|
|
i = i + 1
|
|
}
|
|
return result
|
|
}
|
|
|
|
// HasFullAuth returns true if an authentication request has passed all primary and secondary authentications.
|
|
func (a *AuthRequest) HasFullAuth() bool {
|
|
return a.HasPrimaryAuth() && a.HasSecondaryAuth()
|
|
}
|
|
|
|
// HasPrimaryAuth returns true if a primary authentication mechanism has been passed.
|
|
func (a *AuthRequest) HasPrimaryAuth() bool {
|
|
return a.HasAmr(AuthMethodCert) || a.HasAmr(AuthMethodPassword) || a.HasAmr(AuthMethodExtJwt)
|
|
}
|
|
|
|
// HasSecondaryAuth returns true if all applicable secondary authentications have been passed
|
|
func (a *AuthRequest) HasSecondaryAuth() bool {
|
|
return (!a.SecondaryTotpRequired || a.HasAmr(AuthMethodSecondaryTotp)) &&
|
|
(a.SecondaryExtJwtSigner == nil || a.HasAmrExtJwtId(a.SecondaryExtJwtSigner.Id))
|
|
}
|
|
|
|
// HasAmr returns true if the supplied amr is present
|
|
func (a *AuthRequest) HasAmr(amr string) bool {
|
|
_, found := a.Amr[amr]
|
|
return found
|
|
}
|
|
|
|
func (a *AuthRequest) HasAmrExtJwtId(id string) bool {
|
|
return a.HasAmr(AuthMethodSecondaryExtJwt + ":" + id)
|
|
}
|
|
|
|
// AddAmrExtJwtId records that the external JWT signer with the given ID has been satisfied
|
|
// as a secondary authentication method by adding it to the AMR set in the format
|
|
// "<AuthMethodSecondaryExtJwt>:<id>".
|
|
func (a *AuthRequest) AddAmrExtJwtId(id string) {
|
|
amr := AuthMethodSecondaryExtJwt + ":" + id
|
|
a.AddAmr(amr)
|
|
}
|
|
|
|
// AddAmr adds the supplied amr
|
|
func (a *AuthRequest) AddAmr(amr string) {
|
|
if a.Amr == nil {
|
|
a.Amr = map[string]struct{}{}
|
|
}
|
|
a.Amr[amr] = struct{}{}
|
|
}
|
|
|
|
// GetAudience returns all current audience targets and implements op.AuthRequest
|
|
func (a *AuthRequest) GetAudience() []string {
|
|
return []string{a.ClientID}
|
|
}
|
|
|
|
// GetAuthTime returns the time at which authentication has occurred and implements op.AuthRequest
|
|
func (a *AuthRequest) GetAuthTime() time.Time {
|
|
return a.AuthTime
|
|
}
|
|
|
|
// GetClientID returns the client id requested and implements op.AuthRequest
|
|
func (a *AuthRequest) GetClientID() string {
|
|
return a.ClientID
|
|
}
|
|
|
|
// GetCodeChallenge returns the rp supplied code change and implements op.AuthRequest
|
|
func (a *AuthRequest) GetCodeChallenge() *oidc.CodeChallenge {
|
|
return &oidc.CodeChallenge{
|
|
Challenge: a.CodeChallenge,
|
|
Method: a.CodeChallengeMethod,
|
|
}
|
|
}
|
|
|
|
// GetNonce returns the rp supplied nonce and implements op.AuthRequest
|
|
func (a *AuthRequest) GetNonce() string {
|
|
return a.Nonce
|
|
}
|
|
|
|
// GetRedirectURI returns the rp supplied redirect target and implements op.AuthRequest
|
|
func (a *AuthRequest) GetRedirectURI() string {
|
|
return a.RedirectURI
|
|
}
|
|
|
|
// GetResponseType returns the rp supplied response type and implements op.AuthRequest
|
|
func (a *AuthRequest) GetResponseType() oidc.ResponseType {
|
|
return a.ResponseType
|
|
}
|
|
|
|
// GetResponseMode is not supported and all tokens are turned via query string and implements op.AuthRequest
|
|
func (a *AuthRequest) GetResponseMode() oidc.ResponseMode {
|
|
return ""
|
|
}
|
|
|
|
// GetScopes returns the current scopes and implements op.AuthRequest
|
|
func (a *AuthRequest) GetScopes() []string {
|
|
return a.Scopes
|
|
}
|
|
|
|
// GetState returns the rp provided state and implements op.AuthRequest
|
|
func (a *AuthRequest) GetState() string {
|
|
return a.State
|
|
}
|
|
|
|
// GetSubject returns the target subject and implements op.AuthRequest
|
|
func (a *AuthRequest) GetSubject() string {
|
|
return a.IdentityId
|
|
}
|
|
|
|
// Done returns true once authentication has been completed and implements op.AuthRequest
|
|
func (a *AuthRequest) Done() bool {
|
|
return a.HasFullAuth()
|
|
}
|
|
|
|
// GetCertFingerprint returns the SHA-1 fingerprint of the leaf peer certificate,
|
|
// or an empty string if no peer certificates are present. Only the leaf is
|
|
// returned so that z_cfs binds proof-of-possession to the auth cert and not to
|
|
// incidental intermediates the client may have presented on the TLS chain.
|
|
func (a *AuthRequest) GetCertFingerprint() string {
|
|
if len(a.PeerCerts) == 0 {
|
|
return ""
|
|
}
|
|
return fmt.Sprintf("%x", sha1.Sum(a.PeerCerts[0].Raw))
|
|
}
|
|
|
|
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,
|
|
IsTotpEnrolled: a.IsTotpEnrolled,
|
|
})
|
|
}
|
|
|
|
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 {
|
|
common.RefreshClaims
|
|
}
|
|
|
|
// GetAMR implements op.RefreshTokenRequest
|
|
func (r *RefreshTokenRequest) GetAMR() []string {
|
|
return r.AuthenticationMethodsReferences
|
|
}
|
|
|
|
// GetAudience implements op.RefreshTokenRequest
|
|
func (r *RefreshTokenRequest) GetAudience() []string {
|
|
return r.Audience
|
|
}
|
|
|
|
// GetAuthTime implements op.RefreshTokenRequest
|
|
func (r *RefreshTokenRequest) GetAuthTime() time.Time {
|
|
return r.AuthTime.AsTime()
|
|
}
|
|
|
|
// GetClientID implements op.RefreshTokenRequest
|
|
func (r *RefreshTokenRequest) GetClientID() string {
|
|
return r.ClientID
|
|
}
|
|
|
|
// GetScopes implements op.RefreshTokenRequest
|
|
func (r *RefreshTokenRequest) GetScopes() []string {
|
|
return r.Scopes
|
|
}
|
|
|
|
// GetSubject implements op.RefreshTokenRequest
|
|
func (r *RefreshTokenRequest) GetSubject() string {
|
|
return r.Subject
|
|
}
|
|
|
|
// SetCurrentScopes implements op.RefreshTokenRequest
|
|
func (r *RefreshTokenRequest) SetCurrentScopes(scopes []string) {
|
|
r.Scopes = scopes
|
|
}
|
|
|
|
func (r *RefreshTokenRequest) GetCertFingerprints() []string {
|
|
return r.CertFingerprints
|
|
}
|