diff --git a/Makefile b/Makefile index 524d57b2172d8..1e0ada2893e48 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,7 @@ # Naming convention: # for stable releases we use "1.0.0" format # for pre-releases, we use "1.0.0-beta.2" format -VERSION=2.0.0-rc.4 +VERSION=2.1.0-alpha.2 # These are standard autotools variables, don't change them please BUILDDIR ?= build diff --git a/constants.go b/constants.go index 74a4a0c18b5e5..ec7b73bc3e2d7 100644 --- a/constants.go +++ b/constants.go @@ -132,3 +132,8 @@ const ( // CertExtensionPermitPortForwarding allows user to request port forwarding CertExtensionPermitPortForwarding = "permit-port-forwarding" ) + +const ( + // NetIQ is an identity provider. + NetIQ = "netiq" +) diff --git a/lib/auth/auth.go b/lib/auth/auth.go index 9a5b01e06ab68..9e7218c341a47 100644 --- a/lib/auth/auth.go +++ b/lib/auth/auth.go @@ -749,9 +749,25 @@ func (s *AuthServer) CreateOIDCAuthRequest(req services.OIDCAuthRequest) (*servi if err != nil { return nil, trace.Wrap(err) } + // online is OIDC online scope, "select_account" forces user to always select account - redirectURL := oauthClient.AuthCodeURL(req.StateToken, "online", "select_account") - req.RedirectURL = redirectURL + req.RedirectURL = oauthClient.AuthCodeURL(req.StateToken, "online", "select_account") + + // if the connector has an Authentication Context Class Reference (ACR) value set, + // update redirect url and add it as a query value. + acrValue := connector.GetACR() + if acrValue != "" { + u, err := url.Parse(req.RedirectURL) + if err != nil { + return nil, trace.Wrap(err) + } + q := u.Query() + q.Set("acr_values", acrValue) + u.RawQuery = q.Encode() + req.RedirectURL = u.String() + } + + log.Debugf("[OIDC] Redirect URL: %v", req.RedirectURL) err = s.Identity.CreateOIDCAuthRequest(req, defaults.OIDCAuthRequestTTL) if err != nil { @@ -904,15 +920,16 @@ func claimsFromUserInfo(oidcClient *oidc.Client, issuerURL string, accessToken s } hc := oac.HttpClient() - // go get the provider config so we can find out where the UserInfo endpoint is + // go get the provider config so we can find out where the UserInfo endpoint + // is. if the provider doesn't offer a UserInfo endpoint return not found. pc, err := oidc.FetchProviderConfig(oac.HttpClient(), issuerURL) if err != nil { return nil, trace.Wrap(err) } - // If the provider doesn't offer a UserInfo endpoint don't err. if pc.UserInfoEndpoint == nil { - return nil, nil + return nil, trace.NotFound("UserInfo endpoint not found") } + endpoint := pc.UserInfoEndpoint.String() err = isHTTPS(endpoint) if err != nil { @@ -980,14 +997,13 @@ func (a *AuthServer) getClaims(oidcClient *oidc.Client, issuerURL string, code s userInfoClaims, err := claimsFromUserInfo(oidcClient, issuerURL, t.AccessToken) if err != nil { + if trace.IsNotFound(err) { + log.Debugf("[OIDC] Provider doesn't offer UserInfo endpoint. Returning token claims: %v", idTokenClaims) + return idTokenClaims, nil + } log.Debugf("[OIDC] Unable to fetch UserInfo claims: %v", err) return nil, trace.Wrap(err) } - if userInfoClaims == nil { - log.Warn("[OIDC] Provider doesn't offer UserInfo endpoint. Only token claims will be used.") - return idTokenClaims, nil - } - log.Debugf("[OIDC] UserInfo claims: %v", userInfoClaims) // make sure that the subject in the userinfo claim matches the subject in @@ -1018,6 +1034,56 @@ func (a *AuthServer) getClaims(oidcClient *oidc.Client, issuerURL string, code s return claims, nil } +// validateACRValues validates that we get an appropriate response for acr values. By default +// we expect the same value we send, but this function also handles Identity Provider specific +// forms of validation. +func (a *AuthServer) validateACRValues(acrValue string, identityProvider string, claims jose.Claims) error { + switch identityProvider { + case teleport.NetIQ: + log.Debugf("[OIDC] Validating ACR values with %q rules", identityProvider) + + tokenAcr, ok := claims["acr"] + if !ok { + return trace.BadParameter("acr claim does not exist") + } + tokenAcrMap, ok := tokenAcr.(map[string][]string) + if !ok { + return trace.BadParameter("acr unknown type: %T", tokenAcr) + } + tokenAcrValues, ok := tokenAcrMap["values"] + if !ok { + return trace.BadParameter("acr.values not found in claims") + } + acrValueMatched := false + for _, v := range tokenAcrValues { + if acrValue == v { + acrValueMatched = true + break + } + } + if !acrValueMatched { + log.Debugf("[OIDC] No ACR match found for %q in %q", acrValue, tokenAcrValues) + return trace.BadParameter("acr claim does not match") + } + default: + log.Debugf("[OIDC] Validating ACR values with default rules") + + claimValue, exists, err := claims.StringClaim("acr") + if !exists { + return trace.BadParameter("acr claim does not exist") + } + if err != nil { + return trace.Wrap(err) + } + if claimValue != acrValue { + log.Debugf("[OIDC] No ACR match found %q != %q", acrValue, claimValue) + return trace.BadParameter("acr claim does not match") + } + } + + return nil +} + // ValidateOIDCAuthCallback is called by the proxy to check OIDC query parameters // returned by OIDC Provider, if everything checks out, auth server // will respond with OIDCAuthResponse, otherwise it will return error @@ -1061,6 +1127,16 @@ func (a *AuthServer) ValidateOIDCAuthCallback(q url.Values) (*OIDCAuthResponse, } log.Debugf("[OIDC] Claims: %v", claims) + // if we are sending acr values, make sure we also validate them + acrValue := connector.GetACR() + if acrValue != "" { + err := a.validateACRValues(acrValue, connector.GetProvider(), claims) + if err != nil { + return nil, trace.Wrap(err) + } + log.Debugf("[OIDC] ACR values %q successfully validated", acrValue) + } + ident, err := oidc.IdentityFromClaims(claims) if err != nil { return nil, trace.OAuth2( diff --git a/lib/config/fileconf.go b/lib/config/fileconf.go index e67ba25191f35..4f9900685cef4 100644 --- a/lib/config/fileconf.go +++ b/lib/config/fileconf.go @@ -100,6 +100,8 @@ var ( "client_id": true, "client_secret": true, "redirect_url": true, + "acr_values": true, + "provider": true, "tokens": true, "region": true, "table_name": true, @@ -695,6 +697,11 @@ type OIDCConnector struct { // client's browser back to it after successfull authentication // Should match the URL on Provider's side RedirectURL string `yaml:"redirect_url"` + // ACR is the acr_values parameter to be sent with an authorization request. + ACR string `yaml:"acr_values,omitempty"` + // Provider is the identity provider we connect to. This field is + // only required if using acr_values. + Provider string `yaml:"provider,omitempty"` // Display controls how this connector is displayed Display string `yaml:"display"` // Scope is a list of additional scopes to request from OIDC @@ -736,6 +743,8 @@ func (o *OIDCConnector) Parse() (services.OIDCConnector, error) { ClaimsToRoles: mappings, } v2 := other.V2() + v2.SetACR(o.ACR) + v2.SetProvider(o.Provider) if err := v2.Check(); err != nil { return nil, trace.Wrap(err) } diff --git a/lib/services/oidc.go b/lib/services/oidc.go index d3d824320be7b..f4e0d273dfd6f 100644 --- a/lib/services/oidc.go +++ b/lib/services/oidc.go @@ -48,6 +48,10 @@ type OIDCConnector interface { // client's browser back to it after successfull authentication // Should match the URL on Provider's side GetRedirectURL() string + // GetACR returns the Authentication Context Class Reference (ACR) value. + GetACR() string + // GetProvider returns the identity provider. + GetProvider() string // Display - Friendly name for this provider. GetDisplay() string // Scope is additional scopes set by provder @@ -70,6 +74,10 @@ type OIDCConnector interface { SetIssuerURL(string) // SetRedirectURL sets RedirectURL SetRedirectURL(string) + // SetACR sets the Authentication Context Class Reference (ACR) value. + SetACR(string) + // SetProvider sets the identity provider. + SetProvider(string) // SetScope sets additional scopes set by provider SetScope([]string) // SetClaimsToRoles sets dynamic mapping from claims to roles @@ -257,6 +265,16 @@ func (o *OIDCConnectorV2) SetRedirectURL(redirectURL string) { o.Spec.RedirectURL = redirectURL } +// SetACR sets the Authentication Context Class Reference (ACR) value. +func (o *OIDCConnectorV2) SetACR(acrValue string) { + o.Spec.ACR = acrValue +} + +// SetProvider sets the identity provider. +func (o *OIDCConnectorV2) SetProvider(identityProvider string) { + o.Spec.Provider = identityProvider +} + // SetScope sets additional scopes set by provider func (o *OIDCConnectorV2) SetScope(scope []string) { o.Spec.Scope = scope @@ -300,6 +318,16 @@ func (o *OIDCConnectorV2) GetRedirectURL() string { return o.Spec.RedirectURL } +// GetACR returns the Authentication Context Class Reference (ACR) value. +func (o *OIDCConnectorV2) GetACR() string { + return o.Spec.ACR +} + +// GetProvider returns the identity provider. +func (o *OIDCConnectorV2) GetProvider() string { + return o.Spec.Provider +} + // Display - Friendly name for this provider. func (o *OIDCConnectorV2) GetDisplay() string { if o.Spec.Display != "" { @@ -497,6 +525,11 @@ type OIDCConnectorSpecV2 struct { // client's browser back to it after successfull authentication // Should match the URL on Provider's side RedirectURL string `json:"redirect_url"` + // ACR is an Authentication Context Class Reference value. The meaning of the ACR + // value is context-specific and varies for identity providers. + ACR string `json:"acr_values,omitempty"` + // Provider is the external identity provider. + Provider string `json:"provider,omitempty"` // Display - Friendly name for this provider. Display string `json:"display,omitempty"` // Scope is additional scopes set by provder @@ -515,6 +548,8 @@ var OIDCConnectorSpecV2Schema = fmt.Sprintf(`{ "client_id": {"type": "string"}, "client_secret": {"type": "string"}, "redirect_url": {"type": "string"}, + "acr_values": {"type": "string"}, + "provider": {"type": "string"}, "display": {"type": "string"}, "scope": { "type": "array", diff --git a/version.go b/version.go index b83340a065c5e..b4e0112b85c6c 100644 --- a/version.go +++ b/version.go @@ -3,7 +3,7 @@ package teleport const ( - Version = "2.0.0-rc.4" + Version = "2.1.0-alpha.2" ) // Gitref variable is automatically set to the output of git-describe