diff --git a/build_request.go b/build_request.go index 55cbb2a..0885c1c 100644 --- a/build_request.go +++ b/build_request.go @@ -46,6 +46,12 @@ func (sp *SAMLServiceProvider) buildAuthnRequest(includeSig bool) (*etree.Docume 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 diff --git a/build_request_test.go b/build_request_test.go index 3930a72..dbb43de 100644 --- a/build_request_test.go +++ b/build_request_test.go @@ -129,3 +129,87 @@ func TestRequestedAuthnContextIncluded(t *testing.T) { require.Equal(t, el.Tag, "AuthnContextClassRef") require.Equal(t, el.Text(), AuthnContextPasswordProtectedTransport) } + +func TestForceAuthnOmitted(t *testing.T) { + spURL := "https://sp.test" + sp := SAMLServiceProvider{ + AssertionConsumerServiceURL: spURL, + AudienceURI: spURL, + IdentityProviderIssuer: spURL, + IdentityProviderSSOURL: "https://idp.test/saml/sso", + } + + request, err := sp.BuildAuthRequest() + require.NoError(t, err) + + doc := etree.NewDocument() + err = doc.ReadFromString(request) + require.NoError(t, err) + + attr := doc.Root().SelectAttr("ForceAuthn") + require.Nil(t, attr) +} + +func TestForceAuthnIncluded(t *testing.T) { + spURL := "https://sp.test" + sp := SAMLServiceProvider{ + AssertionConsumerServiceURL: spURL, + AudienceURI: spURL, + IdentityProviderIssuer: spURL, + IdentityProviderSSOURL: "https://idp.test/saml/sso", + ForceAuthn: true, + } + + request, err := sp.BuildAuthRequest() + require.NoError(t, err) + + doc := etree.NewDocument() + err = doc.ReadFromString(request) + require.NoError(t, err) + + attr := doc.Root().SelectAttr("ForceAuthn") + require.NotNil(t, attr) + require.Equal(t, "true", attr.Value) +} + +func TestIsPassiveOmitted(t *testing.T) { + spURL := "https://sp.test" + sp := SAMLServiceProvider{ + AssertionConsumerServiceURL: spURL, + AudienceURI: spURL, + IdentityProviderIssuer: spURL, + IdentityProviderSSOURL: "https://idp.test/saml/sso", + } + + request, err := sp.BuildAuthRequest() + require.NoError(t, err) + + doc := etree.NewDocument() + err = doc.ReadFromString(request) + require.NoError(t, err) + + attr := doc.Root().SelectAttr("IsPassive") + require.Nil(t, attr) +} + +func TestIsPassiveIncluded(t *testing.T) { + spURL := "https://sp.test" + sp := SAMLServiceProvider{ + AssertionConsumerServiceURL: spURL, + AudienceURI: spURL, + IdentityProviderIssuer: spURL, + IdentityProviderSSOURL: "https://idp.test/saml/sso", + IsPassive: true, + } + + request, err := sp.BuildAuthRequest() + require.NoError(t, err) + + doc := etree.NewDocument() + err = doc.ReadFromString(request) + require.NoError(t, err) + + attr := doc.Root().SelectAttr("IsPassive") + require.NotNil(t, attr) + require.Equal(t, "true", attr.Value) +} diff --git a/saml.go b/saml.go index 88ef8ae..c603132 100644 --- a/saml.go +++ b/saml.go @@ -51,6 +51,15 @@ type SAMLServiceProvider struct { 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