Skip to content

Commit e2ed28b

Browse files
authored
Merge pull request #83 from ITfoxtec/NemLogin3
NemLog-in 3
2 parents a98cf20 + 207c968 commit e2ed28b

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

46 files changed

+26513
-8
lines changed

ITfoxtec.Identity.Saml2.sln

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestWebAppCoreAzureKeyVault
3131
EndProject
3232
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestWebAppCoreAngularApi", "test\TestWebAppCoreAngularApi\TestWebAppCoreAngularApi.csproj", "{66FA6C5B-164C-44F3-9A96-60F0239ABDCE}"
3333
EndProject
34+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestWebAppCoreNemLogin3Sp", "test\TestWebAppCoreNemLogin3Sp\TestWebAppCoreNemLogin3Sp.csproj", "{072134FF-11D2-4EF5-A5C5-0A601DFECE80}"
35+
EndProject
3436
Global
3537
GlobalSection(SolutionConfigurationPlatforms) = preSolution
3638
Debug|Any CPU = Debug|Any CPU
@@ -73,6 +75,10 @@ Global
7375
{66FA6C5B-164C-44F3-9A96-60F0239ABDCE}.Debug|Any CPU.Build.0 = Debug|Any CPU
7476
{66FA6C5B-164C-44F3-9A96-60F0239ABDCE}.Release|Any CPU.ActiveCfg = Release|Any CPU
7577
{66FA6C5B-164C-44F3-9A96-60F0239ABDCE}.Release|Any CPU.Build.0 = Release|Any CPU
78+
{072134FF-11D2-4EF5-A5C5-0A601DFECE80}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
79+
{072134FF-11D2-4EF5-A5C5-0A601DFECE80}.Debug|Any CPU.Build.0 = Debug|Any CPU
80+
{072134FF-11D2-4EF5-A5C5-0A601DFECE80}.Release|Any CPU.ActiveCfg = Release|Any CPU
81+
{072134FF-11D2-4EF5-A5C5-0A601DFECE80}.Release|Any CPU.Build.0 = Release|Any CPU
7682
EndGlobalSection
7783
GlobalSection(SolutionProperties) = preSolution
7884
HideSolutionNode = FALSE
@@ -87,6 +93,7 @@ Global
8793
{1966436F-5CEC-4290-A547-152357C0BD24} = {A05F26DE-17C2-497F-B244-EE6790789066}
8894
{03A37D91-A36B-48C0-90A1-1FCF43621E60} = {DE5976C5-83CD-4518-A05E-0DEC2EA5D17C}
8995
{66FA6C5B-164C-44F3-9A96-60F0239ABDCE} = {DE5976C5-83CD-4518-A05E-0DEC2EA5D17C}
96+
{072134FF-11D2-4EF5-A5C5-0A601DFECE80} = {DE5976C5-83CD-4518-A05E-0DEC2EA5D17C}
9097
EndGlobalSection
9198
GlobalSection(ExtensibilityGlobals) = postSolution
9299
SolutionGuid = {64BB7D39-E92F-466D-B601-276E16FF6EB4}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
using System.Collections.Generic;
2+
using System.Xml.Linq;
3+
4+
namespace ITfoxtec.Identity.Saml2.Schemas.Metadata
5+
{
6+
/// <summary>
7+
/// EncryptionMethod is an optional element that describes the encryption algorithm applied to the cipher data.
8+
/// If the element is absent, the encryption algorithm must be known to the recipient or the decryption will fail.
9+
/// </summary>
10+
public class EncryptionMethodType
11+
{
12+
const string elementName = Saml2MetadataConstants.Message.EncryptionMethod;
13+
14+
/// <summary>
15+
/// [Required]
16+
/// the Algorithm attribute URI.
17+
/// </summary>
18+
public string Algorithm { get; set; }
19+
20+
public XElement ToXElement()
21+
{
22+
var envelope = new XElement(Saml2MetadataConstants.MetadataNamespaceX + elementName);
23+
24+
envelope.Add(GetXContent());
25+
26+
return envelope;
27+
}
28+
29+
protected IEnumerable<XObject> GetXContent()
30+
{
31+
if (Algorithm != null)
32+
{
33+
yield return new XElement(Saml2MetadataConstants.MetadataNamespaceX + Saml2MetadataConstants.Message.Algorithm, Algorithm);
34+
}
35+
}
36+
}
37+
}

src/ITfoxtec.Identity.Saml2/Schemas/Metadata/EntityDescriptor.cs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -92,15 +92,18 @@ public string IdAsString
9292
public EntityDescriptor()
9393
{ }
9494

