diff --git a/Owin.Security.Providers/OpenID/Extensions/OpenIDSimpleRegistrationAuthenticationContextExtensions.cs b/Owin.Security.Providers/OpenID/Extensions/OpenIDSimpleRegistrationAuthenticationContextExtensions.cs new file mode 100644 index 0000000..1001d11 --- /dev/null +++ b/Owin.Security.Providers/OpenID/Extensions/OpenIDSimpleRegistrationAuthenticationContextExtensions.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Owin.Security.Providers.OpenID.Extensions +{ + /// + /// Contains an extension method that makes reading the SREG fields easier. + /// + public static class OpenIDSimpleRegistrationAuthenticationContextExtensions + { + public static OpenIDSimpleRegistrationResult GetSimpleRegistrationResult(this OpenIDAuthenticatedContext context) + { + if (!context.ProtocolExtensionData.ContainsKey(typeof (OpenIDSimpleRegistrationExtension))) + { + return new OpenIDSimpleRegistrationResult(); + } + else + { + return context.ProtocolExtensionData[typeof (OpenIDSimpleRegistrationExtension)] as OpenIDSimpleRegistrationResult; + } + } + } +} \ No newline at end of file diff --git a/Owin.Security.Providers/OpenID/Extensions/OpenIDSimpleRegistrationExtension.cs b/Owin.Security.Providers/OpenID/Extensions/OpenIDSimpleRegistrationExtension.cs new file mode 100644 index 0000000..5593ea7 --- /dev/null +++ b/Owin.Security.Providers/OpenID/Extensions/OpenIDSimpleRegistrationExtension.cs @@ -0,0 +1,109 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using System.Threading.Tasks; + +namespace Owin.Security.Providers.OpenID.Extensions +{ + /// + /// Implements the OpenID Simple Registration Extension http://openid.net/specs/openid-simple-registration-extension-1_0.html + /// + public class OpenIDSimpleRegistrationExtension : IOpenIDProtocolExtension + { + + private static readonly Dictionary claimsMap = new Dictionary() + { + { OpenIDSimpleRegistrationField.NickName, "nickname" }, + { OpenIDSimpleRegistrationField.FullName, "fullname" }, + { OpenIDSimpleRegistrationField.Email, "email" }, + { OpenIDSimpleRegistrationField.DayOfBirth, "dob" }, + { OpenIDSimpleRegistrationField.Gender, "gender" }, + { OpenIDSimpleRegistrationField.PostCode, "postcode" }, + { OpenIDSimpleRegistrationField.Country, "country" }, + { OpenIDSimpleRegistrationField.Language, "language" }, + { OpenIDSimpleRegistrationField.Timezone, "timezone" } + }; + + private const string sregNamespace = "http://openid.net/extensions/sreg/1.1"; + + + /// + /// Gets or sets a list of comma-separated SREG fields that are required. + /// + public HashSet RequiredFields { get; private set; } + + /// + /// Gets or sets a list of comma-separated SREG fields that are optional. + /// + public HashSet OptionalFields { get; private set; } + + /// + /// Gets or sets the SREG policy URL. + /// + public string PolicyUrl { get; set; } + + + /// + /// Initializes a new instance of the class. + /// + public OpenIDSimpleRegistrationExtension() + { + RequiredFields = new HashSet() { OpenIDSimpleRegistrationField.Email, OpenIDSimpleRegistrationField.FullName }; + OptionalFields = new HashSet(); + PolicyUrl = string.Empty; + } + + + /// + /// Appends the SREG required attributes to the request URL constructed on challenge. + /// + public Task OnChallengeAsync(Microsoft.Owin.Security.AuthenticationResponseChallenge challenge, OpenIDAuthorizationEndpointInfo endpoint) + { + endpoint.Url += "&openid.ns.sreg=" + Uri.EscapeDataString(sregNamespace); + + var requiredClaims = string.Join(",", RequiredFields.Select(f => claimsMap[f])); + endpoint.Url += "&openid.sreg.required=" + Uri.EscapeDataString(requiredClaims); + + if (OptionalFields.Any()) + { + var optionalClaims = string.Join(",", OptionalFields.Select(f => claimsMap[f])); + endpoint.Url += "&openid.sreg.optional=" + Uri.EscapeDataString(optionalClaims); + } + + if (!string.IsNullOrEmpty(PolicyUrl)) + { + endpoint.Url += "&openid.sreg.policy_url=" + Uri.EscapeDataString(PolicyUrl); + } + + return Task.FromResult(0); + } + + /// + /// Validates the authentication response message. + /// + public Task OnValidateMessageAsync(Infrastructure.Message message) + { + // no additional checks needed + return Task.FromResult(true); + } + + /// + /// Extracts SREG attributes and returns the results. + /// + public Task OnExtractResults(ClaimsIdentity identity, string claimedId, Infrastructure.Message message) + { + var result = new OpenIDSimpleRegistrationResult(); + foreach (var claim in claimsMap) + { + string value; + if (message.TryGetValue(claim.Value + "." + sregNamespace, out value)) + { + result.Values.Add(claim.Key, value); + } + } + + return Task.FromResult((object)result); + } + } +} diff --git a/Owin.Security.Providers/OpenID/Extensions/OpenIDSimpleRegistrationField.cs b/Owin.Security.Providers/OpenID/Extensions/OpenIDSimpleRegistrationField.cs new file mode 100644 index 0000000..daf1817 --- /dev/null +++ b/Owin.Security.Providers/OpenID/Extensions/OpenIDSimpleRegistrationField.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Owin.Security.Providers.OpenID.Extensions +{ + public enum OpenIDSimpleRegistrationField + { + NickName, + FullName, + Email, + DayOfBirth, + Gender, + PostCode, + Country, + Language, + Timezone + } +} \ No newline at end of file diff --git a/Owin.Security.Providers/OpenID/Extensions/OpenIDSimpleRegistrationResult.cs b/Owin.Security.Providers/OpenID/Extensions/OpenIDSimpleRegistrationResult.cs new file mode 100644 index 0000000..5aa7256 --- /dev/null +++ b/Owin.Security.Providers/OpenID/Extensions/OpenIDSimpleRegistrationResult.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Owin.Security.Providers.OpenID.Extensions +{ + /// + /// Contains values of OpenID Simple Registration Extension fields. + /// + public class OpenIDSimpleRegistrationResult + { + + public IDictionary Values { get; set; } + + + /// + /// Initializes a new instance of the class. + /// + public OpenIDSimpleRegistrationResult() + { + Values = new Dictionary(); + } + + /// + /// Gets the SREG field value. + /// + public string GetFieldValue(OpenIDSimpleRegistrationField field) + { + if (!Values.ContainsKey(field)) return null; + return Values[field]; + } + + } +} \ No newline at end of file diff --git a/Owin.Security.Providers/OpenID/IOpenIDProtocolExtension.cs b/Owin.Security.Providers/OpenID/IOpenIDProtocolExtension.cs new file mode 100644 index 0000000..384e660 --- /dev/null +++ b/Owin.Security.Providers/OpenID/IOpenIDProtocolExtension.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Owin.Security; + +namespace Owin.Security.Providers.OpenID +{ + public interface IOpenIDProtocolExtension + { + + /// + /// Adds the required information in the authorization endpoint URL. + /// + Task OnChallengeAsync(AuthenticationResponseChallenge challenge, OpenIDAuthorizationEndpointInfo endpoint); + + /// + /// Performs additional authentication response message validations. + /// + Task OnValidateMessageAsync(Infrastructure.Message message); + + /// + /// Extracts the data form the authentication response message and returns them. + /// + Task OnExtractResults(System.Security.Claims.ClaimsIdentity identity, string claimedId, Infrastructure.Message message); + } +} \ No newline at end of file diff --git a/Owin.Security.Providers/OpenID/Infrastructure/Message.cs b/Owin.Security.Providers/OpenID/Infrastructure/Message.cs index dba4982..bc6b265 100644 --- a/Owin.Security.Providers/OpenID/Infrastructure/Message.cs +++ b/Owin.Security.Providers/OpenID/Infrastructure/Message.cs @@ -5,7 +5,7 @@ using System.Linq; namespace Owin.Security.Providers.OpenID.Infrastructure { - internal class Message + public class Message { public Message(IReadableStringCollection parameters, bool strict) { diff --git a/Owin.Security.Providers/OpenID/Infrastructure/Property.cs b/Owin.Security.Providers/OpenID/Infrastructure/Property.cs index 16e402c..5729cd8 100644 --- a/Owin.Security.Providers/OpenID/Infrastructure/Property.cs +++ b/Owin.Security.Providers/OpenID/Infrastructure/Property.cs @@ -1,7 +1,7 @@  namespace Owin.Security.Providers.OpenID.Infrastructure { - internal class Property + public class Property { public string Key { get; set; } public string Namespace { get; set; } diff --git a/Owin.Security.Providers/OpenID/OpenIDAuthenticationHandler.cs b/Owin.Security.Providers/OpenID/OpenIDAuthenticationHandler.cs index 342ed5e..b5b5630 100644 --- a/Owin.Security.Providers/OpenID/OpenIDAuthenticationHandler.cs +++ b/Owin.Security.Providers/OpenID/OpenIDAuthenticationHandler.cs @@ -153,6 +153,15 @@ namespace Owin.Security.Providers.OpenID } } + // Allow protocol extensions to add custom message validation rules + foreach (var protocolExtension in Options.ProtocolExtensions) + { + if (!await protocolExtension.OnValidateMessageAsync(message)) + { + messageValidated = false; + } + } + if (messageValidated) { IDictionary attributeExchangeProperties = new Dictionary(); @@ -190,7 +199,7 @@ namespace Owin.Security.Providers.OpenID } SetIdentityInformations(identity, claimedId.Value, attributeExchangeProperties); - + var context = new OpenIDAuthenticatedContext( Context, identity, @@ -198,6 +207,14 @@ namespace Owin.Security.Providers.OpenID responseMessage, attributeExchangeProperties); + + // Let protocol extensions to extract the results from the message + foreach (var protocolExtension in Options.ProtocolExtensions) + { + var result = await protocolExtension.OnExtractResults(identity, claimedId.Value, message); + context.ProtocolExtensionData[protocolExtension.GetType()] = result; + } + await Options.Provider.Authenticated(context); return new AuthenticationTicket(context.Identity, context.Properties); @@ -353,8 +370,18 @@ namespace Owin.Security.Providers.OpenID "&openid.ax.required=" + Uri.EscapeDataString("email,name,first,last,email2,name2,first2,last2"); + // allow protocol extensions to add their own attributes to the endpoint URL + var endpoint = new OpenIDAuthorizationEndpointInfo() + { + Url = authorizationEndpoint + }; + foreach (var protocolExtension in Options.ProtocolExtensions) + { + await protocolExtension.OnChallengeAsync(challenge, endpoint); + } + Response.StatusCode = 302; - Response.Headers.Set("Location", authorizationEndpoint); + Response.Headers.Set("Location", endpoint.Url); } } } diff --git a/Owin.Security.Providers/OpenID/OpenIDAuthenticationOptions.cs b/Owin.Security.Providers/OpenID/OpenIDAuthenticationOptions.cs index 2c50465..0954cec 100644 --- a/Owin.Security.Providers/OpenID/OpenIDAuthenticationOptions.cs +++ b/Owin.Security.Providers/OpenID/OpenIDAuthenticationOptions.cs @@ -1,4 +1,5 @@ -using Microsoft.Owin; +using System.Collections.Generic; +using Microsoft.Owin; using Microsoft.Owin.Security; using System; using System.Net.Http; @@ -77,6 +78,12 @@ namespace Owin.Security.Providers.OpenID /// public string ProviderLoginUri { get; set; } + /// + /// A list of protocol extensions. + /// + public List ProtocolExtensions { get; set; } + + /// /// Initializes a new /// @@ -87,6 +94,7 @@ namespace Owin.Security.Providers.OpenID CallbackPath = new PathString("/signin-openid"); AuthenticationMode = AuthenticationMode.Passive; BackchannelTimeout = TimeSpan.FromSeconds(60); + ProtocolExtensions = new List(); } } } diff --git a/Owin.Security.Providers/OpenID/OpenIDAuthorizationEndpointInfo.cs b/Owin.Security.Providers/OpenID/OpenIDAuthorizationEndpointInfo.cs new file mode 100644 index 0000000..99be04a --- /dev/null +++ b/Owin.Security.Providers/OpenID/OpenIDAuthorizationEndpointInfo.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Owin.Security.Providers.OpenID +{ + public class OpenIDAuthorizationEndpointInfo + { + + public string Url { get; set; } + + } +} \ No newline at end of file diff --git a/Owin.Security.Providers/OpenID/Provider/OpenIDAuthenticatedContext.cs b/Owin.Security.Providers/OpenID/Provider/OpenIDAuthenticatedContext.cs index f83120c..4cd566c 100644 --- a/Owin.Security.Providers/OpenID/Provider/OpenIDAuthenticatedContext.cs +++ b/Owin.Security.Providers/OpenID/Provider/OpenIDAuthenticatedContext.cs @@ -1,4 +1,5 @@ -using Microsoft.Owin; +using System; +using Microsoft.Owin; using Microsoft.Owin.Security; using Microsoft.Owin.Security.Provider; using System.Collections.Generic; @@ -12,7 +13,7 @@ namespace Owin.Security.Providers.OpenID /// public class OpenIDAuthenticatedContext : BaseContext { - /// + /// /// Initializes a /// /// The OWIN environment @@ -32,6 +33,7 @@ namespace Owin.Security.Providers.OpenID Properties = properties; ResponseMessage = responseMessage; AttributeExchangeProperties = attributeExchangeProperties; + ProtocolExtensionData = new Dictionary(); } /// @@ -47,5 +49,7 @@ namespace Owin.Security.Providers.OpenID public XElement ResponseMessage { get; set; } public IDictionary AttributeExchangeProperties { get; private set; } + + public IDictionary ProtocolExtensionData { get; private set; } } } diff --git a/Owin.Security.Providers/Owin.Security.Providers.csproj b/Owin.Security.Providers/Owin.Security.Providers.csproj index 877d185..cb5edca 100644 --- a/Owin.Security.Providers/Owin.Security.Providers.csproj +++ b/Owin.Security.Providers/Owin.Security.Providers.csproj @@ -83,12 +83,18 @@ + + + + + +