//----------------------------------------------------------------------- // // Copyright (c) Microsoft Corporation. All rights reserved. // // // Microsoft Public License (Ms-PL). // See http://www.microsoft.com/opensource/licenses.mspx#Ms-PL // //----------------------------------------------------------------------- namespace DotNetOpenAuth.InfoCard { using System; using System.Collections.Generic; using System.Configuration; using System.Diagnostics.CodeAnalysis; using System.Diagnostics.Contracts; using System.IdentityModel.Claims; using System.IdentityModel.Policy; using System.IdentityModel.Selectors; using System.IdentityModel.Tokens; using System.IO; using System.Linq; using System.Net.Mail; using System.Security.Cryptography; using System.Security.Principal; using System.ServiceModel.Security; using System.Text; using System.Xml; using DotNetOpenAuth.Messaging; /// /// Tools for reading InfoCard tokens. /// internal static class TokenUtility { /// /// Gets the maximum amount the token can be out of sync with time. /// internal static TimeSpan MaximumClockSkew { get { return DotNetOpenAuth.Configuration.DotNetOpenAuthSection.Messaging.MaximumClockSkew; } } /// /// Token Authentication. Translates the decrypted data into a AuthContext. /// /// The token XML reader. /// The audience that the token must be scoped for. /// Use null to indicate any audience is acceptable. /// /// The authorization context carried by the token. /// internal static AuthorizationContext AuthenticateToken(XmlReader reader, Uri audience) { Contract.Ensures(Contract.Result() != null); // Extensibility Point: // in order to accept different token types, you would need to add additional // code to create an authenticationcontext from the security token. // This code only supports SamlSecurityToken objects. SamlSecurityToken token = WSSecurityTokenSerializer.DefaultInstance.ReadToken(reader, null) as SamlSecurityToken; if (null == token) { throw new InformationCardException("Unable to read security token"); } if (null != token.SecurityKeys && token.SecurityKeys.Count > 0) { throw new InformationCardException("Token Security Keys Exist"); } if (audience == null) { Logger.InfoCard.Warn("SAML token Audience checking will be skipped."); } else { if (token.Assertion.Conditions != null && token.Assertion.Conditions.Conditions != null) { foreach (SamlCondition condition in token.Assertion.Conditions.Conditions) { SamlAudienceRestrictionCondition audienceCondition = condition as SamlAudienceRestrictionCondition; if (audienceCondition != null) { Logger.InfoCard.DebugFormat("SAML token audience(s): {0}", audienceCondition.Audiences.ToStringDeferred()); bool match = audienceCondition.Audiences.Contains(audience); if (!match && Logger.InfoCard.IsErrorEnabled) { Logger.InfoCard.ErrorFormat("Expected SAML token audience of {0} but found {1}.", audience.AbsoluteUri, audienceCondition.Audiences.Select(aud => aud.AbsoluteUri).ToStringDeferred()); } // The token is invalid if any condition is not valid. // An audience restriction condition is valid if any audience // matches the Relying Party. InfoCardErrorUtilities.VerifyInfoCard(match, InfoCardStrings.AudienceMismatch); } } } } var samlAuthenticator = new SamlSecurityTokenAuthenticator( new List( new SecurityTokenAuthenticator[] { new RsaSecurityTokenAuthenticator(), new X509SecurityTokenAuthenticator(), }), MaximumClockSkew); return AuthorizationContext.CreateDefaultAuthorizationContext(samlAuthenticator.ValidateToken(token)); } /// /// Translates claims to strings /// /// Claim to translate to a string /// The string representation of a claim's value. [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "False positive.")] internal static string GetResourceValue(Claim claim) { string strClaim = claim.Resource as string; if (!string.IsNullOrEmpty(strClaim)) { return strClaim; } IdentityReference reference = claim.Resource as IdentityReference; if (null != reference) { return reference.Value; } ICspAsymmetricAlgorithm rsa = claim.Resource as ICspAsymmetricAlgorithm; if (null != rsa) { using (SHA256 sha = new SHA256Managed()) { return Convert.ToBase64String(sha.ComputeHash(rsa.ExportCspBlob(false))); } } MailAddress mail = claim.Resource as MailAddress; if (null != mail) { return mail.ToString(); } byte[] bufferValue = claim.Resource as byte[]; if (null != bufferValue) { return Convert.ToBase64String(bufferValue); } return claim.Resource.ToString(); } /// /// Generates a UniqueID based off the Issuer's key /// /// the Authorization Context /// the hash of the internal key of the issuer internal static string GetIssuerPubKeyHash(AuthorizationContext authzContext) { foreach (ClaimSet cs in authzContext.ClaimSets) { Claim currentIssuerClaim = GetUniqueRsaClaim(cs.Issuer); if (currentIssuerClaim != null) { RSA rsa = currentIssuerClaim.Resource as RSA; if (null == rsa) { return null; } return ComputeCombinedId(rsa, string.Empty); } } return null; } /// /// Generates a UniqueID based off the Issuer's key and the PPID. /// /// The Authorization Context /// A unique ID for this user at this web site. internal static string GetUniqueName(AuthorizationContext authzContext) { Requires.NotNull(authzContext, "authzContext"); Claim uniqueIssuerClaim = null; Claim uniqueUserClaim = null; foreach (ClaimSet cs in authzContext.ClaimSets) { Claim currentIssuerClaim = GetUniqueRsaClaim(cs.Issuer); foreach (Claim c in cs.FindClaims(ClaimTypes.PPID, Rights.PossessProperty)) { if (null == currentIssuerClaim) { // Found a claim in a ClaimSet with no RSA issuer. return null; } if (null == uniqueUserClaim) { uniqueUserClaim = c; uniqueIssuerClaim = currentIssuerClaim; } else if (!uniqueIssuerClaim.Equals(currentIssuerClaim)) { // Found two of the desired claims with different // issuers. No unique name. return null; } else if (!uniqueUserClaim.Equals(c)) { // Found two of the desired claims with different // values. No unique name. return null; } } } // No claim of the desired type was found if (null == uniqueUserClaim) { return null; } // Unexpected resource type string claimValue = uniqueUserClaim.Resource as string; if (null == claimValue) { return null; } // Unexpected resource type for RSA RSA rsa = uniqueIssuerClaim.Resource as RSA; if (null == rsa) { return null; } return ComputeCombinedId(rsa, claimValue); } /// /// Generates the Site Specific ID to match the one in the Identity Selector. /// /// The ID displayed by the Identity Selector. /// The personal private identifier. /// A string containing the XXX-XXXX-XXX cosmetic value. [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "False positive.")] internal static string CalculateSiteSpecificID(string ppid) { Requires.NotNull(ppid, "ppid"); Contract.Ensures(!string.IsNullOrEmpty(Contract.Result())); int callSignChars = 10; char[] charMap = "QL23456789ABCDEFGHJKMNPRSTUVWXYZ".ToCharArray(); int charMapLength = charMap.Length; byte[] raw = Convert.FromBase64String(ppid); using (HashAlgorithm hasher = SHA1.Create()) { raw = hasher.ComputeHash(raw); } StringBuilder callSign = new StringBuilder(); for (int i = 0; i < callSignChars; i++) { // after char 3 and char 7, place a dash if (i == 3 || i == 7) { callSign.Append('-'); } callSign.Append(charMap[raw[i] % charMapLength]); } return callSign.ToString(); } /// /// Gets the Unique RSA Claim from the SAML token. /// /// the claimset which contains the claim /// a RSA claim private static Claim GetUniqueRsaClaim(ClaimSet cs) { Requires.NotNull(cs, "cs"); Claim rsa = null; foreach (Claim c in cs.FindClaims(ClaimTypes.Rsa, Rights.PossessProperty)) { if (null == rsa) { rsa = c; } else if (!rsa.Equals(c)) { // Found two non-equal RSA claims return null; } } return rsa; } /// /// Does the actual calculation of a combined ID from a value and an RSA key. /// /// The key of the issuer of the token /// the claim value to hash with. /// A base64 representation of the combined ID. [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "False positive.")] private static string ComputeCombinedId(RSA issuerKey, string claimValue) { Requires.NotNull(issuerKey, "issuerKey"); Requires.NotNull(claimValue, "claimValue"); Contract.Ensures(Contract.Result() != null); int nameLength = Encoding.UTF8.GetByteCount(claimValue); RSAParameters rsaParams = issuerKey.ExportParameters(false); byte[] shaInput; byte[] shaOutput; int i = 0; shaInput = new byte[rsaParams.Modulus.Length + rsaParams.Exponent.Length + nameLength]; rsaParams.Modulus.CopyTo(shaInput, i); i += rsaParams.Modulus.Length; rsaParams.Exponent.CopyTo(shaInput, i); i += rsaParams.Exponent.Length; i += Encoding.UTF8.GetBytes(claimValue, 0, claimValue.Length, shaInput, i); using (SHA256 sha = SHA256.Create()) { shaOutput = sha.ComputeHash(shaInput); } return Convert.ToBase64String(shaOutput); } } }