95-
public EntityDescriptor(Saml2Configuration config)
95+
public EntityDescriptor(Saml2Configuration config, bool signMetadata = true)
9696
{
9797
if (config == null) throw new ArgumentNullException(nameof(config));
9898

9999
Config = config;
100100
EntityId = config.Issuer;
101101
Id = new Saml2Id();
102-
MetadataSigningCertificate = config.SigningCertificate;
103-
CertificateIncludeOption = X509IncludeOption.EndCertOnly;
102+
if (signMetadata)
103+
{
104+
MetadataSigningCertificate = config.SigningCertificate;
105+
CertificateIncludeOption = X509IncludeOption.EndCertOnly;
106+
}
104107
}
105108

106109
public XmlDocument ToXmlDocument()

src/ITfoxtec.Identity.Saml2/Schemas/Metadata/IdPSsoDescriptor.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ protected IEnumerable<XObject> GetXContent()
5050
{
5151
foreach (var encryptionCertificate in EncryptionCertificates)
5252
{
53-
yield return KeyDescriptor(encryptionCertificate, Saml2MetadataConstants.KeyTypes.Encryption);
53+
yield return KeyDescriptor(encryptionCertificate, Saml2MetadataConstants.KeyTypes.Encryption, EncryptionMethods);
5454
}
5555
}
5656

src/ITfoxtec.Identity.Saml2/Schemas/Metadata/SPSsoDescriptor.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ protected IEnumerable<XObject> GetXContent()
7171
{
7272
foreach(var encryptionCertificate in EncryptionCertificates)
7373
{
74-
yield return KeyDescriptor(encryptionCertificate, Saml2MetadataConstants.KeyTypes.Encryption);
74+
yield return KeyDescriptor(encryptionCertificate, Saml2MetadataConstants.KeyTypes.Encryption, EncryptionMethods);
7575
}
7676
}
7777

