forked from mystiq/dex
connector/saml: clean up SAML verification logic and comments
This commit is contained in:
parent
258ec4ff4b
commit
362e0798a4
3 changed files with 195 additions and 269 deletions
|
@ -2,10 +2,8 @@
|
||||||
package saml
|
package saml
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/rand"
|
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/hex"
|
|
||||||
"encoding/pem"
|
"encoding/pem"
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
"errors"
|
"errors"
|
||||||
|
@ -270,12 +268,22 @@ func (p *provider) POSTData(s connector.Scopes, id string) (action, value string
|
||||||
return p.ssoURL, base64.StdEncoding.EncodeToString(data), nil
|
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) {
|
func (p *provider) HandlePOST(s connector.Scopes, samlResponse, inResponseTo string) (ident connector.Identity, err error) {
|
||||||
rawResp, err := base64.StdEncoding.DecodeString(samlResponse)
|
rawResp, err := base64.StdEncoding.DecodeString(samlResponse)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ident, fmt.Errorf("decode response: %v", err)
|
return ident, fmt.Errorf("decode response: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Root element is allowed to not be signed if the Assertion element is.
|
||||||
rootElementSigned := true
|
rootElementSigned := true
|
||||||
if p.validator != nil {
|
if p.validator != nil {
|
||||||
rawResp, rootElementSigned, err = verifyResponseSig(p.validator, rawResp)
|
rawResp, rootElementSigned, err = verifyResponseSig(p.validator, rawResp)
|
||||||
|
@ -289,6 +297,8 @@ func (p *provider) HandlePOST(s connector.Scopes, samlResponse, inResponseTo str
|
||||||
return ident, fmt.Errorf("unmarshal response: %v", err)
|
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 rootElementSigned {
|
||||||
if p.ssoIssuer != "" && resp.Issuer != nil && resp.Issuer.Issuer != p.ssoIssuer {
|
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)
|
return ident, fmt.Errorf("expected Issuer value %s, got %s", p.ssoIssuer, resp.Issuer.Issuer)
|
||||||
|
@ -303,10 +313,14 @@ func (p *provider) HandlePOST(s connector.Scopes, samlResponse, inResponseTo str
|
||||||
// Destination is optional.
|
// Destination is optional.
|
||||||
if resp.Destination != "" && resp.Destination != p.redirectURI {
|
if resp.Destination != "" && resp.Destination != p.redirectURI {
|
||||||
return ident, fmt.Errorf("expected destination %q got %q", p.redirectURI, resp.Destination)
|
return ident, fmt.Errorf("expected destination %q got %q", p.redirectURI, resp.Destination)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = p.validateStatus(&resp); err != nil {
|
// 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
|
return ident, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -315,16 +329,25 @@ func (p *provider) HandlePOST(s connector.Scopes, samlResponse, inResponseTo str
|
||||||
if assertion == nil {
|
if assertion == nil {
|
||||||
return ident, fmt.Errorf("response did not contain an assertion")
|
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
|
subject := assertion.Subject
|
||||||
if subject == nil {
|
if subject == nil {
|
||||||
return ident, fmt.Errorf("response did not contain a subject")
|
return ident, fmt.Errorf("response did not contain a subject")
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = p.validateConditions(assertion); err != nil {
|
// Validate that the response is to the request we originally sent.
|
||||||
|
if err = p.validateSubject(subject, inResponseTo); err != nil {
|
||||||
return ident, err
|
return ident, err
|
||||||
}
|
}
|
||||||
if err = p.validateSubjectConfirmation(subject); 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 {
|
switch {
|
||||||
|
@ -336,53 +359,57 @@ func (p *provider) HandlePOST(s connector.Scopes, samlResponse, inResponseTo str
|
||||||
return ident, fmt.Errorf("subject does not contain an NameID element")
|
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
|
attributes := assertion.AttributeStatement
|
||||||
if attributes == nil {
|
if attributes == nil {
|
||||||
return ident, fmt.Errorf("response did not contain a AttributeStatement")
|
return ident, fmt.Errorf("response did not contain a AttributeStatement")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Grab the email.
|
||||||
if ident.Email, _ = attributes.get(p.emailAttr); ident.Email == "" {
|
if ident.Email, _ = attributes.get(p.emailAttr); ident.Email == "" {
|
||||||
return ident, fmt.Errorf("no attribute with name %q: %s", p.emailAttr, attributes.names())
|
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
|
ident.EmailVerified = true
|
||||||
|
|
||||||
|
// Grab the username.
|
||||||
if ident.Username, _ = attributes.get(p.usernameAttr); ident.Username == "" {
|
if ident.Username, _ = attributes.get(p.usernameAttr); ident.Username == "" {
|
||||||
return ident, fmt.Errorf("no attribute with name %q: %s", p.usernameAttr, attributes.names())
|
return ident, fmt.Errorf("no attribute with name %q: %s", p.usernameAttr, attributes.names())
|
||||||
}
|
}
|
||||||
|
|
||||||
if s.Groups && p.groupsAttr != "" {
|
if !s.Groups || p.groupsAttr == "" {
|
||||||
if p.groupsDelim != "" {
|
// Groups not requested or not configured. We're done.
|
||||||
groupsStr, ok := attributes.get(p.groupsAttr)
|
return ident, nil
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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
|
return ident, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate that the StatusCode of the Response is success.
|
// validateStatus verifies that the response has a good status code or
|
||||||
// Otherwise return a human readable message to the end user
|
// formats a human readble error based on the bad status.
|
||||||
func (p *provider) validateStatus(resp *response) error {
|
func (p *provider) validateStatus(status *status) error {
|
||||||
// Status is mandatory in the Response type
|
|
||||||
status := resp.Status
|
|
||||||
if status == nil {
|
|
||||||
return fmt.Errorf("response did not contain a Status")
|
|
||||||
}
|
|
||||||
// StatusCode is mandatory in the Status type
|
// StatusCode is mandatory in the Status type
|
||||||
statusCode := status.StatusCode
|
statusCode := status.StatusCode
|
||||||
if statusCode == nil {
|
if statusCode == nil {
|
||||||
return fmt.Errorf("response did not contain a StatusCode")
|
return fmt.Errorf("response did not contain a StatusCode")
|
||||||
}
|
}
|
||||||
|
|
||||||
if statusCode.Value != statusCodeSuccess {
|
if statusCode.Value != statusCodeSuccess {
|
||||||
parts := strings.Split(statusCode.Value, ":")
|
parts := strings.Split(statusCode.Value, ":")
|
||||||
lastPart := parts[len(parts)-1]
|
lastPart := parts[len(parts)-1]
|
||||||
|
@ -396,96 +423,107 @@ func (p *provider) validateStatus(resp *response) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Multiple subject SubjectConfirmation can be in the assertion
|
// validateSubject ensures the response is to the request we expect.
|
||||||
// and at least one SubjectConfirmation must be valid.
|
//
|
||||||
// This is described in the spec "Profiles for the OASIS Security
|
// This is described in the spec "Profiles for the OASIS Security
|
||||||
// Assertion Markup Language" in section 3.3 Bearer.
|
// 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
|
// see https://www.oasis-open.org/committees/download.php/35389/sstc-saml-profiles-errata-2.0-wd-06-diff.pdf
|
||||||
func (p *provider) validateSubjectConfirmation(subject *subject) error {
|
//
|
||||||
validSubjectConfirmation := false
|
// Some of these fields are optional, but we're going to be strict here since
|
||||||
subjectConfirmations := subject.SubjectConfirmations
|
// we have no other way of guarenteeing that this is actually the response to
|
||||||
if subjectConfirmations != nil && len(subjectConfirmations) > 0 {
|
// the request we expect.
|
||||||
for _, subjectConfirmation := range subjectConfirmations {
|
func (p *provider) validateSubject(subject *subject, inResponseTo string) error {
|
||||||
// skip if method is wrong
|
// Optional according to the spec, but again, we're going to be strict here.
|
||||||
method := subjectConfirmation.Method
|
if len(subject.SubjectConfirmations) == 0 {
|
||||||
if method != "" && method != subjectConfirmationMethodBearer {
|
return fmt.Errorf("Subject contained no SubjectConfrimations")
|
||||||
continue
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
subjectConfirmationData := subjectConfirmation.SubjectConfirmationData
|
|
||||||
if subjectConfirmationData == nil {
|
data := c.SubjectConfirmationData
|
||||||
continue
|
if data == nil {
|
||||||
|
return fmt.Errorf("SubjectConfirmation contained no SubjectConfirmationData")
|
||||||
}
|
}
|
||||||
inResponseTo := subjectConfirmationData.InResponseTo
|
if data.InResponseTo != inResponseTo {
|
||||||
if inResponseTo != "" {
|
return fmt.Errorf("expected SubjectConfirmationData InResponseTo value %q, got %q", inResponseTo, data.InResponseTo)
|
||||||
// TODO also validate InResponseTo if present
|
|
||||||
}
|
}
|
||||||
// only validate that subjectConfirmationData is not expired
|
|
||||||
|
notBefore := time.Time(data.NotBefore)
|
||||||
|
notOnOrAfter := time.Time(data.NotOnOrAfter)
|
||||||
now := p.now()
|
now := p.now()
|
||||||
notOnOrAfter := time.Time(subjectConfirmationData.NotOnOrAfter)
|
if !notBefore.IsZero() && before(now, notBefore) {
|
||||||
if !notOnOrAfter.IsZero() {
|
return fmt.Errorf("at %s got response that cannot be processed before %s", now, notBefore)
|
||||||
if now.After(notOnOrAfter) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// validate recipient if present
|
if !notOnOrAfter.IsZero() && after(now, notOnOrAfter) {
|
||||||
recipient := subjectConfirmationData.Recipient
|
return fmt.Errorf("at %s got response that cannot be processed because it expired at %s", now, notOnOrAfter)
|
||||||
if recipient != "" && recipient != p.redirectURI {
|
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
validSubjectConfirmation = true
|
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 !validSubjectConfirmation {
|
|
||||||
return fmt.Errorf("no valid SubjectConfirmation was found on this Response")
|
if len(errs) == 1 {
|
||||||
|
return fmt.Errorf("failed to validate subject confirmation: %v", errs[0])
|
||||||
}
|
}
|
||||||
return nil
|
return fmt.Errorf("failed to validate subject confirmation: %v", errs)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validates the Conditions element and all of it's content
|
// 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
|
// See: https://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf
|
||||||
// "2.3.3 Element <Assertion>"
|
// "2.3.3 Element <Assertion>"
|
||||||
func (p *provider) validateConditions(assertion *assertion) error {
|
func (p *provider) validateConditions(conditions *conditions) error {
|
||||||
// Checks if a Conditions element exists
|
// Ensure the conditions haven't expired.
|
||||||
conditions := assertion.Conditions
|
|
||||||
if conditions == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
// Validates Assertion timestamps
|
|
||||||
now := p.now()
|
now := p.now()
|
||||||
notBefore := time.Time(conditions.NotBefore)
|
notBefore := time.Time(conditions.NotBefore)
|
||||||
if !notBefore.IsZero() {
|
if !notBefore.IsZero() && before(now, notBefore) {
|
||||||
if now.Add(allowedClockDrift).Before(notBefore) {
|
return fmt.Errorf("at %s got response that cannot be processed before %s", now, notBefore)
|
||||||
return fmt.Errorf("at %s got response that cannot be processed before %s", now, notBefore)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
notOnOrAfter := time.Time(conditions.NotOnOrAfter)
|
notOnOrAfter := time.Time(conditions.NotOnOrAfter)
|
||||||
if !notOnOrAfter.IsZero() {
|
if !notOnOrAfter.IsZero() && after(now, notOnOrAfter) {
|
||||||
if now.After(notOnOrAfter.Add(allowedClockDrift)) {
|
return fmt.Errorf("at %s got response that cannot be processed because it expired at %s", 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
|
||||||
}
|
}
|
||||||
}
|
|
||||||
// Validates audience
|
if !issuerInAudiences {
|
||||||
audienceValue := p.entityIssuer
|
return fmt.Errorf("required audience %s was not in Response audiences %s", expAud, values)
|
||||||
if audienceValue == "" {
|
|
||||||
audienceValue = p.redirectURI
|
|
||||||
}
|
|
||||||
audienceRestriction := conditions.AudienceRestriction
|
|
||||||
if audienceRestriction != nil {
|
|
||||||
audiences := audienceRestriction.Audiences
|
|
||||||
if audiences != nil && len(audiences) > 0 {
|
|
||||||
values := make([]string, len(audiences))
|
|
||||||
issuerInAudiences := false
|
|
||||||
for i, audience := range audiences {
|
|
||||||
if audience.Value == audienceValue {
|
|
||||||
issuerInAudiences = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
values[i] = audience.Value
|
|
||||||
}
|
|
||||||
if !issuerInAudiences {
|
|
||||||
return fmt.Errorf("required audience %s was not in Response audiences %s", audienceValue, values)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
@ -544,24 +582,14 @@ func verifyResponseSig(validator *dsig.ValidationContext, data []byte) (signed [
|
||||||
return signed, false, err
|
return signed, false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func uuidv4() string {
|
// before determines if a given time is before the current time, with an
|
||||||
u := make([]byte, 16)
|
// allowed clock drift.
|
||||||
if _, err := rand.Read(u); err != nil {
|
func before(now, notBefore time.Time) bool {
|
||||||
panic(err)
|
return now.Add(allowedClockDrift).Before(notBefore)
|
||||||
}
|
}
|
||||||
u[6] = (u[6] | 0x40) & 0x4F
|
|
||||||
u[8] = (u[8] | 0x80) & 0xBF
|
// after determines if a given time is after the current time, with an
|
||||||
|
// allowed clock drift.
|
||||||
r := make([]byte, 36)
|
func after(now, notOnOrAfter time.Time) bool {
|
||||||
r[8] = '-'
|
return now.After(notOnOrAfter.Add(allowedClockDrift))
|
||||||
r[13] = '-'
|
|
||||||
r[18] = '-'
|
|
||||||
r[23] = '-'
|
|
||||||
hex.Encode(r, u[0:4])
|
|
||||||
hex.Encode(r[9:], u[4:6])
|
|
||||||
hex.Encode(r[14:], u[6:8])
|
|
||||||
hex.Encode(r[19:], u[8:10])
|
|
||||||
hex.Encode(r[24:], u[10:])
|
|
||||||
|
|
||||||
return string(r)
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,6 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -47,6 +46,7 @@ type responseTest struct {
|
||||||
now string
|
now string
|
||||||
inResponseTo string
|
inResponseTo string
|
||||||
redirectURI string
|
redirectURI string
|
||||||
|
entityIssuer string
|
||||||
|
|
||||||
// Attribute customization.
|
// Attribute customization.
|
||||||
usernameAttr string
|
usernameAttr string
|
||||||
|
@ -196,6 +196,51 @@ func TestAssertionSignedNotResponse(t *testing.T) {
|
||||||
test.run(t)
|
test.run(t)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestInvalidSubjectInResponseTo(t *testing.T) {
|
||||||
|
test := responseTest{
|
||||||
|
caFile: "testdata/ca.crt",
|
||||||
|
respFile: "testdata/assertion-signed.xml",
|
||||||
|
now: "2017-04-04T04:34:59.330Z",
|
||||||
|
usernameAttr: "Name",
|
||||||
|
emailAttr: "email",
|
||||||
|
inResponseTo: "invalid-id", // Bad InResponseTo value.
|
||||||
|
redirectURI: "http://127.0.0.1:5556/dex/callback",
|
||||||
|
wantErr: true,
|
||||||
|
}
|
||||||
|
test.run(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInvalidSubjectRecipient(t *testing.T) {
|
||||||
|
test := responseTest{
|
||||||
|
caFile: "testdata/ca.crt",
|
||||||
|
respFile: "testdata/assertion-signed.xml",
|
||||||
|
now: "2017-04-04T04:34:59.330Z",
|
||||||
|
usernameAttr: "Name",
|
||||||
|
emailAttr: "email",
|
||||||
|
inResponseTo: "6zmm5mguyebwvajyf2sdwwcw6m",
|
||||||
|
redirectURI: "http://bad.com/dex/callback", // Doesn't match Recipient value.
|
||||||
|
wantErr: true,
|
||||||
|
}
|
||||||
|
test.run(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInvalidAssertionAudience(t *testing.T) {
|
||||||
|
test := responseTest{
|
||||||
|
caFile: "testdata/ca.crt",
|
||||||
|
respFile: "testdata/assertion-signed.xml",
|
||||||
|
now: "2017-04-04T04:34:59.330Z",
|
||||||
|
usernameAttr: "Name",
|
||||||
|
emailAttr: "email",
|
||||||
|
inResponseTo: "6zmm5mguyebwvajyf2sdwwcw6m",
|
||||||
|
redirectURI: "http://127.0.0.1:5556/dex/callback",
|
||||||
|
// EntityIssuer overrides RedirectURI when determining the expected
|
||||||
|
// audience. In this case, ensure the audience is invalid.
|
||||||
|
entityIssuer: "http://localhost:5556/dex/callback",
|
||||||
|
wantErr: true,
|
||||||
|
}
|
||||||
|
test.run(t)
|
||||||
|
}
|
||||||
|
|
||||||
// TestTwoAssertionFirstSigned tries to catch an edge case where an attacker
|
// TestTwoAssertionFirstSigned tries to catch an edge case where an attacker
|
||||||
// provides a second assertion that's not signed.
|
// provides a second assertion that's not signed.
|
||||||
func TestTwoAssertionFirstSigned(t *testing.T) {
|
func TestTwoAssertionFirstSigned(t *testing.T) {
|
||||||
|
@ -236,6 +281,7 @@ func (r responseTest) run(t *testing.T) {
|
||||||
EmailAttr: r.emailAttr,
|
EmailAttr: r.emailAttr,
|
||||||
GroupsAttr: r.groupsAttr,
|
GroupsAttr: r.groupsAttr,
|
||||||
RedirectURI: r.redirectURI,
|
RedirectURI: r.redirectURI,
|
||||||
|
EntityIssuer: r.entityIssuer,
|
||||||
// Never logging in, don't need this.
|
// Never logging in, don't need this.
|
||||||
SSOURL: "http://foo.bar/",
|
SSOURL: "http://foo.bar/",
|
||||||
}
|
}
|
||||||
|
@ -355,152 +401,3 @@ func TestVerifySignedMessageAndSignedAssertion(t *testing.T) {
|
||||||
func TestVerifyUnsignedMessageAndUnsignedAssertion(t *testing.T) {
|
func TestVerifyUnsignedMessageAndUnsignedAssertion(t *testing.T) {
|
||||||
runVerify(t, "testdata/idp-cert.pem", "testdata/idp-resp.xml", false)
|
runVerify(t, "testdata/idp-cert.pem", "testdata/idp-resp.xml", false)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestValidateStatus(t *testing.T) {
|
|
||||||
p := newProvider("", "")
|
|
||||||
var err error
|
|
||||||
resp := response{}
|
|
||||||
// Test missing Status element
|
|
||||||
err = p.validateStatus(&resp)
|
|
||||||
if err == nil || !strings.HasSuffix(err.Error(), `Status`) {
|
|
||||||
t.Fatalf("validation should fail with missing Status")
|
|
||||||
}
|
|
||||||
// Test missing StatusCode element
|
|
||||||
resp.Status = &status{}
|
|
||||||
err = p.validateStatus(&resp)
|
|
||||||
if err == nil || !strings.HasSuffix(err.Error(), `StatusCode`) {
|
|
||||||
t.Fatalf("validation should fail with missing StatusCode")
|
|
||||||
}
|
|
||||||
// Test failed request without StatusMessage
|
|
||||||
resp.Status.StatusCode = &statusCode{
|
|
||||||
Value: ":Requester",
|
|
||||||
}
|
|
||||||
err = p.validateStatus(&resp)
|
|
||||||
if err == nil || !strings.HasSuffix(err.Error(), `"Requester"`) {
|
|
||||||
t.Fatalf("validation should fail with code %q", "Requester")
|
|
||||||
}
|
|
||||||
// Test failed request with StatusMessage
|
|
||||||
resp.Status.StatusMessage = &statusMessage{
|
|
||||||
Value: "Failed",
|
|
||||||
}
|
|
||||||
err = p.validateStatus(&resp)
|
|
||||||
if err == nil || !strings.HasSuffix(err.Error(), `"Requester" -> Failed`) {
|
|
||||||
t.Fatalf("validation should fail with code %q and message %q", "Requester", "Failed")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestValidateSubjectConfirmation(t *testing.T) {
|
|
||||||
p := newProvider("", "")
|
|
||||||
var err error
|
|
||||||
var notAfter time.Time
|
|
||||||
subj := &subject{}
|
|
||||||
// Subject without any SubjectConfirmation
|
|
||||||
err = p.validateSubjectConfirmation(subj)
|
|
||||||
if err == nil {
|
|
||||||
t.Fatalf("validation of %q should fail", "Subject without any SubjectConfirmation")
|
|
||||||
}
|
|
||||||
// SubjectConfirmation without Method and SubjectConfirmationData
|
|
||||||
subj.SubjectConfirmations = []subjectConfirmation{subjectConfirmation{}}
|
|
||||||
err = p.validateSubjectConfirmation(subj)
|
|
||||||
if err == nil {
|
|
||||||
t.Fatalf("validation of %q should fail", "SubjectConfirmation without Method and SubjectConfirmationData")
|
|
||||||
}
|
|
||||||
// SubjectConfirmation with invalid Method and no SubjectConfirmationData
|
|
||||||
subj.SubjectConfirmations = []subjectConfirmation{subjectConfirmation{
|
|
||||||
Method: "invalid",
|
|
||||||
}}
|
|
||||||
err = p.validateSubjectConfirmation(subj)
|
|
||||||
if err == nil {
|
|
||||||
t.Fatalf("validation of %q should fail", "SubjectConfirmation with invalid Method and no SubjectConfirmationData")
|
|
||||||
}
|
|
||||||
// SubjectConfirmation with valid Method and empty SubjectConfirmationData
|
|
||||||
subjConfirmationData := subjectConfirmationData{}
|
|
||||||
subj.SubjectConfirmations = []subjectConfirmation{subjectConfirmation{
|
|
||||||
Method: "urn:oasis:names:tc:SAML:2.0:cm:bearer",
|
|
||||||
SubjectConfirmationData: &subjConfirmationData,
|
|
||||||
}}
|
|
||||||
err = p.validateSubjectConfirmation(subj)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("validation of %q should succeed", "SubjectConfirmation with valid Method and empty SubjectConfirmationData")
|
|
||||||
}
|
|
||||||
// SubjectConfirmationData with invalid Recipient
|
|
||||||
subjConfirmationData.Recipient = "invalid"
|
|
||||||
err = p.validateSubjectConfirmation(subj)
|
|
||||||
if err == nil {
|
|
||||||
t.Fatalf("validation of %q should fail", "SubjectConfirmationData with invalid Recipient")
|
|
||||||
}
|
|
||||||
// expired SubjectConfirmationData
|
|
||||||
notAfter = p.now().Add(-time.Duration(60) * time.Second)
|
|
||||||
subjConfirmationData.NotOnOrAfter = xmlTime(notAfter)
|
|
||||||
subjConfirmationData.Recipient = defaultRedirectURI
|
|
||||||
err = p.validateSubjectConfirmation(subj)
|
|
||||||
if err == nil {
|
|
||||||
t.Fatalf("validation of %q should fail", " expired SubjectConfirmationData")
|
|
||||||
}
|
|
||||||
// valid SubjectConfirmationData
|
|
||||||
notAfter = p.now().Add(+time.Duration(60) * time.Second)
|
|
||||||
subjConfirmationData.NotOnOrAfter = xmlTime(notAfter)
|
|
||||||
subjConfirmationData.Recipient = defaultRedirectURI
|
|
||||||
err = p.validateSubjectConfirmation(subj)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("validation of %q should succed", "valid SubjectConfirmationData")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestValidateConditions(t *testing.T) {
|
|
||||||
p := newProvider("", "")
|
|
||||||
var err error
|
|
||||||
var notAfter, notBefore time.Time
|
|
||||||
cond := conditions{
|
|
||||||
AudienceRestriction: &audienceRestriction{},
|
|
||||||
}
|
|
||||||
assert := &assertion{}
|
|
||||||
// Assertion without Conditions
|
|
||||||
err = p.validateConditions(assert)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("validation of %q should succeed", "Assertion without Conditions")
|
|
||||||
}
|
|
||||||
// Assertion with empty Conditions
|
|
||||||
assert.Conditions = &cond
|
|
||||||
err = p.validateConditions(assert)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("validation of %q should succeed", "Assertion with empty Conditions")
|
|
||||||
}
|
|
||||||
// Conditions with valid timestamps
|
|
||||||
notBefore = p.now().Add(-time.Duration(60) * time.Second)
|
|
||||||
notAfter = p.now().Add(+time.Duration(60) * time.Second)
|
|
||||||
cond.NotBefore = xmlTime(notBefore)
|
|
||||||
cond.NotOnOrAfter = xmlTime(notAfter)
|
|
||||||
err = p.validateConditions(assert)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("validation of %q should succeed", "Conditions with valid timestamps")
|
|
||||||
}
|
|
||||||
// Conditions where notBefore is 45 seconds after now
|
|
||||||
notBefore = p.now().Add(+time.Duration(45) * time.Second)
|
|
||||||
cond.NotBefore = xmlTime(notBefore)
|
|
||||||
err = p.validateConditions(assert)
|
|
||||||
if err == nil {
|
|
||||||
t.Fatalf("validation of %q should fail", "Conditions where notBefore is 45 seconds after now")
|
|
||||||
}
|
|
||||||
// Conditions where notBefore is 15 seconds after now
|
|
||||||
notBefore = p.now().Add(+time.Duration(15) * time.Second)
|
|
||||||
cond.NotBefore = xmlTime(notBefore)
|
|
||||||
err = p.validateConditions(assert)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("validation of %q should succeed", "Conditions where notBefore is 15 seconds after now")
|
|
||||||
}
|
|
||||||
// Audiences contains the redirectURI
|
|
||||||
validAudience := audience{Value: p.redirectURI}
|
|
||||||
cond.AudienceRestriction.Audiences = []audience{validAudience}
|
|
||||||
err = p.validateConditions(assert)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("validation of %q should succeed: %v", "Audiences contains the redirectURI", err)
|
|
||||||
}
|
|
||||||
// Audiences is not empty and not contains the issuer
|
|
||||||
invalidAudience := audience{Value: "invalid"}
|
|
||||||
cond.AudienceRestriction.Audiences = []audience{invalidAudience}
|
|
||||||
err = p.validateConditions(assert)
|
|
||||||
if err == nil {
|
|
||||||
t.Fatalf("validation of %q should succeed", "Audiences is not empty and not contains the issuer")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -86,6 +86,7 @@ type nameID struct {
|
||||||
type subjectConfirmationData struct {
|
type subjectConfirmationData struct {
|
||||||
XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:assertion SubjectConfirmationData"`
|
XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:assertion SubjectConfirmationData"`
|
||||||
|
|
||||||
|
NotBefore xmlTime `xml:"NotBefore,attr,omitempty"`
|
||||||
NotOnOrAfter xmlTime `xml:"NotOnOrAfter,attr,omitempty"`
|
NotOnOrAfter xmlTime `xml:"NotOnOrAfter,attr,omitempty"`
|
||||||
Recipient string `xml:"Recipient,attr,omitempty"`
|
Recipient string `xml:"Recipient,attr,omitempty"`
|
||||||
InResponseTo string `xml:"InResponseTo,attr,omitempty"`
|
InResponseTo string `xml:"InResponseTo,attr,omitempty"`
|
||||||
|
@ -115,7 +116,7 @@ type conditions struct {
|
||||||
NotBefore xmlTime `xml:"NotBefore,attr,omitempty"`
|
NotBefore xmlTime `xml:"NotBefore,attr,omitempty"`
|
||||||
NotOnOrAfter xmlTime `xml:"NotOnOrAfter,attr,omitempty"`
|
NotOnOrAfter xmlTime `xml:"NotOnOrAfter,attr,omitempty"`
|
||||||
|
|
||||||
AudienceRestriction *audienceRestriction `xml:"AudienceRestriction,omitempty"`
|
AudienceRestriction []audienceRestriction `xml:"AudienceRestriction,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type statusCode struct {
|
type statusCode struct {
|
||||||
|
|
Loading…
Reference in a new issue