Skip to content

Commit 7635cf0

Browse files
authored
Merge pull request #112 from ITfoxtec/development
Development
2 parents 7cfe256 + c28dcd0 commit 7635cf0

7 files changed

Lines changed: 255 additions & 131 deletions

File tree

src/AspNetCoreSamlIdPSample/Controllers/SamlController.cs

Lines changed: 133 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@
1919
using FoxIDs.SampleHelperLibrary.Models;
2020
using ITfoxtec.Identity.Util;
2121
using ITfoxtec.Identity.Saml2.Claims;
22+
using System.Web;
23+
using Microsoft.AspNetCore.Mvc.Rendering;
24+
using ITfoxtec.Identity;
2225

2326
namespace AspNetCoreSamlIdPSample.Controllers
2427
{
@@ -48,30 +51,30 @@ public IActionResult Metadata()
4851
entityDescriptor.ValidUntil = 365;
4952
entityDescriptor.IdPSsoDescriptor = new IdPSsoDescriptor
5053
{
51-
SigningCertificates = new X509Certificate2[]
52-
{
54+
SigningCertificates =
55+
[
5356
saml2Config.SigningCertificate
54-
},
55-
//EncryptionCertificates = new X509Certificate2[]
56-
//{
57+
],
58+
//EncryptionCertificates =
59+
//[
5760
// saml2Config.DecryptionCertificate
58-
//},
59-
SingleSignOnServices = new SingleSignOnService[]
60-
{
61+
//],
62+
SingleSignOnServices =
63+
[
6164
new SingleSignOnService { Binding = ProtocolBindings.HttpRedirect, Location = new Uri(UrlCombine.Combine(defaultSite, "/Saml/Login")) }
62-
},
63-
SingleLogoutServices = new SingleLogoutService[]
64-
{
65+
],
66+
SingleLogoutServices =
67+
[
6568
new SingleLogoutService { Binding = ProtocolBindings.HttpPost, Location = new Uri(UrlCombine.Combine(defaultSite, "/Saml/Logout")) }
66-
},
67-
NameIDFormats = new Uri[] { NameIdentifierFormats.X509SubjectName },
69+
],
70+
NameIDFormats = [NameIdentifierFormats.X509SubjectName],
6871
};
69-
entityDescriptor.ContactPersons = new [] {
72+
entityDescriptor.ContactPersons = [
7073
new ContactPerson(ContactTypes.Administrative)
7174
{
7275
Company = "Some sample IdP",
7376
}
74-
};
77+
];
7578
return new Saml2Metadata(entityDescriptor).CreateMetadata().ToActionResult();
7679
}
7780

@@ -86,26 +89,9 @@ public async Task<IActionResult> Login()
8689
{
8790
httpRequest.Binding.Unbind(httpRequest, saml2AuthnRequest);
8891

89-
// **** Handle user login e.g. in GUI ****
90-
// Test user with session index and claims
91-
var session = await idPSessionCookieRepository.GetAsync();
92-
if (session == null)
93-
{
94-
session = new IdPSession
95-
{
96-
RelyingPartyIssuer = relyingParty.Issuer,
97-
NameIdentifier = "12345",
98-
Upn = "12345@email.test",
99-
Email = "some@email.test",
100-
CustomId = "123abc",
101-
CustomName = "Test Users Custom Full Name",
102-
SessionIndex = Guid.NewGuid().ToString()
103-
};
104-
await idPSessionCookieRepository.SaveAsync(session);
105-
}
106-
var claims = GetClaims(session);
107-
108-
return LoginResponse(saml2AuthnRequest.Id, Saml2StatusCodes.Success, httpRequest.Binding.RelayState, relyingParty, session.SessionIndex, claims);
92+
var session = await GetSession(relyingParty);
93+
94+
return LoginResponse(saml2AuthnRequest.Id, Saml2StatusCodes.Success, httpRequest.Binding.RelayState, relyingParty, session.SessionIndex, GetClaims(session));
10995
}
11096
catch (Exception ex)
11197
{
@@ -114,20 +100,78 @@ public async Task<IActionResult> Login()
114100
}
115101
}
116102

