diff options
Diffstat (limited to 'src/DotNetOpenAuth.OpenId/OpenId/Extensions/SimpleRegistration/ClaimsResponse.cs')
-rw-r--r-- | src/DotNetOpenAuth.OpenId/OpenId/Extensions/SimpleRegistration/ClaimsResponse.cs | 357 |
1 files changed, 357 insertions, 0 deletions
diff --git a/src/DotNetOpenAuth.OpenId/OpenId/Extensions/SimpleRegistration/ClaimsResponse.cs b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/SimpleRegistration/ClaimsResponse.cs new file mode 100644 index 0000000..d4df028 --- /dev/null +++ b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/SimpleRegistration/ClaimsResponse.cs @@ -0,0 +1,357 @@ +//----------------------------------------------------------------------- +// <copyright file="ClaimsResponse.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OpenId.Extensions.SimpleRegistration { + using System; + using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; + using System.Diagnostics.Contracts; + using System.Globalization; + using System.Net.Mail; + using System.Text; + using System.Text.RegularExpressions; + using System.Xml.Serialization; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.OpenId.Messages; + + /// <summary> + /// A struct storing Simple Registration field values describing an + /// authenticating user. + /// </summary> + [Serializable] + public sealed class ClaimsResponse : ExtensionBase, IClientScriptExtensionResponse, IMessageWithEvents { + /// <summary> + /// The factory method that may be used in deserialization of this message. + /// </summary> + internal static readonly StandardOpenIdExtensionFactory.CreateDelegate Factory = (typeUri, data, baseMessage, isProviderRole) => { + if ((typeUri == Constants.sreg_ns || Array.IndexOf(Constants.AdditionalTypeUris, typeUri) >= 0) && !isProviderRole) { + return new ClaimsResponse(typeUri); + } + + return null; + }; + + /// <summary> + /// The allowed format for birthdates. + /// </summary> + private static readonly Regex birthDateValidator = new Regex(@"^\d\d\d\d-\d\d-\d\d$"); + + /// <summary> + /// Storage for the raw string birthdate value. + /// </summary> + private string birthDateRaw; + + /// <summary> + /// Backing field for the <see cref="BirthDate"/> property. + /// </summary> + private DateTime? birthDate; + + /// <summary> + /// Backing field for the <see cref="Culture"/> property. + /// </summary> + private CultureInfo culture; + + /// <summary> + /// Initializes a new instance of the <see cref="ClaimsResponse"/> class. + /// </summary> + internal ClaimsResponse() + : this(Constants.sreg_ns) { + } + + /// <summary> + /// Initializes a new instance of the <see cref="ClaimsResponse"/> class. + /// </summary> + /// <param name="typeUriToUse"> + /// The type URI that must be used to identify this extension in the response message. + /// This value should be the same one the relying party used to send the extension request. + /// </param> + internal ClaimsResponse(string typeUriToUse) + : base(new Version(1, 0), typeUriToUse, Constants.AdditionalTypeUris) { + Contract.Requires<ArgumentException>(!String.IsNullOrEmpty(typeUriToUse)); + } + + /// <summary> + /// Gets or sets the nickname the user goes by. + /// </summary> + [MessagePart(Constants.nickname)] + public string Nickname { get; set; } + + /// <summary> + /// Gets or sets the user's email address. + /// </summary> + [MessagePart(Constants.email)] + public string Email { get; set; } + + /// <summary> + /// Gets or sets the full name of a user as a single string. + /// </summary> + [MessagePart(Constants.fullname)] + public string FullName { get; set; } + + /// <summary> + /// Gets or sets the user's birthdate. + /// </summary> + public DateTime? BirthDate { + get { + return this.birthDate; + } + + set { + this.birthDate = value; + + // Don't use property accessor for peer property to avoid infinite loop between the two proeprty accessors. + if (value.HasValue) { + this.birthDateRaw = value.Value.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture); + } else { + this.birthDateRaw = null; + } + } + } + + /// <summary> + /// Gets or sets the raw birth date string given by the extension. + /// </summary> + /// <value>A string in the format yyyy-MM-dd.</value> + [MessagePart(Constants.dob)] + public string BirthDateRaw { + get { + return this.birthDateRaw; + } + + set { + ErrorUtilities.VerifyArgument(value == null || birthDateValidator.IsMatch(value), OpenIdStrings.SregInvalidBirthdate); + if (value != null) { + // Update the BirthDate property, if possible. + // Don't use property accessor for peer property to avoid infinite loop between the two proeprty accessors. + // Some valid sreg dob values like "2000-00-00" will not work as a DateTime struct, + // in which case we null it out, but don't show any error. + DateTime newBirthDate; + if (DateTime.TryParse(value, out newBirthDate)) { + this.birthDate = newBirthDate; + } else { + Logger.OpenId.WarnFormat("Simple Registration birthdate '{0}' could not be parsed into a DateTime and may not include month and/or day information. Setting BirthDate property to null.", value); + this.birthDate = null; + } + } else { + this.birthDate = null; + } + + this.birthDateRaw = value; + } + } + + /// <summary> + /// Gets or sets the gender of the user. + /// </summary> + [MessagePart(Constants.gender, Encoder = typeof(GenderEncoder))] + public Gender? Gender { get; set; } + + /// <summary> + /// Gets or sets the zip code / postal code of the user. + /// </summary> + [MessagePart(Constants.postcode)] + public string PostalCode { get; set; } + + /// <summary> + /// Gets or sets the country of the user. + /// </summary> + [MessagePart(Constants.country)] + public string Country { get; set; } + + /// <summary> + /// Gets or sets the primary/preferred language of the user. + /// </summary> + [MessagePart(Constants.language)] + public string Language { get; set; } + + /// <summary> + /// Gets or sets the user's timezone. + /// </summary> + [MessagePart(Constants.timezone)] + public string TimeZone { get; set; } + + /// <summary> + /// Gets a combination of the user's full name and email address. + /// </summary> + public MailAddress MailAddress { + get { + if (string.IsNullOrEmpty(this.Email)) { + return null; + } else if (string.IsNullOrEmpty(this.FullName)) { + return new MailAddress(this.Email); + } else { + return new MailAddress(this.Email, this.FullName); + } + } + } + + /// <summary> + /// Gets or sets a combination o the language and country of the user. + /// </summary> + [XmlIgnore] + public CultureInfo Culture { + get { + if (this.culture == null && !string.IsNullOrEmpty(this.Language)) { + string cultureString = string.Empty; + cultureString = this.Language; + if (!string.IsNullOrEmpty(this.Country)) { + cultureString += "-" + this.Country; + } + this.culture = CultureInfo.GetCultureInfo(cultureString); + } + + return this.culture; + } + + set { + this.culture = value; + this.Language = (value != null) ? value.TwoLetterISOLanguageName : null; + int indexOfHyphen = (value != null) ? value.Name.IndexOf('-') : -1; + this.Country = indexOfHyphen > 0 ? value.Name.Substring(indexOfHyphen + 1) : null; + } + } + + /// <summary> + /// Gets a value indicating whether this extension is signed by the Provider. + /// </summary> + /// <value> + /// <c>true</c> if this instance is signed by the Provider; otherwise, <c>false</c>. + /// </value> + public bool IsSignedByProvider { + get { return this.IsSignedByRemoteParty; } + } + + /// <summary> + /// Tests equality of two <see cref="ClaimsResponse"/> objects. + /// </summary> + /// <param name="one">One instance to compare.</param> + /// <param name="other">Another instance to compare.</param> + /// <returns>The result of the operator.</returns> + public static bool operator ==(ClaimsResponse one, ClaimsResponse other) { + return one.EqualsNullSafe(other); + } + + /// <summary> + /// Tests inequality of two <see cref="ClaimsResponse"/> objects. + /// </summary> + /// <param name="one">One instance to compare.</param> + /// <param name="other">Another instance to compare.</param> + /// <returns>The result of the operator.</returns> + public static bool operator !=(ClaimsResponse one, ClaimsResponse other) { + return !(one == other); + } + + /// <summary> + /// Tests equality of two <see cref="ClaimsResponse"/> objects. + /// </summary> + /// <param name="obj">The <see cref="T:System.Object"/> to compare with the current <see cref="T:System.Object"/>.</param> + /// <returns> + /// true if the specified <see cref="T:System.Object"/> is equal to the current <see cref="T:System.Object"/>; otherwise, false. + /// </returns> + /// <exception cref="T:System.NullReferenceException"> + /// The <paramref name="obj"/> parameter is null. + /// </exception> + public override bool Equals(object obj) { + ClaimsResponse other = obj as ClaimsResponse; + if (other == null) { + return false; + } + + return + this.BirthDateRaw.EqualsNullSafe(other.BirthDateRaw) && + this.Country.EqualsNullSafe(other.Country) && + this.Language.EqualsNullSafe(other.Language) && + this.Email.EqualsNullSafe(other.Email) && + this.FullName.EqualsNullSafe(other.FullName) && + this.Gender.Equals(other.Gender) && + this.Nickname.EqualsNullSafe(other.Nickname) && + this.PostalCode.EqualsNullSafe(other.PostalCode) && + this.TimeZone.EqualsNullSafe(other.TimeZone); + } + + /// <summary> + /// Serves as a hash function for a particular type. + /// </summary> + /// <returns> + /// A hash code for the current <see cref="T:System.Object"/>. + /// </returns> + public override int GetHashCode() { + return (this.Nickname != null) ? this.Nickname.GetHashCode() : base.GetHashCode(); + } + + #region IClientScriptExtension Members + + /// <summary> + /// Reads the extension information on an authentication response from the provider. + /// </summary> + /// <param name="response">The incoming OpenID response carrying the extension.</param> + /// <returns> + /// A Javascript snippet that when executed on the user agent returns an object with + /// the information deserialized from the extension response. + /// </returns> + /// <remarks> + /// This method is called <b>before</b> the signature on the assertion response has been + /// verified. Therefore all information in these fields should be assumed unreliable + /// and potentially falsified. + /// </remarks> + string IClientScriptExtensionResponse.InitializeJavaScriptData(IProtocolMessageWithExtensions response) { + var sreg = new Dictionary<string, string>(15); + + // Although we could probably whip up a trip with MessageDictionary + // to avoid explicitly setting each field, doing so would likely + // open ourselves up to security exploits from the OP as it would + // make possible sending arbitrary javascript in arbitrary field names. + sreg[Constants.nickname] = this.Nickname; + sreg[Constants.email] = this.Email; + sreg[Constants.fullname] = this.FullName; + sreg[Constants.dob] = this.BirthDateRaw; + sreg[Constants.gender] = this.Gender.HasValue ? this.Gender.Value.ToString() : null; + sreg[Constants.postcode] = this.PostalCode; + sreg[Constants.country] = this.Country; + sreg[Constants.language] = this.Language; + sreg[Constants.timezone] = this.TimeZone; + + return MessagingUtilities.CreateJsonObject(sreg, false); + } + + #endregion + + #region IMessageWithEvents Members + + /// <summary> + /// Called when the message is about to be transmitted, + /// before it passes through the channel binding elements. + /// </summary> + void IMessageWithEvents.OnSending() { + // Null out empty values so we don't send out a lot of empty parameters. + this.Country = EmptyToNull(this.Country); + this.Email = EmptyToNull(this.Email); + this.FullName = EmptyToNull(this.FullName); + this.Language = EmptyToNull(this.Language); + this.Nickname = EmptyToNull(this.Nickname); + this.PostalCode = EmptyToNull(this.PostalCode); + this.TimeZone = EmptyToNull(this.TimeZone); + } + + /// <summary> + /// Called when the message has been received, + /// after it passes through the channel binding elements. + /// </summary> + void IMessageWithEvents.OnReceiving() { + } + + #endregion + + /// <summary> + /// Translates an empty string value to null, or passes through non-empty values. + /// </summary> + /// <param name="value">The value to consider changing to null.</param> + /// <returns>Either null or a non-empty string.</returns> + private static string EmptyToNull(string value) { + return string.IsNullOrEmpty(value) ? null : value; + } + } +}
\ No newline at end of file |