Сопоставление заявлений SAML с помощью ITfoxtec.Identity.Saml2

Я использую проект TestWebAppCore для тестирования SAML интеграция для веб-приложения ASP.NET Core, и я думал, что он работает, но утверждения, связанные с сеансом пользователя, не являются утверждениями, возвращаемыми IdP в ответе SAML, и я не уверен, что это за дополнительная конфигурация требуется для сопоставления возвращенных претензий.

После нажатия кнопки «Войти» меня перенаправляют на свой IdP, после входа в систему мой IdP отвечает следующим ответом SAML (части удалены, чтобы вопрос был кратким):

...
<saml:Subject>
    <saml:NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified"
                    NameQualifier="samlsso"
                    SPNameQualifier="https://my.identity.provider"
                    >edde16f1-9fee-4e44-9c4d-3810a3a6f73a</saml:NameID>
    <saml:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
        <saml:SubjectConfirmationData InResponseTo="_01b18bfb2348b2d1dcc1df73bcdb88dc"
                                        NotOnOrAfter="2020-11-27T13:20:41Z"
                                        Recipient="https://my.identity.provider/samlsso"
                                        />
    </saml:SubjectConfirmation>
</saml:Subject>
...
<saml:AttributeStatement>
    <saml:Attribute Name="MiddleName">
        <saml:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema"
                                xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                                xsi:type="xs:string"
                                >Ben</saml:AttributeValue>
    </saml:Attribute>
    <saml:Attribute Name="email">
        <saml:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema"
                                xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                                xsi:type="xs:string"
                                >[email protected]</saml:AttributeValue>
    </saml:Attribute>
    <saml:Attribute Name="GivenName">
        <saml:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema"
                                xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                                xsi:type="xs:string"
                                >Peter</saml:AttributeValue>
    </saml:Attribute>
    <saml:Attribute Name="FamilyName">
        <saml:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema"
                                xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                                xsi:type="xs:string"
                                >Parker</saml:AttributeValue>
    </saml:Attribute>
</saml:AttributeStatement>
...

После входа я перенаправляюсь на домашнюю страницу и вижу Hi, edde16f1-9fee-4e44-9c4d-3810a3a6f73a, поэтому я нажимаю SAML Claims, и на странице отображается:

The users Claims (Iteration on User.Claims)
http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier
Value: edde16f1-9fee-4e44-9c4d-3810a3a6f73a
http://schemas.microsoft.com/ws/2008/06/identity/claims/authenticationmethod
Value: urn:oasis:names:tc:SAML:2.0:ac:classes:Password
http://schemas.microsoft.com/ws/2008/06/identity/claims/authenticationinstant
Value: 2020-11-27T13:10:37.504Z
http://schemas.itfoxtec.com/ws/2014/02/identity/claims/saml2nameid
Value: edde16f1-9fee-4e44-9c4d-3810a3a6f73a
http://schemas.itfoxtec.com/ws/2014/02/identity/claims/saml2nameidformat
Value: urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress

В этот список не входят утверждения, которые я хочу использовать из ответа SAML, полученного от IdP, поэтому я попытался добавить утверждения в классе ClaimsTransform, немного изменив код:

private static ClaimsPrincipal CreateClaimsPrincipal(ClaimsPrincipal incomingPrincipal)
{
    var claims = new List<Claim>();

    // All claims
    ////claims.AddRange(incomingPrincipal.Claims);

    var givenName = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname";

    claims.Add(new Claim(givenName, GetClaimValue(incomingPrincipal, givenName)));

    // Or custom claims
    //claims.AddRange(GetSaml2LogoutClaims(incomingPrincipal));
    //claims.Add(new Claim(ClaimTypes.NameIdentifier, GetClaimValue(incomingPrincipal, ClaimTypes.NameIdentifier)));

    return new ClaimsPrincipal(new ClaimsIdentity(claims, incomingPrincipal.Identity.AuthenticationType, ClaimTypes.NameIdentifier, ClaimTypes.Role)
    {
        BootstrapContext = ((ClaimsIdentity)incomingPrincipal.Identity).BootstrapContext
    });
}

