func ValidateSignature(md, xp *gosaml.Xp) (err error) { //no ds:Object in signatures certificates := md.Query(nil, gosaml.IdpCertQuery) if len(certificates) == 0 { err = errors.New("no certificates found in metadata") return } signatures := xp.Query(nil, "(/samlp:Response[ds:Signature] | /samlp:Response/saml:Assertion[ds:Signature])") destination := xp.Query1(nil, "/samlp:Response/@Destination") if len(signatures) == 0 { err = fmt.Errorf("%s neither the assertion nor the response was signed", destination) return } verified := 0 signerrors := []error{} for _, certificate := range certificates { var key *rsa.PublicKey _, key, err = gosaml.PublicKeyInfo(md.NodeGetContent(certificate)) if err != nil { return } for _, signature := range signatures { signerror := xp.VerifySignature(signature, key) if signerror != nil { signerrors = append(signerrors, signerror) } else { verified++ } } } if verified == 0 || verified != len(signatures) { errorstring := "" delim := "" for _, e := range signerrors { errorstring += e.Error() + delim delim = ", " } err = fmt.Errorf("%s unable to validate signature: %s", destination, errorstring) return } return }
func ssoService(w http.ResponseWriter, r *http.Request) (err error) { defer r.Body.Close() // handle non ok urls gracefully // var err error // check issuer and acs in md // receiveRequest -> request, issuer md, receiver md // check for IDPList 1st in md, then in request then in query // sanitize idp from query or request request, spmd, _, err := gosaml.GetSAMLMsg(r, "SAMLRequest", hub_ops, hub, nil) if err != nil { return } idp := spmd.Query1(nil, "IDPList/ProviderID") // Need to find a place for IDPList if idp == "" { idp = request.Query1(nil, "IDPList/ProviderID") } if idp == "" { idp = r.URL.Query().Get("idpentityid") } if idp == "" { data := url.Values{} data.Set("return", "https://"+r.Host+r.RequestURI) data.Set("returnIDParam", "idpentityid") http.Redirect(w, r, config["HYBRID_DISCOVERY"]+data.Encode(), http.StatusFound) } else { var idpmd *gosaml.Xp acs := request.Query1(nil, "@AssertionConsumerServiceURL") acsurl := bify.ReplaceAllString(acs, "${1}wayf.wayf.dk/krib.php/$2") request.QueryDashP(nil, "@AssertionConsumerServiceURL", acsurl, nil) idpmd, err = edugain.MDQ(idp) if err != nil { return } const ssoquery = "./md:IDPSSODescriptor/md:SingleSignOnService[@Binding='urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect']/@Location" ssoservice := idpmd.Query1(nil, ssoquery) if ssoservice == "" { } request.QueryDashP(nil, "@Destination", ssoservice, nil) u, _ := gosaml.SAMLRequest2Url(request, "", "", "") http.Redirect(w, r, u.String(), http.StatusFound) } return }
func WayfAttributeHandler(idp_md, hub_md, sp_md, response *gosaml.Xp) (err error) { sourceAttributes := response.Query(nil, `/samlp:Response/saml:Assertion/saml:AttributeStatement`)[0] idp := response.Query1(nil, "/samlp:Response/saml:Issuer") attCS := hub_md.Query(nil, "./md:SPSSODescriptor/md:AttributeConsumingService")[0] // First check for mandatory and multiplicity requestedAttributes := hub_md.Query(attCS, `md:RequestedAttribute[not(@computed)]`) // [@isRequired='true' or @isRequired='1']`) for _, requestedAttribute := range requestedAttributes { name := requestedAttribute.GetAttr("Name") friendlyName := requestedAttribute.GetAttr("FriendlyName") //nameFormat := requestedAttribute.GetAttr("NameFormat") mandatory := hub_md.QueryBool(requestedAttribute, "@mandatory") //must := hub_md.QueryBool(requestedAttribute, "@must") singular := hub_md.QueryBool(requestedAttribute, "@singular") // accept attributes in both uri and basic format attributes := response.Query(sourceAttributes, `saml:Attribute[@Name="`+name+`" or @Name="`+friendlyName+`"]`) if len(attributes) == 0 && mandatory { err = fmt.Errorf("mandatory: %s", friendlyName) return } for _, attribute := range attributes { valueNodes := response.Query(attribute, `saml:AttributeValue`) if len(valueNodes) > 1 && singular { err = fmt.Errorf("multiple values for singular attribute: %s", name) return } if len(valueNodes) != 1 && mandatory { err = fmt.Errorf("mandatory: %s", friendlyName) return } attribute.SetAttr("Name", name) attribute.SetAttr("FriendlyName", friendlyName) attribute.SetAttr("NameFormat", uri) } } // check that the security domain of eppn is one of the domains in the shib:scope list // we just check that everything after the (leftmost|rightmost) @ is in the scope list and save the value for later eppn := response.Query1(sourceAttributes, "saml:Attribute[@Name='urn:oid:1.3.6.1.4.1.5923.1.1.1.6']/saml:AttributeValue") eppnregexp := regexp.MustCompile(`^[^\@]+\@([a-zA-Z0-9\.-]+)$`) matches := eppnregexp.FindStringSubmatch(eppn) if len(matches) != 2 { err = fmt.Errorf("eppn does not seem to be an eppn: %s", eppn) return } securitydomain := matches[1] scope := idp_md.Query(nil, "//shibmd:Scope[.='"+securitydomain+"']") if len(scope) == 0 { err = fmt.Errorf("security domain '%s' for eppn does not match any scopes", securitydomain) } val := idp_md.Query1(nil, "./md:Extensions/wayf:wayf/wayf:wayf_schacHomeOrganizationType") gosaml.CpAndSet(sourceAttributes, response, hub_md, attCS, "schacHomeOrganizationType", val) val = idp_md.Query1(nil, "./md:Extensions/wayf:wayf/wayf:wayf_schacHomeOrganization") gosaml.CpAndSet(sourceAttributes, response, hub_md, attCS, "schacHomeOrganization", val) if response.Query1(sourceAttributes, `saml:Attribute[@FriendlyName="displayName"]/saml:AttributeValue`) == "" { if cn := response.Query1(sourceAttributes, `saml:Attribute[@FriendlyName="cn"]/saml:AttributeValue`); cn != "" { gosaml.CpAndSet(sourceAttributes, response, hub_md, attCS, "displayName", cn) } } salt := "6xfkhc7juin4vlbetmmc0eyxumelnoku" sp := sp_md.Query1(nil, "@entityID") uidhashbase := "uidhashbase" + salt uidhashbase += strconv.Itoa(len(idp)) + ":" + idp uidhashbase += strconv.Itoa(len(sp)) + ":" + sp uidhashbase += strconv.Itoa(len(eppn)) + ":" + eppn uidhashbase += salt eptid := "WAYF-DK-" + hex.EncodeToString(gosaml.Hash(crypto.SHA1, uidhashbase)) gosaml.CpAndSet(sourceAttributes, response, hub_md, attCS, "eduPersonTargetedID", eptid) dkcprpreg := regexp.MustCompile(`^urn:mace:terena.org:schac:personalUniqueID:dk:CPR:(\d\d)(\d\d)(\d\d)(\d)\d\d\d$`) for _, cprelement := range response.Query(sourceAttributes, `saml:Attribute[@FriendlyName="schacPersonalUniqueID"]`) { // schacPersonalUniqueID is multi - use the first DK cpr found cpr := strings.TrimSpace(response.NodeGetContent(cprelement)) if matches := dkcprpreg.FindStringSubmatch(cpr); len(matches) > 0 { cpryear, _ := strconv.Atoi(matches[3]) c7, _ := strconv.Atoi(matches[4]) year := strconv.Itoa(yearfromyearandcifferseven(cpryear, c7)) gosaml.CpAndSet(sourceAttributes, response, hub_md, attCS, "schacDateOfBirth", year+matches[2]+matches[1]) gosaml.CpAndSet(sourceAttributes, response, hub_md, attCS, "schacYearOfBirth", year) break } } subsecuritydomain := "." + securitydomain epsas := make(map[string]bool) for _, epsa := range response.QueryMulti(sourceAttributes, `saml:Attribute[@FriendlyName="eduPersonScopedAffiliation"]/saml:AttributeValue`) { epsa = strings.TrimSpace(epsa) epsaparts := strings.SplitN(epsa, "@", 2) if len(epsaparts) != 2 { fmt.Errorf("eduPersonScopedAffiliation: %s does not end with a domain", epsa) return } if !strings.HasSuffix(epsaparts[1], subsecuritydomain) && epsaparts[1] != securitydomain { fmt.Printf("eduPersonScopedAffiliation: %s has not '%s' as a domain suffix", epsa, securitydomain) return } epsas[epsa] = true } // primaryaffiliation => affiliation epaAdd := []string{} eppa := response.Query1(sourceAttributes, `saml:Attribute[@FriendlyName="eduPersonPrimaryAffiliation"]`) eppa = strings.TrimSpace(eppa) epas := response.QueryMulti(sourceAttributes, `saml:Attribute[@FriendlyName="eduPersonAffiliation"]`) epaset := make(map[string]bool) for _, epa := range epas { epaset[strings.TrimSpace(epa)] = true } if !epaset[eppa] { epaAdd = append(epaAdd, eppa) epaset[eppa] = true } // 'student', 'faculty', 'staff', 'employee' => member if epaset["student"] || epaset["faculty"] || epaset["staff"] || epaset["employee"] { epaAdd = append(epaAdd, "member") epaset["member"] = true } d := sourceAttributes.AddChild(hub_md.CopyNode(hub_md.Query(attCS, `md:RequestedAttribute[@FriendlyName="eduPersonAffiliation"]`)[0], 2)) for i, epa := range epaAdd { response.QueryDashP(d, `saml:AttributeValue[`+strconv.Itoa(i+1)+`]`, epa, nil) } d = sourceAttributes.AddChild(hub_md.CopyNode(hub_md.Query(attCS, `md:RequestedAttribute[@FriendlyName="eduPersonScopedAffiliation"]`)[0], 2)) i := 1 for epa, _ := range epaset { if epsas[epa] { continue } response.QueryDashP(d, `saml:AttributeValue[`+strconv.Itoa(i)+`]`, epa+"@"+securitydomain, nil) i += 1 } return // legal affiliations 'student', 'faculty', 'staff', 'affiliate', 'alum', 'employee', 'library-walk-in', 'member' // affiliations => scopedaffiliations }
// ApplyMods changes a SAML message by applying an array of xpath expressions and a value // If the value is "" the nodes are unlinked // if the value starts with "+ " the the node content is prefixed with the rest of the value // Otherwise the node content is replaced with the value func ApplyMods(xp *gosaml.Xp, m mods) { //log.Printf("%+v\n", m) //log.Println(xp.X2s()) for _, change := range m { if change.value == "" { //log.Printf("changeval: '%s'\n", change.value) for _, element := range xp.Query(nil, change.path) { //log.Printf("unlink: %s\n", change.path) xp.UnlinkNode(element) } } else if strings.HasPrefix(change.value, "+ ") { for _, element := range xp.Query(nil, change.path) { value := xp.NodeGetContent(element) xp.NodeSetContent(element, strings.Fields(change.value)[1]+value) } } else { xp.QueryDashP(nil, change.path, change.value, nil) } } //log.Println(xp.X2s()) }