//----------------------------------------------------------------------- // // Copyright (c) Outercurve Foundation. All rights reserved. // //----------------------------------------------------------------------- namespace DotNetOpenAuth.OpenId.Extensions.SimpleRegistration { using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Net.Mail; using System.Text; using System.Text.RegularExpressions; using System.Xml.Serialization; using DotNetOpenAuth.Logging; using DotNetOpenAuth.Messaging; using DotNetOpenAuth.OpenId.Messages; using Validation; /// /// A struct storing Simple Registration field values describing an /// authenticating user. /// [Serializable] public sealed class ClaimsResponse : ExtensionBase, IClientScriptExtensionResponse, IMessageWithEvents { /// /// The factory method that may be used in deserialization of this message. /// internal static readonly StandardOpenIdExtensionFactory.CreateDelegate Factory = (typeUri, data, baseMessage, isProviderRole) => { if ((typeUri == Constants.TypeUris.Standard || Array.IndexOf(Constants.AdditionalTypeUris, typeUri) >= 0) && !isProviderRole) { return new ClaimsResponse(typeUri); } return null; }; /// /// The allowed format for birthdates. /// private static readonly Regex birthDateValidator = new Regex(@"^\d\d\d\d-\d\d-\d\d$"); /// /// Storage for the raw string birthdate value. /// private string birthDateRaw; /// /// Backing field for the property. /// private DateTime? birthDate; /// /// Backing field for the property. /// private CultureInfo culture; /// /// Initializes a new instance of the class /// using the most common, and spec prescribed type URI. /// public ClaimsResponse() : this(Constants.TypeUris.Standard) { } /// /// Initializes a new instance of the class. /// /// /// 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. /// Commonly used type URIs supported by relying parties are defined in the /// class. /// public ClaimsResponse(string typeUriToUse = Constants.TypeUris.Standard) : base(new Version(1, 0), typeUriToUse, Constants.AdditionalTypeUris) { Requires.NotNullOrEmpty(typeUriToUse, "typeUriToUse"); } /// /// Gets or sets the nickname the user goes by. /// [MessagePart(Constants.nickname)] public string Nickname { get; set; } /// /// Gets or sets the user's email address. /// [MessagePart(Constants.email)] public string Email { get; set; } /// /// Gets or sets the full name of a user as a single string. /// [MessagePart(Constants.fullname)] public string FullName { get; set; } /// /// Gets or sets the user's birthdate. /// 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; } } } /// /// Gets or sets the raw birth date string given by the extension. /// /// A string in the format yyyy-MM-dd. [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; } } /// /// Gets or sets the gender of the user. /// [MessagePart(Constants.gender, Encoder = typeof(GenderEncoder))] public Gender? Gender { get; set; } /// /// Gets or sets the zip code / postal code of the user. /// [MessagePart(Constants.postcode)] public string PostalCode { get; set; } /// /// Gets or sets the country of the user. /// [MessagePart(Constants.country)] public string Country { get; set; } /// /// Gets or sets the primary/preferred language of the user. /// [MessagePart(Constants.language)] public string Language { get; set; } /// /// Gets or sets the user's timezone. /// [MessagePart(Constants.timezone)] public string TimeZone { get; set; } /// /// Gets a combination of the user's full name and email address. /// 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); } } } /// /// Gets or sets a combination of the language and country of the user. /// [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; } // language-country may not always form a recongized valid culture. // For instance, a Google OpenID Provider can return a random combination // of language and country based on user settings. try { this.culture = CultureInfo.GetCultureInfo(cultureString); } catch (ArgumentException) { // CultureNotFoundException derives from this, and .NET 3.5 throws the base type // Fallback to just reporting a culture based on language. this.culture = CultureInfo.GetCultureInfo(this.Language); } } 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; } } /// /// Gets a value indicating whether this extension is signed by the Provider. /// /// /// true if this instance is signed by the Provider; otherwise, false. /// public bool IsSignedByProvider { get { return this.IsSignedByRemoteParty; } } /// /// Tests equality of two objects. /// /// One instance to compare. /// Another instance to compare. /// The result of the operator. public static bool operator ==(ClaimsResponse one, ClaimsResponse other) { return one.EqualsNullSafe(other); } /// /// Tests inequality of two objects. /// /// One instance to compare. /// Another instance to compare. /// The result of the operator. public static bool operator !=(ClaimsResponse one, ClaimsResponse other) { return !(one == other); } /// /// Tests equality of two objects. /// /// The to compare with the current . /// /// true if the specified is equal to the current ; otherwise, false. /// /// /// The parameter is null. /// 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); } /// /// Serves as a hash function for a particular type. /// /// /// A hash code for the current . /// public override int GetHashCode() { return (this.Nickname != null) ? this.Nickname.GetHashCode() : base.GetHashCode(); } #region IClientScriptExtension Members /// /// Reads the extension information on an authentication response from the provider. /// /// The incoming OpenID response carrying the extension. /// /// A Javascript snippet that when executed on the user agent returns an object with /// the information deserialized from the extension response. /// /// /// This method is called before the signature on the assertion response has been /// verified. Therefore all information in these fields should be assumed unreliable /// and potentially falsified. /// string IClientScriptExtensionResponse.InitializeJavaScriptData(IProtocolMessageWithExtensions response) { var sreg = new Dictionary(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 /// /// Called when the message is about to be transmitted, /// before it passes through the channel binding elements. /// 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); } /// /// Called when the message has been received, /// after it passes through the channel binding elements. /// void IMessageWithEvents.OnReceiving() { } #endregion /// /// Translates an empty string value to null, or passes through non-empty values. /// /// The value to consider changing to null. /// Either null or a non-empty string. private static string EmptyToNull(string value) { return string.IsNullOrEmpty(value) ? null : value; } } }