private static Claim GetClaim(ClaimsPrincipal principal, string claimType)
{
    return ((ClaimsIdentity)principal.Identity).Claims.FirstOrDefault(c => c.Type == claimType);
}

private static string GetClaimValue(ClaimsPrincipal principal, string claimType)
{
    var claim = GetClaim(principal, claimType);
    return claim?.Value;
}

Но это изменение кода приводит к ошибке класса Claim:

Value cannot be null.

Значение кажется нулевым, если я добавляю претензию с GivenName или адресом претензии. Есть ли дополнительная конфигурация, которую мне не хватает, которая позволит мне использовать утверждения в разделе AttributeStatement?

Обновить

Дальнейшее чтение кода меня смущает, на маршруте AssertionConsumerService тестовый код создает совершенно новый SAMLResponse? Новый ответ не содержит никаких атрибутов из ответа IdP, которые объясняли бы, почему нет заявлений.

Если код должен работать именно так, можно ли включить утверждения из ответа IdP в новый ответ, сгенерированный ITfoxtec.identity.saml2?

<saml2p:Response xmlns:saml2p="urn:oasis:names:tc:SAML:2.0:protocol"
                Destination="https://my.test.website/Auth/AssertionConsumerService"
                ID="_9099f6ccf0b9ac7703d6b320df6357a0"
                InResponseTo="_08e3a2b0-4ac8-4673-80bc-31460812738f"
                IssueInstant="2020-11-28T01:13:15.726Z"
                Version="2.0"
                >
    <saml2:Issuer xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion"
                Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity"
                >https://my.test.provider</saml2:Issuer>
    <Signature xmlns="http://www.w3.org/2000/09/xmldsig#">
        <SignedInfo>
            <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#" />
            <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256" />
            <Reference URI="#_9099f6ccf0b9ac7703d6b320df6357a0">
                <Transforms>
                    <Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature" />
                    <Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#" />
                </Transforms>
                <DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1" />
                <DigestValue>IRYj+9sUoEsO5rEgEj+laMogGk0=</DigestValue>
            </Reference>
        </SignedInfo>
        <SignatureValue>...removed...</SignatureValue>
        <ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
            <ds:X509Data>
                <ds:X509Certificate>...removed...</ds:X509Certificate>
            </ds:X509Data>
        </ds:KeyInfo>
    </Signature>
    <saml2p:Status>
        <saml2p:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success" />
    </saml2p:Status>
    <saml2:Assertion xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion"
                    ID="_fbe41ccdaa799fe0c3038d5d07edc18e"
                    IssueInstant="2020-11-28T01:13:15.726Z"
                    Version="2.0"
                    >
        <saml2:Issuer Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity">https://my.test.provider</saml2:Issuer>
        <Signature xmlns="http://www.w3.org/2000/09/xmldsig#">
            <SignedInfo>
                <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#" />
                <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256" />
                <Reference URI="#_fbe41ccdaa799fe0c3038d5d07edc18e">
                    <Transforms>
                        <Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature" />
                        <Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#" />
                    </Transforms>
                    <DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1" />
                    <DigestValue>FbSefxSL8LDE1pJdhScHaNijdEY=</DigestValue>
                </Reference>
            </SignedInfo>
            <SignatureValue>...removed...</SignatureValue>
            <ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
                <ds:X509Data>
                    <ds:X509Certificate>...removed...</ds:X509Certificate>
                </ds:X509Data>
            </ds:KeyInfo>
        </Signature>
        <saml2:Subject>
            <saml2:NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress">edde16f1-9fee-4e44-9c4d-3810a3a6f73a</saml2:NameID>
            <saml2:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
                <saml2:SubjectConfirmationData InResponseTo="_08e3a2b0-4ac8-4673-80bc-31460812738f"
                                            NotOnOrAfter="2020-11-28T01:18:15.726Z"
                                            Recipient="https://my.test.website/Auth/AssertionConsumerService"
                                            />
            </saml2:SubjectConfirmation>
        </saml2:Subject>
        <saml2:Conditions NotBefore="2020-11-28T01:13:15.726Z"
                        NotOnOrAfter="2020-11-28T01:18:15.726Z"
                        >
            <saml2:AudienceRestriction>
                <saml2:Audience>https://my.test.website</saml2:Audience>
            </saml2:AudienceRestriction>
        </saml2:Conditions>
        <saml2:AuthnStatement AuthnInstant="2020-11-28T01:13:15.647Z">
            <saml2:AuthnContext>
                <saml2:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:Password</saml2:AuthnContextClassRef>
            </saml2:AuthnContext>
        </saml2:AuthnStatement>
    </saml2:Assertion>
