Skip to content
This repository was archived by the owner on Apr 10, 2025. It is now read-only.

Commit c63a827

Browse files
committed
Adds SSO shorcircuiting
1 parent 7f1af4d commit c63a827

File tree

13 files changed

+287
-76
lines changed

13 files changed

+287
-76
lines changed

src/AspNetCore.IdpSample/Startup.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ public void ConfigureServices(IServiceCollection services)
5353

5454
sp.SupportedBindings.Clear();
5555
sp.SupportedBindings.Add(BindingType.Post);
56+
57+
sp.Enabled = false;
5658
});
5759
})
5860
;

src/Solid.Identity.Protocols.Saml2p/Authentication/Saml2pAuthenticationHandler.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ protected override async Task<HandleRequestResult> HandleRemoteAuthenticateAsync
4343
try
4444
{
4545
var result = await Context.FinishSsoAsync();
46+
if(!result.IsSuccessful)
47+
4648
var properties = new AuthenticationProperties
4749
{
4850
IssuedUtc = result.SecurityToken.ValidFrom,

src/Solid.Identity.Protocols.Saml2p/Cache/Saml2pCache.cs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using Microsoft.Extensions.Caching.Distributed;
2+
using Solid.Identity.Protocols.Saml2p.Models;
23
using Solid.Identity.Protocols.Saml2p.Models.Protocol;
34
using System;
45
using System.Collections.Generic;
@@ -24,12 +25,38 @@ public Task CacheRequestAsync(string key, AuthnRequest request)
2425
return _inner.SetAsync(key, json);
2526
}
2627

28+
public Task CacheStatusAsync(string key, SamlResponseStatus status)
29+
=> CacheStatusAsync(key, status, null);
30+
31+
public Task CacheStatusAsync(string key, SamlResponseStatus status, SamlResponseStatus? subStatus)
32+
=> CacheStatusAsync(key, (Status: status, SubStatus: subStatus));
33+
34+
private Task CacheStatusAsync(string key, (SamlResponseStatus Status, SamlResponseStatus? SubStatus) status)
35+
{
36+
var json = JsonSerializer.SerializeToUtf8Bytes(status);
37+
return _inner.SetAsync($"{key}_status", json);
38+
}
39+
2740
public async Task<AuthnRequest> FetchRequestAsync(string key)
2841
{
2942
var json = await _inner.GetAsync(key);
3043
if (json == null) return null;
3144

3245
return JsonSerializer.Deserialize<AuthnRequest>(json);
3346
}
47+
48+
public async Task<(SamlResponseStatus Status, SamlResponseStatus? SubStatus)?> FetchStatusAsync(string key)
49+
{
50+
var json = await _inner.GetAsync($"{key}_status");
51+
if (json == null) return null;
52+
53+
return JsonSerializer.Deserialize<(SamlResponseStatus Status, SamlResponseStatus? SubStatus)>(json);
54+
}
55+
56+
public async Task RemoveAsync(string key)
57+
{
58+
await _inner.RemoveAsync(key);
59+
await _inner.RemoveAsync($"{key}_status");
60+
}
3461
}
3562
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Runtime.Serialization;
4+
using System.Text;
5+
6+
namespace Solid.Identity.Protocols.Saml2p.Exceptions
7+
{
8+
/// <summary>
9+
/// A exception that can be thrown during SP authentication.
10+
/// </summary>
11+
public class SamlResponseException : Exception
12+
{
13+
/// <summary>
14+
/// Creates a new exception.
15+
/// </summary>
16+
/// <param name="partnerId">The id of the partner to send the response.</param>
17+
/// <param name="status">The status of the response.</param>
18+
/// <param name="subStatus">The substatus of the response.</param>
19+
public SamlResponseException(string partnerId, Uri status, Uri subStatus)
20+
: base(GenerateMessage(partnerId, status, subStatus))
21+
{
22+
}
23+
24+
private static string GenerateMessage(string partnerId, Uri status, Uri subStatus)
25+
{
26+
var builder = new StringBuilder();
27+
builder.AppendLine($"SSO using parnter '{partnerId}' failed.");
28+
builder.AppendLine($" SAMLResponse status: {status}");
29+
builder.AppendLine($" SAMLResponse substatus: {subStatus}");
30+
return builder.ToString();
31+
}
32+
}
33+
}