117-
private Saml2Configuration GetLoginSaml2Config(RelyingParty relyingParty)
103+
[Route("IdPInitiated")]
104+
public IActionResult IdPInitiated()
118105
{
119-
var loginSaml2Config = new Saml2Configuration
106+
return base.View(new IdPInitiatedViewModel { RelyingPartyIssuers = GetRelyingPartyListItems() });
107+
}
108+
109+
[HttpPost("IdPInitiated")]
110+
public async Task<IActionResult> IdPInitiated(IdPInitiatedViewModel idPInitiatedViewModel)
111+
{
112+
if ("oidc".Equals(idPInitiatedViewModel.ApplicationType, StringComparison.OrdinalIgnoreCase) && idPInitiatedViewModel.ApplicationRedirectURL.IsNullOrWhiteSpace())
120113
{
121-
Issuer = saml2Config.Issuer,
122-
SigningCertificate = saml2Config.SigningCertificate,
123-
SignatureAlgorithm = saml2Config.SignatureAlgorithm,
124-
CertificateValidationMode = saml2Config.CertificateValidationMode,
125-
RevocationMode = saml2Config.RevocationMode
126-
};
127-
loginSaml2Config.AllowedAudienceUris.AddRange(saml2Config.AllowedAudienceUris);
128-
loginSaml2Config.EncryptionCertificate = relyingParty.EncryptionCertificate;
114+
ModelState.AddModelError(nameof(idPInitiatedViewModel.ApplicationRedirectURL), $"The {nameof(idPInitiatedViewModel.ApplicationRedirectURL)} field is required for OpenID Connect (oidc)");
115+
}
129116

130-
return loginSaml2Config;
117+
if (!ModelState.IsValid)
118+
{
119+
idPInitiatedViewModel.RelyingPartyIssuers = GetRelyingPartyListItems();
120+
return View(idPInitiatedViewModel);
121+
}
122+
123+
var relyingParty = ValidateRelyingParty(idPInitiatedViewModel.RelyingPartyIssuer);
124+
125+
var binding = new Saml2PostBinding();
126+
binding.RelayState = string.Join('&', GetRelayState(idPInitiatedViewModel));
127+
128+
var response = new Saml2AuthnResponse(GetLoginSaml2Config(relyingParty));
129+
response.Status = Saml2StatusCodes.Success;
130+
131+
var session = await GetSession(relyingParty);
132+
133+
return LoginResponse(null, Saml2StatusCodes.Success, binding.RelayState, relyingParty, session.SessionIndex, GetClaims(session));
134+
135+
136+
//var claimsIdentity = new ClaimsIdentity(GetClaims(session));
137+
//response.NameId = new Saml2NameIdentifier(claimsIdentity.Claims.Where(c => c.Type == ClaimTypes.NameIdentifier).Select(c => c.Value).Single(), NameIdentifierFormats.Persistent);
138+
//response.ClaimsIdentity = claimsIdentity;
139+
//var token = response.CreateSecurityToken(relyingParty.Issuer);
140+
141+
//return binding.Bind(response).ToActionResult();
142+
}
143+
144+
private IEnumerable<string> GetRelayState(IdPInitiatedViewModel idPInitiatedViewModel)
145+
{
146+
yield return $"app_name={HttpUtility.UrlEncode(idPInitiatedViewModel.ApplicationName.ToLower())}";
147+
yield return $"app_type={idPInitiatedViewModel.ApplicationType.ToLower()}";
148+
if (!idPInitiatedViewModel.ApplicationRedirectURL.IsNullOrWhiteSpace())
149+
{
150+
yield return $"app_redirect={HttpUtility.UrlEncode(idPInitiatedViewModel.ApplicationRedirectURL)}";
151+
}
152+
}
153+
154+
private async Task<IdPSession> GetSession(RelyingParty relyingParty)
155+
{
156+
// **** Handle user login e.g. in GUI ****
157+
// Test user with session index and claims
158+
var session = await idPSessionCookieRepository.GetAsync();
159+
if (session == null)
160+
{
161+
session = new IdPSession
162+
{
163+
RelyingPartyIssuer = relyingParty.Issuer,
164+
NameIdentifier = "12345",
165+
Upn = "12345@email.test",
166+
Email = "some@email.test",
167+
CustomId = "123abc",
168+
CustomName = "Test Users Custom Full Name",
169+
SessionIndex = Guid.NewGuid().ToString()
170+
};
171+
await idPSessionCookieRepository.SaveAsync(session);
172+
}
173+
174+
return session;
131175
}
132176

