Skip to content

Commit

Permalink
Merge branch 'master' of https://github.com/jitbit/AspNetSaml
Browse files Browse the repository at this point in the history
  • Loading branch information
alex-jitbit committed Jun 26, 2023
2 parents 8ba1471 + e87731a commit ef55d4d
Show file tree
Hide file tree
Showing 4 changed files with 89 additions and 34 deletions.
44 changes: 43 additions & 1 deletion AspNetSaml.Tests/UnitTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,23 @@ public void TestSamlResponseValidator()
Assert.IsTrue(samlresp.GetCustomAttribute("uid") == "test");
}

[TestMethod]
public void TestSamlResponseValidatorAdvanced()
{
var cert = @"-----BEGIN CERTIFICATE-----
MIIClTCCAX0CBgGICgolYzANBgkqhkiG9w0BAQsFADAOMQwwCgYDVQQDDANQT0MwHhcNMjMwNTExMDg1ODM3WhcNMzMwNTExMDkwMDE3WjAOMQwwCgYDVQQDDANQT0MwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCdKUug5y3ifMXH2kPGPib3APKzA1n9GEsAV304irs9oKK91iCpmQL0SfmMRtyWILPUTSSfKb+Ius2U9AgcjIs517DsbZYTZAglpuZ1DUZTN4IM2PRBrt2bpKv8vQTplesKw6QnWFGrjlOPtw1UmsTnciqiy71GHssSNlLvMObpyW02tt0mGbWQRvCeIwt+aXTB2xrK7buBNJ8yUwdJ0VOpfsUR0yLmV2N/oN0F+f1I/kxn/COEgFZiqJWWEyRCMCXafetU+dq8YMtcO149CKxK66WgTyanAjBf2jv7v5Gk3/0vrLFEIPtHBonDFFQeGw/sTV6bJG+tIS1CX5R/guZRAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAEdXFmQ0BNE4IrE+aEueIl/eyyb90jdU1gmtlrqIvR+RsuQlJzasjE5qW1vcZTdV+omQpeePnIY94KwkhbWwaMsshq7Zi7bbyNWmhc0Mo3o6ONbr3Q6fvfNBePbObGfVCFRT3mgwiqrR59Wmku4PopRS/DXYvbQoim5rxiClAHyN0PkcX6u5J7mmzV1RiZ5OE4fJkIHXXmvUc6NeeFOx8EUnEDrVbfyBn9AK0IZAoj7/jKAJPv5DsBZH3iuFwjSOCAIkpr3W0YcITBeRAvdAri9eFpJ3GO1ZKjynpQaUNWeB3JBjJeNBfQszzmEHlv3Lrayiv2+/uTjFZ2DT7jfxaMw=
-----END CERTIFICATE-----";

var samlresp = new Saml.Response(cert);
samlresp.LoadXml(@"<samlp:Response xmlns:samlp=""urn:oasis:names:tc:SAML:2.0:protocol"" xmlns:saml=""urn:oasis:names:tc:SAML:2.0:assertion"" Destination=""http://localhost:5167/Home/SamlConsume"" ID=""ID_c5a5b7f0-91f3-4d71-90d2-df63c1101bae"" InResponseTo=""_336f6f63-3890-4efb-b4d5-332d3d7486ff"" IssueInstant=""2023-05-24T16:18:35.039Z"" Version=""2.0""><saml:Issuer>http://keycloak:1080/realms/POC</saml:Issuer><dsig:Signature xmlns:dsig=""http://www.w3.org/2000/09/xmldsig#""><dsig:SignedInfo><dsig:CanonicalizationMethod Algorithm=""http://www.w3.org/2001/10/xml-exc-c14n#"" /><dsig:SignatureMethod Algorithm=""http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"" /><dsig:Reference URI=""#ID_c5a5b7f0-91f3-4d71-90d2-df63c1101bae""><dsig:Transforms><dsig:Transform Algorithm=""http://www.w3.org/2000/09/xmldsig#enveloped-signature"" /><dsig:Transform Algorithm=""http://www.w3.org/2001/10/xml-exc-c14n#"" /></dsig:Transforms><dsig:DigestMethod Algorithm=""http://www.w3.org/2001/04/xmlenc#sha256"" /><dsig:DigestValue>UrJzr9Ja0f4Ks+K6TPEfQ53bw1veGXHtMZpLmRrr/ww=</dsig:DigestValue></dsig:Reference></dsig:SignedInfo><dsig:SignatureValue>EAM65nY/e0YkK/H0nw+hdt6PhUIEs5jtftvP/NuHCSFjsVNj8L4jIT7Gvso8r9gSnwz0FJetVK16LjHdN+0f8Od2BDk9njD7KBQx9v9ich12zl1Ny+T6dLtc4XypkvoPwscna7KIQOEn8xeKBq4IbC+gPYfJEQ3GjnQ5JuXhJW5GValLELKWbH21oECRL6VAs7BAohQy2/BbTTGM1tbeuqWIZrqdP/KKOpiHxVIPwzwC8EuQmrhYiaJ9tOzNtBJGD5IW7L6Z6GIhVX2yQPuEW/gfb/bYCi6+0KD664YBICfyJLSarbcK6qgafP9YUdJ48qopiHXbuZ1m8ceCfC0Kow==</dsig:SignatureValue><dsig:KeyInfo><dsig:X509Data><dsig:X509Certificate>MIIClTCCAX0CBgGICgolYzANBgkqhkiG9w0BAQsFADAOMQwwCgYDVQQDDANQT0MwHhcNMjMwNTExMDg1ODM3WhcNMzMwNTExMDkwMDE3WjAOMQwwCgYDVQQDDANQT0MwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCdKUug5y3ifMXH2kPGPib3APKzA1n9GEsAV304irs9oKK91iCpmQL0SfmMRtyWILPUTSSfKb+Ius2U9AgcjIs517DsbZYTZAglpuZ1DUZTN4IM2PRBrt2bpKv8vQTplesKw6QnWFGrjlOPtw1UmsTnciqiy71GHssSNlLvMObpyW02tt0mGbWQRvCeIwt+aXTB2xrK7buBNJ8yUwdJ0VOpfsUR0yLmV2N/oN0F+f1I/kxn/COEgFZiqJWWEyRCMCXafetU+dq8YMtcO149CKxK66WgTyanAjBf2jv7v5Gk3/0vrLFEIPtHBonDFFQeGw/sTV6bJG+tIS1CX5R/guZRAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAEdXFmQ0BNE4IrE+aEueIl/eyyb90jdU1gmtlrqIvR+RsuQlJzasjE5qW1vcZTdV+omQpeePnIY94KwkhbWwaMsshq7Zi7bbyNWmhc0Mo3o6ONbr3Q6fvfNBePbObGfVCFRT3mgwiqrR59Wmku4PopRS/DXYvbQoim5rxiClAHyN0PkcX6u5J7mmzV1RiZ5OE4fJkIHXXmvUc6NeeFOx8EUnEDrVbfyBn9AK0IZAoj7/jKAJPv5DsBZH3iuFwjSOCAIkpr3W0YcITBeRAvdAri9eFpJ3GO1ZKjynpQaUNWeB3JBjJeNBfQszzmEHlv3Lrayiv2+/uTjFZ2DT7jfxaMw=</dsig:X509Certificate></dsig:X509Data></dsig:KeyInfo></dsig:Signature><samlp:Status><samlp:StatusCode Value=""urn:oasis:names:tc:SAML:2.0:status:Success"" /></samlp:Status><saml:Assertion xmlns=""urn:oasis:names:tc:SAML:2.0:assertion"" ID=""ID_4f3af568-ac8a-479f-ba5e-c41a665556cf"" IssueInstant=""2023-05-24T16:18:35.039Z"" Version=""2.0""><saml:Issuer>http://keycloak:1080/realms/POC</saml:Issuer><saml:Subject><saml:NameID Format=""urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified"">guest</saml:NameID><saml:SubjectConfirmation Method=""urn:oasis:names:tc:SAML:2.0:cm:bearer""><saml:SubjectConfirmationData InResponseTo=""_336f6f63-3890-4efb-b4d5-332d3d7486ff"" NotOnOrAfter=""2024-01-18T16:18:33.039Z"" Recipient=""http://localhost:5167/Home/SamlConsume"" /></saml:SubjectConfirmation></saml:Subject><saml:Conditions NotBefore=""2023-05-24T16:18:33.039Z"" NotOnOrAfter=""2024-01-18T16:18:33.039Z""><saml:AudienceRestriction><saml:Audience>WebApp3</saml:Audience></saml:AudienceRestriction></saml:Conditions><saml:AuthnStatement AuthnInstant=""2023-05-24T16:18:35.039Z"" SessionIndex=""f954efd3-4332-4ff8-8cb7-8600174f22b0::f8e67f48-0a80-457e-a669-1e37bd0338d1"" SessionNotOnOrAfter=""2023-05-25T02:18:35.039Z""><saml:AuthnContext><saml:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:unspecified</saml:AuthnContextClassRef></saml:AuthnContext></saml:AuthnStatement><saml:AttributeStatement><saml:Attribute FriendlyName=""email"" Name=""urn:oid:1.2.840.113549.1.9.1"" NameFormat=""urn:oasis:names:tc:SAML:2.0:attrname-format:basic""><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 FriendlyName=""surname"" Name=""urn:oid:2.5.4.4"" NameFormat=""urn:oasis:names:tc:SAML:2.0:attrname-format:basic""><saml:AttributeValue xmlns:xs=""http://www.w3.org/2001/XMLSchema"" xmlns:xsi=""http://www.w3.org/2001/XMLSchema-instance"" xsi:type=""xs:string"">Guest</saml:AttributeValue></saml:Attribute><saml:Attribute FriendlyName=""givenName"" Name=""urn:oid:2.5.4.42"" NameFormat=""urn:oasis:names:tc:SAML:2.0:attrname-format:basic""><saml:AttributeValue xmlns:xs=""http://www.w3.org/2001/XMLSchema"" xmlns:xsi=""http://www.w3.org/2001/XMLSchema-instance"" xsi:type=""xs:string"">Guest</saml:AttributeValue></saml:Attribute><saml:Attribute Name=""Role"" NameFormat=""urn:oasis:names:tc:SAML:2.0:attrname-format:basic""><saml:AttributeValue xmlns:xs=""http://www.w3.org/2001/XMLSchema"" xmlns:xsi=""http://www.w3.org/2001/XMLSchema-instance"" xsi:type=""xs:string"">uma_authorization</saml:AttributeValue></saml:Attribute><saml:Attribute Name=""Role"" NameFormat=""urn:oasis:names:tc:SAML:2.0:attrname-format:basic""><saml:AttributeValue xmlns:xs=""http://www.w3.org/2001/XMLSchema"" xmlns:xsi=""http://www.w3.org/2001/XMLSchema-instance"" xsi:type=""xs:string"">offline_access</saml:AttributeValue></saml:Attribute><saml:Attribute Name=""Role"" NameFormat=""urn:oasis:names:tc:SAML:2.0:attrname-format:basic""><saml:AttributeValue xmlns:xs=""http://www.w3.org/2001/XMLSchema"" xmlns:xsi=""http://www.w3.org/2001/XMLSchema-instance"" xsi:type=""xs:string"">default-roles-poc</saml:AttributeValue></saml:Attribute><saml:Attribute Name=""Role"" NameFormat=""urn:oasis:names:tc:SAML:2.0:attrname-format:basic""><saml:AttributeValue xmlns:xs=""http://www.w3.org/2001/XMLSchema"" xmlns:xsi=""http://www.w3.org/2001/XMLSchema-instance"" xsi:type=""xs:string"">view-profile</saml:AttributeValue></saml:Attribute><saml:Attribute Name=""Role"" NameFormat=""urn:oasis:names:tc:SAML:2.0:attrname-format:basic""><saml:AttributeValue xmlns:xs=""http://www.w3.org/2001/XMLSchema"" xmlns:xsi=""http://www.w3.org/2001/XMLSchema-instance"" xsi:type=""xs:string"">manage-account</saml:AttributeValue></saml:Attribute><saml:Attribute Name=""Role"" NameFormat=""urn:oasis:names:tc:SAML:2.0:attrname-format:basic""><saml:AttributeValue xmlns:xs=""http://www.w3.org/2001/XMLSchema"" xmlns:xsi=""http://www.w3.org/2001/XMLSchema-instance"" xsi:type=""xs:string"">manage-account-links</saml:AttributeValue></saml:Attribute><saml:Attribute Name=""Role"" NameFormat=""urn:oasis:names:tc:SAML:2.0:attrname-format:basic""><saml:AttributeValue xmlns:xs=""http://www.w3.org/2001/XMLSchema"" xmlns:xsi=""http://www.w3.org/2001/XMLSchema-instance"" xsi:type=""xs:string"">SimpleUser</saml:AttributeValue></saml:Attribute></saml:AttributeStatement></saml:Assertion></samlp:Response>");

Assert.IsTrue(samlresp.IsValid());

Assert.IsTrue(samlresp.GetCustomAttributeViaFriendlyName("givenName") == "Guest");

Assert.IsTrue(Enumerable.SequenceEqual(samlresp.GetCustomAttributeAsList("Role"), new List<string> { "uma_authorization", "offline_access", "default-roles-poc", "view-profile", "manage-account", "manage-account-links", "SimpleUser" }));
}