src/Solid.Identity.Protocols.Saml2p/Extensions/EnumExtensions.cs

Lines changed: 61 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ namespace Solid.Identity.Protocols.Saml2p.Models
66
{
77
internal static class EnumExtensions
88
{
9-
public static string ToBindingTypeString(this BindingType bindingType)
9+
public static string ToProtocolBindingString(this BindingType bindingType)
1010
{
1111
switch(bindingType)
1212
{
@@ -17,9 +17,66 @@ public static string ToBindingTypeString(this BindingType bindingType)
1717
throw new ArgumentException($"Unsupported binding type: {bindingType}");
1818
}
1919

20-
//public static string ToStatusString(this SamlResponseStatus status)
21-
//{
20+
public static Uri ToStatusUri(this SamlResponseStatus status)
21+
{
22+
switch (status)
23+
{
24+
case SamlResponseStatus.Success: return Saml2pConstants.Statuses.Success;
25+
case SamlResponseStatus.Requester: return Saml2pConstants.Statuses.Requester;
26+
case SamlResponseStatus.Responder: return Saml2pConstants.Statuses.Responder;
27+
case SamlResponseStatus.AuthnFailed: return Saml2pConstants.Statuses.AuthnFailed;
28+
case SamlResponseStatus.VersionMismatch: return Saml2pConstants.Statuses.VersionMismatch;
29+
case SamlResponseStatus.InvalidAttrNameOrValue: return Saml2pConstants.Statuses.InvalidAttrNameOrValue;
30+
case SamlResponseStatus.InvalidNameIDPolicy: return Saml2pConstants.Statuses.InvalidNameIDPolicy;
31+
case SamlResponseStatus.NoAuthnContext: return Saml2pConstants.Statuses.NoAuthnContext;
32+
case SamlResponseStatus.NoAvailableIDP: return Saml2pConstants.Statuses.NoAvailableIDP;
33+
case SamlResponseStatus.NoPassive: return Saml2pConstants.Statuses.NoPassive;
34+
case SamlResponseStatus.NoSupportedIDP: return Saml2pConstants.Statuses.NoSupportedIDP;
35+
case SamlResponseStatus.PartialLogout: return Saml2pConstants.Statuses.PartialLogout;
36+
case SamlResponseStatus.ProxyCountExceeded: return Saml2pConstants.Statuses.ProxyCountExceeded;
37+
case SamlResponseStatus.RequestDenied: return Saml2pConstants.Statuses.RequestDenied;
38+
case SamlResponseStatus.RequestUnsupported: return Saml2pConstants.Statuses.RequestUnsupported;
39+
case SamlResponseStatus.RequestVersionDeprecated: return Saml2pConstants.Statuses.RequestVersionDeprecated;
40+
case SamlResponseStatus.RequestVersionTooHigh: return Saml2pConstants.Statuses.RequestVersionTooHigh;
41+
case SamlResponseStatus.RequestVersionTooLow: return Saml2pConstants.Statuses.RequestVersionTooLow;
42+
case SamlResponseStatus.ResourceNotRecognized: return Saml2pConstants.Statuses.ResourceNotRecognized;
43+
case SamlResponseStatus.TooManyResponses: return Saml2pConstants.Statuses.TooManyResponses;
44+
case SamlResponseStatus.UnknownAttrProfile: return Saml2pConstants.Statuses.UnknownAttrProfile;
45+
case SamlResponseStatus.UnknownPrincipal: return Saml2pConstants.Statuses.UnknownPrincipal;
46+
case SamlResponseStatus.UnsupportedBinding: return Saml2pConstants.Statuses.UnsupportedBinding;
47+
}
48+
throw new ArgumentException($"Unknown status: {status}");
49+
}
2250

23-
//}
51+
public static string ToStatusString(this SamlResponseStatus status)
52+
{
53+
switch (status)
54+
{
55+
case SamlResponseStatus.Success: return Saml2pConstants.Statuses.SuccessString;
56+
case SamlResponseStatus.Requester: return Saml2pConstants.Statuses.RequesterString;
57+
case SamlResponseStatus.Responder: return Saml2pConstants.Statuses.ResponderString;
58+
case SamlResponseStatus.AuthnFailed: return Saml2pConstants.Statuses.AuthnFailedString;
59+
case SamlResponseStatus.VersionMismatch: return Saml2pConstants.Statuses.VersionMismatchString;
60+
case SamlResponseStatus.InvalidAttrNameOrValue: return Saml2pConstants.Statuses.InvalidAttrNameOrValueString;
61+
case SamlResponseStatus.InvalidNameIDPolicy: return Saml2pConstants.Statuses.InvalidNameIDPolicyString;
62+
case SamlResponseStatus.NoAuthnContext: return Saml2pConstants.Statuses.NoAuthnContextString;
63+
case SamlResponseStatus.NoAvailableIDP: return Saml2pConstants.Statuses.NoAvailableIDPString;
64+
case SamlResponseStatus.NoPassive: return Saml2pConstants.Statuses.NoPassiveString;
65+
case SamlResponseStatus.NoSupportedIDP: return Saml2pConstants.Statuses.NoSupportedIDPString;
66+
case SamlResponseStatus.PartialLogout: return Saml2pConstants.Statuses.PartialLogoutString;
67+
case SamlResponseStatus.ProxyCountExceeded: return Saml2pConstants.Statuses.ProxyCountExceededString;
68+
case SamlResponseStatus.RequestDenied: return Saml2pConstants.Statuses.RequestDeniedString;
69+
case SamlResponseStatus.RequestUnsupported: return Saml2pConstants.Statuses.RequestUnsupportedString;
70+
case SamlResponseStatus.RequestVersionDeprecated: return Saml2pConstants.Statuses.RequestVersionDeprecatedString;
71+
case SamlResponseStatus.RequestVersionTooHigh: return Saml2pConstants.Statuses.RequestVersionTooHighString;
72+
case SamlResponseStatus.RequestVersionTooLow: return Saml2pConstants.Statuses.RequestVersionTooLowString;
73+
case SamlResponseStatus.ResourceNotRecognized: return Saml2pConstants.Statuses.ResourceNotRecognizedString;
74+
case SamlResponseStatus.TooManyResponses: return Saml2pConstants.Statuses.TooManyResponsesString;
75+
case SamlResponseStatus.UnknownAttrProfile: return Saml2pConstants.Statuses.UnknownAttrProfileString;
76+
case SamlResponseStatus.UnknownPrincipal: return Saml2pConstants.Statuses.UnknownPrincipalString;
77+
case SamlResponseStatus.UnsupportedBinding: return Saml2pConstants.Statuses.UnsupportedBindingString;
78+
}
79+
throw new ArgumentException($"Unknown status: {status}");
80+
}
2481
}
2582
}

src/Solid.Identity.Protocols.Saml2p/Factories/AuthnRequestFactory.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,8 @@ public async Task<AuthnRequest> CreateAuthnRequestAsync(HttpContext context, ISa
4343
var request = new AuthnRequest
4444
{
4545
Id = $"_{Guid.NewGuid()}",
46-
ProviderName = idp.Id,
46+
// TODO: have some sort of providername default
47+
ProviderName = idp.ExpectedIssuer ?? _options.DefaultIssuer,
4748
AssertionConsumerServiceUrl = GetAcsUrl(context.Request),
4849
IssueInstant = _systemClock.UtcNow.UtcDateTime,
4950
Issuer = idp.ExpectedIssuer ?? _options.DefaultIssuer,

src/Solid.Identity.Protocols.Saml2p/Factories/SamlResponseFactory.cs

Lines changed: 17 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,14 @@ public SamlResponseFactory(IOptions<Saml2pOptions> options)
2222
public SamlResponse Create(ISaml2pServiceProvider partner, string authnRequestId = null, string relayState = null, SamlResponseStatus status = SamlResponseStatus.Success, SamlResponseStatus? subStatus = null, Saml2SecurityToken token = null)
2323
{
2424
var destination = new Uri(partner.BaseUrl, partner.AssertionConsumerServiceEndpoint);
25-
if (authnRequestId != null)
26-
token.SetRecipient(destination, authnRequestId);
27-
else
28-
token.SetRecipient(destination);
29-
token.SetNotOnOrAfter();
25+
if (token != null)
26+
{
27+
if (authnRequestId != null)
28+
token.SetRecipient(destination, authnRequestId);
29+
else
30+
token.SetRecipient(destination);
31+
token.SetNotOnOrAfter();
32+
}
3033

3134
var response = new SamlResponse
3235
{
@@ -53,40 +56,20 @@ private Status Convert(SamlResponseStatus status, SamlResponseStatus? subStatus)
5356
}
5457
};
5558

59+
var sub = Convert(subStatus);
60+
if (sub != null)
61+
converted.StatusCode.SubCode = new StatusCode
62+
{
63+
Value = sub
64+
};
65+
5666
return converted;
5767
}
5868

5969
private Uri Convert(SamlResponseStatus? status)
60-
{
61-
switch(status)
62-
{
63-
case SamlResponseStatus.Success: return Saml2pConstants.Statuses.Success;
64-
case SamlResponseStatus.AuthnFailed: return Saml2pConstants.Statuses.AuthnFailed;
65-
}
70+
{
71+
if (status.HasValue) return status.Value.ToStatusUri();
6672
return null;
67-
//"urn:oasis:names:tc:SAML:2.0:status:Success"
68-
//"urn:oasis:names:tc:SAML:2.0:status:Requester"
69-
//"urn:oasis:names:tc:SAML:2.0:status:Responder"
70-
//"urn:oasis:names:tc:SAML:2.0:status:VersionMismatch"
71-
//"urn:oasis:names:tc:SAML:2.0:status:AuthnFailed"
72-
//"urn:oasis:names:tc:SAML:2.0:status:InvalidAttrNameOrValue"
73-
//"urn:oasis:names:tc:SAML:2.0:status:InvalidNameIDPolicy"
74-
//"urn:oasis:names:tc:SAML:2.0:status:NoAuthnContext"
75-
//"urn:oasis:names:tc:SAML:2.0:status:NoAvailableIDP"
76-
//"urn:oasis:names:tc:SAML:2.0:status:NoPassive"
77-
//"urn:oasis:names:tc:SAML:2.0:status:NoSupportedIDP"
78-
//"urn:oasis:names:tc:SAML:2.0:status:PartialLogout"
79-
//"urn:oasis:names:tc:SAML:2.0:status:ProxyCountExceeded"
80-
//"urn:oasis:names:tc:SAML:2.0:status:RequestDenied"
81-
//"urn:oasis:names:tc:SAML:2.0:status:RequestUnsupported"
82-
//"urn:oasis:names:tc:SAML:2.0:status:RequestVersionDeprecated"
83-
//"urn:oasis:names:tc:SAML:2.0:status:RequestVersionTooHigh"
84-
//"urn:oasis:names:tc:SAML:2.0:status:RequestVersionTooLow"
85-
//"urn:oasis:names:tc:SAML:2.0:status:ResourceNotRecognized"
86-
//"urn:oasis:names:tc:SAML:2.0:status:TooManyResponses"
87-
//"urn:oasis:names:tc:SAML:2.0:status:UnknownAttrProfile"
88-
//"urn:oasis:names:tc:SAML:2.0:status:UnknownPrincipal"
89-
//"urn:oasis:names:tc:SAML:2.0:status:UnsupportedBinding"
9073
}
9174
}
9275
}

src/Solid.Identity.Protocols.Saml2p/Middleware/Idp/AcceptSsoEndpointMiddleware.cs

Lines changed: 60 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
using Solid.Identity.Protocols.Saml2p.Models.Context;
1616
using Solid.Identity.Protocols.Saml2p.Providers;
1717
using Solid.Identity.Protocols.Saml2p.Services;
18+
using Solid.Identity.Protocols.Saml2p.Models;
19+
using Microsoft.IdentityModel.Tokens.Saml2;
1820

1921
namespace Solid.Identity.Protocols.Saml2p.Middleware.Idp
2022
{
@@ -41,33 +43,81 @@ public override async Task InvokeAsync(HttpContext context)
4143

4244
Logger.LogInformation("Accepting SAML2P authentication (IDP flow).");
4345
Trace($"Received SAMLRequest using {binding} binding.", request);
44-
var partnerId = request.Issuer;
45-
var partner = await Partners.GetServiceProviderAsync(partnerId);
46+
var partner = await Partners.GetServiceProviderAsync(request.Issuer);
4647

4748
if (partner == null)
48-
throw new SecurityException($"Partner '{partnerId}' not found.");
49+
throw new SecurityException($"Partner '{request.Issuer}' not found.");
4950

50-
if (!partner.Enabled)
51-
throw new SecurityException($"Partner '{partnerId}' is disabled.");
51+
//if (!partner.Enabled)
52+
// throw new SecurityException($"Partner '{partnerId}' is disabled.");
5253

53-
if (!partner.CanInitiateSso)
54-
throw new SecurityException($"Partner '{partnerId}' is not allowed to initiate SSO.");
54+
//if (!partner.CanInitiateSso)
55+
// throw new SecurityException($"Partner '{partnerId}' is not allowed to initiate SSO.");
5556

5657
await Cache.CacheRequestAsync(request.Id, request);
5758
var ssoContext = new AcceptSsoContext
5859
{
59-
PartnerId = partner.Id,
60+
PartnerId = request.Issuer,
6061
Partner = partner,
6162
Request = request,
6263
ReturnUrl = GenerateReturnUrl(context, request.Id)
6364
};
6465

6566
await Events.InvokeAsync(Options, partner, e => e.OnAcceptSso(context.RequestServices, ssoContext));
66-
67-
if(ssoContext.AuthenticationScheme != null)
67+
68+
if (!IsValid(ssoContext, out var status, out var subStatus) && status.HasValue)
69+
{
70+
await Cache.CacheStatusAsync(request.Id, status.Value, subStatus);
71+
context.Response.Redirect(ssoContext.ReturnUrl);
72+
}
73+
else if (ssoContext.AuthenticationScheme != null)
6874
await ChallengeAsync(context, request, ssoContext.ReturnUrl, ssoContext.AuthenticationScheme);
6975
else
7076
await ChallengeAsync(context, request, ssoContext.ReturnUrl);
7177
}
78+
79+
// TODO: extract this to a validator class and test it
80+
private bool IsValid(AcceptSsoContext context, out SamlResponseStatus? status, out SamlResponseStatus? subStatus)
81+
{
82+
if(context.Request.Version != Saml2Constants.Version)
83+
{
84+
status = SamlResponseStatus.VersionMismatch;
85+
subStatus = null;
86+
return false;
87+
}
88+
89+
if (!string.IsNullOrEmpty(context.Request.ProtocolBinding) &&
90+
Options.SupportedBindings.Select(b => b.ToProtocolBindingString()).Any(b => b == context.Request.ProtocolBinding))
91+
{
92+
status = SamlResponseStatus.Requester;
93+
subStatus = SamlResponseStatus.UnsupportedBinding;
94+
return false;
95+
}
96+
97+
if (context.Partner == null)
98+
{
99+
status = SamlResponseStatus.Requester;
100+
subStatus = SamlResponseStatus.RequestDenied;
101+
return false;
102+
}
103+
104+
if (!context.Partner.Enabled || !context.Partner.CanInitiateSso)
105+
{
106+
status = SamlResponseStatus.Requester;
107+
subStatus = SamlResponseStatus.RequestDenied;
108+
return false;
109+
}
110+
111+
if (!context.Partner.SupportedBindings.Any())
112+
{
113+
status = SamlResponseStatus.Responder;
114+
subStatus = SamlResponseStatus.UnsupportedBinding;
115+
return false;
116+
}
117+
118+
status = null;
119+
subStatus = null;
120+
return true;
121+
}
72122
}
73123
}

0 commit comments

Comments
 (0)