</saml2p:Response>

Код для AssertionConsumerService:

[Route("AssertionConsumerService")]
public async Task<IActionResult> AssertionConsumerService()
{
    var binding = new Saml2PostBinding();
    var saml2AuthnResponse = new Saml2AuthnResponse(config);

    binding.ReadSamlResponse(Request.ToGenericHttpRequest(), saml2AuthnResponse);
    if (saml2AuthnResponse.Status != Saml2StatusCodes.Success)
    {
        throw new AuthenticationException($"SAML Response status: {saml2AuthnResponse.Status}");
    }
    binding.Unbind(Request.ToGenericHttpRequest(), saml2AuthnResponse);
    await saml2AuthnResponse.CreateSession(HttpContext, claimsTransform: (claimsPrincipal) => ClaimsTransform.Transform(claimsPrincipal));

    var relayStateQuery = binding.GetRelayStateQuery();
    var returnUrl = relayStateQuery.ContainsKey(relayStateReturnUrl) ? relayStateQuery[relayStateReturnUrl] : Url.Content("~/");
    return Redirect(returnUrl);
}

person Bluecakes    schedule 27.11.2020    source источник
comment
Если вы установите точку останова на преобразовании утверждений, содержит ли incomingPrincipal утверждения до преобразования? Кроме того, <saml:Attribute Name="GivenName"> не отправляется с пространством имен в вашем значении входящего утверждения, поэтому он не будет соответствовать строке с пространством имен, которую вы проверяете в предоставленном вами коде.   -  person Adam G    schedule 27.11.2020
comment
Привет, @AdamG, входящий принципал не содержит претензий до преобразования, потому что я обнаружил, что код генерирует собственный новый ответ SAML без данных от IdP. Если вы знаете, как изменить код, чтобы включить ответ от Idp, я был бы очень благодарен.   -  person Bluecakes    schedule 28.11.2020
comment
Я бы посоветовал попробовать вход, инициированный IdP, из IdP-заглушки Sustainsys по адресу stubidp.sustainsys.com - выберите один из поддельных пользователей или укажите свой собственный NameId, индекс сеанса и атрибуты и введите конечную точку URL-адреса ACS. Мне удалось успешно загрузить заявки в проект TestWebAppCore без каких-либо изменений, так что это может быть вопрос работы в обратном направлении из этого заведомо исправного состояния.   -  person Adam G    schedule 28.11.2020
comment
Хм, я смог передать претензии, используя stubidp, но затем я заметил, что все элементы XML в ответе от stubidp были <saml2:xxxx>, тогда как мой собственный IdP передает XML обратно как <saml:xxx>, интересно, вызывает ли это проблемы. Я бы так не подумал, так как оба ответа - это SAML версии 2.0, но ему нужно будет прочитать элемент <saml:NameId>, чтобы получить NameId из исходного ответа.   -  person Bluecakes    schedule 29.11.2020


Ответы (1)


Я не пробовал читать атрибуты, которые выглядят так, как вы описываете. Но я думаю, что библиотека должна уметь читать атрибуты.

Обычно атрибуты выглядят так:

<Attribute Name="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname">
   <AttributeValue>Peter</AttributeValue>
</Attribute>

С полным пространством имен. Однако также должна быть возможность прочитать претензию с таким именем, как givenname.

Пакет ITfoxtec Identity SAML поддерживает только SAML 2.0. В SAML 2.0 NameID имеет пространство имен SAML 2.0:

<NameID Format="urn:oasis:names:tc:SAML:2.0:nameid-format:persistent">edde16f1-9fee-4e44-9c4d-3810a3a6f73a</NameID>

Возможно, в XML есть другие проблемы, которые не соответствуют SAML 2.0.

person Anders Revsgaard    schedule 29.11.2020