src/ITfoxtec.Identity.Saml2/Schemas/Metadata/Saml2MetadataConstants.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
namespace ITfoxtec.Identity.Saml2.Schemas.Metadata
55
{
6-
internal class Saml2MetadataConstants
6+
public class Saml2MetadataConstants
77
{
88
/// <summary>
99
/// The XML namespace of the Metadata.
@@ -20,6 +20,7 @@ internal class Saml2MetadataConstants
2020
public static readonly XName MetadataNamespaceNameX = XNamespace.Xmlns + "m";
2121

2222
public const string AttributeNameFormat = "urn:oasis:names:tc:SAML:2.0:attrname-format:basic";
23+
public const string AttributeNameFormatUri = "urn:oasis:names:tc:SAML:2.0:attrname-format:uri";
2324

2425
public class Message
2526
{
@@ -51,6 +52,10 @@ public class Message
5152

5253
public const string KeyDescriptor = "KeyDescriptor";
5354

55+
public const string EncryptionMethod = "EncryptionMethod";
56+
57+
public const string Algorithm = "Algorithm";
58+
5459
public const string Use = "use";
5560

5661
public const string KeyInfo = "KeyInfo";

src/ITfoxtec.Identity.Saml2/Schemas/Metadata/SsoDescriptorType.cs

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using System.Collections.Generic;
3+
using System.Linq;
34
using System.Security.Cryptography.X509Certificates;
45
using System.Security.Cryptography.Xml;
56
using System.Xml;
@@ -40,6 +41,12 @@ public abstract class SsoDescriptorType
4041
/// </summary>
4142
public IEnumerable<X509Certificate2> EncryptionCertificates { get; set; }
4243

44+
/// <summary>
45+
/// [Optional]
46+
/// Specifying algorithms and algorithm-specific settings supported by the entity.
47+
/// </summary>
48+
public IEnumerable<EncryptionMethodType> EncryptionMethods { get; set; }
49+
4350
/// <summary>
4451
/// [Optional]
4552
/// Zero or one element of type EndpointType that describe endpoints that support the Single
@@ -55,14 +62,33 @@ public abstract class SsoDescriptorType
5562
/// </summary>
5663
public IEnumerable<Uri> NameIDFormats { get; set; }
5764

58-
protected XObject KeyDescriptor(X509Certificate2 certificate, string keyType)
65+
/// <summary>
66+
/// Configure default encryption methods used by .NET.
67+
/// </summary>
68+
public void SetDefaultEncryptionMethods()
69+
{
70+
EncryptionMethods = new[] { new EncryptionMethodType { Algorithm = EncryptedXml.XmlEncAES256Url }, new EncryptionMethodType { Algorithm = EncryptedXml.XmlEncRSAOAEPUrl } };
71+
}
72+
73+
protected XObject KeyDescriptor(X509Certificate2 certificate, string keyType, IEnumerable<EncryptionMethodType> encryptionMethods = null)
5974
{
6075
var keyinfo = new KeyInfo();
6176
keyinfo.AddClause(new KeyInfoX509Data(certificate, CertificateIncludeOption));
6277

63-
return new XElement(Saml2MetadataConstants.MetadataNamespaceX + Saml2MetadataConstants.Message.KeyDescriptor,
78+
var keyDescriptorElement = new XElement(Saml2MetadataConstants.MetadataNamespaceX + Saml2MetadataConstants.Message.KeyDescriptor,
6479
new XAttribute(Saml2MetadataConstants.Message.Use, keyType),
6580
XElement.Parse(keyinfo.GetXml().OuterXml));
81+
82+
if (keyType == Saml2MetadataConstants.KeyTypes.Encryption && encryptionMethods?.Count() > 0)
83+
{
84+
foreach(var encryptionMethod in encryptionMethods)
85+
{
86+
keyDescriptorElement.Add(new XElement(Saml2MetadataConstants.MetadataNamespaceX + Saml2MetadataConstants.Message.EncryptionMethod,
87+
new XAttribute(Saml2MetadataConstants.Message.Algorithm, encryptionMethod.Algorithm)));
88+
}
89+
}
90+
91+
return keyDescriptorElement;
6692
}
6793

6894
protected void ReadKeyDescriptors(XmlElement xmlElement)
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
using ITfoxtec.Identity.Saml2;
2+
using ITfoxtec.Identity.Saml2.Schemas;
3+
using ITfoxtec.Identity.Saml2.MvcCore;
4+
using System;
5+
using System.Collections.Generic;
6+
using System.Diagnostics;
7+
using System.Threading.Tasks;
8+
using Microsoft.AspNetCore.Authorization;
9+
using Microsoft.AspNetCore.Mvc;
10+
using TestWebAppCoreNemLogin3Sp.Identity;
11+
using Microsoft.Extensions.Options;
12+
using System.Security.Authentication;
13+
using System.Security.Claims;
14+
using System.Linq;
15+
16+
namespace TestWebAppCoreNemLogin3Sp.Controllers
17+
{
18+
[AllowAnonymous]
19+
[Route("Auth")]
20+
public class AuthController : Controller
21+
{
22+
const string relayStateReturnUrl = "ReturnUrl";
23+
private readonly Saml2Configuration config;
24+
25+
public AuthController(IOptions<Saml2Configuration> configAccessor)
26+
{
27+
config = configAccessor.Value;
28+
}
29+
30+
[Route("Login")]
31+
public IActionResult Login(string returnUrl = null)
32+
{
33+
var binding = new Saml2RedirectBinding();
34+
binding.SetRelayStateQuery(new Dictionary<string, string> { { relayStateReturnUrl, returnUrl ?? Url.Content("~/") } });
35+
36+
return binding.Bind(new Saml2AuthnRequest(config)
37+
{
38+
//ForceAuthn = true,
39+
RequestedAuthnContext = new RequestedAuthnContext
40+
{
41+
Comparison = AuthnContextComparisonTypes.Minimum,
42+
AuthnContextClassRef = new string[]
43+
{
44+
//"https://data.gov.dk/concept/core/nsis/loa/Low"
45+
"https://data.gov.dk/concept/core/nsis/loa/Substantial",
46+
//"https://data.gov.dk/concept/core/nsis/loa/High"
47+
48+
//"https://nemlogin.dk/internal/credential/type/nemidkeycard"
49+
//"https://nemlogin.dk/internal/credential/type/nemidkeyfile"
50+
//"https://nemlogin.dk/internal/credential/type/mitid"
51+
//"https://nemlogin.dk/internal/credential/type/local"
52+
//"https://nemlogin.dk/internal/credential/type/test"
53+
54+
//"https://data.gov.dk/eid/Professional"
55+
//"https://data.gov.dk/eid/Person"
56+
},
57+
},
58+
}).ToActionResult();
59+
}
60+
61+
[Route("AssertionConsumerService")]
62+
public async Task<IActionResult> AssertionConsumerService()
63+
{
64+
var binding = new Saml2PostBinding();
65+
var saml2AuthnResponse = new Saml2AuthnResponse(config);
66+
67+
binding.ReadSamlResponse(Request.ToGenericHttpRequest(), saml2AuthnResponse);
68+
if (saml2AuthnResponse.Status != Saml2StatusCodes.Success)
69+
{
70+
throw new AuthenticationException($"SAML Response status: {saml2AuthnResponse.Status}");
71+
}
72+
binding.Unbind(Request.ToGenericHttpRequest(), saml2AuthnResponse);
73+
await saml2AuthnResponse.CreateSession(HttpContext, claimsTransform: (claimsPrincipal) => ClaimsTransform.Transform(CheckAssurance(claimsPrincipal)));
74+
75+
var relayStateQuery = binding.GetRelayStateQuery();
76+
var returnUrl = relayStateQuery.ContainsKey(relayStateReturnUrl) ? relayStateQuery[relayStateReturnUrl] : Url.Content("~/");
77+
return Redirect(returnUrl);
78+
}
79+
80+
private ClaimsPrincipal CheckAssurance(ClaimsPrincipal claimsPrincipal)
81+
{
82+
var nsisLevelAccepted = claimsPrincipal.Claims.Where(c => c.Type == OioSaml3ClaimTypes.NsisLoa && (c.Value == NsisLevels.Substantial || c.Value == NsisLevels.High)).Any();
83+
var oldAssuranceLevelAccepted = claimsPrincipal.Claims.Where(c => c.Type == OioSaml2ClaimTypes.AssuranceLevel && Convert.ToInt32(c.Value) >= 3).Any();
84+
85+
if (!nsisLevelAccepted && !oldAssuranceLevelAccepted)
86+
{
87+
throw new Exception("Assurance level not accepted.");
88+
}
89+
90+
return claimsPrincipal;
91+
}
92+
93+
[HttpPost("Logout")]
94+
[ValidateAntiForgeryToken]
95+
public async Task<IActionResult> Logout()
96+
{
97+
if (!User.Identity.IsAuthenticated)
98+
{
99+
return Redirect(Url.Content("~/"));
100+
}
101+
102+
var binding = new Saml2PostBinding();
103+
var saml2LogoutRequest = await new Saml2LogoutRequest(config, User).DeleteSession(HttpContext);
104+
return binding.Bind(saml2LogoutRequest).ToActionResult();
105+
}
106+
107+
[Route("LoggedOut")]
108+
public IActionResult LoggedOut()
109+
{
110+
var binding = new Saml2PostBinding();
111+
binding.Unbind(Request.ToGenericHttpRequest(), new Saml2LogoutResponse(config));
112+
113+
return Redirect(Url.Content("~/"));
114+
}
115+
116+
[Route("SingleLogout")]
117+
public async Task<IActionResult> SingleLogout()
118+
{
119+
Saml2StatusCodes status;
120+
var requestBinding = new Saml2PostBinding();
121+
var logoutRequest = new Saml2LogoutRequest(config, User);
122+
try
123+
{
124+
requestBinding.Unbind(Request.ToGenericHttpRequest(), logoutRequest);
125+
status = Saml2StatusCodes.Success;
126+
await logoutRequest.DeleteSession(HttpContext);
127+
}
128+
catch (Exception exc)
129+
{
130+
// log exception
131+
Debug.WriteLine("SingleLogout error: " + exc.ToString());
132+
status = Saml2StatusCodes.RequestDenied;
133+
}
134+
135+
var responsebinding = new Saml2PostBinding();
136+
responsebinding.RelayState = requestBinding.RelayState;
137+
var saml2LogoutResponse = new Saml2LogoutResponse(config)
138+
{
139+
InResponseToAsString = logoutRequest.IdAsString,
140+
Status = status,
141+
};
142+
return responsebinding.Bind(saml2LogoutResponse).ToActionResult();
143+
}
144+
}
145+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
using System.Linq;
2+
using Microsoft.AspNetCore.Mvc;
3+
using Microsoft.AspNetCore.Authorization;
4+
using System.Security.Claims;
5+
6+
namespace TestWebAppCoreNemLogin3Sp.Controllers
7+
{
8+
public class HomeController : Controller
9+
{
10+
public IActionResult Index()
11+
{
12+
return View();
13+
}
14+
15+
[Authorize]
16+
public IActionResult Secure()
17+
{
18+
// The NameIdentifier
19+
var nameIdentifier = User.Claims.Where(c => c.Type == ClaimTypes.NameIdentifier).Select(c => c.Value).Single();
20+
21+
return View();
22+
}
23+
24+
public IActionResult Error()
25+
{
26+
return View();
27+
}
28+
}
29+
}

0 commit comments

Comments
 (0)