[TestMethod]
public void TestSamlRequest()
{
Expand All @@ -94,5 +111,30 @@ public void TestSamlRequest()
Assert.IsTrue(str.EndsWith(@"ProtocolBinding=""urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"" AssertionConsumerServiceURL=""http://www.myapp.com/SamlConsume"" xmlns:samlp=""urn:oasis:names:tc:SAML:2.0:protocol""><saml:Issuer xmlns:saml=""urn:oasis:names:tc:SAML:2.0:assertion"">http://www.myapp.com</saml:Issuer><samlp:NameIDPolicy Format=""urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified"" AllowCreate=""true"" /></samlp:AuthnRequest>"));

}
}

[TestMethod]
public void TestStringToByteArray()
{
//test that the old StringToByteArray was generating same result as the new Encoding.ASCII.GetBytes

var cert = @"-----BEGIN CERTIFICATE-----
MIICajCCAdOgAwIBAgIBADANBgkqhkiG9w0BAQ0FADBSMQswCQYDVQQGEwJ1czETMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UECgwMT25lbG9naW4gSW5jMRcwFQYDVQQDDA5zcC5leGFtcGxlLmNvbTAeFw0xNDA3MTcxNDEyNTZaFw0xNTA3MTcxNDEyNTZaMFIxCzAJBgNVBAYTAnVzMRMwEQYDVQQIDApDYWxpZm9ybmlhMRUwEwYDVQQKDAxPbmVsb2dpbiBJbmMxFzAVBgNVBAMMDnNwLmV4YW1wbGUuY29tMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDZx+ON4IUoIWxgukTb1tOiX3bMYzYQiwWPUNMp+Fq82xoNogso2bykZG0yiJm5o8zv/sd6pGouayMgkx/2FSOdc36T0jGbCHuRSbtia0PEzNIRtmViMrt3AeoWBidRXmZsxCNLwgIV6dn2WpuE5Az0bHgpZnQxTKFek0BMKU/d8wIDAQABo1AwTjAdBgNVHQ4EFgQUGHxYqZYyX7cTxKVODVgZwSTdCnwwHwYDVR0jBBgwFoAUGHxYqZYyX7cTxKVODVgZwSTdCnwwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQ0FAAOBgQByFOl+hMFICbd3DJfnp2Rgd/dqttsZG/tyhILWvErbio/DEe98mXpowhTkC04ENprOyXi7ZbUqiicF89uAGyt1oqgTUCD1VsLahqIcmrzgumNyTwLGWo17WDAa1/usDhetWAMhgzF/Cnf5ek0nK00m0YZGyc4LzgD0CROMASTWNg==
-----END CERTIFICATE-----";


var x = StringToByteArray(cert);
var y = Encoding.ASCII.GetBytes(cert);
Assert.IsTrue(x.SequenceEqual(y));
}

