Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pkg/samlcache/assertion_validity_template.gotmpl
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<samlp:Status>
<samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/>
</samlp:Status>
<Assertion xmlns="urn:oasis:names:tc:SAML:2.0:assertion" ID="_f85be5f5-584c-4711-8c9d-5b13c4c49f89" IssueInstant="2016-09-10T02:54:39.386Z" Version="2.0">
<Assertion xmlns="urn:oasis:names:tc:SAML:2.0:assertion" ID="_f85be5f5-584c-4711-8c9d-5b13c4c49f89" IssueInstant="{{.IssueInstantRFC3339Time}}" Version="2.0">
<Issuer>http://id.example.com/adfs/services/trust</Issuer>
<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
<ds:SignedInfo>
Expand Down
18 changes: 15 additions & 3 deletions pkg/samlcache/samlcache.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,13 @@ const (
// SAMLAssertionValidityJitter is a small time delta used to avoid the race condition
// where a token expires between the time it's validated and re-used.
SAMLAssertionValidityJitter = -1 * time.Second
SAMLCacheFilePermissions = 0600
SAMLCacheDirPermissions = 0700
SAMLCacheDir = "saml2aws"
// AWSSAMLRedemptionWindow is how long after its IssueInstant AWS STS accepts an
// assertion ("Token must be redeemed within 5 minutes of issuance"). It is often
// shorter than the NotOnOrAfter the IdP sets on SubjectConfirmationData.
AWSSAMLRedemptionWindow = 5 * time.Minute
SAMLCacheFilePermissions = 0600
SAMLCacheDirPermissions = 0700
SAMLCacheDir = "saml2aws"
)

// SAMLCacheProvider loads aws credentials file
Expand Down Expand Up @@ -89,6 +93,14 @@ func (p *SAMLCacheProvider) IsValid() bool {

logger.Debug("MFA Token expiry date:", ValidUntil.Format(time.RFC3339))

issuedAt, err := saml2aws.ExtractAssertionIssueInstant(data)
if err != nil {
logger.Debug("Could not extract the assertion IssueInstant", err)
} else if redeemBy := issuedAt.Add(AWSSAMLRedemptionWindow); redeemBy.Before(ValidUntil) {
logger.Debug("AWS redemption deadline:", redeemBy.Format(time.RFC3339))
ValidUntil = redeemBy
}

return time.Now().Before(ValidUntil.Add(SAMLAssertionValidityJitter))
}

Expand Down
50 changes: 48 additions & 2 deletions pkg/samlcache/samlcache_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,15 +85,21 @@ func TestCanRead(t *testing.T) {
}

type AssertionTemplateData struct {
ExpiryRFC3339Time string
ExpiryRFC3339Time string
IssueInstantRFC3339Time string
}

func templateAssertion(t time.Time) (string, error) {
return templateIssuedAssertion(time.Now(), t)
}

func templateIssuedAssertion(issued, expiry time.Time) (string, error) {

newfile, _ := os.CreateTemp("", "assert_tmpl_")

data := AssertionTemplateData{
ExpiryRFC3339Time: t.Format(time.RFC3339),
ExpiryRFC3339Time: expiry.Format(time.RFC3339),
IssueInstantRFC3339Time: issued.UTC().Format(time.RFC3339Nano),
}

content, _ := os.ReadFile("./assertion_validity_template.gotmpl")
Expand Down Expand Up @@ -206,3 +212,43 @@ func TestIsNotValid3(t *testing.T) {
}

}

func TestIsNotValidPastAWSRedemptionWindow(t *testing.T) {

// Issued six minutes ago with an hour of advertised validity: AWS STS would
// reject it ("Token must be redeemed within 5 minutes of issuance").
issued := time.Now().Add(-6 * time.Minute)
tmpFile, err := templateIssuedAssertion(issued, issued.Add(time.Hour))
defer os.Remove(tmpFile)
if err != nil {
t.Error(err)
}

p := SAMLCacheProvider{
Filename: tmpFile,
}

if p.IsValid() {
t.Error("The assertion is past the AWS redemption window and should be invalid!")
}

}

func TestIsValidWithinAWSRedemptionWindow(t *testing.T) {

issued := time.Now().Add(-4 * time.Minute)
tmpFile, err := templateIssuedAssertion(issued, issued.Add(time.Hour))
defer os.Remove(tmpFile)
if err != nil {
t.Error(err)
}

p := SAMLCacheProvider{
Filename: tmpFile,
}

if !p.IsValid() {
t.Error("The assertion is within the AWS redemption window and should be valid!")
}

}
22 changes: 22 additions & 0 deletions saml.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,28 @@ func ExtractMFATokenExpiryTime(data []byte) (time.Time, error) {
return time.Parse(time.RFC3339, ValidUntilString)
}

// ExtractAssertionIssueInstant returns the IssueInstant of the Assertion
func ExtractAssertionIssueInstant(data []byte) (time.Time, error) {
var t time.Time

doc := etree.NewDocument()
if err := doc.ReadFromBytes(data); err != nil {
return t, err
}

assertionElement := doc.FindElement(".//Assertion")
if assertionElement == nil {
return t, ErrMissingAssertion
}

issueInstant := assertionElement.SelectAttrValue("IssueInstant", "")
if issueInstant == "" {
return t, ErrMissingElement{Tag: "IssueInstant"}
}

return time.Parse(time.RFC3339, issueInstant)
}

// ExtractAwsRoles given an assertion document extract the aws roles
func ExtractAwsRoles(data []byte) ([]string, error) {

Expand Down
19 changes: 19 additions & 0 deletions saml_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,3 +95,22 @@ func TestExtractMFATokenDuration2(t *testing.T) {
t.Log(err)
assert.NotNil(t, err)
}

func TestExtractAssertionIssueInstant(t *testing.T) {
data, err := os.ReadFile("testdata/assertion.xml")
assert.Nil(t, err)

issued, err := ExtractAssertionIssueInstant(data)

assert.Nil(t, err)
assert.Equal(t, "2016-09-10T02:54:39.386Z", issued.Format(time.RFC3339Nano))
}

func TestExtractAssertionIssueInstantFail(t *testing.T) {
data, err := os.ReadFile("testdata/notxml.xml")
assert.Nil(t, err)

_, err = ExtractAssertionIssueInstant(data)

assert.Error(t, err)
}