/* * Copyright 2017-2020 Kopano and its licensors * * 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 * * http://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 identifier import ( "errors" "fmt" "net/http" "net/url" "time" "github.com/crewjam/saml" "github.com/libregraph/oidc-go" "github.com/longsleep/rndm" "github.com/sirupsen/logrus" "github.com/libregraph/lico/identity/authorities" konnectoidc "github.com/libregraph/lico/oidc" "github.com/libregraph/lico/identity/authorities/samlext" "github.com/libregraph/lico/utils" ) func (i *Identifier) writeSAML2Start(rw http.ResponseWriter, req *http.Request, authority *authorities.Details) { var err error if authority == nil { err = konnectoidc.NewOAuth2Error(oidc.ErrorCodeOAuth2TemporarilyUnavailable, "no authority") } else if !authority.IsReady() { err = konnectoidc.NewOAuth2Error(oidc.ErrorCodeOAuth2TemporarilyUnavailable, "authority not ready") } switch typedErr := err.(type) { case nil: // breaks case *konnectoidc.OAuth2Error: // Redirect back, with error. i.logger.WithFields(utils.ErrorAsFields(err)).Debugln("saml2 start error") // NOTE(longsleep): Pass along error ID but not the description to avoid // leaking potentially internal information to our RP. uri, _ := url.Parse(i.authorizationEndpointURI.String()) query, _ := url.ParseQuery(req.URL.RawQuery) query.Del("flow") query.Set("error", typedErr.ErrorID) query.Set("error_description", "identifier failed to authenticate") uri.RawQuery = query.Encode() utils.WriteRedirect(rw, http.StatusFound, uri, nil, false) return default: i.logger.WithError(err).Errorln("identifier failed to process saml2 start") i.ErrorPage(rw, http.StatusInternalServerError, "", "saml2 start failed") return } sd := &StateData{ State: rndm.GenerateRandomString(32), RawQuery: req.URL.RawQuery, Ref: authority.ID, } uri, extra, err := authority.MakeRedirectAuthenticationRequestURL(sd.State) if err != nil { i.logger.WithError(err).Errorln("identifier failed to create authentication request: %w", err) i.ErrorPage(rw, http.StatusInternalServerError, "", "saml2 start failed") return } sd.Extra = extra // Set cookie which is consumed by the callback later. err = i.SetStateToStateCookie(req.Context(), rw, "saml2/acs", sd) if err != nil { i.logger.WithError(err).Debugln("identifier failed to set saml2 state cookie") i.ErrorPage(rw, http.StatusInternalServerError, "", "failed to set cookie") return } utils.WriteRedirect(rw, http.StatusFound, uri, nil, false) } func (i *Identifier) writeSAML2AssertionConsumerService(rw http.ResponseWriter, req *http.Request) { var err error var sd *StateData var user *IdentifiedUser var authority *authorities.Details for { sd, err = i.GetStateFromStateCookie(req.Context(), rw, req, "saml2/acs", req.Form.Get("RelayState")) if err != nil { err = fmt.Errorf("failed to decode saml2 acs state: %v", err) break } if sd == nil { err = errors.New("state not found") break } // Load authority with client_id in state. authority, _ = i.authorities.Lookup(req.Context(), sd.Ref) if authority == nil { i.logger.Debugln("identifier failed to find authority in saml2 acs") err = konnectoidc.NewOAuth2Error(oidc.ErrorCodeOAuth2InvalidRequest, "unknown client_id") break } if authority.AuthorityType != authorities.AuthorityTypeSAML2 { err = errors.New("unknown authority type") break } // Parse incoming state response. var assertion *saml.Assertion if assertionRaw, parseErr := authority.ParseStateResponse(req, sd.State, sd.Extra); parseErr == nil { assertion = assertionRaw.(*saml.Assertion) } else { err = parseErr break } // Lookup username and user. un, claims, claimsErr := authority.IdentityClaimValue(assertion) if claimsErr != nil { i.logger.WithError(claimsErr).Debugln("identifier failed to get username from saml2 acs assertion") err = konnectoidc.NewOAuth2Error(oidc.ErrorCodeOAuth2InsufficientScope, "identity claim not found") break } username := &un // TODO(longsleep): This flow currently does not provide a hello // context, means that downwards a backend might fail to resolve the // user when it requires additional information for multiple backend // routing. user, err = i.resolveUser(req.Context(), *username) if err != nil { i.logger.WithError(err).WithField("username", *username).Debugln("identifier failed to resolve saml2 acs user with backend") // TODO(longsleep): Break on validation error. err = konnectoidc.NewOAuth2Error(oidc.ErrorCodeOAuth2AccessDenied, "failed to resolve user") break } if user == nil || user.Subject() == "" { err = konnectoidc.NewOAuth2Error(oidc.ErrorCodeOAuth2AccessDenied, "no such user") break } // Apply additional authority claims. if sessionNotOnOrAfter, ok := claims["SessionNotOnOrAfter"]; ok { user.expiresAfter = sessionNotOnOrAfter.(*time.Time) } var logonRef string if nameIDTransient, ok := claims["TransientNameID"]; ok { logonRef = "transient:" + nameIDTransient.(string) } else if nameIDPersistent, ok := claims["PersistentNameID"]; ok { logonRef = "persistent:" + nameIDPersistent.(string) } else if nameIDUnspecified, ok := claims["UnspecifiedNameID"]; ok { logonRef = "unspecified:" + nameIDUnspecified.(string) } if logonRef != "" { user.logonRef = &logonRef } if authority.Trusted { // Use external authority session, if the external authority is trusted. if sessionIndexString, ok := claims["SessionIndex"]; ok { sessionIndex := sessionIndexString.(string) user.sessionRef = &sessionIndex } } // Get user meta data. // TODO(longsleep): This is an additional request to the backend. This // should be avoided. Best would be if the backend would return everything // in one shot (TODO in core). err = i.updateUser(req.Context(), user, authority) if err != nil { i.logger.WithError(err).Debugln("identifier failed to get user data in saml2 acs request") err = konnectoidc.NewOAuth2Error(oidc.ErrorCodeOAuth2AccessDenied, "failed to get user data") break } // Set logon time. user.logonAt = time.Now() err = i.SetUserToLogonCookie(req.Context(), rw, user) if err != nil { i.logger.WithError(err).Errorln("identifier failed to serialize logon ticket in saml2 acs") i.ErrorPage(rw, http.StatusInternalServerError, "", "failed to serialize logon ticket") return } break } if sd == nil { i.logger.WithError(err).Debugln("identifier saml2 acs without state") i.ErrorPage(rw, http.StatusBadRequest, "", "state not found") return } uri, _ := url.Parse(i.authorizationEndpointURI.String()) query, _ := url.ParseQuery(sd.RawQuery) query.Del("flow") query.Set("identifier", MustBeSignedIn) query.Set("prompt", oidc.PromptNone) switch typedErr := err.(type) { case nil: // breaks case *saml.InvalidResponseError: i.logger.WithError(err).WithFields(logrus.Fields{ "reason": typedErr.PrivateErr, }).Debugf("saml2 acs invalid response") query.Set("error", oidc.ErrorCodeOAuth2AccessDenied) query.Set("error_description", "identifier received invalid response") // breaks case *konnectoidc.OAuth2Error: // Pass along OAuth2 error. i.logger.WithFields(utils.ErrorAsFields(err)).Debugln("saml2 acs error") // NOTE(longsleep): Pass along error ID but not the description to avoid // leaking potetially internal information to our RP. query.Set("error", typedErr.ErrorID) query.Set("error_description", "identifier failed to authenticate") //breaks default: i.logger.WithError(err).Errorln("identifier failed to process saml2 acs") i.ErrorPage(rw, http.StatusInternalServerError, "", "saml2 acs failed") return } uri.RawQuery = query.Encode() utils.WriteRedirect(rw, http.StatusFound, uri, nil, false) } func (i *Identifier) writeSAMLSingleLogoutServiceRequest(rw http.ResponseWriter, req *http.Request) { lor, err := samlext.NewIdpLogoutRequest(req) if err != nil { i.logger.WithError(err).Debugln("identifier failed to process saml2 slo request") i.ErrorPage(rw, http.StatusBadRequest, "", "failed to parse request") return } err = lor.Validate() if err != nil { i.logger.WithError(err).Debugln("identifier saml2 slo request validation failed") i.ErrorPage(rw, http.StatusBadRequest, "", "slo request validation failed") return } // In http://docs.oasis-open.org/security/saml/v2.0/saml-bindings-2.0-os.pdf ยง3.4.5.2 // we get a description of the Destination attribute: // // If the message is signed, the Destination XML attribute in the root SAML // element of the protocol message MUST contain the URL to which the sender // has instructed the user agent to deliver the message. The recipient MUST // then verify that the value matches the location at which the message has // been received. // // We require the destination be correct either (a) if signing is enabled or // (b) if it was provided. mustHaveDestination := lor.SigAlg != nil mustHaveDestination = mustHaveDestination || lor.Request.Destination != "" if mustHaveDestination { uri, _ := i.absoluteURLForRoute("saml2/slo") if lor.Request.Destination != uri.String() { i.logger.WithField("destination", lor.Request.Destination).Debugln("identifier saml2 slo request with wrong desitation") i.ErrorPage(rw, http.StatusBadRequest, "", "slo request destination wrong") return } } // Find matching authority. authority, found := i.authorities.Find(req.Context(), func(authority authorities.AuthorityRegistration) bool { if authority.AuthorityType() != authorities.AuthorityTypeSAML2 { return false } if lor.Request.Issuer.Value == authority.Issuer() { return true } return false }) if !found { i.logger.WithField("issuer", lor.Request.Issuer.Value).Debugln("identifier saml2 slo request from unknown issuer") i.ErrorPage(rw, http.StatusBadRequest, "", "slo request issuer unknown") return } authorityDetails := authority.Authority() if lor.SigAlg == nil { // Never consider trusted if not signed. authorityDetails.Trusted = false } if authorityDetails.AuthorityType != authorities.AuthorityTypeSAML2 { i.logger.WithField("issuer", lor.Request.Issuer.Value).Debugln("identifier saml2 slo request for unknown authority type") i.ErrorPage(rw, http.StatusBadRequest, "", "slo request issuer authority type unknown") return } // Validate. validated, err := authority.ValidateIdpEndSessionRequest(lor, lor.RelayState) if err != nil { i.logger.WithError(err).WithField("issuer", authority.Issuer()).Debugln("identifier saml2 slo request authority validation failed") i.ErrorPage(rw, http.StatusBadRequest, "", "slo request authority validation failed") return } if !validated && authorityDetails.Trusted { // Never consider unvalidated logout requests as trusted. authorityDetails.Trusted = false } user, _ := i.GetUserFromLogonCookie(req.Context(), req, 0, false) if user != nil { // Compare signed in SAML SessionIndex with the on provided in the LogoutRequest. if user.SessionRef() != nil { if lor.Request.SessionIndex == nil { i.logger.Debugln("identifier saml2 slo request without session index") i.ErrorPage(rw, http.StatusBadRequest, "", "slo request missing session index") return } if lor.Request.SessionIndex.Value != *user.SessionRef() { i.logger.Debugln("identifier saml2 slo request for other session index") i.ErrorPage(rw, http.StatusBadRequest, "", "slo request session index mismatch") return } } if authorityDetails != nil && authorityDetails.Trusted { // Directly clear identifier session when a trusted authority requests it. err = i.UnsetLogonCookie(req.Context(), user, rw) if err != nil { i.logger.WithError(err).Errorln("identifier saml2 slo failed to unset logon cookie") i.ErrorPage(rw, http.StatusInternalServerError, "", "saml2 slo logout failed") return } } } else { // Ignore when not signed in, for end session. } if authorityDetails == nil || !authorityDetails.Trusted { // Handle directly by redirecting to our logout confirm url for untrusted // registies or when no URL was set. uri, _ := i.absoluteURLForRoute("goodbye") query := &url.Values{} uri.RawQuery = query.Encode() utils.WriteRedirect(rw, http.StatusFound, uri, nil, false) return } uri, _, err := authorityDetails.MakeRedirectEndSessionResponseURL(lor.Request, lor.RelayState) if err != nil { i.logger.WithError(err).Errorln("failed to make saml2 slo redirect request url") i.ErrorPage(rw, http.StatusInternalServerError, "", "saml2 slo failed") return } if uri == nil { i.logger.Warnln("saml2 slo reached dead end, no post logout redirect uri available") // Fall back to logout confirm url. uri, _ = i.absoluteURLForRoute("goodbye") } utils.WriteRedirect(rw, http.StatusFound, uri, nil, false) } func (i *Identifier) writeSAMLSingleLogoutServiceResponse(rw http.ResponseWriter, req *http.Request) { lor, err := samlext.NewIdpLogoutResponse(req) if err != nil { i.logger.WithError(err).Debugln("identifier failed to process saml2 slo response") i.ErrorPage(rw, http.StatusBadRequest, "", "failed to parse response") return } err = lor.Validate() if err != nil { i.logger.WithError(err).Debugln("identifier saml2 slo response validation failed") i.ErrorPage(rw, http.StatusBadRequest, "", "response validation failed") return } sd, err := i.GetStateFromStateCookie(req.Context(), rw, req, "_/saml2/slo", lor.RelayState) if err != nil { i.logger.WithError(err).Debugln("identifier saml2 slo response failed to load state") i.ErrorPage(rw, http.StatusBadRequest, "", "response state invalid") return } if sd == nil { i.logger.WithError(err).Debugln("identifier saml2 slo response failed as state is missing") i.ErrorPage(rw, http.StatusBadRequest, "", "response state missing") return } authority, found := i.authorities.Get(req.Context(), sd.Ref) if !found { i.ErrorPage(rw, http.StatusBadRequest, "", "no authority") return } authorityDetails := authority.Authority() if lor.SigAlg == nil { // Never consider trusted if not signed. authorityDetails.Trusted = false } if authorityDetails.AuthorityType != authorities.AuthorityTypeSAML2 { i.logger.WithField("issuer", authority.Issuer()).Debugln("identifier saml2 slo response for unknown authority type") i.ErrorPage(rw, http.StatusBadRequest, "", "slo response issuer authority type unknown") return } // Validate. validated, err := authority.ValidateIdpEndSessionResponse(lor, lor.RelayState) if err != nil { i.logger.WithError(err).WithField("issuer", authority.Issuer()).Debugln("identifier saml2 slo response authority validation failed") i.ErrorPage(rw, http.StatusBadRequest, "", "slo response authority validation failed") return } if !validated && authorityDetails.Trusted { // Never consider unvalidated logout responses as trusted. authorityDetails.Trusted = false } if lor.Response.Status.StatusCode.Value != saml.StatusSuccess { i.logger.WithField("status", lor.Response.Status.StatusCode).Debugln("saml2 slo response without success status") } // Extract destination URI from state data (its put into the RawQuery field). uri, err := url.Parse(sd.RawQuery) if err != nil { i.logger.WithError(err).Errorln("failed to parse slo response redirect url from state data") i.ErrorPage(rw, http.StatusInternalServerError, "", "saml2 slo response failed") return } if uri == nil || uri.String() == "" { i.logger.Warnln("saml2 slo reached dead end, no post logout redirect uri available") // Fall back to our signed out url or goodbye route. if i.Config.SignedOutEndpointURI != nil { uri = i.Config.SignedOutEndpointURI } else { uri, _ = i.absoluteURLForRoute("goodbye") } } if sd.State != "" { query := uri.Query() query.Set("state", sd.State) uri.RawQuery = query.Encode() } utils.WriteRedirect(rw, http.StatusFound, uri, nil, false) }