private static byte[] StringToByteArray(string st)
{
byte[] bytes = new byte[st.Length];
for (int i = 0; i < st.Length; i++)
{
bytes[i] = (byte)st[i];
}
return bytes;
}
}
}
2 changes: 1 addition & 1 deletion AspNetSaml/AspNetSaml.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<TargetFramework>netstandard2.0</TargetFramework>
<PackageId>AspNetSaml</PackageId>
<Title>AspNetSaml</Title>
<Version>2.0.0</Version>
<Version>2.0.1</Version>
<Authors>Alex from Jitbit</Authors>
<Copyright>Copyright Jitbit 2017-2023</Copyright>
<PackageProjectUrl>https://github.com/jitbit/AspNetSaml</PackageProjectUrl>
Expand Down
40 changes: 22 additions & 18 deletions AspNetSaml/Saml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
*/

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Xml;
using System.Security.Cryptography.X509Certificates;
using System.Security.Cryptography.Xml;
Expand All @@ -16,31 +18,21 @@ namespace Saml
{
public class Response
{
private static byte[] StringToByteArray(string st)
{
byte[] bytes = new byte[st.Length];
for (int i = 0; i < st.Length; i++)
{
bytes[i] = (byte)st[i];
}
return bytes;
}

protected XmlDocument _xmlDoc;
protected readonly X509Certificate2 _certificate;
protected XmlNamespaceManager _xmlNameSpaceManager; //we need this one to run our XPath queries on the SAML XML

public string Xml { get { return _xmlDoc.OuterXml; } }

public Response(string certificateStr, string responseString)
: this(StringToByteArray(certificateStr), responseString) { }
: this(Encoding.ASCII.GetBytes(certificateStr), responseString) { }

public Response(byte[] certificateBytes, string responseString) : this(certificateBytes)
{
LoadXmlFromBase64(responseString);
}

public Response(string certificateStr) : this(StringToByteArray(certificateStr)) { }
public Response(string certificateStr) : this(Encoding.ASCII.GetBytes(certificateStr)) { }

public Response(byte[] certificateBytes)
{
Expand Down Expand Up @@ -183,11 +175,23 @@ public virtual string GetLocation()
public string GetCustomAttribute(string attr)
{
XmlNode node = _xmlDoc.SelectSingleNode("/samlp:Response/saml:Assertion[1]/saml:AttributeStatement/saml:Attribute[@Name='" + attr + "']/saml:AttributeValue", _xmlNameSpaceManager);
return node == null ? null : node.InnerText;
}

//returns namespace manager, we need one b/c MS says so... Otherwise XPath doesnt work in an XML doc with namespaces
//see https://stackoverflow.com/questions/7178111/why-is-xmlnamespacemanager-necessary
return node == null ? null : node.InnerText;
}

public string GetCustomAttributeViaFriendlyName(string attr)
{
XmlNode node = _xmlDoc.SelectSingleNode("/samlp:Response/saml:Assertion[1]/saml:AttributeStatement/saml:Attribute[@FriendlyName='" + attr + "']/saml:AttributeValue", _xmlNameSpaceManager);
return node == null ? null : node.InnerText;
}

public List<string> GetCustomAttributeAsList(string attr)
{
XmlNodeList nodes = _xmlDoc.SelectNodes("/samlp:Response/saml:Assertion[1]/saml:AttributeStatement/saml:Attribute[@Name='" + attr + "']/saml:AttributeValue", _xmlNameSpaceManager);
return nodes == null ? null : nodes.Cast<XmlNode>().Select(x => x.InnerText).ToList();
}

//returns namespace manager, we need one b/c MS says so... Otherwise XPath doesnt work in an XML doc with namespaces
//see https://stackoverflow.com/questions/7178111/why-is-xmlnamespacemanager-necessary
private XmlNamespaceManager GetNamespaceManager()
{
XmlNamespaceManager manager = new XmlNamespaceManager(_xmlDoc.NameTable);
Expand Down Expand Up @@ -220,7 +224,7 @@ public enum AuthRequestFormat
public AuthRequest(string issuer, string assertionConsumerServiceUrl)
{
_id = "_" + Guid.NewGuid().ToString();
_issue_instant = DateTime.Now.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ", System.Globalization.CultureInfo.InvariantCulture);
_issue_instant = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ", System.Globalization.CultureInfo.InvariantCulture);

_issuer = issuer;
_assertionConsumerServiceUrl = assertionConsumerServiceUrl;
Expand Down
37 changes: 23 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,12 @@ SAML workflow has 2 steps:
1. User is redirected to the SAML provider (with some magic in the query-string) where he authenticates
2. User is redirected back to your app, where you validate the payload

Here's how you do it (this example is for ASP.NET MVC):
Here's how you do it (this example is for ASP.NET Core MVC):

## 1. Redirecting the user to the saml provider:

```c#
//this example is an ASP.NET MVC action method
//this example is an ASP.NET Core MVC action method
public IActionResult Login()
{
//TODO: specify the SAML provider url here, aka "Endpoint"
Expand All @@ -35,9 +35,9 @@ public IActionResult Login()
var request = new AuthRequest(
"http://www.myapp.com", //TODO: put your app's "entity ID" here
"http://www.myapp.com/SamlConsume" //TODO: put Assertion Consumer URL (where the provider should redirect users after authenticating)
);
);

//redirect the user to the SAML provider
//now send the user to the SAML provider
return Redirect(request.GetRedirectUrl(samlEndpoint));
}
```
Expand All @@ -46,11 +46,11 @@ public IActionResult Login()

User is sent back to your app - you need to validate the SAML response ("assertion") that you recieved via POST.

Here's an example of how you do it in ASP.NET MVC
Here's an example of how you do it in ASP.NET Core MVC

```c#
//ASP.NET MVC action method... But you can easily modify the code for Web-forms etc.
public IActionResult SamlConsume()
//ASP.NET Core MVC action method... But you can easily modify the code for old .NET Framework, Web-forms etc.
public async Task<IActionResult> SamlConsume()
{
// 1. TODO: specify the certificate that your SAML provider gave you
string samlCertificate = @"-----BEGIN CERTIFICATE-----
Expand All @@ -61,21 +61,30 @@ BLAHBLAHBLAHBLAHBLAHBLAHBLAHBLAHBLAHBLAHBLAHBLAH123543==
var samlResponse = new Response(samlCertificate, Request.Form["SAMLResponse"]);

// 3. DONE!
if (samlResponse.IsValid()) //all good
if (samlResponse.IsValid()) //all good?
{
//WOOHOO!!! the user is logged in
username = samlResponse.GetNameID();
var username = samlResponse.GetNameID(); //let's get the username
//user has been authenticated
//put your code here, like set a cookie or something...
//the user has been authenticated
//now call context.SignInAsync() for ASP.NET Core
//or call FormsAuthentication.SetAuthCookie() for .NET Framework
//or call context.SignInAsync() for ASP.NET Core
//or do something else
//or do something else, like set a cookie or something...
//FOR EXAMPLE this is how you sign-in a user in ASP.NET Core 3,5,6,7
await context.SignInAsync(new ClaimsPrincipal(
new ClaimsIdentity(
new[] { new Claim(ClaimTypes.Name, username) },
CookieAuthenticationDefaults.AuthenticationScheme)));

return Redirect("~/");
}

return Content("Unauthorized");
}
```

# Reading more attributes from the provider
# Bonus: reading more attributes from the provider

SAML providers usually send more data with their response: username, first/last names etc. Here's how to get it:

Expand Down

0 comments on commit ef55d4d

Please sign in to comment.