diff --git a/controller/api/apis.go b/controller/api/apis.go index fe5c6ea6d..6aa2ebbb1 100644 --- a/controller/api/apis.go +++ b/controller/api/apis.go @@ -239,8 +239,15 @@ type RESTFedAuthData struct { MasterToken string `json:"master_token"` } +// Used to generate redirect request for integration like SAML or OIDC. type RESTTokenRedirect struct { + // The NeuVector URL to redirect after authentication/logout. Redirect string `json:"redirect_endpoint"` + // (Optional) + // When absent, the redirect url will be used as issuer in SAML request. + // When it is specified, the value here will be used as the issuer. + // This is for Single Logout where redirect url and issue can be different. + Issuer string `json:"issuer"` } type RESTToken struct { @@ -346,6 +353,12 @@ type RESTServerSAML struct { DefaultRole string `json:"default_role"` RoleGroups map[string][]string `json:"role_groups,omitempty"` // role -> groups GroupMappedRoles []*share.GroupRoleMapping `json:"group_mapped_roles,omitempty"` // group -> (role -> domains) + + AuthnSigningEnabled bool `json:"authn_signing_enabled,omitempty"` // Optional. Enable signing AuthnRequest. Default off. + SigningCert string `json:"signing_cert,omitempty"` // Optional. + //SigningKey string `json:"signing_key,omitempty"` // Optional. + SLOEnabled bool `json:"slo_enabled,omitempty"` // Optional. + SLOURL string `json:"slo_url,omitempty"` // Optional. } type RESTServerOIDC struct { @@ -420,6 +433,12 @@ type RESTServerSAMLConfig struct { RoleGroups *map[string][]string `json:"role_groups,omitempty"` // role -> groups. deprecated since 4.2 GroupMappedRoles *[]*share.GroupRoleMapping `json:"group_mapped_roles,omitempty"` // group -> (role -> domains) X509CertExtra *[]string `json:"x509_cert_extra,omitempty"` + + AuthnSigningEnabled *bool `json:"authn_signing_enabled,omitempty"` // Optional. Enable signing AuthnRequest. Default off. + SigningCert *string `json:"signing_cert,omitempty"` // Optional. + SigningKey *string `json:"signing_key,omitempty"` // Optional. + SLOEnabled *bool `json:"slo_enabled,omitempty"` // Optional. + SLOURL *string `json:"slo_url,omitempty"` // Optional. } type RESTServerSAMLConfigCfgMap struct { @@ -1043,14 +1062,14 @@ type RESTConversationEndpointConfigData struct { } type RESTConversationReportEntry struct { - Bytes uint64 `json:"bytes"` - Sessions uint32 `json:"sessions"` - Port string `json:"port,omitempty"` - Application string `json:"application,omitempty"` - PolicyAction string `json:"policy_action"` - CIP string `json:"client_ip,omitempty"` - SIP string `json:"server_ip,omitempty"` - FQDN string `json:"fqdn,omitempty"` + Bytes uint64 `json:"bytes"` + Sessions uint32 `json:"sessions"` + Port string `json:"port,omitempty"` + Application string `json:"application,omitempty"` + PolicyAction string `json:"policy_action"` + CIP string `json:"client_ip,omitempty"` + SIP string `json:"server_ip,omitempty"` + FQDN string `json:"fqdn,omitempty"` } type RESTConversationReport struct { diff --git a/controller/api/apis.yaml b/controller/api/apis.yaml index ce96704d5..610d97f1b 100644 --- a/controller/api/apis.yaml +++ b/controller/api/apis.yaml @@ -9704,6 +9704,15 @@ definitions: type: array items: $ref: '#/definitions/RESTX509CertInfo' + slo_enabled: + type: boolean + example: true + slo_url: + type: string + example: https://dev.oktapreview.com/app/examplesamlapp_1/exjlpo0/slo/saml + signing_cert: + type: string + example: E7B0OS/N3KMVCL6KNMZ2+LOV90S7854NSD84P0BF RESTServer: type: object required: @@ -9837,6 +9846,18 @@ definitions: items: type: string example: ["E7B0OS/N3KMVCL6KNMZ2+LOV90S7854NSD84P0BF", "E7B0OS/N3KMVCL6KNMZ2+LOV90S7854NSD84P0BF"] + slo_enabled: + type: boolean + example: true + slo_url: + type: string + example: https://dev.oktapreview.com/app/examplesamlapp_1/exjlpo0/slo/saml + signing_cert: + type: string + example: E7B0OS/N3KMVCL6KNMZ2+LOV90S7854NSD84P0BF + signing_key: + type: string + example: E7B0OS/N3KMVCL6KNMZ2+LOV90S7854NSD84P0BF RESTServerConfig: type: object required: diff --git a/controller/api/internal_apis.yaml b/controller/api/internal_apis.yaml new file mode 100644 index 000000000..bdd032707 --- /dev/null +++ b/controller/api/internal_apis.yaml @@ -0,0 +1,155 @@ +swagger: '2.0' + +################################################################################ +# Internal API Information # +################################################################################ +info: + description: Secure Docker and Kubernetes based container deployments with the NeuVector run-time security solution. + title: NeuVector Internal API + version: '1.0' +schemes: + - https + +################################################################################ +# Tags # +################################################################################ +tags: + - name: Authentication + description: Authenticates login or logout + +################################################################################ +# Paths # +################################################################################ +paths: + /v1/token_auth_server/{server}: + get: + summary: Generate login request for integration, e.g., OIDC or SAML. + tags: + - Authentication + parameters: + - in: body + name: body + description: OIDC/SAML login data + required: true + schema: + $ref: '#/definitions/RESTGenerateServerLoginRequest' + produces: + - application/json + responses: + '200': + description: Success + schema: + $ref: '#/definitions/RESTGenerateServerLoginResponse' + '400': + description: Bad request + schema: + $ref: '#/definitions/RESTError' + post: + summary: Create login token via integration, e.g., OIDC or SAML. + tags: + - Authentication + parameters: + - in: body + name: body + description: OIDC/SAML login data + required: true + schema: + $ref: '#/definitions/RESTGenerateServerLoginRequest' + produces: + - application/json + responses: + '200': + description: Success + schema: + $ref: '#/definitions/RESTGenerateServerLoginResponse' + '400': + description: Bad request + schema: + $ref: '#/definitions/RESTError' + /v1/token_auth_server/{server}/slo: + get: + summary: Create redirect url for Single Signout request. Currently only SAML is supported. + tags: + - Authentication + security: + - TokenAuth: [] + parameters: + - in: body + name: body + description: OIDC/SAML logout data + required: true + schema: + $ref: '#/definitions/RESTGenerateServerLogoutRequest' + produces: + - application/json + responses: + '200': + description: Success + schema: + $ref: '#/definitions/RESTGenerateServerLogoutResponse' + '400': + description: Bad request + schema: + $ref: '#/definitions/RESTError' + '401': + description: Unauthorized + schema: + $ref: '#/definitions/RESTError' +################################################################################ +# Definitions # +################################################################################ +definitions: + RESTGenerateServerLoginRequest: + required: + - 'redirect_endpoint' + type: object + properties: + redirect_endpoint: + description: 'The URL used in redirect request, e.g., SAML Authn request.' + type: string + example: 'https:///token_auth_server' + issuer: + description: 'The issuer of the login request. When absent, redirect_endpoint will be used.' + type: string + example: 'https:///token_auth_server' + RESTGenerateServerLoginResponse: + required: + - 'redirect_endpoint' + type: object + properties: + redirect: + type: object + properties: + redirect_url: + description: 'The URL to be used by browser to make redirect request.' + type: string + example: 'https://login.microsoftonline.com/xxx/saml2?SAMLRequest=lFLLbt...' + server_name: + description: 'The server resource name used to generate this redirect request' + type: string + example: 'saml1' + server_type: + description: 'Type of this redirect request.' + type: string + enum: [oidc, saml] + example: 'saml' + RESTGenerateServerLogoutRequest: + $ref: '#/definitions/RESTGenerateServerLoginRequest' + RESTGenerateServerLogoutResponse: + $ref: '#/definitions/RESTGenerateServerLoginResponse' + RESTError: + type: object + required: + - code + - error + - message + properties: + code: + type: integer + example: 6 + error: + type: string + example: 'Request in wrong format' + message: + type: string + example: 'Get redirect URL request error' diff --git a/controller/rest/auth.go b/controller/rest/auth.go index b54bfef51..79d936205 100644 --- a/controller/rest/auth.go +++ b/controller/rest/auth.go @@ -35,6 +35,8 @@ type loginSession struct { mainSessionUser string // From master token claim, i.e. master cluster login's fullname. Empty otherwise token string fullname string + nameid string // Used by SAML Single Logout + sessionIndex string // Used by SAML Single Logout domain string server string timeout uint32 @@ -61,6 +63,8 @@ type tokenClaim struct { MainSessionUser string `json:"main_session_user"` // from fullname in master login token's claim. empty when the token is generated for local cluster login Timeout uint32 `json:"timeout"` Roles access.DomainRole `json:"roles"` + NameID string `json:"nameId,omitempty"` // Used by SAML Single Logout + SessionIndex string `json:"sessionIndex,omitempty"` // Used by SAML Single Logout jwt.StandardClaims } @@ -83,6 +87,12 @@ type tRancherUser struct { domainRoles access.DomainRole } +// Extra information of Single Sign-On session +type SsoSession struct { + SAMLNameID string + SAMLSessionIndex string +} + var errTokenExpired error = errors.New("token expired") var recordFedAuthSessions bool = false // set to true for testing: handlerDumpAuthData var loginFedSessions map[string]utils.Set = make(map[string]utils.Set) // for testing: key is mainSessionID, value is a set of regular tokens @@ -180,6 +190,8 @@ func newLoginSessionFromToken(token string, claims *tokenClaim, now time.Time) ( lastAt: now, eolAt: time.Unix(claims.ExpiresAt, 0), domainRoles: claims.Roles, + nameid: claims.NameID, + sessionIndex: claims.SessionIndex, } if rc := _registerLoginSession(s); rc != userOK { updateFedLoginSession(s) @@ -189,10 +201,10 @@ func newLoginSessionFromToken(token string, claims *tokenClaim, now time.Time) ( } } -func newLoginSessionFromUser(user *share.CLUSUser, roles access.DomainRole, remote, mainSessionID, mainSessionUser string) (*loginSession, int) { +func newLoginSessionFromUser(user *share.CLUSUser, roles access.DomainRole, remote, mainSessionID, mainSessionUser string, sso *SsoSession) (*loginSession, int) { // Note: JWT keys should be loaded in initJWTSignKey() before calling this function. - id, token, claims := jwtGenerateToken(user, roles, remote, mainSessionID, mainSessionUser) + id, token, claims := jwtGenerateToken(user, roles, remote, mainSessionID, mainSessionUser, sso) now := user.LastLoginAt // Already updated s := &loginSession{ id: id, @@ -212,6 +224,10 @@ func newLoginSessionFromUser(user *share.CLUSUser, roles access.DomainRole, remo if mainSessionID != _interactiveSessionID && !strings.HasPrefix(mainSessionID, _rancherSessionPrefix) { s.timeout = s.timeout * 8 } + if sso != nil { + s.nameid = sso.SAMLNameID + s.sessionIndex = sso.SAMLSessionIndex + } var rc int userMutex.Lock() @@ -895,7 +911,7 @@ func lookupShadowUser(server, username, userid, email, role string, roleDomains return newUser, true } -func loginUser(user *share.CLUSUser, masterRoles access.DomainRole, remote, mainSessionID, mainSessionUser, fedRole string) (*loginSession, int) { +func loginUser(user *share.CLUSUser, masterRoles access.DomainRole, remote, mainSessionID, mainSessionUser, fedRole string, sso *SsoSession) (*loginSession, int) { // 1. When a cluster is promoted to master cluster, the default admin user is automatically assigned fedAdmin role // 2. When a master cluster is demoted, users with fed role(fedAdmin) are downgraded to with admin/reader role // 3. On master cluster, only users with fedAdmin role can assign fed roles to other users @@ -921,7 +937,7 @@ func loginUser(user *share.CLUSUser, masterRoles access.DomainRole, remote, main roles[access.AccessDomainGlobal] = user.Role } - return newLoginSessionFromUser(user, roles, remote, mainSessionID, mainSessionUser) + return newLoginSessionFromUser(user, roles, remote, mainSessionID, mainSessionUser, sso) } func compareUserWithLogin(user *share.CLUSUser, login *loginSession) bool { @@ -1311,7 +1327,7 @@ func jwtValidateFedJoinTicket(encryptedTicket, secret string) error { return validateEncryptedData(encryptedTicket, secret, true) } -func jwtGenerateToken(user *share.CLUSUser, roles access.DomainRole, remote, mainSessionID, mainSessionUser string) (string, string, *tokenClaim) { +func jwtGenerateToken(user *share.CLUSUser, roles access.DomainRole, remote, mainSessionID, mainSessionUser string, sso *SsoSession) (string, string, *tokenClaim) { id := utils.GetRandomID(idLength, "") installID := GetInstallationID() now := time.Now() @@ -1333,6 +1349,10 @@ func jwtGenerateToken(user *share.CLUSUser, roles access.DomainRole, remote, mai ExpiresAt: now.Add(jwtTokenLife).Unix(), }, } + if sso != nil { + c.NameID = sso.SAMLNameID + c.SessionIndex = sso.SAMLSessionIndex + } if r, ok := roles[access.AccessDomainGlobal]; ok && (r == api.UserRoleIBMSA || r == api.UserRoleImportStatus) && len(roles) == 1 { if r == api.UserRoleIBMSA { c.Timeout = uint32(30 * 60) // jwtIbmSaTokenLife, 30 minutes @@ -2220,7 +2240,7 @@ func handlerAuthLogin(w http.ResponseWriter, r *http.Request, ps httprouter.Para rc = userInvalidRequest } else { // Login user accounting - login, rc = loginUser(user, nil, remote, mainSessionID, "", fedRole) + login, rc = loginUser(user, nil, remote, mainSessionID, "", fedRole, nil) } if rc != userOK { if rc == userTimeout { @@ -2333,7 +2353,7 @@ func handlerFedAuthLogin(w http.ResponseWriter, r *http.Request, ps httprouter.P } // Login user accounting - login, rc := loginUser(user, claims.Roles, remote, claims.MainSessionID, claims.MainSessionUser, api.FedRoleJoint) + login, rc := loginUser(user, claims.Roles, remote, claims.MainSessionID, claims.MainSessionUser, api.FedRoleJoint, nil) if rc != userOK { log.WithFields(log.Fields{"user": auth.JointUsername, "rc": rc}).Error("Fed master login failed") authLog(share.CLUSEvAuthLoginFailed, auth.JointUsername, remote, "", nil, "") @@ -2396,6 +2416,7 @@ func handlerAuthLoginServer(w http.ResponseWriter, r *http.Request, ps httproute var user *share.CLUSUser var defaultPW bool var localAuthed bool + var sso SsoSession if data.Password != nil { if len(data.Password.Password) == 0 { @@ -2464,7 +2485,7 @@ func handlerAuthLoginServer(w http.ResponseWriter, r *http.Request, ps httproute } if cs.SAML != nil { - attrs, err := remoteAuther.SAMLSPAuth(cs.SAML, data.Token) + nameid, sessionIndex, attrs, err := remoteAuther.SAMLSPAuth(cs.SAML, data.Token) if err != nil || attrs == nil { log.WithFields(log.Fields{"server": server, "error": err}).Error("User login failed") authLog(share.CLUSEvAuthLoginFailed, "", remote, "", nil, "") @@ -2487,6 +2508,8 @@ func handlerAuthLoginServer(w http.ResponseWriter, r *http.Request, ps httproute restRespError(w, http.StatusUnauthorized, api.RESTErrUnauthorized) return } + sso.SAMLNameID = nameid + sso.SAMLSessionIndex = sessionIndex } else if cs.OIDC != nil { claims, err := remoteAuther.OIDCAuth(cs.OIDC, data.Token) if err != nil || claims == nil { @@ -2520,7 +2543,7 @@ func handlerAuthLoginServer(w http.ResponseWriter, r *http.Request, ps httproute fedRole, _ := cacher.GetFedMembershipRole(accReadAll) // Login user accounting - login, rc := loginUser(user, nil, remote, _interactiveSessionID, "", fedRole) + login, rc := loginUser(user, nil, remote, _interactiveSessionID, "", fedRole, &sso) if rc != userOK { if rc == userTimeout { log.WithFields(log.Fields{"user": user.Fullname}).Error("User login timeout") @@ -2586,7 +2609,7 @@ func handlerAuthLogout(w http.ResponseWriter, r *http.Request, ps httprouter.Par fedRole, err := cacher.GetFedMembershipRole(acc) if err == nil && fedRole == api.FedRoleMaster { ids := cacher.GetFedJoinedClusterIdMap(acc) - for id, _ := range ids { + for id := range ids { joinedCluster := cacher.GetFedJoinedCluster(id, acc) if joinedCluster.ID != "" { var token string diff --git a/controller/rest/auth_test.go b/controller/rest/auth_test.go index dbc96396e..56992090f 100644 --- a/controller/rest/auth_test.go +++ b/controller/rest/auth_test.go @@ -61,11 +61,11 @@ func (a *mockRemoteAuth) LDAPAuth(ldap *share.CLUSServerLDAP, username, password } } -func (a *mockRemoteAuth) SAMLSPAuth(csaml *share.CLUSServerSAML, tokenData *api.RESTAuthToken) (map[string][]string, error) { +func (a *mockRemoteAuth) SAMLSPAuth(csaml *share.CLUSServerSAML, tokenData *api.RESTAuthToken) (string, string, map[string][]string, error) { if user, ok := a.samlUsers[tokenData.Token]; ok { - return user.attrs, nil + return "nameID", "sessionIndex", user.attrs, nil } else { - return nil, errors.New("Authentication failed") + return "", "", nil, errors.New("Authentication failed") } } @@ -1017,7 +1017,7 @@ func TestJWTSignValidate(t *testing.T) { } remote := "10.1.2.3" - _, tokenString, _ := jwtGenerateToken(user, roles, remote, "", "") + _, tokenString, _ := jwtGenerateToken(user, roles, remote, "", "", nil) token, _ := jwtValidateToken(tokenString, "", nil) if token.Fullname != user.Fullname { diff --git a/controller/rest/configmap.go b/controller/rest/configmap.go index 4fbffa571..cf24124b2 100644 --- a/controller/rest/configmap.go +++ b/controller/rest/configmap.go @@ -204,7 +204,7 @@ func handleoidccfg(yaml_data []byte, load bool, skip *bool, context *configMapHa } if remoteAuther == nil { - remoteAuther = auth.NewRemoteAuther() + remoteAuther = auth.NewRemoteAuther(nil) } cs, _, _ := clusHelper.GetServerRev(name, accAdmin) diff --git a/controller/rest/ibmsa.go b/controller/rest/ibmsa.go index 6fa5ab4eb..6c81e145c 100644 --- a/controller/rest/ibmsa.go +++ b/controller/rest/ibmsa.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" "io/ioutil" + //"math/rand" "mime" "net/http" @@ -340,7 +341,7 @@ func handlerGetIBMSAEpSetupToken(w http.ResponseWriter, r *http.Request, ps http if i := strings.Index(remote, ":"); i > 0 { remote = remote[:i] } - if s, rc := loginUser(user, nil, remote, _interactiveSessionID, "", api.FedRoleNone); rc == userOK { + if s, rc := loginUser(user, nil, remote, _interactiveSessionID, "", api.FedRoleNone, nil); rc == userOK { resp := api.RESTIBMSASetupToken{ AccessToken: s.token, } diff --git a/controller/rest/mock_test.go b/controller/rest/mock_test.go index 20fab5449..296b5d424 100644 --- a/controller/rest/mock_test.go +++ b/controller/rest/mock_test.go @@ -486,7 +486,7 @@ func mockLoginUser(name, role, fedRole string, roleDomains map[string][]string) RoleDomains: roleDomains, } - login, _ := loginUser(user, nil, "", _interactiveSessionID, "", fedRole) + login, _ := loginUser(user, nil, "", _interactiveSessionID, "", fedRole, nil) return login } @@ -546,6 +546,7 @@ func initTest() { router.PATCH("/v1/server/:name/group/:group", handlerServerGroupRoleDomainsConfig) // For 4.3(+) router.PATCH("/v1/server/:name/groups", handlerServerGroupsOrderConfig) // For 4.3(+) router.DELETE("/v1/server/:name", handlerServerDelete) + router.GET("/v1/token_auth_server/:server/slo", handlerGenerateSLORequest) router.POST("/v1/scan/registry", handlerRegistryCreate) router.PATCH("/v1/scan/registry/:name", handlerRegistryConfig) @@ -743,6 +744,18 @@ func loginServerToken(token, server string) *mockResponseWriter { return w } +func loginServerGetSLORedirectURL(token, server string) *mockResponseWriter { + w := new(mockResponseWriter) + data := api.RESTTokenRedirect{ + Redirect: "https://localhost/samlslo", + } + body, _ := json.Marshal(data) + r, _ := http.NewRequest("GET", "/v1/token_auth_server/"+server+"/slo", bytes.NewBuffer(body)) + r.Header.Add("X-Auth-Token", token) + router.ServeHTTP(w, r) + return w +} + func logout(token string) *mockResponseWriter { w := new(mockResponseWriter) r, _ := http.NewRequest("DELETE", "/v1/auth", bytes.NewBuffer([]byte{})) diff --git a/controller/rest/rest.go b/controller/rest/rest.go index 0d8cb98b2..d8fdf7bdb 100644 --- a/controller/rest/rest.go +++ b/controller/rest/rest.go @@ -1385,7 +1385,7 @@ func InitContext(ctx *Context) { auditQueue = ctx.AuditQueue messenger = ctx.Messenger - remoteAuther = auth.NewRemoteAuther() + remoteAuther = auth.NewRemoteAuther(nil) clusHelper = kv.GetClusterHelper() cfgHelper = kv.GetConfigHelper() @@ -1446,9 +1446,10 @@ func StartRESTServer() { //r.POST("/v1/password_profile", handlerPwdProfileCreate) r.PATCH("/v1/password_profile/:name", handlerPwdProfileConfig) //r.DELETE("/v1/password_profile/:name", handlerPwdProfileDelete) - r.GET("/v1/token_auth_server", handlerTokenAuthServerList) // Skip API document - r.GET("/v1/token_auth_server/:server", handlerTokenAuthServerRequest) // Skip API document - r.POST("/v1/token_auth_server/:server", handlerTokenAuthServerRequest) // Skip API document + r.GET("/v1/token_auth_server", handlerTokenAuthServerList) // Skip API document + r.GET("/v1/token_auth_server/:server", handlerTokenAuthServerRequest) + r.POST("/v1/token_auth_server/:server", handlerTokenAuthServerRequest) + r.GET("/v1/token_auth_server/:server/slo", handlerGenerateSLORequest) r.GET("/v1/server", handlerServerList) r.GET("/v1/server/:name", handlerServerShow) r.GET("/v1/server/:name/user", handlerServerUserList) diff --git a/controller/rest/server.go b/controller/rest/server.go index b78bf7bba..f94a8042a 100644 --- a/controller/rest/server.go +++ b/controller/rest/server.go @@ -2,10 +2,10 @@ package rest import ( "crypto/rsa" + "crypto/tls" "crypto/x509" "encoding/json" "encoding/pem" - "errors" "fmt" "io/ioutil" "net/http" @@ -13,6 +13,8 @@ import ( "sort" "strings" + "github.com/pkg/errors" + "github.com/julienschmidt/httprouter" log "github.com/sirupsen/logrus" @@ -100,13 +102,18 @@ func server2REST(cs *share.CLUSServer) *api.RESTServer { rs.Type = api.ServerTypeSAML rs.SAML = &api.RESTServerSAML{ - SSOURL: cs.SAML.SSOURL, - Issuer: cs.SAML.Issuer, - X509Cert: cs.SAML.X509Cert, - GroupClaim: cs.SAML.GroupClaim, - Enable: cs.Enable, - DefaultRole: cs.SAML.DefaultRole, - GroupMappedRoles: cs.SAML.GroupMappedRoles, + SSOURL: cs.SAML.SSOURL, + Issuer: cs.SAML.Issuer, + X509Cert: cs.SAML.X509Cert, + GroupClaim: cs.SAML.GroupClaim, + Enable: cs.Enable, + DefaultRole: cs.SAML.DefaultRole, + GroupMappedRoles: cs.SAML.GroupMappedRoles, + AuthnSigningEnabled: cs.SAML.AuthnSigningEnabled, + SigningCert: cs.SAML.SigningCert, + //SigningKey: cs.SAML.SigningKey, + SLOEnabled: cs.SAML.SLOEnabled, + SLOURL: cs.SAML.SLOURL, } rs.SAML.X509Certs = parseX509CertInfo(cs.SAML) @@ -132,23 +139,23 @@ func server2REST(cs *share.CLUSServer) *api.RESTServer { return nil } -func parseX509CertInfo(csaml *share.CLUSServerSAML)[]api.RESTX509CertInfo { +func parseX509CertInfo(csaml *share.CLUSServerSAML) []api.RESTX509CertInfo { certsInfo := make([]api.RESTX509CertInfo, 0) var certs []string certs = append(certs, csaml.X509Cert) certs = append(certs, csaml.X509CertExtra...) - for _,c := range certs { + for _, c := range certs { block, _ := pem.Decode([]byte(c)) if block != nil { cert, err := x509.ParseCertificate(block.Bytes) if err == nil { - oneCert := api.RESTX509CertInfo { - X509Cert: c, - IssuerCommonName: cert.Issuer.CommonName, + oneCert := api.RESTX509CertInfo{ + X509Cert: c, + IssuerCommonName: cert.Issuer.CommonName, SubjectCommonName: cert.Subject.CommonName, - ValidityNotAfter: uint64(cert.NotAfter.UTC().Unix()), + ValidityNotAfter: uint64(cert.NotAfter.UTC().Unix()), } certsInfo = append(certsInfo, oneCert) @@ -390,9 +397,11 @@ func handlerTokenAuthServerRequest(w http.ResponseWriter, r *http.Request, ps ht } if cs.SAML != nil && cs.Enable { + // Redirect url is used to generate Issuer in AuthnRequest. + // The string is generated by manager depending on how user connects to NeuVector, so we have to take this argument. log.WithFields(log.Fields{"redirect": data.Redirect}).Debug() - if url, err := remoteAuther.SAMLSPGetRedirectURL(cs.SAML, &data); err != nil { + if url, err := remoteAuther.SAMLSPGetRedirectURL(cs.SAML, &data, nil); err != nil { log.WithFields(log.Fields{"server": name, "error": err}).Error("Failed to get redirect URL") restRespError(w, http.StatusBadRequest, api.RESTErrInvalidRequest) } else { @@ -418,6 +427,61 @@ func handlerTokenAuthServerRequest(w http.ResponseWriter, r *http.Request, ps ht } } +func handlerGenerateSLORequest(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { + log.WithFields(log.Fields{"URL": r.URL.String()}).Debug() + defer r.Body.Close() + + var resp api.RESTTokenAuthServersRedirectData + var url string + var err error + + acc, login := getAccessControl(w, r, "") + if acc == nil { + return + } + + // Read body + var data api.RESTTokenRedirect + err = json.NewDecoder(r.Body).Decode(&data) + if err != nil { + e := "Get redirect URL request error" + log.WithFields(log.Fields{"error": err, "redirect": data.Redirect}).Error(e) + restRespErrorMessage(w, http.StatusBadRequest, api.RESTErrInvalidRequest, e) + return + } + + // Handle SAML SLO + if login.nameid == "" { + log.Debug("This user has no nameid associated. Do not generate SAML SLO request.") + restRespSuccess(w, r, &resp, acc, login, nil, "") + return + } + cs, _, err := clusHelper.GetServerRev(login.server, access.NewReaderAccessControl()) + if err != nil { + log.WithError(err).Warn("failed to get saml server info") + restRespError(w, http.StatusBadRequest, api.RESTErrInvalidRequest) + return + } + + if !cs.SAML.SLOEnabled { + log.Debug("SAML SLO is not enabled.") + restRespSuccess(w, r, &resp, acc, login, nil, "") + return + } + + remoteAuth := auth.NewRemoteAuther(nil) + if url, err = remoteAuth.SAMLSPGetLogoutURL(cs.SAML, &data, login.nameid, login.sessionIndex, nil); err != nil { + log.WithError(err).Warn("failed to generate saml logout url") + restRespError(w, http.StatusBadRequest, api.RESTErrInvalidRequest) + return + } + + log.WithField("url", url).Debug("SAML SLO request generated") + resp.Redirect = &api.RESTTokenAuthServerRedirect{Name: login.server, Type: api.ServerTypeSAML, RedirectURL: url} + restRespSuccess(w, r, &resp, nil, nil, nil, "") + return +} + func handlerTokenAuthServerList(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { log.WithFields(log.Fields{"URL": r.URL.String()}).Debug() defer r.Body.Close() @@ -534,7 +598,7 @@ func validateAuthServer(cas *share.CLUSServerAuth) error { if !access.IsValidRole(mappedRoles.GlobalRole, access.CONST_VISIBLE_USER_ROLE) { return fmt.Errorf("Invalid global role(%s) in group(%s) mapping", mappedRoles.GlobalRole, mappedRoles.Group) } - for role, _ := range mappedRoles.RoleDomains { + for role := range mappedRoles.RoleDomains { if !access.IsValidRole(role, access.CONST_VISIBLE_DOMAIN_ROLE) { return fmt.Errorf("Invalid domain role(%s) in group(%s) mapping", role, mappedRoles.Group) } @@ -678,9 +742,20 @@ func validateSAMLServer(cs *share.CLUSServer) error { if _, err := url.Parse(csaml.SSOURL); err != nil { return errors.New("Invalid SAML Single-sign-on URL format") } + if csaml.SLOURL != "" { + if _, err := url.Parse(csaml.SLOURL); err != nil { + return errors.New("Invalid SAML Single-sign-on URL format") + } + } + + if len(csaml.SigningCert) > 0 || len(csaml.SigningKey) > 0 { + if _, err := tls.X509KeyPair([]byte(csaml.SigningCert), []byte(csaml.SigningKey)); err != nil { + return errors.Wrap(err, "invalid key cert pair") + } + } var certs []string - certs = append(certs, csaml.X509Cert) // original one + certs = append(certs, csaml.X509Cert) // original one certs = append(certs, csaml.X509CertExtra...) for _, c := range certs { @@ -743,6 +818,26 @@ func updateSAMLServer(cs *share.CLUSServer, saml *api.RESTServerSAMLConfig, acc csaml.X509CertExtra = append(csaml.X509CertExtra, c) } } + if saml.AuthnSigningEnabled != nil { + csaml.AuthnSigningEnabled = *saml.AuthnSigningEnabled + } + + if saml.SigningCert != nil && saml.SigningKey != nil { + // Reject invalid certs. Its caller is expected to return 400 Bad Request + _, err := tls.X509KeyPair([]byte(*saml.SigningCert), []byte(*saml.SigningKey)) + if err != nil { + return errors.New("failed to parse key pair") + } + csaml.SigningKey = *saml.SigningKey + csaml.SigningCert = *saml.SigningCert + } + + if saml.SLOEnabled != nil { + csaml.SLOEnabled = *saml.SLOEnabled + } + if saml.SLOURL != nil { + csaml.SLOURL = *saml.SLOURL + } var err error var groupRoleMappings []*share.GroupRoleMapping @@ -1541,8 +1636,10 @@ func handlerServerGroupRoleDomainsConfig(w http.ResponseWriter, r *http.Request, // parameter 'groupRoleMappings': the server's group role mapping data // parameter 'groups': provided by caller for requesting which groups should be moved forward (with restrictions, see below) -// [*] if any group in groupRoleMappings has fedAdmin/fedReader as the mapped role for global domain, it can be moved only if the caller is fedAdmin role! -// besides, the sorting must follow the following rules +// +// [*] if any group in groupRoleMappings has fedAdmin/fedReader as the mapped role for global domain, it can be moved only if the caller is fedAdmin role! +// besides, the sorting must follow the following rules +// // The priority of sorting rules: ("requested groups" means the 'groups' parameter from caller) // 1. requested groups from 'groups' list(in order) that have fedAdmin/fedReader role mapped for global domain // 2. unrequested groups from 'groupRoleMappings' list(in order) that have fedAdmin/fedReader role mapped for global domain @@ -1550,12 +1647,12 @@ func handlerServerGroupRoleDomainsConfig(w http.ResponseWriter, r *http.Request, // 4. unrequested groups from 'groupRoleMappings' list(in order) that do not have fedAdmin/fedReader role mapped for global domain // // error is returned for any of the following cases: -// 1. duplicate group in 'groups' parameter -// 2. a group in 'groups' parameter is not configured to have any role mapping yet -// 3. non-fedAdmin user tries to move any group that has fedAdmin/fedReader mapped role for global domain -// Q: how to detect non-fedAdmin user tries to move any group that have fedAdmin/fedReader mapped role for global domain? -// A: 1. we sort groupRoleMappings based on the above 4 rules and get a new 'sortedList' -// 2. we compare 'groupRoleMappings' with 'sortedList' to see whether any group that has fedAdmin/fedReader mapped role for global domain is moved +// 1. duplicate group in 'groups' parameter +// 2. a group in 'groups' parameter is not configured to have any role mapping yet +// 3. non-fedAdmin user tries to move any group that has fedAdmin/fedReader mapped role for global domain +// Q: how to detect non-fedAdmin user tries to move any group that have fedAdmin/fedReader mapped role for global domain? +// A: 1. we sort groupRoleMappings based on the above 4 rules and get a new 'sortedList' +// 2. we compare 'groupRoleMappings' with 'sortedList' to see whether any group that has fedAdmin/fedReader mapped role for global domain is moved func sortGroupRoleMappings(groups []string, groupRoleMappings []*share.GroupRoleMapping, acc *access.AccessControl) ([]*share.GroupRoleMapping, error) { groupRoleMappingsMap := make(map[string]*share.GroupRoleMapping, len(groupRoleMappings)) sortedList := make([]*share.GroupRoleMapping, 0, len(groupRoleMappings)) diff --git a/controller/rest/system.go b/controller/rest/system.go index ba4a3976e..b933c2f6a 100644 --- a/controller/rest/system.go +++ b/controller/rest/system.go @@ -2214,7 +2214,7 @@ func _importHandler(w http.ResponseWriter, r *http.Request, tid, importType, tem Server: login.server, } domainRoles := access.DomainRole{access.AccessDomainGlobal: api.UserRoleImportStatus} - _, tempToken, _ = jwtGenerateToken(user, domainRoles, login.remote, login.mainSessionID, "") + _, tempToken, _ = jwtGenerateToken(user, domainRoles, login.remote, login.mainSessionID, "", nil) } importTask.TotalLines = lines diff --git a/go.mod b/go.mod index 9f726c2ee..77377c8f4 100644 --- a/go.mod +++ b/go.mod @@ -11,6 +11,7 @@ replace ( github.com/golang/protobuf => github.com/golang/protobuf v1.3.3 github.com/kubernetes/cri-api => k8s.io/cri-api v0.22.3 github.com/opencontainers/runc => github.com/opencontainers/runc v1.0.0-rc5 + github.com/russellhaering/gosaml2 => github.com/holyspectral/gosaml2 v0.0.0-20231003195827-3d916621a704 golang.org/x/net => golang.org/x/net v0.0.0-20200822124328-c89045814202 google.golang.org/genproto => google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55 google.golang.org/grpc => google.golang.org/grpc v1.30.1 @@ -44,7 +45,7 @@ replace ( require ( github.com/aws/aws-sdk-go v1.42.22 - github.com/beevik/etree v1.1.0 + github.com/beevik/etree v1.2.0 github.com/cenk/hub v1.0.1 // indirect github.com/cenkalti/hub v1.0.1 // indirect github.com/cenkalti/rpc2 v0.0.0-20210604223624-c1acbc6ec984 @@ -61,28 +62,33 @@ require ( github.com/glenn-brown/golang-pkg-pcre v0.0.0-20120522223659-48bb82a8b8ce github.com/gogo/googleapis v1.4.1 // indirect github.com/golang/protobuf v1.5.2 + github.com/google/go-cmp v0.5.9 // indirect github.com/google/uuid v1.3.0 github.com/hashicorp/go-cleanhttp v0.5.2 github.com/hashicorp/go-rootcerts v1.0.2 github.com/hashicorp/go-version v1.4.0 github.com/hashicorp/serf v0.9.7 + github.com/jonboulle/clockwork v0.3.0 github.com/julienschmidt/httprouter v1.3.0 github.com/knqyf263/go-rpmdb v0.0.0-20220209103220-0f7a6d951a6d + github.com/kr/pretty v0.3.1 // indirect github.com/mitchellh/mapstructure v1.4.3 github.com/neuvector/k8s v1.2.1-0.20220214174348-d0b3f377461e - github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d + github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d // indirect github.com/opencontainers/go-digest v1.0.0 github.com/opencontainers/runtime-spec v1.0.3-0.20210326190908-1c3f411f0417 github.com/pkg/errors v0.9.1 github.com/pquerna/cachecontrol v0.0.0-20171018203845-0dec1b30a021 - github.com/russellhaering/goxmldsig v1.1.1 + github.com/russellhaering/gosaml2 v0.0.0-00010101000000-000000000000 + github.com/russellhaering/goxmldsig v1.4.0 github.com/samalba/dockerclient v0.0.0-20160531175551-a30362618471 github.com/sirupsen/logrus v1.8.1 github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72 github.com/streadway/simpleuuid v0.0.0-20130420165545-6617b501e485 - github.com/stretchr/testify v1.7.0 + github.com/stretchr/testify v1.7.1 github.com/vishvananda/netlink v1.1.0 github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74 + golang.org/x/crypto v0.0.0-20220128200615-198e4374d7ed // indirect golang.org/x/net v0.0.0-20220722155237-a158d28d115b golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f golang.org/x/sys v0.0.0-20220209214540-3681064d5158 diff --git a/go.sum b/go.sum index f08427c0b..6273780b1 100644 --- a/go.sum +++ b/go.sum @@ -107,6 +107,8 @@ github.com/aws/aws-sdk-go v1.42.22 h1:EwcM7/+Ytg6xK+jbeM2+f9OELHqPiEiEKetT/GgAr7 github.com/aws/aws-sdk-go v1.42.22/go.mod h1:585smgzpB/KqRA+K3y/NL/oYRqQvpNJYvLm+LY1U59Q= github.com/beevik/etree v1.1.0 h1:T0xke/WvNtMoCqgzPhkX2r4rjY3GDZFi+FjpRZY2Jbs= github.com/beevik/etree v1.1.0/go.mod h1:r8Aw8JqVegEf0w2fDnATrX9VpkMcyFeM0FhwO62wh+A= +github.com/beevik/etree v1.2.0 h1:l7WETslUG/T+xOPs47dtd6jov2Ii/8/OjCldk5fYfQw= +github.com/beevik/etree v1.2.0/go.mod h1:aiPf89g/1k3AShMVAzriilpcE4R/Vuor90y83zVZWFc= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= @@ -394,6 +396,8 @@ github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.1.0 h1:Hsa8mG0dQ46ij8Sl2AYJDUv1oA9/d6Vk+3LG99Oe02g= github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -483,6 +487,8 @@ github.com/hashicorp/serf v0.9.7 h1:hkdgbqizGQHuU5IPqYM1JdSMV8nKfpuOnZYXssk9muY= github.com/hashicorp/serf v0.9.7/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4= github.com/heketi/heketi v10.3.0+incompatible/go.mod h1:bB9ly3RchcQqsQ9CpyaQwvva7RS5ytVoSoholZQON6o= github.com/heketi/tests v0.0.0-20151005000721-f3775cbcefd6/go.mod h1:xGMAM8JLi7UkZt1i4FQeQy0R2T8GLUwQhOP5M1gBhy4= +github.com/holyspectral/gosaml2 v0.0.0-20231003195827-3d916621a704 h1:K66i63+dsx3APnli0n/DSdR/C4A0KG7mGkEw7Ku+NDw= +github.com/holyspectral/gosaml2 v0.0.0-20231003195827-3d916621a704/go.mod h1:dBE+6wnR9IO1NcY7kpleh4v56TJ7ztbuzMikTr6m45A= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= @@ -501,6 +507,10 @@ github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfC github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= github.com/jonboulle/clockwork v0.2.2 h1:UOGuzwb1PwsrDAObMuhUnj0p5ULPj8V/xJ7Kx9qUBdQ= github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8= +github.com/jonboulle/clockwork v0.3.0 h1:9BSCMi8C+0qdApAp4auwX0RkLGUjs956h0EkuQymUhg= +github.com/jonboulle/clockwork v0.3.0/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8= +github.com/jonboulle/clockwork v0.4.0 h1:p4Cf1aMWXnXAUh8lVfewRBx1zaTSYKrKMF2g3ST4RZ4= +github.com/jonboulle/clockwork v0.4.0/go.mod h1:xgRqUGwRcjKCO1vbZUEtSLrqKoPSsUpK7fnezOII0kc= github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= @@ -545,6 +555,8 @@ github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfn github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= @@ -563,6 +575,8 @@ github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs= +github.com/mattermost/xml-roundtrip-validator v0.1.0 h1:RXbVD2UAl7A7nOTR4u7E3ILa4IbtvKBHw64LDsmu9hU= +github.com/mattermost/xml-roundtrip-validator v0.1.0/go.mod h1:qccnGMcpgwcNaBnxqpJpWWUiPNr5H3O8eDgGV9gT5To= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= @@ -733,9 +747,11 @@ github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFR github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rubiojr/go-vhd v0.0.0-20200706105327-02e210299021/go.mod h1:DM5xW0nvfNNm2uytzsvhI3OnX8uzaRAg8UX/CnDqbto= -github.com/russellhaering/goxmldsig v1.1.1 h1:vI0r2osGF1A9PLvsGdPUAGwEIrKa4Pj5sesSBsebIxM= -github.com/russellhaering/goxmldsig v1.1.1/go.mod h1:gM4MDENBQf7M+V824SGfyIUVFWydB7n0KkEubVJl+Tw= +github.com/russellhaering/goxmldsig v1.4.0 h1:8UcDh/xGyQiyrW+Fq5t8f+l2DLB1+zlhYzkPUJ7Qhys= +github.com/russellhaering/goxmldsig v1.4.0/go.mod h1:gM4MDENBQf7M+V824SGfyIUVFWydB7n0KkEubVJl+Tw= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= @@ -813,6 +829,8 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5 github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/syndtr/gocapability v0.0.0-20180916011248-d98352740cb2 h1:b6uOv7YOFK0TYG7HtkIgExQo+2RdLuwRft63jn2HWj8= github.com/syndtr/gocapability v0.0.0-20180916011248-d98352740cb2/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= @@ -914,6 +932,8 @@ golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 h1:HWj/xjIHfjYU5nVXpTM0s3 golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 h1:7I4JAnoQBe7ZtJcBaYHi5UtiO8tQHbUSXxL+pnGRANg= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20220128200615-198e4374d7ed h1:YoWVYYAfvQ4ddHv3OKmIvX7NCAhFGTj62VP2l2kfBbA= +golang.org/x/crypto v0.0.0-20220128200615-198e4374d7ed/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= diff --git a/share/auth/auth.go b/share/auth/auth.go index 8355c0a81..27c5e4e8f 100644 --- a/share/auth/auth.go +++ b/share/auth/auth.go @@ -2,19 +2,28 @@ package auth import ( "context" - "errors" + "crypto/tls" + "crypto/x509" + "encoding/pem" "fmt" + "net/url" "strconv" "time" + "github.com/beevik/etree" + "github.com/jonboulle/clockwork" + saml2 "github.com/russellhaering/gosaml2" + dsig "github.com/russellhaering/goxmldsig" log "github.com/sirupsen/logrus" "golang.org/x/oauth2" "gopkg.in/ldap.v2" + "github.com/pkg/errors" + "github.com/neuvector/neuvector/controller/api" "github.com/neuvector/neuvector/share" "github.com/neuvector/neuvector/share/auth/oidc" - "github.com/neuvector/neuvector/share/auth/saml" + "github.com/neuvector/neuvector/share/utils" ) @@ -33,18 +42,24 @@ const ( type RemoteAuthInterface interface { LDAPAuth(ldap *share.CLUSServerLDAP, username, password string) (map[string]string, []string, error) - SAMLSPGetRedirectURL(csaml *share.CLUSServerSAML, redir *api.RESTTokenRedirect) (string, error) - SAMLSPAuth(csaml *share.CLUSServerSAML, tokenData *api.RESTAuthToken) (map[string][]string, error) + + SAMLSPGetLogoutURL(csaml *share.CLUSServerSAML, redir *api.RESTTokenRedirect, nameid string, sessionIndex string, overrides map[string]string) (string, error) + SAMLSPGetRedirectURL(csaml *share.CLUSServerSAML, redir *api.RESTTokenRedirect, overrides map[string]string) (string, error) + // Return Name ID, session index, and attributes. + SAMLSPAuth(csaml *share.CLUSServerSAML, tokenData *api.RESTAuthToken) (string, string, map[string][]string, error) OIDCDiscover(issuer string) (string, string, string, string, error) OIDCGetRedirectURL(csaml *share.CLUSServerOIDC, redir *api.RESTTokenRedirect) (string, error) OIDCAuth(coidc *share.CLUSServerOIDC, tokenData *api.RESTAuthToken) (map[string]interface{}, error) } -func NewRemoteAuther() RemoteAuthInterface { - return &remoteAuth{} +func NewRemoteAuther(fakeTime *time.Time) RemoteAuthInterface { + return &remoteAuth{ + fakeTime: fakeTime, + } } type remoteAuth struct { + fakeTime *time.Time // For unit-tests } const defaultLDAPAuthTimeout = time.Second * 10 @@ -122,45 +137,180 @@ func (a *remoteAuth) LDAPAuth(cldap *share.CLUSServerLDAP, username, password st return attrs, groups, nil } -func (a *remoteAuth) SAMLSPGetRedirectURL(csaml *share.CLUSServerSAML, redir *api.RESTTokenRedirect) (string, error) { - // Use redirect URL as Entity ID - sp := saml.ServiceProvider{ - IDPSSOURL: csaml.SSOURL, - IDPSSODescriptorURL: redir.Redirect, - IDPPublicCert: csaml.X509Cert, +func GenerateSamlSP(csaml *share.CLUSServerSAML, spissuer string, redirurl string, timeOverride *time.Time) (*saml2.SAMLServiceProvider, error) { + var keystore dsig.X509KeyStore + + certStore := dsig.MemoryX509CertificateStore{ + Roots: []*x509.Certificate{}, + } + + parseAndStoreCert := func(x509cert string) error { + var err error + block, _ := pem.Decode([]byte(x509cert)) + if block == nil { + return errors.New("failed to decode pem block") + } + + idpCert, err := x509.ParseCertificate(block.Bytes) + if err != nil { + return err + } + certStore.Roots = append(certStore.Roots, idpCert) + return nil + } + + if err := parseAndStoreCert(csaml.X509Cert); err != nil { + log.WithError(err).Error("failed to parse X509Cert. Skip this cert.") + } + + for _, cert := range csaml.X509CertExtra { + if err := parseAndStoreCert(cert); err != nil { + log.WithError(err).Error("failed to parse X509Cert. Skip this cert.") + } + } + + if csaml.SigningCert != "" && csaml.SigningKey != "" { + cert, err := tls.X509KeyPair([]byte(csaml.SigningCert), []byte(csaml.SigningKey)) + if err != nil { + return nil, errors.Wrap(err, "failed to parse key pair") + } + keystore = dsig.TLSCertKeyStore(cert) + } + + // For unit-test + var clockOverride *dsig.Clock + if timeOverride != nil { + clockOverride = dsig.NewFakeClock(clockwork.NewFakeClockAt(*timeOverride)) } - r := sp.GetAuthnRequest() - return r.GetAuthnRequestURL(a.generateState()) + return &saml2.SAMLServiceProvider{ + IdentityProviderSSOURL: csaml.SSOURL, + IdentityProviderSLOURL: csaml.SLOURL, + + ServiceProviderIssuer: spissuer, + IDPCertificateStore: &certStore, + SPKeyStore: keystore, + + // Use redirect URL as AudienceURI. + IdentityProviderIssuer: csaml.Issuer, + AssertionConsumerServiceURL: redirurl, + AudienceURI: redirurl, + + SignAuthnRequests: csaml.AuthnSigningEnabled, + + // Required by Okta. Otherwise you would get this error message: + // Your request resulted in an error. NameIDPolicy '' is not the configured Name ID Format + // 'urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified' for the app + NameIdFormat: "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified", + Clock: clockOverride, + }, nil } -func (a *remoteAuth) SAMLSPAuth(csaml *share.CLUSServerSAML, tokenData *api.RESTAuthToken) (map[string][]string, error) { - var certs []string - certs = append(certs, csaml.X509Cert) - certs = append(certs, csaml.X509CertExtra...) +func (a *remoteAuth) SAMLSPGetRedirectURL(csaml *share.CLUSServerSAML, redir *api.RESTTokenRedirect, overrides map[string]string) (string, error) { + // For backward compatibility, use Authn response redirect url as SP issuer. (https:///token_auth_server) + sp, err := GenerateSamlSP(csaml, redir.Redirect, redir.Redirect, a.fakeTime) + if err != nil { + return "", err + } - r, err := saml.ParseSAMLResponse(tokenData.Token) + // This has to be no signature. + doc, err := sp.BuildAuthRequestDocumentNoSig() if err != nil { - return nil, err + return "", err } - for i, c := range certs { - sp := saml.ServiceProvider{ - IDPSSOURL: csaml.SSOURL, - IDPSSODescriptorURL: csaml.Issuer, - IDPPublicCert: c, + if overrides != nil { + // Allow unit-tests to override elements + for k, v := range overrides { + path, err := etree.CompilePath("./samlp:AuthnRequest") + if err != nil { + return "", errors.Wrap(err, "failed to parse xml path") + } + for _, e := range doc.FindElementsPath(path) { + attr := e.SelectAttr(k) + attr.Value = v + } } + } - err = r.Validate(&sp, true) - if err != nil { - log.WithFields(log.Fields{"samlCertIndex": i, "error": err}).Debug("saml cert failed") - } else { - log.WithFields(log.Fields{"samlCertIndex": i}).Debug("saml cert succeed") - return r.GetAttributes(), nil + // In our previous version of https://github.com/RobotsAndPencils/go-saml, we don't send relay state. + // Keep the same behavior for backward compatibility. + return sp.BuildAuthURLRedirect("", doc) +} + +func (a *remoteAuth) SAMLSPGetLogoutURL(csaml *share.CLUSServerSAML, redir *api.RESTTokenRedirect, nameid string, sessionIndex string, overrides map[string]string) (string, error) { + + // In Azure AD, SSO and SLO must come from the same issuer. + // Caller should specify issuer when it wants to have a different url for SLO response. + issuer := redir.Issuer + if issuer == "" { + issuer = redir.Redirect + } + sp, err := GenerateSamlSP(csaml, issuer, redir.Redirect, a.fakeTime) + if err != nil { + return "", errors.Wrap(err, "failed to generate saml service provider") + } + + // Should be no sig in document. + doc, err := sp.BuildLogoutRequestDocumentNoSig(nameid, sessionIndex) + if err != nil { + return "", errors.Wrap(err, "failed to build saml slo document") + } + + if overrides != nil { + // Allow unit-tests to override elements + for k, v := range overrides { + path, err := etree.CompilePath("./samlp:LogoutRequest") + if err != nil { + return "", errors.Wrap(err, "failed to parse xml path") + } + for _, e := range doc.FindElementsPath(path) { + attr := e.SelectAttr(k) + attr.Value = v + } + } + } + + return sp.BuildLogoutURLRedirect("", doc) +} + +// Return Name ID, session index, and attributes. +func (a *remoteAuth) SAMLSPAuth(csaml *share.CLUSServerSAML, tokenData *api.RESTAuthToken) (string, string, map[string][]string, error) { + // Authn response redirect url (AssertionConsumerServiceURL) as SP issuer. (https:///token_auth_server) + sp, err := GenerateSamlSP(csaml, tokenData.Redirect, tokenData.Redirect, a.fakeTime) + if err != nil { + return "", "", map[string][]string{}, err + } + + // Token is the whole query parameters. + q, err := url.ParseQuery(tokenData.Token) + if err != nil { + return "", "", nil, errors.New("Invalid URL query format") + } + resp := q.Get("SAMLResponse") + if resp == "" { + return "", "", nil, errors.New("SAMLResponse not present") + } + + assertionInfo, err := sp.RetrieveAssertionInfo(resp) + if err != nil { + return "", "", map[string][]string{}, err + } + + if assertionInfo.WarningInfo.InvalidTime { + return "", "", map[string][]string{}, errors.New("invalid time") + } + + out := map[string][]string{} + for k, v := range assertionInfo.Values { + values := []string{} + for _, attr := range v.Values { + values = append(values, attr.Value) } + out[k] = values } - return nil, err // err will be the last r.Validate() result + return assertionInfo.NameID, assertionInfo.SessionIndex, out, nil } func (a *remoteAuth) OIDCDiscover(issuer string) (string, string, string, string, error) { diff --git a/share/auth/auth_test.go b/share/auth/auth_test.go new file mode 100644 index 000000000..66305d226 --- /dev/null +++ b/share/auth/auth_test.go @@ -0,0 +1,328 @@ +package auth_test + +import ( + "crypto/x509" + "testing" + "time" + + "github.com/neuvector/neuvector/controller/api" + "github.com/neuvector/neuvector/controller/kv" + "github.com/neuvector/neuvector/share" + "github.com/neuvector/neuvector/share/auth" + "github.com/stretchr/testify/assert" +) + +// +// ***Read before you change this test file.*** +// +// Because SAML requires multiple components to work, and it highly depends on each vendor's implementation. +// Therefore, we only have regression tests here to make sure that the working code will not fail. +// +// How to update these data. +// 1. Update api.RESTTokenRedirect, SSOURL, SLOURL, Issuer to reflect your environment settings. +// 2. Update cert, key that you store in NV and idpCert you retrieved from Okta. +// 3. Run the test to generate URL. +// 4. Verify the URL to make sure it works as expected. https://www.samltool.com/decode.php is useful here. +// 5. Revert your change in step#1. +// 6. Regenerate the URL and update the testdata. + +// To setup Okta: +// 1. Single sign-on URL: https://NV/token_auth_server +// 2. SP Entity ID: https://NV/token_auth_server +// 3. Name ID format: Unspecified +// 4. Extra attributes Email, Username and NVRoleGroup. +// 5. Import signing key. +// 6. Allow app to initiate single logout. +// 7. Check SLO initiation +// 8. Response URL: https://NV/samlslo +// 9. SP Issuer: https://NV/samlslo + +// This test verifies unsigned Authn request. +// On Okta, validate SAML requests with signature certificates should be "unchecked". +func TestOktaSAMLUnsignedAuthnRequest(t *testing.T) { + fakeTime := time.Date(2023, time.October, 4, 21, 8, 30, 0, time.UTC) + remoteAuth := auth.NewRemoteAuther(&fakeTime) + + // Generate IdP cert/key + idpCert, _, err := kv.GenTlsKeyCert("IDPKey", "", "", kv.ValidityPeriod{ + Year: 1, + }, x509.ExtKeyUsageAny) + assert.Nil(t, err) + + reqData := api.RESTTokenRedirect{ + Redirect: "https://example.com/token_auth_server", + } + url, err := remoteAuth.SAMLSPGetRedirectURL(&share.CLUSServerSAML{ + CLUSServerAuth: share.CLUSServerAuth{ + GroupMappedRoles: []*share.GroupRoleMapping{ + &share.GroupRoleMapping{ + Group: "group1", + GlobalRole: api.UserRoleReader, + RoleDomains: make(map[string][]string), + }, + }, + }, + SSOURL: "https://dev.okta.com/app/dev/xxxxx/sso/saml", + Issuer: "https://example.com/token_auth_server", + X509Cert: string(idpCert), + }, &reqData, + map[string]string{ + "ID": "_ec47ec78-a7f4-458d-86ad-f8e5dc85eb8a", + }, + ) + + // Verify if it's consistent with Authn request for Okta. + assert.Equal(t, "https://dev.okta.com/app/dev/xxxxx/sso/saml?SAMLRequest=jJJBb9swDIX%2FisC7LcdNFkOoA2QNhgXotqDJdtglYGVmEWpJnkhl2b8f4rZAd1gwHSk%2Bfg98vGX0%2FWCWWY7hgX5mYlFn3wc240cLOQUTkR2bgJ7YiDXb5ad7U5eVGVKUaGMPbyTXFchMSVwMoNarFvZkp3Oy86bA%2BWFaTGdNVzTvsCsODc0628zosUFQ3yixi6GFuqxAbV6o713oXPhxHfj43MTm4263KTZftjtQy1cTdzFw9pS2lE7O0teH%2BxaOIgMbremMfuiptNFriU8U9pjluGdKJ0qg1syZ1oEFg7RQV%2FVNMamKarqrJ6ZqzE31HdSKWFxAGa2%2Fzu3oVMYnwXEwDsOloM%2BXp5mjvqwQFmMqZmSkxX85utVvJS%2BpfkZP69Um9s7%2BVsu%2Bj7%2FuEqFQC5IygfoQk0f59wIn5WSsuK44jK0mBx7IuoOjDvTiGfr39Sz%2BBAAA%2F%2F8%3D", url) +} + +// This test verifies signed Authn request. +// On Okta, validate SAML requests with signature certificates should be "checked". +func TestOktaSAMLSignedAuthnRequest(t *testing.T) { + cert := `-----BEGIN CERTIFICATE----- +MIIDazCCAlOgAwIBAgIUGOj9G2/XbRCF9QhmuyhqCGf/et0wDQYJKoZIhvcNAQEL +BQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM +GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yMzA3MjgwMDEzMTBaFw0yNDA3 +MjcwMDEzMTBaMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEw +HwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwggEiMA0GCSqGSIb3DQEB +AQUAA4IBDwAwggEKAoIBAQC5vn5AHjxELLmzr8P2noWjhwmI7TxH+5ZBerjQnbKI +agSqL3UnTxc0GsOhGciP7bdOvpgCLCtpxWIEmjTYfOH/Z8Mr6X9XeAK8N9K+CLDM +D5HQeqwPLUoe9RPKcil24MkpiorN/o99cWat0F5XeEFTJeSsBQ1Gv5AvZrDk2NHu +SsDZ7OzLvTQOqSGkoaUB0zUCLRempUML33i5YLUf/OKl1e1IzgIXDUlRSY3qnMSw +Po+k0xuJEp2FJnRnzfKokE8rSODOmhif5g17b4tnL0K79CUCZgkJScGlBWcUZHKt +fAuf9dplECCRUKknk98TYfGPkhnsRyuqVo2S8rSQbu5HAgMBAAGjUzBRMB0GA1Ud +DgQWBBTSUC03SNS2pkb+i30ADeORHK9JizAfBgNVHSMEGDAWgBTSUC03SNS2pkb+ +i30ADeORHK9JizAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQBU +3/3Il9b+fCKp/N8IvgZxVEiGs+MfUtbNgdZidovpfAEHRrgCZBS6+nI8uA9vS+5n +AaMSpe4KXuhRWxp0HPruqXZF2Ipolk+gBAxPRqF5CMLLoTiJA45bzdbOYHlahqve +vn2m965TmKZSMGUMdeVALANKInsQLP7jxNmoX9PJ+76fPReGRIYxV5Y0Ko8iipLH +LWTr21onnXVr5qvoJ4RsiMCWtVtk6tpxOvC2H7IbVKCfeS8mgJ+xB+7OWn/2MwGk +0twZ4lJoT/IvIs3Szq6sdAPXXFs/qug2AUgeLNPViVkW6XA8Nnv9h26KhkLUJ13U +WTDHcR4IliyrrbdkfJiG +-----END CERTIFICATE-----` + + key := `-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC5vn5AHjxELLmz +r8P2noWjhwmI7TxH+5ZBerjQnbKIagSqL3UnTxc0GsOhGciP7bdOvpgCLCtpxWIE +mjTYfOH/Z8Mr6X9XeAK8N9K+CLDMD5HQeqwPLUoe9RPKcil24MkpiorN/o99cWat +0F5XeEFTJeSsBQ1Gv5AvZrDk2NHuSsDZ7OzLvTQOqSGkoaUB0zUCLRempUML33i5 +YLUf/OKl1e1IzgIXDUlRSY3qnMSwPo+k0xuJEp2FJnRnzfKokE8rSODOmhif5g17 +b4tnL0K79CUCZgkJScGlBWcUZHKtfAuf9dplECCRUKknk98TYfGPkhnsRyuqVo2S +8rSQbu5HAgMBAAECggEAHHcUiwf3LW17Qg3IJtXRXie2Lt1IdTGZq8w+YX4hW6V/ +tIMUXaNRx4Logxpb8a64/mDYE9EoEAwXQuRM//ZXfhgtQWAFy2ZRaP3XFpdnXMZw +DraWArdqmgbt8wL+1sCJI4wfTIVcARntZeq+YoJD5JW0jyYxDCwUUSdYKaIOvglE +hylSCysUw8kE/zpTmlm6MD3Q7ILfVcezbWyaGQbz4hmJEkVogMQaqHrHv00c0EW+ +0lSdsm5wgky8YSALsEWRLbdLxtb0YWZaRgv12QDlLyjz5XqVbOTajW77HCKp3bTo +Vfx31tU5sWfdToyxPydN3KBbGMkZBDvUizHO9C6F1QKBgQDEwfHPoRYoEKkkJljz +rIAn2AKth64pikEhpxfBOqpElLfqQr0lK8hNqN7hmhDKF9ZB0mnmV+NqgDL5GQjp +FKFgUDpNU61SLdJbpfhX30kfGZvKJGkJrsKbori3NViiHC65hX6IIBxrZctc8SNa +BZya/o1wiLu+x/zzxSEy6i8sowKBgQDxq6FpmJV8jjkQ3xicnm56CtOh8lBAcycL +tatkIkQUBDWFaxEpbtQuz/o1lY/p/frm++8wzom5tePtweSpUqYwNx/R1fqY8EFA +hkcX4Ea8+a4clhI7e6gbdj+sdyvHLZtrFd7MhS7UPOcemiCkW72j2eyep4GkfjUH +kn8IWNBODQKBgQCteCBtYiRapnW5PWXnUAqdFkEmJR1T2mSZ+utinQpI9KVBkB2a +jANJFL2MQXzT8DgiSBS91HbYCrbmD0Bf3qR4ecMtMbz5WxS/YJCXSHD7TmSfz4Ib +20wQU2JvhETkh9xaDGwGL+ledpzZEHCOiawMqqigsqx0A3XspbwjW3zD6QKBgFln +ijZbeWnz346rShqezfYeTT7LOv8s2pQNaFOKDa9uAzLRci7mzl5nGIR8SRpimFCd +gVaIAhGPBbxuj55IcizCJ+ZkB+pOb4VkZ6aglOrSX6Q5rJMO4xkNvO6bw7lS2P1b +wOnel31y7nm2wT6spdKZC12CUIa/HfUoMBCxcpZ9AoGARg0PijCk5QWmYiJkwXge +R/an9nanajMItgZBY1AxOmyOTvTvVwvzozwLPoVIH5kfBnhK8H/DFrG0J4Ixv48e +Ac24T2ndenjpLnEycZLM9V+/U0rAfLkKgOOWSL+NlkOPgoWD9erDJj/g5Udjbywk +ma7nkie3ORja96UTROAZ77o= +-----END PRIVATE KEY-----` + + fakeTime := time.Date(2023, time.October, 4, 21, 8, 30, 0, time.UTC) + remoteAuth := auth.NewRemoteAuther(&fakeTime) + + // Generate IdP cert/key + idpCert, _, err := kv.GenTlsKeyCert("IDPKey", "", "", kv.ValidityPeriod{ + Year: 1, + }, x509.ExtKeyUsageAny) + assert.Nil(t, err) + + reqData := api.RESTTokenRedirect{ + Redirect: "https://example.com/token_auth_server", + } + url, err := remoteAuth.SAMLSPGetRedirectURL(&share.CLUSServerSAML{ + CLUSServerAuth: share.CLUSServerAuth{ + GroupMappedRoles: []*share.GroupRoleMapping{ + &share.GroupRoleMapping{ + Group: "group1", + GlobalRole: api.UserRoleReader, + RoleDomains: make(map[string][]string), + }, + }, + }, + SSOURL: "https://dev.okta.com/app/dev/xxxxx/sso/saml", + Issuer: "https://example.com/token_auth_server", + X509Cert: string(idpCert), + SLOEnabled: false, + SigningCert: string(cert), + SigningKey: string(key), + }, &reqData, + map[string]string{ + "ID": "_ec47ec78-a7f4-458d-86ad-f8e5dc85eb8a", + }, + ) + + // Verify if it's consistent with Authn request for Okta. + assert.Equal(t, "https://dev.okta.com/app/dev/xxxxx/sso/saml?SAMLRequest=jJJBb9swDIX%2FisC7LcdNFkOoA2QNhgXotqDJdtglYGVmEWpJnkhl2b8f4rZAd1gwHSk%2Bfg98vGX0%2FWCWWY7hgX5mYlFn3wc240cLOQUTkR2bgJ7YiDXb5ad7U5eVGVKUaGMPbyTXFchMSVwMoNarFvZkp3Oy86bA%2BWFaTGdNVzTvsCsODc0628zosUFQ3yixi6GFuqxAbV6o713oXPhxHfj43MTm4263KTZftjtQy1cTdzFw9pS2lE7O0teH%2BxaOIgMbremMfuiptNFriU8U9pjluGdKJ0qg1syZ1oEFg7RQV%2FVNMamKarqrJ6ZqzE31HdSKWFxAGa2%2Fzu3oVMYnwXEwDsOloM%2BXp5mjvqwQFmMqZmSkxX85utVvJS%2BpfkZP69Um9s7%2BVsu%2Bj7%2FuEqFQC5IygfoQk0f59wIn5WSsuK44jK0mBx7IuoOjDvTiGfr39Sz%2BBAAA%2F%2F8%3D", url) +} + +// Verify NV can accept Okta Authn response. +func TestOktaSAMLAuthnResponse(t *testing.T) { + tokenData := api.RESTAuthToken{ + Token: "SAMLResponse=PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c2FtbDJwOlJlc3BvbnNlIERlc3RpbmF0aW9uPSJodHRwczovL2V4YW1wbGUuY29tL3Rva2VuX2F1dGhfc2VydmVyIiBJRD0iaWQ3MTM5NjAxOTA1MzI3MDI4MzM1OTM4ODYiIEluUmVzcG9uc2VUbz0iX2Y4MDkxOGVmLWQzMWItNDRhMi05ZWEzLTY0ODcxNDViNDU4ZSIgSXNzdWVJbnN0YW50PSIyMDIzLTEwLTA2VDAwOjUxOjA4Ljg5MloiIFZlcnNpb249IjIuMCIgeG1sbnM6c2FtbDJwPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6cHJvdG9jb2wiIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSI%2BPHNhbWwyOklzc3VlciBGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpuYW1laWQtZm9ybWF0OmVudGl0eSIgeG1sbnM6c2FtbDI9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphc3NlcnRpb24iPmh0dHA6Ly93d3cub2t0YS5jb20vZXhrYm41Ymw4djhkYVNwZDQ1ZDc8L3NhbWwyOklzc3Vlcj48ZHM6U2lnbmF0dXJlIHhtbG5zOmRzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjIj48ZHM6U2lnbmVkSW5mbz48ZHM6Q2Fub25pY2FsaXphdGlvbk1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPjxkczpTaWduYXR1cmVNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGRzaWctbW9yZSNyc2Etc2hhMjU2Ii8%2BPGRzOlJlZmVyZW5jZSBVUkk9IiNpZDcxMzk2MDE5MDUzMjcwMjgzMzU5Mzg4NiI%2BPGRzOlRyYW5zZm9ybXM%2BPGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNlbnZlbG9wZWQtc2lnbmF0dXJlIi8%2BPGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyI%2BPGVjOkluY2x1c2l2ZU5hbWVzcGFjZXMgUHJlZml4TGlzdD0ieHMiIHhtbG5zOmVjPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzEwL3htbC1leGMtYzE0biMiLz48L2RzOlRyYW5zZm9ybT48L2RzOlRyYW5zZm9ybXM%2BPGRzOkRpZ2VzdE1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jI3NoYTI1NiIvPjxkczpEaWdlc3RWYWx1ZT45ZjRRcTB5djBWQmFxOWRZa1JYQ0UrWHFkNGh2cys5N0tXbHZyTkZUMVhJPTwvZHM6RGlnZXN0VmFsdWU%2BPC9kczpSZWZlcmVuY2U%2BPC9kczpTaWduZWRJbmZvPjxkczpTaWduYXR1cmVWYWx1ZT5EQ1BSUXZRWldsV0NSSXNITUszRk91aWY2eHlacm8yUVpRMGFYQUJnTzQra1M3VVVoMmJBQjB1SUdvK2Q1MDRGb0NsT0xzeW5OR1M4YUJRVU1jTXllQTllbnFPTGExR0NWZ2REVDBDKzlEaHlma0x0MUJ6SFI1SXE5WVBSMy9jbENYN2lBTTNyelRubHpSaGJyWmZvWEtnYjZrck9FWHMyNGl4ZldKUG1jR0VhYWtwQ3RFTTJreGIxMmpVZVkxOXVNNjRCM0I1V09aR3hrQUhzejFjdzZEME1wRno5bWpWWUtxUXNpakxubXZpYUcyZ0x3SjZGQ3ZwZTU4V1lycUFtelBvWkV1L0lyRFg1ZzdVay9aYk1mSk9qeDFMNGRyckNOZHg3REIzM0hUY3ZERTc5bzc5Y0dZcHVVc1ovc2NaSEtISFliWmptc0FOcmhZVlRoRTA4K1E9PTwvZHM6U2lnbmF0dXJlVmFsdWU%2BPGRzOktleUluZm8%2BPGRzOlg1MDlEYXRhPjxkczpYNTA5Q2VydGlmaWNhdGU%2BTUlJRHFEQ0NBcENnQXdJQkFnSUdBWXJpZ09nbk1BMEdDU3FHU0liM0RRRUJDd1VBTUlHVU1Rc3dDUVlEVlFRR0V3SlZVekVUTUJFRwpBMVVFQ0F3S1EyRnNhV1p2Y201cFlURVdNQlFHQTFVRUJ3d05VMkZ1SUVaeVlXNWphWE5qYnpFTk1Bc0dBMVVFQ2d3RVQydDBZVEVVCk1CSUdBMVVFQ3d3TFUxTlBVSEp2ZG1sa1pYSXhGVEFUQmdOVkJBTU1ER1JsZGkweE5UY3pOamcyTlRFY01Cb0dDU3FHU0liM0RRRUoKQVJZTmFXNW1iMEJ2YTNSaExtTnZiVEFlRncweU16QTVNamt4T1RVeU16TmFGdzB6TXpBNU1qa3hPVFV6TXpOYU1JR1VNUXN3Q1FZRApWUVFHRXdKVlV6RVRNQkVHQTFVRUNBd0tRMkZzYVdadmNtNXBZVEVXTUJRR0ExVUVCd3dOVTJGdUlFWnlZVzVqYVhOamJ6RU5NQXNHCkExVUVDZ3dFVDJ0MFlURVVNQklHQTFVRUN3d0xVMU5QVUhKdmRtbGtaWEl4RlRBVEJnTlZCQU1NREdSbGRpMHhOVGN6TmpnMk5URWMKTUJvR0NTcUdTSWIzRFFFSkFSWU5hVzVtYjBCdmEzUmhMbU52YlRDQ0FTSXdEUVlKS29aSWh2Y05BUUVCQlFBRGdnRVBBRENDQVFvQwpnZ0VCQUtOdmpyQWU5cXE4WmQrN2JLQlV5ZW12c3NTbkgzV2hndXNySlcyTVVwUjZKaEJYaytmck9LSUE2Z0trcG94VVBQZEpsOUFqCjlSMThkZWN0YXNsa2VoaUR1d2ExTVZWeFRCSi9BK21xaGNBUWlKQjhJUHVXelUxVGh0eEpMMU9SMmlDWXJhZWVEcFBVL1JmUXJJVGQKL1NaTm5pcDZzeVZZbW9JRk93MWM5cjlGd0JjQ1RmcnBTWHl6NnMwd1VYZ0dsRnYraUtmMmVXdCtOeHRFVHYrQ0NldHV3dFpFTHRGSQpvNVE0RmhUb3Z5dEJCejU2Q2J5SnBmUEcxS3JQN1ZvbElBOVRhUElPdEIzZUMyOTd2ZHBMTUhoMjhIQzZKeHZ5ci80SEpzUytUbytmCk15dzU1dTl1Y0xhM1RXdkZwVEVZOGpJNC92Q0ozTHRPc2IzQU5sdzZHQ2NDQXdFQUFUQU5CZ2txaGtpRzl3MEJBUXNGQUFPQ0FRRUEKbUZBOFZKeGp5U3FRd1lJNklCa09NK3Y4ajBoa0hicjI3MlpnV1pHL3NNek5EamZLaitmQTdXeVdtYWJpT3RLakREZDg1aERTRldQdgo2a3dqdzd0NUpXRjl6bFpPaTZVSWdwTTUxYmFCWkUyUmYrTGRFbGNBOW41bEVNSlRSeUtZamdrUWVpNUt2WVNWVXZvbXcydVUwajFQCndlYnF2TTQ0VTNCcnJiajNZWStLQ3crcitJekw0REc5UDhCZC9RM3Z3VDhPWGRub1crQW5SaXQ0OTNGK0N2ZzltbUFzV0ViSDdOeXUKSDlZNmNvY3pxaCt5aTN0VzBHOFB5bE1hV0N3VmRUWGdOYlFHbWM3SHNDalVjWWFETVZUZGVJN3gzVjhoUVZhM0dzTEZBNGNvSFM0QwpSRWczTEEzWVN5ZUx4eWh3U1VPSlNIZit5QktMWkZyRGxiSlE0dz09PC9kczpYNTA5Q2VydGlmaWNhdGU%2BPC9kczpYNTA5RGF0YT48L2RzOktleUluZm8%2BPC9kczpTaWduYXR1cmU%2BPHNhbWwycDpTdGF0dXMgeG1sbnM6c2FtbDJwPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6cHJvdG9jb2wiPjxzYW1sMnA6U3RhdHVzQ29kZSBWYWx1ZT0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnN0YXR1czpTdWNjZXNzIi8%2BPC9zYW1sMnA6U3RhdHVzPjxzYW1sMjpBc3NlcnRpb24gSUQ9ImlkNzEzOTYwMTkwODIxNDM3NjczNDE4NDMwIiBJc3N1ZUluc3RhbnQ9IjIwMjMtMTAtMDZUMDA6NTE6MDguODkyWiIgVmVyc2lvbj0iMi4wIiB4bWxuczpzYW1sMj0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiIgeG1sbnM6eHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hIj48c2FtbDI6SXNzdWVyIEZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOm5hbWVpZC1mb3JtYXQ6ZW50aXR5IiB4bWxuczpzYW1sMj0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiI%2BaHR0cDovL3d3dy5va3RhLmNvbS9leGtibjVibDh2OGRhU3BkNDVkNzwvc2FtbDI6SXNzdWVyPjxkczpTaWduYXR1cmUgeG1sbnM6ZHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyMiPjxkczpTaWduZWRJbmZvPjxkczpDYW5vbmljYWxpemF0aW9uTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIi8%2BPGRzOlNpZ25hdHVyZU1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZHNpZy1tb3JlI3JzYS1zaGEyNTYiLz48ZHM6UmVmZXJlbmNlIFVSST0iI2lkNzEzOTYwMTkwODIxNDM3NjczNDE4NDMwIj48ZHM6VHJhbnNmb3Jtcz48ZHM6VHJhbnNmb3JtIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI2VudmVsb3BlZC1zaWduYXR1cmUiLz48ZHM6VHJhbnNmb3JtIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIj48ZWM6SW5jbHVzaXZlTmFtZXNwYWNlcyBQcmVmaXhMaXN0PSJ4cyIgeG1sbnM6ZWM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPjwvZHM6VHJhbnNmb3JtPjwvZHM6VHJhbnNmb3Jtcz48ZHM6RGlnZXN0TWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8wNC94bWxlbmMjc2hhMjU2Ii8%2BPGRzOkRpZ2VzdFZhbHVlPkZrWmxPQVNYcWpNeFJ2bTA4bTN0TU5rMkcvZVFSOUZNcUN3bWcxSHIvb0U9PC9kczpEaWdlc3RWYWx1ZT48L2RzOlJlZmVyZW5jZT48L2RzOlNpZ25lZEluZm8%2BPGRzOlNpZ25hdHVyZVZhbHVlPlNRbXNyYUVBeVBlOGV4ekcweFI1Qy9pb2RjbjIreVMzZDVNR01aL01kakNlWWNwYko4ekgyQkR0RWZYaXkvTm1kbmhuM0pSQkh4RHBsNktIaW1QWEh0VjFiLzZobnNEMnRTR0xYaWRMYkpWakllMmdRNkNOTkNwNi9OT1ZGcGU1VWovNGxSUmdiek8vczRmek1hSHN2Q0V3TDd2VnFjZnpWL0FWeHAwYzlOajVsWVRHU2ZkUXRjakRTVHNRLzdhRm45cGdqekJqa2FSTmI0NkpvblNCVkZaZ1graE15MUh6VjY2YmJtYUozL2lHVXRkMGNFbWpsZ0xOVnlKdjdoUGFjbVdDNHVRdGIyWk1PaVBiYkZoaDBNak5TNFdtL2IxZ1VRVFFNeXJQTUFFODJ3VU5NN3RQcWNqTHpQcXZwd0Urays3QlVvbGZKNkRaU2I3Y1dQTmVIQT09PC9kczpTaWduYXR1cmVWYWx1ZT48ZHM6S2V5SW5mbz48ZHM6WDUwOURhdGE%2BPGRzOlg1MDlDZXJ0aWZpY2F0ZT5NSUlEcURDQ0FwQ2dBd0lCQWdJR0FZcmlnT2duTUEwR0NTcUdTSWIzRFFFQkN3VUFNSUdVTVFzd0NRWURWUVFHRXdKVlV6RVRNQkVHCkExVUVDQXdLUTJGc2FXWnZjbTVwWVRFV01CUUdBMVVFQnd3TlUyRnVJRVp5WVc1amFYTmpiekVOTUFzR0ExVUVDZ3dFVDJ0MFlURVUKTUJJR0ExVUVDd3dMVTFOUFVISnZkbWxrWlhJeEZUQVRCZ05WQkFNTURHUmxkaTB4TlRjek5qZzJOVEVjTUJvR0NTcUdTSWIzRFFFSgpBUllOYVc1bWIwQnZhM1JoTG1OdmJUQWVGdzB5TXpBNU1qa3hPVFV5TXpOYUZ3MHpNekE1TWpreE9UVXpNek5hTUlHVU1Rc3dDUVlEClZRUUdFd0pWVXpFVE1CRUdBMVVFQ0F3S1EyRnNhV1p2Y201cFlURVdNQlFHQTFVRUJ3d05VMkZ1SUVaeVlXNWphWE5qYnpFTk1Bc0cKQTFVRUNnd0VUMnQwWVRFVU1CSUdBMVVFQ3d3TFUxTlBVSEp2ZG1sa1pYSXhGVEFUQmdOVkJBTU1ER1JsZGkweE5UY3pOamcyTlRFYwpNQm9HQ1NxR1NJYjNEUUVKQVJZTmFXNW1iMEJ2YTNSaExtTnZiVENDQVNJd0RRWUpLb1pJaHZjTkFRRUJCUUFEZ2dFUEFEQ0NBUW9DCmdnRUJBS052anJBZTlxcThaZCs3YktCVXllbXZzc1NuSDNXaGd1c3JKVzJNVXBSNkpoQlhrK2ZyT0tJQTZnS2twb3hVUFBkSmw5QWoKOVIxOGRlY3Rhc2xrZWhpRHV3YTFNVlZ4VEJKL0ErbXFoY0FRaUpCOElQdVd6VTFUaHR4SkwxT1IyaUNZcmFlZURwUFUvUmZRcklUZAovU1pObmlwNnN5Vlltb0lGT3cxYzlyOUZ3QmNDVGZycFNYeXo2czB3VVhnR2xGditpS2YyZVd0K054dEVUditDQ2V0dXd0WkVMdEZJCm81UTRGaFRvdnl0QkJ6NTZDYnlKcGZQRzFLclA3Vm9sSUE5VGFQSU90QjNlQzI5N3ZkcExNSGgyOEhDNkp4dnlyLzRISnNTK1RvK2YKTXl3NTV1OXVjTGEzVFd2RnBURVk4akk0L3ZDSjNMdE9zYjNBTmx3NkdDY0NBd0VBQVRBTkJna3Foa2lHOXcwQkFRc0ZBQU9DQVFFQQptRkE4Vkp4anlTcVF3WUk2SUJrT00rdjhqMGhrSGJyMjcyWmdXWkcvc016TkRqZktqK2ZBN1d5V21hYmlPdEtqRERkODVoRFNGV1B2CjZrd2p3N3Q1SldGOXpsWk9pNlVJZ3BNNTFiYUJaRTJSZitMZEVsY0E5bjVsRU1KVFJ5S1lqZ2tRZWk1S3ZZU1ZVdm9tdzJ1VTBqMVAKd2VicXZNNDRVM0JycmJqM1lZK0tDdytyK0l6TDRERzlQOEJkL1EzdndUOE9YZG5vVytBblJpdDQ5M0YrQ3ZnOW1tQXNXRWJIN055dQpIOVk2Y29jenFoK3lpM3RXMEc4UHlsTWFXQ3dWZFRYZ05iUUdtYzdIc0NqVWNZYURNVlRkZUk3eDNWOGhRVmEzR3NMRkE0Y29IUzRDClJFZzNMQTNZU3llTHh5aHdTVU9KU0hmK3lCS0xaRnJEbGJKUTR3PT08L2RzOlg1MDlDZXJ0aWZpY2F0ZT48L2RzOlg1MDlEYXRhPjwvZHM6S2V5SW5mbz48L2RzOlNpZ25hdHVyZT48c2FtbDI6U3ViamVjdCB4bWxuczpzYW1sMj0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiI%2BPHNhbWwyOk5hbWVJRCBGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjEuMTpuYW1laWQtZm9ybWF0OnVuc3BlY2lmaWVkIj5hYUBiYi5jYzwvc2FtbDI6TmFtZUlEPjxzYW1sMjpTdWJqZWN0Q29uZmlybWF0aW9uIE1ldGhvZD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmNtOmJlYXJlciI%2BPHNhbWwyOlN1YmplY3RDb25maXJtYXRpb25EYXRhIEluUmVzcG9uc2VUbz0iX2Y4MDkxOGVmLWQzMWItNDRhMi05ZWEzLTY0ODcxNDViNDU4ZSIgTm90T25PckFmdGVyPSIyMDIzLTEwLTA2VDAwOjU2OjA4Ljg5MloiIFJlY2lwaWVudD0iaHR0cHM6Ly9leGFtcGxlLmNvbS90b2tlbl9hdXRoX3NlcnZlciIvPjwvc2FtbDI6U3ViamVjdENvbmZpcm1hdGlvbj48L3NhbWwyOlN1YmplY3Q%2BPHNhbWwyOkNvbmRpdGlvbnMgTm90QmVmb3JlPSIyMDIzLTEwLTA2VDAwOjQ2OjA4Ljg5MloiIE5vdE9uT3JBZnRlcj0iMjAyMy0xMC0wNlQwMDo1NjowOC44OTJaIiB4bWxuczpzYW1sMj0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiI%2BPHNhbWwyOkF1ZGllbmNlUmVzdHJpY3Rpb24%2BPHNhbWwyOkF1ZGllbmNlPmh0dHBzOi8vMTAuMS41Ni4xMDI6MzAyNzYvdG9rZW5fYXV0aF9zZXJ2ZXI8L3NhbWwyOkF1ZGllbmNlPjwvc2FtbDI6QXVkaWVuY2VSZXN0cmljdGlvbj48L3NhbWwyOkNvbmRpdGlvbnM%2BPHNhbWwyOkF1dGhuU3RhdGVtZW50IEF1dGhuSW5zdGFudD0iMjAyMy0xMC0wNlQwMDo1MTowOC44OTJaIiBTZXNzaW9uSW5kZXg9Il9mODA5MThlZi1kMzFiLTQ0YTItOWVhMy02NDg3MTQ1YjQ1OGUiIHhtbG5zOnNhbWwyPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXNzZXJ0aW9uIj48c2FtbDI6QXV0aG5Db250ZXh0PjxzYW1sMjpBdXRobkNvbnRleHRDbGFzc1JlZj51cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YWM6Y2xhc3NlczpQYXNzd29yZFByb3RlY3RlZFRyYW5zcG9ydDwvc2FtbDI6QXV0aG5Db250ZXh0Q2xhc3NSZWY%2BPC9zYW1sMjpBdXRobkNvbnRleHQ%2BPC9zYW1sMjpBdXRoblN0YXRlbWVudD48c2FtbDI6QXR0cmlidXRlU3RhdGVtZW50IHhtbG5zOnNhbWwyPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXNzZXJ0aW9uIj48c2FtbDI6QXR0cmlidXRlIE5hbWU9IkVtYWlsIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OnVuc3BlY2lmaWVkIj48c2FtbDI6QXR0cmlidXRlVmFsdWUgeG1sbnM6eHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hIiB4bWxuczp4c2k9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hLWluc3RhbmNlIiB4c2k6dHlwZT0ieHM6c3RyaW5nIj5hYUBiYi5jYzwvc2FtbDI6QXR0cmlidXRlVmFsdWU%2BPC9zYW1sMjpBdHRyaWJ1dGU%2BPHNhbWwyOkF0dHJpYnV0ZSBOYW1lPSJVc2VybmFtZSIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDp1bnNwZWNpZmllZCI%2BPHNhbWwyOkF0dHJpYnV0ZVZhbHVlIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgeG1sbnM6eHNpPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYS1pbnN0YW5jZSIgeHNpOnR5cGU9InhzOnN0cmluZyI%2BSmltPC9zYW1sMjpBdHRyaWJ1dGVWYWx1ZT48L3NhbWwyOkF0dHJpYnV0ZT48c2FtbDI6QXR0cmlidXRlIE5hbWU9Ik5WUm9sZUdyb3VwIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OnVuc3BlY2lmaWVkIj48c2FtbDI6QXR0cmlidXRlVmFsdWUgeG1sbnM6eHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hIiB4bWxuczp4c2k9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hLWluc3RhbmNlIiB4c2k6dHlwZT0ieHM6c3RyaW5nIj5FdmVyeW9uZTwvc2FtbDI6QXR0cmlidXRlVmFsdWU%2BPC9zYW1sMjpBdHRyaWJ1dGU%2BPC9zYW1sMjpBdHRyaWJ1dGVTdGF0ZW1lbnQ%2BPC9zYW1sMjpBc3NlcnRpb24%2BPC9zYW1sMnA6UmVzcG9uc2U%2B&RelayState=", + State: "", + Redirect: "https://example.com/token_auth_server", + } + idpCert := `-----BEGIN CERTIFICATE----- +MIIDqDCCApCgAwIBAgIGAYrigOgnMA0GCSqGSIb3DQEBCwUAMIGUMQswCQYDVQQGEwJVUzETMBEG +A1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzENMAsGA1UECgwET2t0YTEU +MBIGA1UECwwLU1NPUHJvdmlkZXIxFTATBgNVBAMMDGRldi0xNTczNjg2NTEcMBoGCSqGSIb3DQEJ +ARYNaW5mb0Bva3RhLmNvbTAeFw0yMzA5MjkxOTUyMzNaFw0zMzA5MjkxOTUzMzNaMIGUMQswCQYD +VQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzENMAsG +A1UECgwET2t0YTEUMBIGA1UECwwLU1NPUHJvdmlkZXIxFTATBgNVBAMMDGRldi0xNTczNjg2NTEc +MBoGCSqGSIb3DQEJARYNaW5mb0Bva3RhLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC +ggEBAKNvjrAe9qq8Zd+7bKBUyemvssSnH3WhgusrJW2MUpR6JhBXk+frOKIA6gKkpoxUPPdJl9Aj +9R18dectaslkehiDuwa1MVVxTBJ/A+mqhcAQiJB8IPuWzU1ThtxJL1OR2iCYraeeDpPU/RfQrITd +/SZNnip6syVYmoIFOw1c9r9FwBcCTfrpSXyz6s0wUXgGlFv+iKf2eWt+NxtETv+CCetuwtZELtFI +o5Q4FhTovytBBz56CbyJpfPG1KrP7VolIA9TaPIOtB3eC297vdpLMHh28HC6Jxvyr/4HJsS+To+f +Myw55u9ucLa3TWvFpTEY8jI4/vCJ3LtOsb3ANlw6GCcCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEA +mFA8VJxjySqQwYI6IBkOM+v8j0hkHbr272ZgWZG/sMzNDjfKj+fA7WyWmabiOtKjDDd85hDSFWPv +6kwjw7t5JWF9zlZOi6UIgpM51baBZE2Rf+LdElcA9n5lEMJTRyKYjgkQei5KvYSVUvomw2uU0j1P +webqvM44U3Brrbj3YY+KCw+r+IzL4DG9P8Bd/Q3vwT8OXdnoW+AnRit493F+Cvg9mmAsWEbH7Nyu +H9Y6coczqh+yi3tW0G8PylMaWCwVdTXgNbQGmc7HsCjUcYaDMVTdeI7x3V8hQVa3GsLFA4coHS4C +REg3LA3YSyeLxyhwSUOJSHf+yBKLZFrDlbJQ4w== +-----END CERTIFICATE-----` + + fakeTime := time.Date(2023, time.October, 6, 00, 52, 0, 0, time.UTC) + remoteAuth := auth.NewRemoteAuther(&fakeTime) + + nameid, sessionindex, attrs, err := remoteAuth.SAMLSPAuth(&share.CLUSServerSAML{ + CLUSServerAuth: share.CLUSServerAuth{ + GroupMappedRoles: []*share.GroupRoleMapping{ + &share.GroupRoleMapping{ + Group: "group1", + GlobalRole: api.UserRoleReader, + RoleDomains: make(map[string][]string), + }, + }, + }, + Issuer: "http://www.okta.com/exkbn5bl8v8daSpd45d7", + X509Cert: string(idpCert), + }, &tokenData) + + assert.Nil(t, err) + assert.Equal(t, "aa@bb.cc", nameid) + assert.Equal(t, "_f80918ef-d31b-44a2-9ea3-6487145b458e", sessionindex) + assert.Equal(t, map[string][]string{ + "Email": { + "aa@bb.cc", + }, + "NVRoleGroup": { + "Everyone", + }, + "Username": { + "Jim", + }, + }, attrs) +} + +// This test verifies signed logout request. (always signed) +// Enable Okta's SLO feature so you can generate test data. +func TestOktaSAMLSLORequest(t *testing.T) { + cert := `-----BEGIN CERTIFICATE----- +MIIDazCCAlOgAwIBAgIUGOj9G2/XbRCF9QhmuyhqCGf/et0wDQYJKoZIhvcNAQEL +BQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM +GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yMzA3MjgwMDEzMTBaFw0yNDA3 +MjcwMDEzMTBaMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEw +HwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwggEiMA0GCSqGSIb3DQEB +AQUAA4IBDwAwggEKAoIBAQC5vn5AHjxELLmzr8P2noWjhwmI7TxH+5ZBerjQnbKI +agSqL3UnTxc0GsOhGciP7bdOvpgCLCtpxWIEmjTYfOH/Z8Mr6X9XeAK8N9K+CLDM +D5HQeqwPLUoe9RPKcil24MkpiorN/o99cWat0F5XeEFTJeSsBQ1Gv5AvZrDk2NHu +SsDZ7OzLvTQOqSGkoaUB0zUCLRempUML33i5YLUf/OKl1e1IzgIXDUlRSY3qnMSw +Po+k0xuJEp2FJnRnzfKokE8rSODOmhif5g17b4tnL0K79CUCZgkJScGlBWcUZHKt +fAuf9dplECCRUKknk98TYfGPkhnsRyuqVo2S8rSQbu5HAgMBAAGjUzBRMB0GA1Ud +DgQWBBTSUC03SNS2pkb+i30ADeORHK9JizAfBgNVHSMEGDAWgBTSUC03SNS2pkb+ +i30ADeORHK9JizAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQBU +3/3Il9b+fCKp/N8IvgZxVEiGs+MfUtbNgdZidovpfAEHRrgCZBS6+nI8uA9vS+5n +AaMSpe4KXuhRWxp0HPruqXZF2Ipolk+gBAxPRqF5CMLLoTiJA45bzdbOYHlahqve +vn2m965TmKZSMGUMdeVALANKInsQLP7jxNmoX9PJ+76fPReGRIYxV5Y0Ko8iipLH +LWTr21onnXVr5qvoJ4RsiMCWtVtk6tpxOvC2H7IbVKCfeS8mgJ+xB+7OWn/2MwGk +0twZ4lJoT/IvIs3Szq6sdAPXXFs/qug2AUgeLNPViVkW6XA8Nnv9h26KhkLUJ13U +WTDHcR4IliyrrbdkfJiG +-----END CERTIFICATE-----` + + key := `-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC5vn5AHjxELLmz +r8P2noWjhwmI7TxH+5ZBerjQnbKIagSqL3UnTxc0GsOhGciP7bdOvpgCLCtpxWIE +mjTYfOH/Z8Mr6X9XeAK8N9K+CLDMD5HQeqwPLUoe9RPKcil24MkpiorN/o99cWat +0F5XeEFTJeSsBQ1Gv5AvZrDk2NHuSsDZ7OzLvTQOqSGkoaUB0zUCLRempUML33i5 +YLUf/OKl1e1IzgIXDUlRSY3qnMSwPo+k0xuJEp2FJnRnzfKokE8rSODOmhif5g17 +b4tnL0K79CUCZgkJScGlBWcUZHKtfAuf9dplECCRUKknk98TYfGPkhnsRyuqVo2S +8rSQbu5HAgMBAAECggEAHHcUiwf3LW17Qg3IJtXRXie2Lt1IdTGZq8w+YX4hW6V/ +tIMUXaNRx4Logxpb8a64/mDYE9EoEAwXQuRM//ZXfhgtQWAFy2ZRaP3XFpdnXMZw +DraWArdqmgbt8wL+1sCJI4wfTIVcARntZeq+YoJD5JW0jyYxDCwUUSdYKaIOvglE +hylSCysUw8kE/zpTmlm6MD3Q7ILfVcezbWyaGQbz4hmJEkVogMQaqHrHv00c0EW+ +0lSdsm5wgky8YSALsEWRLbdLxtb0YWZaRgv12QDlLyjz5XqVbOTajW77HCKp3bTo +Vfx31tU5sWfdToyxPydN3KBbGMkZBDvUizHO9C6F1QKBgQDEwfHPoRYoEKkkJljz +rIAn2AKth64pikEhpxfBOqpElLfqQr0lK8hNqN7hmhDKF9ZB0mnmV+NqgDL5GQjp +FKFgUDpNU61SLdJbpfhX30kfGZvKJGkJrsKbori3NViiHC65hX6IIBxrZctc8SNa +BZya/o1wiLu+x/zzxSEy6i8sowKBgQDxq6FpmJV8jjkQ3xicnm56CtOh8lBAcycL +tatkIkQUBDWFaxEpbtQuz/o1lY/p/frm++8wzom5tePtweSpUqYwNx/R1fqY8EFA +hkcX4Ea8+a4clhI7e6gbdj+sdyvHLZtrFd7MhS7UPOcemiCkW72j2eyep4GkfjUH +kn8IWNBODQKBgQCteCBtYiRapnW5PWXnUAqdFkEmJR1T2mSZ+utinQpI9KVBkB2a +jANJFL2MQXzT8DgiSBS91HbYCrbmD0Bf3qR4ecMtMbz5WxS/YJCXSHD7TmSfz4Ib +20wQU2JvhETkh9xaDGwGL+ledpzZEHCOiawMqqigsqx0A3XspbwjW3zD6QKBgFln +ijZbeWnz346rShqezfYeTT7LOv8s2pQNaFOKDa9uAzLRci7mzl5nGIR8SRpimFCd +gVaIAhGPBbxuj55IcizCJ+ZkB+pOb4VkZ6aglOrSX6Q5rJMO4xkNvO6bw7lS2P1b +wOnel31y7nm2wT6spdKZC12CUIa/HfUoMBCxcpZ9AoGARg0PijCk5QWmYiJkwXge +R/an9nanajMItgZBY1AxOmyOTvTvVwvzozwLPoVIH5kfBnhK8H/DFrG0J4Ixv48e +Ac24T2ndenjpLnEycZLM9V+/U0rAfLkKgOOWSL+NlkOPgoWD9erDJj/g5Udjbywk +ma7nkie3ORja96UTROAZ77o= +-----END PRIVATE KEY-----` + + fakeTime := time.Date(2023, time.October, 6, 0, 6, 30, 0, time.UTC) + remoteAuth := auth.NewRemoteAuther(&fakeTime) + + // Generate IdP cert/key + idpCert, _, err := kv.GenTlsKeyCert("IDPKey", "", "", kv.ValidityPeriod{ + Year: 1, + }, x509.ExtKeyUsageAny) + assert.Nil(t, err) + + reqData := api.RESTTokenRedirect{ + Redirect: "https://example.com/token_auth_server", + } + url, err := remoteAuth.SAMLSPGetLogoutURL(&share.CLUSServerSAML{ + CLUSServerAuth: share.CLUSServerAuth{ + GroupMappedRoles: []*share.GroupRoleMapping{ + &share.GroupRoleMapping{ + Group: "group1", + GlobalRole: api.UserRoleReader, + RoleDomains: make(map[string][]string), + }, + }, + }, + SSOURL: "https://dev.okta.com/app/dev/xxxxx/sso/saml", + // TODO: Make sure issuer and redirect don't conflict. + Issuer: "https://example.com/token_auth_server", + X509Cert: string(idpCert), + SLOEnabled: true, + SLOURL: "https://dev.okta.com/app/dev/xxxxx/slo/saml", + SigningCert: string(cert), + SigningKey: string(key), + }, &reqData, + "aa@bb.cc", + "_6d713693-660a-4740-b9e0-c1ac2321fabe", + map[string]string{ + "ID": "_ec47ec78-a7f4-458d-86ad-f8e5dc85eb8a", + }, + ) + assert.Nil(t, err) + + // Verify if it's consistent with logout request for Okta. + assert.Equal(t, "https://dev.okta.com/app/dev/xxxxx/slo/saml?SAMLRequest=fJLf65swFMX%2FFcl7NP6o%2Bg1fZYMyELo9rGMPe5Frcl2lmrjcWPzzh7aFbrDl8SSfc8K5951gGmd5sj%2Ft4r%2FirwXJB%2Bs0GpL7TcUWZ6QFGkgamJCkV%2FL88fNJJqGQs7PeKjuyF%2BT%2FBBCh84M1LGiOFWtRZQWqouRQ9BnPDqXmZQ6a9yUetCoP2JXAgu%2FoaLCmYkkoWNAQLdgY8mB8xRKRpDwWXOTfhJAil6n4wYIjkh8M%2BJ26eD%2BTjCKNt9BePYTKThHM8yZE63YiGm20%2FZ7VeyNyz3D1k8QVpnnEHfT2iqaFxV9aQndD9x69Inf%2BC0zYHINP1k3g%2F11JHMa7Mmje70%2FlYmhGNfQDalYDfOi6UKlHwt20fszsjLSV0hiNa93muojT%2FC3leS6AZ0UmePeGgqsYVJImcQ8d3m3%2BIp%2FiHytQ%2Fw4AAP%2F%2F&SigAlg=http%3A%2F%2Fwww.w3.org%2F2001%2F04%2Fxmldsig-more%23rsa-sha256&Signature=IAXj56i9MlhULmRq9v0LNTeFmmuce4PK%2B41jiEM%2BYX5HFgdav7u6%2B8zlNk5PQB%2FGqYsXpT4CbhB%2BwVDj4DaL87OFDAk5urz2Af3CK209ktL0YO1MT3D%2BitwP8nzmxdfQ1LIxQp%2B8MMk6vVlLzosY3M0wtxMnQ3QOO229BT13FTt%2BNtTxzY4TiUtgPaa7xzAVdgKLZFabPG8U%2FvKZkttaifjMRK1V1px42KiRB6WoD1bWRRdAZecfUg4AcUH%2BOs21OwmL4LVQiLkCzuXNL4dOlqqbxz5P9AjXZS5XTfog1fMvxhqM2Rk0pOnUxTyrlctJVigzENhmvn1MuJPjSCt3JA%3D%3D", url) +} + +func TestOktaSAMLSLOResponse(t *testing.T) { +} diff --git a/share/auth/saml/LICENSE b/share/auth/saml/LICENSE deleted file mode 100644 index 6ee215690..000000000 --- a/share/auth/saml/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2015 Robots and Pencils - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/share/auth/saml/authnrequest.go b/share/auth/saml/authnrequest.go deleted file mode 100644 index e93a6817b..000000000 --- a/share/auth/saml/authnrequest.go +++ /dev/null @@ -1,188 +0,0 @@ -// Copyright 2014 Matthew Baird, Andrew Mussey -// -// 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 saml - -import ( - "encoding/base64" - "encoding/xml" - "net/url" - "time" -) - -func newAuthnRequest() *AuthnRequest { - id := getID() - return &AuthnRequest{ - XMLName: xml.Name{ - Local: "samlp:AuthnRequest", - }, - SAMLP: "urn:oasis:names:tc:SAML:2.0:protocol", - SAML: "urn:oasis:names:tc:SAML:2.0:assertion", - SAMLSIG: "http://www.w3.org/2000/09/xmldsig#", - ID: id, - ProtocolBinding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST", - Version: "2.0", - AssertionConsumerServiceURL: "", // caller must populate ar.AppSettings.AssertionConsumerServiceURL, - Issuer: Issuer{ - XMLName: xml.Name{ - Local: "saml:Issuer", - }, - Url: "", // caller must populate ar.AppSettings.Issuer - SAML: "urn:oasis:names:tc:SAML:2.0:assertion", - }, - IssueInstant: time.Now().UTC().Format(time.RFC3339), // 2018-12-28: Changed from RFC3339Nano, not supported by ADFS - NameIDPolicy: NameIDPolicy{ - XMLName: xml.Name{ - Local: "samlp:NameIDPolicy", - }, - AllowCreate: true, - Format: "urn:oasis:names:tc:SAML:2.0:nameid-format:transient", - }, - /* - RequestedAuthnContext: RequestedAuthnContext{ - XMLName: xml.Name{ - Local: "samlp:RequestedAuthnContext", - }, - SAMLP: "urn:oasis:names:tc:SAML:2.0:protocol", - Comparison: "exact", - AuthnContextClassRef: AuthnContextClassRef{ - XMLName: xml.Name{ - Local: "saml:AuthnContextClassRef", - }, - SAML: "urn:oasis:names:tc:SAML:2.0:assertion", - Transport: "urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport", - }, - }, - */ - Signature: &Signature{ - XMLName: xml.Name{ - Local: "samlsig:Signature", - }, - Id: "Signature1", - SignedInfo: SignedInfo{ - XMLName: xml.Name{ - Local: "samlsig:SignedInfo", - }, - CanonicalizationMethod: CanonicalizationMethod{ - XMLName: xml.Name{ - Local: "samlsig:CanonicalizationMethod", - }, - Algorithm: "http://www.w3.org/2001/10/xml-exc-c14n#", - }, - SignatureMethod: SignatureMethod{ - XMLName: xml.Name{ - Local: "samlsig:SignatureMethod", - }, - Algorithm: "http://www.w3.org/2000/09/xmldsig#rsa-sha1", - }, - SamlsigReference: SamlsigReference{ - XMLName: xml.Name{ - Local: "samlsig:Reference", - }, - URI: "#" + id, - Transforms: Transforms{ - XMLName: xml.Name{ - Local: "samlsig:Transforms", - }, - Transform: []Transform{Transform{ - XMLName: xml.Name{ - Local: "samlsig:Transform", - }, - Algorithm: "http://www.w3.org/2000/09/xmldsig#enveloped-signature", - }}, - }, - DigestMethod: DigestMethod{ - XMLName: xml.Name{ - Local: "samlsig:DigestMethod", - }, - Algorithm: "http://www.w3.org/2000/09/xmldsig#sha1", - }, - DigestValue: DigestValue{ - XMLName: xml.Name{ - Local: "samlsig:DigestValue", - }, - }, - }, - }, - SignatureValue: SignatureValue{ - XMLName: xml.Name{ - Local: "samlsig:SignatureValue", - }, - }, - KeyInfo: KeyInfo{ - XMLName: xml.Name{ - Local: "samlsig:KeyInfo", - }, - X509Data: X509Data{ - XMLName: xml.Name{ - Local: "samlsig:X509Data", - }, - X509Certificate: X509Certificate{ - XMLName: xml.Name{ - Local: "samlsig:X509Certificate", - }, - Cert: "", // caller must populate cert, - }, - }, - }, - }, - } -} - -func (r *AuthnRequest) String() (string, error) { - b, err := xml.MarshalIndent(r, "", " ") - if err != nil { - return "", err - } - - return string(b), nil -} - -func (r *AuthnRequest) EncodedString() (string, error) { - saml, err := r.String() - if err != nil { - return "", err - } - b64XML := base64.StdEncoding.EncodeToString([]byte(saml)) - return b64XML, nil -} - -func (r *AuthnRequest) CompressedEncodedString() (string, error) { - saml, err := r.String() - if err != nil { - return "", err - } - compressed := compress([]byte(saml)) - b64XML := base64.StdEncoding.EncodeToString(compressed) - return b64XML, nil -} - -// GetAuthnRequestURL generate a URL for the AuthnRequest to the IdP with the SAMLRequst parameter encoded -func (r *AuthnRequest) GetAuthnRequestURL(state string) (string, error) { - u, err := url.Parse(r.Destination) - if err != nil { - return "", err - } - - b64XML, err := r.CompressedEncodedString() - if err != nil { - return "", err - } - - q := u.Query() - q.Add("SAMLRequest", b64XML) - //q.Add("RelayState", state) - u.RawQuery = q.Encode() - return u.String(), nil -} diff --git a/share/auth/saml/authnresponse.go b/share/auth/saml/authnresponse.go deleted file mode 100644 index 5b7718389..000000000 --- a/share/auth/saml/authnresponse.go +++ /dev/null @@ -1,143 +0,0 @@ -package saml - -import ( - "crypto/x509" - "encoding/base64" - "encoding/pem" - "encoding/xml" - "errors" - "net/url" - "time" - - "github.com/beevik/etree" - "github.com/russellhaering/goxmldsig" - log "github.com/sirupsen/logrus" -) - -func ParseSAMLResponse(token string) (*Response, error) { - q, err := url.ParseQuery(token) - if err != nil { - return nil, errors.New("Invalid URL query format") - } - resp := q.Get("SAMLResponse") - if resp == "" { - return nil, errors.New("SAMLResponse not present") - } - - var response Response - bytesXML, err := base64.StdEncoding.DecodeString(resp) - if err != nil { - return nil, errors.New("SAMLResponse not base64 encoded") - } - if err = xml.Unmarshal(bytesXML, &response); err != nil { - bytesXML = decompress(bytesXML) - if err = xml.Unmarshal(bytesXML, &response); err != nil { - return nil, errors.New("Unable to parse SAMLResponse into XML") - } - } - - response.bytesXML = bytesXML - return &response, nil -} - -func (r *Response) Validate(s *ServiceProvider, checkTime bool) error { - if r.Version != "2.0" { - return errors.New("unsupported SAML Version") - } - - if len(r.ID) == 0 { - return errors.New("missing ID attribute on SAML Response") - } - - if len(r.Assertion.ID) == 0 { - return errors.New("no Assertions") - } - if len(r.Signature.SignatureValue.Value) == 0 { - return errors.New("no signature") - } - if r.Assertion.Subject.SubjectConfirmation.Method != "urn:oasis:names:tc:SAML:2.0:cm:bearer" { - return errors.New("assertion method exception") - } - - if r.Issuer.Url != s.IDPSSODescriptorURL { - return errors.New("issuer mismatch") - } - - // validate signature - block, _ := pem.Decode([]byte(s.IDPPublicCert)) - if block == nil { - return errors.New("decode certificate error") - } - certs, err := x509.ParseCertificate(block.Bytes) - if err != nil { - log.WithFields(log.Fields{"error": err}).Error("failed to parse certificate") - return errors.New("parse certificate error") - } - ctx := dsig.NewDefaultValidationContext(&dsig.MemoryX509CertificateStore{ - Roots: []*x509.Certificate{certs}, - }) - - doc := etree.NewDocument() - doc.ReadFromBytes(r.bytesXML) - - if _, err = ctx.Validate(doc.Root()); err != nil { - log.WithFields(log.Fields{"error": err}).Error("failed to validate signature") - return errors.New("validate signature error") - } - - if checkTime { - expires := r.Assertion.Subject.SubjectConfirmation.SubjectConfirmationData.NotOnOrAfter - notOnOrAfter, e := time.Parse(time.RFC3339, expires) - if e != nil { - return e - } - if notOnOrAfter.Before(time.Now()) { - return errors.New("assertion has expired on: " + expires) - } - } - - return nil -} - -func (r *Response) String() (string, error) { - b, err := xml.MarshalIndent(r, "", " ") - if err != nil { - return "", err - } - - return string(b), nil -} - -// GetAttribute by Name or by FriendlyName. Return blank string if not found -func (r *Response) GetAttribute(name string) string { - for _, attr := range r.Assertion.AttributeStatement.Attributes { - if attr.Name == name || attr.FriendlyName == name { - return attr.AttributeValues[0].Value - } - } - return "" -} - -func (r *Response) GetAttributeValues(name string) []string { - var values []string - for _, attr := range r.Assertion.AttributeStatement.Attributes { - if attr.Name == name || attr.FriendlyName == name { - for _, v := range attr.AttributeValues { - values = append(values, v.Value) - } - } - } - return values -} - -func (r *Response) GetAttributes() map[string][]string { - attrs := make(map[string][]string) - for _, attr := range r.Assertion.AttributeStatement.Attributes { - var values []string - for _, v := range attr.AttributeValues { - values = append(values, v.Value) - } - attrs[attr.Name] = values - } - return attrs -} diff --git a/share/auth/saml/response_test.go b/share/auth/saml/response_test.go deleted file mode 100644 index 5bc05eca9..000000000 --- a/share/auth/saml/response_test.go +++ /dev/null @@ -1,49 +0,0 @@ -package saml - -import ( - "testing" -) - -func TestParseResponse(t *testing.T) { - query := "SAMLResponse=PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c2FtbDJwOlJlc3BvbnNlIHhtbG5zOnNhbWwycD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnByb3RvY29sIiBEZXN0aW5hdGlvbj0iaHR0cHM6Ly8xMC4yMTEuNTUuMTA6ODQ0My90b2tlbl9hdXRoX3NlcnZlciIgSUQ9ImlkMTE1ODQ5NzQxODI5NzQ2NDgxMTYxNzUyMzUxIiBJblJlc3BvbnNlVG89Il9jNDc4NmJmYi03ZWVkLTQ4NmMtNDZhNS05NWI0YmExNTQzZmIiIElzc3VlSW5zdGFudD0iMjAxNy0wOS0yMFQyMjo0NzowNi4wMjRaIiBWZXJzaW9uPSIyLjAiIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSI%2BPHNhbWwyOklzc3VlciB4bWxuczpzYW1sMj0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiIgRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6bmFtZWlkLWZvcm1hdDplbnRpdHkiPmh0dHA6Ly93d3cub2t0YS5jb20vZXhrYmpncDlhMDRZS0l2cW8waDc8L3NhbWwyOklzc3Vlcj48ZHM6U2lnbmF0dXJlIHhtbG5zOmRzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjIj48ZHM6U2lnbmVkSW5mbz48ZHM6Q2Fub25pY2FsaXphdGlvbk1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPjxkczpTaWduYXR1cmVNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGRzaWctbW9yZSNyc2Etc2hhMjU2Ii8%2BPGRzOlJlZmVyZW5jZSBVUkk9IiNpZDExNTg0OTc0MTgyOTc0NjQ4MTE2MTc1MjM1MSI%2BPGRzOlRyYW5zZm9ybXM%2BPGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNlbnZlbG9wZWQtc2lnbmF0dXJlIi8%2BPGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyI%2BPGVjOkluY2x1c2l2ZU5hbWVzcGFjZXMgeG1sbnM6ZWM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIgUHJlZml4TGlzdD0ieHMiLz48L2RzOlRyYW5zZm9ybT48L2RzOlRyYW5zZm9ybXM%2BPGRzOkRpZ2VzdE1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jI3NoYTI1NiIvPjxkczpEaWdlc3RWYWx1ZT5TRkNIamVhc3ZXREJDTXFxcHA3clkwOGVXbDhpdDJib0lrQTBHMUtZU0hNPTwvZHM6RGlnZXN0VmFsdWU%2BPC9kczpSZWZlcmVuY2U%2BPC9kczpTaWduZWRJbmZvPjxkczpTaWduYXR1cmVWYWx1ZT5NNU5lMDJyNDBKdjZlOHNYWmZWd0Z6RjI4empRSERXaXp4Y0lmTlRBVHRZV2tZNlI3aHlxbnJoY1BRbHpnNUJPcDAvdldOSTIwdXdWZjhYZU03Vkd0Y1N1bHBmZE5pYjRMV00vOFhFOGV5NFBLVmVIanBmbWNBdVkvK2V5bHRBaE1GTEpkZEIxTzZmQjFwQm9GTTZXLzVYcFlxOXE3OGdGNitNZVNDTGlJUU40d3RHK3pSSlVrcjJsR0NDU05mUWtLdnFvZXZqUGRIcHZCSmhFQVhpcGRRRUVRb29LK29kd1dXdDVtY1EyMDQxcWx2eGVIbGNGYjZOQk1uUkIwSDU5SWpoMmtZeEZHcitramsyMkxkd3RFM1RuVGVYTFJ1Q1k4VVk5MEpOYlVnVE94WEJhYTJhMTR2NzBjM25UZ3JJZmIrUHloYUlZVlN6STcwRE84Vkora2c9PTwvZHM6U2lnbmF0dXJlVmFsdWU%2BPGRzOktleUluZm8%2BPGRzOlg1MDlEYXRhPjxkczpYNTA5Q2VydGlmaWNhdGU%2BTUlJRHBEQ0NBb3lnQXdJQkFnSUdBVjIrL3BEK01BMEdDU3FHU0liM0RRRUJDd1VBTUlHU01Rc3dDUVlEVlFRR0V3SlZVekVUTUJFRwpBMVVFQ0F3S1EyRnNhV1p2Y201cFlURVdNQlFHQTFVRUJ3d05VMkZ1SUVaeVlXNWphWE5qYnpFTk1Bc0dBMVVFQ2d3RVQydDBZVEVVCk1CSUdBMVVFQ3d3TFUxTlBVSEp2ZG1sa1pYSXhFekFSQmdOVkJBTU1DbVJsZGkweU5UVXpPRGd4SERBYUJna3Foa2lHOXcwQkNRRVcKRFdsdVptOUFiMnQwWVM1amIyMHdIaGNOTVRjd09EQTNNak14T0RBd1doY05NamN3T0RBM01qTXhPVEF3V2pDQmtqRUxNQWtHQTFVRQpCaE1DVlZNeEV6QVJCZ05WQkFnTUNrTmhiR2xtYjNKdWFXRXhGakFVQmdOVkJBY01EVk5oYmlCR2NtRnVZMmx6WTI4eERUQUxCZ05WCkJBb01CRTlyZEdFeEZEQVNCZ05WQkFzTUMxTlRUMUJ5YjNacFpHVnlNUk13RVFZRFZRUUREQXBrWlhZdE1qVTFNemc0TVJ3d0dnWUoKS29aSWh2Y05BUWtCRmcxcGJtWnZRRzlyZEdFdVkyOXRNSUlCSWpBTkJna3Foa2lHOXcwQkFRRUZBQU9DQVE4QU1JSUJDZ0tDQVFFQQppMERadVhsMGZkYkJpdFZOS3ZUajVEc3FkTXFkT3RhSGh4ZWxoSUxRVCtkek1iVzkwOGJUVW5NaEVDaFdtWFZxTjJKakZ6ZXkycEs1CmJzY2U2T0tHWTVYZSthc0c0V01ka2tIeWE2SjA0SW1nRlU0a1Q2cVlsNCt2cTN5R01zT2FHOFQ3TnNtY00wR2xMWklqYkVmT0N1Z1AKa043MTRzeURwemNKY0VmSmszV3E5RlNEVE1WRUNRaGpLM3NaRy9yNGlQNThTWmxUaEhBL1hYTWZGMHl5QzRRWEVGNnMrRFBzdGVGegpjeDIrQjh2aVNVR1p2R010eXNKMi9sTjFuazlxNU9RRWE0MkptUDBDdHpmQTU1UWJMWmtYdFNlR0p5d3FOMTBheWdGS0JBN3ZlMHF6Cmk2UTM5Uk5ndG84MTNvbFU3L3ozVlRsckFzZGhaWHJpMHhMUVFRSURBUUFCTUEwR0NTcUdTSWIzRFFFQkN3VUFBNElCQVFCcjJNbnAKR1ZmVjdkTVdobWJ0ZHJPTms0VW1VKysrVHRMT2cvS3Fxc1BxVlc5Zy9HM2xZSHFubmpTTGR4OEVLSk5KTUFOK3VKRllERGZjSHowYgpKU3lTRngrTGQrUkUwRjJLOURHTEliNDc2ZmhScm1LZTE0aXZ6Qm50ZGtvUHhrQnlXU0NScm9IMlAwQmh6U3dIZUlGTTNlQmR2RktFCnZQYXFON2owWFF2RnR5dGQwM0hsRU5jNEtBZExLbENZcjBIYWs0RVlRMXRadUJrK3p3RmVIUE0rYkZ5KzJlZjRDdmgyZEJsbzFhMFcKZStOaDNvUTlEdjlaWFJXZzVPTUxYL053ZnYyaVdMZW5EWmUzWkFoVGVzRWpyUXJxbThGT3REajBLbnJOVVYwZGErYVRHOTNaTXF4VQpta2RaNHJxeEhEaW1WZHNCQ2VWb3kwMXdlbUxLL2FGaDwvZHM6WDUwOUNlcnRpZmljYXRlPjwvZHM6WDUwOURhdGE%2BPC9kczpLZXlJbmZvPjwvZHM6U2lnbmF0dXJlPjxzYW1sMnA6U3RhdHVzIHhtbG5zOnNhbWwycD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnByb3RvY29sIj48c2FtbDJwOlN0YXR1c0NvZGUgVmFsdWU9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpzdGF0dXM6U3VjY2VzcyIvPjwvc2FtbDJwOlN0YXR1cz48c2FtbDI6QXNzZXJ0aW9uIHhtbG5zOnNhbWwyPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXNzZXJ0aW9uIiBJRD0iaWQxMTU4NDk3NDE4MzAwMTgzOTQ0NTAwODQ1NCIgSXNzdWVJbnN0YW50PSIyMDE3LTA5LTIwVDIyOjQ3OjA2LjAyNFoiIFZlcnNpb249IjIuMCIgeG1sbnM6eHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hIj48c2FtbDI6SXNzdWVyIEZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOm5hbWVpZC1mb3JtYXQ6ZW50aXR5IiB4bWxuczpzYW1sMj0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiI%2BaHR0cDovL3d3dy5va3RhLmNvbS9leGtiamdwOWEwNFlLSXZxbzBoNzwvc2FtbDI6SXNzdWVyPjxkczpTaWduYXR1cmUgeG1sbnM6ZHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyMiPjxkczpTaWduZWRJbmZvPjxkczpDYW5vbmljYWxpemF0aW9uTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIi8%2BPGRzOlNpZ25hdHVyZU1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZHNpZy1tb3JlI3JzYS1zaGEyNTYiLz48ZHM6UmVmZXJlbmNlIFVSST0iI2lkMTE1ODQ5NzQxODMwMDE4Mzk0NDUwMDg0NTQiPjxkczpUcmFuc2Zvcm1zPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjZW52ZWxvcGVkLXNpZ25hdHVyZSIvPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzEwL3htbC1leGMtYzE0biMiPjxlYzpJbmNsdXNpdmVOYW1lc3BhY2VzIHhtbG5zOmVjPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzEwL3htbC1leGMtYzE0biMiIFByZWZpeExpc3Q9InhzIi8%2BPC9kczpUcmFuc2Zvcm0%2BPC9kczpUcmFuc2Zvcm1zPjxkczpEaWdlc3RNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGVuYyNzaGEyNTYiLz48ZHM6RGlnZXN0VmFsdWU%2BOVRCTW5aZlNZdjcrVTBVbnlJNEdwdG8vaFZGZis3bTh2UXNTN1pvVHM1az08L2RzOkRpZ2VzdFZhbHVlPjwvZHM6UmVmZXJlbmNlPjwvZHM6U2lnbmVkSW5mbz48ZHM6U2lnbmF0dXJlVmFsdWU%2BZEMreEZ5RCtBaDlmS3YrSkpjbnZ4MHZsTEUzYXhZZEhkaXd5Wk5aS1NhdVc0UDViTnlBMEkxU1VlVFFOLy9tV0t1OExkb3lPZDl2NFFWblpkMENtTGRFZXRLZUwwQTd3VlZva25Ua00xdWkxbnN3OExScU9Ld0M5ZFpPcWM1Y05PNFpoS1FZbDZZUjY3WmZjVWVXUWl6N0t2VTRJNytsRVFMcENHOXRya1J1dkZUTGhLWFlMSWpVWWVSWmhKcy9pYzMzYS9NKzM2anVsQjFmOTU3eHoxTFVZRW8raXVHRklmay9Ld0tSK1cwWW1rNi9kSUZHQlVpeld3Tm9iN1lsVkp1blR3MjZ6ZTNlU293dWp3dFlGd2V4amlTc05lQTRQaktVdkVISVFFdkVOclF6czk4UU9UbUtHS2JNL2R4NDI3WWRMTmZWYzhKQWM5Z2FSek1iUXFnPT08L2RzOlNpZ25hdHVyZVZhbHVlPjxkczpLZXlJbmZvPjxkczpYNTA5RGF0YT48ZHM6WDUwOUNlcnRpZmljYXRlPk1JSURwRENDQW95Z0F3SUJBZ0lHQVYyKy9wRCtNQTBHQ1NxR1NJYjNEUUVCQ3dVQU1JR1NNUXN3Q1FZRFZRUUdFd0pWVXpFVE1CRUcKQTFVRUNBd0tRMkZzYVdadmNtNXBZVEVXTUJRR0ExVUVCd3dOVTJGdUlFWnlZVzVqYVhOamJ6RU5NQXNHQTFVRUNnd0VUMnQwWVRFVQpNQklHQTFVRUN3d0xVMU5QVUhKdmRtbGtaWEl4RXpBUkJnTlZCQU1NQ21SbGRpMHlOVFV6T0RneEhEQWFCZ2txaGtpRzl3MEJDUUVXCkRXbHVabTlBYjJ0MFlTNWpiMjB3SGhjTk1UY3dPREEzTWpNeE9EQXdXaGNOTWpjd09EQTNNak14T1RBd1dqQ0JrakVMTUFrR0ExVUUKQmhNQ1ZWTXhFekFSQmdOVkJBZ01Da05oYkdsbWIzSnVhV0V4RmpBVUJnTlZCQWNNRFZOaGJpQkdjbUZ1WTJselkyOHhEVEFMQmdOVgpCQW9NQkU5cmRHRXhGREFTQmdOVkJBc01DMU5UVDFCeWIzWnBaR1Z5TVJNd0VRWURWUVFEREFwa1pYWXRNalUxTXpnNE1Sd3dHZ1lKCktvWklodmNOQVFrQkZnMXBibVp2UUc5cmRHRXVZMjl0TUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQ0FRRUEKaTBEWnVYbDBmZGJCaXRWTkt2VGo1RHNxZE1xZE90YUhoeGVsaElMUVQrZHpNYlc5MDhiVFVuTWhFQ2hXbVhWcU4ySmpGemV5MnBLNQpic2NlNk9LR1k1WGUrYXNHNFdNZGtrSHlhNkowNEltZ0ZVNGtUNnFZbDQrdnEzeUdNc09hRzhUN05zbWNNMEdsTFpJamJFZk9DdWdQCmtONzE0c3lEcHpjSmNFZkprM1dxOUZTRFRNVkVDUWhqSzNzWkcvcjRpUDU4U1psVGhIQS9YWE1mRjB5eUM0UVhFRjZzK0RQc3RlRnoKY3gyK0I4dmlTVUdadkdNdHlzSjIvbE4xbms5cTVPUUVhNDJKbVAwQ3R6ZkE1NVFiTFprWHRTZUdKeXdxTjEwYXlnRktCQTd2ZTBxegppNlEzOVJOZ3RvODEzb2xVNy96M1ZUbHJBc2RoWlhyaTB4TFFRUUlEQVFBQk1BMEdDU3FHU0liM0RRRUJDd1VBQTRJQkFRQnIyTW5wCkdWZlY3ZE1XaG1idGRyT05rNFVtVSsrK1R0TE9nL0txcXNQcVZXOWcvRzNsWUhxbm5qU0xkeDhFS0pOSk1BTit1SkZZRERmY0h6MGIKSlN5U0Z4K0xkK1JFMEYySzlER0xJYjQ3NmZoUnJtS2UxNGl2ekJudGRrb1B4a0J5V1NDUnJvSDJQMEJoelN3SGVJRk0zZUJkdkZLRQp2UGFxTjdqMFhRdkZ0eXRkMDNIbEVOYzRLQWRMS2xDWXIwSGFrNEVZUTF0WnVCayt6d0ZlSFBNK2JGeSsyZWY0Q3ZoMmRCbG8xYTBXCmUrTmgzb1E5RHY5WlhSV2c1T01MWC9Od2Z2MmlXTGVuRFplM1pBaFRlc0VqclFycW04Rk90RGowS25yTlVWMGRhK2FURzkzWk1xeFUKbWtkWjRycXhIRGltVmRzQkNlVm95MDF3ZW1MSy9hRmg8L2RzOlg1MDlDZXJ0aWZpY2F0ZT48L2RzOlg1MDlEYXRhPjwvZHM6S2V5SW5mbz48L2RzOlNpZ25hdHVyZT48c2FtbDI6U3ViamVjdCB4bWxuczpzYW1sMj0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiI%2BPHNhbWwyOk5hbWVJRCBGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjEuMTpuYW1laWQtZm9ybWF0OnVuc3BlY2lmaWVkIj5hdHVuZ0BuZXV2ZWN0b3IuY29tPC9zYW1sMjpOYW1lSUQ%2BPHNhbWwyOlN1YmplY3RDb25maXJtYXRpb24gTWV0aG9kPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6Y206YmVhcmVyIj48c2FtbDI6U3ViamVjdENvbmZpcm1hdGlvbkRhdGEgSW5SZXNwb25zZVRvPSJfYzQ3ODZiZmItN2VlZC00ODZjLTQ2YTUtOTViNGJhMTU0M2ZiIiBOb3RPbk9yQWZ0ZXI9IjIwMTctMDktMjBUMjI6NTI6MDYuMDI0WiIgUmVjaXBpZW50PSJodHRwczovLzEwLjIxMS41NS4xMDo4NDQzL3Rva2VuX2F1dGhfc2VydmVyIi8%2BPC9zYW1sMjpTdWJqZWN0Q29uZmlybWF0aW9uPjwvc2FtbDI6U3ViamVjdD48c2FtbDI6Q29uZGl0aW9ucyBOb3RCZWZvcmU9IjIwMTctMDktMjBUMjI6NDI6MDYuMDI0WiIgTm90T25PckFmdGVyPSIyMDE3LTA5LTIwVDIyOjUyOjA2LjAyNFoiIHhtbG5zOnNhbWwyPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXNzZXJ0aW9uIj48c2FtbDI6QXVkaWVuY2VSZXN0cmljdGlvbj48c2FtbDI6QXVkaWVuY2U%2BaHR0cDovLzEwLjEuNS4xNzo1MDAwL3NhbWwvc3NvL2V4YW1wbGUtb2t0YS1jb208L3NhbWwyOkF1ZGllbmNlPjwvc2FtbDI6QXVkaWVuY2VSZXN0cmljdGlvbj48L3NhbWwyOkNvbmRpdGlvbnM%2BPHNhbWwyOkF1dGhuU3RhdGVtZW50IEF1dGhuSW5zdGFudD0iMjAxNy0wOS0yMFQyMTozOTo1MS4yNjlaIiBTZXNzaW9uSW5kZXg9Il9jNDc4NmJmYi03ZWVkLTQ4NmMtNDZhNS05NWI0YmExNTQzZmIiIHhtbG5zOnNhbWwyPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXNzZXJ0aW9uIj48c2FtbDI6QXV0aG5Db250ZXh0PjxzYW1sMjpBdXRobkNvbnRleHRDbGFzc1JlZj51cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YWM6Y2xhc3NlczpQYXNzd29yZFByb3RlY3RlZFRyYW5zcG9ydDwvc2FtbDI6QXV0aG5Db250ZXh0Q2xhc3NSZWY%2BPC9zYW1sMjpBdXRobkNvbnRleHQ%2BPC9zYW1sMjpBdXRoblN0YXRlbWVudD48c2FtbDI6QXR0cmlidXRlU3RhdGVtZW50IHhtbG5zOnNhbWwyPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXNzZXJ0aW9uIj48c2FtbDI6QXR0cmlidXRlIE5hbWU9IkZpcnN0TmFtZSIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDp1bnNwZWNpZmllZCI%2BPHNhbWwyOkF0dHJpYnV0ZVZhbHVlIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgeG1sbnM6eHNpPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYS1pbnN0YW5jZSIgeHNpOnR5cGU9InhzOnN0cmluZyI%2BYW5kc29uPC9zYW1sMjpBdHRyaWJ1dGVWYWx1ZT48L3NhbWwyOkF0dHJpYnV0ZT48c2FtbDI6QXR0cmlidXRlIE5hbWU9Ikxhc3ROYW1lIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OnVuc3BlY2lmaWVkIj48c2FtbDI6QXR0cmlidXRlVmFsdWUgeG1sbnM6eHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hIiB4bWxuczp4c2k9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hLWluc3RhbmNlIiB4c2k6dHlwZT0ieHM6c3RyaW5nIj50dW5nPC9zYW1sMjpBdHRyaWJ1dGVWYWx1ZT48L3NhbWwyOkF0dHJpYnV0ZT48c2FtbDI6QXR0cmlidXRlIE5hbWU9IkVtYWlsIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OnVuc3BlY2lmaWVkIj48c2FtbDI6QXR0cmlidXRlVmFsdWUgeG1sbnM6eHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hIiB4bWxuczp4c2k9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hLWluc3RhbmNlIiB4c2k6dHlwZT0ieHM6c3RyaW5nIj5hdHVuZ0BuZXV2ZWN0b3IuY29tPC9zYW1sMjpBdHRyaWJ1dGVWYWx1ZT48L3NhbWwyOkF0dHJpYnV0ZT48c2FtbDI6QXR0cmlidXRlIE5hbWU9IkFkbWluR3JvdXAiIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6dW5zcGVjaWZpZWQiPjxzYW1sMjpBdHRyaWJ1dGVWYWx1ZSB4bWxuczp4cz0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEiIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIHhzaTp0eXBlPSJ4czpzdHJpbmciPmFkbWluPC9zYW1sMjpBdHRyaWJ1dGVWYWx1ZT48L3NhbWwyOkF0dHJpYnV0ZT48c2FtbDI6QXR0cmlidXRlIE5hbWU9IlJlYWRlckdyb3VwIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OnVuc3BlY2lmaWVkIj48c2FtbDI6QXR0cmlidXRlVmFsdWUgeG1sbnM6eHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hIiB4bWxuczp4c2k9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hLWluc3RhbmNlIiB4c2k6dHlwZT0ieHM6c3RyaW5nIj5yZWFkZXI8L3NhbWwyOkF0dHJpYnV0ZVZhbHVlPjwvc2FtbDI6QXR0cmlidXRlPjwvc2FtbDI6QXR0cmlidXRlU3RhdGVtZW50Pjwvc2FtbDI6QXNzZXJ0aW9uPjwvc2FtbDJwOlJlc3BvbnNlPg%3D%3D&RelayState=" - - sp := ServiceProvider{ - IDPSSOURL: "https://dev-255388.oktapreview.com/app/neuvectordev255388_examplesamlapplication_1/exkbjgp9a04YKIvqo0h7/sso/saml", - IDPSSODescriptorURL: "http://www.okta.com/exkbjgp9a04YKIvqo0h7", - IDPPublicCert: `-----BEGIN CERTIFICATE----- -MIIDpDCCAoygAwIBAgIGAV2+/pD+MA0GCSqGSIb3DQEBCwUAMIGSMQswCQYDVQQGEwJVUzETMBEG -A1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzENMAsGA1UECgwET2t0YTEU -MBIGA1UECwwLU1NPUHJvdmlkZXIxEzARBgNVBAMMCmRldi0yNTUzODgxHDAaBgkqhkiG9w0BCQEW -DWluZm9Ab2t0YS5jb20wHhcNMTcwODA3MjMxODAwWhcNMjcwODA3MjMxOTAwWjCBkjELMAkGA1UE -BhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcMDVNhbiBGcmFuY2lzY28xDTALBgNV -BAoMBE9rdGExFDASBgNVBAsMC1NTT1Byb3ZpZGVyMRMwEQYDVQQDDApkZXYtMjU1Mzg4MRwwGgYJ -KoZIhvcNAQkBFg1pbmZvQG9rdGEuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA -i0DZuXl0fdbBitVNKvTj5DsqdMqdOtaHhxelhILQT+dzMbW908bTUnMhEChWmXVqN2JjFzey2pK5 -bsce6OKGY5Xe+asG4WMdkkHya6J04ImgFU4kT6qYl4+vq3yGMsOaG8T7NsmcM0GlLZIjbEfOCugP -kN714syDpzcJcEfJk3Wq9FSDTMVECQhjK3sZG/r4iP58SZlThHA/XXMfF0yyC4QXEF6s+DPsteFz -cx2+B8viSUGZvGMtysJ2/lN1nk9q5OQEa42JmP0CtzfA55QbLZkXtSeGJywqN10aygFKBA7ve0qz -i6Q39RNgto813olU7/z3VTlrAsdhZXri0xLQQQIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQBr2Mnp -GVfV7dMWhmbtdrONk4UmU+++TtLOg/KqqsPqVW9g/G3lYHqnnjSLdx8EKJNJMAN+uJFYDDfcHz0b -JSySFx+Ld+RE0F2K9DGLIb476fhRrmKe14ivzBntdkoPxkByWSCRroH2P0BhzSwHeIFM3eBdvFKE -vPaqN7j0XQvFtytd03HlENc4KAdLKlCYr0Hak4EYQ1tZuBk+zwFeHPM+bFy+2ef4Cvh2dBlo1a0W -e+Nh3oQ9Dv9ZXRWg5OMLX/Nwfv2iWLenDZe3ZAhTesEjrQrqm8FOtDj0KnrNUV0da+aTG93ZMqxU -mkdZ4rqxHDimVdsBCeVoy01wemLK/aFh ------END CERTIFICATE----- -`, - } - - resp, err := ParseSAMLResponse(query) - if err != nil { - t.Errorf("Parse SAML response error: %v\n", err.Error()) - } - - err = resp.Validate(&sp, false) - if err != nil { - t.Errorf("Validate SAML signature error: %v\n", err.Error()) - } - - attrs := resp.GetAttributes() - if v, ok := attrs["LastName"]; !ok || len(v) != 1 || v[0] != "tung" { - t.Errorf("Error in attributes: %v\n", attrs) - } -} diff --git a/share/auth/saml/saml.go b/share/auth/saml/saml.go deleted file mode 100644 index c01cdb1f9..000000000 --- a/share/auth/saml/saml.go +++ /dev/null @@ -1,30 +0,0 @@ -package saml - -type ServiceProvider struct { - PublicCertPath string - PrivateKeyPath string - IDPSSOURL string - IDPSSODescriptorURL string - IDPPublicCert string - AssertionConsumerServiceURL string - SPSignRequest bool - - publicCert string - privateKey string -} - -// GetSignedAuthnRequest returns a singed XML document that represents a AuthnRequest SAML document -func (s *ServiceProvider) GetAuthnRequest() *AuthnRequest { - r := newAuthnRequest() - r.AssertionConsumerServiceURL = s.AssertionConsumerServiceURL - r.Destination = s.IDPSSOURL - r.Issuer.Url = s.IDPSSODescriptorURL - r.Signature.KeyInfo.X509Data.X509Certificate.Cert = loadCertificate(s.IDPPublicCert) - - if !s.SPSignRequest { - r.SAMLSIG = "" - r.Signature = nil - } - - return r -} diff --git a/share/auth/saml/types.go b/share/auth/saml/types.go deleted file mode 100644 index 3b09b0fb5..000000000 --- a/share/auth/saml/types.go +++ /dev/null @@ -1,296 +0,0 @@ -package saml - -import "encoding/xml" - -type AuthnRequest struct { - XMLName xml.Name - SAMLP string `xml:"xmlns:samlp,attr"` - SAML string `xml:"xmlns:saml,attr"` - SAMLSIG string `xml:"xmlns:samlsig,attr,omitempty"` - ID string `xml:"ID,attr"` - Version string `xml:"Version,attr"` - ProtocolBinding string `xml:"ProtocolBinding,attr"` - AssertionConsumerServiceURL string `xml:"AssertionConsumerServiceURL,attr"` - Destination string `xml:"Destination,attr"` - IssueInstant string `xml:"IssueInstant,attr"` - // AssertionConsumerServiceIndex int `xml:"AssertionConsumerServiceIndex,attr"` - // AttributeConsumingServiceIndex int `xml:"AttributeConsumingServiceIndex,attr"` - Issuer Issuer `xml:"Issuer"` - NameIDPolicy NameIDPolicy `xml:"NameIDPolicy"` - // RequestedAuthnContext RequestedAuthnContext `xml:"RequestedAuthnContext"` - Signature *Signature `xml:"Signature,omitempty"` - originalString string -} - -type Issuer struct { - XMLName xml.Name - SAML string `xml:"xmlns:saml,attr"` - Url string `xml:",innerxml"` -} - -type NameIDPolicy struct { - XMLName xml.Name - AllowCreate bool `xml:"AllowCreate,attr"` - Format string `xml:"Format,attr"` -} - -type RequestedAuthnContext struct { - XMLName xml.Name - SAMLP string `xml:"xmlns:samlp,attr"` - Comparison string `xml:"Comparison,attr"` - AuthnContextClassRef AuthnContextClassRef `xml:"AuthnContextClassRef"` -} - -type AuthnContextClassRef struct { - XMLName xml.Name - SAML string `xml:"xmlns:saml,attr"` - Transport string `xml:",innerxml"` -} - -type Signature struct { - XMLName xml.Name - Id string `xml:"Id,attr"` - SignedInfo SignedInfo - SignatureValue SignatureValue - KeyInfo KeyInfo -} - -type SignedInfo struct { - XMLName xml.Name - CanonicalizationMethod CanonicalizationMethod - SignatureMethod SignatureMethod - SamlsigReference SamlsigReference -} - -type SignatureValue struct { - XMLName xml.Name - Value string `xml:",innerxml"` -} - -type KeyInfo struct { - XMLName xml.Name - X509Data X509Data `xml:",innerxml"` -} - -type CanonicalizationMethod struct { - XMLName xml.Name - Algorithm string `xml:"Algorithm,attr"` -} - -type SignatureMethod struct { - XMLName xml.Name - Algorithm string `xml:"Algorithm,attr"` -} - -type SamlsigReference struct { - XMLName xml.Name - URI string `xml:"URI,attr"` - Transforms Transforms `xml:",innerxml"` - DigestMethod DigestMethod `xml:",innerxml"` - DigestValue DigestValue `xml:",innerxml"` -} - -type X509Data struct { - XMLName xml.Name - X509Certificate X509Certificate `xml:",innerxml"` -} - -type Transforms struct { - XMLName xml.Name - Transform []Transform -} - -type DigestMethod struct { - XMLName xml.Name - Algorithm string `xml:"Algorithm,attr"` -} - -type DigestValue struct { - XMLName xml.Name -} - -type X509Certificate struct { - XMLName xml.Name - Cert string `xml:",innerxml"` -} - -type Transform struct { - XMLName xml.Name - Algorithm string `xml:"Algorithm,attr"` -} - -type EntityDescriptor struct { - XMLName xml.Name - DS string `xml:"xmlns:ds,attr"` - XMLNS string `xml:"xmlns,attr"` - MD string `xml:"xmlns:md,attr"` - EntityId string `xml:"entityID,attr"` - - Extensions Extensions `xml:"Extensions"` - SPSSODescriptor SPSSODescriptor `xml:"SPSSODescriptor"` -} - -type Extensions struct { - XMLName xml.Name - Alg string `xml:"xmlns:alg,attr"` - MDAttr string `xml:"xmlns:mdattr,attr"` - MDRPI string `xml:"xmlns:mdrpi,attr"` - - EntityAttributes string `xml:"EntityAttributes"` -} - -type SPSSODescriptor struct { - XMLName xml.Name - ProtocolSupportEnumeration string `xml:"protocolSupportEnumeration,attr"` - SigningKeyDescriptor KeyDescriptor - EncryptionKeyDescriptor KeyDescriptor - // SingleLogoutService SingleLogoutService `xml:"SingleLogoutService"` - AssertionConsumerServices []AssertionConsumerService -} - -type EntityAttributes struct { - XMLName xml.Name - SAML string `xml:"xmlns:saml,attr"` - - EntityAttributes []Attribute `xml:"Attribute"` // should be array?? -} - -type SPSSODescriptors struct { -} - -type KeyDescriptor struct { - XMLName xml.Name - Use string `xml:"use,attr"` - KeyInfo KeyInfo `xml:"KeyInfo"` -} - -type SingleLogoutService struct { - Binding string `xml:"Binding,attr"` - Location string `xml:"Location,attr"` -} - -type AssertionConsumerService struct { - XMLName xml.Name - Binding string `xml:"Binding,attr"` - Location string `xml:"Location,attr"` - Index string `xml:"index,attr"` -} - -type Response struct { - XMLName xml.Name - SAMLP string `xml:"xmlns:samlp,attr"` - SAML string `xml:"xmlns:saml,attr"` - SAMLSIG string `xml:"xmlns:samlsig,attr"` - Destination string `xml:"Destination,attr"` - ID string `xml:"ID,attr"` - Version string `xml:"Version,attr"` - IssueInstant string `xml:"IssueInstant,attr"` - InResponseTo string `xml:"InResponseTo,attr"` - - Issuer Issuer `xml:"Issuer"` - Signature Signature `xml:"Signature"` - Status Status `xml:"Status"` - Assertion Assertion `xml:"Assertion"` - - bytesXML []byte -} - -type Assertion struct { - XMLName xml.Name - ID string `xml:"ID,attr"` - Version string `xml:"Version,attr"` - XS string `xml:"xmlns:xs,attr"` - XSI string `xml:"xmlns:xsi,attr"` - SAML string `xml:"xmlns:saml,attr"` - IssueInstant string `xml:"IssueInstant,attr"` - Issuer Issuer `xml:"Issuer"` - Subject Subject - Conditions Conditions - AuthnStatements []AuthnStatement `xml:"AuthnStatement,omitempty"` - AttributeStatement AttributeStatement -} - -type Conditions struct { - XMLName xml.Name - NotBefore string `xml:",attr"` - NotOnOrAfter string `xml:",attr"` - AudienceRestrictions []AudienceRestriction `xml:"AudienceRestriction,omitempty"` -} - -type AudienceRestriction struct { - XMLName xml.Name - Audiences []Audience `xml:"Audience"` -} - -type Audience struct { - XMLName xml.Name - Value string `xml:",innerxml"` -} - -type Subject struct { - XMLName xml.Name - NameID NameID - SubjectConfirmation SubjectConfirmation -} - -type SubjectConfirmation struct { - XMLName xml.Name - Method string `xml:",attr"` - SubjectConfirmationData SubjectConfirmationData -} - -type Status struct { - XMLName xml.Name - StatusCode StatusCode `xml:"StatusCode"` -} - -type SubjectConfirmationData struct { - XMLName xml.Name - InResponseTo string `xml:",attr"` - NotOnOrAfter string `xml:",attr"` - Recipient string `xml:",attr"` -} - -type NameID struct { - XMLName xml.Name - Format string `xml:",attr"` - SPNameQualifier string `xml:",attr,omitempty"` - Value string `xml:",innerxml"` -} - -type StatusCode struct { - XMLName xml.Name - Value string `xml:",attr"` -} - -type AttributeValue struct { - XMLName xml.Name - Type string `xml:"xsi:type,attr"` - Value string `xml:",innerxml"` -} - -type Attribute struct { - XMLName xml.Name - Name string `xml:",attr"` - FriendlyName string `xml:",attr,omitempty"` - NameFormat string `xml:",attr"` - AttributeValues []AttributeValue `xml:"AttributeValue"` -} - -type AttributeStatement struct { - XMLName xml.Name - Attributes []Attribute `xml:"Attribute"` -} - -type AuthnStatement struct { - XMLName xml.Name - AuthnInstant string `xml:",attr"` - SessionNotOnOrAfter string `xml:",attr,omitempty"` - SessionIndex string `xml:",attr,omitempty"` - AuthnContext AuthnContext `xml:"AuthnContext"` -} - -type AuthnContext struct { - XMLName xml.Name - AuthnContextClassRef AuthnContextClassRef `xml:"AuthnContextClassRef"` -} diff --git a/share/auth/saml/utils.go b/share/auth/saml/utils.go deleted file mode 100644 index cebc536e5..000000000 --- a/share/auth/saml/utils.go +++ /dev/null @@ -1,58 +0,0 @@ -package saml - -import ( - "bytes" - "compress/flate" - "fmt" - "io" - "regexp" - "strings" - - "github.com/nu7hatch/gouuid" -) - -func getID() string { - u, _ := uuid.NewV4() - return fmt.Sprintf("_%s", u.String()) -} - -func compressString(in string) string { - buf := new(bytes.Buffer) - compressor, _ := flate.NewWriter(buf, 9) - compressor.Write([]byte(in)) - compressor.Close() - return buf.String() -} - -func decompressString(in string) string { - buf := new(bytes.Buffer) - decompressor := flate.NewReader(strings.NewReader(in)) - io.Copy(buf, decompressor) - decompressor.Close() - return buf.String() -} - -func compress(in []byte) []byte { - buf := new(bytes.Buffer) - compressor, _ := flate.NewWriter(buf, 9) - compressor.Write(in) - compressor.Close() - return buf.Bytes() -} - -func decompress(in []byte) []byte { - buf := new(bytes.Buffer) - decompressor := flate.NewReader(bytes.NewReader(in)) - io.Copy(buf, decompressor) - decompressor.Close() - return buf.Bytes() -} - -func loadCertificate(cert string) string { - re := regexp.MustCompile("---(.*)CERTIFICATE(.*)---") - out := re.ReplaceAllString(cert, "") - out = strings.Trim(out, " \n") - out = strings.Replace(out, "\n", "", -1) - - return out -} diff --git a/share/clus_apis.go b/share/clus_apis.go index 10db654cf..a8cd593e4 100644 --- a/share/clus_apis.go +++ b/share/clus_apis.go @@ -863,11 +863,16 @@ type CLUSServerLDAP struct { type CLUSServerSAML struct { CLUSServerAuth - SSOURL string `json:"sso_url"` - Issuer string `json:"issuer"` - X509Cert string `json:"x509_cert,cloak"` - GroupClaim string `json:"group_claim"` - X509CertExtra []string `json:"x509_cert_extra"` + SSOURL string `json:"sso_url"` + Issuer string `json:"issuer"` + X509Cert string `json:"x509_cert,cloak"` + GroupClaim string `json:"group_claim"` + X509CertExtra []string `json:"x509_cert_extra"` + AuthnSigningEnabled bool `json:"authn_signing_enabled,omitempty"` + SigningCert string `json:"signing_cert,cloak,omitempty"` + SigningKey string `json:"signing_key,cloak,omitempty"` + SLOEnabled bool `json:"slo_enabled,omitempty"` + SLOURL string `json:"slo_url,omitempty"` } type CLUSServerOIDC struct { diff --git a/vendor/github.com/beevik/etree/.travis.yml b/vendor/github.com/beevik/etree/.travis.yml deleted file mode 100644 index f4cb25d47..000000000 --- a/vendor/github.com/beevik/etree/.travis.yml +++ /dev/null @@ -1,14 +0,0 @@ -language: go -sudo: false - -go: - - 1.11.x - - tip - -matrix: - allow_failures: - - go: tip - -script: - - go vet ./... - - go test -v ./... diff --git a/vendor/github.com/beevik/etree/CONTRIBUTORS b/vendor/github.com/beevik/etree/CONTRIBUTORS index 03211a85e..da4740767 100644 --- a/vendor/github.com/beevik/etree/CONTRIBUTORS +++ b/vendor/github.com/beevik/etree/CONTRIBUTORS @@ -8,3 +8,5 @@ Nicolas Piganeau (npiganeau) Chris Brown (ccbrown) Earncef Sequeira (earncef) Gabriel de Labachelerie (wuzuf) +Martin Dosch (mdosch) +Hugo Wetterberg (hugowetterberg) diff --git a/vendor/github.com/beevik/etree/LICENSE b/vendor/github.com/beevik/etree/LICENSE index 26f1f7751..ef7b286ac 100644 --- a/vendor/github.com/beevik/etree/LICENSE +++ b/vendor/github.com/beevik/etree/LICENSE @@ -1,4 +1,4 @@ -Copyright 2015-2019 Brett Vickers. All rights reserved. +Copyright 2015-2023 Brett Vickers. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions diff --git a/vendor/github.com/beevik/etree/README.md b/vendor/github.com/beevik/etree/README.md index 08ec26b0a..62f0babcb 100644 --- a/vendor/github.com/beevik/etree/README.md +++ b/vendor/github.com/beevik/etree/README.md @@ -1,4 +1,3 @@ -[![Build Status](https://travis-ci.org/beevik/etree.svg?branch=master)](https://travis-ci.org/beevik/etree) [![GoDoc](https://godoc.org/github.com/beevik/etree?status.svg)](https://godoc.org/github.com/beevik/etree) etree diff --git a/vendor/github.com/beevik/etree/RELEASE_NOTES.md b/vendor/github.com/beevik/etree/RELEASE_NOTES.md index ee59d7abf..4a2ce2ab2 100644 --- a/vendor/github.com/beevik/etree/RELEASE_NOTES.md +++ b/vendor/github.com/beevik/etree/RELEASE_NOTES.md @@ -1,3 +1,47 @@ +Release v1.2.0 +============== + +**New Features** + +* Add the ability to write XML fragments using Token WriteTo functions. +* Add the ability to re-indent an XML element as though it were the root of + the document. +* Add a ReadSettings option to preserve CDATA blocks when reading and XML + document. + +Release v1.1.4 +============== + +**New Features** + +* Add the ability to preserve whitespace in leaf elements during indent. +* Add the ability to suppress a document-trailing newline during indent. +* Add choice of XML attribute quoting style (single-quote or double-quote). + +**Removed Features** + +* Removed the CDATA preservation change introduced in v1.1.3. It was + implemented in a way that broke the ability to process XML documents + encoded using non-UTF8 character sets. + +Release v1.1.3 +============== + +* XML reads now preserve CDATA sections instead of converting them to + standard character data. + +Release v1.1.2 +============== + +* Fixed a path parsing bug. +* The `Element.Text` function now handles comments embedded between + character data spans. + +Release v1.1.1 +============== + +* Updated go version in `go.mod` to 1.20 + Release v1.1.0 ============== diff --git a/vendor/github.com/beevik/etree/etree.go b/vendor/github.com/beevik/etree/etree.go index 9e24f9012..83df8b2f4 100644 --- a/vendor/github.com/beevik/etree/etree.go +++ b/vendor/github.com/beevik/etree/etree.go @@ -18,14 +18,19 @@ import ( ) const ( - // NoIndent is used with Indent to disable all indenting. + // NoIndent is used with the IndentSettings record to remove all + // indenting. NoIndent = -1 ) // ErrXML is returned when XML parsing fails due to incorrect formatting. var ErrXML = errors.New("etree: invalid XML format") -// ReadSettings allow for changing the default behavior of the ReadFrom* +// cdataPrefix is used to detect CDATA text when ReadSettings.PreserveCData is +// true. +var cdataPrefix = []byte(" and '. Default: false. CanonicalAttrVal bool - // When outputting indented XML, use a carriage return and linefeed - // ("\r\n") as a new-line delimiter instead of just a linefeed ("\n"). - // This is useful on Windows-based systems. + // AttrSingleQuote causes attributes to use single quotes (attr='example') + // instead of double quotes (attr = "example") when set to true. Default: + // false. + AttrSingleQuote bool + + // UseCRLF causes the document's Indent* methods to use a carriage return + // followed by a linefeed ("\r\n") when outputting a newline. If false, + // only a linefeed is used ("\n"). Default: false. + // + // Deprecated: UseCRLF is deprecated. Use IndentSettings.UseCRLF instead. UseCRLF bool } @@ -78,25 +113,104 @@ func newWriteSettings() WriteSettings { CanonicalEndTags: false, CanonicalText: false, CanonicalAttrVal: false, + AttrSingleQuote: false, UseCRLF: false, } } -// A Token is an empty interface that represents an Element, CharData, -// Comment, Directive, or ProcInst. +// dup creates a duplicate of the WriteSettings object. +func (s *WriteSettings) dup() WriteSettings { + return *s +} + +// IndentSettings determine the behavior of the Document's Indent* methods. +type IndentSettings struct { + // Spaces indicates the number of spaces to insert for each level of + // indentation. Set to etree.NoIndent to remove all indentation. Ignored + // when UseTabs is true. Default: 4. + Spaces int + + // UseTabs causes tabs to be used instead of spaces when indenting. + // Default: false. + UseTabs bool + + // UseCRLF causes newlines to be written as a carriage return followed by + // a linefeed ("\r\n"). If false, only a linefeed character is output + // for a newline ("\n"). Default: false. + UseCRLF bool + + // PreserveLeafWhitespace causes indent methods to preserve whitespace + // within XML elements containing only non-CDATA character data. Default: + // false. + PreserveLeafWhitespace bool + + // SuppressTrailingWhitespace suppresses the generation of a trailing + // whitespace characters (such as newlines) at the end of the indented + // document. Default: false. + SuppressTrailingWhitespace bool +} + +// NewIndentSettings creates a default IndentSettings record. +func NewIndentSettings() *IndentSettings { + return &IndentSettings{ + Spaces: 4, + UseTabs: false, + UseCRLF: false, + PreserveLeafWhitespace: false, + SuppressTrailingWhitespace: false, + } +} + +type indentFunc func(depth int) string + +func getIndentFunc(s *IndentSettings) indentFunc { + if s.UseTabs { + if s.UseCRLF { + return func(depth int) string { return indentCRLF(depth, indentTabs) } + } else { + return func(depth int) string { return indentLF(depth, indentTabs) } + } + } else { + if s.Spaces < 0 { + return func(depth int) string { return "" } + } else if s.UseCRLF { + return func(depth int) string { return indentCRLF(depth*s.Spaces, indentSpaces) } + } else { + return func(depth int) string { return indentLF(depth*s.Spaces, indentSpaces) } + } + } +} + +// Writer is the interface that wraps the Write* methods called by each token +// type's WriteTo function. +type Writer interface { + io.StringWriter + io.ByteWriter + io.Writer +} + +// A Token is an interface type used to represent XML elements, character +// data, CDATA sections, XML comments, XML directives, and XML processing +// instructions. type Token interface { Parent() *Element Index() int + WriteTo(w Writer, s *WriteSettings) dup(parent *Element) Token setParent(parent *Element) setIndex(index int) - writeTo(w *bufio.Writer, s *WriteSettings) } -// A Document is a container holding a complete XML hierarchy. Its embedded -// element contains zero or more children, one of which is usually the root -// element. The embedded element may include other children such as -// processing instructions or BOM CharData tokens. +// A Document is a container holding a complete XML tree. +// +// A document has a single embedded element, which contains zero or more child +// tokens, one of which is usually the root element. The embedded element may +// include other children such as processing instruction tokens or character +// data tokens. The document's embedded element is never directly serialized; +// only its children are. +// +// A document also contains read and write settings, which influence the way +// the document is deserialized, serialized, and indented. type Document struct { Element ReadSettings ReadSettings @@ -112,7 +226,7 @@ type Element struct { index int // token index in parent's children } -// An Attr represents a key-value attribute of an XML element. +// An Attr represents a key-value attribute within an XML element. type Attr struct { Space, Key string // The attribute's namespace prefix and key Value string // The attribute value string @@ -123,17 +237,18 @@ type Attr struct { type charDataFlags uint8 const ( - // The CharData was created by an indent function as whitespace. + // The CharData contains only whitespace. whitespaceFlag charDataFlags = 1 << iota // The CharData contains a CDATA section. cdataFlag ) -// CharData can be used to represent character data or a CDATA section within -// an XML document. +// CharData may be used to represent simple text data or a CDATA section +// within an XML document. The Data property should never be modified +// directly; use the SetData method instead. type CharData struct { - Data string + Data string // the simple text or CDATA section content parent *Element index int flags charDataFlags @@ -141,22 +256,22 @@ type CharData struct { // A Comment represents an XML comment. type Comment struct { - Data string + Data string // the comment's text parent *Element index int } // A Directive represents an XML directive. type Directive struct { - Data string + Data string // the directive string parent *Element index int } // A ProcInst represents an XML processing instruction. type ProcInst struct { - Target string - Inst string + Target string // the processing instruction target + Inst string // the processing instruction value parent *Element index int } @@ -164,19 +279,32 @@ type ProcInst struct { // NewDocument creates an XML document without a root element. func NewDocument() *Document { return &Document{ - Element{Child: make([]Token, 0)}, - newReadSettings(), - newWriteSettings(), + Element: Element{Child: make([]Token, 0)}, + ReadSettings: newReadSettings(), + WriteSettings: newWriteSettings(), } } +// NewDocumentWithRoot creates an XML document and sets the element 'e' as its +// root element. If the element 'e' is already part of another document, it is +// first removed from its existing document. +func NewDocumentWithRoot(e *Element) *Document { + d := NewDocument() + d.SetRoot(e) + return d +} + // Copy returns a recursive, deep copy of the document. func (d *Document) Copy() *Document { - return &Document{*(d.dup(nil).(*Element)), d.ReadSettings, d.WriteSettings} + return &Document{ + Element: *(d.Element.dup(nil).(*Element)), + ReadSettings: d.ReadSettings.dup(), + WriteSettings: d.WriteSettings.dup(), + } } -// Root returns the root element of the document, or nil if there is no root -// element. +// Root returns the root element of the document. It returns nil if there is +// no root element. func (d *Document) Root() *Element { for _, t := range d.Child { if c, ok := t.(*Element); ok { @@ -186,25 +314,23 @@ func (d *Document) Root() *Element { return nil } -// SetRoot replaces the document's root element with e. If the document -// already has a root when this function is called, then the document's -// original root is unbound first. If the element e is bound to another -// document (or to another element within a document), then it is unbound -// first. +// SetRoot replaces the document's root element with the element 'e'. If the +// document already has a root element when this function is called, then the +// existing root element is unbound from the document. If the element 'e' is +// part of another document, then it is unbound from the other document. func (d *Document) SetRoot(e *Element) { if e.parent != nil { e.parent.RemoveChild(e) } - p := &d.Element - e.setParent(p) - // If there is already a root element, replace it. + p := &d.Element for i, t := range p.Child { if _, ok := t.(*Element); ok { t.setParent(nil) t.setIndex(-1) p.Child[i] = e + e.setParent(p) e.setIndex(i) return } @@ -214,15 +340,16 @@ func (d *Document) SetRoot(e *Element) { p.addChild(e) } -// ReadFrom reads XML from the reader r into the document d. It returns the -// number of bytes read and any error encountered. +// ReadFrom reads XML from the reader 'r' into this document. The function +// returns the number of bytes read and any error encountered. func (d *Document) ReadFrom(r io.Reader) (n int64, err error) { return d.Element.readFrom(r, d.ReadSettings) } -// ReadFromFile reads XML from the string s into the document d. -func (d *Document) ReadFromFile(filename string) error { - f, err := os.Open(filename) +// ReadFromFile reads XML from a local file at path 'filepath' into this +// document. +func (d *Document) ReadFromFile(filepath string) error { + f, err := os.Open(filepath) if err != nil { return err } @@ -231,34 +358,33 @@ func (d *Document) ReadFromFile(filename string) error { return err } -// ReadFromBytes reads XML from the byte slice b into the document d. +// ReadFromBytes reads XML from the byte slice 'b' into the this document. func (d *Document) ReadFromBytes(b []byte) error { _, err := d.ReadFrom(bytes.NewReader(b)) return err } -// ReadFromString reads XML from the string s into the document d. +// ReadFromString reads XML from the string 's' into this document. func (d *Document) ReadFromString(s string) error { _, err := d.ReadFrom(strings.NewReader(s)) return err } -// WriteTo serializes an XML document into the writer w. It -// returns the number of bytes written and any error encountered. +// WriteTo serializes the document out to the writer 'w'. The function returns +// the number of bytes written and any error encountered. func (d *Document) WriteTo(w io.Writer) (n int64, err error) { - cw := newCountWriter(w) - b := bufio.NewWriter(cw) + xw := newXmlWriter(w) + b := bufio.NewWriter(xw) for _, c := range d.Child { - c.writeTo(b, &d.WriteSettings) + c.WriteTo(b, &d.WriteSettings) } - err, n = b.Flush(), cw.bytes + err, n = b.Flush(), xw.bytes return } -// WriteToFile serializes an XML document into the file named -// filename. -func (d *Document) WriteToFile(filename string) error { - f, err := os.Create(filename) +// WriteToFile serializes the document out to the file at path 'filepath'. +func (d *Document) WriteToFile(filepath string) error { + f, err := os.Create(filepath) if err != nil { return err } @@ -267,8 +393,7 @@ func (d *Document) WriteToFile(filename string) error { return err } -// WriteToBytes serializes the XML document into a slice of -// bytes. +// WriteToBytes serializes this document into a slice of bytes. func (d *Document) WriteToBytes() (b []byte, err error) { var buf bytes.Buffer if _, err = d.WriteTo(&buf); err != nil { @@ -277,7 +402,7 @@ func (d *Document) WriteToBytes() (b []byte, err error) { return buf.Bytes(), nil } -// WriteToString serializes the XML document into a string. +// WriteToString serializes this document into a string. func (d *Document) WriteToString() (s string, err error) { var b []byte if b, err = d.WriteToBytes(); err != nil { @@ -286,41 +411,54 @@ func (d *Document) WriteToString() (s string, err error) { return string(b), nil } -type indentFunc func(depth int) string - // Indent modifies the document's element tree by inserting character data -// tokens containing newlines and indentation. The amount of indentation per -// depth level is given as spaces. Pass etree.NoIndent for spaces if you want -// no indentation at all. +// tokens containing newlines and spaces for indentation. The amount of +// indentation per depth level is given by the 'spaces' parameter. Other than +// the number of spaces, default IndentSettings are used. func (d *Document) Indent(spaces int) { - var indent indentFunc - switch { - case spaces < 0: - indent = func(depth int) string { return "" } - case d.WriteSettings.UseCRLF == true: - indent = func(depth int) string { return indentCRLF(depth*spaces, indentSpaces) } - default: - indent = func(depth int) string { return indentLF(depth*spaces, indentSpaces) } - } - d.Element.indent(0, indent) + s := NewIndentSettings() + s.Spaces = spaces + d.IndentWithSettings(s) } // IndentTabs modifies the document's element tree by inserting CharData -// tokens containing newlines and tabs for indentation. One tab is used per -// indentation level. +// tokens containing newlines and tabs for indentation. One tab is used per +// indentation level. Other than the use of tabs, default IndentSettings +// are used. func (d *Document) IndentTabs() { - var indent indentFunc - switch d.WriteSettings.UseCRLF { - case true: - indent = func(depth int) string { return indentCRLF(depth, indentTabs) } - default: - indent = func(depth int) string { return indentLF(depth, indentTabs) } + s := NewIndentSettings() + s.UseTabs = true + d.IndentWithSettings(s) +} + +// IndentWithSettings modifies the document's element tree by inserting +// character data tokens containing newlines and indentation. The behavior +// of the indentation algorithm is configured by the indent settings. +func (d *Document) IndentWithSettings(s *IndentSettings) { + // WriteSettings.UseCRLF is deprecated. Until removed from the package, it + // overrides IndentSettings.UseCRLF when true. + if d.WriteSettings.UseCRLF { + s.UseCRLF = true } - d.Element.indent(0, indent) + + d.Element.indent(0, getIndentFunc(s), s) + + if s.SuppressTrailingWhitespace { + d.Element.stripTrailingWhitespace() + } +} + +// Unindent modifies the document's element tree by removing character data +// tokens containing only whitespace. Other than the removal of indentation, +// default IndentSettings are used. +func (d *Document) Unindent() { + s := NewIndentSettings() + s.Spaces = NoIndent + d.IndentWithSettings(s) } -// NewElement creates an unparented element with the specified tag. The tag -// may be prefixed by a namespace prefix and a colon. +// NewElement creates an unparented element with the specified tag (i.e., +// name). The tag may include a namespace prefix followed by a colon. func NewElement(tag string) *Element { space, stag := spaceDecompose(tag) return newElement(space, stag, nil) @@ -345,7 +483,8 @@ func newElement(space, tag string, parent *Element) *Element { // Copy creates a recursive, deep copy of the element and all its attributes // and children. The returned element has no parent but can be parented to a -// another element using AddElement, or to a document using SetRoot. +// another element using AddChild, or added to a document with SetRoot or +// NewDocumentWithRoot. func (e *Element) Copy() *Element { return e.dup(nil).(*Element) } @@ -400,16 +539,6 @@ func (e *Element) findDefaultNamespaceURI() string { return e.parent.findDefaultNamespaceURI() } -// hasText returns true if the element has character data immediately -// folllowing the element's opening tag. -func (e *Element) hasText() bool { - if len(e.Child) == 0 { - return false - } - _, ok := e.Child[0].(*CharData) - return ok -} - // namespacePrefix returns the namespace prefix associated with the element. func (e *Element) namespacePrefix() string { return e.Space @@ -433,8 +562,10 @@ func (e *Element) Text() string { if text == "" { text = cd.Data } else { - text = text + cd.Data + text += cd.Data } + } else if _, ok := ch.(*Comment); ok { + // ignore } else { break } @@ -470,7 +601,7 @@ func (e *Element) Tail() string { if text == "" { text = cd.Data } else { - text = text + cd.Data + text += cd.Data } } else { break @@ -548,30 +679,30 @@ func (e *Element) findTermCharDataIndex(start int) int { return len(e.Child) } -// CreateElement creates an element with the specified tag and adds it as the -// last child element of the element e. The tag may be prefixed by a namespace -// prefix and a colon. +// CreateElement creates a new element with the specified tag (i.e., name) and +// adds it as the last child token of this element. The tag may include a +// prefix followed by a colon. func (e *Element) CreateElement(tag string) *Element { space, stag := spaceDecompose(tag) return newElement(space, stag, e) } -// AddChild adds the token t as the last child of element e. If token t was -// already the child of another element, it is first removed from its current +// AddChild adds the token 't' as the last child of the element. If token 't' +// was already the child of another element, it is first removed from its // parent element. func (e *Element) AddChild(t Token) { if t.Parent() != nil { t.Parent().RemoveChild(t) } - - t.setParent(e) e.addChild(t) } -// InsertChild inserts the token t before e's existing child token ex. If ex -// is nil or ex is not a child of e, then t is added to the end of e's child -// token list. If token t was already the child of another element, it is -// first removed from its current parent element. +// InsertChild inserts the token 't' into this element's list of children just +// before the element's existing child token 'ex'. If the existing element +// 'ex' does not appear in this element's list of child tokens, then 't' is +// added to the end of this element's list of child tokens. If token 't' is +// already the child of another element, it is first removed from the other +// element's list of child tokens. // // Deprecated: InsertChild is deprecated. Use InsertChildAt instead. func (e *Element) InsertChild(ex Token, t Token) { @@ -596,10 +727,10 @@ func (e *Element) InsertChild(ex Token, t Token) { } } -// InsertChildAt inserts the token t into the element e's list of child tokens -// just before the requested index. If the index is greater than or equal to -// the length of the list of child tokens, the token t is added to the end of -// the list. +// InsertChildAt inserts the token 't' into this element's list of child +// tokens just before the requested 'index'. If the index is greater than or +// equal to the length of the list of child tokens, then the token 't' is +// added to the end of the list of child tokens. func (e *Element) InsertChildAt(index int, t Token) { if index >= len(e.Child) { e.AddChild(t) @@ -624,9 +755,9 @@ func (e *Element) InsertChildAt(index int, t Token) { } } -// RemoveChild attempts to remove the token t from element e's list of -// children. If the token t is a child of e, then it is returned. Otherwise, -// nil is returned. +// RemoveChild attempts to remove the token 't' from this element's list of +// child tokens. If the token 't' was a child of this element, then it is +// removed and returned. Otherwise, nil is returned. func (e *Element) RemoveChild(t Token) Token { if t.Parent() != e { return nil @@ -634,9 +765,9 @@ func (e *Element) RemoveChild(t Token) Token { return e.RemoveChildAt(t.Index()) } -// RemoveChildAt removes the index-th child token from the element e. The -// removed child token is returned. If the index is out of bounds, no child is -// removed and nil is returned. +// RemoveChildAt removes the child token appearing in slot 'index' of this +// element's list of child tokens. The removed child token is then returned. +// If the index is out of bounds, no child is removed and nil is returned. func (e *Element) RemoveChildAt(index int) Token { if index >= len(e.Child) { return nil @@ -652,25 +783,42 @@ func (e *Element) RemoveChildAt(index int) Token { return t } -// ReadFrom reads XML from the reader r and stores the result as a new child -// of element e. +// ReadFrom reads XML from the reader 'ri' and stores the result as a new +// child of this element. func (e *Element) readFrom(ri io.Reader, settings ReadSettings) (n int64, err error) { - r := newCountReader(ri) + var r xmlReader + var pr *xmlPeekReader + if settings.PreserveCData { + pr = newXmlPeekReader(ri) + r = pr + } else { + r = newXmlSimpleReader(ri) + } + dec := xml.NewDecoder(r) dec.CharsetReader = settings.CharsetReader dec.Strict = !settings.Permissive dec.Entity = settings.Entity + var stack stack stack.push(e) for { + if pr != nil { + pr.PeekPrepare(dec.InputOffset(), len(cdataPrefix)) + } + t, err := dec.RawToken() + switch { case err == io.EOF: - return r.bytes, nil + if len(stack.data) != 1 { + return r.Bytes(), ErrXML + } + return r.Bytes(), nil case err != nil: - return r.bytes, err + return r.Bytes(), err case stack.empty(): - return r.bytes, ErrXML + return r.Bytes(), ErrXML } top := stack.peek().(*Element) @@ -683,12 +831,24 @@ func (e *Element) readFrom(ri io.Reader, settings ReadSettings) (n int64, err er } stack.push(e) case xml.EndElement: + if top.Tag != t.Name.Local || top.Space != t.Name.Space { + return r.Bytes(), ErrXML + } stack.pop() case xml.CharData: data := string(t) var flags charDataFlags - if isWhitespace(data) { - flags = whitespaceFlag + if pr != nil { + peekBuf := pr.PeekFinalize() + if bytes.Equal(peekBuf, cdataPrefix) { + flags = cdataFlag + } else if isWhitespace(data) { + flags = whitespaceFlag + } + } else { + if isWhitespace(data) { + flags = whitespaceFlag + } } newCharData(data, flags, top) case xml.Comment: @@ -701,9 +861,10 @@ func (e *Element) readFrom(ri io.Reader, settings ReadSettings) (n int64, err er } } -// SelectAttr finds an element attribute matching the requested key and -// returns it if found. Returns nil if no matching attribute is found. The key -// may be prefixed by a namespace prefix and a colon. +// SelectAttr finds an element attribute matching the requested 'key' and, if +// found, returns a pointer to the matching attribute. The function returns +// nil if no matching attribute is found. The key may include a namespace +// prefix followed by a colon. func (e *Element) SelectAttr(key string) *Attr { space, skey := spaceDecompose(key) for i, a := range e.Attr { @@ -714,9 +875,10 @@ func (e *Element) SelectAttr(key string) *Attr { return nil } -// SelectAttrValue finds an element attribute matching the requested key and -// returns its value if found. The key may be prefixed by a namespace prefix -// and a colon. If the key is not found, the dflt value is returned instead. +// SelectAttrValue finds an element attribute matching the requested 'key' and +// returns its value if found. If no matching attribute is found, the function +// returns the 'dflt' value instead. The key may include a namespace prefix +// followed by a colon. func (e *Element) SelectAttrValue(key, dflt string) string { space, skey := spaceDecompose(key) for _, a := range e.Attr { @@ -727,7 +889,7 @@ func (e *Element) SelectAttrValue(key, dflt string) string { return dflt } -// ChildElements returns all elements that are children of element e. +// ChildElements returns all elements that are children of this element. func (e *Element) ChildElements() []*Element { var elements []*Element for _, t := range e.Child { @@ -738,9 +900,9 @@ func (e *Element) ChildElements() []*Element { return elements } -// SelectElement returns the first child element with the given tag. The tag -// may be prefixed by a namespace prefix and a colon. Returns nil if no -// element with a matching tag was found. +// SelectElement returns the first child element with the given 'tag' (i.e., +// name). The function returns nil if no child element matching the tag is +// found. The tag may include a namespace prefix followed by a colon. func (e *Element) SelectElement(tag string) *Element { space, stag := spaceDecompose(tag) for _, t := range e.Child { @@ -751,8 +913,8 @@ func (e *Element) SelectElement(tag string) *Element { return nil } -// SelectElements returns a slice of all child elements with the given tag. -// The tag may be prefixed by a namespace prefix and a colon. +// SelectElements returns a slice of all child elements with the given 'tag' +// (i.e., name). The tag may include a namespace prefix followed by a colon. func (e *Element) SelectElements(tag string) []*Element { space, stag := spaceDecompose(tag) var elements []*Element @@ -764,39 +926,39 @@ func (e *Element) SelectElements(tag string) []*Element { return elements } -// FindElement returns the first element matched by the XPath-like path -// string. Returns nil if no element is found using the path. Panics if an -// invalid path string is supplied. +// FindElement returns the first element matched by the XPath-like 'path' +// string. The function returns nil if no child element is found using the +// path. It panics if an invalid path string is supplied. func (e *Element) FindElement(path string) *Element { return e.FindElementPath(MustCompilePath(path)) } -// FindElementPath returns the first element matched by the XPath-like path -// string. Returns nil if no element is found using the path. +// FindElementPath returns the first element matched by the 'path' object. The +// function returns nil if no element is found using the path. func (e *Element) FindElementPath(path Path) *Element { p := newPather() elements := p.traverse(e, path) - switch { - case len(elements) > 0: + if len(elements) > 0 { return elements[0] - default: - return nil } + return nil } -// FindElements returns a slice of elements matched by the XPath-like path -// string. Panics if an invalid path string is supplied. +// FindElements returns a slice of elements matched by the XPath-like 'path' +// string. The function returns nil if no child element is found using the +// path. It panics if an invalid path string is supplied. func (e *Element) FindElements(path string) []*Element { return e.FindElementsPath(MustCompilePath(path)) } -// FindElementsPath returns a slice of elements matched by the Path object. +// FindElementsPath returns a slice of elements matched by the 'path' object. func (e *Element) FindElementsPath(path Path) []*Element { p := newPather() return p.traverse(e, path) } -// GetPath returns the absolute path of the element. +// GetPath returns the absolute path of the element. The absolute path is the +// full path from the document's root. func (e *Element) GetPath() string { path := []string{} for seg := e; seg != nil; seg = seg.Parent() { @@ -813,9 +975,9 @@ func (e *Element) GetPath() string { return "/" + strings.Join(path, "/") } -// GetRelativePath returns the path of the element relative to the source +// GetRelativePath returns the path of this element relative to the 'source' // element. If the two elements are not part of the same element tree, then -// GetRelativePath returns the empty string. +// the function returns the empty string. func (e *Element) GetRelativePath(source *Element) string { var path []*Element @@ -884,10 +1046,20 @@ func (e *Element) GetRelativePath(source *Element) string { return strings.Join(parts, "/") } -// indent recursively inserts proper indentation between an -// XML element's child tokens. -func (e *Element) indent(depth int, indent indentFunc) { - e.stripIndent() +// IndentWithSettings modifies the element and its child tree by inserting +// character data tokens containing newlines and indentation. The behavior of +// the indentation algorithm is configured by the indent settings. Because +// this function indents the element as if it were at the root of a document, +// it is most useful when called just before writing the element as an XML +// fragment using WriteTo. +func (e *Element) IndentWithSettings(s *IndentSettings) { + e.indent(1, getIndentFunc(s), s) +} + +// indent recursively inserts proper indentation between an XML element's +// child tokens. +func (e *Element) indent(depth int, indent indentFunc, s *IndentSettings) { + e.stripIndent(s) n := len(e.Child) if n == 0 { return @@ -915,7 +1087,7 @@ func (e *Element) indent(depth int, indent indentFunc) { // Recursively process child elements. if ce, ok := c.(*Element); ok { - ce.indent(depth+1, indent) + ce.indent(depth+1, indent, s) } } @@ -931,7 +1103,7 @@ func (e *Element) indent(depth int, indent indentFunc) { } // stripIndent removes any previously inserted indentation. -func (e *Element) stripIndent() { +func (e *Element) stripIndent(s *IndentSettings) { // Count the number of non-indent child tokens n := len(e.Child) for _, c := range e.Child { @@ -942,6 +1114,9 @@ func (e *Element) stripIndent() { if n == len(e.Child) { return } + if n == 0 && len(e.Child) == 1 && s.PreserveLeafWhitespace { + return + } // Strip out indent CharData newChild := make([]Token, n) @@ -957,6 +1132,17 @@ func (e *Element) stripIndent() { e.Child = newChild } +// stripTrailingWhitespace removes any trailing whitespace CharData tokens +// from the element's children. +func (e *Element) stripTrailingWhitespace() { + for i := len(e.Child) - 1; i >= 0; i-- { + if cd, ok := e.Child[i].(*CharData); !ok || !cd.IsWhitespace() { + e.Child = e.Child[:i+1] + return + } + } +} + // dup duplicates the element. func (e *Element) dup(parent *Element) Token { ne := &Element{ @@ -970,47 +1156,35 @@ func (e *Element) dup(parent *Element) Token { for i, t := range e.Child { ne.Child[i] = t.dup(ne) } - for i, a := range e.Attr { - ne.Attr[i] = a - } + copy(ne.Attr, e.Attr) return ne } -// Parent returns the element token's parent element, or nil if it has no -// parent. +// Parent returns this element's parent element. It returns nil if this +// element has no parent. func (e *Element) Parent() *Element { return e.parent } // Index returns the index of this element within its parent element's -// list of child tokens. If this element has no parent element, the index -// is -1. +// list of child tokens. If this element has no parent, then the function +// returns -1. func (e *Element) Index() int { return e.index } -// setParent replaces the element token's parent. -func (e *Element) setParent(parent *Element) { - e.parent = parent -} - -// setIndex sets the element token's index within its parent's Child slice. -func (e *Element) setIndex(index int) { - e.index = index -} - -// writeTo serializes the element to the writer w. -func (e *Element) writeTo(w *bufio.Writer, s *WriteSettings) { +// WriteTo serializes the element to the writer w. +func (e *Element) WriteTo(w Writer, s *WriteSettings) { w.WriteByte('<') w.WriteString(e.FullTag()) for _, a := range e.Attr { w.WriteByte(' ') - a.writeTo(w, s) + a.WriteTo(w, s) } if len(e.Child) > 0 { - w.WriteString(">") + w.WriteByte('>') for _, c := range e.Child { - c.writeTo(w, s) + c.WriteTo(w, s) } w.Write([]byte{'<', '/'}) w.WriteString(e.FullTag()) @@ -1026,15 +1200,27 @@ func (e *Element) writeTo(w *bufio.Writer, s *WriteSettings) { } } +// setParent replaces this element token's parent. +func (e *Element) setParent(parent *Element) { + e.parent = parent +} + +// setIndex sets this element token's index within its parent's Child slice. +func (e *Element) setIndex(index int) { + e.index = index +} + // addChild adds a child token to the element e. func (e *Element) addChild(t Token) { + t.setParent(e) t.setIndex(len(e.Child)) e.Child = append(e.Child, t) } -// CreateAttr creates an attribute and adds it to element e. The key may be -// prefixed by a namespace prefix and a colon. If an attribute with the key -// already exists, its value is replaced. +// CreateAttr creates an attribute with the specified 'key' and 'value' and +// adds it to this element. If an attribute with same key already exists on +// this element, then its value is replaced. The key may include a namespace +// prefix followed by a colon. func (e *Element) CreateAttr(key, value string) *Attr { space, skey := spaceDecompose(key) return e.createAttr(space, skey, value, e) @@ -1058,10 +1244,10 @@ func (e *Element) createAttr(space, key, value string, parent *Element) *Attr { return &e.Attr[len(e.Attr)-1] } -// RemoveAttr removes and returns a copy of the first attribute of the element -// whose key matches the given key. The key may be prefixed by a namespace -// prefix and a colon. If a matching attribute does not exist, nil is -// returned. +// RemoveAttr removes the first attribute of this element whose key matches +// 'key'. It returns a copy of the removed attribute if a match is found. If +// no match is found, it returns nil. The key may include a namespace prefix +// followed by a colon. func (e *Element) RemoveAttr(key string) *Attr { space, skey := spaceDecompose(key) for i, a := range e.Attr { @@ -1078,7 +1264,7 @@ func (e *Element) RemoveAttr(key string) *Attr { return nil } -// SortAttrs sorts the element's attributes lexicographically by key. +// SortAttrs sorts this element's attributes lexicographically by key. func (e *Element) SortAttrs() { sort.Sort(byAttr(e.Attr)) } @@ -1101,7 +1287,7 @@ func (a byAttr) Less(i, j int) bool { return sp < 0 } -// FullKey returns the attribute a's complete key, including namespace prefix +// FullKey returns this attribute's complete key, including namespace prefix // if present. func (a *Attr) FullKey() string { if a.Space == "" { @@ -1110,22 +1296,29 @@ func (a *Attr) FullKey() string { return a.Space + ":" + a.Key } -// Element returns the element containing the attribute. +// Element returns a pointer to the element containing this attribute. func (a *Attr) Element() *Element { return a.element } -// NamespaceURI returns the XML namespace URI associated with the attribute. -// If the element is part of the XML default namespace, NamespaceURI returns -// the empty string. +// NamespaceURI returns the XML namespace URI associated with this attribute. +// The function returns the empty string if the attribute is unprefixed or +// if the attribute is part of the XML default namespace. func (a *Attr) NamespaceURI() string { - return a.element.NamespaceURI() + if a.Space == "" { + return "" + } + return a.element.findLocalNamespaceURI(a.Space) } -// writeTo serializes the attribute to the writer. -func (a *Attr) writeTo(w *bufio.Writer, s *WriteSettings) { +// WriteTo serializes the attribute to the writer. +func (a *Attr) WriteTo(w Writer, s *WriteSettings) { w.WriteString(a.FullKey()) - w.WriteString(`="`) + if s.AttrSingleQuote { + w.WriteString(`='`) + } else { + w.WriteString(`="`) + } var m escapeMode if s.CanonicalAttrVal { m = escapeCanonicalAttr @@ -1133,20 +1326,26 @@ func (a *Attr) writeTo(w *bufio.Writer, s *WriteSettings) { m = escapeNormal } escapeString(w, a.Value, m) - w.WriteByte('"') + if s.AttrSingleQuote { + w.WriteByte('\'') + } else { + w.WriteByte('"') + } } -// NewText creates a parentless CharData token containing character data. +// NewText creates an unparented CharData token containing simple text data. func NewText(text string) *CharData { return newCharData(text, 0, nil) } -// NewCData creates a parentless XML character CDATA section. +// NewCData creates an unparented XML character CDATA section with 'data' as +// its content. func NewCData(data string) *CharData { return newCharData(data, cdataFlag, nil) } -// NewCharData creates a parentless CharData token containing character data. +// NewCharData creates an unparented CharData token containing simple text +// data. // // Deprecated: NewCharData is deprecated. Instead, use NewText, which does the // same thing. @@ -1159,7 +1358,7 @@ func NewCharData(data string) *CharData { func newCharData(data string, flags charDataFlags, parent *Element) *CharData { c := &CharData{ Data: data, - parent: parent, + parent: nil, index: -1, flags: flags, } @@ -1169,75 +1368,67 @@ func newCharData(data string, flags charDataFlags, parent *Element) *CharData { return c } -// CreateText creates a CharData token containing character data and adds it -// as a child of element e. +// CreateText creates a CharData token containing simple text data and adds it +// to the end of this element's list of child tokens. func (e *Element) CreateText(text string) *CharData { return newCharData(text, 0, e) } -// CreateCData creates a CharData token containing a CDATA section and adds it -// as a child of element e. +// CreateCData creates a CharData token containing a CDATA section with 'data' +// as its content and adds it to the end of this element's list of child +// tokens. func (e *Element) CreateCData(data string) *CharData { return newCharData(data, cdataFlag, e) } -// CreateCharData creates a CharData token containing character data and adds -// it as a child of element e. +// CreateCharData creates a CharData token containing simple text data and +// adds it to the end of this element's list of child tokens. // // Deprecated: CreateCharData is deprecated. Instead, use CreateText, which // does the same thing. func (e *Element) CreateCharData(data string) *CharData { - return newCharData(data, 0, e) + return e.CreateText(data) } -// dup duplicates the character data. -func (c *CharData) dup(parent *Element) Token { - return &CharData{ - Data: c.Data, - flags: c.flags, - parent: parent, - index: c.index, +// SetData modifies the content of the CharData token. In the case of a +// CharData token containing simple text, the simple text is modified. In the +// case of a CharData token containing a CDATA section, the CDATA section's +// content is modified. +func (c *CharData) SetData(text string) { + c.Data = text + if isWhitespace(text) { + c.flags |= whitespaceFlag + } else { + c.flags &= ^whitespaceFlag } } -// IsCData returns true if the character data token is to be encoded as a -// CDATA section. +// IsCData returns true if this CharData token is contains a CDATA section. It +// returns false if the CharData token contains simple text. func (c *CharData) IsCData() bool { return (c.flags & cdataFlag) != 0 } -// IsWhitespace returns true if the character data token was created by one of -// the document Indent methods to contain only whitespace. +// IsWhitespace returns true if this CharData token contains only whitespace. func (c *CharData) IsWhitespace() bool { return (c.flags & whitespaceFlag) != 0 } -// Parent returns the character data token's parent element, or nil if it has -// no parent. +// Parent returns this CharData token's parent element, or nil if it has no +// parent. func (c *CharData) Parent() *Element { return c.parent } // Index returns the index of this CharData token within its parent element's -// list of child tokens. If this CharData token has no parent element, the -// index is -1. +// list of child tokens. If this CharData token has no parent, then the +// function returns -1. func (c *CharData) Index() int { return c.index } -// setParent replaces the character data token's parent. -func (c *CharData) setParent(parent *Element) { - c.parent = parent -} - -// setIndex sets the CharData token's index within its parent element's Child -// slice. -func (c *CharData) setIndex(index int) { - c.index = index -} - -// writeTo serializes character data to the writer. -func (c *CharData) writeTo(w *bufio.Writer, s *WriteSettings) { +// WriteTo serializes character data to the writer. +func (c *CharData) WriteTo(w Writer, s *WriteSettings) { if c.IsCData() { w.WriteString(`") +} + // setParent replaces the comment token's parent. func (c *Comment) setParent(parent *Element) { c.parent = parent @@ -1309,14 +1528,7 @@ func (c *Comment) setIndex(index int) { c.index = index } -// writeTo serialies the comment to the writer. -func (c *Comment) writeTo(w *bufio.Writer, s *WriteSettings) { - w.WriteString("") -} - -// NewDirective creates a parentless XML directive. +// NewDirective creates an unparented XML directive token. func NewDirective(data string) *Directive { return newDirective(data, nil) } @@ -1326,7 +1538,7 @@ func NewDirective(data string) *Directive { func newDirective(data string, parent *Element) *Directive { d := &Directive{ Data: data, - parent: parent, + parent: nil, index: -1, } if parent != nil { @@ -1335,8 +1547,8 @@ func newDirective(data string, parent *Element) *Directive { return d } -// CreateDirective creates an XML directive and adds it as the last child of -// element e. +// CreateDirective creates an XML directive token with the specified 'data' +// value and adds it as the last child token of this element. func (e *Element) CreateDirective(data string) *Directive { return newDirective(data, e) } @@ -1357,12 +1569,19 @@ func (d *Directive) Parent() *Element { } // Index returns the index of this Directive token within its parent element's -// list of child tokens. If this Directive token has no parent element, the -// index is -1. +// list of child tokens. If this Directive token has no parent, then the +// function returns -1. func (d *Directive) Index() int { return d.index } +// WriteTo serializes the XML directive to the writer. +func (d *Directive) WriteTo(w Writer, s *WriteSettings) { + w.WriteString("") +} + // setParent replaces the directive token's parent. func (d *Directive) setParent(parent *Element) { d.parent = parent @@ -1374,14 +1593,7 @@ func (d *Directive) setIndex(index int) { d.index = index } -// writeTo serializes the XML directive to the writer. -func (d *Directive) writeTo(w *bufio.Writer, s *WriteSettings) { - w.WriteString("") -} - -// NewProcInst creates a parentless XML processing instruction. +// NewProcInst creates an unparented XML processing instruction. func NewProcInst(target, inst string) *ProcInst { return newProcInst(target, inst, nil) } @@ -1392,7 +1604,7 @@ func newProcInst(target, inst string, parent *Element) *ProcInst { p := &ProcInst{ Target: target, Inst: inst, - parent: parent, + parent: nil, index: -1, } if parent != nil { @@ -1401,8 +1613,9 @@ func newProcInst(target, inst string, parent *Element) *ProcInst { return p } -// CreateProcInst creates a processing instruction and adds it as a child of -// element e. +// CreateProcInst creates an XML processing instruction token with the +// specified 'target' and instruction 'inst'. It is then added as the last +// child token of this element. func (e *Element) CreateProcInst(target, inst string) *ProcInst { return newProcInst(target, inst, e) } @@ -1424,12 +1637,23 @@ func (p *ProcInst) Parent() *Element { } // Index returns the index of this ProcInst token within its parent element's -// list of child tokens. If this ProcInst token has no parent element, the -// index is -1. +// list of child tokens. If this ProcInst token has no parent, then the +// function returns -1. func (p *ProcInst) Index() int { return p.index } +// WriteTo serializes the processing instruction to the writer. +func (p *ProcInst) WriteTo(w Writer, s *WriteSettings) { + w.WriteString("") +} + // setParent replaces the processing instruction token's parent. func (p *ProcInst) setParent(parent *Element) { p.parent = parent @@ -1440,14 +1664,3 @@ func (p *ProcInst) setParent(parent *Element) { func (p *ProcInst) setIndex(index int) { p.index = index } - -// writeTo serializes the processing instruction to the writer. -func (p *ProcInst) writeTo(w *bufio.Writer, s *WriteSettings) { - w.WriteString("") -} diff --git a/vendor/github.com/beevik/etree/go.mod b/vendor/github.com/beevik/etree/go.mod new file mode 100644 index 000000000..6c628cf83 --- /dev/null +++ b/vendor/github.com/beevik/etree/go.mod @@ -0,0 +1,3 @@ +module github.com/beevik/etree + +go 1.13 diff --git a/vendor/github.com/beevik/etree/helpers.go b/vendor/github.com/beevik/etree/helpers.go index 825e14e91..b31fd754c 100644 --- a/vendor/github.com/beevik/etree/helpers.go +++ b/vendor/github.com/beevik/etree/helpers.go @@ -5,7 +5,6 @@ package etree import ( - "bufio" "io" "strings" "unicode/utf8" @@ -83,38 +82,157 @@ func (f *fifo) grow() { f.data, f.head, f.tail = buf, 0, count } -// countReader implements a proxy reader that counts the number of +// xmlReader provides the interface by which an XML byte stream is +// processed and decoded. +type xmlReader interface { + Bytes() int64 + Read(p []byte) (n int, err error) +} + +// xmlSimpleReader implements a proxy reader that counts the number of // bytes read from its encapsulated reader. -type countReader struct { +type xmlSimpleReader struct { r io.Reader bytes int64 } -func newCountReader(r io.Reader) *countReader { - return &countReader{r: r} +func newXmlSimpleReader(r io.Reader) xmlReader { + return &xmlSimpleReader{r, 0} +} + +func (xr *xmlSimpleReader) Bytes() int64 { + return xr.bytes +} + +func (xr *xmlSimpleReader) Read(p []byte) (n int, err error) { + n, err = xr.r.Read(p) + xr.bytes += int64(n) + return n, err +} + +// xmlPeekReader implements a proxy reader that counts the number of +// bytes read from its encapsulated reader. It also allows the caller to +// "peek" at the previous portions of the buffer after they have been +// parsed. +type xmlPeekReader struct { + r io.Reader + bytes int64 // total bytes read by the Read function + buf []byte // internal read buffer + bufSize int // total bytes used in the read buffer + bufOffset int64 // total bytes read when buf was last filled + window []byte // current read buffer window + peekBuf []byte // buffer used to store data to be peeked at later + peekOffset int64 // total read offset of the start of the peek buffer +} + +func newXmlPeekReader(r io.Reader) *xmlPeekReader { + buf := make([]byte, 4096) + return &xmlPeekReader{ + r: r, + bytes: 0, + buf: buf, + bufSize: 0, + bufOffset: 0, + window: buf[0:0], + peekBuf: make([]byte, 0), + peekOffset: -1, + } +} + +func (xr *xmlPeekReader) Bytes() int64 { + return xr.bytes +} + +func (xr *xmlPeekReader) Read(p []byte) (n int, err error) { + if len(xr.window) == 0 { + err = xr.fill() + if err != nil { + return 0, err + } + if len(xr.window) == 0 { + return 0, nil + } + } + + if len(xr.window) < len(p) { + n = len(xr.window) + } else { + n = len(p) + } + + copy(p, xr.window) + xr.window = xr.window[n:] + xr.bytes += int64(n) + + return n, err +} + +func (xr *xmlPeekReader) PeekPrepare(offset int64, maxLen int) { + if maxLen > cap(xr.peekBuf) { + xr.peekBuf = make([]byte, 0, maxLen) + } + xr.peekBuf = xr.peekBuf[0:0] + xr.peekOffset = offset + xr.updatePeekBuf() +} + +func (xr *xmlPeekReader) PeekFinalize() []byte { + xr.updatePeekBuf() + return xr.peekBuf } -func (cr *countReader) Read(p []byte) (n int, err error) { - b, err := cr.r.Read(p) - cr.bytes += int64(b) - return b, err +func (xr *xmlPeekReader) fill() error { + xr.bufOffset = xr.bytes + xr.bufSize = 0 + n, err := xr.r.Read(xr.buf) + if err != nil { + xr.window, xr.bufSize = xr.buf[0:0], 0 + return err + } + xr.window, xr.bufSize = xr.buf[:n], n + xr.updatePeekBuf() + return nil +} + +func (xr *xmlPeekReader) updatePeekBuf() { + peekRemain := cap(xr.peekBuf) - len(xr.peekBuf) + if xr.peekOffset >= 0 && peekRemain > 0 { + rangeMin := xr.peekOffset + rangeMax := xr.peekOffset + int64(cap(xr.peekBuf)) + bufMin := xr.bufOffset + bufMax := xr.bufOffset + int64(xr.bufSize) + if rangeMin < bufMin { + rangeMin = bufMin + } + if rangeMax > bufMax { + rangeMax = bufMax + } + if rangeMax > rangeMin { + rangeMin -= xr.bufOffset + rangeMax -= xr.bufOffset + if int(rangeMax-rangeMin) > peekRemain { + rangeMax = rangeMin + int64(peekRemain) + } + xr.peekBuf = append(xr.peekBuf, xr.buf[rangeMin:rangeMax]...) + } + } } -// countWriter implements a proxy writer that counts the number of +// xmlWriter implements a proxy writer that counts the number of // bytes written by its encapsulated writer. -type countWriter struct { +type xmlWriter struct { w io.Writer bytes int64 } -func newCountWriter(w io.Writer) *countWriter { - return &countWriter{w: w} +func newXmlWriter(w io.Writer) *xmlWriter { + return &xmlWriter{w: w} } -func (cw *countWriter) Write(p []byte) (n int, err error) { - b, err := cw.w.Write(p) - cw.bytes += int64(b) - return b, err +func (xw *xmlWriter) Write(p []byte) (n int, err error) { + n, err = xw.w.Write(p) + xw.bytes += int64(n) + return n, err } // isWhitespace returns true if the byte slice contains only @@ -211,7 +329,7 @@ const ( ) // escapeString writes an escaped version of a string to the writer. -func escapeString(w *bufio.Writer, s string, m escapeMode) { +func escapeString(w Writer, s string, m escapeMode) { var esc []byte last := 0 for i := 0; i < len(s); { diff --git a/vendor/github.com/beevik/etree/path.go b/vendor/github.com/beevik/etree/path.go index 82db0ac55..a6d67acee 100644 --- a/vendor/github.com/beevik/etree/path.go +++ b/vendor/github.com/beevik/etree/path.go @@ -19,66 +19,73 @@ be modified by one or more bracket-enclosed "filters". Selectors are used to traverse the etree from element to element, while filters are used to narrow the list of candidate elements at each node. -Although etree Path strings are similar to XPath strings -(https://www.w3.org/TR/1999/REC-xpath-19991116/), they have a more limited set -of selectors and filtering options. +Although etree Path strings are structurally and behaviorally similar to XPath +strings (https://www.w3.org/TR/1999/REC-xpath-19991116/), they have a more +limited set of selectors and filtering options. -The following selectors are supported by etree Path strings: +The following selectors are supported by etree paths: - . Select the current element. - .. Select the parent of the current element. - * Select all child elements of the current element. - / Select the root element when used at the start of a path. - // Select all descendants of the current element. - tag Select all child elements with a name matching the tag. + . Select the current element. + .. Select the parent of the current element. + * Select all child elements of the current element. + / Select the root element when used at the start of a path. + // Select all descendants of the current element. + tag Select all child elements with a name matching the tag. -The following basic filters are supported by etree Path strings: +The following basic filters are supported: - [@attrib] Keep elements with an attribute named attrib. - [@attrib='val'] Keep elements with an attribute named attrib and value matching val. - [tag] Keep elements with a child element named tag. - [tag='val'] Keep elements with a child element named tag and text matching val. - [n] Keep the n-th element, where n is a numeric index starting from 1. + [@attrib] Keep elements with an attribute named attrib. + [@attrib='val'] Keep elements with an attribute named attrib and value matching val. + [tag] Keep elements with a child element named tag. + [tag='val'] Keep elements with a child element named tag and text matching val. + [n] Keep the n-th element, where n is a numeric index starting from 1. -The following function filters are also supported: +The following function-based filters are supported: - [text()] Keep elements with non-empty text. - [text()='val'] Keep elements whose text matches val. - [local-name()='val'] Keep elements whose un-prefixed tag matches val. - [name()='val'] Keep elements whose full tag exactly matches val. - [namespace-prefix()='val'] Keep elements whose namespace prefix matches val. - [namespace-uri()='val'] Keep elements whose namespace URI matches val. + [text()] Keep elements with non-empty text. + [text()='val'] Keep elements whose text matches val. + [local-name()='val'] Keep elements whose un-prefixed tag matches val. + [name()='val'] Keep elements whose full tag exactly matches val. + [namespace-prefix()] Keep elements with non-empty namespace prefixes. + [namespace-prefix()='val'] Keep elements whose namespace prefix matches val. + [namespace-uri()] Keep elements with non-empty namespace URIs. + [namespace-uri()='val'] Keep elements whose namespace URI matches val. -Here are some examples of Path strings: +Below are some examples of etree path strings. -- Select the bookstore child element of the root element: - /bookstore +Select the bookstore child element of the root element: -- Beginning from the root element, select the title elements of all -descendant book elements having a 'category' attribute of 'WEB': - //book[@category='WEB']/title + /bookstore -- Beginning from the current element, select the first descendant -book element with a title child element containing the text 'Great -Expectations': - .//book[title='Great Expectations'][1] +Beginning from the root element, select the title elements of all descendant +book elements having a 'category' attribute of 'WEB': -- Beginning from the current element, select all child elements of -book elements with an attribute 'language' set to 'english': - ./book/*[@language='english'] + //book[@category='WEB']/title -- Beginning from the current element, select all child elements of -book elements containing the text 'special': - ./book/*[text()='special'] +Beginning from the current element, select the first descendant book element +with a title child element containing the text 'Great Expectations': -- Beginning from the current element, select all descendant book -elements whose title child element has a 'language' attribute of 'french': - .//book/title[@language='french']/.. + .//book[title='Great Expectations'][1] -- Beginning from the current element, select all book elements +Beginning from the current element, select all child elements of book elements +with an attribute 'language' set to 'english': + + ./book/*[@language='english'] + +Beginning from the current element, select all child elements of book elements +containing the text 'special': + + ./book/*[text()='special'] + +Beginning from the current element, select all descendant book elements whose +title child element has a 'language' attribute of 'french': + + .//book/title[@language='french']/.. + +Beginning from the current element, select all descendant book elements belonging to the http://www.w3.org/TR/html4/ namespace: - .//book[namespace-uri()='http://www.w3.org/TR/html4/'] + .//book[namespace-uri()='http://www.w3.org/TR/html4/'] */ type Path struct { segments []segment @@ -178,7 +185,7 @@ func (p *pather) traverse(e *Element, path Path) []*Element { return p.results } -// eval evalutes the current path node by applying the remaining +// eval evaluates the current path node by applying the remaining // path's selector rules against the node's element. func (p *pather) eval(n node) { p.candidates = p.candidates[0:0] @@ -210,7 +217,7 @@ type compiler struct { func (c *compiler) parsePath(path string) []segment { // If path ends with //, fix it if strings.HasSuffix(path, "//") { - path = path + "*" + path += "*" } var segments []segment @@ -232,7 +239,7 @@ func (c *compiler) parsePath(path string) []segment { } func splitPath(path string) []string { - pieces := make([]string, 0) + var pieces []string start := 0 inquote := false for i := 0; i+1 <= len(path); i++ { @@ -255,7 +262,7 @@ func (c *compiler) parseSegment(path string) segment { } for i := 1; i < len(pieces); i++ { fpath := pieces[i] - if fpath[len(fpath)-1] != ']' { + if len(fpath) == 0 || fpath[len(fpath)-1] != ']' { c.err = ErrPath("path has invalid filter [brackets].") break } @@ -280,15 +287,12 @@ func (c *compiler) parseSelector(path string) selector { } } -var fnTable = map[string]struct { - hasFn func(e *Element) bool - getValFn func(e *Element) string -}{ - "local-name": {nil, (*Element).name}, - "name": {nil, (*Element).FullTag}, - "namespace-prefix": {nil, (*Element).namespacePrefix}, - "namespace-uri": {nil, (*Element).NamespaceURI}, - "text": {(*Element).hasText, (*Element).Text}, +var fnTable = map[string]func(e *Element) string{ + "local-name": (*Element).name, + "name": (*Element).FullTag, + "namespace-prefix": (*Element).namespacePrefix, + "namespace-uri": (*Element).NamespaceURI, + "text": (*Element).Text, } // parseFilter parses a path filter contained within [brackets]. @@ -314,11 +318,11 @@ func (c *compiler) parseFilter(path string) filter { case key[0] == '@': return newFilterAttrVal(key[1:], value) case strings.HasSuffix(key, "()"): - fn := key[:len(key)-2] - if t, ok := fnTable[fn]; ok && t.getValFn != nil { - return newFilterFuncVal(t.getValFn, value) + name := key[:len(key)-2] + if fn, ok := fnTable[name]; ok { + return newFilterFuncVal(fn, value) } - c.err = ErrPath("path has unknown function " + fn) + c.err = ErrPath("path has unknown function " + name) return nil default: return newFilterChildText(key, value) @@ -330,11 +334,11 @@ func (c *compiler) parseFilter(path string) filter { case path[0] == '@': return newFilterAttr(path[1:]) case strings.HasSuffix(path, "()"): - fn := path[:len(path)-2] - if t, ok := fnTable[fn]; ok && t.hasFn != nil { - return newFilterFunc(t.hasFn) + name := path[:len(path)-2] + if fn, ok := fnTable[name]; ok { + return newFilterFunc(fn) } - c.err = ErrPath("path has unknown function " + fn) + c.err = ErrPath("path has unknown function " + name) return nil case isInteger(path): pos, _ := strconv.Atoi(path) @@ -496,16 +500,16 @@ func (f *filterAttrVal) apply(p *pather) { // filterFunc filters the candidate list for elements satisfying a custom // boolean function. type filterFunc struct { - fn func(e *Element) bool + fn func(e *Element) string } -func newFilterFunc(fn func(e *Element) bool) *filterFunc { +func newFilterFunc(fn func(e *Element) string) *filterFunc { return &filterFunc{fn} } func (f *filterFunc) apply(p *pather) { for _, c := range p.candidates { - if f.fn(c) { + if f.fn(c) != "" { p.scratch = append(p.scratch, c) } } diff --git a/vendor/github.com/jonboulle/clockwork/clockwork.go b/vendor/github.com/jonboulle/clockwork/clockwork.go index 1018051f4..9e1c8143a 100644 --- a/vendor/github.com/jonboulle/clockwork/clockwork.go +++ b/vendor/github.com/jonboulle/clockwork/clockwork.go @@ -13,6 +13,7 @@ type Clock interface { Now() time.Time Since(t time.Time) time.Duration NewTicker(d time.Duration) Ticker + NewTimer(d time.Duration) Timer } // FakeClock provides an interface for a clock which can be @@ -70,6 +71,10 @@ func (rc *realClock) NewTicker(d time.Duration) Ticker { return &realTicker{time.NewTicker(d)} } +func (rc *realClock) NewTimer(d time.Duration) Timer { + return &realTimer{time.NewTimer(d)} +} + type fakeClock struct { sleepers []*sleeper blockers []*blocker @@ -113,12 +118,12 @@ func (fc *fakeClock) After(d time.Duration) <-chan time.Time { return done } -// notifyBlockers notifies all the blockers waiting until the -// given number of sleepers are waiting on the fakeClock. It -// returns an updated slice of blockers (i.e. those still waiting) +// notifyBlockers notifies all the blockers waiting until the at least the given +// number of sleepers are waiting on the fakeClock. It returns an updated slice +// of blockers (i.e. those still waiting) func notifyBlockers(blockers []*blocker, count int) (newBlockers []*blocker) { for _, b := range blockers { - if b.count == count { + if b.count <= count { close(b.ch) } else { newBlockers = append(newBlockers, b) @@ -132,7 +137,7 @@ func (fc *fakeClock) Sleep(d time.Duration) { <-fc.After(d) } -// Time returns the current time of the fakeClock +// Now returns the current time of the fakeClock func (fc *fakeClock) Now() time.Time { fc.l.RLock() t := fc.time @@ -145,6 +150,8 @@ func (fc *fakeClock) Since(t time.Time) time.Duration { return fc.Now().Sub(t) } +// NewTicker returns a ticker that will expire only after calls to fakeClock +// Advance have moved the clock passed the given duration func (fc *fakeClock) NewTicker(d time.Duration) Ticker { ft := &fakeTicker{ c: make(chan time.Time, 1), @@ -156,6 +163,25 @@ func (fc *fakeClock) NewTicker(d time.Duration) Ticker { return ft } +// NewTimer returns a timer that will fire only after calls to fakeClock +// Advance have moved the clock passed the given duration +func (fc *fakeClock) NewTimer(d time.Duration) Timer { + stopped := uint32(0) + if d <= 0 { + stopped = 1 + } + ft := &fakeTimer{ + c: make(chan time.Time, 1), + stop: make(chan struct{}, 1), + reset: make(chan reset, 1), + clock: fc, + stopped: stopped, + } + + ft.run(d) + return ft +} + // Advance advances fakeClock to a new point in time, ensuring channels from any // previous invocations of After are notified appropriately before returning func (fc *fakeClock) Advance(d time.Duration) { @@ -179,12 +205,12 @@ func (fc *fakeClock) Advance(d time.Duration) { // (callers of Sleep or After) func (fc *fakeClock) BlockUntil(n int) { fc.l.Lock() - // Fast path: current number of sleepers is what we're looking for - if len(fc.sleepers) == n { + // Fast path: we already have >= n sleepers. + if len(fc.sleepers) >= n { fc.l.Unlock() return } - // Otherwise, set up a new blocker + // Otherwise, we have < n sleepers. Set up a new blocker to wait for more. b := &blocker{ count: n, ch: make(chan struct{}), diff --git a/vendor/github.com/jonboulle/clockwork/context.go b/vendor/github.com/jonboulle/clockwork/context.go new file mode 100644 index 000000000..edbb368f0 --- /dev/null +++ b/vendor/github.com/jonboulle/clockwork/context.go @@ -0,0 +1,25 @@ +package clockwork + +import ( + "context" +) + +// contextKey is private to this package so we can ensure uniqueness here. This +// type identifies context values provided by this package. +type contextKey string + +// keyClock provides a clock for injecting during tests. If absent, a real clock should be used. +var keyClock = contextKey("clock") // clockwork.Clock + +// AddToContext creates a derived context that references the specified clock. +func AddToContext(ctx context.Context, clock Clock) context.Context { + return context.WithValue(ctx, keyClock, clock) +} + +// FromContext extracts a clock from the context. If not present, a real clock is returned. +func FromContext(ctx context.Context) Clock { + if clock, ok := ctx.Value(keyClock).(Clock); ok { + return clock + } + return NewRealClock() +} diff --git a/vendor/github.com/jonboulle/clockwork/timer.go b/vendor/github.com/jonboulle/clockwork/timer.go new file mode 100644 index 000000000..d41d9616c --- /dev/null +++ b/vendor/github.com/jonboulle/clockwork/timer.go @@ -0,0 +1,97 @@ +package clockwork + +import ( + "sync/atomic" + "time" +) + +// Timer provides an interface which can be used instead of directly +// using the timer within the time module. The real-time timer t +// provides events through t.C which becomes now t.Chan() to make +// this channel requirement definable in this interface. +type Timer interface { + Chan() <-chan time.Time + Reset(d time.Duration) bool + Stop() bool +} + +type realTimer struct { + *time.Timer +} + +func (r realTimer) Chan() <-chan time.Time { + return r.C +} + +type fakeTimer struct { + c chan time.Time + clock FakeClock + stop chan struct{} + reset chan reset + stopped uint32 +} + +func (f *fakeTimer) Chan() <-chan time.Time { + return f.c +} + +func (f *fakeTimer) Reset(d time.Duration) bool { + stopped := f.Stop() + + f.reset <- reset{t: f.clock.Now().Add(d), next: f.clock.After(d)} + if d > 0 { + atomic.StoreUint32(&f.stopped, 0) + } + + return stopped +} + +func (f *fakeTimer) Stop() bool { + if atomic.CompareAndSwapUint32(&f.stopped, 0, 1) { + f.stop <- struct{}{} + return true + } + return false +} + +type reset struct { + t time.Time + next <-chan time.Time +} + +// run initializes a background goroutine to send the timer event to the timer channel +// after the period. Events are discarded if the underlying ticker channel does not have +// enough capacity. +func (f *fakeTimer) run(initialDuration time.Duration) { + nextTick := f.clock.Now().Add(initialDuration) + next := f.clock.After(initialDuration) + + waitForReset := func() (time.Time, <-chan time.Time) { + for { + select { + case <-f.stop: + continue + case r := <-f.reset: + return r.t, r.next + } + } + } + + go func() { + for { + select { + case <-f.stop: + case <-next: + atomic.StoreUint32(&f.stopped, 1) + select { + case f.c <- nextTick: + default: + } + + next = nil + } + + nextTick, next = waitForReset() + } + }() +} diff --git a/vendor/github.com/mattermost/xml-roundtrip-validator/LICENSE.txt b/vendor/github.com/mattermost/xml-roundtrip-validator/LICENSE.txt new file mode 100644 index 000000000..261eeb9e9 --- /dev/null +++ b/vendor/github.com/mattermost/xml-roundtrip-validator/LICENSE.txt @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. diff --git a/vendor/github.com/mattermost/xml-roundtrip-validator/README.md b/vendor/github.com/mattermost/xml-roundtrip-validator/README.md new file mode 100644 index 000000000..ec81a2a67 --- /dev/null +++ b/vendor/github.com/mattermost/xml-roundtrip-validator/README.md @@ -0,0 +1,73 @@ +# xml-roundtrip-validator + +The Go module `github.com/mattermost/xml-roundtrip-validator` implements mitigations for multiple security issues in Go's `encoding/xml`. Applications that use `encoding/xml` for security-critical operations, such as XML signature validation and SAML, may use the `Validate` and `ValidateAll` functions to avoid impact from malicious XML inputs. + +## Usage + +### Validate + +```Go +import ( + "strings" + + xrv "github.com/mattermost/xml-roundtrip-validator" +) + +func DoStuffWithXML(input string) { + if err := xrv.Validate(strings.NewReader(input)); err != nil { + panic(err) + } + // validation succeeded, input is safe + actuallyDoStuffWithXML(input) +} +``` + +### ValidateAll + +```Go +import ( + "strings" + + xrv "github.com/mattermost/xml-roundtrip-validator" +) + +func DoStuffWithXML(input string) { + if errs := xrv.ValidateAll(strings.NewReader(input)); len(errs) != 0 { + for err := range errs { + // here you can log each error individually if you like + } + return + } + // validation succeeded, input is safe + actuallyDoStuffWithXML(input) +} +``` + +### CLI + +Compiling: + +``` +$ go build cmd/xrv.go +``` + +Running: + +``` +$ ./xrv good.xml +Document validated without errors +$ ./xrv bad.xml +validator: in token starting at 2:5: roundtrip error: expected {{ :Element} []}, observed {{ Element} []} +$ ./xrv -all bad.xml +validator: in token starting at 2:5: roundtrip error: expected {{ :Element} []}, observed {{ Element} []} +validator: in token starting at 3:5: roundtrip error: expected {{ Element} [{{ :attr} z}]}, observed {{ Element} [{{ attr} z}]} +``` + +## Go vulnerabilities addressed + +Descriptions of the Go vulnerabilities addressed by this module can be found in the advisories directory. Specifically, the issues addressed are: + + - [Element namespace prefix instability](./advisories/unstable-elements.md) + - [Attribute namespace prefix instability](./advisories/unstable-attributes.md) + - [Directive comment instability](./advisories/unstable-directives.md) + - Any other similar roundtrip issues we may not know about diff --git a/vendor/github.com/mattermost/xml-roundtrip-validator/SECURITY.md b/vendor/github.com/mattermost/xml-roundtrip-validator/SECURITY.md new file mode 100644 index 000000000..4cb6c58d1 --- /dev/null +++ b/vendor/github.com/mattermost/xml-roundtrip-validator/SECURITY.md @@ -0,0 +1,25 @@ +Security +======== + +Safety and data security is of the utmost priority for the Mattermost community. If you are a security researcher and have discovered a security vulnerability in our codebase, we would appreciate your help in disclosing it to us in a responsible manner. + +Reporting security issues +------------------------- + +**Please do not use GitHub issues for security-sensitive communication.** + +Security issues in the community test server, any of the open source codebases maintained by Mattermost, or any of our commercial offerings should be reported via email to [responsibledisclosure@mattermost.com](mailto:responsibledisclosure@mattermost.com). Mattermost is committed to working together with researchers and keeping them updated throughout the patching process. Researchers who responsibly report valid security issues will be publicly credited for their efforts (if they so choose). + +For a more detailed description of the disclosure process and a list of researchers who have previously contributed to the disclosure program, see [Report a Security Vulnerability](https://mattermost.com/security-vulnerability-report/) on the Mattermost website. + +Security updates +---------------- + +Mattermost has a mandatory upgrade policy, and updates are only provided for the latest 3 releases and the current Extended Support Release (ESR). Critical updates are delivered as dot releases. Details on security updates are announced 30 days after the availability of the update. + +For more details about the security content of past releases, see the [Security Updates](https://mattermost.com/security-updates/) page on the Mattermost website. For timely notifications about new security updates, subscribe to the [Security Bulletins Mailing List](https://about.mattermost.com/security-bulletin). + +Contributing to this policy +--------------------------- + +If you have feedback or suggestions on improving this policy document, please [create an issue](https://github.com/mattermost/mattermost-server/issues/new). diff --git a/vendor/github.com/mattermost/xml-roundtrip-validator/go.mod b/vendor/github.com/mattermost/xml-roundtrip-validator/go.mod new file mode 100644 index 000000000..227e437ae --- /dev/null +++ b/vendor/github.com/mattermost/xml-roundtrip-validator/go.mod @@ -0,0 +1,5 @@ +module github.com/mattermost/xml-roundtrip-validator + +go 1.14 + +require github.com/stretchr/testify v1.6.1 diff --git a/vendor/github.com/mattermost/xml-roundtrip-validator/go.sum b/vendor/github.com/mattermost/xml-roundtrip-validator/go.sum new file mode 100644 index 000000000..1f1e7af97 --- /dev/null +++ b/vendor/github.com/mattermost/xml-roundtrip-validator/go.sum @@ -0,0 +1,12 @@ +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/vendor/github.com/mattermost/xml-roundtrip-validator/validator.go b/vendor/github.com/mattermost/xml-roundtrip-validator/validator.go new file mode 100644 index 000000000..523659dc0 --- /dev/null +++ b/vendor/github.com/mattermost/xml-roundtrip-validator/validator.go @@ -0,0 +1,292 @@ +package validator + +import ( + "bytes" + "encoding/xml" + "errors" + "fmt" + "io" +) + +// XMLRoundtripError is returned when a round-trip token doesn't match the original +type XMLRoundtripError struct { + Expected, Observed xml.Token + Overflow []byte +} + +func (err XMLRoundtripError) Error() string { + if len(err.Overflow) == 0 { + return fmt.Sprintf("roundtrip error: expected %v, observed %v", err.Expected, err.Observed) + } + return fmt.Sprintf("roundtrip error: unexpected overflow after token: %s", err.Overflow) +} + +// XMLValidationError is returned when validating an XML document fails +type XMLValidationError struct { + Start, End, Line, Column int64 + err error +} + +func (err XMLValidationError) Error() string { + return fmt.Sprintf("validator: in token starting at %d:%d: %s", err.Line, err.Column, err.err.Error()) +} + +func (err XMLValidationError) Unwrap() error { + return err.err +} + +// Validate makes sure the given XML bytes survive round trips through encoding/xml without mutations +func Validate(xmlReader io.Reader) error { + xmlBuffer := &bytes.Buffer{} + xmlReader = &byteReader{io.TeeReader(xmlReader, xmlBuffer)} + decoder := xml.NewDecoder(xmlReader) + decoder.Strict = false + decoder.CharsetReader = func(charset string, input io.Reader) (io.Reader, error) { return input, nil } + offset := int64(0) + for { + token, err := decoder.RawToken() + if err == io.EOF { + return nil + } else if err != nil { + return err + } + if err := CheckToken(token); err != nil { + xmlBytes := xmlBuffer.Bytes() + line := bytes.Count(xmlBytes[0:offset], []byte{'\n'}) + 1 + lineStart := int64(bytes.LastIndexByte(xmlBytes[0:offset], '\n')) + 1 + column := offset - lineStart + 1 + return XMLValidationError{ + Start: offset, + End: decoder.InputOffset(), + Line: int64(line), + Column: column, + err: err, + } + } + offset = decoder.InputOffset() + } +} + +// ValidateAll is like Validate, but instead of returning after the first error, +// it accumulates errors and validates the entire document +func ValidateAll(xmlReader io.Reader) []error { + xmlBuffer := &bytes.Buffer{} + xmlReader = io.TeeReader(xmlReader, xmlBuffer) + errs := []error{} + offset := int64(0) + line := int64(1) + column := int64(1) + for { + err := Validate(xmlReader) + if err == nil { + // reached the end with no additional errors + break + } + validationError := XMLValidationError{} + if errors.As(err, &validationError) { + // validation errors contain line numbers and offsets, but + // these offsets are based on the offset where Validate + // was called, so they need to be adjusted to accordingly + validationError.Start += offset + validationError.End += offset + if validationError.Line == 1 { + validationError.Column += column - 1 + } + validationError.Line += line - 1 + errs = append(errs, validationError) + xmlBytes := xmlBuffer.Bytes() + offset += int64(len(xmlBytes)) + newLines := int64(bytes.Count(xmlBytes, []byte("\n"))) + line += newLines + if newLines > 0 { + column = int64(len(xmlBytes) - bytes.LastIndex(xmlBytes, []byte("\n"))) + } else { + column += int64(len(xmlBytes)) + } + xmlBuffer.Reset() + } else { + // this was not a validation error, but likely + // completely unparseable XML instead; no point + // in trying to continue + errs = append(errs, err) + break + } + } + return errs +} + +// bufio implements a ByteReader but we explicitly don't want any buffering +type byteReader struct { + r io.Reader +} + +func (r *byteReader) ReadByte() (byte, error) { + var p [1]byte + n, err := r.r.Read(p[:]) + + // The doc for the io.ByteReader interface states: + // If ReadByte returns an error, no input byte was consumed, and the returned byte value is undefined. + // So if a byte is actually extracted from the reader, and we want to return it, we mustn't return the error. + if n > 0 { + // this byteReader is only used in the context of the Validate() function, + // we deliberately choose to completely ignore the error in this case. + // return the byte extracted from the reader + return p[0], nil + } + + return 0, err +} + +func (r *byteReader) Read(p []byte) (int, error) { + return r.r.Read(p) +} + +// CheckToken computes a round trip for a given xml.Token and returns an +// error if the newly calculated token differs from the original +func CheckToken(before xml.Token) error { + buffer := &bytes.Buffer{} + encoder := xml.NewEncoder(buffer) + + switch t := before.(type) { + case xml.EndElement: + // xml.Encoder expects matching StartElements for all EndElements + if err := encoder.EncodeToken(xml.StartElement{Name: t.Name}); err != nil { + return err + } + } + + if err := encoder.EncodeToken(before); err != nil { + return err + } + if err := encoder.Flush(); err != nil { + return err + } + encoded := buffer.Bytes() + decoder := xml.NewDecoder(bytes.NewReader(encoded)) + decoder.CharsetReader = func(charset string, input io.Reader) (io.Reader, error) { return input, nil } + + switch before.(type) { + case xml.EndElement: + // throw away the StartElement we added above + if _, err := decoder.RawToken(); err != nil { + return err + } + } + + after, err := decoder.RawToken() + if err != nil { + return err + } + + if !tokenEquals(before, after) { + return XMLRoundtripError{before, after, nil} + } + offset := decoder.InputOffset() + if offset != int64(len(encoded)) { + // this is likely unreachable, but just in case + return XMLRoundtripError{before, after, encoded[offset:]} + } + return nil +} + +func tokenEquals(before, after xml.Token) bool { + switch t1 := before.(type) { + + case xml.CharData: + t2, ok := after.(xml.CharData) + if !ok { + return false + } + return bytes.Equal(t1, t2) + + case xml.Comment: + t2, ok := after.(xml.Comment) + if !ok { + return false + } + return bytes.Equal(t1, t2) + + case xml.Directive: + t2, ok := after.(xml.Directive) + if !ok { + return false + } + return bytes.Equal(t1, t2) + + case xml.EndElement: + t2, ok := after.(xml.EndElement) + if !ok { + return false + } + // local name should equal; namespace prefixes get erased + return t1.Name.Local == t2.Name.Local && t2.Name.Space == "" + + case xml.ProcInst: + t2, ok := after.(xml.ProcInst) + if !ok { + return false + } + return t1.Target == t2.Target && bytes.Equal(t1.Inst, t2.Inst) + + case xml.StartElement: + t2, ok := after.(xml.StartElement) + if !ok { + return false + } + // encoding/xml messes up namespace prefixes on both tag and attribute names; + // they need adjusting to make the comparison possible + fixNamespacePrefixes(&t1, &t2) + if t1.Name != t2.Name { + return false + } + if len(t1.Attr) != len(t2.Attr) { + return false + } + // after the call to fixNamespacePrefixes, all attributes should match; + // ordering is preserved + for i, attr := range t1.Attr { + if attr != t2.Attr[i] { + return false + } + } + return true + } + return false +} + +func fixNamespacePrefixes(before, after *xml.StartElement) { + // if the after token has more attributes than the before token, + // the round trip likely introduced new xmlns attributes + if len(after.Attr) > len(before.Attr) { + + // handle erased tag prefixes; the corresponding xmlns attribute is always the first one + if (before.Name.Space != "" && after.Name.Space == "" && after.Attr[0].Name == xml.Name{Local: "xmlns"}) { + after.Name.Space = after.Attr[0].Value + after.Attr = after.Attr[1:] + } + + // handle attribute prefixes; the xmlns attribute always comes immediately before the prefixed attribute + for len(after.Attr) > len(before.Attr) && len(after.Attr) > 1 { + var xmlns *xml.Attr + i := 1 + for ; i < len(after.Attr); i++ { + if after.Attr[i-1].Name.Space == "xmlns" && after.Attr[i-1].Name.Local == after.Attr[i].Name.Space { + xmlns = &after.Attr[i-1] + break + } + } + if xmlns == nil { + break + } + prefix := xmlns.Name.Local + space := xmlns.Value + copy(after.Attr[i-1:], after.Attr[i:]) + after.Attr = after.Attr[:len(after.Attr)-1] + for j := range after.Attr { + if after.Attr[j].Name.Space == prefix { + after.Attr[j].Name.Space = space + } + } + } + } +} diff --git a/vendor/github.com/nu7hatch/gouuid/.gitignore b/vendor/github.com/nu7hatch/gouuid/.gitignore deleted file mode 100644 index f9d9cd8ab..000000000 --- a/vendor/github.com/nu7hatch/gouuid/.gitignore +++ /dev/null @@ -1,11 +0,0 @@ -_obj -_test -*.6 -*.out -_testmain.go -\#* -.\#* -*.log -_cgo* -*.o -*.a diff --git a/vendor/github.com/nu7hatch/gouuid/COPYING b/vendor/github.com/nu7hatch/gouuid/COPYING deleted file mode 100644 index d7849fd8f..000000000 --- a/vendor/github.com/nu7hatch/gouuid/COPYING +++ /dev/null @@ -1,19 +0,0 @@ -Copyright (C) 2011 by Krzysztof Kowalik - -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies -of the Software, and to permit persons to whom the Software is furnished to do -so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. \ No newline at end of file diff --git a/vendor/github.com/nu7hatch/gouuid/README.md b/vendor/github.com/nu7hatch/gouuid/README.md deleted file mode 100644 index e3d025d5e..000000000 --- a/vendor/github.com/nu7hatch/gouuid/README.md +++ /dev/null @@ -1,21 +0,0 @@ -# Pure Go UUID implementation - -This package provides immutable UUID structs and the functions -NewV3, NewV4, NewV5 and Parse() for generating versions 3, 4 -and 5 UUIDs as specified in [RFC 4122](http://www.ietf.org/rfc/rfc4122.txt). - -## Installation - -Use the `go` tool: - - $ go get github.com/nu7hatch/gouuid - -## Usage - -See [documentation and examples](http://godoc.org/github.com/nu7hatch/gouuid) -for more information. - -## Copyright - -Copyright (C) 2011 by Krzysztof Kowalik . See [COPYING](https://github.com/nu7hatch/gouuid/tree/master/COPYING) -file for details. diff --git a/vendor/github.com/nu7hatch/gouuid/uuid.go b/vendor/github.com/nu7hatch/gouuid/uuid.go deleted file mode 100644 index ac9623b72..000000000 --- a/vendor/github.com/nu7hatch/gouuid/uuid.go +++ /dev/null @@ -1,173 +0,0 @@ -// This package provides immutable UUID structs and the functions -// NewV3, NewV4, NewV5 and Parse() for generating versions 3, 4 -// and 5 UUIDs as specified in RFC 4122. -// -// Copyright (C) 2011 by Krzysztof Kowalik -package uuid - -import ( - "crypto/md5" - "crypto/rand" - "crypto/sha1" - "encoding/hex" - "errors" - "fmt" - "hash" - "regexp" -) - -// The UUID reserved variants. -const ( - ReservedNCS byte = 0x80 - ReservedRFC4122 byte = 0x40 - ReservedMicrosoft byte = 0x20 - ReservedFuture byte = 0x00 -) - -// The following standard UUIDs are for use with NewV3() or NewV5(). -var ( - NamespaceDNS, _ = ParseHex("6ba7b810-9dad-11d1-80b4-00c04fd430c8") - NamespaceURL, _ = ParseHex("6ba7b811-9dad-11d1-80b4-00c04fd430c8") - NamespaceOID, _ = ParseHex("6ba7b812-9dad-11d1-80b4-00c04fd430c8") - NamespaceX500, _ = ParseHex("6ba7b814-9dad-11d1-80b4-00c04fd430c8") -) - -// Pattern used to parse hex string representation of the UUID. -// FIXME: do something to consider both brackets at one time, -// current one allows to parse string with only one opening -// or closing bracket. -const hexPattern = "^(urn\\:uuid\\:)?\\{?([a-z0-9]{8})-([a-z0-9]{4})-" + - "([1-5][a-z0-9]{3})-([a-z0-9]{4})-([a-z0-9]{12})\\}?$" - -var re = regexp.MustCompile(hexPattern) - -// A UUID representation compliant with specification in -// RFC 4122 document. -type UUID [16]byte - -// ParseHex creates a UUID object from given hex string -// representation. Function accepts UUID string in following -// formats: -// -// uuid.ParseHex("6ba7b814-9dad-11d1-80b4-00c04fd430c8") -// uuid.ParseHex("{6ba7b814-9dad-11d1-80b4-00c04fd430c8}") -// uuid.ParseHex("urn:uuid:6ba7b814-9dad-11d1-80b4-00c04fd430c8") -// -func ParseHex(s string) (u *UUID, err error) { - md := re.FindStringSubmatch(s) - if md == nil { - err = errors.New("Invalid UUID string") - return - } - hash := md[2] + md[3] + md[4] + md[5] + md[6] - b, err := hex.DecodeString(hash) - if err != nil { - return - } - u = new(UUID) - copy(u[:], b) - return -} - -// Parse creates a UUID object from given bytes slice. -func Parse(b []byte) (u *UUID, err error) { - if len(b) != 16 { - err = errors.New("Given slice is not valid UUID sequence") - return - } - u = new(UUID) - copy(u[:], b) - return -} - -// Generate a UUID based on the MD5 hash of a namespace identifier -// and a name. -func NewV3(ns *UUID, name []byte) (u *UUID, err error) { - if ns == nil { - err = errors.New("Invalid namespace UUID") - return - } - u = new(UUID) - // Set all bits to MD5 hash generated from namespace and name. - u.setBytesFromHash(md5.New(), ns[:], name) - u.setVariant(ReservedRFC4122) - u.setVersion(3) - return -} - -// Generate a random UUID. -func NewV4() (u *UUID, err error) { - u = new(UUID) - // Set all bits to randomly (or pseudo-randomly) chosen values. - _, err = rand.Read(u[:]) - if err != nil { - return - } - u.setVariant(ReservedRFC4122) - u.setVersion(4) - return -} - -// Generate a UUID based on the SHA-1 hash of a namespace identifier -// and a name. -func NewV5(ns *UUID, name []byte) (u *UUID, err error) { - u = new(UUID) - // Set all bits to truncated SHA1 hash generated from namespace - // and name. - u.setBytesFromHash(sha1.New(), ns[:], name) - u.setVariant(ReservedRFC4122) - u.setVersion(5) - return -} - -// Generate a MD5 hash of a namespace and a name, and copy it to the -// UUID slice. -func (u *UUID) setBytesFromHash(hash hash.Hash, ns, name []byte) { - hash.Write(ns[:]) - hash.Write(name) - copy(u[:], hash.Sum([]byte{})[:16]) -} - -// Set the two most significant bits (bits 6 and 7) of the -// clock_seq_hi_and_reserved to zero and one, respectively. -func (u *UUID) setVariant(v byte) { - switch v { - case ReservedNCS: - u[8] = (u[8] | ReservedNCS) & 0xBF - case ReservedRFC4122: - u[8] = (u[8] | ReservedRFC4122) & 0x7F - case ReservedMicrosoft: - u[8] = (u[8] | ReservedMicrosoft) & 0x3F - } -} - -// Variant returns the UUID Variant, which determines the internal -// layout of the UUID. This will be one of the constants: RESERVED_NCS, -// RFC_4122, RESERVED_MICROSOFT, RESERVED_FUTURE. -func (u *UUID) Variant() byte { - if u[8]&ReservedNCS == ReservedNCS { - return ReservedNCS - } else if u[8]&ReservedRFC4122 == ReservedRFC4122 { - return ReservedRFC4122 - } else if u[8]&ReservedMicrosoft == ReservedMicrosoft { - return ReservedMicrosoft - } - return ReservedFuture -} - -// Set the four most significant bits (bits 12 through 15) of the -// time_hi_and_version field to the 4-bit version number. -func (u *UUID) setVersion(v byte) { - u[6] = (u[6] & 0xF) | (v << 4) -} - -// Version returns a version number of the algorithm used to -// generate the UUID sequence. -func (u *UUID) Version() uint { - return uint(u[6] >> 4) -} - -// Returns unparsed version of the generated UUID sequence. -func (u *UUID) String() string { - return fmt.Sprintf("%x-%x-%x-%x-%x", u[0:4], u[4:6], u[6:8], u[8:10], u[10:]) -} diff --git a/vendor/github.com/russellhaering/gosaml2/.gitignore b/vendor/github.com/russellhaering/gosaml2/.gitignore new file mode 100644 index 000000000..9ed3b07ce --- /dev/null +++ b/vendor/github.com/russellhaering/gosaml2/.gitignore @@ -0,0 +1 @@ +*.test diff --git a/vendor/github.com/russellhaering/gosaml2/LICENSE b/vendor/github.com/russellhaering/gosaml2/LICENSE new file mode 100644 index 000000000..67db85882 --- /dev/null +++ b/vendor/github.com/russellhaering/gosaml2/LICENSE @@ -0,0 +1,175 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. diff --git a/vendor/github.com/russellhaering/gosaml2/README.md b/vendor/github.com/russellhaering/gosaml2/README.md new file mode 100644 index 000000000..4cbb22d77 --- /dev/null +++ b/vendor/github.com/russellhaering/gosaml2/README.md @@ -0,0 +1,35 @@ +# gosaml2 + +[![Build Status](https://github.com/russellhaering/gosaml2/actions/workflows/test.yml/badge.svg?branch=main)](https://github.com/russellhaering/gosaml2/actions/workflows/test.yml?query=branch%3Amain) +[![GoDoc](https://godoc.org/github.com/russellhaering/gosaml2?status.svg)](https://godoc.org/github.com/russellhaering/gosaml2) + +SAML 2.0 implemementation for Service Providers based on [etree](https://github.com/beevik/etree) +and [goxmldsig](https://github.com/russellhaering/goxmldsig), a pure Go +implementation of XML digital signatures. + +## Installation + +Install `gosaml2` into your `$GOPATH` using `go get`: + +``` +go get github.com/russellhaering/gosaml2 +``` + +## Example + +See [demo.go](s2example/demo.go). + +## Supported Identity Providers + +This library is meant to be a generic SAML implementation. If you find a +standards compliant identity provider that it doesn't work with please +submit a bug or pull request. + +The following identity providers have been tested: + +* Okta +* Auth0 +* Shibboleth +* Ipsilon +* OneLogin +* Azure Active Directory (Azure AD) diff --git a/vendor/github.com/russellhaering/gosaml2/SECURITY.md b/vendor/github.com/russellhaering/gosaml2/SECURITY.md new file mode 100644 index 000000000..a20c440b6 --- /dev/null +++ b/vendor/github.com/russellhaering/gosaml2/SECURITY.md @@ -0,0 +1,5 @@ +# Security Policy + +## Reporting a Vulnerability + +Security vulnerabilities can be reported using GitHub's [private vulnerability reporting tool](https://github.com/russellhaering/gosaml2/security/advisories/new). diff --git a/vendor/github.com/russellhaering/gosaml2/attribute.go b/vendor/github.com/russellhaering/gosaml2/attribute.go new file mode 100644 index 000000000..d1992d79a --- /dev/null +++ b/vendor/github.com/russellhaering/gosaml2/attribute.go @@ -0,0 +1,66 @@ +// Copyright 2016 Russell Haering et al. +// +// 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 saml2 + +import "github.com/russellhaering/gosaml2/types" + +// Values is a convenience wrapper for a map of strings to Attributes, which +// can be used for easy access to the string values of Attribute lists. +type Values map[string]types.Attribute + +// Get is a safe method (nil maps will not panic) for returning the first value +// for an attribute at a key, or the empty string if none exists. +func (vals Values) Get(k string) string { + if vals == nil { + return "" + } + if v, ok := vals[k]; ok && len(v.Values) > 0 { + return string(v.Values[0].Value) + } + return "" +} + +//GetSize returns the number of values for an attribute at a key. +//Returns '0' in case of error or if key is not found. +func (vals Values) GetSize(k string) int { + if vals == nil { + return 0 + } + + v, ok := vals[k] + if ok { + return len(v.Values) + } + + return 0 +} + +//GetAll returns all the values for an attribute at a key. +//Returns an empty slice in case of error of if key is not found. +func (vals Values) GetAll(k string) []string { + var av []string + + if vals == nil { + return av + } + + if v, ok := vals[k]; ok && len(v.Values) > 0 { + for i := 0; i < len(v.Values); i++ { + av = append(av, string(v.Values[i].Value)) + } + } + + return av +} diff --git a/vendor/github.com/russellhaering/gosaml2/authn_request.go b/vendor/github.com/russellhaering/gosaml2/authn_request.go new file mode 100644 index 000000000..096b6b6c2 --- /dev/null +++ b/vendor/github.com/russellhaering/gosaml2/authn_request.go @@ -0,0 +1,30 @@ +// Copyright 2016 Russell Haering et al. +// +// 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 saml2 + +import "time" + +// AuthNRequest is the go struct representation of an authentication request +type AuthNRequest struct { + ID string `xml:",attr"` + Version string `xml:",attr"` + ProtocolBinding string `xml:",attr"` + AssertionConsumerServiceURL string `xml:",attr"` + + IssueInstant time.Time `xml:",attr"` + + Destination string `xml:",attr"` + Issuer string +} diff --git a/vendor/github.com/russellhaering/gosaml2/build_logout_response.go b/vendor/github.com/russellhaering/gosaml2/build_logout_response.go new file mode 100644 index 000000000..bfdf94318 --- /dev/null +++ b/vendor/github.com/russellhaering/gosaml2/build_logout_response.go @@ -0,0 +1,158 @@ +// Copyright 2016 Russell Haering et al. +// +// 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 saml2 + +import ( + "bytes" + "encoding/base64" + "html/template" + + "github.com/beevik/etree" + "github.com/russellhaering/gosaml2/uuid" +) + +func (sp *SAMLServiceProvider) buildLogoutResponse(statusCodeValue string, reqID string, includeSig bool) (*etree.Document, error) { + logoutResponse := &etree.Element{ + Space: "samlp", + Tag: "LogoutResponse", + } + + logoutResponse.CreateAttr("xmlns:samlp", "urn:oasis:names:tc:SAML:2.0:protocol") + logoutResponse.CreateAttr("xmlns:saml", "urn:oasis:names:tc:SAML:2.0:assertion") + + arId := uuid.NewV4() + + logoutResponse.CreateAttr("ID", "_"+arId.String()) + logoutResponse.CreateAttr("Version", "2.0") + logoutResponse.CreateAttr("IssueInstant", sp.Clock.Now().UTC().Format(issueInstantFormat)) + logoutResponse.CreateAttr("Destination", sp.IdentityProviderSLOURL) + logoutResponse.CreateAttr("InResponseTo", reqID) + + // NOTE(russell_h): In earlier versions we mistakenly sent the IdentityProviderIssuer + // in the AuthnRequest. For backwards compatibility we will fall back to that + // behavior when ServiceProviderIssuer isn't set. + if sp.ServiceProviderIssuer != "" { + logoutResponse.CreateElement("saml:Issuer").SetText(sp.ServiceProviderIssuer) + } else { + logoutResponse.CreateElement("saml:Issuer").SetText(sp.IdentityProviderIssuer) + } + + status := logoutResponse.CreateElement("samlp:Status") + statusCode := status.CreateElement("samlp:StatusCode") + statusCode.CreateAttr("Value", statusCodeValue) + + doc := etree.NewDocument() + + // Only POST binding includes in (includeSig) + if includeSig { + signed, err := sp.SignLogoutResponse(logoutResponse) + if err != nil { + return nil, err + } + + doc.SetRoot(signed) + } else { + doc.SetRoot(logoutResponse) + } + return doc, nil +} +func (sp *SAMLServiceProvider) BuildLogoutResponseDocument(status string, reqID string) (*etree.Document, error) { + return sp.buildLogoutResponse(status, reqID, true) +} + +func (sp *SAMLServiceProvider) BuildLogoutResponseDocumentNoSig(status string, reqID string) (*etree.Document, error) { + return sp.buildLogoutResponse(status, reqID, false) +} + +func (sp *SAMLServiceProvider) SignLogoutResponse(el *etree.Element) (*etree.Element, error) { + ctx := sp.SigningContext() + + sig, err := ctx.ConstructSignature(el, true) + if err != nil { + return nil, err + } + + ret := el.Copy() + + var children []etree.Token + children = append(children, ret.Child[0]) // issuer is always first + children = append(children, sig) // next is the signature + children = append(children, ret.Child[1:]...) // then all other children + ret.Child = children + + return ret, nil +} + +func (sp *SAMLServiceProvider) buildLogoutResponseBodyPostFromDocument(relayState string, doc *etree.Document) ([]byte, error) { + respBuf, err := doc.WriteToBytes() + if err != nil { + return nil, err + } + + encodedRespBuf := base64.StdEncoding.EncodeToString(respBuf) + + var tmpl *template.Template + var rv bytes.Buffer + + if relayState != "" { + tmpl = template.Must(template.New("saml-post-form").Parse(`` + + `
` + + `` + + `` + + `` + + `
` + + `` + + `` + + ``)) + data := struct { + URL string + SAMLResponse string + RelayState string + }{ + URL: sp.IdentityProviderSLOURL, + SAMLResponse: encodedRespBuf, + RelayState: relayState, + } + if err = tmpl.Execute(&rv, data); err != nil { + return nil, err + } + } else { + tmpl = template.Must(template.New("saml-post-form").Parse(`` + + `
` + + `` + + `` + + `
` + + `` + + `` + + ``)) + data := struct { + URL string + SAMLResponse string + }{ + URL: sp.IdentityProviderSLOURL, + SAMLResponse: encodedRespBuf, + } + + if err = tmpl.Execute(&rv, data); err != nil { + return nil, err + } + } + + return rv.Bytes(), nil +} + +func (sp *SAMLServiceProvider) BuildLogoutResponseBodyPostFromDocument(relayState string, doc *etree.Document) ([]byte, error) { + return sp.buildLogoutResponseBodyPostFromDocument(relayState, doc) +} diff --git a/vendor/github.com/russellhaering/gosaml2/build_request.go b/vendor/github.com/russellhaering/gosaml2/build_request.go new file mode 100644 index 000000000..0885c1cc8 --- /dev/null +++ b/vendor/github.com/russellhaering/gosaml2/build_request.go @@ -0,0 +1,557 @@ +// Copyright 2016 Russell Haering et al. +// +// 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 saml2 + +import ( + "bytes" + "compress/flate" + "encoding/base64" + "fmt" + "html/template" + "net/http" + "net/url" + + "github.com/beevik/etree" + "github.com/russellhaering/gosaml2/uuid" +) + +const issueInstantFormat = "2006-01-02T15:04:05Z" + +func (sp *SAMLServiceProvider) buildAuthnRequest(includeSig bool) (*etree.Document, error) { + authnRequest := &etree.Element{ + Space: "samlp", + Tag: "AuthnRequest", + } + + authnRequest.CreateAttr("xmlns:samlp", "urn:oasis:names:tc:SAML:2.0:protocol") + authnRequest.CreateAttr("xmlns:saml", "urn:oasis:names:tc:SAML:2.0:assertion") + + arId := uuid.NewV4() + + authnRequest.CreateAttr("ID", "_"+arId.String()) + authnRequest.CreateAttr("Version", "2.0") + authnRequest.CreateAttr("ProtocolBinding", "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST") + authnRequest.CreateAttr("AssertionConsumerServiceURL", sp.AssertionConsumerServiceURL) + authnRequest.CreateAttr("IssueInstant", sp.Clock.Now().UTC().Format(issueInstantFormat)) + authnRequest.CreateAttr("Destination", sp.IdentityProviderSSOURL) + if sp.ForceAuthn { + authnRequest.CreateAttr("ForceAuthn", "true") + } + if sp.IsPassive { + authnRequest.CreateAttr("IsPassive", "true") + } + + // NOTE(russell_h): In earlier versions we mistakenly sent the IdentityProviderIssuer + // in the AuthnRequest. For backwards compatibility we will fall back to that + // behavior when ServiceProviderIssuer isn't set. + if sp.ServiceProviderIssuer != "" { + authnRequest.CreateElement("saml:Issuer").SetText(sp.ServiceProviderIssuer) + } else { + authnRequest.CreateElement("saml:Issuer").SetText(sp.IdentityProviderIssuer) + } + + nameIdPolicy := authnRequest.CreateElement("samlp:NameIDPolicy") + nameIdPolicy.CreateAttr("AllowCreate", "true") + if sp.NameIdFormat != "" { + nameIdPolicy.CreateAttr("Format", sp.NameIdFormat) + } + + if sp.RequestedAuthnContext != nil { + requestedAuthnContext := authnRequest.CreateElement("samlp:RequestedAuthnContext") + requestedAuthnContext.CreateAttr("Comparison", sp.RequestedAuthnContext.Comparison) + + for _, context := range sp.RequestedAuthnContext.Contexts { + authnContextClassRef := requestedAuthnContext.CreateElement("saml:AuthnContextClassRef") + authnContextClassRef.SetText(context) + } + } + + doc := etree.NewDocument() + + // Only POST binding includes in (includeSig) + if sp.SignAuthnRequests && includeSig { + signed, err := sp.SignAuthnRequest(authnRequest) + if err != nil { + return nil, err + } + + doc.SetRoot(signed) + } else { + doc.SetRoot(authnRequest) + } + return doc, nil +} + +func (sp *SAMLServiceProvider) BuildAuthRequestDocument() (*etree.Document, error) { + return sp.buildAuthnRequest(true) +} + +func (sp *SAMLServiceProvider) BuildAuthRequestDocumentNoSig() (*etree.Document, error) { + return sp.buildAuthnRequest(false) +} + +// SignAuthnRequest takes a document, builds a signature, creates another document +// and inserts the signature in it. According to the schema, the position of the +// signature is right after the Issuer [1] then all other children. +// +// [1] https://docs.oasis-open.org/security/saml/v2.0/saml-schema-protocol-2.0.xsd +func (sp *SAMLServiceProvider) SignAuthnRequest(el *etree.Element) (*etree.Element, error) { + ctx := sp.SigningContext() + + sig, err := ctx.ConstructSignature(el, true) + if err != nil { + return nil, err + } + + ret := el.Copy() + + var children []etree.Token + children = append(children, ret.Child[0]) // issuer is always first + children = append(children, sig) // next is the signature + children = append(children, ret.Child[1:]...) // then all other children + ret.Child = children + + return ret, nil +} + +// BuildAuthRequest builds for identity provider +func (sp *SAMLServiceProvider) BuildAuthRequest() (string, error) { + doc, err := sp.BuildAuthRequestDocument() + if err != nil { + return "", err + } + return doc.WriteToString() +} + +func (sp *SAMLServiceProvider) buildAuthURLFromDocument(relayState, binding string, doc *etree.Document) (string, error) { + parsedUrl, err := url.Parse(sp.IdentityProviderSSOURL) + if err != nil { + return "", err + } + + authnRequest, err := doc.WriteToString() + if err != nil { + return "", err + } + + buf := &bytes.Buffer{} + + fw, err := flate.NewWriter(buf, flate.DefaultCompression) + if err != nil { + return "", fmt.Errorf("flate NewWriter error: %v", err) + } + + _, err = fw.Write([]byte(authnRequest)) + if err != nil { + return "", fmt.Errorf("flate.Writer Write error: %v", err) + } + + err = fw.Close() + if err != nil { + return "", fmt.Errorf("flate.Writer Close error: %v", err) + } + + qs := parsedUrl.Query() + + qs.Add("SAMLRequest", base64.StdEncoding.EncodeToString(buf.Bytes())) + + if relayState != "" { + qs.Add("RelayState", relayState) + } + + if sp.SignAuthnRequests && binding == BindingHttpRedirect { + // Sign URL encoded query (see Section 3.4.4.1 DEFLATE Encoding of saml-bindings-2.0-os.pdf) + ctx := sp.SigningContext() + qs.Add("SigAlg", ctx.GetSignatureMethodIdentifier()) + var rawSignature []byte + if rawSignature, err = ctx.SignString(signatureInputString(qs.Get("SAMLRequest"), qs.Get("RelayState"), qs.Get("SigAlg"))); err != nil { + return "", fmt.Errorf("unable to sign query string of redirect URL: %v", err) + } + + // Now add base64 encoded Signature + qs.Add("Signature", base64.StdEncoding.EncodeToString(rawSignature)) + } + + //Here the parameters may appear in any order. + parsedUrl.RawQuery = qs.Encode() + return parsedUrl.String(), nil +} + +func (sp *SAMLServiceProvider) BuildAuthURLFromDocument(relayState string, doc *etree.Document) (string, error) { + return sp.buildAuthURLFromDocument(relayState, BindingHttpPost, doc) +} + +func (sp *SAMLServiceProvider) BuildAuthURLRedirect(relayState string, doc *etree.Document) (string, error) { + return sp.buildAuthURLFromDocument(relayState, BindingHttpRedirect, doc) +} + +func (sp *SAMLServiceProvider) buildAuthBodyPostFromDocument(relayState string, doc *etree.Document) ([]byte, error) { + reqBuf, err := doc.WriteToBytes() + if err != nil { + return nil, err + } + + encodedReqBuf := base64.StdEncoding.EncodeToString(reqBuf) + + var tmpl *template.Template + var rv bytes.Buffer + + if relayState != "" { + tmpl = template.Must(template.New("saml-post-form").Parse(`` + + `
` + + `` + + `` + + `` + + `
` + + ``)) + + data := struct { + URL string + SAMLRequest string + RelayState string + }{ + URL: sp.IdentityProviderSSOURL, + SAMLRequest: encodedReqBuf, + RelayState: relayState, + } + if err = tmpl.Execute(&rv, data); err != nil { + return nil, err + } + } else { + tmpl = template.Must(template.New("saml-post-form").Parse(`` + + `
` + + `` + + `` + + `
` + + ``)) + + data := struct { + URL string + SAMLRequest string + }{ + URL: sp.IdentityProviderSSOURL, + SAMLRequest: encodedReqBuf, + } + if err = tmpl.Execute(&rv, data); err != nil { + return nil, err + } + } + + return rv.Bytes(), nil +} + +//BuildAuthBodyPost builds the POST body to be sent to IDP. +func (sp *SAMLServiceProvider) BuildAuthBodyPost(relayState string) ([]byte, error) { + var doc *etree.Document + var err error + + if sp.SignAuthnRequests { + doc, err = sp.BuildAuthRequestDocument() + } else { + doc, err = sp.BuildAuthRequestDocumentNoSig() + } + + if err != nil { + return nil, err + } + + return sp.buildAuthBodyPostFromDocument(relayState, doc) +} + +//BuildAuthBodyPostFromDocument builds the POST body to be sent to IDP. +//It takes the AuthnRequest xml as input. +func (sp *SAMLServiceProvider) BuildAuthBodyPostFromDocument(relayState string, doc *etree.Document) ([]byte, error) { + return sp.buildAuthBodyPostFromDocument(relayState, doc) +} + +// BuildAuthURL builds redirect URL to be sent to principal +func (sp *SAMLServiceProvider) BuildAuthURL(relayState string) (string, error) { + doc, err := sp.BuildAuthRequestDocument() + if err != nil { + return "", err + } + return sp.BuildAuthURLFromDocument(relayState, doc) +} + +// AuthRedirect takes a ResponseWriter and Request from an http interaction and +// redirects to the SAMLServiceProvider's configured IdP, including the +// relayState provided, if any. +func (sp *SAMLServiceProvider) AuthRedirect(w http.ResponseWriter, r *http.Request, relayState string) (err error) { + url, err := sp.BuildAuthURL(relayState) + if err != nil { + return err + } + + http.Redirect(w, r, url, http.StatusFound) + return nil +} + +func (sp *SAMLServiceProvider) buildLogoutRequest(includeSig bool, nameID string, sessionIndex string) (*etree.Document, error) { + logoutRequest := &etree.Element{ + Space: "samlp", + Tag: "LogoutRequest", + } + + logoutRequest.CreateAttr("xmlns:samlp", "urn:oasis:names:tc:SAML:2.0:protocol") + logoutRequest.CreateAttr("xmlns:saml", "urn:oasis:names:tc:SAML:2.0:assertion") + + arId := uuid.NewV4() + + logoutRequest.CreateAttr("ID", "_"+arId.String()) + logoutRequest.CreateAttr("Version", "2.0") + logoutRequest.CreateAttr("IssueInstant", sp.Clock.Now().UTC().Format(issueInstantFormat)) + logoutRequest.CreateAttr("Destination", sp.IdentityProviderSLOURL) + + // NOTE(russell_h): In earlier versions we mistakenly sent the IdentityProviderIssuer + // in the AuthnRequest. For backwards compatibility we will fall back to that + // behavior when ServiceProviderIssuer isn't set. + // TODO: Throw error in case Issuer is empty. + if sp.ServiceProviderIssuer != "" { + logoutRequest.CreateElement("saml:Issuer").SetText(sp.ServiceProviderIssuer) + } else { + logoutRequest.CreateElement("saml:Issuer").SetText(sp.IdentityProviderIssuer) + } + + nameId := logoutRequest.CreateElement("saml:NameID") + nameId.SetText(nameID) + nameId.CreateAttr("Format", sp.NameIdFormat) + + //Section 3.7.1 - http://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf says + //SessionIndex is optional. If the IDP supports SLO then it must send SessionIndex as per + //Section 4.1.4.2 of https://docs.oasis-open.org/security/saml/v2.0/saml-profiles-2.0-os.pdf. + //As per section 4.4.3.1 of //docs.oasis-open.org/security/saml/v2.0/saml-profiles-2.0-os.pdf, + //a LogoutRequest issued by Session Participant to Identity Provider, must contain + //at least one SessionIndex element needs to be included. + nameId = logoutRequest.CreateElement("samlp:SessionIndex") + nameId.SetText(sessionIndex) + + doc := etree.NewDocument() + + if includeSig { + signed, err := sp.SignLogoutRequest(logoutRequest) + if err != nil { + return nil, err + } + + doc.SetRoot(signed) + } else { + doc.SetRoot(logoutRequest) + } + + return doc, nil +} + +func (sp *SAMLServiceProvider) SignLogoutRequest(el *etree.Element) (*etree.Element, error) { + ctx := sp.SigningContext() + + sig, err := ctx.ConstructSignature(el, true) + if err != nil { + return nil, err + } + + ret := el.Copy() + + var children []etree.Token + children = append(children, ret.Child[0]) // issuer is always first + children = append(children, sig) // next is the signature + children = append(children, ret.Child[1:]...) // then all other children + ret.Child = children + + return ret, nil +} + +func (sp *SAMLServiceProvider) BuildLogoutRequestDocumentNoSig(nameID string, sessionIndex string) (*etree.Document, error) { + return sp.buildLogoutRequest(false, nameID, sessionIndex) +} + +func (sp *SAMLServiceProvider) BuildLogoutRequestDocument(nameID string, sessionIndex string) (*etree.Document, error) { + return sp.buildLogoutRequest(true, nameID, sessionIndex) +} + +//BuildLogoutBodyPostFromDocument builds the POST body to be sent to IDP. +//It takes the LogoutRequest xml as input. +func (sp *SAMLServiceProvider) BuildLogoutBodyPostFromDocument(relayState string, doc *etree.Document) ([]byte, error) { + return sp.buildLogoutBodyPostFromDocument(relayState, doc) +} + +func (sp *SAMLServiceProvider) buildLogoutBodyPostFromDocument(relayState string, doc *etree.Document) ([]byte, error) { + reqBuf, err := doc.WriteToBytes() + if err != nil { + return nil, err + } + + encodedReqBuf := base64.StdEncoding.EncodeToString(reqBuf) + var tmpl *template.Template + var rv bytes.Buffer + + if relayState != "" { + tmpl = template.Must(template.New("saml-post-form").Parse(`` + + `
` + + `` + + `` + + `` + + `
` + + ``)) + + data := struct { + URL string + SAMLRequest string + RelayState string + }{ + URL: sp.IdentityProviderSLOURL, + SAMLRequest: encodedReqBuf, + RelayState: relayState, + } + if err = tmpl.Execute(&rv, data); err != nil { + return nil, err + } + } else { + tmpl = template.Must(template.New("saml-post-form").Parse(`` + + `
` + + `` + + `` + + `
` + + ``)) + + data := struct { + URL string + SAMLRequest string + }{ + URL: sp.IdentityProviderSLOURL, + SAMLRequest: encodedReqBuf, + } + if err = tmpl.Execute(&rv, data); err != nil { + return nil, err + } + } + + return rv.Bytes(), nil +} + +func (sp *SAMLServiceProvider) BuildLogoutURLRedirect(relayState string, doc *etree.Document) (string, error) { + return sp.buildLogoutURLFromDocument(relayState, BindingHttpRedirect, doc) +} + +func (sp *SAMLServiceProvider) buildLogoutURLFromDocument(relayState, binding string, doc *etree.Document) (string, error) { + parsedUrl, err := url.Parse(sp.IdentityProviderSLOURL) + if err != nil { + return "", err + } + + logoutRequest, err := doc.WriteToString() + if err != nil { + return "", err + } + + buf := &bytes.Buffer{} + + fw, err := flate.NewWriter(buf, flate.DefaultCompression) + if err != nil { + return "", fmt.Errorf("flate NewWriter error: %v", err) + } + + _, err = fw.Write([]byte(logoutRequest)) + if err != nil { + return "", fmt.Errorf("flate.Writer Write error: %v", err) + } + + err = fw.Close() + if err != nil { + return "", fmt.Errorf("flate.Writer Close error: %v", err) + } + + qs := parsedUrl.Query() + + qs.Add("SAMLRequest", base64.StdEncoding.EncodeToString(buf.Bytes())) + + if relayState != "" { + qs.Add("RelayState", relayState) + } + + if binding == BindingHttpRedirect { + // Sign URL encoded query (see Section 3.4.4.1 DEFLATE Encoding of saml-bindings-2.0-os.pdf) + ctx := sp.SigningContext() + qs.Add("SigAlg", ctx.GetSignatureMethodIdentifier()) + var rawSignature []byte + //qs.Encode() sorts the keys (See https://golang.org/pkg/net/url/#Values.Encode). + //If RelayState parameter is present then RelayState parameter + //will be put first by Encode(). Hence encode them separately and concatenate. + //Signature string has to have parameters in the order - SAMLRequest=value&RelayState=value&SigAlg=value. + //(See Section 3.4.4.1 saml-bindings-2.0-os.pdf). + var orderedParams = []string{"SAMLRequest", "RelayState", "SigAlg"} + + var paramValueMap = make(map[string]string) + paramValueMap["SAMLRequest"] = base64.StdEncoding.EncodeToString(buf.Bytes()) + if relayState != "" { + paramValueMap["RelayState"] = relayState + } + paramValueMap["SigAlg"] = ctx.GetSignatureMethodIdentifier() + + ss := "" + + for _, k := range orderedParams { + v, ok := paramValueMap[k] + if ok { + //Add the value after URL encoding. + u := url.Values{} + u.Add(k, v) + e := u.Encode() + if ss != "" { + ss += "&" + e + } else { + ss = e + } + } + } + + //Now generate the signature on the string of ordered parameters. + if rawSignature, err = ctx.SignString(ss); err != nil { + return "", fmt.Errorf("unable to sign query string of redirect URL: %v", err) + } + + // Now add base64 encoded Signature + qs.Add("Signature", base64.StdEncoding.EncodeToString(rawSignature)) + } + + //Here the parameters may appear in any order. + parsedUrl.RawQuery = qs.Encode() + return parsedUrl.String(), nil +} + +// signatureInputString constructs the string to be fed into the signature algorithm, as described +// in section 3.4.4.1 of +// https://www.oasis-open.org/committees/download.php/56779/sstc-saml-bindings-errata-2.0-wd-06.pdf +func signatureInputString(samlRequest, relayState, sigAlg string) string { + var params [][2]string + if relayState == "" { + params = [][2]string{{"SAMLRequest", samlRequest}, {"SigAlg", sigAlg}} + } else { + params = [][2]string{{"SAMLRequest", samlRequest}, {"RelayState", relayState}, {"SigAlg", sigAlg}} + } + + var buf bytes.Buffer + for _, kv := range params { + k, v := kv[0], kv[1] + if buf.Len() > 0 { + buf.WriteByte('&') + } + buf.WriteString(url.QueryEscape(k) + "=" + url.QueryEscape(v)) + } + return buf.String() +} diff --git a/vendor/github.com/russellhaering/gosaml2/decode_logout_request.go b/vendor/github.com/russellhaering/gosaml2/decode_logout_request.go new file mode 100644 index 000000000..57c7eb8fa --- /dev/null +++ b/vendor/github.com/russellhaering/gosaml2/decode_logout_request.go @@ -0,0 +1,85 @@ +// Copyright 2016 Russell Haering et al. +// +// 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 saml2 + +import ( + "encoding/base64" + "fmt" + + dsig "github.com/russellhaering/goxmldsig" +) + +func (sp *SAMLServiceProvider) validateLogoutRequestAttributes(request *LogoutRequest) error { + if request.Destination != "" && request.Destination != sp.ServiceProviderSLOURL { + return ErrInvalidValue{ + Key: DestinationAttr, + Expected: sp.ServiceProviderSLOURL, + Actual: request.Destination, + } + } + + if request.Version != "2.0" { + return ErrInvalidValue{ + Reason: ReasonUnsupported, + Key: "SAML version", + Expected: "2.0", + Actual: request.Version, + } + } + + return nil +} + +func (sp *SAMLServiceProvider) ValidateEncodedLogoutRequestPOST(encodedRequest string) (*LogoutRequest, error) { + raw, err := base64.StdEncoding.DecodeString(encodedRequest) + if err != nil { + return nil, err + } + + // Parse the raw request - parseResponse is generic + doc, el, err := parseResponse(raw, sp.MaximumDecompressedBodySize) + if err != nil { + return nil, err + } + + var requestSignatureValidated bool + if !sp.SkipSignatureValidation { + el, err = sp.validateElementSignature(el) + if err == dsig.ErrMissingSignature { + // Unfortunately we just blew away our Response + el = doc.Root() + } else if err != nil { + return nil, err + } else if el == nil { + return nil, fmt.Errorf("missing transformed logout request") + } else { + requestSignatureValidated = true + } + } + + decodedRequest := &LogoutRequest{} + err = xmlUnmarshalElement(el, decodedRequest) + if err != nil { + return nil, fmt.Errorf("unable to unmarshal logout request: %v", err) + } + decodedRequest.SignatureValidated = requestSignatureValidated + + err = sp.ValidateDecodedLogoutRequest(decodedRequest) + if err != nil { + return nil, err + } + + return decodedRequest, nil +} diff --git a/vendor/github.com/russellhaering/gosaml2/decode_response.go b/vendor/github.com/russellhaering/gosaml2/decode_response.go new file mode 100644 index 000000000..6830d72ee --- /dev/null +++ b/vendor/github.com/russellhaering/gosaml2/decode_response.go @@ -0,0 +1,465 @@ +// Copyright 2016 Russell Haering et al. +// +// 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 saml2 + +import ( + "bytes" + "compress/flate" + "crypto/tls" + "crypto/x509" + "encoding/base64" + "fmt" + "io" + "io/ioutil" + + "encoding/xml" + + "github.com/beevik/etree" + rtvalidator "github.com/mattermost/xml-roundtrip-validator" + "github.com/russellhaering/gosaml2/types" + dsig "github.com/russellhaering/goxmldsig" + "github.com/russellhaering/goxmldsig/etreeutils" +) + +const ( + defaultMaxDecompressedResponseSize = 5 * 1024 * 1024 +) + +func (sp *SAMLServiceProvider) validationContext() *dsig.ValidationContext { + ctx := dsig.NewDefaultValidationContext(sp.IDPCertificateStore) + ctx.Clock = sp.Clock + return ctx +} + +// validateResponseAttributes validates a SAML Response's tag and attributes. It does +// not inspect child elements of the Response at all. +func (sp *SAMLServiceProvider) validateResponseAttributes(response *types.Response) error { + if response.Destination != "" && response.Destination != sp.AssertionConsumerServiceURL { + return ErrInvalidValue{ + Key: DestinationAttr, + Expected: sp.AssertionConsumerServiceURL, + Actual: response.Destination, + } + } + + if response.Version != "2.0" { + return ErrInvalidValue{ + Reason: ReasonUnsupported, + Key: "SAML version", + Expected: "2.0", + Actual: response.Version, + } + } + + return nil +} + +// validateLogoutResponseAttributes validates a SAML Response's tag and attributes. It does +// not inspect child elements of the Response at all. +func (sp *SAMLServiceProvider) validateLogoutResponseAttributes(response *types.LogoutResponse) error { + if response.Destination != "" && response.Destination != sp.ServiceProviderSLOURL { + return ErrInvalidValue{ + Key: DestinationAttr, + Expected: sp.ServiceProviderSLOURL, + Actual: response.Destination, + } + } + + if response.Version != "2.0" { + return ErrInvalidValue{ + Reason: ReasonUnsupported, + Key: "SAML version", + Expected: "2.0", + Actual: response.Version, + } + } + + return nil +} + +func xmlUnmarshalElement(el *etree.Element, obj interface{}) error { + doc := etree.NewDocument() + doc.SetRoot(el) + data, err := doc.WriteToBytes() + if err != nil { + return err + } + + err = xml.Unmarshal(data, obj) + if err != nil { + return err + } + return nil +} + +func (sp *SAMLServiceProvider) getDecryptCert() (*tls.Certificate, error) { + if sp.SPKeyStore == nil { + return nil, fmt.Errorf("no decryption certs available") + } + + //This is the tls.Certificate we'll use to decrypt any encrypted assertions + var decryptCert tls.Certificate + + switch crt := sp.SPKeyStore.(type) { + case dsig.TLSCertKeyStore: + // Get the tls.Certificate directly if possible + decryptCert = tls.Certificate(crt) + + default: + + //Otherwise, construct one from the results of GetKeyPair + pk, cert, err := sp.SPKeyStore.GetKeyPair() + if err != nil { + return nil, fmt.Errorf("error getting keypair: %v", err) + } + + decryptCert = tls.Certificate{ + Certificate: [][]byte{cert}, + PrivateKey: pk, + } + } + + if sp.ValidateEncryptionCert { + // Check Validity period of certificate + if len(decryptCert.Certificate) < 1 || len(decryptCert.Certificate[0]) < 1 { + return nil, fmt.Errorf("empty decryption cert") + } else if cert, err := x509.ParseCertificate(decryptCert.Certificate[0]); err != nil { + return nil, fmt.Errorf("invalid x509 decryption cert: %v", err) + } else { + now := sp.Clock.Now() + if now.Before(cert.NotBefore) || now.After(cert.NotAfter) { + return nil, fmt.Errorf("decryption cert is not valid at this time") + } + } + } + + return &decryptCert, nil +} + +func (sp *SAMLServiceProvider) decryptAssertions(el *etree.Element) error { + var decryptCert *tls.Certificate + + decryptAssertion := func(ctx etreeutils.NSContext, encryptedElement *etree.Element) error { + if encryptedElement.Parent() != el { + return fmt.Errorf("found encrypted assertion with unexpected parent element: %s", encryptedElement.Parent().Tag) + } + + detached, err := etreeutils.NSDetatch(ctx, encryptedElement) // make a detached copy + if err != nil { + return fmt.Errorf("unable to detach encrypted assertion: %v", err) + } + + encryptedAssertion := &types.EncryptedAssertion{} + err = xmlUnmarshalElement(detached, encryptedAssertion) + if err != nil { + return fmt.Errorf("unable to unmarshal encrypted assertion: %v", err) + } + + if decryptCert == nil { + decryptCert, err = sp.getDecryptCert() + if err != nil { + return fmt.Errorf("unable to get decryption certificate: %v", err) + } + } + + raw, derr := encryptedAssertion.DecryptBytes(decryptCert) + if derr != nil { + return fmt.Errorf("unable to decrypt encrypted assertion: %v", derr) + } + + doc, _, err := parseResponse(raw, sp.MaximumDecompressedBodySize) + if err != nil { + return fmt.Errorf("unable to create element from decrypted assertion bytes: %v", err) + } + + // Replace the original encrypted assertion with the decrypted one. + if el.RemoveChild(encryptedElement) == nil { + // Out of an abundance of caution, make sure removed worked + panic("unable to remove encrypted assertion") + } + + el.AddChild(doc.Root()) + return nil + } + + if err := etreeutils.NSFindIterate(el, SAMLAssertionNamespace, EncryptedAssertionTag, decryptAssertion); err != nil { + return err + } else { + return nil + } +} + +func (sp *SAMLServiceProvider) validateElementSignature(el *etree.Element) (*etree.Element, error) { + return sp.validationContext().Validate(el) +} + +func (sp *SAMLServiceProvider) validateAssertionSignatures(el *etree.Element) error { + signedAssertions := 0 + unsignedAssertions := 0 + validateAssertion := func(ctx etreeutils.NSContext, unverifiedAssertion *etree.Element) error { + parent := unverifiedAssertion.Parent() + if parent == nil { + return fmt.Errorf("parent is nil") + } + if parent != el { + return fmt.Errorf("found assertion with unexpected parent element: %s", unverifiedAssertion.Parent().Tag) + } + + detached, err := etreeutils.NSDetatch(ctx, unverifiedAssertion) // make a detached copy + if err != nil { + return fmt.Errorf("unable to detach unverified assertion: %v", err) + } + + assertion, err := sp.validationContext().Validate(detached) + if err == dsig.ErrMissingSignature { + unsignedAssertions++ + return nil + } else if err != nil { + return err + } + + // Replace the original unverified Assertion with the verified one. Note that + // if the Response is not signed, only signed Assertions (and not the parent Response) can be trusted. + if el.RemoveChild(unverifiedAssertion) == nil { + // Out of an abundance of caution, check to make sure an Assertion was actually + // removed. If it wasn't a programming error has occurred. + panic("unable to remove assertion") + } + + el.AddChild(assertion) + signedAssertions++ + + return nil + } + + if err := etreeutils.NSFindIterate(el, SAMLAssertionNamespace, AssertionTag, validateAssertion); err != nil { + return err + } else if signedAssertions > 0 && unsignedAssertions > 0 { + return fmt.Errorf("invalid to have both signed and unsigned assertions") + } else if signedAssertions < 1 { + return dsig.ErrMissingSignature + } else { + return nil + } +} + +// ValidateEncodedResponse both decodes and validates, based on SP +// configuration, an encoded, signed response. It will also appropriately +// decrypt a response if the assertion was encrypted +func (sp *SAMLServiceProvider) ValidateEncodedResponse(encodedResponse string) (*types.Response, error) { + raw, err := base64.StdEncoding.DecodeString(encodedResponse) + if err != nil { + return nil, err + } + + // Parse the raw response + doc, el, err := parseResponse(raw, sp.MaximumDecompressedBodySize) + if err != nil { + return nil, err + } + + var responseSignatureValidated bool + if !sp.SkipSignatureValidation { + el, err = sp.validateElementSignature(el) + if err == dsig.ErrMissingSignature { + // Unfortunately we just blew away our Response + el = doc.Root() + } else if err != nil { + return nil, err + } else if el == nil { + return nil, fmt.Errorf("missing transformed response") + } else { + responseSignatureValidated = true + } + } + + err = sp.decryptAssertions(el) + if err != nil { + return nil, err + } + + var assertionSignaturesValidated bool + if !sp.SkipSignatureValidation { + err = sp.validateAssertionSignatures(el) + if err == dsig.ErrMissingSignature { + if !responseSignatureValidated { + return nil, fmt.Errorf("response and/or assertions must be signed") + } + } else if err != nil { + return nil, err + } else { + assertionSignaturesValidated = true + } + } + + decodedResponse := &types.Response{} + err = xmlUnmarshalElement(el, decodedResponse) + if err != nil { + return nil, fmt.Errorf("unable to unmarshal response: %v", err) + } + decodedResponse.SignatureValidated = responseSignatureValidated + if assertionSignaturesValidated { + for idx := 0; idx < len(decodedResponse.Assertions); idx++ { + decodedResponse.Assertions[idx].SignatureValidated = true + } + } + + err = sp.Validate(decodedResponse) + if err != nil { + return nil, err + } + + return decodedResponse, nil +} + +// DecodeUnverifiedBaseResponse decodes several attributes from a SAML response for the purpose +// of determining how to validate the response. This is useful for Service Providers which +// expose a single Assertion Consumer Service URL but consume Responses from many IdPs. +func DecodeUnverifiedBaseResponse(encodedResponse string) (*types.UnverifiedBaseResponse, error) { + raw, err := base64.StdEncoding.DecodeString(encodedResponse) + if err != nil { + return nil, err + } + + var response *types.UnverifiedBaseResponse + + err = maybeDeflate(raw, defaultMaxDecompressedResponseSize, func(maybeXML []byte) error { + response = &types.UnverifiedBaseResponse{} + return xml.Unmarshal(maybeXML, response) + }) + if err != nil { + return nil, err + } + + return response, nil +} + +// maybeDeflate invokes the passed decoder over the passed data. If an error is +// returned, it then attempts to deflate the passed data before re-invoking +// the decoder over the deflated data. +func maybeDeflate(data []byte, maxSize int64, decoder func([]byte) error) error { + err := decoder(data) + if err == nil { + return nil + } + + // Default to 5MB max size + if maxSize == 0 { + maxSize = defaultMaxDecompressedResponseSize + } + + lr := io.LimitReader(flate.NewReader(bytes.NewReader(data)), maxSize+1) + + deflated, err := ioutil.ReadAll(lr) + if err != nil { + return err + } + + if int64(len(deflated)) > maxSize { + return fmt.Errorf("deflated response exceeds maximum size of %d bytes", maxSize) + } + + return decoder(deflated) +} + +// parseResponse is a helper function that was refactored out so that the XML parsing behavior can be isolated and unit tested +func parseResponse(xml []byte, maxSize int64) (*etree.Document, *etree.Element, error) { + var doc *etree.Document + var rawXML []byte + + err := maybeDeflate(xml, maxSize, func(xml []byte) error { + doc = etree.NewDocument() + rawXML = xml + return doc.ReadFromBytes(xml) + }) + if err != nil { + return nil, nil, err + } + + el := doc.Root() + if el == nil { + return nil, nil, fmt.Errorf("unable to parse response") + } + + // Examine the response for attempts to exploit weaknesses in Go's encoding/xml + err = rtvalidator.Validate(bytes.NewReader(rawXML)) + if err != nil { + return nil, nil, err + } + + return doc, el, nil +} + +// DecodeUnverifiedLogoutResponse decodes several attributes from a SAML Logout response, without doing any verifications. +func DecodeUnverifiedLogoutResponse(encodedResponse string) (*types.LogoutResponse, error) { + raw, err := base64.StdEncoding.DecodeString(encodedResponse) + if err != nil { + return nil, err + } + + var response *types.LogoutResponse + + err = maybeDeflate(raw, defaultMaxDecompressedResponseSize, func(maybeXML []byte) error { + response = &types.LogoutResponse{} + return xml.Unmarshal(maybeXML, response) + }) + if err != nil { + return nil, err + } + + return response, nil +} + +func (sp *SAMLServiceProvider) ValidateEncodedLogoutResponsePOST(encodedResponse string) (*types.LogoutResponse, error) { + raw, err := base64.StdEncoding.DecodeString(encodedResponse) + if err != nil { + return nil, err + } + + // Parse the raw response + doc, el, err := parseResponse(raw, sp.MaximumDecompressedBodySize) + if err != nil { + return nil, err + } + + var responseSignatureValidated bool + if !sp.SkipSignatureValidation { + el, err = sp.validateElementSignature(el) + if err == dsig.ErrMissingSignature { + // Unfortunately we just blew away our Response + el = doc.Root() + } else if err != nil { + return nil, err + } else if el == nil { + return nil, fmt.Errorf("missing transformed logout response") + } else { + responseSignatureValidated = true + } + } + + decodedResponse := &types.LogoutResponse{} + err = xmlUnmarshalElement(el, decodedResponse) + if err != nil { + return nil, fmt.Errorf("unable to unmarshal logout response: %v", err) + } + decodedResponse.SignatureValidated = responseSignatureValidated + + err = sp.ValidateDecodedLogoutResponse(decodedResponse) + if err != nil { + return nil, err + } + + return decodedResponse, nil +} diff --git a/vendor/github.com/russellhaering/gosaml2/go.mod b/vendor/github.com/russellhaering/gosaml2/go.mod new file mode 100644 index 000000000..1f96510a4 --- /dev/null +++ b/vendor/github.com/russellhaering/gosaml2/go.mod @@ -0,0 +1,11 @@ +module github.com/russellhaering/gosaml2 + +go 1.13 + +require ( + github.com/beevik/etree v1.2.0 + github.com/jonboulle/clockwork v0.3.0 + github.com/mattermost/xml-roundtrip-validator v0.1.0 + github.com/russellhaering/goxmldsig v1.4.0 + github.com/stretchr/testify v1.7.1 +) diff --git a/vendor/github.com/russellhaering/gosaml2/go.sum b/vendor/github.com/russellhaering/gosaml2/go.sum new file mode 100644 index 000000000..2fcb7de2f --- /dev/null +++ b/vendor/github.com/russellhaering/gosaml2/go.sum @@ -0,0 +1,39 @@ +github.com/beevik/etree v1.1.0/go.mod h1:r8Aw8JqVegEf0w2fDnATrX9VpkMcyFeM0FhwO62wh+A= +github.com/beevik/etree v1.2.0 h1:l7WETslUG/T+xOPs47dtd6jov2Ii/8/OjCldk5fYfQw= +github.com/beevik/etree v1.2.0/go.mod h1:aiPf89g/1k3AShMVAzriilpcE4R/Vuor90y83zVZWFc= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8= +github.com/jonboulle/clockwork v0.3.0 h1:9BSCMi8C+0qdApAp4auwX0RkLGUjs956h0EkuQymUhg= +github.com/jonboulle/clockwork v0.3.0/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/mattermost/xml-roundtrip-validator v0.1.0 h1:RXbVD2UAl7A7nOTR4u7E3ILa4IbtvKBHw64LDsmu9hU= +github.com/mattermost/xml-roundtrip-validator v0.1.0/go.mod h1:qccnGMcpgwcNaBnxqpJpWWUiPNr5H3O8eDgGV9gT5To= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= +github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= +github.com/russellhaering/goxmldsig v1.4.0 h1:8UcDh/xGyQiyrW+Fq5t8f+l2DLB1+zlhYzkPUJ7Qhys= +github.com/russellhaering/goxmldsig v1.4.0/go.mod h1:gM4MDENBQf7M+V824SGfyIUVFWydB7n0KkEubVJl+Tw= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/vendor/github.com/russellhaering/gosaml2/logout_request.go b/vendor/github.com/russellhaering/gosaml2/logout_request.go new file mode 100644 index 000000000..886e73bf1 --- /dev/null +++ b/vendor/github.com/russellhaering/gosaml2/logout_request.go @@ -0,0 +1,37 @@ +// Copyright 2016 Russell Haering et al. +// +// 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 saml2 + +import ( + "encoding/xml" + "github.com/russellhaering/gosaml2/types" + "time" +) + +// LogoutRequest is the go struct representation of a logout request +type LogoutRequest struct { + XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:protocol LogoutRequest"` + ID string `xml:"ID,attr"` + Version string `xml:"Version,attr"` + //ProtocolBinding string `xml:",attr"` + + IssueInstant time.Time `xml:"IssueInstant,attr"` + + Destination string `xml:"Destination,attr"` + Issuer *types.Issuer `xml:"Issuer"` + + NameID *types.NameID `xml:"NameID"` + SignatureValidated bool `xml:"-"` // not read, not dumped +} diff --git a/vendor/github.com/russellhaering/gosaml2/retrieve_assertion.go b/vendor/github.com/russellhaering/gosaml2/retrieve_assertion.go new file mode 100644 index 000000000..af70aa6df --- /dev/null +++ b/vendor/github.com/russellhaering/gosaml2/retrieve_assertion.go @@ -0,0 +1,111 @@ +// Copyright 2016 Russell Haering et al. +// +// 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 saml2 + +import "fmt" + +//ErrMissingElement is the error type that indicates an element and/or attribute is +//missing. It provides a structured error that can be more appropriately acted +//upon. +type ErrMissingElement struct { + Tag, Attribute string +} + +type ErrVerification struct { + Cause error +} + +func (e ErrVerification) Error() string { + return fmt.Sprintf("error validating response: %s", e.Cause.Error()) +} + +//ErrMissingAssertion indicates that an appropriate assertion element could not +//be found in the SAML Response +var ( + ErrMissingAssertion = ErrMissingElement{Tag: AssertionTag} +) + +func (e ErrMissingElement) Error() string { + if e.Attribute != "" { + return fmt.Sprintf("missing %s attribute on %s element", e.Attribute, e.Tag) + } + return fmt.Sprintf("missing %s element", e.Tag) +} + +//RetrieveAssertionInfo takes an encoded response and returns the AssertionInfo +//contained, or an error message if an error has been encountered. +func (sp *SAMLServiceProvider) RetrieveAssertionInfo(encodedResponse string) (*AssertionInfo, error) { + assertionInfo := &AssertionInfo{ + Values: make(Values), + } + + response, err := sp.ValidateEncodedResponse(encodedResponse) + if err != nil { + return nil, ErrVerification{Cause: err} + } + + // TODO: Support multiple assertions + if len(response.Assertions) == 0 { + return nil, ErrMissingAssertion + } + + assertion := response.Assertions[0] + assertionInfo.Assertions = response.Assertions + assertionInfo.ResponseSignatureValidated = response.SignatureValidated + + warningInfo, err := sp.VerifyAssertionConditions(&assertion) + if err != nil { + return nil, err + } + + //Get the NameID + subject := assertion.Subject + if subject == nil { + return nil, ErrMissingElement{Tag: SubjectTag} + } + + nameID := subject.NameID + if nameID == nil { + return nil, ErrMissingElement{Tag: NameIdTag} + } + + assertionInfo.NameID = nameID.Value + + //Get the actual assertion attributes + attributeStatement := assertion.AttributeStatement + if attributeStatement == nil && !sp.AllowMissingAttributes { + return nil, ErrMissingElement{Tag: AttributeStatementTag} + } + + if attributeStatement != nil { + for _, attribute := range attributeStatement.Attributes { + assertionInfo.Values[attribute.Name] = attribute + } + } + + if assertion.AuthnStatement != nil { + if assertion.AuthnStatement.AuthnInstant != nil { + assertionInfo.AuthnInstant = assertion.AuthnStatement.AuthnInstant + } + if assertion.AuthnStatement.SessionNotOnOrAfter != nil { + assertionInfo.SessionNotOnOrAfter = assertion.AuthnStatement.SessionNotOnOrAfter + } + + assertionInfo.SessionIndex = assertion.AuthnStatement.SessionIndex + } + + assertionInfo.WarningInfo = warningInfo + return assertionInfo, nil +} diff --git a/vendor/github.com/russellhaering/gosaml2/run_test.sh b/vendor/github.com/russellhaering/gosaml2/run_test.sh new file mode 100644 index 000000000..cfe5b2ea9 --- /dev/null +++ b/vendor/github.com/russellhaering/gosaml2/run_test.sh @@ -0,0 +1,12 @@ +#!/bin/bash +cd `dirname $0` +DIRS=`git grep -l 'func Test' | xargs dirname | sort -u` +for DIR in $DIRS +do + echo + echo "dir: $DIR" + echo "======================================" + pushd $DIR >/dev/null + go test -v || exit 1 + popd >/dev/null +done diff --git a/vendor/github.com/russellhaering/gosaml2/saml.go b/vendor/github.com/russellhaering/gosaml2/saml.go new file mode 100644 index 000000000..836c2d551 --- /dev/null +++ b/vendor/github.com/russellhaering/gosaml2/saml.go @@ -0,0 +1,298 @@ +// Copyright 2016 Russell Haering et al. +// +// 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 saml2 + +import ( + "encoding/base64" + "sync" + "time" + + "github.com/russellhaering/gosaml2/types" + dsig "github.com/russellhaering/goxmldsig" + dsigtypes "github.com/russellhaering/goxmldsig/types" +) + +type ErrSaml struct { + Message string + System error +} + +func (serr ErrSaml) Error() string { + if serr.Message != "" { + return serr.Message + } + return "SAML error" +} + +type SAMLServiceProvider struct { + IdentityProviderSSOURL string + IdentityProviderSSOBinding string + IdentityProviderSLOURL string + IdentityProviderSLOBinding string + IdentityProviderIssuer string + + AssertionConsumerServiceURL string + ServiceProviderSLOURL string + ServiceProviderIssuer string + + SignAuthnRequests bool + SignAuthnRequestsAlgorithm string + SignAuthnRequestsCanonicalizer dsig.Canonicalizer + + // ForceAuthn attribute in authentication request forces the identity provider to + // re-authenticate the presenter directly rather than rely on a previous security context. + // NOTE: If both ForceAuthn and IsPassive are "true", the identity provider MUST NOT freshly + // authenticate the presenter unless the constraints of IsPassive can be met. + ForceAuthn bool + // IsPassive attribute in authentication request requires that the identity provider and the + // user agent itself MUST NOT visibly take control of the user interface from the requester + // and interact with the presenter in a noticeable fashion. + IsPassive bool + // RequestedAuthnContext allows service providers to require that the identity + // provider use specific authentication mechanisms. Leaving this unset will + // permit the identity provider to choose the auth method. To maximize compatibility + // with identity providers it is recommended to leave this unset. + RequestedAuthnContext *RequestedAuthnContext + AudienceURI string + IDPCertificateStore dsig.X509CertificateStore + SPKeyStore dsig.X509KeyStore // Required encryption key, default signing key + SPSigningKeyStore dsig.X509KeyStore // Optional signing key + NameIdFormat string + ValidateEncryptionCert bool + SkipSignatureValidation bool + AllowMissingAttributes bool + Clock *dsig.Clock + + // MaximumDecompressedBodySize is the maximum size to which a compressed + // SAML document will be decompressed. If a compresed document is exceeds + // this size during decompression an error will be returned. + MaximumDecompressedBodySize int64 + + signingContextMu sync.RWMutex + signingContext *dsig.SigningContext +} + +// RequestedAuthnContext controls which authentication mechanisms are requested of +// the identity provider. It is generally sufficient to omit this and let the +// identity provider select an authentication mechansim. +type RequestedAuthnContext struct { + // The RequestedAuthnContext comparison policy to use. See the section 3.3.2.2.1 + // of the SAML 2.0 specification for details. Constants named AuthnPolicyMatch* + // contain standardized values. + Comparison string + + // Contexts will be passed as AuthnContextClassRefs. For example, to force password + // authentication on some identity providers, Contexts should have a value of + // []string{AuthnContextPasswordProtectedTransport}, and Comparison should have a + // value of AuthnPolicyMatchExact. + Contexts []string +} + +func (sp *SAMLServiceProvider) Metadata() (*types.EntityDescriptor, error) { + keyDescriptors := make([]types.KeyDescriptor, 0, 2) + if sp.GetSigningKey() != nil { + signingCertBytes, err := sp.GetSigningCertBytes() + if err != nil { + return nil, err + } + keyDescriptors = append(keyDescriptors, types.KeyDescriptor{ + Use: "signing", + KeyInfo: dsigtypes.KeyInfo{ + X509Data: dsigtypes.X509Data{ + X509Certificates: []dsigtypes.X509Certificate{dsigtypes.X509Certificate{ + Data: base64.StdEncoding.EncodeToString(signingCertBytes), + }}, + }, + }, + }) + } + if sp.GetEncryptionKey() != nil { + encryptionCertBytes, err := sp.GetEncryptionCertBytes() + if err != nil { + return nil, err + } + keyDescriptors = append(keyDescriptors, types.KeyDescriptor{ + Use: "encryption", + KeyInfo: dsigtypes.KeyInfo{ + X509Data: dsigtypes.X509Data{ + X509Certificates: []dsigtypes.X509Certificate{{ + Data: base64.StdEncoding.EncodeToString(encryptionCertBytes), + }}, + }, + }, + EncryptionMethods: []types.EncryptionMethod{ + {Algorithm: types.MethodAES128GCM}, + {Algorithm: types.MethodAES192GCM}, + {Algorithm: types.MethodAES256GCM}, + {Algorithm: types.MethodAES128CBC}, + {Algorithm: types.MethodAES256CBC}, + }, + }) + } + return &types.EntityDescriptor{ + ValidUntil: sp.Clock.Now().UTC().Add(time.Hour * 24 * 7), // 7 days + EntityID: sp.ServiceProviderIssuer, + SPSSODescriptor: &types.SPSSODescriptor{ + AuthnRequestsSigned: sp.SignAuthnRequests, + WantAssertionsSigned: !sp.SkipSignatureValidation, + ProtocolSupportEnumeration: SAMLProtocolNamespace, + KeyDescriptors: keyDescriptors, + AssertionConsumerServices: []types.IndexedEndpoint{{ + Binding: BindingHttpPost, + Location: sp.AssertionConsumerServiceURL, + Index: 1, + }}, + }, + }, nil +} + +func (sp *SAMLServiceProvider) MetadataWithSLO(validityHours int64) (*types.EntityDescriptor, error) { + signingCertBytes, err := sp.GetSigningCertBytes() + if err != nil { + return nil, err + } + encryptionCertBytes, err := sp.GetEncryptionCertBytes() + if err != nil { + return nil, err + } + + if validityHours <= 0 { + // By default let's keep it to 7 days. + validityHours = int64(time.Hour * 24 * 7) + } + + return &types.EntityDescriptor{ + ValidUntil: sp.Clock.Now().UTC().Add(time.Duration(validityHours)), // default 7 days + EntityID: sp.ServiceProviderIssuer, + SPSSODescriptor: &types.SPSSODescriptor{ + AuthnRequestsSigned: sp.SignAuthnRequests, + WantAssertionsSigned: !sp.SkipSignatureValidation, + ProtocolSupportEnumeration: SAMLProtocolNamespace, + KeyDescriptors: []types.KeyDescriptor{ + { + Use: "signing", + KeyInfo: dsigtypes.KeyInfo{ + X509Data: dsigtypes.X509Data{ + X509Certificates: []dsigtypes.X509Certificate{{ + Data: base64.StdEncoding.EncodeToString(signingCertBytes), + }}, + }, + }, + }, + { + Use: "encryption", + KeyInfo: dsigtypes.KeyInfo{ + X509Data: dsigtypes.X509Data{ + X509Certificates: []dsigtypes.X509Certificate{{ + Data: base64.StdEncoding.EncodeToString(encryptionCertBytes), + }}, + }, + }, + EncryptionMethods: []types.EncryptionMethod{ + {Algorithm: types.MethodAES128GCM, DigestMethod: nil}, + {Algorithm: types.MethodAES192GCM, DigestMethod: nil}, + {Algorithm: types.MethodAES256GCM, DigestMethod: nil}, + {Algorithm: types.MethodAES128CBC, DigestMethod: nil}, + {Algorithm: types.MethodAES256CBC, DigestMethod: nil}, + }, + }, + }, + AssertionConsumerServices: []types.IndexedEndpoint{{ + Binding: BindingHttpPost, + Location: sp.AssertionConsumerServiceURL, + Index: 1, + }}, + SingleLogoutServices: []types.Endpoint{{ + Binding: BindingHttpPost, + Location: sp.ServiceProviderSLOURL, + }}, + }, + }, nil +} + +func (sp *SAMLServiceProvider) GetEncryptionKey() dsig.X509KeyStore { + return sp.SPKeyStore +} + +func (sp *SAMLServiceProvider) GetSigningKey() dsig.X509KeyStore { + if sp.SPSigningKeyStore == nil { + return sp.GetEncryptionKey() // Default is signing key is same as encryption key + } + return sp.SPSigningKeyStore +} + +func (sp *SAMLServiceProvider) GetEncryptionCertBytes() ([]byte, error) { + if _, encryptionCert, err := sp.GetEncryptionKey().GetKeyPair(); err != nil { + return nil, ErrSaml{Message: "no SP encryption certificate", System: err} + } else if len(encryptionCert) < 1 { + return nil, ErrSaml{Message: "empty SP encryption certificate"} + } else { + return encryptionCert, nil + } +} + +func (sp *SAMLServiceProvider) GetSigningCertBytes() ([]byte, error) { + if _, signingCert, err := sp.GetSigningKey().GetKeyPair(); err != nil { + return nil, ErrSaml{Message: "no SP signing certificate", System: err} + } else if len(signingCert) < 1 { + return nil, ErrSaml{Message: "empty SP signing certificate"} + } else { + return signingCert, nil + } +} + +func (sp *SAMLServiceProvider) SigningContext() *dsig.SigningContext { + sp.signingContextMu.RLock() + signingContext := sp.signingContext + sp.signingContextMu.RUnlock() + + if signingContext != nil { + return signingContext + } + + sp.signingContextMu.Lock() + defer sp.signingContextMu.Unlock() + + sp.signingContext = dsig.NewDefaultSigningContext(sp.GetSigningKey()) + sp.signingContext.SetSignatureMethod(sp.SignAuthnRequestsAlgorithm) + if sp.SignAuthnRequestsCanonicalizer != nil { + sp.signingContext.Canonicalizer = sp.SignAuthnRequestsCanonicalizer + } + + return sp.signingContext +} + +type ProxyRestriction struct { + Count int + Audience []string +} + +type WarningInfo struct { + OneTimeUse bool + ProxyRestriction *ProxyRestriction + NotInAudience bool + InvalidTime bool +} + +type AssertionInfo struct { + NameID string + Values Values + WarningInfo *WarningInfo + SessionIndex string + AuthnInstant *time.Time + SessionNotOnOrAfter *time.Time + Assertions []types.Assertion + ResponseSignatureValidated bool +} diff --git a/vendor/github.com/russellhaering/gosaml2/test_constants.go b/vendor/github.com/russellhaering/gosaml2/test_constants.go new file mode 100644 index 000000000..514ef37cf --- /dev/null +++ b/vendor/github.com/russellhaering/gosaml2/test_constants.go @@ -0,0 +1,419 @@ +// Copyright 2016 Russell Haering et al. +// +// 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 saml2 + +var idpCertificate = ` +-----BEGIN CERTIFICATE----- +MIIDpDCCAoygAwIBAgIGAVLIBhAwMA0GCSqGSIb3DQEBBQUAMIGSMQswCQYDVQQGEwJVUzETMBEG +A1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzENMAsGA1UECgwET2t0YTEU +MBIGA1UECwwLU1NPUHJvdmlkZXIxEzARBgNVBAMMCmRldi0xMTY4MDcxHDAaBgkqhkiG9w0BCQEW +DWluZm9Ab2t0YS5jb20wHhcNMTYwMjA5MjE1MjA2WhcNMjYwMjA5MjE1MzA2WjCBkjELMAkGA1UE +BhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcMDVNhbiBGcmFuY2lzY28xDTALBgNV +BAoMBE9rdGExFDASBgNVBAsMC1NTT1Byb3ZpZGVyMRMwEQYDVQQDDApkZXYtMTE2ODA3MRwwGgYJ +KoZIhvcNAQkBFg1pbmZvQG9rdGEuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA +mtjBOZ8MmhUyi8cGk4dUY6Fj1MFDt/q3FFiaQpLzu3/q5lRVUNUBbAtqQWwY10dzfZguHOuvA5p5 +QyiVDvUhe+XkVwN2R2WfArQJRTPnIcOaHrxqQf3o5cCIG21ZtysFHJSo8clPSOe+0VsoRgcJ1aF4 +2rODwgqRRZdO9Wh3502XlJ799DJQ23IC7XasKEsGKzJqhlRrfd/FyIuZT0sFHDKRz5snSJhm9gpN +uQlCmk7ONZ1sXqtt+nBIfWIqeoYQubPW7pT5GTc7wouWq4TCjHJiK9k2HiyNxW0E3JX08swEZi2+ +LVDjgLzNc4lwjSYIj3AOtPZs8s606oBdIBni4wIDAQABMA0GCSqGSIb3DQEBBQUAA4IBAQBMxSkJ +TxkXxsoKNW0awJNpWRbU81QpheMFfENIzLam4Itc/5kSZAaSy/9e2QKfo4jBo/MMbCq2vM9TyeJQ +DJpRaioUTd2lGh4TLUxAxCxtUk/pascL+3Nn936LFmUCLxaxnbeGzPOXAhscCtU1H0nFsXRnKx5a +cPXYSKFZZZktieSkww2Oi8dg2DYaQhGQMSFMVqgVfwEu4bvCRBvdSiNXdWGCZQmFVzBZZ/9rOLzP +pvTFTPnpkavJm81FLlUhiE/oFgKlCDLWDknSpXAI0uZGERcwPca6xvIMh86LjQKjbVci9FYDStXC +qRnqQ+TccSu/B6uONFsDEngGcXSKfB+a +-----END CERTIFICATE----- +` + +const rawResponse = ` +http://www.okta.com/exk5zt0r12Edi4rD20h7http://www.okta.com/exk5zt0r12Edi4rD20h7FsWGCBC+t/LaVkUKUvRQpzyZTmlxUzw4R9FOzXPPJRw=hS50WgYs/cn3uxmhrza/0/0QW3H7bwdjPZ2hQmG7IeSd7awTOghBqdrjvaPfQ7tRW+UK6ewMgIBVKG6jV3qYAWeW2U70hMb7hE9qJqBKyYyimmhVWULx1HB2YmlU1wmispywoPlXQ6gj0iWaL2RFI83vUp7X50eZ6dELqoJVZpzQI065Tt0TG7UuKUW1flYsbiS9NaXnuw+mcrBW25ZA9F5CLePHki01ZzUw+XtNmKthEb7SR30mzPoj08Dji22daYvGu82IR01wIZPoQJPCGMT6y2xC/pQPqGljAg/vUa+gaYgaMaAVYxhk/hfgMUBlOeKACBaGTmygab1Nz5KvPg==MIIDpDCCAoygAwIBAgIGAVLIBhAwMA0GCSqGSIb3DQEBBQUAMIGSMQswCQYDVQQGEwJVUzETMBEG +A1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzENMAsGA1UECgwET2t0YTEU +MBIGA1UECwwLU1NPUHJvdmlkZXIxEzARBgNVBAMMCmRldi0xMTY4MDcxHDAaBgkqhkiG9w0BCQEW +DWluZm9Ab2t0YS5jb20wHhcNMTYwMjA5MjE1MjA2WhcNMjYwMjA5MjE1MzA2WjCBkjELMAkGA1UE +BhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcMDVNhbiBGcmFuY2lzY28xDTALBgNV +BAoMBE9rdGExFDASBgNVBAsMC1NTT1Byb3ZpZGVyMRMwEQYDVQQDDApkZXYtMTE2ODA3MRwwGgYJ +KoZIhvcNAQkBFg1pbmZvQG9rdGEuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA +mtjBOZ8MmhUyi8cGk4dUY6Fj1MFDt/q3FFiaQpLzu3/q5lRVUNUBbAtqQWwY10dzfZguHOuvA5p5 +QyiVDvUhe+XkVwN2R2WfArQJRTPnIcOaHrxqQf3o5cCIG21ZtysFHJSo8clPSOe+0VsoRgcJ1aF4 +2rODwgqRRZdO9Wh3502XlJ799DJQ23IC7XasKEsGKzJqhlRrfd/FyIuZT0sFHDKRz5snSJhm9gpN +uQlCmk7ONZ1sXqtt+nBIfWIqeoYQubPW7pT5GTc7wouWq4TCjHJiK9k2HiyNxW0E3JX08swEZi2+ +LVDjgLzNc4lwjSYIj3AOtPZs8s606oBdIBni4wIDAQABMA0GCSqGSIb3DQEBBQUAA4IBAQBMxSkJ +TxkXxsoKNW0awJNpWRbU81QpheMFfENIzLam4Itc/5kSZAaSy/9e2QKfo4jBo/MMbCq2vM9TyeJQ +DJpRaioUTd2lGh4TLUxAxCxtUk/pascL+3Nn936LFmUCLxaxnbeGzPOXAhscCtU1H0nFsXRnKx5a +cPXYSKFZZZktieSkww2Oi8dg2DYaQhGQMSFMVqgVfwEu4bvCRBvdSiNXdWGCZQmFVzBZZ/9rOLzP +pvTFTPnpkavJm81FLlUhiE/oFgKlCDLWDknSpXAI0uZGERcwPca6xvIMh86LjQKjbVci9FYDStXC +qRnqQ+TccSu/B6uONFsDEngGcXSKfB+aphoebe.simon@scaleft.com123urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransportPhoebeSimonphoebe.simon@scaleft.comphoebesimon` + +const manInTheMiddledResponse = ` +http://www.okta.com/exk5zt0r12Edi4rD20h7ijTqmVmDy7ssK+rvmJaCQ6AQaFaXz+HIN/r6O37B0eQ=G09fAYXGDLK+/jAekHsNL0RLo40Xm6+VwXmUj0IDIrvIIv/mJU5VD6ylOLnPezLDBVY9BJst1YCz+8krdvmQ8Stkd6qiN2bN/5KpCdika111YGpeNdMmg/E57ZG3S895hTNJQYOfCwhPFUtQuXLkspOaw81pcqOTr+bVSofJ8uQP7cVQa/ANxbjKAj0fhAuxAvZfiqPms5Stv4sNGpzULUDJl87CoEleHExGmpTsI7Qt3EvGToPMZXPHF4MGvuC0Z2ZD4iI6Pr7xk98t54PJtAX2qJu1tZqBJmL0Qcq5spl9W3yC1tAZuDeFLm1C4/T9crO2Q5WILP/tkw/yJ+ZttQ==MIIDpDCCAoygAwIBAgIGAVLIBhAwMA0GCSqGSIb3DQEBBQUAMIGSMQswCQYDVQQGEwJVUzETMBEG +A1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzENMAsGA1UECgwET2t0YTEU +MBIGA1UECwwLU1NPUHJvdmlkZXIxEzARBgNVBAMMCmRldi0xMTY4MDcxHDAaBgkqhkiG9w0BCQEW +DWluZm9Ab2t0YS5jb20wHhcNMTYwMjA5MjE1MjA2WhcNMjYwMjA5MjE1MzA2WjCBkjELMAkGA1UE +BhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcMDVNhbiBGcmFuY2lzY28xDTALBgNV +BAoMBE9rdGExFDASBgNVBAsMC1NTT1Byb3ZpZGVyMRMwEQYDVQQDDApkZXYtMTE2ODA3MRwwGgYJ +KoZIhvcNAQkBFg1pbmZvQG9rdGEuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA +mtjBOZ8MmhUyi8cGk4dUY6Fj1MFDt/q3FFiaQpLzu3/q5lRVUNUBbAtqQWwY10dzfZguHOuvA5p5 +QyiVDvUhe+XkVwN2R2WfArQJRTPnIcOaHrxqQf3o5cCIG21ZtysFHJSo8clPSOe+0VsoRgcJ1aF4 +2rODwgqRRZdO9Wh3502XlJ799DJQ23IC7XasKEsGKzJqhlRrfd/FyIuZT0sFHDKRz5snSJhm9gpN +uQlCmk7ONZ1sXqtt+nBIfWIqeoYQubPW7pT5GTc7wouWq4TCjHJiK9k2HiyNxW0E3JX08swEZi2+ +LVDjgLzNc4lwjSYIj3AOtPZs8s606oBdIBni4wIDAQABMA0GCSqGSIb3DQEBBQUAA4IBAQBMxSkJ +TxkXxsoKNW0awJNpWRbU81QpheMFfENIzLam4Itc/5kSZAaSy/9e2QKfo4jBo/MMbCq2vM9TyeJQ +DJpRaioUTd2lGh4TLUxAxCxtUk/pascL+3Nn936LFmUCLxaxnbeGzPOXAhscCtU1H0nFsXRnKx5a +cPXYSKFZZZktieSkww2Oi8dg2DYaQhGQMSFMVqgVfwEu4bvCRBvdSiNXdWGCZQmFVzBZZ/9rOLzP +pvTFTPnpkavJm81FLlUhiE/oFgKlCDLWDknSpXAI0uZGERcwPca6xvIMh86LjQKjbVci9FYDStXC +qRnqQ+TccSu/B6uONFsDEngGcXSKfB+ahttp://www.okta.com/exk5zt0r12Edi4rD20h7zln6sheEO2JBdanrT5mZtJZ192tGHavuBpCFHQsJFVg=dHh6TWbnjtImyrfjPTX5QzE/6Vm/HsRWVvWWlvFAddf/CvhO4Kc5j8C7hvQoYMLhYuZMFFSReGysuDy5IscOJwTGhhcvb238qHSGGs6q8OUBCsmLSDAbIaGA++LV/tkUZ2ridGIi0yT81UOl1oT1batlHsK3eMyxkpnFmvBzIm4tGTzRkOPpYRLeiM9bxbKI+DM/623DCXyBCLYBzJo1O6QE02aLajwRMi/vmiV4LSiGlFcY9TtDCafdVJRv0tIQ25BQoT4feuHdr6S8xOSpGgRYH5ECamVOt4e079XdEkVUiSzQokiUkgDlTXEyerPLOVsOk4PW5nRs86sXIiGL5w==MIIDpDCCAoygAwIBAgIGAVLIBhAwMA0GCSqGSIb3DQEBBQUAMIGSMQswCQYDVQQGEwJVUzETMBEG +A1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzENMAsGA1UECgwET2t0YTEU +MBIGA1UECwwLU1NPUHJvdmlkZXIxEzARBgNVBAMMCmRldi0xMTY4MDcxHDAaBgkqhkiH9w0BCQEW +DWluZm9Ab2t0YS5jb20wHhcNMTYwMjA5MjE1MjA2WhcNMjYwMjA5MjE1MzA2WjCBkjELMAkGA1UE +BhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcMDVNhbiBGcmFuY2lzY28xDTALBgNV +BAoMBE9rdGExFDASBgNVBAsMC1NTT1Byb3ZpZGVyMRMwEQYDVQQDDApkZXYtMTE2ODA3MRwwGgYJ +KoZIhvcNAQkBFg1pbmZvQG9rdGEuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA +mtjBOZ8MmhUyi8cGk4dUY6Fj1MFDt/q3FFiaQpLzu3/q5lRVUNUBbAtqQWwY10dzfZguHOuvA5p5 +QyiVDvUhe+XkVwN2R2WfArQJRTPnIcOaHrxqQf3o5cCIG21ZtysFHJSo8clPSOe+0VsoRgcJ1aF4 +2rODwgqRRZdO9Wh3502XlJ799DJQ23IC7XasKEsGKzJqhlRrfd/FyIuZT0sFHDKRz5snSJhm9gpN +uQlCmk7ONZ1sXqtt+nBIfWIqeoYQubPW7pT5GTc7wouWq4TCjHJiK9k2HiyNxW0E3JX08swEZi2+ +LVDjgLzNc4lwjSYIj3AOtPZs8s606oBdIBni4wIDAQABMA0GCSqGSIb3DQEBBQUAA4IBAQBMxSkJ +TxkXxsoKNW0awJNpWRbU81QpheMFfENIzLam4Itc/5kSZAaSy/9e2QKfo4jBo/MMbCq2vM9TyeJQ +DJpRaioUTd2lGh4TLUxAxCxtUk/pascL+3Nn936LFmUCLxaxnbeGzPOXAhscCtU1H0nFsXRnKx5a +cPXYSKFZZZktieSkww2Oi8dg2DYaQhGQMSFMVqgVfwEu4bvCRBvdSiNXdWGCZQmFVzBZZ/9rOLzP +pvTFTPnpkavJm81FLlUhiE/oFgKlCDLWDknSpXAI0uZGERcwPca6xvIMh86LjQKjbVci9FYDStXC +qRnqQ+TccSu/B6uONFsDEngGcXSKfB+aphoebe.simon@scaleft.com123urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransportPhoebeSimonphoebe.simon@scaleft.com` + +const alteredReferenceURIResponse = ` +http://www.okta.com/exk5zt0r12Edi4rD20h7ijTqmVmDy7ssK+rvmJaCQ6AQaFaXz+HIN/r6O37B0eQ=G09fAYXGDLK+/jAekHsNL0RLo40Xm6+VwXmUj0IDIrvIIv/mJU5VD6ylOLnPezLDBVY9BJst1YCz+8krdvmQ8Stkd6qiN2bN/5KpCdika111YGpeNdMmg/E57ZG3S895hTNJQYOfCwhPFUtQuXLkspOaw81pcqOTr+bVSofJ8uQP7cVQa/ANxbjKAj0fhAuxAvZfiqPms5Stv4sNGpzULUDJl87CoEleHExGmpTsI7Qt3EvGToPMZXPHF4MGvuC0Z2ZD4iI6Pr7xk98t54PJtAX2qJu1tZqBJmL0Qcq5spl9W3yC1tAZuDeFLm1C4/T9crO2Q5WILP/tkw/yJ+ZttQ==MIIDpDCCAoygAwIBAgIGAVLIBhAwMA0GCSqGSIb3DQEBBQUAMIGSMQswCQYDVQQGEwJVUzETMBEG +A1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzENMAsGA1UECgwET2t0YTEU +MBIGA1UECwwLU1NPUHJvdmlkZXIxEzARBgNVBAMMCmRldi0xMTY4MDcxHDAaBgkqhkiG9w0BCQEW +DWluZm9Ab2t0YS5jb20wHhcNMTYwMjA5MjE1MjA2WhcNMjYwMjA5MjE1MzA2WjCBkjELMAkGA1UE +BhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcMDVNhbiBGcmFuY2lzY28xDTALBgNV +BAoMBE9rdGExFDASBgNVBAsMC1NTT1Byb3ZpZGVyMRMwEQYDVQQDDApkZXYtMTE2ODA3MRwwGgYJ +KoZIhvcNAQkBFg1pbmZvQG9rdGEuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA +mtjBOZ8MmhUyi8cGk4dUY6Fj1MFDt/q3FFiaQpLzu3/q5lRVUNUBbAtqQWwY10dzfZguHOuvA5p5 +QyiVDvUhe+XkVwN2R2WfArQJRTPnIcOaHrxqQf3o5cCIG21ZtysFHJSo8clPSOe+0VsoRgcJ1aF4 +2rODwgqRRZdO9Wh3502XlJ799DJQ23IC7XasKEsGKzJqhlRrfd/FyIuZT0sFHDKRz5snSJhm9gpN +uQlCmk7ONZ1sXqtt+nBIfWIqeoYQubPW7pT5GTc7wouWq4TCjHJiK9k2HiyNxW0E3JX08swEZi2+ +LVDjgLzNc4lwjSYIj3AOtPZs8s606oBdIBni4wIDAQABMA0GCSqGSIb3DQEBBQUAA4IBAQBMxSkJ +TxkXxsoKNW0awJNpWRbU81QpheMFfENIzLam4Itc/5kSZAaSy/9e2QKfo4jBo/MMbCq2vM9TyeJQ +DJpRaioUTd2lGh4TLUxAxCxtUk/pascL+3Nn936LFmUCLxaxnbeGzPOXAhscCtU1H0nFsXRnKx5a +cPXYSKFZZZktieSkww2Oi8dg2DYaQhGQMSFMVqgVfwEu4bvCRBvdSiNXdWGCZQmFVzBZZ/9rOLzP +pvTFTPnpkavJm81FLlUhiE/oFgKlCDLWDknSpXAI0uZGERcwPca6xvIMh86LjQKjbVci9FYDStXC +qRnqQ+TccSu/B6uONFsDEngGcXSKfB+ahttp://www.okta.com/exk5zt0r12Edi4rD20h7zln6sheEO2JBdanrT5mZtJZ192tGHavuBpCFHQsJFVg=dHh6TWbnjtImyrfjPTX5QzE/6Vm/HsRWVvWWlvFAddf/CvhO4Kc5j8C7hvQoYMLhYuZMFFSReGysuDy5IscOJwTGhhcvb238qHSGGs6q8OUBCsmLSDAbIaGA++LV/tkUZ2ridGIi0yT81UOl1oT1batlHsK3eMyxkpnFmvBzIm4tGTzRkOPpYRLeiM9bxbKI+DM/623DCXyBCLYBzJo1O6QE02aLajwRMi/vmiV4LSiGlFcY9TtDCafdVJRv0tIQ25BQoT4feuHdr6S8xOSpGgRYH5ECamVOt4e079XdEkVUiSzQokiUkgDlTXEyerPLOVsOk4PW5nRs86sXIiGL5w==MIIDpDCCAoygAwIBAgIGAVLIBhAwMA0GCSqGSIb3DQEBBQUAMIGSMQswCQYDVQQGEwJVUzETMBEG +A1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzENMAsGA1UECgwET2t0YTEU +MBIGA1UECwwLU1NPUHJvdmlkZXIxEzARBgNVBAMMCmRldi0xMTY4MDcxHDAaBgkqhkiH9w0BCQEW +DWluZm9Ab2t0YS5jb20wHhcNMTYwMjA5MjE1MjA2WhcNMjYwMjA5MjE1MzA2WjCBkjELMAkGA1UE +BhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcMDVNhbiBGcmFuY2lzY28xDTALBgNV +BAoMBE9rdGExFDASBgNVBAsMC1NTT1Byb3ZpZGVyMRMwEQYDVQQDDApkZXYtMTE2ODA3MRwwGgYJ +KoZIhvcNAQkBFg1pbmZvQG9rdGEuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA +mtjBOZ8MmhUyi8cGk4dUY6Fj1MFDt/q3FFiaQpLzu3/q5lRVUNUBbAtqQWwY10dzfZguHOuvA5p5 +QyiVDvUhe+XkVwN2R2WfArQJRTPnIcOaHrxqQf3o5cCIG21ZtysFHJSo8clPSOe+0VsoRgcJ1aF4 +2rODwgqRRZdO9Wh3502XlJ799DJQ23IC7XasKEsGKzJqhlRrfd/FyIuZT0sFHDKRz5snSJhm9gpN +uQlCmk7ONZ1sXqtt+nBIfWIqeoYQubPW7pT5GTc7wouWq4TCjHJiK9k2HiyNxW0E3JX08swEZi2+ +LVDjgLzNc4lwjSYIj3AOtPZs8s606oBdIBni4wIDAQABMA0GCSqGSIb3DQEBBQUAA4IBAQBMxSkJ +TxkXxsoKNW0awJNpWRbU81QpheMFfENIzLam4Itc/5kSZAaSy/9e2QKfo4jBo/MMbCq2vM9TyeJQ +DJpRaioUTd2lGh4TLUxAxCxtUk/pascL+3Nn936LFmUCLxaxnbeGzPOXAhscCtU1H0nFsXRnKx5a +cPXYSKFZZZktieSkww2Oi8dg2DYaQhGQMSFMVqgVfwEu4bvCRBvdSiNXdWGCZQmFVzBZZ/9rOLzP +pvTFTPnpkavJm81FLlUhiE/oFgKlCDLWDknSpXAI0uZGERcwPca6xvIMh86LjQKjbVci9FYDStXC +qRnqQ+TccSu/B6uONFsDEngGcXSKfB+aphoebe.simon@scaleft.com123urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransportPhoebeSimonphoebe.simon@scaleft.com` + +const alteredSignedInfoResponse = ` +http://www.okta.com/exk5zt0r12Edi4rD20h7ijTqmVmDy7ssK+rvmJaCQ6AQaFaXz+HIN/r6O37B0eQ=G09fAYXGDLK+/jAekHsNL0RLo40Xm6+VwXmUj0IDIrvIIv/mJU5VD6ylOLnPezLDBVY9BJst1YCz+8krdvmQ8Stkd6qiN2bN/5KpCdika111YGpeNdMmg/E57ZG3S895hTNJQYOfCwhPFUtQuXLkspOaw81pcqOTr+bVSofJ8uQP7cVQa/ANxbjKAj0fhAuxAvZfiqPms5Stv4sNGpzULUDJl87CoEleHExGmpTsI7Qt3EvGToPMZXPHF4MGvuC0Z2ZD4iI6Pr7xk98t54PJtAX2qJu1tZqBJmL0Qcq5spl9W3yC1tAZuDeFLm1C4/T9crO2Q5WILP/tkw/yJ+ZttQ==MIIDpDCCAoygAwIBAgIGAVLIBhAwMA0GCSqGSIb3DQEBBQUAMIGSMQswCQYDVQQGEwJVUzETMBEG +A1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzENMAsGA1UECgwET2t0YTEV +MBIGA1UECwwLU1NPUHJvdmlkZXIxEzARBgNVBAMMCmRldi0xMTY4MDcxHDAaBgkqhkiG9w0BCQEW +DWluZm9Ab2t0YS5jb20wHhcNMTYwMjA5MjE1MjA2WhcNMjYwMjA5MjE1MzA2WjCBkjELMAkGA1UE +BhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcMDVNhbiBGcmFuY2lzY28xDTALBgNV +BAoMBE9rdGExFDASBgNVBAsMC1NTT1Byb3ZpZGVyMRMwEQYDVQQDDApkZXYtMTE2ODA3MRwwGgYJ +KoZIhvcNAQkBFg1pbmZvQG9rdGEuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA +mtjBOZ8MmhUyi8cGk4dUY6Fj1MFDt/q3FFiaQpLzu3/q5lRVUNUBbAtqQWwY10dzfZguHOuvA5p5 +QyiVDvUhe+XkVwN2R2WfArQJRTPnIcOaHrxqQf3o5cCIG21ZtysFHJSo8clPSOe+0VsoRgcJ1aF4 +2rODwgqRRZdO9Wh3502XlJ799DJQ23IC7XasKEsGKzJqhlRrfd/FyIuZT0sFHDKRz5snSJhm9gpN +uQlCmk7ONZ1sXqtt+nBIfWIqeoYQubPW7pT5GTc7wouWq4TCjHJiK9k2HiyNxW0E3JX08swEZi2+ +LVDjgLzNc4lwjSYIj3AOtPZs8s606oBdIBni4wIDAQABMA0GCSqGSIb3DQEBBQUAA4IBAQBMxSkJ +TxkXxsoKNW0awJNpWRbU81QpheMFfENIzLam4Itc/5kSZAaSy/9e2QKfo4jBo/MMbCq2vM9TyeJQ +DJpRaioUTd2lGh4TLUxAxCxtUk/pascL+3Nn936LFmUCLxaxnbeGzPOXAhscCtU1H0nFsXRnKx5a +cPXYSKFZZZktieSkww2Oi8dg2DYaQhGQMSFMVqgVfwEu4bvCRBvdSiNXdWGCZQmFVzBZZ/9rOLzP +pvTFTPnpkavJm81FLlUhiE/oFgKlCDLWDknSpXAI0uZGERcwPca6xvIMh86LjQKjbVci9FYDStXC +qRnqQ+TccSu/B6uONFsDEngGcXSKfB+ahttp://www.okta.com/exk5zt0r12Edi4rD20h7zln6sheEO2JBdanrT5mZtJZ192tGHavuBpCFHQsJFVg=dHh6TWbnjtImyrfjPTX5QzE/6Vm/HsRWVvWWlvFAddf/CvhO4Kc5j8C7hvQoYMLhYuZMFFSReGysuDy5IscOJwTGhhcvb238qHSGGs6q8OUBCsmLSDAbIaGA++LV/tkUZ2ridGIi0yT81UOl1oT1batlHsK3eMyxkpnFmvBzIm4tGTzRkOPpYRLeiM9bxbKI+DM/623DCXyBCLYBzJo1O6QE02aLajwRMi/vmiV4LSiGlFcY9TtDCafdVJRv0tIQ25BQoT4feuHdr6S8xOSpGgRYH5ECamVOt4e079XdEkVUiSzQokiUkgDlTXEyerPLOVsOk4PW5nRs86sXIiGL5w==MIIDpDCCAoygAwIBAgIGAVLIBhAwMA0GCSqGSIb3DQEBBQUAMIGSMQswCQYDVQQGEwJVUzETMBEG +A1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzENMAsGA1UECgwET2t0YTEU +MBIGA1UECwwLU1NPUHJvdmlkZXIxEzARBgNVBAMMCmRldi0xMTY4MDcxHDAaBgkqhkiH9w0BCQEW +DWluZm9Ab2t0YS5jb20wHhcNMTYwMjA5MjE1MjA2WhcNMjYwMjA5MjE1MzA2WjCBkjELMAkGA1UE +BhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcMDVNhbiBGcmFuY2lzY28xDTALBgNV +BAoMBE9rdGExFDASBgNVBAsMC1NTT1Byb3ZpZGVyMRMwEQYDVQQDDApkZXYtMTE2ODA3MRwwGgYJ +KoZIhvcNAQkBFg1pbmZvQG9rdGEuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA +mtjBOZ8MmhUyi8cGk4dUY6Fj1MFDt/q3FFiaQpLzu3/q5lRVUNUBbAtqQWwY10dzfZguHOuvA5p5 +QyiVDvUhe+XkVwN2R2WfArQJRTPnIcOaHrxqQf3o5cCIG21ZtysFHJSo8clPSOe+0VsoRgcJ1aF4 +2rODwgqRRZdO9Wh3502XlJ799DJQ23IC7XasKEsGKzJqhlRrfd/FyIuZT0sFHDKRz5snSJhm9gpN +uQlCmk7ONZ1sXqtt+nBIfWIqeoYQubPW7pT5GTc7wouWq4TCjHJiK9k2HiyNxW0E3JX08swEZi2+ +LVDjgLzNc4lwjSYIj3AOtPZs8s606oBdIBni4wIDAQABMA0GCSqGSIb3DQEBBQUAA4IBAQBMxSkJ +TxkXxsoKNW0awJNpWRbU81QpheMFfENIzLam4Itc/5kSZAaSy/9e2QKfo4jBo/MMbCq2vM9TyeJQ +DJpRaioUTd2lGh4TLUxAxCxtUk/pascL+3Nn936LFmUCLxaxnbeGzPOXAhscCtU1H0nFsXRnKx5a +cPXYSKFZZZktieSkww2Oi8dg2DYaQhGQMSFMVqgVfwEu4bvCRBvdSiNXdWGCZQmFVzBZZ/9rOLzP +pvTFTPnpkavJm81FLlUhiE/oFgKlCDLWDknSpXAI0uZGERcwPca6xvIMh86LjQKjbVci9FYDStXC +qRnqQ+TccSu/B6uONFsDEngGcXSKfB+aphoebe.simon@scaleft.com123urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransportPhoebeSimonphoebe.simon@scaleft.com` + +const alteredRecipientResponse = ` +http://www.okta.com/exk5zt0r12Edi4rD20h7http://www.okta.com/exk5zt0r12Edi4rD20h7FsWGCBC+t/LaVkUKUvRQpzyZTmlxUzw4R9FOzXPPJRw=hS50WgYs/cn3uxmhrza/0/0QW3H7bwdjPZ2hQmG7IeSd7awTOghBqdrjvaPfQ7tRW+UK6ewMgIBVKG6jV3qYAWeW2U70hMb7hE9qJqBKyYyimmhVWULx1HB2YmlU1wmispywoPlXQ6gj0iWaL2RFI83vUp7X50eZ6dELqoJVZpzQI065Tt0TG7UuKUW1flYsbiS9NaXnuw+mcrBW25ZA9F5CLePHki01ZzUw+XtNmKthEb7SR30mzPoj08Dji22daYvGu82IR01wIZPoQJPCGMT6y2xC/pQPqGljAg/vUa+gaYgaMaAVYxhk/hfgMUBlOeKACBaGTmygab1Nz5KvPg==MIIDpDCCAoygAwIBAgIGAVLIBhAwMA0GCSqGSIb3DQEBBQUAMIGSMQswCQYDVQQGEwJVUzETMBEG +A1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzENMAsGA1UECgwET2t0YTEU +MBIGA1UECwwLU1NPUHJvdmlkZXIxEzARBgNVBAMMCmRldi0xMTY4MDcxHDAaBgkqhkiG9w0BCQEW +DWluZm9Ab2t0YS5jb20wHhcNMTYwMjA5MjE1MjA2WhcNMjYwMjA5MjE1MzA2WjCBkjELMAkGA1UE +BhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcMDVNhbiBGcmFuY2lzY28xDTALBgNV +BAoMBE9rdGExFDASBgNVBAsMC1NTT1Byb3ZpZGVyMRMwEQYDVQQDDApkZXYtMTE2ODA3MRwwGgYJ +KoZIhvcNAQkBFg1pbmZvQG9rdGEuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA +mtjBOZ8MmhUyi8cGk4dUY6Fj1MFDt/q3FFiaQpLzu3/q5lRVUNUBbAtqQWwY10dzfZguHOuvA5p5 +QyiVDvUhe+XkVwN2R2WfArQJRTPnIcOaHrxqQf3o5cCIG21ZtysFHJSo8clPSOe+0VsoRgcJ1aF4 +2rODwgqRRZdO9Wh3502XlJ799DJQ23IC7XasKEsGKzJqhlRrfd/FyIuZT0sFHDKRz5snSJhm9gpN +uQlCmk7ONZ1sXqtt+nBIfWIqeoYQubPW7pT5GTc7wouWq4TCjHJiK9k2HiyNxW0E3JX08swEZi2+ +LVDjgLzNc4lwjSYIj3AOtPZs8s606oBdIBni4wIDAQABMA0GCSqGSIb3DQEBBQUAA4IBAQBMxSkJ +TxkXxsoKNW0awJNpWRbU81QpheMFfENIzLam4Itc/5kSZAaSy/9e2QKfo4jBo/MMbCq2vM9TyeJQ +DJpRaioUTd2lGh4TLUxAxCxtUk/pascL+3Nn936LFmUCLxaxnbeGzPOXAhscCtU1H0nFsXRnKx5a +cPXYSKFZZZktieSkww2Oi8dg2DYaQhGQMSFMVqgVfwEu4bvCRBvdSiNXdWGCZQmFVzBZZ/9rOLzP +pvTFTPnpkavJm81FLlUhiE/oFgKlCDLWDknSpXAI0uZGERcwPca6xvIMh86LjQKjbVci9FYDStXC +qRnqQ+TccSu/B6uONFsDEngGcXSKfB+aphoebe.simon@scaleft.com123urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransportPhoebeSimonphoebe.simon@scaleft.comphoebe.simon@scaleft.com` + +const alteredSubjectConfirmationMethodResponse = ` +http://www.okta.com/exk5zt0r12Edi4rD20h7http://www.okta.com/exk5zt0r12Edi4rD20h7FsWGCBC+t/LaVkUKUvRQpzyZTmlxUzw4R9FOzXPPJRw=hS50WgYs/cn3uxmhrza/0/0QW3H7bwdjPZ2hQmG7IeSd7awTOghBqdrjvaPfQ7tRW+UK6ewMgIBVKG6jV3qYAWeW2U70hMb7hE9qJqBKyYyimmhVWULx1HB2YmlU1wmispywoPlXQ6gj0iWaL2RFI83vUp7X50eZ6dELqoJVZpzQI065Tt0TG7UuKUW1flYsbiS9NaXnuw+mcrBW25ZA9F5CLePHki01ZzUw+XtNmKthEb7SR30mzPoj08Dji22daYvGu82IR01wIZPoQJPCGMT6y2xC/pQPqGljAg/vUa+gaYgaMaAVYxhk/hfgMUBlOeKACBaGTmygab1Nz5KvPg==MIIDpDCCAoygAwIBAgIGAVLIBhAwMA0GCSqGSIb3DQEBBQUAMIGSMQswCQYDVQQGEwJVUzETMBEG +A1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzENMAsGA1UECgwET2t0YTEU +MBIGA1UECwwLU1NPUHJvdmlkZXIxEzARBgNVBAMMCmRldi0xMTY4MDcxHDAaBgkqhkiG9w0BCQEW +DWluZm9Ab2t0YS5jb20wHhcNMTYwMjA5MjE1MjA2WhcNMjYwMjA5MjE1MzA2WjCBkjELMAkGA1UE +BhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcMDVNhbiBGcmFuY2lzY28xDTALBgNV +BAoMBE9rdGExFDASBgNVBAsMC1NTT1Byb3ZpZGVyMRMwEQYDVQQDDApkZXYtMTE2ODA3MRwwGgYJ +KoZIhvcNAQkBFg1pbmZvQG9rdGEuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA +mtjBOZ8MmhUyi8cGk4dUY6Fj1MFDt/q3FFiaQpLzu3/q5lRVUNUBbAtqQWwY10dzfZguHOuvA5p5 +QyiVDvUhe+XkVwN2R2WfArQJRTPnIcOaHrxqQf3o5cCIG21ZtysFHJSo8clPSOe+0VsoRgcJ1aF4 +2rODwgqRRZdO9Wh3502XlJ799DJQ23IC7XasKEsGKzJqhlRrfd/FyIuZT0sFHDKRz5snSJhm9gpN +uQlCmk7ONZ1sXqtt+nBIfWIqeoYQubPW7pT5GTc7wouWq4TCjHJiK9k2HiyNxW0E3JX08swEZi2+ +LVDjgLzNc4lwjSYIj3AOtPZs8s606oBdIBni4wIDAQABMA0GCSqGSIb3DQEBBQUAA4IBAQBMxSkJ +TxkXxsoKNW0awJNpWRbU81QpheMFfENIzLam4Itc/5kSZAaSy/9e2QKfo4jBo/MMbCq2vM9TyeJQ +DJpRaioUTd2lGh4TLUxAxCxtUk/pascL+3Nn936LFmUCLxaxnbeGzPOXAhscCtU1H0nFsXRnKx5a +cPXYSKFZZZktieSkww2Oi8dg2DYaQhGQMSFMVqgVfwEu4bvCRBvdSiNXdWGCZQmFVzBZZ/9rOLzP +pvTFTPnpkavJm81FLlUhiE/oFgKlCDLWDknSpXAI0uZGERcwPca6xvIMh86LjQKjbVci9FYDStXC +qRnqQ+TccSu/B6uONFsDEngGcXSKfB+aphoebe.simon@scaleft.com123urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransportPhoebeSimonphoebe.simon@scaleft.comphoebe.simon@scaleft.com` + +const alteredDestinationResponse = ` +http://www.okta.com/exk5zt0r12Edi4rD20h7http://www.okta.com/exk5zt0r12Edi4rD20h7FsWGCBC+t/LaVkUKUvRQpzyZTmlxUzw4R9FOzXPPJRw=hS50WgYs/cn3uxmhrza/0/0QW3H7bwdjPZ2hQmG7IeSd7awTOghBqdrjvaPfQ7tRW+UK6ewMgIBVKG6jV3qYAWeW2U70hMb7hE9qJqBKyYyimmhVWULx1HB2YmlU1wmispywoPlXQ6gj0iWaL2RFI83vUp7X50eZ6dELqoJVZpzQI065Tt0TG7UuKUW1flYsbiS9NaXnuw+mcrBW25ZA9F5CLePHki01ZzUw+XtNmKthEb7SR30mzPoj08Dji22daYvGu82IR01wIZPoQJPCGMT6y2xC/pQPqGljAg/vUa+gaYgaMaAVYxhk/hfgMUBlOeKACBaGTmygab1Nz5KvPg==MIIDpDCCAoygAwIBAgIGAVLIBhAwMA0GCSqGSIb3DQEBBQUAMIGSMQswCQYDVQQGEwJVUzETMBEG +A1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzENMAsGA1UECgwET2t0YTEU +MBIGA1UECwwLU1NPUHJvdmlkZXIxEzARBgNVBAMMCmRldi0xMTY4MDcxHDAaBgkqhkiG9w0BCQEW +DWluZm9Ab2t0YS5jb20wHhcNMTYwMjA5MjE1MjA2WhcNMjYwMjA5MjE1MzA2WjCBkjELMAkGA1UE +BhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcMDVNhbiBGcmFuY2lzY28xDTALBgNV +BAoMBE9rdGExFDASBgNVBAsMC1NTT1Byb3ZpZGVyMRMwEQYDVQQDDApkZXYtMTE2ODA3MRwwGgYJ +KoZIhvcNAQkBFg1pbmZvQG9rdGEuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA +mtjBOZ8MmhUyi8cGk4dUY6Fj1MFDt/q3FFiaQpLzu3/q5lRVUNUBbAtqQWwY10dzfZguHOuvA5p5 +QyiVDvUhe+XkVwN2R2WfArQJRTPnIcOaHrxqQf3o5cCIG21ZtysFHJSo8clPSOe+0VsoRgcJ1aF4 +2rODwgqRRZdO9Wh3502XlJ799DJQ23IC7XasKEsGKzJqhlRrfd/FyIuZT0sFHDKRz5snSJhm9gpN +uQlCmk7ONZ1sXqtt+nBIfWIqeoYQubPW7pT5GTc7wouWq4TCjHJiK9k2HiyNxW0E3JX08swEZi2+ +LVDjgLzNc4lwjSYIj3AOtPZs8s606oBdIBni4wIDAQABMA0GCSqGSIb3DQEBBQUAA4IBAQBMxSkJ +TxkXxsoKNW0awJNpWRbU81QpheMFfENIzLam4Itc/5kSZAaSy/9e2QKfo4jBo/MMbCq2vM9TyeJQ +DJpRaioUTd2lGh4TLUxAxCxtUk/pascL+3Nn936LFmUCLxaxnbeGzPOXAhscCtU1H0nFsXRnKx5a +cPXYSKFZZZktieSkww2Oi8dg2DYaQhGQMSFMVqgVfwEu4bvCRBvdSiNXdWGCZQmFVzBZZ/9rOLzP +pvTFTPnpkavJm81FLlUhiE/oFgKlCDLWDknSpXAI0uZGERcwPca6xvIMh86LjQKjbVci9FYDStXC +qRnqQ+TccSu/B6uONFsDEngGcXSKfB+aphoebe.simon@scaleft.com123urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransportPhoebeSimonphoebe.simon@scaleft.comphoebe.simon@scaleft.com` + +const alteredVersionResponse = ` +http://www.okta.com/exk5zt0r12Edi4rD20h7http://www.okta.com/exk5zt0r12Edi4rD20h7FsWGCBC+t/LaVkUKUvRQpzyZTmlxUzw4R9FOzXPPJRw=hS50WgYs/cn3uxmhrza/0/0QW3H7bwdjPZ2hQmG7IeSd7awTOghBqdrjvaPfQ7tRW+UK6ewMgIBVKG6jV3qYAWeW2U70hMb7hE9qJqBKyYyimmhVWULx1HB2YmlU1wmispywoPlXQ6gj0iWaL2RFI83vUp7X50eZ6dELqoJVZpzQI065Tt0TG7UuKUW1flYsbiS9NaXnuw+mcrBW25ZA9F5CLePHki01ZzUw+XtNmKthEb7SR30mzPoj08Dji22daYvGu82IR01wIZPoQJPCGMT6y2xC/pQPqGljAg/vUa+gaYgaMaAVYxhk/hfgMUBlOeKACBaGTmygab1Nz5KvPg==MIIDpDCCAoygAwIBAgIGAVLIBhAwMA0GCSqGSIb3DQEBBQUAMIGSMQswCQYDVQQGEwJVUzETMBEG +A1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzENMAsGA1UECgwET2t0YTEU +MBIGA1UECwwLU1NPUHJvdmlkZXIxEzARBgNVBAMMCmRldi0xMTY4MDcxHDAaBgkqhkiG9w0BCQEW +DWluZm9Ab2t0YS5jb20wHhcNMTYwMjA5MjE1MjA2WhcNMjYwMjA5MjE1MzA2WjCBkjELMAkGA1UE +BhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcMDVNhbiBGcmFuY2lzY28xDTALBgNV +BAoMBE9rdGExFDASBgNVBAsMC1NTT1Byb3ZpZGVyMRMwEQYDVQQDDApkZXYtMTE2ODA3MRwwGgYJ +KoZIhvcNAQkBFg1pbmZvQG9rdGEuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA +mtjBOZ8MmhUyi8cGk4dUY6Fj1MFDt/q3FFiaQpLzu3/q5lRVUNUBbAtqQWwY10dzfZguHOuvA5p5 +QyiVDvUhe+XkVwN2R2WfArQJRTPnIcOaHrxqQf3o5cCIG21ZtysFHJSo8clPSOe+0VsoRgcJ1aF4 +2rODwgqRRZdO9Wh3502XlJ799DJQ23IC7XasKEsGKzJqhlRrfd/FyIuZT0sFHDKRz5snSJhm9gpN +uQlCmk7ONZ1sXqtt+nBIfWIqeoYQubPW7pT5GTc7wouWq4TCjHJiK9k2HiyNxW0E3JX08swEZi2+ +LVDjgLzNc4lwjSYIj3AOtPZs8s606oBdIBni4wIDAQABMA0GCSqGSIb3DQEBBQUAA4IBAQBMxSkJ +TxkXxsoKNW0awJNpWRbU81QpheMFfENIzLam4Itc/5kSZAaSy/9e2QKfo4jBo/MMbCq2vM9TyeJQ +DJpRaioUTd2lGh4TLUxAxCxtUk/pascL+3Nn936LFmUCLxaxnbeGzPOXAhscCtU1H0nFsXRnKx5a +cPXYSKFZZZktieSkww2Oi8dg2DYaQhGQMSFMVqgVfwEu4bvCRBvdSiNXdWGCZQmFVzBZZ/9rOLzP +pvTFTPnpkavJm81FLlUhiE/oFgKlCDLWDknSpXAI0uZGERcwPca6xvIMh86LjQKjbVci9FYDStXC +qRnqQ+TccSu/B6uONFsDEngGcXSKfB+aphoebe.simon@scaleft.com123urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransportPhoebeSimonphoebe.simon@scaleft.comphoebe.simon@scaleft.com` + +const missingIDResponse = ` +http://www.okta.com/exk5zt0r12Edi4rD20h7mj+xyS5DtKVNbbFq4caWhGcrirqNzv7mIHNzHQH/f60=GA1URoMOE5EFfkHYimGXm7Ecph/m0s135VyF9Wut6NSpuZdQ2crM1IslvKCRjkE09rZgagQQMAThUcOFuX35dZPz9J4Ihpt1juhfGv1AV8I8jiOKFETj65MiPabDEi8+P6YWf4qNujAJXHKJIa/MFXBqoKR/imLQT8eu1nhVBQGYqWwZePddfXO2JYk2ce7mtnyMT0dUVb+o+tlEDYa7ri9fj4JL/z1XX7yrbVZxn2mdKPJtSSP8uHNOWSM6j1vp4oK+KSDviBfiVLlVA58noz5GyFtp642h+LV2quKbncMFfnfB1kfHLK/xaz9UaDBy+bHK4oGzSpVhZqcOzzliKA==MIIDpDCCAoygAwIBAgIGAVLIBhAwMA0GCSqGSIb3DQEBBQUAMIGSMQswCQYDVQQGEwJVUzETMBEG +A1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzENMAsGA1UECgwET2t0YTEU +MBIGA1UECwwLU1NPUHJvdmlkZXIxEzARBgNVBAMMCmRldi0xMTY4MDcxHDAaBgkqhkiG9w0BCQEW +DWluZm9Ab2t0YS5jb20wHhcNMTYwMjA5MjE1MjA2WhcNMjYwMjA5MjE1MzA2WjCBkjELMAkGA1UE +BhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcMDVNhbiBGcmFuY2lzY28xDTALBgNV +BAoMBE9rdGExFDASBgNVBAsMC1NTT1Byb3ZpZGVyMRMwEQYDVQQDDApkZXYtMTE2ODA3MRwwGgYJ +KoZIhvcNAQkBFg1pbmZvQG9rdGEuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA +mtjBOZ8MmhUyi8cGk4dUY6Fj1MFDt/q3FFiaQpLzu3/q5lRVUNUBbAtqQWwY10dzfZguHOuvA5p5 +QyiVDvUhe+XkVwN2R2WfArQJRTPnIcOaHrxqQf3o5cCIG21ZtysFHJSo8clPSOe+0VsoRgcJ1aF4 +2rODwgqRRZdO9Wh3502XlJ799DJQ23IC7XasKEsGKzJqhlRrfd/FyIuZT0sFHDKRz5snSJhm9gpN +uQlCmk7ONZ1sXqtt+nBIfWIqeoYQubPW7pT5GTc7wouWq4TCjHJiK9k2HiyNxW0E3JX08swEZi2+ +LVDjgLzNc4lwjSYIj3AOtPZs8s606oBdIBni4wIDAQABMA0GCSqGSIb3DQEBBQUAA4IBAQBMxSkJ +TxkXxsoKNW0awJNpWRbU81QpheMFfENIzLam4Itc/5kSZAaSy/9e2QKfo4jBo/MMbCq2vM9TyeJQ +DJpRaioUTd2lGh4TLUxAxCxtUk/pascL+3Nn936LFmUCLxaxnbeGzPOXAhscCtU1H0nFsXRnKx5a +cPXYSKFZZZktieSkww2Oi8dg2DYaQhGQMSFMVqgVfwEu4bvCRBvdSiNXdWGCZQmFVzBZZ/9rOLzP +pvTFTPnpkavJm81FLlUhiE/oFgKlCDLWDknSpXAI0uZGERcwPca6xvIMh86LjQKjbVci9FYDStXC +qRnqQ+TccSu/B6uONFsDEngGcXSKfB+ahttp://www.okta.com/exk5zt0r12Edi4rD20h7FsWGCBC+t/LaVkUKUvRQpzyZTmlxUzw4R9FOzXPPJRw=hS50WgYs/cn3uxmhrza/0/0QW3H7bwdjPZ2hQmG7IeSd7awTOghBqdrjvaPfQ7tRW+UK6ewMgIBVKG6jV3qYAWeW2U70hMb7hE9qJqBKyYyimmhVWULx1HB2YmlU1wmispywoPlXQ6gj0iWaL2RFI83vUp7X50eZ6dELqoJVZpzQI065Tt0TG7UuKUW1flYsbiS9NaXnuw+mcrBW25ZA9F5CLePHki01ZzUw+XtNmKthEb7SR30mzPoj08Dji22daYvGu82IR01wIZPoQJPCGMT6y2xC/pQPqGljAg/vUa+gaYgaMaAVYxhk/hfgMUBlOeKACBaGTmygab1Nz5KvPg==MIIDpDCCAoygAwIBAgIGAVLIBhAwMA0GCSqGSIb3DQEBBQUAMIGSMQswCQYDVQQGEwJVUzETMBEG +A1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzENMAsGA1UECgwET2t0YTEU +MBIGA1UECwwLU1NPUHJvdmlkZXIxEzARBgNVBAMMCmRldi0xMTY4MDcxHDAaBgkqhkiG9w0BCQEW +DWluZm9Ab2t0YS5jb20wHhcNMTYwMjA5MjE1MjA2WhcNMjYwMjA5MjE1MzA2WjCBkjELMAkGA1UE +BhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcMDVNhbiBGcmFuY2lzY28xDTALBgNV +BAoMBE9rdGExFDASBgNVBAsMC1NTT1Byb3ZpZGVyMRMwEQYDVQQDDApkZXYtMTE2ODA3MRwwGgYJ +KoZIhvcNAQkBFg1pbmZvQG9rdGEuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA +mtjBOZ8MmhUyi8cGk4dUY6Fj1MFDt/q3FFiaQpLzu3/q5lRVUNUBbAtqQWwY10dzfZguHOuvA5p5 +QyiVDvUhe+XkVwN2R2WfArQJRTPnIcOaHrxqQf3o5cCIG21ZtysFHJSo8clPSOe+0VsoRgcJ1aF4 +2rODwgqRRZdO9Wh3502XlJ799DJQ23IC7XasKEsGKzJqhlRrfd/FyIuZT0sFHDKRz5snSJhm9gpN +uQlCmk7ONZ1sXqtt+nBIfWIqeoYQubPW7pT5GTc7wouWq4TCjHJiK9k2HiyNxW0E3JX08swEZi2+ +LVDjgLzNc4lwjSYIj3AOtPZs8s606oBdIBni4wIDAQABMA0GCSqGSIb3DQEBBQUAA4IBAQBMxSkJ +TxkXxsoKNW0awJNpWRbU81QpheMFfENIzLam4Itc/5kSZAaSy/9e2QKfo4jBo/MMbCq2vM9TyeJQ +DJpRaioUTd2lGh4TLUxAxCxtUk/pascL+3Nn936LFmUCLxaxnbeGzPOXAhscCtU1H0nFsXRnKx5a +cPXYSKFZZZktieSkww2Oi8dg2DYaQhGQMSFMVqgVfwEu4bvCRBvdSiNXdWGCZQmFVzBZZ/9rOLzP +pvTFTPnpkavJm81FLlUhiE/oFgKlCDLWDknSpXAI0uZGERcwPca6xvIMh86LjQKjbVci9FYDStXC +qRnqQ+TccSu/B6uONFsDEngGcXSKfB+aphoebe.simon@scaleft.com123urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransportPhoebeSimonphoebe.simon@scaleft.comphoebe.simon@scaleft.com` + +const assertionInfoResponse = ` +http://www.okta.com/exk5zt0r12Edi4rD20h7http://www.okta.com/exk5zt0r12Edi4rD20h7FsWGCBC+t/LaVkUKUvRQpzyZTmlxUzw4R9FOzXPPJRw=hS50WgYs/cn3uxmhrza/0/0QW3H7bwdjPZ2hQmG7IeSd7awTOghBqdrjvaPfQ7tRW+UK6ewMgIBVKG6jV3qYAWeW2U70hMb7hE9qJqBKyYyimmhVWULx1HB2YmlU1wmispywoPlXQ6gj0iWaL2RFI83vUp7X50eZ6dELqoJVZpzQI065Tt0TG7UuKUW1flYsbiS9NaXnuw+mcrBW25ZA9F5CLePHki01ZzUw+XtNmKthEb7SR30mzPoj08Dji22daYvGu82IR01wIZPoQJPCGMT6y2xC/pQPqGljAg/vUa+gaYgaMaAVYxhk/hfgMUBlOeKACBaGTmygab1Nz5KvPg==MIIDpDCCAoygAwIBAgIGAVLIBhAwMA0GCSqGSIb3DQEBBQUAMIGSMQswCQYDVQQGEwJVUzETMBEG +A1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzENMAsGA1UECgwET2t0YTEU +MBIGA1UECwwLU1NPUHJvdmlkZXIxEzARBgNVBAMMCmRldi0xMTY4MDcxHDAaBgkqhkiG9w0BCQEW +DWluZm9Ab2t0YS5jb20wHhcNMTYwMjA5MjE1MjA2WhcNMjYwMjA5MjE1MzA2WjCBkjELMAkGA1UE +BhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcMDVNhbiBGcmFuY2lzY28xDTALBgNV +BAoMBE9rdGExFDASBgNVBAsMC1NTT1Byb3ZpZGVyMRMwEQYDVQQDDApkZXYtMTE2ODA3MRwwGgYJ +KoZIhvcNAQkBFg1pbmZvQG9rdGEuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA +mtjBOZ8MmhUyi8cGk4dUY6Fj1MFDt/q3FFiaQpLzu3/q5lRVUNUBbAtqQWwY10dzfZguHOuvA5p5 +QyiVDvUhe+XkVwN2R2WfArQJRTPnIcOaHrxqQf3o5cCIG21ZtysFHJSo8clPSOe+0VsoRgcJ1aF4 +2rODwgqRRZdO9Wh3502XlJ799DJQ23IC7XasKEsGKzJqhlRrfd/FyIuZT0sFHDKRz5snSJhm9gpN +uQlCmk7ONZ1sXqtt+nBIfWIqeoYQubPW7pT5GTc7wouWq4TCjHJiK9k2HiyNxW0E3JX08swEZi2+ +LVDjgLzNc4lwjSYIj3AOtPZs8s606oBdIBni4wIDAQABMA0GCSqGSIb3DQEBBQUAA4IBAQBMxSkJ +TxkXxsoKNW0awJNpWRbU81QpheMFfENIzLam4Itc/5kSZAaSy/9e2QKfo4jBo/MMbCq2vM9TyeJQ +DJpRaioUTd2lGh4TLUxAxCxtUk/pascL+3Nn936LFmUCLxaxnbeGzPOXAhscCtU1H0nFsXRnKx5a +cPXYSKFZZZktieSkww2Oi8dg2DYaQhGQMSFMVqgVfwEu4bvCRBvdSiNXdWGCZQmFVzBZZ/9rOLzP +pvTFTPnpkavJm81FLlUhiE/oFgKlCDLWDknSpXAI0uZGERcwPca6xvIMh86LjQKjbVci9FYDStXC +qRnqQ+TccSu/B6uONFsDEngGcXSKfB+aphoebe.simon@scaleft.com123urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransportPhoebeSimonphoebe.simon@scaleft.comphoebe.simon@scaleft.com` + +const assertionInfoModifiedAudienceResponse = ` +http://www.okta.com/exk5zt0r12Edi4rD20h7http://www.okta.com/exk5zt0r12Edi4rD20h7FsWGCBC+t/LaVkUKUvRQpzyZTmlxUzw4R9FOzXPPJRw=hS50WgYs/cn3uxmhrza/0/0QW3H7bwdjPZ2hQmG7IeSd7awTOghBqdrjvaPfQ7tRW+UK6ewMgIBVKG6jV3qYAWeW2U70hMb7hE9qJqBKyYyimmhVWULx1HB2YmlU1wmispywoPlXQ6gj0iWaL2RFI83vUp7X50eZ6dELqoJVZpzQI065Tt0TG7UuKUW1flYsbiS9NaXnuw+mcrBW25ZA9F5CLePHki01ZzUw+XtNmKthEb7SR30mzPoj08Dji22daYvGu82IR01wIZPoQJPCGMT6y2xC/pQPqGljAg/vUa+gaYgaMaAVYxhk/hfgMUBlOeKACBaGTmygab1Nz5KvPg==MIIDpDCCAoygAwIBAgIGAVLIBhAwMA0GCSqGSIb3DQEBBQUAMIGSMQswCQYDVQQGEwJVUzETMBEG +A1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzENMAsGA1UECgwET2t0YTEU +MBIGA1UECwwLU1NPUHJvdmlkZXIxEzARBgNVBAMMCmRldi0xMTY4MDcxHDAaBgkqhkiG9w0BCQEW +DWluZm9Ab2t0YS5jb20wHhcNMTYwMjA5MjE1MjA2WhcNMjYwMjA5MjE1MzA2WjCBkjELMAkGA1UE +BhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcMDVNhbiBGcmFuY2lzY28xDTALBgNV +BAoMBE9rdGExFDASBgNVBAsMC1NTT1Byb3ZpZGVyMRMwEQYDVQQDDApkZXYtMTE2ODA3MRwwGgYJ +KoZIhvcNAQkBFg1pbmZvQG9rdGEuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA +mtjBOZ8MmhUyi8cGk4dUY6Fj1MFDt/q3FFiaQpLzu3/q5lRVUNUBbAtqQWwY10dzfZguHOuvA5p5 +QyiVDvUhe+XkVwN2R2WfArQJRTPnIcOaHrxqQf3o5cCIG21ZtysFHJSo8clPSOe+0VsoRgcJ1aF4 +2rODwgqRRZdO9Wh3502XlJ799DJQ23IC7XasKEsGKzJqhlRrfd/FyIuZT0sFHDKRz5snSJhm9gpN +uQlCmk7ONZ1sXqtt+nBIfWIqeoYQubPW7pT5GTc7wouWq4TCjHJiK9k2HiyNxW0E3JX08swEZi2+ +LVDjgLzNc4lwjSYIj3AOtPZs8s606oBdIBni4wIDAQABMA0GCSqGSIb3DQEBBQUAA4IBAQBMxSkJ +TxkXxsoKNW0awJNpWRbU81QpheMFfENIzLam4Itc/5kSZAaSy/9e2QKfo4jBo/MMbCq2vM9TyeJQ +DJpRaioUTd2lGh4TLUxAxCxtUk/pascL+3Nn936LFmUCLxaxnbeGzPOXAhscCtU1H0nFsXRnKx5a +cPXYSKFZZZktieSkww2Oi8dg2DYaQhGQMSFMVqgVfwEu4bvCRBvdSiNXdWGCZQmFVzBZZ/9rOLzP +pvTFTPnpkavJm81FLlUhiE/oFgKlCDLWDknSpXAI0uZGERcwPca6xvIMh86LjQKjbVci9FYDStXC +qRnqQ+TccSu/B6uONFsDEngGcXSKfB+aphoebe.simon@scaleft.com124urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransportPhoebeSimonphoebe.simon@scaleft.comphoebe.simon@scaleft.com` + +const assertionInfoOneTimeUseResponse = ` +http://www.okta.com/exk5zt0r12Edi4rD20h7http://www.okta.com/exk5zt0r12Edi4rD20h7FsWGCBC+t/LaVkUKUvRQpzyZTmlxUzw4R9FOzXPPJRw=hS50WgYs/cn3uxmhrza/0/0QW3H7bwdjPZ2hQmG7IeSd7awTOghBqdrjvaPfQ7tRW+UK6ewMgIBVKG6jV3qYAWeW2U70hMb7hE9qJqBKyYyimmhVWULx1HB2YmlU1wmispywoPlXQ6gj0iWaL2RFI83vUp7X50eZ6dELqoJVZpzQI065Tt0TG7UuKUW1flYsbiS9NaXnuw+mcrBW25ZA9F5CLePHki01ZzUw+XtNmKthEb7SR30mzPoj08Dji22daYvGu82IR01wIZPoQJPCGMT6y2xC/pQPqGljAg/vUa+gaYgaMaAVYxhk/hfgMUBlOeKACBaGTmygab1Nz5KvPg==MIIDpDCCAoygAwIBAgIGAVLIBhAwMA0GCSqGSIb3DQEBBQUAMIGSMQswCQYDVQQGEwJVUzETMBEG +A1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzENMAsGA1UECgwET2t0YTEU +MBIGA1UECwwLU1NPUHJvdmlkZXIxEzARBgNVBAMMCmRldi0xMTY4MDcxHDAaBgkqhkiG9w0BCQEW +DWluZm9Ab2t0YS5jb20wHhcNMTYwMjA5MjE1MjA2WhcNMjYwMjA5MjE1MzA2WjCBkjELMAkGA1UE +BhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcMDVNhbiBGcmFuY2lzY28xDTALBgNV +BAoMBE9rdGExFDASBgNVBAsMC1NTT1Byb3ZpZGVyMRMwEQYDVQQDDApkZXYtMTE2ODA3MRwwGgYJ +KoZIhvcNAQkBFg1pbmZvQG9rdGEuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA +mtjBOZ8MmhUyi8cGk4dUY6Fj1MFDt/q3FFiaQpLzu3/q5lRVUNUBbAtqQWwY10dzfZguHOuvA5p5 +QyiVDvUhe+XkVwN2R2WfArQJRTPnIcOaHrxqQf3o5cCIG21ZtysFHJSo8clPSOe+0VsoRgcJ1aF4 +2rODwgqRRZdO9Wh3502XlJ799DJQ23IC7XasKEsGKzJqhlRrfd/FyIuZT0sFHDKRz5snSJhm9gpN +uQlCmk7ONZ1sXqtt+nBIfWIqeoYQubPW7pT5GTc7wouWq4TCjHJiK9k2HiyNxW0E3JX08swEZi2+ +LVDjgLzNc4lwjSYIj3AOtPZs8s606oBdIBni4wIDAQABMA0GCSqGSIb3DQEBBQUAA4IBAQBMxSkJ +TxkXxsoKNW0awJNpWRbU81QpheMFfENIzLam4Itc/5kSZAaSy/9e2QKfo4jBo/MMbCq2vM9TyeJQ +DJpRaioUTd2lGh4TLUxAxCxtUk/pascL+3Nn936LFmUCLxaxnbeGzPOXAhscCtU1H0nFsXRnKx5a +cPXYSKFZZZktieSkww2Oi8dg2DYaQhGQMSFMVqgVfwEu4bvCRBvdSiNXdWGCZQmFVzBZZ/9rOLzP +pvTFTPnpkavJm81FLlUhiE/oFgKlCDLWDknSpXAI0uZGERcwPca6xvIMh86LjQKjbVci9FYDStXC +qRnqQ+TccSu/B6uONFsDEngGcXSKfB+aphoebe.simon@scaleft.com123urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransportPhoebeSimonphoebe.simon@scaleft.comphoebe.simon@scaleft.com` + +const assertionInfoProxyRestrictionResponse = ` +http://www.okta.com/exk5zt0r12Edi4rD20h7http://www.okta.com/exk5zt0r12Edi4rD20h7FsWGCBC+t/LaVkUKUvRQpzyZTmlxUzw4R9FOzXPPJRw=hS50WgYs/cn3uxmhrza/0/0QW3H7bwdjPZ2hQmG7IeSd7awTOghBqdrjvaPfQ7tRW+UK6ewMgIBVKG6jV3qYAWeW2U70hMb7hE9qJqBKyYyimmhVWULx1HB2YmlU1wmispywoPlXQ6gj0iWaL2RFI83vUp7X50eZ6dELqoJVZpzQI065Tt0TG7UuKUW1flYsbiS9NaXnuw+mcrBW25ZA9F5CLePHki01ZzUw+XtNmKthEb7SR30mzPoj08Dji22daYvGu82IR01wIZPoQJPCGMT6y2xC/pQPqGljAg/vUa+gaYgaMaAVYxhk/hfgMUBlOeKACBaGTmygab1Nz5KvPg==MIIDpDCCAoygAwIBAgIGAVLIBhAwMA0GCSqGSIb3DQEBBQUAMIGSMQswCQYDVQQGEwJVUzETMBEG +A1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzENMAsGA1UECgwET2t0YTEU +MBIGA1UECwwLU1NPUHJvdmlkZXIxEzARBgNVBAMMCmRldi0xMTY4MDcxHDAaBgkqhkiG9w0BCQEW +DWluZm9Ab2t0YS5jb20wHhcNMTYwMjA5MjE1MjA2WhcNMjYwMjA5MjE1MzA2WjCBkjELMAkGA1UE +BhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcMDVNhbiBGcmFuY2lzY28xDTALBgNV +BAoMBE9rdGExFDASBgNVBAsMC1NTT1Byb3ZpZGVyMRMwEQYDVQQDDApkZXYtMTE2ODA3MRwwGgYJ +KoZIhvcNAQkBFg1pbmZvQG9rdGEuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA +mtjBOZ8MmhUyi8cGk4dUY6Fj1MFDt/q3FFiaQpLzu3/q5lRVUNUBbAtqQWwY10dzfZguHOuvA5p5 +QyiVDvUhe+XkVwN2R2WfArQJRTPnIcOaHrxqQf3o5cCIG21ZtysFHJSo8clPSOe+0VsoRgcJ1aF4 +2rODwgqRRZdO9Wh3502XlJ799DJQ23IC7XasKEsGKzJqhlRrfd/FyIuZT0sFHDKRz5snSJhm9gpN +uQlCmk7ONZ1sXqtt+nBIfWIqeoYQubPW7pT5GTc7wouWq4TCjHJiK9k2HiyNxW0E3JX08swEZi2+ +LVDjgLzNc4lwjSYIj3AOtPZs8s606oBdIBni4wIDAQABMA0GCSqGSIb3DQEBBQUAA4IBAQBMxSkJ +TxkXxsoKNW0awJNpWRbU81QpheMFfENIzLam4Itc/5kSZAaSy/9e2QKfo4jBo/MMbCq2vM9TyeJQ +DJpRaioUTd2lGh4TLUxAxCxtUk/pascL+3Nn936LFmUCLxaxnbeGzPOXAhscCtU1H0nFsXRnKx5a +cPXYSKFZZZktieSkww2Oi8dg2DYaQhGQMSFMVqgVfwEu4bvCRBvdSiNXdWGCZQmFVzBZZ/9rOLzP +pvTFTPnpkavJm81FLlUhiE/oFgKlCDLWDknSpXAI0uZGERcwPca6xvIMh86LjQKjbVci9FYDStXC +qRnqQ+TccSu/B6uONFsDEngGcXSKfB+aphoebe.simon@scaleft.com123123urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransportPhoebeSimonphoebe.simon@scaleft.comphoebe.simon@scaleft.com` + +const assertionInfoProxyRestrictionNoCountResponse = ` +http://www.okta.com/exk5zt0r12Edi4rD20h7http://www.okta.com/exk5zt0r12Edi4rD20h7FsWGCBC+t/LaVkUKUvRQpzyZTmlxUzw4R9FOzXPPJRw=hS50WgYs/cn3uxmhrza/0/0QW3H7bwdjPZ2hQmG7IeSd7awTOghBqdrjvaPfQ7tRW+UK6ewMgIBVKG6jV3qYAWeW2U70hMb7hE9qJqBKyYyimmhVWULx1HB2YmlU1wmispywoPlXQ6gj0iWaL2RFI83vUp7X50eZ6dELqoJVZpzQI065Tt0TG7UuKUW1flYsbiS9NaXnuw+mcrBW25ZA9F5CLePHki01ZzUw+XtNmKthEb7SR30mzPoj08Dji22daYvGu82IR01wIZPoQJPCGMT6y2xC/pQPqGljAg/vUa+gaYgaMaAVYxhk/hfgMUBlOeKACBaGTmygab1Nz5KvPg==MIIDpDCCAoygAwIBAgIGAVLIBhAwMA0GCSqGSIb3DQEBBQUAMIGSMQswCQYDVQQGEwJVUzETMBEG +A1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzENMAsGA1UECgwET2t0YTEU +MBIGA1UECwwLU1NPUHJvdmlkZXIxEzARBgNVBAMMCmRldi0xMTY4MDcxHDAaBgkqhkiG9w0BCQEW +DWluZm9Ab2t0YS5jb20wHhcNMTYwMjA5MjE1MjA2WhcNMjYwMjA5MjE1MzA2WjCBkjELMAkGA1UE +BhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcMDVNhbiBGcmFuY2lzY28xDTALBgNV +BAoMBE9rdGExFDASBgNVBAsMC1NTT1Byb3ZpZGVyMRMwEQYDVQQDDApkZXYtMTE2ODA3MRwwGgYJ +KoZIhvcNAQkBFg1pbmZvQG9rdGEuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA +mtjBOZ8MmhUyi8cGk4dUY6Fj1MFDt/q3FFiaQpLzu3/q5lRVUNUBbAtqQWwY10dzfZguHOuvA5p5 +QyiVDvUhe+XkVwN2R2WfArQJRTPnIcOaHrxqQf3o5cCIG21ZtysFHJSo8clPSOe+0VsoRgcJ1aF4 +2rODwgqRRZdO9Wh3502XlJ799DJQ23IC7XasKEsGKzJqhlRrfd/FyIuZT0sFHDKRz5snSJhm9gpN +uQlCmk7ONZ1sXqtt+nBIfWIqeoYQubPW7pT5GTc7wouWq4TCjHJiK9k2HiyNxW0E3JX08swEZi2+ +LVDjgLzNc4lwjSYIj3AOtPZs8s606oBdIBni4wIDAQABMA0GCSqGSIb3DQEBBQUAA4IBAQBMxSkJ +TxkXxsoKNW0awJNpWRbU81QpheMFfENIzLam4Itc/5kSZAaSy/9e2QKfo4jBo/MMbCq2vM9TyeJQ +DJpRaioUTd2lGh4TLUxAxCxtUk/pascL+3Nn936LFmUCLxaxnbeGzPOXAhscCtU1H0nFsXRnKx5a +cPXYSKFZZZktieSkww2Oi8dg2DYaQhGQMSFMVqgVfwEu4bvCRBvdSiNXdWGCZQmFVzBZZ/9rOLzP +pvTFTPnpkavJm81FLlUhiE/oFgKlCDLWDknSpXAI0uZGERcwPca6xvIMh86LjQKjbVci9FYDStXC +qRnqQ+TccSu/B6uONFsDEngGcXSKfB+aphoebe.simon@scaleft.com123123urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransportPhoebeSimonphoebe.simon@scaleft.comphoebe.simon@scaleft.com` + +const assertionInfoProxyRestrictionNoAudienceResponse = ` +http://www.okta.com/exk5zt0r12Edi4rD20h7http://www.okta.com/exk5zt0r12Edi4rD20h7FsWGCBC+t/LaVkUKUvRQpzyZTmlxUzw4R9FOzXPPJRw=hS50WgYs/cn3uxmhrza/0/0QW3H7bwdjPZ2hQmG7IeSd7awTOghBqdrjvaPfQ7tRW+UK6ewMgIBVKG6jV3qYAWeW2U70hMb7hE9qJqBKyYyimmhVWULx1HB2YmlU1wmispywoPlXQ6gj0iWaL2RFI83vUp7X50eZ6dELqoJVZpzQI065Tt0TG7UuKUW1flYsbiS9NaXnuw+mcrBW25ZA9F5CLePHki01ZzUw+XtNmKthEb7SR30mzPoj08Dji22daYvGu82IR01wIZPoQJPCGMT6y2xC/pQPqGljAg/vUa+gaYgaMaAVYxhk/hfgMUBlOeKACBaGTmygab1Nz5KvPg==MIIDpDCCAoygAwIBAgIGAVLIBhAwMA0GCSqGSIb3DQEBBQUAMIGSMQswCQYDVQQGEwJVUzETMBEG +A1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzENMAsGA1UECgwET2t0YTEU +MBIGA1UECwwLU1NPUHJvdmlkZXIxEzARBgNVBAMMCmRldi0xMTY4MDcxHDAaBgkqhkiG9w0BCQEW +DWluZm9Ab2t0YS5jb20wHhcNMTYwMjA5MjE1MjA2WhcNMjYwMjA5MjE1MzA2WjCBkjELMAkGA1UE +BhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcMDVNhbiBGcmFuY2lzY28xDTALBgNV +BAoMBE9rdGExFDASBgNVBAsMC1NTT1Byb3ZpZGVyMRMwEQYDVQQDDApkZXYtMTE2ODA3MRwwGgYJ +KoZIhvcNAQkBFg1pbmZvQG9rdGEuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA +mtjBOZ8MmhUyi8cGk4dUY6Fj1MFDt/q3FFiaQpLzu3/q5lRVUNUBbAtqQWwY10dzfZguHOuvA5p5 +QyiVDvUhe+XkVwN2R2WfArQJRTPnIcOaHrxqQf3o5cCIG21ZtysFHJSo8clPSOe+0VsoRgcJ1aF4 +2rODwgqRRZdO9Wh3502XlJ799DJQ23IC7XasKEsGKzJqhlRrfd/FyIuZT0sFHDKRz5snSJhm9gpN +uQlCmk7ONZ1sXqtt+nBIfWIqeoYQubPW7pT5GTc7wouWq4TCjHJiK9k2HiyNxW0E3JX08swEZi2+ +LVDjgLzNc4lwjSYIj3AOtPZs8s606oBdIBni4wIDAQABMA0GCSqGSIb3DQEBBQUAA4IBAQBMxSkJ +TxkXxsoKNW0awJNpWRbU81QpheMFfENIzLam4Itc/5kSZAaSy/9e2QKfo4jBo/MMbCq2vM9TyeJQ +DJpRaioUTd2lGh4TLUxAxCxtUk/pascL+3Nn936LFmUCLxaxnbeGzPOXAhscCtU1H0nFsXRnKx5a +cPXYSKFZZZktieSkww2Oi8dg2DYaQhGQMSFMVqgVfwEu4bvCRBvdSiNXdWGCZQmFVzBZZ/9rOLzP +pvTFTPnpkavJm81FLlUhiE/oFgKlCDLWDknSpXAI0uZGERcwPca6xvIMh86LjQKjbVci9FYDStXC +qRnqQ+TccSu/B6uONFsDEngGcXSKfB+aphoebe.simon@scaleft.com123urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransportPhoebeSimonphoebe.simon@scaleft.comphoebe.simon@scaleft.com` + +const exampleBase64 = `PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c2FtbDJwOlJlc3BvbnNlIHhtbG5zOnNhbWwycD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnByb3RvY29sIiBEZXN0aW5hdGlvbj0iaHR0cDovL2xvY2FsaG9zdDo4MDgwL3YxL19zYW1sX2NhbGxiYWNrIiBJRD0iaWQxMDM1MzI4MDQ2NDc3ODc5NzUzODEzMjUiIEluUmVzcG9uc2VUbz0iXzg2OTljNjU1LWM0ODItNDUxYS05YjdmLTYxNjY4ZjE0MGI0NyIgSXNzdWVJbnN0YW50PSIyMDE2LTAzLTE2VDAxOjAyOjU3LjY4MloiIFZlcnNpb249IjIuMCI+PHNhbWwyOklzc3VlciB4bWxuczpzYW1sMj0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiIgRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6bmFtZWlkLWZvcm1hdDplbnRpdHkiPmh0dHA6Ly93d3cub2t0YS5jb20vZXhrNXp0MHIxMkVkaTRyRDIwaDc8L3NhbWwyOklzc3Vlcj48ZHM6U2lnbmF0dXJlIHhtbG5zOmRzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjIj48ZHM6U2lnbmVkSW5mbz48ZHM6Q2Fub25pY2FsaXphdGlvbk1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPjxkczpTaWduYXR1cmVNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGRzaWctbW9yZSNyc2Etc2hhMjU2Ii8+PGRzOlJlZmVyZW5jZSBVUkk9IiNpZDEwMzUzMjgwNDY0Nzc4Nzk3NTM4MTMyNSI+PGRzOlRyYW5zZm9ybXM+PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNlbnZlbG9wZWQtc2lnbmF0dXJlIi8+PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPjwvZHM6VHJhbnNmb3Jtcz48ZHM6RGlnZXN0TWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8wNC94bWxlbmMjc2hhMjU2Ii8+PGRzOkRpZ2VzdFZhbHVlPm5wVEFsNmtyYWtzQmxDUmx1bmJ5RDZuSUNUY2ZzRGFIalBYVnhvRFBydzA9PC9kczpEaWdlc3RWYWx1ZT48L2RzOlJlZmVyZW5jZT48L2RzOlNpZ25lZEluZm8+PGRzOlNpZ25hdHVyZVZhbHVlPlNiQjAzZkkxVFZzdEo3cTFCNlh4OFlSR2tEcE5ROGFyNHpGM3AzYWlra2NxOFRUUzBlUjI4Rm9RdU4xSFg3MlBuMnJjY0U0T05pellOUzYvcnZybHlWL1NsWFhtQzltaFRMUlBlSno1bXJ4anFPNVFZRDFZM0l6bW5rZlE2S3V0dWtrY0dPSkVwYTN2WWVzZjVKS1JTKzBXR1J0ek9TNHdKRjE4b0dJWitiYThQNmd4bU1yeUE4eEIvZUpneHBmcm1VYkJqUEhMU2ZsamViaDg4RWlOSUQwODhYdVNHeWQrM0RtcFc1QjUyRFFCOGNBeXlPQlJrUlJjcUxGSWd4aWJtdnRJaWVxdVUwYTJuY29qcHUwKzRvamwrNHdEQ1dkR09FeXF0Sm9UUVhDNHNLUmFVNzlGSzVJRmZFaVlNcXZpRkQwb2F1NHNQajBnbkZDRUY1Rmw0dz09PC9kczpTaWduYXR1cmVWYWx1ZT48ZHM6S2V5SW5mbz48ZHM6WDUwOURhdGE+PGRzOlg1MDlDZXJ0aWZpY2F0ZT5NSUlEcERDQ0FveWdBd0lCQWdJR0FWTElCaEF3TUEwR0NTcUdTSWIzRFFFQkJRVUFNSUdTTVFzd0NRWURWUVFHRXdKVlV6RVRNQkVHCkExVUVDQXdLUTJGc2FXWnZjbTVwWVRFV01CUUdBMVVFQnd3TlUyRnVJRVp5WVc1amFYTmpiekVOTUFzR0ExVUVDZ3dFVDJ0MFlURVUKTUJJR0ExVUVDd3dMVTFOUFVISnZkbWxrWlhJeEV6QVJCZ05WQkFNTUNtUmxkaTB4TVRZNE1EY3hIREFhQmdrcWhraUc5dzBCQ1FFVwpEV2x1Wm05QWIydDBZUzVqYjIwd0hoY05NVFl3TWpBNU1qRTFNakEyV2hjTk1qWXdNakE1TWpFMU16QTJXakNCa2pFTE1Ba0dBMVVFCkJoTUNWVk14RXpBUkJnTlZCQWdNQ2tOaGJHbG1iM0p1YVdFeEZqQVVCZ05WQkFjTURWTmhiaUJHY21GdVkybHpZMjh4RFRBTEJnTlYKQkFvTUJFOXJkR0V4RkRBU0JnTlZCQXNNQzFOVFQxQnliM1pwWkdWeU1STXdFUVlEVlFRRERBcGtaWFl0TVRFMk9EQTNNUnd3R2dZSgpLb1pJaHZjTkFRa0JGZzFwYm1adlFHOXJkR0V1WTI5dE1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBCm10akJPWjhNbWhVeWk4Y0drNGRVWTZGajFNRkR0L3EzRkZpYVFwTHp1My9xNWxSVlVOVUJiQXRxUVd3WTEwZHpmWmd1SE91dkE1cDUKUXlpVkR2VWhlK1hrVndOMlIyV2ZBclFKUlRQbkljT2FIcnhxUWYzbzVjQ0lHMjFadHlzRkhKU284Y2xQU09lKzBWc29SZ2NKMWFGNAoyck9Ed2dxUlJaZE85V2gzNTAyWGxKNzk5REpRMjNJQzdYYXNLRXNHS3pKcWhsUnJmZC9GeUl1WlQwc0ZIREtSejVzblNKaG05Z3BOCnVRbENtazdPTloxc1hxdHQrbkJJZldJcWVvWVF1YlBXN3BUNUdUYzd3b3VXcTRUQ2pISmlLOWsySGl5TnhXMEUzSlgwOHN3RVppMisKTFZEamdMek5jNGx3alNZSWozQU90UFpzOHM2MDZvQmRJQm5pNHdJREFRQUJNQTBHQ1NxR1NJYjNEUUVCQlFVQUE0SUJBUUJNeFNrSgpUeGtYeHNvS05XMGF3Sk5wV1JiVTgxUXBoZU1GZkVOSXpMYW00SXRjLzVrU1pBYVN5LzllMlFLZm80akJvL01NYkNxMnZNOVR5ZUpRCkRKcFJhaW9VVGQybEdoNFRMVXhBeEN4dFVrL3Bhc2NMKzNObjkzNkxGbVVDTHhheG5iZUd6UE9YQWhzY0N0VTFIMG5Gc1hSbkt4NWEKY1BYWVNLRlpaWmt0aWVTa3d3Mk9pOGRnMkRZYVFoR1FNU0ZNVnFnVmZ3RXU0YnZDUkJ2ZFNpTlhkV0dDWlFtRlZ6QlpaLzlyT0x6UApwdlRGVFBucGthdkptODFGTGxVaGlFL29GZ0tsQ0RMV0RrblNwWEFJMHVaR0VSY3dQY2E2eHZJTWg4NkxqUUtqYlZjaTlGWURTdFhDCnFSbnFRK1RjY1N1L0I2dU9ORnNERW5nR2NYU0tmQithPC9kczpYNTA5Q2VydGlmaWNhdGU+PC9kczpYNTA5RGF0YT48L2RzOktleUluZm8+PC9kczpTaWduYXR1cmU+PHNhbWwycDpTdGF0dXMgeG1sbnM6c2FtbDJwPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6cHJvdG9jb2wiPjxzYW1sMnA6U3RhdHVzQ29kZSBWYWx1ZT0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnN0YXR1czpTdWNjZXNzIi8+PC9zYW1sMnA6U3RhdHVzPjxzYW1sMjpBc3NlcnRpb24geG1sbnM6c2FtbDI9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphc3NlcnRpb24iIElEPSJpZDEwMzUzMjgwNDY1MjY1ODg5MDAwODk0MjQiIElzc3VlSW5zdGFudD0iMjAxNi0wMy0xNlQwMTowMjo1Ny42ODJaIiBWZXJzaW9uPSIyLjAiPjxzYW1sMjpJc3N1ZXIgRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6bmFtZWlkLWZvcm1hdDplbnRpdHkiIHhtbG5zOnNhbWwyPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXNzZXJ0aW9uIj5odHRwOi8vd3d3Lm9rdGEuY29tL2V4azV6dDByMTJFZGk0ckQyMGg3PC9zYW1sMjpJc3N1ZXI+PGRzOlNpZ25hdHVyZSB4bWxuczpkcz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnIyI+PGRzOlNpZ25lZEluZm8+PGRzOkNhbm9uaWNhbGl6YXRpb25NZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzEwL3htbC1leGMtYzE0biMiLz48ZHM6U2lnbmF0dXJlTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8wNC94bWxkc2lnLW1vcmUjcnNhLXNoYTI1NiIvPjxkczpSZWZlcmVuY2UgVVJJPSIjaWQxMDM1MzI4MDQ2NTI2NTg4OTAwMDg5NDI0Ij48ZHM6VHJhbnNmb3Jtcz48ZHM6VHJhbnNmb3JtIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI2VudmVsb3BlZC1zaWduYXR1cmUiLz48ZHM6VHJhbnNmb3JtIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIi8+PC9kczpUcmFuc2Zvcm1zPjxkczpEaWdlc3RNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGVuYyNzaGEyNTYiLz48ZHM6RGlnZXN0VmFsdWU+Tm8xVnlRbGs4WGlmNEZpSitoYVZpd0VReVNJekJhMTRsR3kwY29DbjBjOD08L2RzOkRpZ2VzdFZhbHVlPjwvZHM6UmVmZXJlbmNlPjwvZHM6U2lnbmVkSW5mbz48ZHM6U2lnbmF0dXJlVmFsdWU+VlNWOFZ3NDdxN24vWFp3YVFPUFdRZUtJNVpBNjlmbkdaeUVGaGV4NHh1YUlmQytMT1luZmQ4cThxY1pzbTFNNmt2NDdIL2RSNllYUklNalBLWFpleVgvTUtjbUdQQ2FkcVdGVDdFV0Z2enVPL3V5L0FCL0NMNVpDUWlZOUgvYU9oRHlzTzhnbHNlMVMrWTJLMEN3dnNvUndNZkZpTzJYT1loVk9zbmdVU2tDQmRMSUI2T3E0Zitac0swcncvRTc5bjlRVWQ4b3dEcTNkVkMxOFNGWVlkY0lWRGhRcHBnbHl1QkVaZnUydEcwNmdEOWpsczdaRTh2amNNZkhtaHVIdHhsSDNvdk5MQjM1TkZPL1ZyQ05kRnFtRDc2R25FQTk4Zm9pSnhDWDh2ek5IRjRyUFVGWEFFZGlTNE9kUUF4YjdqTk5Wb0tWWXVhZHVuTHlneXNaR1NnPT08L2RzOlNpZ25hdHVyZVZhbHVlPjxkczpLZXlJbmZvPjxkczpYNTA5RGF0YT48ZHM6WDUwOUNlcnRpZmljYXRlPk1JSURwRENDQW95Z0F3SUJBZ0lHQVZMSUJoQXdNQTBHQ1NxR1NJYjNEUUVCQlFVQU1JR1NNUXN3Q1FZRFZRUUdFd0pWVXpFVE1CRUcKQTFVRUNBd0tRMkZzYVdadmNtNXBZVEVXTUJRR0ExVUVCd3dOVTJGdUlFWnlZVzVqYVhOamJ6RU5NQXNHQTFVRUNnd0VUMnQwWVRFVQpNQklHQTFVRUN3d0xVMU5QVUhKdmRtbGtaWEl4RXpBUkJnTlZCQU1NQ21SbGRpMHhNVFk0TURjeEhEQWFCZ2txaGtpRzl3MEJDUUVXCkRXbHVabTlBYjJ0MFlTNWpiMjB3SGhjTk1UWXdNakE1TWpFMU1qQTJXaGNOTWpZd01qQTVNakUxTXpBMldqQ0JrakVMTUFrR0ExVUUKQmhNQ1ZWTXhFekFSQmdOVkJBZ01Da05oYkdsbWIzSnVhV0V4RmpBVUJnTlZCQWNNRFZOaGJpQkdjbUZ1WTJselkyOHhEVEFMQmdOVgpCQW9NQkU5cmRHRXhGREFTQmdOVkJBc01DMU5UVDFCeWIzWnBaR1Z5TVJNd0VRWURWUVFEREFwa1pYWXRNVEUyT0RBM01Sd3dHZ1lKCktvWklodmNOQVFrQkZnMXBibVp2UUc5cmRHRXVZMjl0TUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQ0FRRUEKbXRqQk9aOE1taFV5aThjR2s0ZFVZNkZqMU1GRHQvcTNGRmlhUXBMenUzL3E1bFJWVU5VQmJBdHFRV3dZMTBkemZaZ3VIT3V2QTVwNQpReWlWRHZVaGUrWGtWd04yUjJXZkFyUUpSVFBuSWNPYUhyeHFRZjNvNWNDSUcyMVp0eXNGSEpTbzhjbFBTT2UrMFZzb1JnY0oxYUY0CjJyT0R3Z3FSUlpkTzlXaDM1MDJYbEo3OTlESlEyM0lDN1hhc0tFc0dLekpxaGxScmZkL0Z5SXVaVDBzRkhES1J6NXNuU0pobTlncE4KdVFsQ21rN09OWjFzWHF0dCtuQklmV0lxZW9ZUXViUFc3cFQ1R1RjN3dvdVdxNFRDakhKaUs5azJIaXlOeFcwRTNKWDA4c3dFWmkyKwpMVkRqZ0x6TmM0bHdqU1lJajNBT3RQWnM4czYwNm9CZElCbmk0d0lEQVFBQk1BMEdDU3FHU0liM0RRRUJCUVVBQTRJQkFRQk14U2tKClR4a1h4c29LTlcwYXdKTnBXUmJVODFRcGhlTUZmRU5JekxhbTRJdGMvNWtTWkFhU3kvOWUyUUtmbzRqQm8vTU1iQ3Eydk05VHllSlEKREpwUmFpb1VUZDJsR2g0VExVeEF4Q3h0VWsvcGFzY0wrM05uOTM2TEZtVUNMeGF4bmJlR3pQT1hBaHNjQ3RVMUgwbkZzWFJuS3g1YQpjUFhZU0tGWlpaa3RpZVNrd3cyT2k4ZGcyRFlhUWhHUU1TRk1WcWdWZndFdTRidkNSQnZkU2lOWGRXR0NaUW1GVnpCWlovOXJPTHpQCnB2VEZUUG5wa2F2Sm04MUZMbFVoaUUvb0ZnS2xDRExXRGtuU3BYQUkwdVpHRVJjd1BjYTZ4dklNaDg2TGpRS2piVmNpOUZZRFN0WEMKcVJucVErVGNjU3UvQjZ1T05Gc0RFbmdHY1hTS2ZCK2E8L2RzOlg1MDlDZXJ0aWZpY2F0ZT48L2RzOlg1MDlEYXRhPjwvZHM6S2V5SW5mbz48L2RzOlNpZ25hdHVyZT48c2FtbDI6U3ViamVjdCB4bWxuczpzYW1sMj0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiI+PHNhbWwyOk5hbWVJRCBGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjEuMTpuYW1laWQtZm9ybWF0OnVuc3BlY2lmaWVkIj5ydXNzZWxsLmhhZXJpbmdAc2NhbGVmdC5jb208L3NhbWwyOk5hbWVJRD48c2FtbDI6U3ViamVjdENvbmZpcm1hdGlvbiBNZXRob2Q9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpjbTpiZWFyZXIiPjxzYW1sMjpTdWJqZWN0Q29uZmlybWF0aW9uRGF0YSBJblJlc3BvbnNlVG89Il84Njk5YzY1NS1jNDgyLTQ1MWEtOWI3Zi02MTY2OGYxNDBiNDciIE5vdE9uT3JBZnRlcj0iMjAxNi0wMy0xNlQwMTowNzo1Ny42ODJaIiBSZWNpcGllbnQ9Imh0dHA6Ly9sb2NhbGhvc3Q6ODA4MC92MS9fc2FtbF9jYWxsYmFjayIvPjwvc2FtbDI6U3ViamVjdENvbmZpcm1hdGlvbj48L3NhbWwyOlN1YmplY3Q+PHNhbWwyOkNvbmRpdGlvbnMgTm90QmVmb3JlPSIyMDE2LTAzLTE2VDAwOjU3OjU3LjY4MloiIE5vdE9uT3JBZnRlcj0iMjAxNi0wMy0xNlQwMTowNzo1Ny42ODJaIiB4bWxuczpzYW1sMj0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiI+PHNhbWwyOkF1ZGllbmNlUmVzdHJpY3Rpb24+PHNhbWwyOkF1ZGllbmNlPjEyMzwvc2FtbDI6QXVkaWVuY2U+PC9zYW1sMjpBdWRpZW5jZVJlc3RyaWN0aW9uPjwvc2FtbDI6Q29uZGl0aW9ucz48c2FtbDI6QXV0aG5TdGF0ZW1lbnQgQXV0aG5JbnN0YW50PSIyMDE2LTAzLTE2VDAxOjAyOjU3LjY4MloiIFNlc3Npb25JbmRleD0iXzg2OTljNjU1LWM0ODItNDUxYS05YjdmLTYxNjY4ZjE0MGI0NyIgeG1sbnM6c2FtbDI9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphc3NlcnRpb24iPjxzYW1sMjpBdXRobkNvbnRleHQ+PHNhbWwyOkF1dGhuQ29udGV4dENsYXNzUmVmPnVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphYzpjbGFzc2VzOlBhc3N3b3JkUHJvdGVjdGVkVHJhbnNwb3J0PC9zYW1sMjpBdXRobkNvbnRleHRDbGFzc1JlZj48L3NhbWwyOkF1dGhuQ29udGV4dD48L3NhbWwyOkF1dGhuU3RhdGVtZW50Pjwvc2FtbDI6QXNzZXJ0aW9uPjwvc2FtbDJwOlJlc3BvbnNlPg==` + +const exampleBase64_2 = `PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c2FtbDJwOlJlc3BvbnNlIHhtbG5zOnNhbWwycD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnByb3RvY29sIiBEZXN0aW5hdGlvbj0iaHR0cDovL2xvY2FsaG9zdDo4MDgwL3YxL19zYW1sX2NhbGxiYWNrIiBJRD0iaWQyMTI4MjQ4OTI5NTEwNjcwODM0NTU5MTg1IiBJblJlc3BvbnNlVG89Il9kYTIxM2RmOC1lZjk1LTQxZDAtYjliZi03MWQyNzE3MzVjZDciIElzc3VlSW5zdGFudD0iMjAxNi0wMy0yOFQxNjozODoxOC41NjVaIiBWZXJzaW9uPSIyLjAiIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSI+PHNhbWwyOklzc3VlciB4bWxuczpzYW1sMj0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiIgRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6bmFtZWlkLWZvcm1hdDplbnRpdHkiPmh0dHA6Ly93d3cub2t0YS5jb20vZXhrNXp0MHIxMkVkaTRyRDIwaDc8L3NhbWwyOklzc3Vlcj48ZHM6U2lnbmF0dXJlIHhtbG5zOmRzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjIj48ZHM6U2lnbmVkSW5mbz48ZHM6Q2Fub25pY2FsaXphdGlvbk1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPjxkczpTaWduYXR1cmVNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGRzaWctbW9yZSNyc2Etc2hhMjU2Ii8+PGRzOlJlZmVyZW5jZSBVUkk9IiNpZDIxMjgyNDg5Mjk1MTA2NzA4MzQ1NTkxODUiPjxkczpUcmFuc2Zvcm1zPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjZW52ZWxvcGVkLXNpZ25hdHVyZSIvPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzEwL3htbC1leGMtYzE0biMiPjxlYzpJbmNsdXNpdmVOYW1lc3BhY2VzIHhtbG5zOmVjPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzEwL3htbC1leGMtYzE0biMiIFByZWZpeExpc3Q9InhzIi8+PC9kczpUcmFuc2Zvcm0+PC9kczpUcmFuc2Zvcm1zPjxkczpEaWdlc3RNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGVuYyNzaGEyNTYiLz48ZHM6RGlnZXN0VmFsdWU+V3ZnVy9KZlA0bWpVKy8xd3R5WDA2RTlFR3hZTnNvQ1UrcmJTWm5Bdmoycz08L2RzOkRpZ2VzdFZhbHVlPjwvZHM6UmVmZXJlbmNlPjwvZHM6U2lnbmVkSW5mbz48ZHM6U2lnbmF0dXJlVmFsdWU+R0ExVVJvTU9FNUVGZmtIWWltR1htN0VjcGgvbTBzMTM1VnlGOVd1dDZOU3B1WmRRMmNyTTFJc2x2S0NSamtFMDlyWmdhZ1FRTUFUaFVjT0Z1WDM1ZFpQejlKNElocHQxanVoZkd2MUFWOEk4amlPS0ZFVGo2NU1pUGFiREVpOCtQNllXZjRxTnVqQUpYSEtKSWEvTUZYQnFvS1IvaW1MUVQ4ZXUxbmhWQlFHWXFXd1plUGRkZlhPMkpZazJjZTdtdG55TVQwZFVWYitvK3RsRURZYTdyaTlmajRKTC96MVhYN3lyYlZaeG4ybWRLUEp0U1NQOHVITk9XU002ajF2cDRvSytLU0R2aUJmaVZMbFZBNThub3o1R3lGdHA2NDJoK0xWMnF1S2JuY01GZm5mQjFrZkhMSy94YXo5VWFEQnkrYkhLNG9HelNwVmhacWNPenpsaUtBPT08L2RzOlNpZ25hdHVyZVZhbHVlPjxkczpLZXlJbmZvPjxkczpYNTA5RGF0YT48ZHM6WDUwOUNlcnRpZmljYXRlPk1JSURwRENDQW95Z0F3SUJBZ0lHQVZMSUJoQXdNQTBHQ1NxR1NJYjNEUUVCQlFVQU1JR1NNUXN3Q1FZRFZRUUdFd0pWVXpFVE1CRUcKQTFVRUNBd0tRMkZzYVdadmNtNXBZVEVXTUJRR0ExVUVCd3dOVTJGdUlFWnlZVzVqYVhOamJ6RU5NQXNHQTFVRUNnd0VUMnQwWVRFVQpNQklHQTFVRUN3d0xVMU5QVUhKdmRtbGtaWEl4RXpBUkJnTlZCQU1NQ21SbGRpMHhNVFk0TURjeEhEQWFCZ2txaGtpRzl3MEJDUUVXCkRXbHVabTlBYjJ0MFlTNWpiMjB3SGhjTk1UWXdNakE1TWpFMU1qQTJXaGNOTWpZd01qQTVNakUxTXpBMldqQ0JrakVMTUFrR0ExVUUKQmhNQ1ZWTXhFekFSQmdOVkJBZ01Da05oYkdsbWIzSnVhV0V4RmpBVUJnTlZCQWNNRFZOaGJpQkdjbUZ1WTJselkyOHhEVEFMQmdOVgpCQW9NQkU5cmRHRXhGREFTQmdOVkJBc01DMU5UVDFCeWIzWnBaR1Z5TVJNd0VRWURWUVFEREFwa1pYWXRNVEUyT0RBM01Sd3dHZ1lKCktvWklodmNOQVFrQkZnMXBibVp2UUc5cmRHRXVZMjl0TUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQ0FRRUEKbXRqQk9aOE1taFV5aThjR2s0ZFVZNkZqMU1GRHQvcTNGRmlhUXBMenUzL3E1bFJWVU5VQmJBdHFRV3dZMTBkemZaZ3VIT3V2QTVwNQpReWlWRHZVaGUrWGtWd04yUjJXZkFyUUpSVFBuSWNPYUhyeHFRZjNvNWNDSUcyMVp0eXNGSEpTbzhjbFBTT2UrMFZzb1JnY0oxYUY0CjJyT0R3Z3FSUlpkTzlXaDM1MDJYbEo3OTlESlEyM0lDN1hhc0tFc0dLekpxaGxScmZkL0Z5SXVaVDBzRkhES1J6NXNuU0pobTlncE4KdVFsQ21rN09OWjFzWHF0dCtuQklmV0lxZW9ZUXViUFc3cFQ1R1RjN3dvdVdxNFRDakhKaUs5azJIaXlOeFcwRTNKWDA4c3dFWmkyKwpMVkRqZ0x6TmM0bHdqU1lJajNBT3RQWnM4czYwNm9CZElCbmk0d0lEQVFBQk1BMEdDU3FHU0liM0RRRUJCUVVBQTRJQkFRQk14U2tKClR4a1h4c29LTlcwYXdKTnBXUmJVODFRcGhlTUZmRU5JekxhbTRJdGMvNWtTWkFhU3kvOWUyUUtmbzRqQm8vTU1iQ3Eydk05VHllSlEKREpwUmFpb1VUZDJsR2g0VExVeEF4Q3h0VWsvcGFzY0wrM05uOTM2TEZtVUNMeGF4bmJlR3pQT1hBaHNjQ3RVMUgwbkZzWFJuS3g1YQpjUFhZU0tGWlpaa3RpZVNrd3cyT2k4ZGcyRFlhUWhHUU1TRk1WcWdWZndFdTRidkNSQnZkU2lOWGRXR0NaUW1GVnpCWlovOXJPTHpQCnB2VEZUUG5wa2F2Sm04MUZMbFVoaUUvb0ZnS2xDRExXRGtuU3BYQUkwdVpHRVJjd1BjYTZ4dklNaDg2TGpRS2piVmNpOUZZRFN0WEMKcVJucVErVGNjU3UvQjZ1T05Gc0RFbmdHY1hTS2ZCK2E8L2RzOlg1MDlDZXJ0aWZpY2F0ZT48L2RzOlg1MDlEYXRhPjwvZHM6S2V5SW5mbz48L2RzOlNpZ25hdHVyZT48c2FtbDJwOlN0YXR1cyB4bWxuczpzYW1sMnA9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpwcm90b2NvbCI+PHNhbWwycDpTdGF0dXNDb2RlIFZhbHVlPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6c3RhdHVzOlN1Y2Nlc3MiLz48L3NhbWwycDpTdGF0dXM+PHNhbWwyOkFzc2VydGlvbiB4bWxuczpzYW1sMj0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiIgSUQ9ImlkMjEyODI0ODkyOTU3NzY3ODIxMjY0NjgzMTkiIElzc3VlSW5zdGFudD0iMjAxNi0wMy0yOFQxNjozODoxOC41NjVaIiBWZXJzaW9uPSIyLjAiIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSI+PHNhbWwyOklzc3VlciBGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpuYW1laWQtZm9ybWF0OmVudGl0eSIgeG1sbnM6c2FtbDI9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphc3NlcnRpb24iPmh0dHA6Ly93d3cub2t0YS5jb20vZXhrNXp0MHIxMkVkaTRyRDIwaDc8L3NhbWwyOklzc3Vlcj48ZHM6U2lnbmF0dXJlIHhtbG5zOmRzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjIj48ZHM6U2lnbmVkSW5mbz48ZHM6Q2Fub25pY2FsaXphdGlvbk1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPjxkczpTaWduYXR1cmVNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzA0L3htbGRzaWctbW9yZSNyc2Etc2hhMjU2Ii8+PGRzOlJlZmVyZW5jZSBVUkk9IiNpZDIxMjgyNDg5Mjk1Nzc2NzgyMTI2NDY4MzE5Ij48ZHM6VHJhbnNmb3Jtcz48ZHM6VHJhbnNmb3JtIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI2VudmVsb3BlZC1zaWduYXR1cmUiLz48ZHM6VHJhbnNmb3JtIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIj48ZWM6SW5jbHVzaXZlTmFtZXNwYWNlcyB4bWxuczplYz0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIiBQcmVmaXhMaXN0PSJ4cyIvPjwvZHM6VHJhbnNmb3JtPjwvZHM6VHJhbnNmb3Jtcz48ZHM6RGlnZXN0TWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8wNC94bWxlbmMjc2hhMjU2Ii8+PGRzOkRpZ2VzdFZhbHVlPkZzV0dDQkMrdC9MYVZrVUtVdlJRcHp5WlRtbHhVenc0UjlGT3pYUFBKUnc9PC9kczpEaWdlc3RWYWx1ZT48L2RzOlJlZmVyZW5jZT48L2RzOlNpZ25lZEluZm8+PGRzOlNpZ25hdHVyZVZhbHVlPmhTNTBXZ1lzL2NuM3V4bWhyemEvMC8wUVczSDdid2RqUFoyaFFtRzdJZVNkN2F3VE9naEJxZHJqdmFQZlE3dFJXK1VLNmV3TWdJQlZLRzZqVjNxWUFXZVcyVTcwaE1iN2hFOXFKcUJLeVl5aW1taFZXVUx4MUhCMlltbFUxd21pc3B5d29QbFhRNmdqMGlXYUwyUkZJODN2VXA3WDUwZVo2ZEVMcW9KVlpwelFJMDY1VHQwVEc3VXVLVVcxZmxZc2JpUzlOYVhudXcrbWNyQlcyNVpBOUY1Q0xlUEhraTAxWnpVdytYdE5tS3RoRWI3U1IzMG16UG9qMDhEamkyMmRhWXZHdTgySVIwMXdJWlBvUUpQQ0dNVDZ5MnhDL3BRUHFHbGpBZy92VWErZ2FZZ2FNYUFWWXhoay9oZmdNVUJsT2VLQUNCYUdUbXlnYWIxTno1S3ZQZz09PC9kczpTaWduYXR1cmVWYWx1ZT48ZHM6S2V5SW5mbz48ZHM6WDUwOURhdGE+PGRzOlg1MDlDZXJ0aWZpY2F0ZT5NSUlEcERDQ0FveWdBd0lCQWdJR0FWTElCaEF3TUEwR0NTcUdTSWIzRFFFQkJRVUFNSUdTTVFzd0NRWURWUVFHRXdKVlV6RVRNQkVHCkExVUVDQXdLUTJGc2FXWnZjbTVwWVRFV01CUUdBMVVFQnd3TlUyRnVJRVp5WVc1amFYTmpiekVOTUFzR0ExVUVDZ3dFVDJ0MFlURVUKTUJJR0ExVUVDd3dMVTFOUFVISnZkbWxrWlhJeEV6QVJCZ05WQkFNTUNtUmxkaTB4TVRZNE1EY3hIREFhQmdrcWhraUc5dzBCQ1FFVwpEV2x1Wm05QWIydDBZUzVqYjIwd0hoY05NVFl3TWpBNU1qRTFNakEyV2hjTk1qWXdNakE1TWpFMU16QTJXakNCa2pFTE1Ba0dBMVVFCkJoTUNWVk14RXpBUkJnTlZCQWdNQ2tOaGJHbG1iM0p1YVdFeEZqQVVCZ05WQkFjTURWTmhiaUJHY21GdVkybHpZMjh4RFRBTEJnTlYKQkFvTUJFOXJkR0V4RkRBU0JnTlZCQXNNQzFOVFQxQnliM1pwWkdWeU1STXdFUVlEVlFRRERBcGtaWFl0TVRFMk9EQTNNUnd3R2dZSgpLb1pJaHZjTkFRa0JGZzFwYm1adlFHOXJkR0V1WTI5dE1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBCm10akJPWjhNbWhVeWk4Y0drNGRVWTZGajFNRkR0L3EzRkZpYVFwTHp1My9xNWxSVlVOVUJiQXRxUVd3WTEwZHpmWmd1SE91dkE1cDUKUXlpVkR2VWhlK1hrVndOMlIyV2ZBclFKUlRQbkljT2FIcnhxUWYzbzVjQ0lHMjFadHlzRkhKU284Y2xQU09lKzBWc29SZ2NKMWFGNAoyck9Ed2dxUlJaZE85V2gzNTAyWGxKNzk5REpRMjNJQzdYYXNLRXNHS3pKcWhsUnJmZC9GeUl1WlQwc0ZIREtSejVzblNKaG05Z3BOCnVRbENtazdPTloxc1hxdHQrbkJJZldJcWVvWVF1YlBXN3BUNUdUYzd3b3VXcTRUQ2pISmlLOWsySGl5TnhXMEUzSlgwOHN3RVppMisKTFZEamdMek5jNGx3alNZSWozQU90UFpzOHM2MDZvQmRJQm5pNHdJREFRQUJNQTBHQ1NxR1NJYjNEUUVCQlFVQUE0SUJBUUJNeFNrSgpUeGtYeHNvS05XMGF3Sk5wV1JiVTgxUXBoZU1GZkVOSXpMYW00SXRjLzVrU1pBYVN5LzllMlFLZm80akJvL01NYkNxMnZNOVR5ZUpRCkRKcFJhaW9VVGQybEdoNFRMVXhBeEN4dFVrL3Bhc2NMKzNObjkzNkxGbVVDTHhheG5iZUd6UE9YQWhzY0N0VTFIMG5Gc1hSbkt4NWEKY1BYWVNLRlpaWmt0aWVTa3d3Mk9pOGRnMkRZYVFoR1FNU0ZNVnFnVmZ3RXU0YnZDUkJ2ZFNpTlhkV0dDWlFtRlZ6QlpaLzlyT0x6UApwdlRGVFBucGthdkptODFGTGxVaGlFL29GZ0tsQ0RMV0RrblNwWEFJMHVaR0VSY3dQY2E2eHZJTWg4NkxqUUtqYlZjaTlGWURTdFhDCnFSbnFRK1RjY1N1L0I2dU9ORnNERW5nR2NYU0tmQithPC9kczpYNTA5Q2VydGlmaWNhdGU+PC9kczpYNTA5RGF0YT48L2RzOktleUluZm8+PC9kczpTaWduYXR1cmU+PHNhbWwyOlN1YmplY3QgeG1sbnM6c2FtbDI9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphc3NlcnRpb24iPjxzYW1sMjpOYW1lSUQgRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoxLjE6bmFtZWlkLWZvcm1hdDplbWFpbEFkZHJlc3MiPnBob2ViZS5zaW1vbkBzY2FsZWZ0LmNvbTwvc2FtbDI6TmFtZUlEPjxzYW1sMjpTdWJqZWN0Q29uZmlybWF0aW9uIE1ldGhvZD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmNtOmJlYXJlciI+PHNhbWwyOlN1YmplY3RDb25maXJtYXRpb25EYXRhIEluUmVzcG9uc2VUbz0iX2RhMjEzZGY4LWVmOTUtNDFkMC1iOWJmLTcxZDI3MTczNWNkNyIgTm90T25PckFmdGVyPSIyMDE2LTAzLTI4VDE2OjQzOjE4LjU2NVoiIFJlY2lwaWVudD0iaHR0cDovL2xvY2FsaG9zdDo4MDgwL3YxL19zYW1sX2NhbGxiYWNrIi8+PC9zYW1sMjpTdWJqZWN0Q29uZmlybWF0aW9uPjwvc2FtbDI6U3ViamVjdD48c2FtbDI6Q29uZGl0aW9ucyBOb3RCZWZvcmU9IjIwMTYtMDMtMjhUMTY6MzM6MTguNTY1WiIgTm90T25PckFmdGVyPSIyMDE2LTAzLTI4VDE2OjQzOjE4LjU2NVoiIHhtbG5zOnNhbWwyPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXNzZXJ0aW9uIj48c2FtbDI6QXVkaWVuY2VSZXN0cmljdGlvbj48c2FtbDI6QXVkaWVuY2U+MTIzPC9zYW1sMjpBdWRpZW5jZT48L3NhbWwyOkF1ZGllbmNlUmVzdHJpY3Rpb24+PC9zYW1sMjpDb25kaXRpb25zPjxzYW1sMjpBdXRoblN0YXRlbWVudCBBdXRobkluc3RhbnQ9IjIwMTYtMDMtMjhUMTY6Mzg6MTguNTY1WiIgU2Vzc2lvbkluZGV4PSJfZGEyMTNkZjgtZWY5NS00MWQwLWI5YmYtNzFkMjcxNzM1Y2Q3IiB4bWxuczpzYW1sMj0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiI+PHNhbWwyOkF1dGhuQ29udGV4dD48c2FtbDI6QXV0aG5Db250ZXh0Q2xhc3NSZWY+dXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFjOmNsYXNzZXM6UGFzc3dvcmRQcm90ZWN0ZWRUcmFuc3BvcnQ8L3NhbWwyOkF1dGhuQ29udGV4dENsYXNzUmVmPjwvc2FtbDI6QXV0aG5Db250ZXh0Pjwvc2FtbDI6QXV0aG5TdGF0ZW1lbnQ+PHNhbWwyOkF0dHJpYnV0ZVN0YXRlbWVudCB4bWxuczpzYW1sMj0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiI+PHNhbWwyOkF0dHJpYnV0ZSBOYW1lPSJGaXJzdE5hbWUiIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6dW5zcGVjaWZpZWQiPjxzYW1sMjpBdHRyaWJ1dGVWYWx1ZSB4bWxuczp4cz0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEiIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIHhzaTp0eXBlPSJ4czpzdHJpbmciPlBob2ViZTwvc2FtbDI6QXR0cmlidXRlVmFsdWU+PC9zYW1sMjpBdHRyaWJ1dGU+PHNhbWwyOkF0dHJpYnV0ZSBOYW1lPSJMYXN0TmFtZSIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDp1bnNwZWNpZmllZCI+PHNhbWwyOkF0dHJpYnV0ZVZhbHVlIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgeG1sbnM6eHNpPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYS1pbnN0YW5jZSIgeHNpOnR5cGU9InhzOnN0cmluZyI+U2ltb248L3NhbWwyOkF0dHJpYnV0ZVZhbHVlPjwvc2FtbDI6QXR0cmlidXRlPjxzYW1sMjpBdHRyaWJ1dGUgTmFtZT0iRW1haWwiIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6dW5zcGVjaWZpZWQiPjxzYW1sMjpBdHRyaWJ1dGVWYWx1ZSB4bWxuczp4cz0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEiIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIHhzaTp0eXBlPSJ4czpzdHJpbmciPnBob2ViZS5zaW1vbkBzY2FsZWZ0LmNvbTwvc2FtbDI6QXR0cmlidXRlVmFsdWU+PC9zYW1sMjpBdHRyaWJ1dGU+PHNhbWwyOkF0dHJpYnV0ZSBOYW1lPSJMb2dpbiIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDp1bnNwZWNpZmllZCI+PHNhbWwyOkF0dHJpYnV0ZVZhbHVlIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgeG1sbnM6eHNpPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYS1pbnN0YW5jZSIgeHNpOnR5cGU9InhzOnN0cmluZyI+cGhvZWJlLnNpbW9uQHNjYWxlZnQuY29tPC9zYW1sMjpBdHRyaWJ1dGVWYWx1ZT48L3NhbWwyOkF0dHJpYnV0ZT48L3NhbWwyOkF0dHJpYnV0ZVN0YXRlbWVudD48L3NhbWwyOkFzc2VydGlvbj48L3NhbWwycDpSZXNwb25zZT4=` + +const commentInjectionAttackResponse = ` +http://www.okta.com/exk5zt0r12Edi4rD20h7http://www.okta.com/exk5zt0r12Edi4rD20h7FsWGCBC+t/LaVkUKUvRQpzyZTmlxUzw4R9FOzXPPJRw=hS50WgYs/cn3uxmhrza/0/0QW3H7bwdjPZ2hQmG7IeSd7awTOghBqdrjvaPfQ7tRW+UK6ewMgIBVKG6jV3qYAWeW2U70hMb7hE9qJqBKyYyimmhVWULx1HB2YmlU1wmispywoPlXQ6gj0iWaL2RFI83vUp7X50eZ6dELqoJVZpzQI065Tt0TG7UuKUW1flYsbiS9NaXnuw+mcrBW25ZA9F5CLePHki01ZzUw+XtNmKthEb7SR30mzPoj08Dji22daYvGu82IR01wIZPoQJPCGMT6y2xC/pQPqGljAg/vUa+gaYgaMaAVYxhk/hfgMUBlOeKACBaGTmygab1Nz5KvPg==MIIDpDCCAoygAwIBAgIGAVLIBhAwMA0GCSqGSIb3DQEBBQUAMIGSMQswCQYDVQQGEwJVUzETMBEG +A1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzENMAsGA1UECgwET2t0YTEU +MBIGA1UECwwLU1NPUHJvdmlkZXIxEzARBgNVBAMMCmRldi0xMTY4MDcxHDAaBgkqhkiG9w0BCQEW +DWluZm9Ab2t0YS5jb20wHhcNMTYwMjA5MjE1MjA2WhcNMjYwMjA5MjE1MzA2WjCBkjELMAkGA1UE +BhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcMDVNhbiBGcmFuY2lzY28xDTALBgNV +BAoMBE9rdGExFDASBgNVBAsMC1NTT1Byb3ZpZGVyMRMwEQYDVQQDDApkZXYtMTE2ODA3MRwwGgYJ +KoZIhvcNAQkBFg1pbmZvQG9rdGEuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA +mtjBOZ8MmhUyi8cGk4dUY6Fj1MFDt/q3FFiaQpLzu3/q5lRVUNUBbAtqQWwY10dzfZguHOuvA5p5 +QyiVDvUhe+XkVwN2R2WfArQJRTPnIcOaHrxqQf3o5cCIG21ZtysFHJSo8clPSOe+0VsoRgcJ1aF4 +2rODwgqRRZdO9Wh3502XlJ799DJQ23IC7XasKEsGKzJqhlRrfd/FyIuZT0sFHDKRz5snSJhm9gpN +uQlCmk7ONZ1sXqtt+nBIfWIqeoYQubPW7pT5GTc7wouWq4TCjHJiK9k2HiyNxW0E3JX08swEZi2+ +LVDjgLzNc4lwjSYIj3AOtPZs8s606oBdIBni4wIDAQABMA0GCSqGSIb3DQEBBQUAA4IBAQBMxSkJ +TxkXxsoKNW0awJNpWRbU81QpheMFfENIzLam4Itc/5kSZAaSy/9e2QKfo4jBo/MMbCq2vM9TyeJQ +DJpRaioUTd2lGh4TLUxAxCxtUk/pascL+3Nn936LFmUCLxaxnbeGzPOXAhscCtU1H0nFsXRnKx5a +cPXYSKFZZZktieSkww2Oi8dg2DYaQhGQMSFMVqgVfwEu4bvCRBvdSiNXdWGCZQmFVzBZZ/9rOLzP +pvTFTPnpkavJm81FLlUhiE/oFgKlCDLWDknSpXAI0uZGERcwPca6xvIMh86LjQKjbVci9FYDStXC +qRnqQ+TccSu/B6uONFsDEngGcXSKfB+aphoebe.simon@scaleft.com.evil.com123urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransportPhoebeSimonphoebe.simon@scaleft.comphoebesimon` + + +const doubleColonAssertionInjectionAttackResponse = ` + +https://app.onelogin.com/saml/metadata/634027 + +<::Assertion xmlns="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" Version="2.0" ID="x" IssueInstant="2017-03-08T07:53:39Z">https://app.onelogin.com/saml/metadata/634027gd5V090n/m4JRrtpo5WgrwPyyy0=what@launchdarkly.com{audience}urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport +https://app.onelogin.com/saml/metadata/634027gd5V090n/m4JRrtpo5WgrwPyyy0=SLzvdNM+1R1+3XsXpC+/RIvb5L4Lhy7Eb7caPG2CLMPYhzbKLAwIiT7/0fEMO/xL7rdIgEShbcU9iu5PX4hGYBhirsFIZvdHytns5+JKHnlVBmHm4TsSU1z+dGMXBa//L0KFSrvdgBUpsr5vs50SuYnnVp61VN+zCLMqO221CQfP95QyMcSQ+fiyq4GOmWLwQy1m1+NV3U8zlapp6FIH5stW/dp4OqpRdafV96rVwmmR4yeUw7VAzbJuMrPgkXO9nUbHeMUTgQxkQ4ThzG5jt6fT+Ro1NOYS4zpVtzqlQwGzqWxQVRLEqXIf500/Qi0NuFQOW42ZAUiXDgdLENTVGA==MIIEJjCCAw6gAwIBAgIUOHrykO4ce1TbjvGgXXVVnR4NsqMwDQYJKoZIhvcNAQEFBQAwXTELMAkGA1UEBhMCVVMxFTATBgNVBAoMDExhdW5jaERhcmtseTEVMBMGA1UECwwMT25lTG9naW4gSWRQMSAwHgYDVQQDDBdPbmVMb2dpbiBBY2NvdW50IDEwMjEyNzAeFw0xNzAzMDYwMjQ2NTNaFw0yMjAzMDcwMjQ2NTNaMF0xCzAJBgNVBAYTAlVTMRUwEwYDVQQKDAxMYXVuY2hEYXJrbHkxFTATBgNVBAsMDE9uZUxvZ2luIElkUDEgMB4GA1UEAwwXT25lTG9naW4gQWNjb3VudCAxMDIxMjcwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCaJ02AnJe5vq+zzkmrIHhRy8V/UxJogbJGEJW6nqrEmO7Q4sXO7dLIKxGccCEz0KAavGKWzSX9uhVvKpazpD4bW80wPQIgFxN3CjiA3qlYIfhhh4emSZo2AnaTuG4BPVGFNPx0jxXGAhh/3xkpIsqARJFPB6njT2+MwFctm3fockx3Yp4e1xoUD8qQR0f/8oq1LjrYd2Vlckmmw7qrzSqS8POHW/I1jx9Y/vAjTPWDKXmbmLcTe3188PDrthSyoBuaAGBRVTP9WTuYMh4kGvmfX6sNvIDGejUcUCq6IObRr4xLSZiGy5uoyqsQc9agAhQm+26Gpq0R3NSvN91JdbZHAgMBAAGjgd0wgdowDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUnbxBsHgNVq3OSXEuG5EkR0Jd1UswgZoGA1UdIwSBkjCBj4AUnbxBsHgNVq3OSXEuG5EkR0Jd1UuhYaRfMF0xCzAJBgNVBAYTAlVTMRUwEwYDVQQKDAxMYXVuY2hEYXJrbHkxFTATBgNVBAsMDE9uZUxvZ2luIElkUDEgMB4GA1UEAwwXT25lTG9naW4gQWNjb3VudCAxMDIxMjeCFDh68pDuHHtU247xoF11VZ0eDbKjMA4GA1UdDwEB/wQEAwIHgDANBgkqhkiG9w0BAQUFAAOCAQEAL/6j2qpMCrnolwKT7mfPEpA6btbtl0R0t6zSwYUVU9T3PK0/P3LKXvbjSySov0E4R9d5qlOcyj5CbYiuqAO2aON3xy82s0dN3FHRiO6kcjoRPwVIIF0S8x7tpzcPKa42zSPfBqMRw4ezUEzTijFriepkSWST1Btr3QeK2Cxhr0fC1xmw/YK82BV0/oVRslGL27ro+v3/dNY0A0r32Xe2+THomrY/YaZaDCPCjHo8dlxrX3D/mPfoiiKSkm2mGagQXT0giTHVo3oIq+u+KdrBcQn65EBcjfFKDIeFCdiVmO0xPl9mmWskVRLy2/wpuDIp6hnAphl9lj5DY48eBsrEXQ==arun@launchdarkly.com{audience}urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport + +` \ No newline at end of file diff --git a/vendor/github.com/russellhaering/gosaml2/types/encrypted_assertion.go b/vendor/github.com/russellhaering/gosaml2/types/encrypted_assertion.go new file mode 100644 index 000000000..150e505e3 --- /dev/null +++ b/vendor/github.com/russellhaering/gosaml2/types/encrypted_assertion.go @@ -0,0 +1,97 @@ +// Copyright 2016 Russell Haering et al. +// +// 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 types + +import ( + "bytes" + "crypto/cipher" + "crypto/tls" + "encoding/base64" + "encoding/xml" + "fmt" +) + +type EncryptedAssertion struct { + XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:assertion EncryptedAssertion"` + EncryptionMethod EncryptionMethod `xml:"EncryptedData>EncryptionMethod"` + EncryptedKey EncryptedKey `xml:"EncryptedData>KeyInfo>EncryptedKey"` + DetEncryptedKey EncryptedKey `xml:"EncryptedKey"` // detached EncryptedKey element + CipherValue string `xml:"EncryptedData>CipherData>CipherValue"` +} + +func (ea *EncryptedAssertion) DecryptBytes(cert *tls.Certificate) ([]byte, error) { + data, err := base64.StdEncoding.DecodeString(ea.CipherValue) + if err != nil { + return nil, err + } + + // EncryptedKey must include CipherValue. EncryptedKey may be part of EncryptedData. + ek := &ea.EncryptedKey + if ek.CipherValue == "" { + // Use detached EncryptedKey element (sibling of EncryptedData). See: + // https://www.w3.org/TR/2002/REC-xmlenc-core-20021210/Overview.html#sec-Extensions-to-KeyInfo + ek = &ea.DetEncryptedKey + } + k, err := ek.DecryptSymmetricKey(cert) + if err != nil { + return nil, fmt.Errorf("cannot decrypt, error retrieving private key: %s", err) + } + + switch ea.EncryptionMethod.Algorithm { + case MethodAES128GCM, MethodAES192GCM, MethodAES256GCM: + c, err := cipher.NewGCM(k) + if err != nil { + return nil, fmt.Errorf("cannot create AES-GCM: %s", err) + } + + nonce, data := data[:c.NonceSize()], data[c.NonceSize():] + plainText, err := c.Open(nil, nonce, data, nil) + if err != nil { + return nil, fmt.Errorf("cannot open AES-GCM: %s", err) + } + return plainText, nil + case MethodAES128CBC, MethodAES256CBC, MethodTripleDESCBC: + nonce, data := data[:k.BlockSize()], data[k.BlockSize():] + c := cipher.NewCBCDecrypter(k, nonce) + c.CryptBlocks(data, data) + + // Remove zero bytes + data = bytes.TrimRight(data, "\x00") + + // Calculate index to remove based on padding + padLength := data[len(data)-1] + lastGoodIndex := len(data) - int(padLength) + return data[:lastGoodIndex], nil + default: + return nil, fmt.Errorf("unknown symmetric encryption method %#v", ea.EncryptionMethod.Algorithm) + } +} + +// Decrypt decrypts and unmarshals the EncryptedAssertion. +func (ea *EncryptedAssertion) Decrypt(cert *tls.Certificate) (*Assertion, error) { + plaintext, err := ea.DecryptBytes(cert) + if err != nil { + return nil, fmt.Errorf("Error decrypting assertion: %v", err) + } + + assertion := &Assertion{} + + err = xml.Unmarshal(plaintext, assertion) + if err != nil { + return nil, fmt.Errorf("Error unmarshaling assertion: %v", err) + } + + return assertion, nil +} diff --git a/vendor/github.com/russellhaering/gosaml2/types/encrypted_key.go b/vendor/github.com/russellhaering/gosaml2/types/encrypted_key.go new file mode 100644 index 000000000..82db74ba5 --- /dev/null +++ b/vendor/github.com/russellhaering/gosaml2/types/encrypted_key.go @@ -0,0 +1,189 @@ +// Copyright 2016 Russell Haering et al. +// +// 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 types + +import ( + "bytes" + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "crypto/rsa" + "crypto/sha1" + "crypto/sha256" + "crypto/sha512" + "crypto/tls" + "encoding/base64" + "encoding/hex" + "fmt" + "hash" + "strings" +) + +//EncryptedKey contains the decryption key data from the saml2 core and xmlenc +//standards. +type EncryptedKey struct { + // EncryptionMethod string `xml:"EncryptionMethod>Algorithm"` + X509Data string `xml:"KeyInfo>X509Data>X509Certificate"` + CipherValue string `xml:"CipherData>CipherValue"` + EncryptionMethod EncryptionMethod +} + +//EncryptionMethod specifies the type of encryption that was used. +type EncryptionMethod struct { + Algorithm string `xml:",attr,omitempty"` + //Digest method is present for algorithms like RSA-OAEP. + //See https://www.w3.org/TR/xmlenc-core1/. + //To convey the digest methods an entity supports, + //DigestMethod in extensions element is used. + //See http://docs.oasis-open.org/security/saml/Post2.0/sstc-saml-metadata-algsupport.html. + DigestMethod *DigestMethod `xml:",omitempty"` +} + +//DigestMethod is a digest type specification +type DigestMethod struct { + Algorithm string `xml:",attr,omitempty"` +} + +//Well-known public-key encryption methods +const ( + MethodRSAOAEP = "http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p" + MethodRSAOAEP2 = "http://www.w3.org/2009/xmlenc11#rsa-oaep" + MethodRSAv1_5 = "http://www.w3.org/2001/04/xmlenc#rsa-1_5" +) + +//Well-known private key encryption methods +const ( + MethodAES128GCM = "http://www.w3.org/2009/xmlenc11#aes128-gcm" + MethodAES192GCM = "http://www.w3.org/2009/xmlenc11#aes192-gcm" + MethodAES256GCM = "http://www.w3.org/2009/xmlenc11#aes256-gcm" + MethodAES128CBC = "http://www.w3.org/2001/04/xmlenc#aes128-cbc" + MethodAES256CBC = "http://www.w3.org/2001/04/xmlenc#aes256-cbc" + MethodTripleDESCBC = "http://www.w3.org/2001/04/xmlenc#tripledes-cbc" +) + +//Well-known hash methods +const ( + MethodSHA1 = "http://www.w3.org/2000/09/xmldsig#sha1" + MethodSHA256 = "http://www.w3.org/2000/09/xmldsig#sha256" + MethodSHA512 = "http://www.w3.org/2000/09/xmldsig#sha512" +) + +//SHA-1 is commonly used for certificate fingerprints (openssl -fingerprint and ADFS thumbprint). +//SHA-1 is sufficient for our purposes here (error message). +func debugKeyFp(keyBytes []byte) string { + if len(keyBytes) < 1 { + return "" + } + hashFunc := sha1.New() + hashFunc.Write(keyBytes) + sum := strings.ToLower(hex.EncodeToString(hashFunc.Sum(nil))) + var ret string + for idx := 0; idx+1 < len(sum); idx += 2 { + if idx == 0 { + ret += sum[idx : idx+2] + } else { + ret += ":" + sum[idx:idx+2] + } + } + return ret +} + +//DecryptSymmetricKey returns the private key contained in the EncryptedKey document +func (ek *EncryptedKey) DecryptSymmetricKey(cert *tls.Certificate) (cipher.Block, error) { + if len(cert.Certificate) < 1 { + return nil, fmt.Errorf("decryption tls.Certificate has no public certs attached") + } + + // The EncryptedKey may or may not include X509Data (certificate). + // If included, the EncryptedKey certificate: + // - is FYI only (fail if it does not match the SP certificate) + // - is NOT used to decrypt CipherData + if ek.X509Data != "" { + if encCert, err := base64.StdEncoding.DecodeString(ek.X509Data); err != nil { + return nil, fmt.Errorf("error decoding EncryptedKey certificate: %v", err) + } else if !bytes.Equal(cert.Certificate[0], encCert) { + return nil, fmt.Errorf("key decryption attempted with mismatched cert, SP cert(%.11s), assertion cert(%.11s)", + debugKeyFp(cert.Certificate[0]), debugKeyFp(encCert)) + } + } + + cipherText, err := base64.StdEncoding.DecodeString(ek.CipherValue) + if err != nil { + return nil, err + } + + switch pk := cert.PrivateKey.(type) { + case *rsa.PrivateKey: + var h hash.Hash + + if ek.EncryptionMethod.DigestMethod == nil { + //if digest method is not present lets set default method to SHA1. + //Digest method is used by methods like RSA-OAEP. + h = sha1.New() + } else { + switch ek.EncryptionMethod.DigestMethod.Algorithm { + case "", MethodSHA1: + h = sha1.New() // default + case MethodSHA256: + h = sha256.New() + case MethodSHA512: + h = sha512.New() + default: + return nil, fmt.Errorf("unsupported digest algorithm: %v", + ek.EncryptionMethod.DigestMethod.Algorithm) + } + } + + switch ek.EncryptionMethod.Algorithm { + case "": + return nil, fmt.Errorf("missing encryption algorithm") + case MethodRSAOAEP, MethodRSAOAEP2: + pt, err := rsa.DecryptOAEP(h, rand.Reader, pk, cipherText, nil) + if err != nil { + return nil, fmt.Errorf("rsa internal error: %v", err) + } + + b, err := aes.NewCipher(pt) + if err != nil { + return nil, err + } + + return b, nil + case MethodRSAv1_5: + pt, err := rsa.DecryptPKCS1v15(rand.Reader, pk, cipherText) + if err != nil { + return nil, fmt.Errorf("rsa internal error: %v", err) + } + + //From https://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf the xml encryption + //methods to be supported are from http://www.w3.org/2001/04/xmlenc#Element. + //https://www.w3.org/TR/2002/REC-xmlenc-core-20021210/Overview.html#Element. + //https://www.w3.org/TR/2002/REC-xmlenc-core-20021210/#sec-Algorithms + //Sec 5.4 Key Transport: + //The RSA v1.5 Key Transport algorithm given below are those used in conjunction with TRIPLEDES + //Please also see https://www.w3.org/TR/xmlenc-core/#sec-Algorithms and + //https://www.w3.org/TR/xmlenc-core/#rsav15note. + b, err := aes.NewCipher(pt) + if err != nil { + return nil, err + } + + return b, nil + default: + return nil, fmt.Errorf("unsupported encryption algorithm: %s", ek.EncryptionMethod.Algorithm) + } + } + return nil, fmt.Errorf("no cipher for decoding symmetric key") +} diff --git a/vendor/github.com/russellhaering/gosaml2/types/metadata.go b/vendor/github.com/russellhaering/gosaml2/types/metadata.go new file mode 100644 index 000000000..149f0739e --- /dev/null +++ b/vendor/github.com/russellhaering/gosaml2/types/metadata.go @@ -0,0 +1,102 @@ +// Copyright 2016 Russell Haering et al. +// +// 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 types + +import ( + "encoding/xml" + "time" + + dsigtypes "github.com/russellhaering/goxmldsig/types" +) + +type EntityDescriptor struct { + XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:metadata EntityDescriptor"` + ValidUntil time.Time `xml:"validUntil,attr"` + // SAML 2.0 8.3.6 Entity Identifier could be used to represent issuer + EntityID string `xml:"entityID,attr"` + SPSSODescriptor *SPSSODescriptor `xml:"SPSSODescriptor,omitempty"` + IDPSSODescriptor *IDPSSODescriptor `xml:"IDPSSODescriptor,omitempty"` + Extensions *Extensions `xml:"Extensions,omitempty"` +} + +type Endpoint struct { + Binding string `xml:"Binding,attr"` + Location string `xml:"Location,attr"` + ResponseLocation string `xml:"ResponseLocation,attr,omitempty"` +} + +type IndexedEndpoint struct { + Binding string `xml:"Binding,attr"` + Location string `xml:"Location,attr"` + Index int `xml:"index,attr"` +} + +type SPSSODescriptor struct { + XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:metadata SPSSODescriptor"` + AuthnRequestsSigned bool `xml:"AuthnRequestsSigned,attr"` + WantAssertionsSigned bool `xml:"WantAssertionsSigned,attr"` + ProtocolSupportEnumeration string `xml:"protocolSupportEnumeration,attr"` + KeyDescriptors []KeyDescriptor `xml:"KeyDescriptor"` + SingleLogoutServices []Endpoint `xml:"SingleLogoutService"` + NameIDFormats []string `xml:"NameIDFormat"` + AssertionConsumerServices []IndexedEndpoint `xml:"AssertionConsumerService"` + Extensions *Extensions `xml:"Extensions,omitempty"` +} + +type IDPSSODescriptor struct { + XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:metadata IDPSSODescriptor"` + WantAuthnRequestsSigned bool `xml:"WantAuthnRequestsSigned,attr"` + KeyDescriptors []KeyDescriptor `xml:"KeyDescriptor"` + NameIDFormats []NameIDFormat `xml:"NameIDFormat"` + SingleSignOnServices []SingleSignOnService `xml:"SingleSignOnService"` + SingleLogoutServices []SingleLogoutService `xml:"SingleLogoutService"` + Attributes []Attribute `xml:"Attribute"` + Extensions *Extensions `xml:"Extensions,omitempty"` +} + +type KeyDescriptor struct { + XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:metadata KeyDescriptor"` + Use string `xml:"use,attr"` + KeyInfo dsigtypes.KeyInfo `xml:"KeyInfo"` + EncryptionMethods []EncryptionMethod `xml:"EncryptionMethod"` +} + +type NameIDFormat struct { + XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:metadata NameIDFormat"` + Value string `xml:",chardata"` +} + +type SingleSignOnService struct { + XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:metadata SingleSignOnService"` + Binding string `xml:"Binding,attr"` + Location string `xml:"Location,attr"` +} + +type SingleLogoutService struct { + XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:metadata SingleLogoutService"` + Binding string `xml:"Binding,attr"` + Location string `xml:"Location,attr"` +} + +type SigningMethod struct { + Algorithm string `xml:",attr"` + MinKeySize string `xml:"MinKeySize,attr,omitempty"` + MaxKeySize string `xml:"MaxKeySize,attr,omitempty"` +} + +type Extensions struct { + DigestMethod *DigestMethod `xml:",omitempty"` + SigningMethod *SigningMethod `xml:",omitempty"` +} diff --git a/vendor/github.com/russellhaering/gosaml2/types/response.go b/vendor/github.com/russellhaering/gosaml2/types/response.go new file mode 100644 index 000000000..1e9474a07 --- /dev/null +++ b/vendor/github.com/russellhaering/gosaml2/types/response.go @@ -0,0 +1,187 @@ +// Copyright 2016 Russell Haering et al. +// +// 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 types + +import ( + "encoding/xml" + "time" +) + +// UnverifiedBaseResponse extracts several basic attributes of a SAML Response +// which may be useful in deciding how to validate the Response. An UnverifiedBaseResponse +// is parsed by this library prior to any validation of the Response, so the +// values it contains may have been supplied by an attacker and should not be +// trusted as authoritative from the IdP. +type UnverifiedBaseResponse struct { + XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:protocol Response"` + ID string `xml:"ID,attr"` + InResponseTo string `xml:"InResponseTo,attr"` + Destination string `xml:"Destination,attr"` + Version string `xml:"Version,attr"` + Issuer *Issuer `xml:"Issuer"` +} + +type Response struct { + XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:protocol Response"` + ID string `xml:"ID,attr"` + InResponseTo string `xml:"InResponseTo,attr"` + Destination string `xml:"Destination,attr"` + Version string `xml:"Version,attr"` + IssueInstant time.Time `xml:"IssueInstant,attr"` + Status *Status `xml:"Status"` + Issuer *Issuer `xml:"Issuer"` + Assertions []Assertion `xml:"Assertion"` + EncryptedAssertions []EncryptedAssertion `xml:"EncryptedAssertion"` + SignatureValidated bool `xml:"-"` // not read, not dumped +} + +type LogoutResponse struct { + XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:protocol LogoutResponse"` + ID string `xml:"ID,attr"` + InResponseTo string `xml:"InResponseTo,attr"` + Destination string `xml:"Destination,attr"` + Version string `xml:"Version,attr"` + IssueInstant time.Time `xml:"IssueInstant,attr"` + Status *Status `xml:"Status"` + Issuer *Issuer `xml:"Issuer"` + SignatureValidated bool `xml:"-"` // not read, not dumped +} + +type Status struct { + XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:protocol Status"` + StatusCode *StatusCode `xml:"StatusCode"` +} + +type StatusCode struct { + XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:protocol StatusCode"` + Value string `xml:"Value,attr"` +} + +type Issuer struct { + XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:assertion Issuer"` + Value string `xml:",chardata"` +} + +type Signature struct { + SignatureDocument []byte `xml:",innerxml"` +} + +type Assertion struct { + XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:assertion Assertion"` + Version string `xml:"Version,attr"` + ID string `xml:"ID,attr"` + IssueInstant time.Time `xml:"IssueInstant,attr"` + Issuer *Issuer `xml:"Issuer"` + Signature *Signature `xml:"Signature"` + Subject *Subject `xml:"Subject"` + Conditions *Conditions `xml:"Conditions"` + AttributeStatement *AttributeStatement `xml:"AttributeStatement"` + AuthnStatement *AuthnStatement `xml:"AuthnStatement"` + SignatureValidated bool `xml:"-"` // not read, not dumped +} + +type Subject struct { + XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:assertion Subject"` + NameID *NameID `xml:"NameID"` + SubjectConfirmation *SubjectConfirmation `xml:"SubjectConfirmation"` +} + +type AuthnContext struct { + XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:assertion AuthnContext"` + AuthnContextClassRef *AuthnContextClassRef `xml:"AuthnContextClassRef"` +} + +type AuthnContextClassRef struct { + XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:assertion AuthnContextClassRef"` + Value string `xml:",chardata"` +} + +type NameID struct { + XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:assertion NameID"` + Value string `xml:",chardata"` +} + +type SubjectConfirmation struct { + XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:assertion SubjectConfirmation"` + Method string `xml:"Method,attr"` + SubjectConfirmationData *SubjectConfirmationData `xml:"SubjectConfirmationData"` +} + +type SubjectConfirmationData struct { + XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:assertion SubjectConfirmationData"` + NotOnOrAfter string `xml:"NotOnOrAfter,attr"` + Recipient string `xml:"Recipient,attr"` + InResponseTo string `xml:"InResponseTo,attr"` +} + +type Conditions struct { + XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:assertion Conditions"` + NotBefore string `xml:"NotBefore,attr"` + NotOnOrAfter string `xml:"NotOnOrAfter,attr"` + AudienceRestrictions []AudienceRestriction `xml:"AudienceRestriction"` + OneTimeUse *OneTimeUse `xml:"OneTimeUse"` + ProxyRestriction *ProxyRestriction `xml:"ProxyRestriction"` +} + +type AudienceRestriction struct { + XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:assertion AudienceRestriction"` + Audiences []Audience `xml:"Audience"` +} + +type Audience struct { + XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:assertion Audience"` + Value string `xml:",chardata"` +} + +type OneTimeUse struct { + XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:assertion OneTimeUse"` +} + +type ProxyRestriction struct { + XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:assertion ProxyRestriction"` + Count int `xml:"Count,attr"` + Audience []Audience `xml:"Audience"` +} + +type AttributeStatement struct { + XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:assertion AttributeStatement"` + Attributes []Attribute `xml:"Attribute"` +} + +type Attribute struct { + XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:assertion Attribute"` + FriendlyName string `xml:"FriendlyName,attr"` + Name string `xml:"Name,attr"` + NameFormat string `xml:"NameFormat,attr"` + Values []AttributeValue `xml:"AttributeValue"` +} + +type AttributeValue struct { + XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:assertion AttributeValue"` + Type string `xml:"xsi:type,attr"` + Value string `xml:",chardata"` +} + +type AuthnStatement struct { + XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:assertion AuthnStatement"` + //Section 4.1.4.2 - https://docs.oasis-open.org/security/saml/v2.0/saml-profiles-2.0-os.pdf + //If the identity provider supports the Single Logout profile, defined in Section 4.4 + //, any such authentication statements MUST include a SessionIndex attribute to enable + //per-session logout requests by the service provider. + SessionIndex string `xml:"SessionIndex,attr,omitempty"` + AuthnInstant *time.Time `xml:"AuthnInstant,attr,omitempty"` + SessionNotOnOrAfter *time.Time `xml:"SessionNotOnOrAfter,attr,omitempty"` + AuthnContext *AuthnContext `xml:"AuthnContext"` +} diff --git a/vendor/github.com/russellhaering/gosaml2/uuid/uuid.go b/vendor/github.com/russellhaering/gosaml2/uuid/uuid.go new file mode 100644 index 000000000..8dcb7a5cb --- /dev/null +++ b/vendor/github.com/russellhaering/gosaml2/uuid/uuid.go @@ -0,0 +1,41 @@ +// Copyright 2016 Russell Haering et al. +// +// 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 uuid + +// relevant bits from https://github.com/abneptis/GoUUID/blob/master/uuid.go + +import ( + "crypto/rand" + "fmt" +) + +type UUID [16]byte + +// NewV4 returns random generated UUID. +func NewV4() *UUID { + u := &UUID{} + _, err := rand.Read(u[:16]) + if err != nil { + panic(err) + } + + u[8] = (u[8] | 0x80) & 0xBf + u[6] = (u[6] | 0x40) & 0x4f + return u +} + +func (u *UUID) String() string { + return fmt.Sprintf("%x-%x-%x-%x-%x", u[:4], u[4:6], u[6:8], u[8:10], u[10:]) +} diff --git a/vendor/github.com/russellhaering/gosaml2/validate.go b/vendor/github.com/russellhaering/gosaml2/validate.go new file mode 100644 index 000000000..44e9d1dcc --- /dev/null +++ b/vendor/github.com/russellhaering/gosaml2/validate.go @@ -0,0 +1,309 @@ +// Copyright 2016 Russell Haering et al. +// +// 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 saml2 + +import ( + "fmt" + "time" + + "github.com/russellhaering/gosaml2/types" +) + +//ErrParsing indicates that the value present in an assertion could not be +//parsed. It can be inspected for the specific tag name, the contents, and the +//intended type. +type ErrParsing struct { + Tag, Value, Type string +} + +func (ep ErrParsing) Error() string { + return fmt.Sprintf("Error parsing %s tag value as type %s", ep.Tag, ep.Value) +} + +//Oft-used messages +const ( + ReasonUnsupported = "Unsupported" + ReasonExpired = "Expired" +) + +//ErrInvalidValue indicates that the expected value did not match the received +//value. +type ErrInvalidValue struct { + Key, Expected, Actual string + Reason string +} + +func (e ErrInvalidValue) Error() string { + if e.Reason == "" { + e.Reason = "Unrecognized" + } + return fmt.Sprintf("%s %s value, Expected: %s, Actual: %s", e.Reason, e.Key, e.Expected, e.Actual) +} + +//Well-known methods of subject confirmation +const ( + SubjMethodBearer = "urn:oasis:names:tc:SAML:2.0:cm:bearer" +) + +//VerifyAssertionConditions inspects an assertion element and makes sure that +//all SAML2 contracts are upheld. +func (sp *SAMLServiceProvider) VerifyAssertionConditions(assertion *types.Assertion) (*WarningInfo, error) { + warningInfo := &WarningInfo{} + now := sp.Clock.Now() + + conditions := assertion.Conditions + if conditions == nil { + return nil, ErrMissingElement{Tag: ConditionsTag} + } + + if conditions.NotBefore == "" { + return nil, ErrMissingElement{Tag: ConditionsTag, Attribute: NotBeforeAttr} + } + + notBefore, err := time.Parse(time.RFC3339, conditions.NotBefore) + if err != nil { + return nil, ErrParsing{Tag: NotBeforeAttr, Value: conditions.NotBefore, Type: "time.RFC3339"} + } + + if now.Before(notBefore) { + warningInfo.InvalidTime = true + } + + if conditions.NotOnOrAfter == "" { + return nil, ErrMissingElement{Tag: ConditionsTag, Attribute: NotOnOrAfterAttr} + } + + notOnOrAfter, err := time.Parse(time.RFC3339, conditions.NotOnOrAfter) + if err != nil { + return nil, ErrParsing{Tag: NotOnOrAfterAttr, Value: conditions.NotOnOrAfter, Type: "time.RFC3339"} + } + + if now.After(notOnOrAfter) { + warningInfo.InvalidTime = true + } + + for _, audienceRestriction := range conditions.AudienceRestrictions { + matched := false + + for _, audience := range audienceRestriction.Audiences { + if audience.Value == sp.AudienceURI { + matched = true + break + } + } + + if !matched { + warningInfo.NotInAudience = true + break + } + } + + if conditions.OneTimeUse != nil { + warningInfo.OneTimeUse = true + } + + proxyRestriction := conditions.ProxyRestriction + if proxyRestriction != nil { + proxyRestrictionInfo := &ProxyRestriction{ + Count: proxyRestriction.Count, + Audience: []string{}, + } + + for _, audience := range proxyRestriction.Audience { + proxyRestrictionInfo.Audience = append(proxyRestrictionInfo.Audience, audience.Value) + } + + warningInfo.ProxyRestriction = proxyRestrictionInfo + } + + return warningInfo, nil +} + +//Validate ensures that the assertion passed is valid for the current Service +//Provider. +func (sp *SAMLServiceProvider) Validate(response *types.Response) error { + err := sp.validateResponseAttributes(response) + if err != nil { + return err + } + + if len(response.Assertions) == 0 { + return ErrMissingAssertion + } + + issuer := response.Issuer + if issuer == nil { + // FIXME?: SAML Core 2.0 Section 3.2.2 has Response.Issuer as [Optional] + return ErrMissingElement{Tag: IssuerTag} + } + + if sp.IdentityProviderIssuer != "" && response.Issuer.Value != sp.IdentityProviderIssuer { + return ErrInvalidValue{ + Key: IssuerTag, + Expected: sp.IdentityProviderIssuer, + Actual: response.Issuer.Value, + } + } + + status := response.Status + if status == nil { + return ErrMissingElement{Tag: StatusTag} + } + + statusCode := status.StatusCode + if statusCode == nil { + return ErrMissingElement{Tag: StatusCodeTag} + } + + if statusCode.Value != StatusCodeSuccess { + return ErrInvalidValue{ + Key: StatusCodeTag, + Expected: StatusCodeSuccess, + Actual: statusCode.Value, + } + } + + for _, assertion := range response.Assertions { + issuer = assertion.Issuer + if issuer == nil { + return ErrMissingElement{Tag: IssuerTag} + } + if sp.IdentityProviderIssuer != "" && assertion.Issuer.Value != sp.IdentityProviderIssuer { + return ErrInvalidValue{ + Key: IssuerTag, + Expected: sp.IdentityProviderIssuer, + Actual: issuer.Value, + } + } + + subject := assertion.Subject + if subject == nil { + return ErrMissingElement{Tag: SubjectTag} + } + + subjectConfirmation := subject.SubjectConfirmation + if subjectConfirmation == nil { + return ErrMissingElement{Tag: SubjectConfirmationTag} + } + + if subjectConfirmation.Method != SubjMethodBearer { + return ErrInvalidValue{ + Reason: ReasonUnsupported, + Key: SubjectConfirmationTag, + Expected: SubjMethodBearer, + Actual: subjectConfirmation.Method, + } + } + + subjectConfirmationData := subjectConfirmation.SubjectConfirmationData + if subjectConfirmationData == nil { + return ErrMissingElement{Tag: SubjectConfirmationDataTag} + } + + if subjectConfirmationData.Recipient != sp.AssertionConsumerServiceURL { + return ErrInvalidValue{ + Key: RecipientAttr, + Expected: sp.AssertionConsumerServiceURL, + Actual: subjectConfirmationData.Recipient, + } + } + + if subjectConfirmationData.NotOnOrAfter == "" { + return ErrMissingElement{Tag: SubjectConfirmationDataTag, Attribute: NotOnOrAfterAttr} + } + + notOnOrAfter, err := time.Parse(time.RFC3339, subjectConfirmationData.NotOnOrAfter) + if err != nil { + return ErrParsing{Tag: NotOnOrAfterAttr, Value: subjectConfirmationData.NotOnOrAfter, Type: "time.RFC3339"} + } + + now := sp.Clock.Now() + if now.After(notOnOrAfter) { + return ErrInvalidValue{ + Reason: ReasonExpired, + Key: NotOnOrAfterAttr, + Expected: now.Format(time.RFC3339), + Actual: subjectConfirmationData.NotOnOrAfter, + } + } + + } + + return nil +} + +func (sp *SAMLServiceProvider) ValidateDecodedLogoutResponse(response *types.LogoutResponse) error { + err := sp.validateLogoutResponseAttributes(response) + if err != nil { + return err + } + + issuer := response.Issuer + if issuer == nil { + // FIXME?: SAML Core 2.0 Section 3.2.2 has Response.Issuer as [Optional] + return ErrMissingElement{Tag: IssuerTag} + } + + if sp.IdentityProviderIssuer != "" && response.Issuer.Value != sp.IdentityProviderIssuer { + return ErrInvalidValue{ + Key: IssuerTag, + Expected: sp.IdentityProviderIssuer, + Actual: response.Issuer.Value, + } + } + + status := response.Status + if status == nil { + return ErrMissingElement{Tag: StatusTag} + } + + statusCode := status.StatusCode + if statusCode == nil { + return ErrMissingElement{Tag: StatusCodeTag} + } + + if statusCode.Value != StatusCodeSuccess { + return ErrInvalidValue{ + Key: StatusCodeTag, + Expected: StatusCodeSuccess, + Actual: statusCode.Value, + } + } + + return nil +} + +func (sp *SAMLServiceProvider) ValidateDecodedLogoutRequest(request *LogoutRequest) error { + err := sp.validateLogoutRequestAttributes(request) + if err != nil { + return err + } + + issuer := request.Issuer + if issuer == nil { + // FIXME?: SAML Core 2.0 Section 3.2.2 has Response.Issuer as [Optional] + return ErrMissingElement{Tag: IssuerTag} + } + + if sp.IdentityProviderIssuer != "" && request.Issuer.Value != sp.IdentityProviderIssuer { + return ErrInvalidValue{ + Key: IssuerTag, + Expected: sp.IdentityProviderIssuer, + Actual: request.Issuer.Value, + } + } + + return nil +} diff --git a/vendor/github.com/russellhaering/gosaml2/xml_constants.go b/vendor/github.com/russellhaering/gosaml2/xml_constants.go new file mode 100644 index 000000000..de36b4774 --- /dev/null +++ b/vendor/github.com/russellhaering/gosaml2/xml_constants.go @@ -0,0 +1,74 @@ +// Copyright 2016 Russell Haering et al. +// +// 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 saml2 + +const ( + ResponseTag = "Response" + AssertionTag = "Assertion" + EncryptedAssertionTag = "EncryptedAssertion" + SubjectTag = "Subject" + NameIdTag = "NameID" + SubjectConfirmationTag = "SubjectConfirmation" + SubjectConfirmationDataTag = "SubjectConfirmationData" + AttributeStatementTag = "AttributeStatement" + AttributeValueTag = "AttributeValue" + ConditionsTag = "Conditions" + AudienceRestrictionTag = "AudienceRestriction" + AudienceTag = "Audience" + OneTimeUseTag = "OneTimeUse" + ProxyRestrictionTag = "ProxyRestriction" + IssuerTag = "Issuer" + StatusTag = "Status" + StatusCodeTag = "StatusCode" +) + +const ( + DestinationAttr = "Destination" + VersionAttr = "Version" + IdAttr = "ID" + MethodAttr = "Method" + RecipientAttr = "Recipient" + NameAttr = "Name" + NotBeforeAttr = "NotBefore" + NotOnOrAfterAttr = "NotOnOrAfter" + CountAttr = "Count" +) + +const ( + NameIdFormatPersistent = "urn:oasis:names:tc:SAML:2.0:nameid-format:persistent" + NameIdFormatTransient = "urn:oasis:names:tc:SAML:2.0:nameid-format:transient" + NameIdFormatEmailAddress = "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress" + NameIdFormatUnspecified = "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified" + NameIdFormatX509SubjectName = "urn:oasis:names:tc:SAML:1.1:nameid-format:x509SubjectName" + + AuthnContextPasswordProtectedTransport = "urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport" + + AuthnPolicyMatchExact = "exact" + AuthnPolicyMatchMinimum = "minimum" + AuthnPolicyMatchMaximum = "maximum" + AuthnPolicyMatchBetter = "better" + + StatusCodeSuccess = "urn:oasis:names:tc:SAML:2.0:status:Success" + StatusCodePartialLogout = "urn:oasis:names:tc:SAML:2.0:status:PartialLogout" + StatusCodeUnknownPrincipal = "urn:oasis:names:tc:SAML:2.0:status:UnknownPrincipal" + + BindingHttpPost = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" + BindingHttpRedirect = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" +) + +const ( + SAMLAssertionNamespace = "urn:oasis:names:tc:SAML:2.0:assertion" + SAMLProtocolNamespace = "urn:oasis:names:tc:SAML:2.0:protocol" +) diff --git a/vendor/github.com/russellhaering/goxmldsig/README.md b/vendor/github.com/russellhaering/goxmldsig/README.md index 9464e61ec..c572e8c75 100644 --- a/vendor/github.com/russellhaering/goxmldsig/README.md +++ b/vendor/github.com/russellhaering/goxmldsig/README.md @@ -1,6 +1,6 @@ # goxmldsig -[![Build Status](https://travis-ci.org/russellhaering/goxmldsig.svg?branch=master)](https://travis-ci.org/russellhaering/goxmldsig) +![Build Status](https://github.com/russellhaering/goxmldsig/actions/workflows/test.yml/badge.svg?branch=main) [![GoDoc](https://godoc.org/github.com/russellhaering/goxmldsig?status.svg)](https://godoc.org/github.com/russellhaering/goxmldsig) XML Digital Signatures implemented in pure Go. @@ -15,6 +15,19 @@ $ go get github.com/russellhaering/goxmldsig ## Usage +Include the [`types.Signature`](https://pkg.go.dev/github.com/russellhaering/goxmldsig/types#Signature) struct from this package in your application messages. + +```go +import ( + sigtypes "github.com/russellhaering/goxmldsig/types" +) + +type AppHdr struct { + ... + Signature *sigtypes.Signature +} +``` + ### Signing ```go diff --git a/vendor/github.com/russellhaering/goxmldsig/canonicalize.go b/vendor/github.com/russellhaering/goxmldsig/canonicalize.go index 05655ebc4..4d51f4a08 100644 --- a/vendor/github.com/russellhaering/goxmldsig/canonicalize.go +++ b/vendor/github.com/russellhaering/goxmldsig/canonicalize.go @@ -25,12 +25,12 @@ func (c *NullCanonicalizer) Algorithm() AlgorithmID { } func (c *NullCanonicalizer) Canonicalize(el *etree.Element) ([]byte, error) { - scope := make(map[string]struct{}) - return canonicalSerialize(canonicalPrep(el, scope, false)) + return canonicalSerialize(canonicalPrep(el, false, true)) } type c14N10ExclusiveCanonicalizer struct { prefixList string + comments bool } // MakeC14N10ExclusiveCanonicalizerWithPrefixList constructs an exclusive Canonicalizer @@ -38,12 +38,22 @@ type c14N10ExclusiveCanonicalizer struct { func MakeC14N10ExclusiveCanonicalizerWithPrefixList(prefixList string) Canonicalizer { return &c14N10ExclusiveCanonicalizer{ prefixList: prefixList, + comments: false, + } +} + +// MakeC14N10ExclusiveWithCommentsCanonicalizerWithPrefixList constructs an exclusive Canonicalizer +// from a PrefixList in NMTOKENS format (a white space separated list). +func MakeC14N10ExclusiveWithCommentsCanonicalizerWithPrefixList(prefixList string) Canonicalizer { + return &c14N10ExclusiveCanonicalizer{ + prefixList: prefixList, + comments: true, } } // Canonicalize transforms the input Element into a serialized XML document in canonical form. func (c *c14N10ExclusiveCanonicalizer) Canonicalize(el *etree.Element) ([]byte, error) { - err := etreeutils.TransformExcC14n(el, c.prefixList) + err := etreeutils.TransformExcC14n(el, c.prefixList, c.comments) if err != nil { return nil, err } @@ -52,58 +62,74 @@ func (c *c14N10ExclusiveCanonicalizer) Canonicalize(el *etree.Element) ([]byte, } func (c *c14N10ExclusiveCanonicalizer) Algorithm() AlgorithmID { + if c.comments { + return CanonicalXML10ExclusiveWithCommentsAlgorithmId + } return CanonicalXML10ExclusiveAlgorithmId } -type c14N11Canonicalizer struct{} +type c14N11Canonicalizer struct { + comments bool +} // MakeC14N11Canonicalizer constructs an inclusive canonicalizer. func MakeC14N11Canonicalizer() Canonicalizer { - return &c14N11Canonicalizer{} + return &c14N11Canonicalizer{ + comments: false, + } +} + +// MakeC14N11WithCommentsCanonicalizer constructs an inclusive canonicalizer. +func MakeC14N11WithCommentsCanonicalizer() Canonicalizer { + return &c14N11Canonicalizer{ + comments: true, + } } // Canonicalize transforms the input Element into a serialized XML document in canonical form. func (c *c14N11Canonicalizer) Canonicalize(el *etree.Element) ([]byte, error) { - scope := make(map[string]struct{}) - return canonicalSerialize(canonicalPrep(el, scope, true)) + return canonicalSerialize(canonicalPrep(el, true, c.comments)) } func (c *c14N11Canonicalizer) Algorithm() AlgorithmID { + if c.comments { + return CanonicalXML11WithCommentsAlgorithmId + } return CanonicalXML11AlgorithmId } -type c14N10RecCanonicalizer struct{} +type c14N10RecCanonicalizer struct { + comments bool +} // MakeC14N10RecCanonicalizer constructs an inclusive canonicalizer. func MakeC14N10RecCanonicalizer() Canonicalizer { - return &c14N10RecCanonicalizer{} + return &c14N10RecCanonicalizer{ + comments: false, + } +} + +// MakeC14N10WithCommentsCanonicalizer constructs an inclusive canonicalizer. +func MakeC14N10WithCommentsCanonicalizer() Canonicalizer { + return &c14N10RecCanonicalizer{ + comments: true, + } } // Canonicalize transforms the input Element into a serialized XML document in canonical form. -func (c *c14N10RecCanonicalizer) Canonicalize(el *etree.Element) ([]byte, error) { - scope := make(map[string]struct{}) - return canonicalSerialize(canonicalPrep(el, scope, true)) +func (c *c14N10RecCanonicalizer) Canonicalize(inputXML *etree.Element) ([]byte, error) { + parentNamespaceAttributes, parentXmlAttributes := getParentNamespaceAndXmlAttributes(inputXML) + inputXMLCopy := inputXML.Copy() + enhanceNamespaceAttributes(inputXMLCopy, parentNamespaceAttributes, parentXmlAttributes) + return canonicalSerialize(canonicalPrep(inputXMLCopy, true, c.comments)) } func (c *c14N10RecCanonicalizer) Algorithm() AlgorithmID { + if c.comments { + return CanonicalXML10WithCommentsAlgorithmId + } return CanonicalXML10RecAlgorithmId -} - -type c14N10CommentCanonicalizer struct{} - -// MakeC14N10CommentCanonicalizer constructs an inclusive canonicalizer. -func MakeC14N10CommentCanonicalizer() Canonicalizer { - return &c14N10CommentCanonicalizer{} -} - -// Canonicalize transforms the input Element into a serialized XML document in canonical form. -func (c *c14N10CommentCanonicalizer) Canonicalize(el *etree.Element) ([]byte, error) { - scope := make(map[string]struct{}) - return canonicalSerialize(canonicalPrep(el, scope, true)) -} -func (c *c14N10CommentCanonicalizer) Algorithm() AlgorithmID { - return CanonicalXML10CommentAlgorithmId } func composeAttr(space, key string) string { @@ -132,24 +158,50 @@ const nsSpace = "xmlns" // // TODO(russell_h): This is very similar to excCanonicalPrep - perhaps they should // be unified into one parameterized function? -func canonicalPrep(el *etree.Element, seenSoFar map[string]struct{}, strip bool) *etree.Element { - _seenSoFar := make(map[string]struct{}) +func canonicalPrep(el *etree.Element, strip bool, comments bool) *etree.Element { + return canonicalPrepInner(el, make(map[string]string), strip, comments) +} + +func canonicalPrepInner(el *etree.Element, seenSoFar map[string]string, strip bool, comments bool) *etree.Element { + _seenSoFar := make(map[string]string) for k, v := range seenSoFar { _seenSoFar[k] = v } ne := el.Copy() sort.Sort(etreeutils.SortedAttrs(ne.Attr)) - if len(ne.Attr) != 0 { - for _, attr := range ne.Attr { - if attr.Space != nsSpace { - continue - } + n := 0 + for _, attr := range ne.Attr { + if attr.Space != nsSpace && !(attr.Space == "" && attr.Key == nsSpace) { + ne.Attr[n] = attr + n++ + continue + } + + if attr.Space == nsSpace { key := attr.Space + ":" + attr.Key - if _, seen := _seenSoFar[key]; seen { - ne.RemoveAttr(attr.Space + ":" + attr.Key) + if uri, seen := _seenSoFar[key]; !seen || attr.Value != uri { + ne.Attr[n] = attr + n++ + _seenSoFar[key] = attr.Value + } + } else { + if uri, seen := _seenSoFar[nsSpace]; (!seen && attr.Value != "") || attr.Value != uri { + ne.Attr[n] = attr + n++ + _seenSoFar[nsSpace] = attr.Value + } + } + } + ne.Attr = ne.Attr[:n] + + if !comments { + c := 0 + for c < len(ne.Child) { + if _, ok := ne.Child[c].(*etree.Comment); ok { + ne.RemoveChildAt(c) } else { - _seenSoFar[key] = struct{}{} + c++ } } } @@ -157,7 +209,7 @@ func canonicalPrep(el *etree.Element, seenSoFar map[string]struct{}, strip bool) for i, token := range ne.Child { childElement, ok := token.(*etree.Element) if ok { - ne.Child[i] = canonicalPrep(childElement, _seenSoFar, strip) + ne.Child[i] = canonicalPrepInner(childElement, _seenSoFar, strip, comments) } } @@ -176,3 +228,44 @@ func canonicalSerialize(el *etree.Element) ([]byte, error) { return doc.WriteToBytes() } + +func getParentNamespaceAndXmlAttributes(el *etree.Element) (map[string]string, map[string]string) { + namespaceMap := make(map[string]string, 23) + xmlMap := make(map[string]string, 5) + parents := make([]*etree.Element, 0, 23) + n1 := el.Parent() + if n1 == nil { + return namespaceMap, xmlMap + } + parent := n1 + for parent != nil { + parents = append(parents, parent) + parent = parent.Parent() + } + for i := len(parents) - 1; i > -1; i-- { + elementPos := parents[i] + for _, attr := range elementPos.Attr { + if attr.Space == "xmlns" && (attr.Key != "xml" || attr.Value != "http://www.w3.org/XML/1998/namespace") { + namespaceMap[attr.Key] = attr.Value + } else if attr.Space == "" && attr.Key == "xmlns" { + namespaceMap[attr.Key] = attr.Value + } else if attr.Space == "xml" { + xmlMap[attr.Key] = attr.Value + } + } + } + return namespaceMap, xmlMap +} + +func enhanceNamespaceAttributes(el *etree.Element, parentNamespaces map[string]string, parentXmlAttributes map[string]string) { + for prefix, uri := range parentNamespaces { + if prefix == "xmlns" { + el.CreateAttr("xmlns", uri) + } else { + el.CreateAttr("xmlns:"+prefix, uri) + } + } + for attr, value := range parentXmlAttributes { + el.CreateAttr("xml:"+attr, value) + } +} diff --git a/vendor/github.com/russellhaering/goxmldsig/etreeutils/canonicalize.go b/vendor/github.com/russellhaering/goxmldsig/etreeutils/canonicalize.go index e9f8deb18..82ceb0a2a 100644 --- a/vendor/github.com/russellhaering/goxmldsig/etreeutils/canonicalize.go +++ b/vendor/github.com/russellhaering/goxmldsig/etreeutils/canonicalize.go @@ -8,7 +8,7 @@ import ( ) // TransformExcC14n transforms the passed element into xml-exc-c14n form. -func TransformExcC14n(el *etree.Element, inclusiveNamespacesPrefixList string) error { +func TransformExcC14n(el *etree.Element, inclusiveNamespacesPrefixList string, comments bool) error { prefixes := strings.Fields(inclusiveNamespacesPrefixList) prefixSet := make(map[string]struct{}, len(prefixes)) @@ -16,7 +16,8 @@ func TransformExcC14n(el *etree.Element, inclusiveNamespacesPrefixList string) e prefixSet[prefix] = struct{}{} } - err := transformExcC14n(DefaultNSContext, DefaultNSContext, el, prefixSet) + ctx := NewDefaultNSContext() + err := transformExcC14n(ctx, ctx, el, prefixSet, comments) if err != nil { return err } @@ -24,14 +25,14 @@ func TransformExcC14n(el *etree.Element, inclusiveNamespacesPrefixList string) e return nil } -func transformExcC14n(ctx, declared NSContext, el *etree.Element, inclusiveNamespaces map[string]struct{}) error { +func transformExcC14n(ctx, declared NSContext, el *etree.Element, inclusiveNamespaces map[string]struct{}, comments bool) error { scope, err := ctx.SubContext(el) if err != nil { return err } visiblyUtilizedPrefixes := map[string]struct{}{ - el.Space: struct{}{}, + el.Space: {}, } filteredAttrs := []etree.Attr{} @@ -86,9 +87,20 @@ func transformExcC14n(ctx, declared NSContext, el *etree.Element, inclusiveNames sort.Sort(SortedAttrs(el.Attr)) + if !comments { + c := 0 + for c < len(el.Child) { + if _, ok := el.Child[c].(*etree.Comment); ok { + el.RemoveChildAt(c) + } else { + c++ + } + } + } + // Transform child elements for _, child := range el.ChildElements() { - err := transformExcC14n(scope, declared, child, inclusiveNamespaces) + err := transformExcC14n(scope, declared, child, inclusiveNamespaces, comments) if err != nil { return err } diff --git a/vendor/github.com/russellhaering/goxmldsig/etreeutils/namespace.go b/vendor/github.com/russellhaering/goxmldsig/etreeutils/namespace.go index baf1124f6..6a5be0363 100644 --- a/vendor/github.com/russellhaering/goxmldsig/etreeutils/namespace.go +++ b/vendor/github.com/russellhaering/goxmldsig/etreeutils/namespace.go @@ -2,9 +2,7 @@ package etreeutils import ( "errors" - "fmt" - "sort" "github.com/beevik/etree" @@ -19,20 +17,25 @@ const ( XMLNSNamespace = "http://www.w3.org/2000/xmlns/" ) -var ( - DefaultNSContext = NSContext{ +func NewDefaultNSContext() NSContext { + defaultLimit := 1000 + return NSContext{ prefixes: map[string]string{ defaultPrefix: XMLNamespace, xmlPrefix: XMLNamespace, xmlnsPrefix: XMLNSNamespace, }, + limit: &defaultLimit, } +} +var ( EmptyNSContext = NSContext{} ErrReservedNamespace = errors.New("disallowed declaration of reserved namespace") ErrInvalidDefaultNamespace = errors.New("invalid default namespace declaration") ErrTraversalHalted = errors.New("traversal halted") + ErrTraversalLimit = errors.New("traversal limit reached") ) type ErrUndeclaredNSPrefix struct { @@ -45,6 +48,17 @@ func (e ErrUndeclaredNSPrefix) Error() string { type NSContext struct { prefixes map[string]string + limit *int +} + +// CheckLimit checks the traversal limit before calling the handler function +func (ctx NSContext) CheckLimit() error { + if *ctx.limit <= 0 { + return ErrTraversalLimit + } + *ctx.limit-- + + return nil } func (ctx NSContext) Copy() NSContext { @@ -53,7 +67,7 @@ func (ctx NSContext) Copy() NSContext { prefixes[k] = v } - return NSContext{prefixes: prefixes} + return NSContext{prefixes: prefixes, limit: ctx.limit} } func (ctx NSContext) declare(prefix, namespace string) etree.Attr { @@ -140,7 +154,12 @@ type NSIterHandler func(NSContext, *etree.Element) error // NSTraverse traverses an element tree, invoking the passed handler for each element // in the tree. func NSTraverse(ctx NSContext, el *etree.Element, handle NSIterHandler) error { - ctx, err := ctx.SubContext(el) + err := ctx.CheckLimit() + if err != nil { + return err + } + + ctx, err = ctx.SubContext(el) if err != nil { return err } @@ -223,7 +242,7 @@ func NSDetatch(ctx NSContext, el *etree.Element) (*etree.Element, error) { // NSSelectOne behaves identically to NSSelectOneCtx, but uses DefaultNSContext as the // surrounding context. func NSSelectOne(el *etree.Element, namespace, tag string) (*etree.Element, error) { - return NSSelectOneCtx(DefaultNSContext, el, namespace, tag) + return NSSelectOneCtx(NewDefaultNSContext(), el, namespace, tag) } // NSSelectOneCtx conducts a depth-first search for an element with the specified namespace @@ -243,7 +262,6 @@ func NSSelectOneCtx(ctx NSContext, el *etree.Element, namespace, tag string) (*e return ErrTraversalHalted }) - if err != nil { return nil, err } @@ -254,7 +272,7 @@ func NSSelectOneCtx(ctx NSContext, el *etree.Element, namespace, tag string) (*e // NSFindIterate behaves identically to NSFindIterateCtx, but uses DefaultNSContext // as the surrounding context. func NSFindIterate(el *etree.Element, namespace, tag string, handle NSIterHandler) error { - return NSFindIterateCtx(DefaultNSContext, el, namespace, tag, handle) + return NSFindIterateCtx(NewDefaultNSContext(), el, namespace, tag, handle) } // NSFindIterateCtx conducts a depth-first traversal searching for elements with the @@ -294,7 +312,7 @@ func NSFindIterateCtx(ctx NSContext, el *etree.Element, namespace, tag string, h // NSFindOne behaves identically to NSFindOneCtx, but uses DefaultNSContext for // context. func NSFindOne(el *etree.Element, namespace, tag string) (*etree.Element, error) { - return NSFindOneCtx(DefaultNSContext, el, namespace, tag) + return NSFindOneCtx(NewDefaultNSContext(), el, namespace, tag) } // NSFindOneCtx conducts a depth-first search for the specified element. If such an element @@ -306,7 +324,6 @@ func NSFindOneCtx(ctx NSContext, el *etree.Element, namespace, tag string) (*etr found = el return ErrTraversalHalted }) - if err != nil { return nil, err } @@ -325,6 +342,11 @@ func NSIterateChildren(ctx NSContext, el *etree.Element, handle NSIterHandler) e // Iterate the child elements. for _, child := range el.ChildElements() { + err := ctx.CheckLimit() + if err != nil { + return err + } + err = handle(ctx, child) if err != nil { return err @@ -368,7 +390,7 @@ func NSFindChildrenIterateCtx(ctx NSContext, el *etree.Element, namespace, tag s // NSFindOneChild behaves identically to NSFindOneChildCtx, but uses // DefaultNSContext for context. func NSFindOneChild(el *etree.Element, namespace, tag string) (*etree.Element, error) { - return NSFindOneChildCtx(DefaultNSContext, el, namespace, tag) + return NSFindOneChildCtx(NewDefaultNSContext(), el, namespace, tag) } // NSFindOneCtx conducts a depth-first search for the specified element. If such an @@ -394,11 +416,10 @@ func NSFindOneChildCtx(ctx NSContext, el *etree.Element, namespace, tag string) func NSBuildParentContext(el *etree.Element) (NSContext, error) { parent := el.Parent() if parent == nil { - return DefaultNSContext, nil + return NewDefaultNSContext(), nil } ctx, err := NSBuildParentContext(parent) - if err != nil { return ctx, err } diff --git a/vendor/github.com/russellhaering/goxmldsig/etreeutils/sort.go b/vendor/github.com/russellhaering/goxmldsig/etreeutils/sort.go index 5871a3913..1dc62829d 100644 --- a/vendor/github.com/russellhaering/goxmldsig/etreeutils/sort.go +++ b/vendor/github.com/russellhaering/goxmldsig/etreeutils/sort.go @@ -55,12 +55,29 @@ func (a SortedAttrs) Less(i, j int) bool { return false } - // Wow. We're still going. Finally, attributes in the same namespace should be - // sorted by key. Attributes in different namespaces should be sorted by the - // actual namespace (_not_ the prefix). For now just use the prefix. + // Attributes with the same prefix should be sorted by their keys. if a[i].Space == a[j].Space { return a[i].Key < a[j].Key } - return a[i].Space < a[j].Space + // Attributes in the same namespace are sorted by their Namespace URI, not the prefix. + // NOTE: This implementation is not complete because it does not consider namespace + // prefixes declared in ancestor elements. A complete solution would ideally use the + // Attribute.NamespaceURI() method obtain a namespace URI for sorting, but the + // beevik/etree library needs to be fixed to provide the correct value first. + if a[i].Key == a[j].Key { + var leftNS, rightNS etree.Attr + for n := range a { + if a[i].Space == a[n].Key { + leftNS = a[n] + } + if a[j].Space == a[n].Key { + rightNS = a[n] + } + } + // Sort based on the NS URIs + return leftNS.Value < rightNS.Value + } + + return a[i].Key < a[j].Key } diff --git a/vendor/github.com/russellhaering/goxmldsig/sign.go b/vendor/github.com/russellhaering/goxmldsig/sign.go index 2be34b7f6..cc77f790a 100644 --- a/vendor/github.com/russellhaering/goxmldsig/sign.go +++ b/vendor/github.com/russellhaering/goxmldsig/sign.go @@ -2,10 +2,12 @@ package dsig import ( "crypto" + "crypto/ecdsa" "crypto/rand" "crypto/rsa" _ "crypto/sha1" _ "crypto/sha256" + "crypto/x509" "encoding/base64" "errors" "fmt" @@ -15,11 +17,18 @@ import ( ) type SigningContext struct { - Hash crypto.Hash + Hash crypto.Hash + + // This field will be nil and unused if the SigningContext is created with + // NewSigningContext KeyStore X509KeyStore IdAttribute string Prefix string Canonicalizer Canonicalizer + + // KeyStore is mutually exclusive with signer and certs + signer crypto.Signer + certs [][]byte } func NewDefaultSigningContext(ks X509KeyStore) *SigningContext { @@ -32,13 +41,54 @@ func NewDefaultSigningContext(ks X509KeyStore) *SigningContext { } } +// NewSigningContext creates a new signing context with the given signer and certificate chain. +// Note that e.g. rsa.PrivateKey implements the crypto.Signer interface. +// The certificate chain is a slice of ASN.1 DER-encoded X.509 certificates. +// A SigningContext created with this function should not use the KeyStore field. +// It will return error if passed a nil crypto.Signer +func NewSigningContext(signer crypto.Signer, certs [][]byte) (*SigningContext, error) { + if signer == nil { + return nil, errors.New("signer cannot be nil for NewSigningContext") + } + ctx := &SigningContext{ + Hash: crypto.SHA256, + IdAttribute: DefaultIdAttr, + Prefix: DefaultPrefix, + Canonicalizer: MakeC14N11Canonicalizer(), + + signer: signer, + certs: certs, + } + return ctx, nil +} + +func (ctx *SigningContext) getPublicKeyAlgorithm() x509.PublicKeyAlgorithm { + if ctx.KeyStore != nil { + return x509.RSA + } else { + switch ctx.signer.Public().(type) { + case *ecdsa.PublicKey: + return x509.ECDSA + case *rsa.PublicKey: + return x509.RSA + } + } + + return x509.UnknownPublicKeyAlgorithm +} + func (ctx *SigningContext) SetSignatureMethod(algorithmID string) error { - hash, ok := signatureMethodsByIdentifier[algorithmID] + info, ok := signatureMethodsByIdentifier[algorithmID] if !ok { - return fmt.Errorf("Unknown SignatureMethod: %s", algorithmID) + return fmt.Errorf("unknown SignatureMethod: %s", algorithmID) } - ctx.Hash = hash + algo := ctx.getPublicKeyAlgorithm() + if info.PublicKeyAlgorithm != algo { + return fmt.Errorf("SignatureMethod %s is incompatible with %s key", algorithmID, algo) + } + + ctx.Hash = info.Hash return nil } @@ -58,6 +108,46 @@ func (ctx *SigningContext) digest(el *etree.Element) ([]byte, error) { return hash.Sum(nil), nil } +func (ctx *SigningContext) signDigest(digest []byte) ([]byte, error) { + if ctx.KeyStore != nil { + key, _, err := ctx.KeyStore.GetKeyPair() + if err != nil { + return nil, err + } + + rawSignature, err := rsa.SignPKCS1v15(rand.Reader, key, ctx.Hash, digest) + if err != nil { + return nil, err + } + + return rawSignature, nil + } else { + rawSignature, err := ctx.signer.Sign(rand.Reader, digest, ctx.Hash) + if err != nil { + return nil, err + } + + return rawSignature, nil + } +} + +func (ctx *SigningContext) getCerts() ([][]byte, error) { + if ctx.KeyStore != nil { + if cs, ok := ctx.KeyStore.(X509ChainStore); ok { + return cs.GetChain() + } + + _, cert, err := ctx.KeyStore.GetKeyPair() + if err != nil { + return nil, err + } + + return [][]byte{cert}, nil + } else { + return ctx.certs, nil + } +} + func (ctx *SigningContext) constructSignedInfo(el *etree.Element, enveloped bool) (*etree.Element, error) { digestAlgorithmIdentifier := ctx.GetDigestAlgorithmIdentifier() if digestAlgorithmIdentifier == "" { @@ -97,7 +187,6 @@ func (ctx *SigningContext) constructSignedInfo(el *etree.Element, enveloped bool reference.CreateAttr(URIAttr, "#"+dataId) } - // /SignedInfo/Reference/Transforms transforms := ctx.createNamespacedElement(reference, TransformsTag) if enveloped { @@ -172,20 +261,12 @@ func (ctx *SigningContext) ConstructSignature(el *etree.Element, enveloped bool) return nil, err } - key, cert, err := ctx.KeyStore.GetKeyPair() + rawSignature, err := ctx.signDigest(digest) if err != nil { return nil, err } - certs := [][]byte{cert} - if cs, ok := ctx.KeyStore.(X509ChainStore); ok { - certs, err = cs.GetChain() - if err != nil { - return nil, err - } - } - - rawSignature, err := rsa.SignPKCS1v15(rand.Reader, key, ctx.Hash, digest) + certs, err := ctx.getCerts() if err != nil { return nil, err } @@ -222,7 +303,9 @@ func (ctx *SigningContext) SignEnveloped(el *etree.Element) (*etree.Element, err } func (ctx *SigningContext) GetSignatureMethodIdentifier() string { - if ident, ok := signatureMethodIdentifiers[ctx.Hash]; ok { + algo := ctx.getPublicKeyAlgorithm() + + if ident, ok := signatureMethodIdentifiers[algo][ctx.Hash]; ok { return ident } return "" @@ -247,11 +330,5 @@ func (ctx *SigningContext) SignString(content string) ([]byte, error) { } digest := hash.Sum(nil) - var signature []byte - if key, _, err := ctx.KeyStore.GetKeyPair(); err != nil { - return nil, fmt.Errorf("unable to fetch key for signing: %v", err) - } else if signature, err = rsa.SignPKCS1v15(rand.Reader, key, ctx.Hash, digest); err != nil { - return nil, fmt.Errorf("error signing: %v", err) - } - return signature, nil + return ctx.signDigest(digest) } diff --git a/vendor/github.com/russellhaering/goxmldsig/validate.go b/vendor/github.com/russellhaering/goxmldsig/validate.go index 840458588..3d0fc9739 100644 --- a/vendor/github.com/russellhaering/goxmldsig/validate.go +++ b/vendor/github.com/russellhaering/goxmldsig/validate.go @@ -2,7 +2,6 @@ package dsig import ( "bytes" - "crypto/rsa" "crypto/x509" "encoding/base64" "errors" @@ -21,7 +20,7 @@ var ( // ErrMissingSignature indicates that no enveloped signature was found referencing // the top level element passed for signature verification. ErrMissingSignature = errors.New("Missing signature referencing the top-level element") - ErrInvalidSignature = errors.New( "Invalid Signature") + ErrInvalidSignature = errors.New("Invalid Signature") ) type ValidationContext struct { @@ -70,7 +69,7 @@ func mapPathToElement(tree, el *etree.Element) []int { for i, child := range tree.Child { if childElement, ok := child.(*etree.Element); ok { childPath := mapPathToElement(childElement, el) - if childElement != nil { + if childPath != nil { return append([]int{i}, childPath...) } } @@ -138,14 +137,25 @@ func (ctx *ValidationContext) transform( canonicalizer = MakeC14N10ExclusiveCanonicalizerWithPrefixList(prefixList) + case CanonicalXML10ExclusiveWithCommentsAlgorithmId: + var prefixList string + if transform.InclusiveNamespaces != nil { + prefixList = transform.InclusiveNamespaces.PrefixList + } + + canonicalizer = MakeC14N10ExclusiveWithCommentsCanonicalizerWithPrefixList(prefixList) + case CanonicalXML11AlgorithmId: canonicalizer = MakeC14N11Canonicalizer() + case CanonicalXML11WithCommentsAlgorithmId: + canonicalizer = MakeC14N11WithCommentsCanonicalizer() + case CanonicalXML10RecAlgorithmId: canonicalizer = MakeC14N10RecCanonicalizer() - case CanonicalXML10CommentAlgorithmId: - canonicalizer = MakeC14N10CommentCanonicalizer() + case CanonicalXML10WithCommentsAlgorithmId: + canonicalizer = MakeC14N10WithCommentsCanonicalizer() default: return nil, nil, errors.New("Unknown Transform Algorithm: " + algo) @@ -202,26 +212,12 @@ func (ctx *ValidationContext) verifySignedInfo(sig *types.Signature, canonicaliz return err } - signatureAlgorithm, ok := signatureMethodsByIdentifier[signatureMethodId] + algo, ok := x509SignatureAlgorithmByIdentifier[signatureMethodId] if !ok { return errors.New("Unknown signature method: " + signatureMethodId) } - hash := signatureAlgorithm.New() - _, err = hash.Write(canonical) - if err != nil { - return err - } - - hashed := hash.Sum(nil) - - pubKey, ok := cert.PublicKey.(*rsa.PublicKey) - if !ok { - return errors.New("Invalid public key") - } - - // Verify that the private key matching the public key from the cert was what was used to sign the 'SignedInfo' and produce the 'SignatureValue' - err = rsa.VerifyPKCS1v15(pubKey, signatureAlgorithm, hashed[:], decodedSignature) + err = cert.CheckSignature(algo, canonical, decodedSignature) if err != nil { return err } @@ -353,9 +349,9 @@ func (ctx *ValidationContext) findSignature(root *etree.Element) (*types.Signatu var canonicalSignedInfo *etree.Element - switch AlgorithmID(c14NAlgorithm) { - case CanonicalXML10ExclusiveAlgorithmId: - err := etreeutils.TransformExcC14n(detachedSignedInfo, "") + switch alg := AlgorithmID(c14NAlgorithm); alg { + case CanonicalXML10ExclusiveAlgorithmId, CanonicalXML10ExclusiveWithCommentsAlgorithmId: + err := etreeutils.TransformExcC14n(detachedSignedInfo, "", alg == CanonicalXML10ExclusiveWithCommentsAlgorithmId) if err != nil { return err } @@ -366,21 +362,18 @@ func (ctx *ValidationContext) findSignature(root *etree.Element) (*types.Signatu // removing of elements below. canonicalSignedInfo = detachedSignedInfo - case CanonicalXML11AlgorithmId: - canonicalSignedInfo = canonicalPrep(detachedSignedInfo, map[string]struct{}{}, true) - - case CanonicalXML10RecAlgorithmId: - canonicalSignedInfo = canonicalPrep(detachedSignedInfo, map[string]struct{}{}, true) + case CanonicalXML11AlgorithmId, CanonicalXML10RecAlgorithmId: + canonicalSignedInfo = canonicalPrep(detachedSignedInfo, true, false) - case CanonicalXML10CommentAlgorithmId: - canonicalSignedInfo = canonicalPrep(detachedSignedInfo, map[string]struct{}{}, true) + case CanonicalXML11WithCommentsAlgorithmId, CanonicalXML10WithCommentsAlgorithmId: + canonicalSignedInfo = canonicalPrep(detachedSignedInfo, true, true) default: return fmt.Errorf("invalid CanonicalizationMethod on Signature: %s", c14NAlgorithm) } + signatureEl.InsertChildAt(signedInfo.Index(), canonicalSignedInfo) signatureEl.RemoveChild(signedInfo) - signatureEl.AddChild(canonicalSignedInfo) found = true diff --git a/vendor/github.com/russellhaering/goxmldsig/xml_constants.go b/vendor/github.com/russellhaering/goxmldsig/xml_constants.go index c4b815b29..0526062e9 100644 --- a/vendor/github.com/russellhaering/goxmldsig/xml_constants.go +++ b/vendor/github.com/russellhaering/goxmldsig/xml_constants.go @@ -1,6 +1,9 @@ package dsig -import "crypto" +import ( + "crypto" + "crypto/x509" +) const ( DefaultPrefix = "ds" @@ -39,19 +42,27 @@ func (id AlgorithmID) String() string { } const ( - RSASHA1SignatureMethod = "http://www.w3.org/2000/09/xmldsig#rsa-sha1" - RSASHA256SignatureMethod = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256" - RSASHA512SignatureMethod = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha512" + RSASHA1SignatureMethod = "http://www.w3.org/2000/09/xmldsig#rsa-sha1" + RSASHA256SignatureMethod = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256" + RSASHA384SignatureMethod = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha384" + RSASHA512SignatureMethod = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha512" + ECDSASHA1SignatureMethod = "http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha1" + ECDSASHA256SignatureMethod = "http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256" + ECDSASHA384SignatureMethod = "http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha384" + ECDSASHA512SignatureMethod = "http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha512" ) -//Well-known signature algorithms +// Well-known signature algorithms const ( // Supported canonicalization algorithms - CanonicalXML10ExclusiveAlgorithmId AlgorithmID = "http://www.w3.org/2001/10/xml-exc-c14n#" - CanonicalXML11AlgorithmId AlgorithmID = "http://www.w3.org/2006/12/xml-c14n11" + CanonicalXML10ExclusiveAlgorithmId AlgorithmID = "http://www.w3.org/2001/10/xml-exc-c14n#" + CanonicalXML10ExclusiveWithCommentsAlgorithmId AlgorithmID = "http://www.w3.org/2001/10/xml-exc-c14n#WithComments" + + CanonicalXML11AlgorithmId AlgorithmID = "http://www.w3.org/2006/12/xml-c14n11" + CanonicalXML11WithCommentsAlgorithmId AlgorithmID = "http://www.w3.org/2006/12/xml-c14n11#WithComments" - CanonicalXML10RecAlgorithmId AlgorithmID = "http://www.w3.org/TR/2001/REC-xml-c14n-20010315" - CanonicalXML10CommentAlgorithmId AlgorithmID = "http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments" + CanonicalXML10RecAlgorithmId AlgorithmID = "http://www.w3.org/TR/2001/REC-xml-c14n-20010315" + CanonicalXML10WithCommentsAlgorithmId AlgorithmID = "http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments" EnvelopedSignatureAltorithmId AlgorithmID = "http://www.w3.org/2000/09/xmldsig#enveloped-signature" ) @@ -59,23 +70,54 @@ const ( var digestAlgorithmIdentifiers = map[crypto.Hash]string{ crypto.SHA1: "http://www.w3.org/2000/09/xmldsig#sha1", crypto.SHA256: "http://www.w3.org/2001/04/xmlenc#sha256", + crypto.SHA384: "http://www.w3.org/2001/04/xmldsig-more#sha384", crypto.SHA512: "http://www.w3.org/2001/04/xmlenc#sha512", } +type signatureMethodInfo struct { + PublicKeyAlgorithm x509.PublicKeyAlgorithm + Hash crypto.Hash +} + var digestAlgorithmsByIdentifier = map[string]crypto.Hash{} -var signatureMethodsByIdentifier = map[string]crypto.Hash{} +var signatureMethodsByIdentifier = map[string]signatureMethodInfo{} func init() { for hash, id := range digestAlgorithmIdentifiers { digestAlgorithmsByIdentifier[id] = hash } - for hash, id := range signatureMethodIdentifiers { - signatureMethodsByIdentifier[id] = hash + for algo, hashToMethod := range signatureMethodIdentifiers { + for hash, method := range hashToMethod { + signatureMethodsByIdentifier[method] = signatureMethodInfo{ + PublicKeyAlgorithm: algo, + Hash: hash, + } + } } } -var signatureMethodIdentifiers = map[crypto.Hash]string{ - crypto.SHA1: RSASHA1SignatureMethod, - crypto.SHA256: RSASHA256SignatureMethod, - crypto.SHA512: RSASHA512SignatureMethod, +var signatureMethodIdentifiers = map[x509.PublicKeyAlgorithm]map[crypto.Hash]string{ + x509.RSA: { + crypto.SHA1: RSASHA1SignatureMethod, + crypto.SHA256: RSASHA256SignatureMethod, + crypto.SHA384: RSASHA384SignatureMethod, + crypto.SHA512: RSASHA512SignatureMethod, + }, + x509.ECDSA: { + crypto.SHA1: ECDSASHA1SignatureMethod, + crypto.SHA256: ECDSASHA256SignatureMethod, + crypto.SHA384: ECDSASHA384SignatureMethod, + crypto.SHA512: ECDSASHA512SignatureMethod, + }, +} + +var x509SignatureAlgorithmByIdentifier = map[string]x509.SignatureAlgorithm{ + RSASHA1SignatureMethod: x509.SHA1WithRSA, + RSASHA256SignatureMethod: x509.SHA256WithRSA, + RSASHA384SignatureMethod: x509.SHA384WithRSA, + RSASHA512SignatureMethod: x509.SHA512WithRSA, + ECDSASHA1SignatureMethod: x509.ECDSAWithSHA1, + ECDSASHA256SignatureMethod: x509.ECDSAWithSHA256, + ECDSASHA384SignatureMethod: x509.ECDSAWithSHA384, + ECDSASHA512SignatureMethod: x509.ECDSAWithSHA512, } diff --git a/vendor/github.com/stretchr/testify/assert/assertion_compare.go b/vendor/github.com/stretchr/testify/assert/assertion_compare.go index 41649d267..3bb22a971 100644 --- a/vendor/github.com/stretchr/testify/assert/assertion_compare.go +++ b/vendor/github.com/stretchr/testify/assert/assertion_compare.go @@ -3,6 +3,7 @@ package assert import ( "fmt" "reflect" + "time" ) type CompareType int @@ -30,6 +31,8 @@ var ( float64Type = reflect.TypeOf(float64(1)) stringType = reflect.TypeOf("") + + timeType = reflect.TypeOf(time.Time{}) ) func compare(obj1, obj2 interface{}, kind reflect.Kind) (CompareType, bool) { @@ -299,6 +302,27 @@ func compare(obj1, obj2 interface{}, kind reflect.Kind) (CompareType, bool) { return compareLess, true } } + // Check for known struct types we can check for compare results. + case reflect.Struct: + { + // All structs enter here. We're not interested in most types. + if !canConvert(obj1Value, timeType) { + break + } + + // time.Time can compared! + timeObj1, ok := obj1.(time.Time) + if !ok { + timeObj1 = obj1Value.Convert(timeType).Interface().(time.Time) + } + + timeObj2, ok := obj2.(time.Time) + if !ok { + timeObj2 = obj2Value.Convert(timeType).Interface().(time.Time) + } + + return compare(timeObj1.UnixNano(), timeObj2.UnixNano(), reflect.Int64) + } } return compareEqual, false @@ -310,7 +334,10 @@ func compare(obj1, obj2 interface{}, kind reflect.Kind) (CompareType, bool) { // assert.Greater(t, float64(2), float64(1)) // assert.Greater(t, "b", "a") func Greater(t TestingT, e1 interface{}, e2 interface{}, msgAndArgs ...interface{}) bool { - return compareTwoValues(t, e1, e2, []CompareType{compareGreater}, "\"%v\" is not greater than \"%v\"", msgAndArgs) + if h, ok := t.(tHelper); ok { + h.Helper() + } + return compareTwoValues(t, e1, e2, []CompareType{compareGreater}, "\"%v\" is not greater than \"%v\"", msgAndArgs...) } // GreaterOrEqual asserts that the first element is greater than or equal to the second @@ -320,7 +347,10 @@ func Greater(t TestingT, e1 interface{}, e2 interface{}, msgAndArgs ...interface // assert.GreaterOrEqual(t, "b", "a") // assert.GreaterOrEqual(t, "b", "b") func GreaterOrEqual(t TestingT, e1 interface{}, e2 interface{}, msgAndArgs ...interface{}) bool { - return compareTwoValues(t, e1, e2, []CompareType{compareGreater, compareEqual}, "\"%v\" is not greater than or equal to \"%v\"", msgAndArgs) + if h, ok := t.(tHelper); ok { + h.Helper() + } + return compareTwoValues(t, e1, e2, []CompareType{compareGreater, compareEqual}, "\"%v\" is not greater than or equal to \"%v\"", msgAndArgs...) } // Less asserts that the first element is less than the second @@ -329,7 +359,10 @@ func GreaterOrEqual(t TestingT, e1 interface{}, e2 interface{}, msgAndArgs ...in // assert.Less(t, float64(1), float64(2)) // assert.Less(t, "a", "b") func Less(t TestingT, e1 interface{}, e2 interface{}, msgAndArgs ...interface{}) bool { - return compareTwoValues(t, e1, e2, []CompareType{compareLess}, "\"%v\" is not less than \"%v\"", msgAndArgs) + if h, ok := t.(tHelper); ok { + h.Helper() + } + return compareTwoValues(t, e1, e2, []CompareType{compareLess}, "\"%v\" is not less than \"%v\"", msgAndArgs...) } // LessOrEqual asserts that the first element is less than or equal to the second @@ -339,7 +372,10 @@ func Less(t TestingT, e1 interface{}, e2 interface{}, msgAndArgs ...interface{}) // assert.LessOrEqual(t, "a", "b") // assert.LessOrEqual(t, "b", "b") func LessOrEqual(t TestingT, e1 interface{}, e2 interface{}, msgAndArgs ...interface{}) bool { - return compareTwoValues(t, e1, e2, []CompareType{compareLess, compareEqual}, "\"%v\" is not less than or equal to \"%v\"", msgAndArgs) + if h, ok := t.(tHelper); ok { + h.Helper() + } + return compareTwoValues(t, e1, e2, []CompareType{compareLess, compareEqual}, "\"%v\" is not less than or equal to \"%v\"", msgAndArgs...) } // Positive asserts that the specified element is positive @@ -347,8 +383,11 @@ func LessOrEqual(t TestingT, e1 interface{}, e2 interface{}, msgAndArgs ...inter // assert.Positive(t, 1) // assert.Positive(t, 1.23) func Positive(t TestingT, e interface{}, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } zero := reflect.Zero(reflect.TypeOf(e)) - return compareTwoValues(t, e, zero.Interface(), []CompareType{compareGreater}, "\"%v\" is not positive", msgAndArgs) + return compareTwoValues(t, e, zero.Interface(), []CompareType{compareGreater}, "\"%v\" is not positive", msgAndArgs...) } // Negative asserts that the specified element is negative @@ -356,8 +395,11 @@ func Positive(t TestingT, e interface{}, msgAndArgs ...interface{}) bool { // assert.Negative(t, -1) // assert.Negative(t, -1.23) func Negative(t TestingT, e interface{}, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } zero := reflect.Zero(reflect.TypeOf(e)) - return compareTwoValues(t, e, zero.Interface(), []CompareType{compareLess}, "\"%v\" is not negative", msgAndArgs) + return compareTwoValues(t, e, zero.Interface(), []CompareType{compareLess}, "\"%v\" is not negative", msgAndArgs...) } func compareTwoValues(t TestingT, e1 interface{}, e2 interface{}, allowedComparesResults []CompareType, failMessage string, msgAndArgs ...interface{}) bool { diff --git a/vendor/github.com/stretchr/testify/assert/assertion_compare_can_convert.go b/vendor/github.com/stretchr/testify/assert/assertion_compare_can_convert.go new file mode 100644 index 000000000..df22c47fc --- /dev/null +++ b/vendor/github.com/stretchr/testify/assert/assertion_compare_can_convert.go @@ -0,0 +1,16 @@ +//go:build go1.17 +// +build go1.17 + +// TODO: once support for Go 1.16 is dropped, this file can be +// merged/removed with assertion_compare_go1.17_test.go and +// assertion_compare_legacy.go + +package assert + +import "reflect" + +// Wrapper around reflect.Value.CanConvert, for compatability +// reasons. +func canConvert(value reflect.Value, to reflect.Type) bool { + return value.CanConvert(to) +} diff --git a/vendor/github.com/stretchr/testify/assert/assertion_compare_legacy.go b/vendor/github.com/stretchr/testify/assert/assertion_compare_legacy.go new file mode 100644 index 000000000..1701af2a3 --- /dev/null +++ b/vendor/github.com/stretchr/testify/assert/assertion_compare_legacy.go @@ -0,0 +1,16 @@ +//go:build !go1.17 +// +build !go1.17 + +// TODO: once support for Go 1.16 is dropped, this file can be +// merged/removed with assertion_compare_go1.17_test.go and +// assertion_compare_can_convert.go + +package assert + +import "reflect" + +// Older versions of Go does not have the reflect.Value.CanConvert +// method. +func canConvert(value reflect.Value, to reflect.Type) bool { + return false +} diff --git a/vendor/github.com/stretchr/testify/assert/assertion_format.go b/vendor/github.com/stretchr/testify/assert/assertion_format.go index 4dfd1229a..27e2420ed 100644 --- a/vendor/github.com/stretchr/testify/assert/assertion_format.go +++ b/vendor/github.com/stretchr/testify/assert/assertion_format.go @@ -123,6 +123,18 @@ func ErrorAsf(t TestingT, err error, target interface{}, msg string, args ...int return ErrorAs(t, err, target, append([]interface{}{msg}, args...)...) } +// ErrorContainsf asserts that a function returned an error (i.e. not `nil`) +// and that the error contains the specified substring. +// +// actualObj, err := SomeFunction() +// assert.ErrorContainsf(t, err, expectedErrorSubString, "error message %s", "formatted") +func ErrorContainsf(t TestingT, theError error, contains string, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return ErrorContains(t, theError, contains, append([]interface{}{msg}, args...)...) +} + // ErrorIsf asserts that at least one of the errors in err's chain matches target. // This is a wrapper for errors.Is. func ErrorIsf(t TestingT, err error, target error, msg string, args ...interface{}) bool { diff --git a/vendor/github.com/stretchr/testify/assert/assertion_forward.go b/vendor/github.com/stretchr/testify/assert/assertion_forward.go index 25337a6f0..d9ea368d0 100644 --- a/vendor/github.com/stretchr/testify/assert/assertion_forward.go +++ b/vendor/github.com/stretchr/testify/assert/assertion_forward.go @@ -222,6 +222,30 @@ func (a *Assertions) ErrorAsf(err error, target interface{}, msg string, args .. return ErrorAsf(a.t, err, target, msg, args...) } +// ErrorContains asserts that a function returned an error (i.e. not `nil`) +// and that the error contains the specified substring. +// +// actualObj, err := SomeFunction() +// a.ErrorContains(err, expectedErrorSubString) +func (a *Assertions) ErrorContains(theError error, contains string, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return ErrorContains(a.t, theError, contains, msgAndArgs...) +} + +// ErrorContainsf asserts that a function returned an error (i.e. not `nil`) +// and that the error contains the specified substring. +// +// actualObj, err := SomeFunction() +// a.ErrorContainsf(err, expectedErrorSubString, "error message %s", "formatted") +func (a *Assertions) ErrorContainsf(theError error, contains string, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return ErrorContainsf(a.t, theError, contains, msg, args...) +} + // ErrorIs asserts that at least one of the errors in err's chain matches target. // This is a wrapper for errors.Is. func (a *Assertions) ErrorIs(err error, target error, msgAndArgs ...interface{}) bool { diff --git a/vendor/github.com/stretchr/testify/assert/assertion_order.go b/vendor/github.com/stretchr/testify/assert/assertion_order.go index 1c3b47182..759448783 100644 --- a/vendor/github.com/stretchr/testify/assert/assertion_order.go +++ b/vendor/github.com/stretchr/testify/assert/assertion_order.go @@ -50,7 +50,7 @@ func isOrdered(t TestingT, object interface{}, allowedComparesResults []CompareT // assert.IsIncreasing(t, []float{1, 2}) // assert.IsIncreasing(t, []string{"a", "b"}) func IsIncreasing(t TestingT, object interface{}, msgAndArgs ...interface{}) bool { - return isOrdered(t, object, []CompareType{compareLess}, "\"%v\" is not less than \"%v\"", msgAndArgs) + return isOrdered(t, object, []CompareType{compareLess}, "\"%v\" is not less than \"%v\"", msgAndArgs...) } // IsNonIncreasing asserts that the collection is not increasing @@ -59,7 +59,7 @@ func IsIncreasing(t TestingT, object interface{}, msgAndArgs ...interface{}) boo // assert.IsNonIncreasing(t, []float{2, 1}) // assert.IsNonIncreasing(t, []string{"b", "a"}) func IsNonIncreasing(t TestingT, object interface{}, msgAndArgs ...interface{}) bool { - return isOrdered(t, object, []CompareType{compareEqual, compareGreater}, "\"%v\" is not greater than or equal to \"%v\"", msgAndArgs) + return isOrdered(t, object, []CompareType{compareEqual, compareGreater}, "\"%v\" is not greater than or equal to \"%v\"", msgAndArgs...) } // IsDecreasing asserts that the collection is decreasing @@ -68,7 +68,7 @@ func IsNonIncreasing(t TestingT, object interface{}, msgAndArgs ...interface{}) // assert.IsDecreasing(t, []float{2, 1}) // assert.IsDecreasing(t, []string{"b", "a"}) func IsDecreasing(t TestingT, object interface{}, msgAndArgs ...interface{}) bool { - return isOrdered(t, object, []CompareType{compareGreater}, "\"%v\" is not greater than \"%v\"", msgAndArgs) + return isOrdered(t, object, []CompareType{compareGreater}, "\"%v\" is not greater than \"%v\"", msgAndArgs...) } // IsNonDecreasing asserts that the collection is not decreasing @@ -77,5 +77,5 @@ func IsDecreasing(t TestingT, object interface{}, msgAndArgs ...interface{}) boo // assert.IsNonDecreasing(t, []float{1, 2}) // assert.IsNonDecreasing(t, []string{"a", "b"}) func IsNonDecreasing(t TestingT, object interface{}, msgAndArgs ...interface{}) bool { - return isOrdered(t, object, []CompareType{compareLess, compareEqual}, "\"%v\" is not less than or equal to \"%v\"", msgAndArgs) + return isOrdered(t, object, []CompareType{compareLess, compareEqual}, "\"%v\" is not less than or equal to \"%v\"", msgAndArgs...) } diff --git a/vendor/github.com/stretchr/testify/assert/assertions.go b/vendor/github.com/stretchr/testify/assert/assertions.go index bcac4401f..0357b2231 100644 --- a/vendor/github.com/stretchr/testify/assert/assertions.go +++ b/vendor/github.com/stretchr/testify/assert/assertions.go @@ -718,10 +718,14 @@ func NotEqualValues(t TestingT, expected, actual interface{}, msgAndArgs ...inte // return (false, false) if impossible. // return (true, false) if element was not found. // return (true, true) if element was found. -func includeElement(list interface{}, element interface{}) (ok, found bool) { +func containsElement(list interface{}, element interface{}) (ok, found bool) { listValue := reflect.ValueOf(list) - listKind := reflect.TypeOf(list).Kind() + listType := reflect.TypeOf(list) + if listType == nil { + return false, false + } + listKind := listType.Kind() defer func() { if e := recover(); e != nil { ok = false @@ -764,7 +768,7 @@ func Contains(t TestingT, s, contains interface{}, msgAndArgs ...interface{}) bo h.Helper() } - ok, found := includeElement(s, contains) + ok, found := containsElement(s, contains) if !ok { return Fail(t, fmt.Sprintf("%#v could not be applied builtin len()", s), msgAndArgs...) } @@ -787,7 +791,7 @@ func NotContains(t TestingT, s, contains interface{}, msgAndArgs ...interface{}) h.Helper() } - ok, found := includeElement(s, contains) + ok, found := containsElement(s, contains) if !ok { return Fail(t, fmt.Sprintf("\"%s\" could not be applied builtin len()", s), msgAndArgs...) } @@ -831,7 +835,7 @@ func Subset(t TestingT, list, subset interface{}, msgAndArgs ...interface{}) (ok for i := 0; i < subsetValue.Len(); i++ { element := subsetValue.Index(i).Interface() - ok, found := includeElement(list, element) + ok, found := containsElement(list, element) if !ok { return Fail(t, fmt.Sprintf("\"%s\" could not be applied builtin len()", list), msgAndArgs...) } @@ -852,7 +856,7 @@ func NotSubset(t TestingT, list, subset interface{}, msgAndArgs ...interface{}) h.Helper() } if subset == nil { - return Fail(t, fmt.Sprintf("nil is the empty set which is a subset of every set"), msgAndArgs...) + return Fail(t, "nil is the empty set which is a subset of every set", msgAndArgs...) } subsetValue := reflect.ValueOf(subset) @@ -875,7 +879,7 @@ func NotSubset(t TestingT, list, subset interface{}, msgAndArgs ...interface{}) for i := 0; i < subsetValue.Len(); i++ { element := subsetValue.Index(i).Interface() - ok, found := includeElement(list, element) + ok, found := containsElement(list, element) if !ok { return Fail(t, fmt.Sprintf("\"%s\" could not be applied builtin len()", list), msgAndArgs...) } @@ -1000,27 +1004,21 @@ func Condition(t TestingT, comp Comparison, msgAndArgs ...interface{}) bool { type PanicTestFunc func() // didPanic returns true if the function passed to it panics. Otherwise, it returns false. -func didPanic(f PanicTestFunc) (bool, interface{}, string) { - - didPanic := false - var message interface{} - var stack string - func() { - - defer func() { - if message = recover(); message != nil { - didPanic = true - stack = string(debug.Stack()) - } - }() - - // call the target function - f() +func didPanic(f PanicTestFunc) (didPanic bool, message interface{}, stack string) { + didPanic = true + defer func() { + message = recover() + if didPanic { + stack = string(debug.Stack()) + } }() - return didPanic, message, stack + // call the target function + f() + didPanic = false + return } // Panics asserts that the code inside the specified PanicTestFunc panics. @@ -1161,11 +1159,15 @@ func InDelta(t TestingT, expected, actual interface{}, delta float64, msgAndArgs bf, bok := toFloat(actual) if !aok || !bok { - return Fail(t, fmt.Sprintf("Parameters must be numerical"), msgAndArgs...) + return Fail(t, "Parameters must be numerical", msgAndArgs...) + } + + if math.IsNaN(af) && math.IsNaN(bf) { + return true } if math.IsNaN(af) { - return Fail(t, fmt.Sprintf("Expected must not be NaN"), msgAndArgs...) + return Fail(t, "Expected must not be NaN", msgAndArgs...) } if math.IsNaN(bf) { @@ -1188,7 +1190,7 @@ func InDeltaSlice(t TestingT, expected, actual interface{}, delta float64, msgAn if expected == nil || actual == nil || reflect.TypeOf(actual).Kind() != reflect.Slice || reflect.TypeOf(expected).Kind() != reflect.Slice { - return Fail(t, fmt.Sprintf("Parameters must be slice"), msgAndArgs...) + return Fail(t, "Parameters must be slice", msgAndArgs...) } actualSlice := reflect.ValueOf(actual) @@ -1250,8 +1252,12 @@ func InDeltaMapValues(t TestingT, expected, actual interface{}, delta float64, m func calcRelativeError(expected, actual interface{}) (float64, error) { af, aok := toFloat(expected) - if !aok { - return 0, fmt.Errorf("expected value %q cannot be converted to float", expected) + bf, bok := toFloat(actual) + if !aok || !bok { + return 0, fmt.Errorf("Parameters must be numerical") + } + if math.IsNaN(af) && math.IsNaN(bf) { + return 0, nil } if math.IsNaN(af) { return 0, errors.New("expected value must not be NaN") @@ -1259,10 +1265,6 @@ func calcRelativeError(expected, actual interface{}) (float64, error) { if af == 0 { return 0, fmt.Errorf("expected value must have a value other than zero to calculate the relative error") } - bf, bok := toFloat(actual) - if !bok { - return 0, fmt.Errorf("actual value %q cannot be converted to float", actual) - } if math.IsNaN(bf) { return 0, errors.New("actual value must not be NaN") } @@ -1298,7 +1300,7 @@ func InEpsilonSlice(t TestingT, expected, actual interface{}, epsilon float64, m if expected == nil || actual == nil || reflect.TypeOf(actual).Kind() != reflect.Slice || reflect.TypeOf(expected).Kind() != reflect.Slice { - return Fail(t, fmt.Sprintf("Parameters must be slice"), msgAndArgs...) + return Fail(t, "Parameters must be slice", msgAndArgs...) } actualSlice := reflect.ValueOf(actual) @@ -1375,6 +1377,27 @@ func EqualError(t TestingT, theError error, errString string, msgAndArgs ...inte return true } +// ErrorContains asserts that a function returned an error (i.e. not `nil`) +// and that the error contains the specified substring. +// +// actualObj, err := SomeFunction() +// assert.ErrorContains(t, err, expectedErrorSubString) +func ErrorContains(t TestingT, theError error, contains string, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if !Error(t, theError, msgAndArgs...) { + return false + } + + actual := theError.Error() + if !strings.Contains(actual, contains) { + return Fail(t, fmt.Sprintf("Error %#v does not contain %#v", actual, contains), msgAndArgs...) + } + + return true +} + // matchRegexp return true if a specified regexp matches a string. func matchRegexp(rx interface{}, str interface{}) bool { @@ -1588,12 +1611,17 @@ func diff(expected interface{}, actual interface{}) string { } var e, a string - if et != reflect.TypeOf("") { - e = spewConfig.Sdump(expected) - a = spewConfig.Sdump(actual) - } else { + + switch et { + case reflect.TypeOf(""): e = reflect.ValueOf(expected).String() a = reflect.ValueOf(actual).String() + case reflect.TypeOf(time.Time{}): + e = spewConfigStringerEnabled.Sdump(expected) + a = spewConfigStringerEnabled.Sdump(actual) + default: + e = spewConfig.Sdump(expected) + a = spewConfig.Sdump(actual) } diff, _ := difflib.GetUnifiedDiffString(difflib.UnifiedDiff{ @@ -1625,6 +1653,14 @@ var spewConfig = spew.ConfigState{ MaxDepth: 10, } +var spewConfigStringerEnabled = spew.ConfigState{ + Indent: " ", + DisablePointerAddresses: true, + DisableCapacities: true, + SortKeys: true, + MaxDepth: 10, +} + type tHelper interface { Helper() } diff --git a/vendor/github.com/stretchr/testify/mock/mock.go b/vendor/github.com/stretchr/testify/mock/mock.go index e2e6a2d23..853da6cce 100644 --- a/vendor/github.com/stretchr/testify/mock/mock.go +++ b/vendor/github.com/stretchr/testify/mock/mock.go @@ -221,6 +221,14 @@ type Mock struct { mutex sync.Mutex } +// String provides a %v format string for Mock. +// Note: this is used implicitly by Arguments.Diff if a Mock is passed. +// It exists because go's default %v formatting traverses the struct +// without acquiring the mutex, which is detected by go test -race. +func (m *Mock) String() string { + return fmt.Sprintf("%[1]T<%[1]p>", m) +} + // TestData holds any data that might be useful for testing. Testify ignores // this data completely allowing you to do whatever you like with it. func (m *Mock) TestData() objx.Map { @@ -720,7 +728,7 @@ func (f argumentMatcher) Matches(argument interface{}) bool { } func (f argumentMatcher) String() string { - return fmt.Sprintf("func(%s) bool", f.fn.Type().In(0).Name()) + return fmt.Sprintf("func(%s) bool", f.fn.Type().In(0).String()) } // MatchedBy can be used to match a mock call based on only certain properties diff --git a/vendor/modules.txt b/vendor/modules.txt index 1576ce1fd..3aae063b1 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -69,7 +69,7 @@ github.com/aws/aws-sdk-go/service/sso github.com/aws/aws-sdk-go/service/sso/ssoiface github.com/aws/aws-sdk-go/service/sts github.com/aws/aws-sdk-go/service/sts/stsiface -# github.com/beevik/etree v1.1.0 +# github.com/beevik/etree v1.2.0 ## explicit github.com/beevik/etree # github.com/cenk/hub v1.0.1 @@ -212,6 +212,8 @@ github.com/golang/protobuf/ptypes github.com/golang/protobuf/ptypes/any github.com/golang/protobuf/ptypes/duration github.com/golang/protobuf/ptypes/timestamp +# github.com/google/go-cmp v0.5.9 +## explicit # github.com/google/gofuzz v1.1.0 github.com/google/gofuzz # github.com/google/uuid v1.3.0 @@ -235,7 +237,8 @@ github.com/hashicorp/golang-lru/simplelru github.com/hashicorp/serf/coordinate # github.com/jmespath/go-jmespath v0.4.0 github.com/jmespath/go-jmespath -# github.com/jonboulle/clockwork v0.2.2 +# github.com/jonboulle/clockwork v0.3.0 +## explicit github.com/jonboulle/clockwork # github.com/json-iterator/go v1.1.11 github.com/json-iterator/go @@ -251,6 +254,10 @@ github.com/knqyf263/go-rpmdb/pkg/bdb github.com/knqyf263/go-rpmdb/pkg/db github.com/knqyf263/go-rpmdb/pkg/ndb github.com/knqyf263/go-rpmdb/pkg/sqlite3 +# github.com/kr/pretty v0.3.1 +## explicit +# github.com/mattermost/xml-roundtrip-validator v0.1.0 +github.com/mattermost/xml-roundtrip-validator # github.com/mattn/go-isatty v0.0.12 github.com/mattn/go-isatty # github.com/mitchellh/go-homedir v1.1.0 @@ -282,7 +289,6 @@ github.com/neuvector/k8s/runtime/schema github.com/neuvector/k8s/util/intstr # github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d ## explicit -github.com/nu7hatch/gouuid # github.com/opencontainers/go-digest v1.0.0 ## explicit github.com/opencontainers/go-digest @@ -307,7 +313,12 @@ github.com/pquerna/cachecontrol github.com/pquerna/cachecontrol/cacheobject # github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 github.com/remyoudompheng/bigfft -# github.com/russellhaering/goxmldsig v1.1.1 +# github.com/russellhaering/gosaml2 v0.0.0-00010101000000-000000000000 => github.com/holyspectral/gosaml2 v0.0.0-20231003195827-3d916621a704 +## explicit +github.com/russellhaering/gosaml2 +github.com/russellhaering/gosaml2/types +github.com/russellhaering/gosaml2/uuid +# github.com/russellhaering/goxmldsig v1.4.0 ## explicit github.com/russellhaering/goxmldsig github.com/russellhaering/goxmldsig/etreeutils @@ -326,7 +337,7 @@ github.com/spaolacci/murmur3 github.com/streadway/simpleuuid # github.com/stretchr/objx v0.2.0 github.com/stretchr/objx -# github.com/stretchr/testify v1.7.0 +# github.com/stretchr/testify v1.7.1 ## explicit github.com/stretchr/testify/assert github.com/stretchr/testify/mock @@ -345,7 +356,8 @@ go.opencensus.io/internal go.opencensus.io/trace go.opencensus.io/trace/internal go.opencensus.io/trace/tracestate -# golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 +# golang.org/x/crypto v0.0.0-20220128200615-198e4374d7ed +## explicit golang.org/x/crypto/ed25519 golang.org/x/crypto/ed25519/internal/edwards25519 golang.org/x/crypto/pbkdf2 @@ -571,6 +583,7 @@ sigs.k8s.io/yaml # github.com/golang/protobuf => github.com/golang/protobuf v1.3.3 # github.com/kubernetes/cri-api => k8s.io/cri-api v0.22.3 # github.com/opencontainers/runc => github.com/opencontainers/runc v1.0.0-rc5 +# github.com/russellhaering/gosaml2 => github.com/holyspectral/gosaml2 v0.0.0-20231003195827-3d916621a704 # golang.org/x/net => golang.org/x/net v0.0.0-20200822124328-c89045814202 # google.golang.org/genproto => google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55 # google.golang.org/grpc => google.golang.org/grpc v1.30.1