133177
private IEnumerable<Claim> GetClaims(IdPSession idPSession)
@@ -145,6 +189,34 @@ private IEnumerable<Claim> GetClaims(IdPSession idPSession)
145189
yield return new Claim(Saml2ClaimTypes.SessionIndex, idPSession.SessionIndex);
146190
}
147191

192+
private IEnumerable<SelectListItem> GetRelyingPartyListItems() => settings.RelyingParties.Select(r => new SelectListItem(r.SingleSignOnDestination.OriginalString, r.Issuer));
193+
194+
private IActionResult LoginResponse(Saml2Id inResponseTo, Saml2StatusCodes status, string relayState, RelyingParty relyingParty, string sessionIndex = null, IEnumerable<Claim> claims = null)
195+
{
196+
var responsebinding = new Saml2PostBinding();
197+
responsebinding.RelayState = relayState;
198+
199+
var saml2AuthnResponse = new Saml2AuthnResponse(GetLoginSaml2Config(relyingParty))
200+
{
201+
InResponseTo = inResponseTo,
202+
Status = status,
203+
Destination = relyingParty.SingleSignOnDestination,
204+
};
205+
if (status == Saml2StatusCodes.Success && claims != null)
206+
{
207+
saml2AuthnResponse.SessionIndex = sessionIndex;
208+
209+
var claimsIdentity = new ClaimsIdentity(claims);
210+
saml2AuthnResponse.NameId = new Saml2NameIdentifier(claimsIdentity.Claims.Where(c => c.Type == ClaimTypes.NameIdentifier).Select(c => c.Value).Single(), NameIdentifierFormats.Persistent);
211+
//saml2AuthnResponse.NameId = new Saml2NameIdentifier(claimsIdentity.Claims.Where(c => c.Type == ClaimTypes.NameIdentifier).Select(c => c.Value).Single());
212+
saml2AuthnResponse.ClaimsIdentity = claimsIdentity;
213+
214+
_ = saml2AuthnResponse.CreateSecurityToken(relyingParty.Issuer);
215+
}
216+
217+
return responsebinding.Bind(saml2AuthnResponse).ToActionResult();
218+
}
219+
148220
[Route("Logout")]
149221
public async Task<IActionResult> Logout()
150222
{
@@ -222,46 +294,36 @@ private string ReadRelyingPartyFromLogoutResponse(Saml2Http.HttpRequest httpRequ
222294
return httpRequest.Binding.ReadSamlResponse(httpRequest, new Saml2LogoutResponse(saml2Config))?.Issuer;
223295
}
224296

225-
private IActionResult LoginResponse(Saml2Id inResponseTo, Saml2StatusCodes status, string relayState, RelyingParty relyingParty, string sessionIndex = null, IEnumerable<Claim> claims = null)
297+
private IActionResult LogoutResponse(Saml2Id inResponseTo, Saml2StatusCodes status, string relayState, string sessionIndex, RelyingParty relyingParty)
226298
{
227299
var responsebinding = new Saml2PostBinding();
228300
responsebinding.RelayState = relayState;
229301

230-
var saml2AuthnResponse = new Saml2AuthnResponse(GetLoginSaml2Config(relyingParty))
302+
var saml2LogoutResponse = new Saml2LogoutResponse(saml2Config)
231303
{
232304
InResponseTo = inResponseTo,
233305
Status = status,
234-
Destination = relyingParty.SingleSignOnDestination,
306+
Destination = relyingParty.SingleLogoutResponseDestination,
307+
SessionIndex = sessionIndex
235308
};
236-
if (status == Saml2StatusCodes.Success && claims != null)
237-
{
238-
saml2AuthnResponse.SessionIndex = sessionIndex;
239309

240-
var claimsIdentity = new ClaimsIdentity(claims);
241-
saml2AuthnResponse.NameId = new Saml2NameIdentifier(claimsIdentity.Claims.Where(c => c.Type == ClaimTypes.NameIdentifier).Select(c => c.Value).Single(), NameIdentifierFormats.Persistent);
242-
//saml2AuthnResponse.NameId = new Saml2NameIdentifier(claimsIdentity.Claims.Where(c => c.Type == ClaimTypes.NameIdentifier).Select(c => c.Value).Single());
243-
saml2AuthnResponse.ClaimsIdentity = claimsIdentity;
244-
245-
_ = saml2AuthnResponse.CreateSecurityToken(relyingParty.Issuer);
246-
}
247-
248-
return responsebinding.Bind(saml2AuthnResponse).ToActionResult();
310+
return responsebinding.Bind(saml2LogoutResponse).ToActionResult();
249311
}
250312

251-
private IActionResult LogoutResponse(Saml2Id inResponseTo, Saml2StatusCodes status, string relayState, string sessionIndex, RelyingParty relyingParty)
313+
private Saml2Configuration GetLoginSaml2Config(RelyingParty relyingParty)
252314
{
253-
var responsebinding = new Saml2PostBinding();
254-
responsebinding.RelayState = relayState;
255-
256-
var saml2LogoutResponse = new Saml2LogoutResponse(saml2Config)
315+
var loginSaml2Config = new Saml2Configuration
257316
{
258-
InResponseTo = inResponseTo,
259-
Status = status,
260-
Destination = relyingParty.SingleLogoutResponseDestination,
261-
SessionIndex = sessionIndex
317+
Issuer = saml2Config.Issuer,
318+
SigningCertificate = saml2Config.SigningCertificate,
319+
SignatureAlgorithm = saml2Config.SignatureAlgorithm,
320+
CertificateValidationMode = saml2Config.CertificateValidationMode,
321+
RevocationMode = saml2Config.RevocationMode
262322
};
323+
loginSaml2Config.AllowedAudienceUris.AddRange(saml2Config.AllowedAudienceUris);
324+
loginSaml2Config.EncryptionCertificate = relyingParty.EncryptionCertificate;
263325

264-
return responsebinding.Bind(saml2LogoutResponse).ToActionResult();
326+
return loginSaml2Config;
265327
}
266328

267329
private RelyingParty ValidateRelyingParty(string issuer)

src/AspNetCoreSamlIdPSample/Models/ErrorViewModel.cs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
using System;
2-
31
namespace AspNetCoreSamlIdPSample.Models
42
{
53
public class ErrorViewModel
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
using Microsoft.AspNetCore.Mvc.Rendering;
2+
using System.Collections.Generic;
3+
using System.ComponentModel.DataAnnotations;
4+
5+
namespace AspNetCoreSamlIdPSample.Models
6+
{
7+
public class IdPInitiatedViewModel
8+
{
9+
[Display(Name = "Authentication method")]
10+
[Required]
11+
[MaxLength(500)]
12+
public string RelyingPartyIssuer { get; set; }
13+
14+
public IEnumerable<SelectListItem> RelyingPartyIssuers { get; set; }
15+
16+
[Display(Name = "Application (technical name)")]
17+
[Required]
18+
[MaxLength(500)]
19+
public string ApplicationName { get; set; }
20+
21+
[Display(Name = "Application redirect URL - required for OpenID Connect (oidc)")]
22+
[MaxLength(500)]
23+
public string ApplicationRedirectURL { get; set; }
24+
25+
[Display(Name = "Application type ('oidc' or 'saml2')")]
26+
[Required]
27+
[MaxLength(10)]
28+
public string ApplicationType { get; set; }
29+
}
30+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
@model IdPInitiatedViewModel
2+
@{
3+
ViewBag.Title = "IdP Initiated Login";
4+
}
5+
6+
<h2>@ViewBag.Title</h2>
7+
8+
<div class="row">
9+
<div class="col-md-12">
10+
<blockquote>
11+
<p>Select the authentication method and fill in the name of the application that the request should be redirected to after successful login.</p>
12+
<h3>Test data</h3>
13+
<p>
14+
<strong>SAML 2.0 sample application</strong>
15+
<div>Application: aspnetcore_saml_sample</div>
16+
<i>The application redirect URL is optional</i>
17+
<div>Application type: saml2</div>
18+
</p>
19+
<p>
20+
<strong>OpenID Connect sample application</strong>
21+
<div>Application: aspnet_oidc_allup_sample</div>
22+
<div>Application redirect URL: https://localhost:44349/home/secure</div>
23+
<i>The redirect URL need to be configured as a redirect URL for the application.</i>
24+
<div>Application type: oidc</div>
25+
</p>
26+
</blockquote>
27+
</div>
28+
</div>
29+
<div class="row">
30+
<div class="col-md-12">
31+
<form method="post">
32+
<div asp-validation-summary="ModelOnly"></div>
33+
<div class="form-group">
34+
<label asp-for="RelyingPartyIssuer" class="control-label"></label>
35+
<select asp-for="RelyingPartyIssuer" asp-items="@Model.RelyingPartyIssuers" class="form-control"></select>
36+
<span asp-validation-for="RelyingPartyIssuer" class="text-danger"></span>
37+
</div>
38+
<div class="form-group">
39+
<label asp-for="ApplicationName" class="label-control"></label>
40+
<input asp-for="ApplicationName" autocomplete="off" class="form-control input-control" autofocus />
41+
<span asp-validation-for="ApplicationName"></span>
42+
</div>
43+
<div class="form-group">
44+
<label asp-for="ApplicationRedirectURL" class="label-control"></label>
45+
<input asp-for="ApplicationRedirectURL" autocomplete="off" class="form-control input-control" autofocus />
46+
<span asp-validation-for="ApplicationRedirectURL"></span>
47+
</div>
48+
<div class="form-group">
49+
<label asp-for="ApplicationType" class="label-control"></label>
50+
<input asp-for="ApplicationType" autocomplete="off" class="form-control input-control" autofocus />
51+
<span asp-validation-for="ApplicationType"></span>
52+
</div>
53+
<div class="form-group">
54+
<input type="submit" value="Login" class="btn btn-primary" />
55+
</div>
56+
</form>
57+
</div>
58+
</div>
59+
60+
@section Scripts {
61+
@{
62+
await Html.RenderPartialAsync("_ValidationScriptsPartial");
63+
}
64+
}

src/AspNetCoreSamlIdPSample/Views/Shared/_Layout.cshtml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,12 @@
2626
<span class="icon-bar"></span>
2727
<span class="icon-bar"></span>
2828
</button>
29-
<a asp-area="" asp-controller="Home" asp-action="Index" class="navbar-brand">AspNetCoreSamlIdPSample</a>
29+
<a asp-controller="Home" asp-action="Index" class="navbar-brand">AspNetCoreSamlIdPSample</a>
3030
</div>
3131
<div class="navbar-collapse collapse">
3232
<ul class="nav navbar-nav">
33-
<li><a asp-area="" asp-controller="Home" asp-action="Index">Home</a></li>
33+
<li><a asp-controller="Home" asp-action="Index">Home</a></li>
34+
<li><a asp-controller="Saml" asp-action="IdPInitiated">IdP Initiated Login</a></li>
3435
<li><a asp-controller="Saml" asp-action="Metadata">Metadata</a></li>
3536
</ul>
3637
</div>

src/AspNetCoreSamlIdPSample/wwwroot/css/site.css

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,7 @@ body {
3535
display: none;
3636
}
3737
}
38+
39+
.field-validation-error {
40+
color: red;
41+
}

0 commit comments

Comments
 (0)