forked from mystiq/dex
26c0206627
Fixes #1304, if we want to be harsh. However, I think if it was the user's intention to pass two certs, and the second one couldn't be read, that shouldn't just disappear. After all, when attempting to login later, that might fail because the expected IdP cert data isn't there. Signed-off-by: Stephan Renatus <srenatus@chef.io>
605 lines
19 KiB
Go
605 lines
19 KiB
Go
// Package saml contains login methods for SAML.
|
|
package saml
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/x509"
|
|
"encoding/base64"
|
|
"encoding/pem"
|
|
"encoding/xml"
|
|
"errors"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/beevik/etree"
|
|
dsig "github.com/russellhaering/goxmldsig"
|
|
"github.com/russellhaering/goxmldsig/etreeutils"
|
|
"github.com/sirupsen/logrus"
|
|
|
|
"github.com/dexidp/dex/connector"
|
|
)
|
|
|
|
const (
|
|
bindingRedirect = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
|
|
bindingPOST = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
|
|
|
|
nameIDFormatEmailAddress = "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"
|
|
nameIDFormatUnspecified = "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified"
|
|
nameIDFormatX509Subject = "urn:oasis:names:tc:SAML:1.1:nameid-format:X509SubjectName"
|
|
nameIDFormatWindowsDN = "urn:oasis:names:tc:SAML:1.1:nameid-format:WindowsDomainQualifiedName"
|
|
nameIDFormatEncrypted = "urn:oasis:names:tc:SAML:2.0:nameid-format:encrypted"
|
|
nameIDFormatEntity = "urn:oasis:names:tc:SAML:2.0:nameid-format:entity"
|
|
nameIDFormatKerberos = "urn:oasis:names:tc:SAML:2.0:nameid-format:kerberos"
|
|
nameIDFormatPersistent = "urn:oasis:names:tc:SAML:2.0:nameid-format:persistent"
|
|
nameIDformatTransient = "urn:oasis:names:tc:SAML:2.0:nameid-format:transient"
|
|
|
|
// top level status codes
|
|
statusCodeSuccess = "urn:oasis:names:tc:SAML:2.0:status:Success"
|
|
|
|
// subject confirmation methods
|
|
subjectConfirmationMethodBearer = "urn:oasis:names:tc:SAML:2.0:cm:bearer"
|
|
|
|
// allowed clock drift for timestamp validation
|
|
allowedClockDrift = time.Duration(30) * time.Second
|
|
)
|
|
|
|
var (
|
|
nameIDFormats = []string{
|
|
nameIDFormatEmailAddress,
|
|
nameIDFormatUnspecified,
|
|
nameIDFormatX509Subject,
|
|
nameIDFormatWindowsDN,
|
|
nameIDFormatEncrypted,
|
|
nameIDFormatEntity,
|
|
nameIDFormatKerberos,
|
|
nameIDFormatPersistent,
|
|
nameIDformatTransient,
|
|
}
|
|
nameIDFormatLookup = make(map[string]string)
|
|
)
|
|
|
|
func init() {
|
|
suffix := func(s, sep string) string {
|
|
if i := strings.LastIndex(s, sep); i > 0 {
|
|
return s[i+1:]
|
|
}
|
|
return s
|
|
}
|
|
for _, format := range nameIDFormats {
|
|
nameIDFormatLookup[suffix(format, ":")] = format
|
|
nameIDFormatLookup[format] = format
|
|
}
|
|
}
|
|
|
|
// Config represents configuration options for the SAML provider.
|
|
type Config struct {
|
|
// TODO(ericchiang): A bunch of these fields could be auto-filled if
|
|
// we supported SAML metadata discovery.
|
|
//
|
|
// https://www.oasis-open.org/committees/download.php/35391/sstc-saml-metadata-errata-2.0-wd-04-diff.pdf
|
|
|
|
EntityIssuer string `json:"entityIssuer"`
|
|
SSOIssuer string `json:"ssoIssuer"`
|
|
SSOURL string `json:"ssoURL"`
|
|
|
|
// X509 CA file or raw data to verify XML signatures.
|
|
CA string `json:"ca"`
|
|
CAData []byte `json:"caData"`
|
|
|
|
InsecureSkipSignatureValidation bool `json:"insecureSkipSignatureValidation"`
|
|
|
|
// Assertion attribute names to lookup various claims with.
|
|
UsernameAttr string `json:"usernameAttr"`
|
|
EmailAttr string `json:"emailAttr"`
|
|
GroupsAttr string `json:"groupsAttr"`
|
|
// If GroupsDelim is supplied the connector assumes groups are returned as a
|
|
// single string instead of multiple attribute values. This delimiter will be
|
|
// used split the groups string.
|
|
GroupsDelim string `json:"groupsDelim"`
|
|
|
|
RedirectURI string `json:"redirectURI"`
|
|
|
|
// Requested format of the NameID. The NameID value is is mapped to the ID Token
|
|
// 'sub' claim.
|
|
//
|
|
// This can be an abbreviated form of the full URI with just the last component. For
|
|
// example, if this value is set to "emailAddress" the format will resolve to:
|
|
//
|
|
// urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress
|
|
//
|
|
// If no value is specified, this value defaults to:
|
|
//
|
|
// urn:oasis:names:tc:SAML:2.0:nameid-format:persistent
|
|
//
|
|
NameIDPolicyFormat string `json:"nameIDPolicyFormat"`
|
|
}
|
|
|
|
type certStore struct {
|
|
certs []*x509.Certificate
|
|
}
|
|
|
|
func (c certStore) Certificates() (roots []*x509.Certificate, err error) {
|
|
return c.certs, nil
|
|
}
|
|
|
|
// Open validates the config and returns a connector. It does not actually
|
|
// validate connectivity with the provider.
|
|
func (c *Config) Open(id string, logger logrus.FieldLogger) (connector.Connector, error) {
|
|
return c.openConnector(logger)
|
|
}
|
|
|
|
func (c *Config) openConnector(logger logrus.FieldLogger) (*provider, error) {
|
|
requiredFields := []struct {
|
|
name, val string
|
|
}{
|
|
{"ssoURL", c.SSOURL},
|
|
{"usernameAttr", c.UsernameAttr},
|
|
{"emailAttr", c.EmailAttr},
|
|
{"redirectURI", c.RedirectURI},
|
|
}
|
|
var missing []string
|
|
for _, f := range requiredFields {
|
|
if f.val == "" {
|
|
missing = append(missing, f.name)
|
|
}
|
|
}
|
|
switch len(missing) {
|
|
case 0:
|
|
case 1:
|
|
return nil, fmt.Errorf("missing required field %q", missing[0])
|
|
default:
|
|
return nil, fmt.Errorf("missing required fields %q", missing)
|
|
}
|
|
|
|
p := &provider{
|
|
entityIssuer: c.EntityIssuer,
|
|
ssoIssuer: c.SSOIssuer,
|
|
ssoURL: c.SSOURL,
|
|
now: time.Now,
|
|
usernameAttr: c.UsernameAttr,
|
|
emailAttr: c.EmailAttr,
|
|
groupsAttr: c.GroupsAttr,
|
|
groupsDelim: c.GroupsDelim,
|
|
redirectURI: c.RedirectURI,
|
|
logger: logger,
|
|
|
|
nameIDPolicyFormat: c.NameIDPolicyFormat,
|
|
}
|
|
|
|
if p.nameIDPolicyFormat == "" {
|
|
p.nameIDPolicyFormat = nameIDFormatPersistent
|
|
} else {
|
|
if format, ok := nameIDFormatLookup[p.nameIDPolicyFormat]; ok {
|
|
p.nameIDPolicyFormat = format
|
|
} else {
|
|
return nil, fmt.Errorf("invalid nameIDPolicyFormat: %q", p.nameIDPolicyFormat)
|
|
}
|
|
}
|
|
|
|
if !c.InsecureSkipSignatureValidation {
|
|
if (c.CA == "") == (c.CAData == nil) {
|
|
return nil, errors.New("must provide either 'ca' or 'caData'")
|
|
}
|
|
|
|
var caData []byte
|
|
if c.CA != "" {
|
|
data, err := ioutil.ReadFile(c.CA)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("read ca file: %v", err)
|
|
}
|
|
caData = data
|
|
} else {
|
|
caData = c.CAData
|
|
}
|
|
|
|
var (
|
|
certs []*x509.Certificate
|
|
block *pem.Block
|
|
)
|
|
for {
|
|
block, caData = pem.Decode(caData)
|
|
if block == nil {
|
|
caData = bytes.TrimSpace(caData)
|
|
if len(caData) > 0 { // if there's some left, we've been given bad caData
|
|
return nil, fmt.Errorf("parse cert: trailing data: %q", string(caData))
|
|
}
|
|
break
|
|
}
|
|
cert, err := x509.ParseCertificate(block.Bytes)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("parse cert: %v", err)
|
|
}
|
|
certs = append(certs, cert)
|
|
}
|
|
if len(certs) == 0 {
|
|
return nil, errors.New("no certificates found in ca data")
|
|
}
|
|
p.validator = dsig.NewDefaultValidationContext(certStore{certs})
|
|
}
|
|
return p, nil
|
|
}
|
|
|
|
type provider struct {
|
|
entityIssuer string
|
|
ssoIssuer string
|
|
ssoURL string
|
|
|
|
now func() time.Time
|
|
|
|
// If nil, don't do signature validation.
|
|
validator *dsig.ValidationContext
|
|
|
|
// Attribute mappings
|
|
usernameAttr string
|
|
emailAttr string
|
|
groupsAttr string
|
|
groupsDelim string
|
|
|
|
redirectURI string
|
|
|
|
nameIDPolicyFormat string
|
|
|
|
logger logrus.FieldLogger
|
|
}
|
|
|
|
func (p *provider) POSTData(s connector.Scopes, id string) (action, value string, err error) {
|
|
|
|
r := &authnRequest{
|
|
ProtocolBinding: bindingPOST,
|
|
ID: id,
|
|
IssueInstant: xmlTime(p.now()),
|
|
Destination: p.ssoURL,
|
|
NameIDPolicy: &nameIDPolicy{
|
|
AllowCreate: true,
|
|
Format: p.nameIDPolicyFormat,
|
|
},
|
|
AssertionConsumerServiceURL: p.redirectURI,
|
|
}
|
|
if p.entityIssuer != "" {
|
|
// Issuer for the request is optional. For example, okta always ignores
|
|
// this value.
|
|
r.Issuer = &issuer{Issuer: p.entityIssuer}
|
|
}
|
|
|
|
data, err := xml.MarshalIndent(r, "", " ")
|
|
if err != nil {
|
|
return "", "", fmt.Errorf("marshal authn request: %v", err)
|
|
}
|
|
|
|
// See: https://docs.oasis-open.org/security/saml/v2.0/saml-bindings-2.0-os.pdf
|
|
// "3.5.4 Message Encoding"
|
|
return p.ssoURL, base64.StdEncoding.EncodeToString(data), nil
|
|
}
|
|
|
|
// HandlePOST interprets a request from a SAML provider attempting to verify a
|
|
// user's identity.
|
|
//
|
|
// The steps taken are:
|
|
//
|
|
// * Verify signature on XML document (or verify sig on assertion elements).
|
|
// * Verify various parts of the Assertion element. Conditions, audience, etc.
|
|
// * Map the Assertion's attribute elements to user info.
|
|
//
|
|
func (p *provider) HandlePOST(s connector.Scopes, samlResponse, inResponseTo string) (ident connector.Identity, err error) {
|
|
rawResp, err := base64.StdEncoding.DecodeString(samlResponse)
|
|
if err != nil {
|
|
return ident, fmt.Errorf("decode response: %v", err)
|
|
}
|
|
|
|
// Root element is allowed to not be signed if the Assertion element is.
|
|
rootElementSigned := true
|
|
if p.validator != nil {
|
|
rawResp, rootElementSigned, err = verifyResponseSig(p.validator, rawResp)
|
|
if err != nil {
|
|
return ident, fmt.Errorf("verify signature: %v", err)
|
|
}
|
|
}
|
|
|
|
var resp response
|
|
if err := xml.Unmarshal(rawResp, &resp); err != nil {
|
|
return ident, fmt.Errorf("unmarshal response: %v", err)
|
|
}
|
|
|
|
// If the root element isn't signed, there's no reason to inspect these
|
|
// elements. They're not verified.
|
|
if rootElementSigned {
|
|
if p.ssoIssuer != "" && resp.Issuer != nil && resp.Issuer.Issuer != p.ssoIssuer {
|
|
return ident, fmt.Errorf("expected Issuer value %s, got %s", p.ssoIssuer, resp.Issuer.Issuer)
|
|
}
|
|
|
|
// Verify InResponseTo value matches the expected ID associated with
|
|
// the RelayState.
|
|
if resp.InResponseTo != inResponseTo {
|
|
return ident, fmt.Errorf("expected InResponseTo value %s, got %s", inResponseTo, resp.InResponseTo)
|
|
}
|
|
|
|
// Destination is optional.
|
|
if resp.Destination != "" && resp.Destination != p.redirectURI {
|
|
return ident, fmt.Errorf("expected destination %q got %q", p.redirectURI, resp.Destination)
|
|
}
|
|
|
|
// Status is a required element.
|
|
if resp.Status == nil {
|
|
return ident, fmt.Errorf("Response did not contain a Status element")
|
|
}
|
|
|
|
if err = p.validateStatus(resp.Status); err != nil {
|
|
return ident, err
|
|
}
|
|
}
|
|
|
|
assertion := resp.Assertion
|
|
if assertion == nil {
|
|
return ident, fmt.Errorf("response did not contain an assertion")
|
|
}
|
|
|
|
// Subject is usually optional, but we need it for the user ID, so complain
|
|
// if it's not present.
|
|
subject := assertion.Subject
|
|
if subject == nil {
|
|
return ident, fmt.Errorf("response did not contain a subject")
|
|
}
|
|
|
|
// Validate that the response is to the request we originally sent.
|
|
if err = p.validateSubject(subject, inResponseTo); err != nil {
|
|
return ident, err
|
|
}
|
|
|
|
// Conditions element is optional, but must be validated if present.
|
|
if assertion.Conditions != nil {
|
|
// Validate that dex is the intended audience of this response.
|
|
if err = p.validateConditions(assertion.Conditions); err != nil {
|
|
return ident, err
|
|
}
|
|
}
|
|
|
|
switch {
|
|
case subject.NameID != nil:
|
|
if ident.UserID = subject.NameID.Value; ident.UserID == "" {
|
|
return ident, fmt.Errorf("NameID element does not contain a value")
|
|
}
|
|
default:
|
|
return ident, fmt.Errorf("subject does not contain an NameID element")
|
|
}
|
|
|
|
// After verifying the assertion, map data in the attribute statements to
|
|
// various user info.
|
|
attributes := assertion.AttributeStatement
|
|
if attributes == nil {
|
|
return ident, fmt.Errorf("response did not contain a AttributeStatement")
|
|
}
|
|
|
|
// Log the actual attributes we got back from the server. This helps debug
|
|
// configuration errors on the server side, where the SAML server doesn't
|
|
// send us the correct attributes.
|
|
p.logger.Infof("parsed and verified saml response attributes %s", attributes)
|
|
|
|
// Grab the email.
|
|
if ident.Email, _ = attributes.get(p.emailAttr); ident.Email == "" {
|
|
return ident, fmt.Errorf("no attribute with name %q: %s", p.emailAttr, attributes.names())
|
|
}
|
|
// TODO(ericchiang): Does SAML have an email_verified equivalent?
|
|
ident.EmailVerified = true
|
|
|
|
// Grab the username.
|
|
if ident.Username, _ = attributes.get(p.usernameAttr); ident.Username == "" {
|
|
return ident, fmt.Errorf("no attribute with name %q: %s", p.usernameAttr, attributes.names())
|
|
}
|
|
|
|
if !s.Groups || p.groupsAttr == "" {
|
|
// Groups not requested or not configured. We're done.
|
|
return ident, nil
|
|
}
|
|
|
|
// Grab the groups.
|
|
if p.groupsDelim != "" {
|
|
groupsStr, ok := attributes.get(p.groupsAttr)
|
|
if !ok {
|
|
return ident, fmt.Errorf("no attribute with name %q: %s", p.groupsAttr, attributes.names())
|
|
}
|
|
// TODO(ericchiang): Do we need to further trim whitespace?
|
|
ident.Groups = strings.Split(groupsStr, p.groupsDelim)
|
|
} else {
|
|
groups, ok := attributes.all(p.groupsAttr)
|
|
if !ok {
|
|
return ident, fmt.Errorf("no attribute with name %q: %s", p.groupsAttr, attributes.names())
|
|
}
|
|
ident.Groups = groups
|
|
}
|
|
return ident, nil
|
|
}
|
|
|
|
// validateStatus verifies that the response has a good status code or
|
|
// formats a human readble error based on the bad status.
|
|
func (p *provider) validateStatus(status *status) error {
|
|
// StatusCode is mandatory in the Status type
|
|
statusCode := status.StatusCode
|
|
if statusCode == nil {
|
|
return fmt.Errorf("response did not contain a StatusCode")
|
|
}
|
|
|
|
if statusCode.Value != statusCodeSuccess {
|
|
parts := strings.Split(statusCode.Value, ":")
|
|
lastPart := parts[len(parts)-1]
|
|
errorMessage := fmt.Sprintf("status code of the Response was not Success, was %q", lastPart)
|
|
statusMessage := status.StatusMessage
|
|
if statusMessage != nil && statusMessage.Value != "" {
|
|
errorMessage += " -> " + statusMessage.Value
|
|
}
|
|
return fmt.Errorf(errorMessage)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// validateSubject ensures the response is to the request we expect.
|
|
//
|
|
// This is described in the spec "Profiles for the OASIS Security
|
|
// Assertion Markup Language" in section 3.3 Bearer.
|
|
// see https://www.oasis-open.org/committees/download.php/35389/sstc-saml-profiles-errata-2.0-wd-06-diff.pdf
|
|
//
|
|
// Some of these fields are optional, but we're going to be strict here since
|
|
// we have no other way of guarenteeing that this is actually the response to
|
|
// the request we expect.
|
|
func (p *provider) validateSubject(subject *subject, inResponseTo string) error {
|
|
// Optional according to the spec, but again, we're going to be strict here.
|
|
if len(subject.SubjectConfirmations) == 0 {
|
|
return fmt.Errorf("Subject contained no SubjectConfrimations")
|
|
}
|
|
|
|
var errs []error
|
|
// One of these must match our assumptions, not all.
|
|
for _, c := range subject.SubjectConfirmations {
|
|
err := func() error {
|
|
if c.Method != subjectConfirmationMethodBearer {
|
|
return fmt.Errorf("unexpected subject confirmation method: %v", c.Method)
|
|
}
|
|
|
|
data := c.SubjectConfirmationData
|
|
if data == nil {
|
|
return fmt.Errorf("SubjectConfirmation contained no SubjectConfirmationData")
|
|
}
|
|
if data.InResponseTo != inResponseTo {
|
|
return fmt.Errorf("expected SubjectConfirmationData InResponseTo value %q, got %q", inResponseTo, data.InResponseTo)
|
|
}
|
|
|
|
notBefore := time.Time(data.NotBefore)
|
|
notOnOrAfter := time.Time(data.NotOnOrAfter)
|
|
now := p.now()
|
|
if !notBefore.IsZero() && before(now, notBefore) {
|
|
return fmt.Errorf("at %s got response that cannot be processed before %s", now, notBefore)
|
|
}
|
|
if !notOnOrAfter.IsZero() && after(now, notOnOrAfter) {
|
|
return fmt.Errorf("at %s got response that cannot be processed because it expired at %s", now, notOnOrAfter)
|
|
}
|
|
if r := data.Recipient; r != "" && r != p.redirectURI {
|
|
return fmt.Errorf("expected Recipient %q got %q", p.redirectURI, r)
|
|
}
|
|
return nil
|
|
}()
|
|
if err == nil {
|
|
// Subject is valid.
|
|
return nil
|
|
}
|
|
errs = append(errs, err)
|
|
}
|
|
|
|
if len(errs) == 1 {
|
|
return fmt.Errorf("failed to validate subject confirmation: %v", errs[0])
|
|
}
|
|
return fmt.Errorf("failed to validate subject confirmation: %v", errs)
|
|
}
|
|
|
|
// validationConditions ensures that dex is the intended audience
|
|
// for the request, and not another service provider.
|
|
//
|
|
// See: https://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf
|
|
// "2.3.3 Element <Assertion>"
|
|
func (p *provider) validateConditions(conditions *conditions) error {
|
|
// Ensure the conditions haven't expired.
|
|
now := p.now()
|
|
notBefore := time.Time(conditions.NotBefore)
|
|
if !notBefore.IsZero() && before(now, notBefore) {
|
|
return fmt.Errorf("at %s got response that cannot be processed before %s", now, notBefore)
|
|
}
|
|
|
|
notOnOrAfter := time.Time(conditions.NotOnOrAfter)
|
|
if !notOnOrAfter.IsZero() && after(now, notOnOrAfter) {
|
|
return fmt.Errorf("at %s got response that cannot be processed because it expired at %s", now, notOnOrAfter)
|
|
}
|
|
|
|
// Sometimes, dex's issuer string can be different than the redirect URI,
|
|
// but if dex's issuer isn't explicitly provided assume the redirect URI.
|
|
expAud := p.entityIssuer
|
|
if expAud == "" {
|
|
expAud = p.redirectURI
|
|
}
|
|
|
|
// AudienceRestriction elements indicate the intended audience(s) of an
|
|
// assertion. If dex isn't in these audiences, reject the assertion.
|
|
//
|
|
// Note that if there are multiple AudienceRestriction elements, each must
|
|
// individually contain dex in their audience list.
|
|
for _, r := range conditions.AudienceRestriction {
|
|
values := make([]string, len(r.Audiences))
|
|
issuerInAudiences := false
|
|
for i, aud := range r.Audiences {
|
|
if aud.Value == expAud {
|
|
issuerInAudiences = true
|
|
break
|
|
}
|
|
values[i] = aud.Value
|
|
}
|
|
|
|
if !issuerInAudiences {
|
|
return fmt.Errorf("required audience %s was not in Response audiences %s", expAud, values)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// verifyResponseSig attempts to verify the signature of a SAML response or
|
|
// the assertion.
|
|
//
|
|
// If the root element is properly signed, this method returns it.
|
|
//
|
|
// The SAML spec requires supporting responses where the root element is
|
|
// unverified, but the sub <Assertion> elements are signed. In these cases,
|
|
// this method returns rootVerified=false to indicate that the <Assertion>
|
|
// elements should be trusted, but all other elements MUST be ignored.
|
|
//
|
|
// Note: we still don't support multiple <Assertion> tags. If there are
|
|
// multiple present this code will only process the first.
|
|
func verifyResponseSig(validator *dsig.ValidationContext, data []byte) (signed []byte, rootVerified bool, err error) {
|
|
doc := etree.NewDocument()
|
|
if err = doc.ReadFromBytes(data); err != nil {
|
|
return nil, false, fmt.Errorf("parse document: %v", err)
|
|
}
|
|
|
|
response := doc.Root()
|
|
transformedResponse, err := validator.Validate(response)
|
|
if err == nil {
|
|
// Root element is verified, return it.
|
|
doc.SetRoot(transformedResponse)
|
|
signed, err = doc.WriteToBytes()
|
|
return signed, true, err
|
|
}
|
|
|
|
// Ensures xmlns are copied down to the assertion element when they are defined in the root
|
|
//
|
|
// TODO: Only select from child elements of the root.
|
|
assertion, err := etreeutils.NSSelectOne(response, "urn:oasis:names:tc:SAML:2.0:assertion", "Assertion")
|
|
if err != nil {
|
|
return nil, false, fmt.Errorf("response does not contain an Assertion element")
|
|
}
|
|
transformedAssertion, err := validator.Validate(assertion)
|
|
if err != nil {
|
|
return nil, false, fmt.Errorf("response does not contain a valid signature element: %v", err)
|
|
}
|
|
|
|
// Verified an assertion but not the response. Can't trust any child elements,
|
|
// except the assertion. Remove them all.
|
|
for _, el := range response.ChildElements() {
|
|
response.RemoveChild(el)
|
|
}
|
|
|
|
// We still return the full <Response> element, even though it's unverified
|
|
// because the <Assertion> element is not a valid XML document on its own.
|
|
// It still requires the root element to define things like namespaces.
|
|
response.AddChild(transformedAssertion)
|
|
signed, err = doc.WriteToBytes()
|
|
return signed, false, err
|
|
}
|
|
|
|
// before determines if a given time is before the current time, with an
|
|
// allowed clock drift.
|
|
func before(now, notBefore time.Time) bool {
|
|
return now.Add(allowedClockDrift).Before(notBefore)
|
|
}
|
|
|
|
// after determines if a given time is after the current time, with an
|
|
// allowed clock drift.
|
|
func after(now, notOnOrAfter time.Time) bool {
|
|
return now.After(notOnOrAfter.Add(allowedClockDrift))
|
|
}
|