diff options
author | Andrew Arnott <andrewarnott@gmail.com> | 2013-05-27 09:32:17 -0700 |
---|---|---|
committer | Andrew Arnott <andrewarnott@gmail.com> | 2013-05-27 09:32:17 -0700 |
commit | 5a0a8ee4c55f323a6c1fbdb619cd89b7d28a94ba (patch) | |
tree | 026bb7a58fc6b80b680f2b5be2a25ddf1efbf0f5 /src | |
parent | e4c746826690259eddba106e8a44d1b52b542faf (diff) | |
parent | 064220dbab72b00f23abd041bf4a30ea87a00d88 (diff) | |
download | DotNetOpenAuth-5a0a8ee4c55f323a6c1fbdb619cd89b7d28a94ba.zip DotNetOpenAuth-5a0a8ee4c55f323a6c1fbdb619cd89b7d28a94ba.tar.gz DotNetOpenAuth-5a0a8ee4c55f323a6c1fbdb619cd89b7d28a94ba.tar.bz2 |
Merge branch 'v4.3'
Conflicts:
samples/OAuthClient/Default.aspx
samples/OAuthClient/Facebook.aspx.cs
samples/OAuthClient/Web.config
samples/OAuthClient/WindowsLive.aspx.cs
samples/OAuthClient/packages.config
src/DotNetOpenAuth.Core/Messaging/OutgoingWebResponse.cs
src/DotNetOpenAuth.Core/Messaging/StandardWebRequestHandler.cs
src/DotNetOpenAuth.OAuth.Consumer/OAuth/ConsumerBase.cs
src/DotNetOpenAuth.OAuth.Consumer/OAuth/OAuth1HmacSha1HttpMessageHandler.cs
src/DotNetOpenAuth.OAuth.Consumer/OAuth/OAuth1HttpMessageHandlerBase.cs
src/DotNetOpenAuth.OAuth.Consumer/OAuth/OAuth1PlainTextMessageHandler.cs
src/DotNetOpenAuth.OAuth.Consumer/OAuth/OAuth1RsaSha1HttpMessageHandler.cs
src/DotNetOpenAuth.OAuth2.Client/OAuth2/WebServerClient.cs
src/packages/repositories.config
src/version.txt
Diffstat (limited to 'src')
14 files changed, 747 insertions, 19 deletions
diff --git a/src/.gitignore b/src/.gitignore index 7aadbf5..651cd4c 100644 --- a/src/.gitignore +++ b/src/.gitignore @@ -10,3 +10,4 @@ _ReSharper.* bin obj Bin +packages diff --git a/src/DotNetOpenAuth.AspNet/Clients/OAuth2/AzureADClaims.cs b/src/DotNetOpenAuth.AspNet/Clients/OAuth2/AzureADClaims.cs new file mode 100644 index 0000000..deb396f --- /dev/null +++ b/src/DotNetOpenAuth.AspNet/Clients/OAuth2/AzureADClaims.cs @@ -0,0 +1,75 @@ +//----------------------------------------------------------------------- +// <copyright file="AzureADClaims.cs" company="Microsoft"> +// Copyright (c) Microsoft. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.AspNet.Clients { + using System; + using System.ComponentModel; + using System.Diagnostics.CodeAnalysis; + using System.Runtime.Serialization; + + /// <summary> + /// Contains clains of a AzureAD token. + /// </summary> + /// <remarks> + /// Technically, this class doesn't need to be public, but because we want to make it serializable in medium trust, it has to be public. + /// </remarks> + [DataContract] + [EditorBrowsable(EditorBrowsableState.Never)] + [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "AzureAD", Justification = "Brand name")] + public class AzureADClaims { + #region Public Properties + + /// <summary> + /// Gets or sets the audience. + /// </summary> + /// <value> The audience token is valid for. </value> + [DataMember(Name = "aud")] + public string Aud { get; set; } + + /// <summary> + /// Gets or sets the issuer. + /// </summary> + /// <value> The issuer. </value> + [DataMember(Name = "iss")] + public string Iss { get; set; } + + /// <summary> + /// Gets or sets the early expiry time. + /// </summary> + /// <value> The early expiry time. </value> + [DataMember(Name = "nbf")] + public string Nbf { get; set; } + + /// <summary> + /// Gets or sets the expiry time. + /// </summary> + /// <value> The expiry time. </value> + [DataMember(Name = "exp")] + public string Exp { get; set; } + + /// <summary> + /// Gets or sets the id of the user. + /// </summary> + /// <value> The id of the user. </value> + [DataMember(Name = "oid")] + public string Oid { get; set; } + + /// <summary> + /// Gets or sets the id of the tenant. + /// </summary> + /// <value> The tenant . </value> + [DataMember(Name = "tid")] + public string Tid { get; set; } + + /// <summary> + /// Gets or sets the appid of application. + /// </summary> + /// <value> The id of the application. </value> + [DataMember(Name = "appid")] + public string Appid { get; set; } + #endregion + } +} diff --git a/src/DotNetOpenAuth.AspNet/Clients/OAuth2/AzureADClient.cs b/src/DotNetOpenAuth.AspNet/Clients/OAuth2/AzureADClient.cs new file mode 100644 index 0000000..c3d6413 --- /dev/null +++ b/src/DotNetOpenAuth.AspNet/Clients/OAuth2/AzureADClient.cs @@ -0,0 +1,460 @@ +//----------------------------------------------------------------------- +// <copyright file="AzureADClient.cs" company="Microsoft"> +// Copyright (c) Microsoft. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.AspNet.Clients { + using System; + using System.Collections.Generic; + using System.Collections.Specialized; + using System.Diagnostics.CodeAnalysis; + using System.IdentityModel.Tokens; + using System.IO; + using System.Net; + using System.Security.Cryptography; + using System.Security.Cryptography.X509Certificates; + using System.Text; + using System.Web; + using System.Web.Script.Serialization; + using System.Xml; + using DotNetOpenAuth.Messaging; + + using Validation; + + /// <summary> + /// The AzureAD client. + /// </summary> + [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "AzureAD", Justification = "Brand name")] + public sealed class AzureADClient : OAuth2Client { + #region Constants and Fields + + /// <summary> + /// The authorization endpoint. + /// </summary> + private const string AuthorizationEndpoint = "https://login.windows.net/common/oauth2/authorize"; + + /// <summary> + /// The token endpoint. + /// </summary> + private const string TokenEndpoint = "https://login.windows.net/common/oauth2/token"; + + /// <summary> + /// The name of the graph resource. + /// </summary> + private const string GraphResource = "https://graph.windows.net"; + + /// <summary> + /// The URL to get the token decoding certificate from. + /// </summary> + private const string MetaDataEndpoint = "https://login.windows.net/evosts.onmicrosoft.com/FederationMetadata/2007-06/FederationMetadata.xml"; + + /// <summary> + /// The URL for AzureAD graph. + /// </summary> + private const string GraphEndpoint = "https://graph.windows.net/"; + + /// <summary> + /// The id of the STS. + /// </summary> + private const string STSName = "https://sts.windows.net"; + + /// <summary> + /// The app id. + /// </summary> + private readonly string appId; + + /// <summary> + /// The app secret. + /// </summary> + private readonly string appSecret; + + /// <summary> + /// The resource to target. + /// </summary> + private readonly string resource; + + /// <summary> + /// Encoding cert. + /// </summary> + private static X509Certificate2[] encodingcert; + + /// <summary> + /// Hash algo used by the X509Cert. + /// </summary> + private static HashAlgorithm hash; + + /// <summary> + /// The tenantid claim for the authcode. + /// </summary> + private string tenantid; + + /// <summary> + /// The userid claim for the authcode. + /// </summary> + private string userid; + #endregion + + #region Constructors and Destructors + + /// <summary> + /// Initializes a new instance of the <see cref="AzureADClient"/> class. + /// </summary> + /// <param name="appId"> + /// The app id. + /// </param> + /// <param name="appSecret"> + /// The app secret. + /// </param> + public AzureADClient(string appId, string appSecret) + : this(appId, appSecret, GraphResource) { + } + + /// <summary> + /// Initializes a new instance of the <see cref="AzureADClient"/> class. + /// </summary> + /// <param name="appId"> + /// The app id. + /// </param> + /// <param name="appSecret"> + /// The app secret. + /// </param> + /// <param name="resource"> + /// The resource of oauth request. + /// </param> + public AzureADClient(string appId, string appSecret, string resource) + : base("azuread") { + Requires.NotNullOrEmpty(appId, "appId"); + Requires.NotNullOrEmpty(appSecret, "appSecret"); + Requires.NotNullOrEmpty(resource, "resource"); + this.appId = appId; + this.appSecret = appSecret; + this.resource = resource; + } + #endregion + + #region Methods + + /// <summary> + /// The get service login url. + /// </summary> + /// <param name="returnUrl"> + /// The return url. + /// </param> + /// <returns>An absolute URI.</returns> + protected override Uri GetServiceLoginUrl(Uri returnUrl) { + var builder = new UriBuilder(AuthorizationEndpoint); + builder.AppendQueryArgs( + new Dictionary<string, string> { + { "client_id", this.appId }, + { "redirect_uri", returnUrl.AbsoluteUri }, + { "response_type", "code" }, + { "resource", this.resource }, + }); + return builder.Uri; + } + + /// <summary> + /// The get user data. + /// </summary> + /// <param name="accessToken"> + /// The access token. + /// </param> + /// <returns>A dictionary of profile data.</returns> + protected override NameValueCollection GetUserData(string accessToken) { + var userData = new NameValueCollection(); + try { + AzureADGraph graphData; + WebRequest request = + WebRequest.Create( + GraphEndpoint + this.tenantid + "/users/" + this.userid + "?api-version=2013-04-05"); + request.Headers = new WebHeaderCollection(); + request.Headers.Add("authorization", accessToken); + using (var response = request.GetResponse()) { + using (var responseStream = response.GetResponseStream()) { + graphData = JsonHelper.Deserialize<AzureADGraph>(responseStream); + } + } + + // this dictionary must contains + userData.AddItemIfNotEmpty("id", graphData.ObjectId); + userData.AddItemIfNotEmpty("username", graphData.UserPrincipalName); + userData.AddItemIfNotEmpty("name", graphData.DisplayName); + + return userData; + } catch (Exception e) { + System.Diagnostics.Debug.WriteLine(e.ToStringDescriptive()); + return userData; + } + } + + /// <summary> + /// Obtains an access token given an authorization code and callback URL. + /// </summary> + /// <param name="returnUrl"> + /// The return url. + /// </param> + /// <param name="authorizationCode"> + /// The authorization code. + /// </param> + /// <returns> + /// The access token. + /// </returns> + protected override string QueryAccessToken(Uri returnUrl, string authorizationCode) { + try { + var entity = + MessagingUtilities.CreateQueryString( + new Dictionary<string, string> { + { "client_id", this.appId }, + { "redirect_uri", returnUrl.AbsoluteUri }, + { "client_secret", this.appSecret }, + { "code", authorizationCode }, + { "grant_type", "authorization_code" }, + { "api_version", "1.0" }, + }); + + WebRequest tokenRequest = WebRequest.Create(TokenEndpoint); + tokenRequest.ContentType = "application/x-www-form-urlencoded"; + tokenRequest.ContentLength = entity.Length; + tokenRequest.Method = "POST"; + + using (Stream requestStream = tokenRequest.GetRequestStream()) { + var writer = new StreamWriter(requestStream); + writer.Write(entity); + writer.Flush(); + } + + HttpWebResponse tokenResponse = (HttpWebResponse)tokenRequest.GetResponse(); + if (tokenResponse.StatusCode == HttpStatusCode.OK) { + using (Stream responseStream = tokenResponse.GetResponseStream()) { + var tokenData = JsonHelper.Deserialize<OAuth2AccessTokenData>(responseStream); + if (tokenData != null) { + AzureADClaims claimsAD; + claimsAD = this.ParseAccessToken(tokenData.AccessToken, true); + if (claimsAD != null) { + this.tenantid = claimsAD.Tid; + this.userid = claimsAD.Oid; + return tokenData.AccessToken; + } + return string.Empty; + } + } + } + + return null; + } catch (Exception e) { + System.Diagnostics.Debug.WriteLine(e.ToStringDescriptive()); + return null; + } + } + + /// <summary> + /// Base64 decode function except that it switches -_ to +/ before base64 decode + /// </summary> + /// <param name="str"> + /// The string to be base64urldecoded. + /// </param> + /// <returns> + /// Decoded string as string using UTF8 encoding. + /// </returns> + private static string Base64URLdecode(string str) { + System.Text.UTF8Encoding encoder = new System.Text.UTF8Encoding(); + return encoder.GetString(Base64URLdecodebyte(str)); + } + + /// <summary> + /// Base64 decode function except that it switches -_ to +/ before base64 decode + /// </summary> + /// <param name="str"> + /// The string to be base64urldecoded. + /// </param> + /// <returns> + /// Decoded string as bytes. + /// </returns> + private static byte[] Base64URLdecodebyte(string str) { + // First replace chars and then pad per spec + str = str.Replace('-', '+').Replace('_', '/'); + str = str.PadRight(str.Length + ((4 - (str.Length % 4)) % 4), '='); + return Convert.FromBase64String(str); + } + + /// <summary> + /// Validate whether the unsigned value is same as signed value + /// </summary> + /// <param name="uval"> + /// The raw input of the string signed using the key + /// </param> + /// <param name="sval"> + /// The signature of the string + /// </param> + /// <param name="certthumb"> + /// The thumbprint of cert used to encrypt token + /// </param> + /// <returns> + /// True if same, false otherwise. + /// </returns> + private static bool ValidateSig(byte[] uval, byte[] sval, byte[] certthumb) { + try { + bool ret = false; + + X509Certificate2[] certx509 = GetEncodingCert(); + string certthumbhex = string.Empty; + + // Get the hexadecimail representation of the certthumbprint + for (int i = 0; i < certthumb.Length; i++) { + certthumbhex += certthumb[i].ToString("X2"); + } + + for (int c = 0; c < certx509.Length; c++) { + // Skip any cert that does not have the same thumbprint as token + if (certx509[c].Thumbprint.ToLower() != certthumbhex.ToLower()) { + continue; + } + X509SecurityToken tok = new X509SecurityToken(certx509[c]); + if (tok == null) { + return false; + } + for (int i = 0; i < tok.SecurityKeys.Count; i++) { + X509AsymmetricSecurityKey key = tok.SecurityKeys[i] as X509AsymmetricSecurityKey; + RSACryptoServiceProvider rsa = key.GetAsymmetricAlgorithm(SecurityAlgorithms.RsaSha256Signature, false) as RSACryptoServiceProvider; + + if (rsa == null) { + continue; + } + ret = rsa.VerifyData(uval, hash, sval); + if (ret == true) { + return ret; + } + } + } + return ret; + } catch (CryptographicException e) { + Console.WriteLine(e.ToStringDescriptive()); + return false; + } + } + + /// <summary> + /// Returns the certificate with which the token is encoded. + /// </summary> + /// <returns> + /// The encoding certificate. + /// </returns> + private static X509Certificate2[] GetEncodingCert() { + if (encodingcert != null) { + return encodingcert; + } + try { + // Lock for exclusive access + lock (typeof(AzureADClient)) { + XmlDocument doc = new XmlDocument(); + + WebRequest request = + WebRequest.Create(MetaDataEndpoint); + using (WebResponse response = request.GetResponse()) { + using (Stream responseStream = response.GetResponseStream()) { + doc.Load(responseStream); + XmlNodeList list = doc.GetElementsByTagName("X509Certificate"); + encodingcert = new X509Certificate2[list.Count]; + for (int i = 0; i < list.Count; i++) { + byte[] todecode_byte = Convert.FromBase64String(list[i].InnerText); + encodingcert[i] = new X509Certificate2(todecode_byte); + } + if (hash == null) { + hash = SHA256.Create(); + } + } + } + } + return encodingcert; + } catch (Exception e) { + System.Diagnostics.Debug.WriteLine(e.ToStringDescriptive()); + return null; + } + } + + /// <summary> + /// Parses the access token into an AzureAD token. + /// </summary> + /// <param name="token"> + /// The token as a string. + /// </param> + /// <param name="validate"> + /// Whether to validate against time\audience. + /// </param> + /// <returns> + /// The claims as an object and null in case of failure. + /// </returns> + private AzureADClaims ParseAccessToken(string token, bool validate) { + try { + // This is the encoded JWT token split into the 3 parts + string[] strparts = token.Split('.'); + + // Decparts has the header and claims section decoded from JWT + string jwtHeader, jwtClaims; + string jwtb64Header, jwtb64Claims, jwtb64Sig; + byte[] jwtSig; + if (strparts.Length != 3) { + return null; + } + jwtb64Header = strparts[0]; + jwtb64Claims = strparts[1]; + jwtb64Sig = strparts[2]; + jwtHeader = Base64URLdecode(jwtb64Header); + jwtClaims = Base64URLdecode(jwtb64Claims); + jwtSig = Base64URLdecodebyte(jwtb64Sig); + + JavaScriptSerializer s1 = new JavaScriptSerializer(); + + AzureADClaims claimsAD = s1.Deserialize<AzureADClaims>(jwtClaims); + AzureADHeader headerAD = s1.Deserialize<AzureADHeader>(jwtHeader); + + if (validate) { + // Check to see if the token is valid + // Check if its JWT and RSA encoded + if (headerAD.Typ.ToUpper() != "JWT") { + return null; + } + + // Check if its JWT and RSA encoded + if (headerAD.Alg.ToUpper() != "RS256") { + return null; + } + if (string.IsNullOrEmpty(headerAD.X5t)) { + return null; + } + + // Check audience to be graph + if (claimsAD.Aud.ToLower().ToLower() != GraphResource.ToLower()) { + return null; + } + + // Check issuer to be sts + if (claimsAD.Iss.ToLower().IndexOf(STSName.ToLower()) != 0) { + return null; + } + + // Check time validity + TimeSpan span = DateTime.UtcNow.Subtract(new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc)); + double secsnow = span.TotalSeconds; + double nbfsecs = Convert.ToDouble(claimsAD.Nbf); + double expsecs = Convert.ToDouble(claimsAD.Exp); + if ((nbfsecs - 100 > secsnow) || (secsnow > expsecs + 100)) { + return null; + } + + // Validate the signature of the token + string tokUnsigned = jwtb64Header + "." + jwtb64Claims; + if (!ValidateSig(Encoding.UTF8.GetBytes(tokUnsigned), jwtSig, Base64URLdecodebyte(headerAD.X5t))) { + return null; + } + } + return claimsAD; + } catch (Exception e) { + System.Diagnostics.Debug.WriteLine(e.ToStringDescriptive()); + return null; + } + } + #endregion + } +} diff --git a/src/DotNetOpenAuth.AspNet/Clients/OAuth2/AzureADGraph.cs b/src/DotNetOpenAuth.AspNet/Clients/OAuth2/AzureADGraph.cs new file mode 100644 index 0000000..8269419 --- /dev/null +++ b/src/DotNetOpenAuth.AspNet/Clients/OAuth2/AzureADGraph.cs @@ -0,0 +1,61 @@ +//----------------------------------------------------------------------- +// <copyright file="AzureADGraph.cs" company="Microsoft"> +// Copyright (c) Microsoft. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.AspNet.Clients { + using System; + using System.ComponentModel; + using System.Diagnostics.CodeAnalysis; + using System.Runtime.Serialization; + + /// <summary> + /// Contains data of a AzureAD user. + /// </summary> + /// <remarks> + /// Technically, this class doesn't need to be public, but because we want to make it serializable in medium trust, it has to be public. + /// </remarks> + [DataContract] + [EditorBrowsable(EditorBrowsableState.Never)] + [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "AzureAD", Justification = "Brand name")] + public class AzureADGraph { + #region Public Properties + + /// <summary> + /// Gets or sets the firstname. + /// </summary> + /// <value> The first name. </value> + [DataMember(Name = "givenName")] + public string GivenName { get; set; } + + /// <summary> + /// Gets or sets the lastname. + /// </summary> + /// <value> The last name. </value> + [DataMember(Name = "surname")] + public string Surname { get; set; } + + /// <summary> + /// Gets or sets the email. + /// </summary> + /// <value> The email. </value> + [DataMember(Name = "userPrincipalName")] + public string UserPrincipalName { get; set; } + + /// <summary> + /// Gets or sets the fullname. + /// </summary> + /// <value> The fullname. </value> + [DataMember(Name = "displayName")] + public string DisplayName { get; set; } + + /// <summary> + /// Gets or sets the id. + /// </summary> + /// <value> The id. </value> + [DataMember(Name = "objectId")] + public string ObjectId { get; set; } + #endregion + } +} diff --git a/src/DotNetOpenAuth.AspNet/Clients/OAuth2/AzureADHeader.cs b/src/DotNetOpenAuth.AspNet/Clients/OAuth2/AzureADHeader.cs new file mode 100644 index 0000000..042eccb --- /dev/null +++ b/src/DotNetOpenAuth.AspNet/Clients/OAuth2/AzureADHeader.cs @@ -0,0 +1,49 @@ +//----------------------------------------------------------------------- +// <copyright file="AzureADHeader.cs" company="Microsoft"> +// Copyright (c) Microsoft. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.AspNet.Clients { + using System; + using System.ComponentModel; + using System.Diagnostics.CodeAnalysis; + using System.Runtime.Serialization; + + /// <summary> + /// Contains header of AzureAD JWT token. + /// </summary> + /// <remarks> + /// Technically, this class doesn't need to be public, but because we want to make it serializable in medium trust, it has to be public. + /// </remarks> + [DataContract] + [EditorBrowsable(EditorBrowsableState.Never)] + [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "AzureAD", Justification = "Brand name")] + + public class AzureADHeader { + #region Public Properties + + /// <summary> + /// Gets or sets the type of token. Will always be JWT + /// </summary> + /// <value> The type of token. </value> + [DataMember(Name = "typ")] + public string Typ { get; set; } + + /// <summary> + /// Gets or sets the algo of the header. + /// </summary> + /// <value> The algo of encoding. </value> + [DataMember(Name = "alg")] + public string Alg { get; set; } + + /// <summary> + /// Gets or sets the thumbprint of the header. + /// </summary> + /// <value> The thumbprint of the cert used to encode. </value> + [DataMember(Name = "x5t")] + public string X5t { get; set; } + + #endregion + } +} diff --git a/src/DotNetOpenAuth.AspNet/Clients/OAuth2/FacebookClient.cs b/src/DotNetOpenAuth.AspNet/Clients/OAuth2/FacebookClient.cs index d595b4f..611f322 100644 --- a/src/DotNetOpenAuth.AspNet/Clients/OAuth2/FacebookClient.cs +++ b/src/DotNetOpenAuth.AspNet/Clients/OAuth2/FacebookClient.cs @@ -41,12 +41,18 @@ namespace DotNetOpenAuth.AspNet.Clients { /// </summary> private readonly string appSecret; + /// <summary> + /// The scope. + /// </summary> + private readonly string[] scope; + #endregion #region Constructors and Destructors /// <summary> - /// Initializes a new instance of the <see cref="FacebookClient"/> class. + /// Initializes a new instance of the <see cref="FacebookClient"/> class + /// with "email" as the scope. /// </summary> /// <param name="appId"> /// The app id. @@ -55,12 +61,30 @@ namespace DotNetOpenAuth.AspNet.Clients { /// The app secret. /// </param> public FacebookClient(string appId, string appSecret) + : this(appId, appSecret, "email") { + } + + /// <summary> + /// Initializes a new instance of the <see cref="FacebookClient"/> class. + /// </summary> + /// <param name="appId"> + /// The app id. + /// </param> + /// <param name="appSecret"> + /// The app secret. + /// </param> + /// <param name="scope"> + /// The scope of authorization to request when authenticating with Facebook. The default is "email". + /// </param> + public FacebookClient(string appId, string appSecret, params string[] scope) : base("facebook") { Requires.NotNullOrEmpty(appId, "appId"); Requires.NotNullOrEmpty(appSecret, "appSecret"); + Requires.NotNullOrEmpty(scope, "scope"); this.appId = appId; this.appSecret = appSecret; + this.scope = scope; } #endregion @@ -81,7 +105,7 @@ namespace DotNetOpenAuth.AspNet.Clients { new Dictionary<string, string> { { "client_id", this.appId }, { "redirect_uri", returnUrl.AbsoluteUri }, - { "scope", "email" }, + { "scope", string.Join(" ", this.scope) }, }); return builder.Uri; } diff --git a/src/DotNetOpenAuth.AspNet/Clients/OAuth2/MicrosoftClient.cs b/src/DotNetOpenAuth.AspNet/Clients/OAuth2/MicrosoftClient.cs index e6642da..5074c0b 100644 --- a/src/DotNetOpenAuth.AspNet/Clients/OAuth2/MicrosoftClient.cs +++ b/src/DotNetOpenAuth.AspNet/Clients/OAuth2/MicrosoftClient.cs @@ -39,21 +39,34 @@ namespace DotNetOpenAuth.AspNet.Clients { /// </summary> private readonly string appSecret; + /// <summary> + /// The requested scopes. + /// </summary> + private readonly string[] requestedScopes; + #endregion #region Constructors and Destructors /// <summary> /// Initializes a new instance of the <see cref="MicrosoftClient"/> class. + /// Requests a scope of "wl.basic" by default, but "wl.signin" is a good minimal alternative. /// </summary> - /// <param name="appId"> - /// The app id. - /// </param> - /// <param name="appSecret"> - /// The app secret. - /// </param> + /// <param name="appId">The app id.</param> + /// <param name="appSecret">The app secret.</param> public MicrosoftClient(string appId, string appSecret) - : this("microsoft", appId, appSecret) { + : this(appId, appSecret, "wl.basic") + { + } + + /// <summary> + /// Initializes a new instance of the <see cref="MicrosoftClient"/> class. + /// </summary> + /// <param name="appId">The app id.</param> + /// <param name="appSecret">The app secret.</param> + /// <param name="requestedScopes">One or more requested scopes.</param> + public MicrosoftClient(string appId, string appSecret, params string[] requestedScopes) + : this("microsoft", appId, appSecret, requestedScopes) { } /// <summary> @@ -62,13 +75,15 @@ namespace DotNetOpenAuth.AspNet.Clients { /// <param name="providerName">The provider name.</param> /// <param name="appId">The app id.</param> /// <param name="appSecret">The app secret.</param> - protected MicrosoftClient(string providerName, string appId, string appSecret) + /// <param name="requestedScopes">One or more requested scopes.</param> + protected MicrosoftClient(string providerName, string appId, string appSecret, string[] requestedScopes) : base(providerName) { Requires.NotNullOrEmpty(appId, "appId"); Requires.NotNullOrEmpty(appSecret, "appSecret"); this.appId = appId; this.appSecret = appSecret; + this.requestedScopes = requestedScopes; } #endregion @@ -94,7 +109,7 @@ namespace DotNetOpenAuth.AspNet.Clients { builder.AppendQueryArgs( new Dictionary<string, string> { { "client_id", this.appId }, - { "scope", "wl.basic" }, + { "scope", string.Join(" ", this.requestedScopes) }, { "response_type", "code" }, { "redirect_uri", returnUrl.AbsoluteUri }, }); diff --git a/src/DotNetOpenAuth.AspNet/DotNetOpenAuth.AspNet.csproj b/src/DotNetOpenAuth.AspNet/DotNetOpenAuth.AspNet.csproj index b3b52d9..2966042 100644 --- a/src/DotNetOpenAuth.AspNet/DotNetOpenAuth.AspNet.csproj +++ b/src/DotNetOpenAuth.AspNet/DotNetOpenAuth.AspNet.csproj @@ -49,6 +49,10 @@ <ItemGroup> <Compile Include="AuthenticationResult.cs" /> <Compile Include="Clients\DictionaryExtensions.cs" /> + <Compile Include="Clients\OAuth2\AzureADClaims.cs" /> + <Compile Include="Clients\OAuth2\AzureADClient.cs" /> + <Compile Include="Clients\OAuth2\AzureADGraph.cs" /> + <Compile Include="Clients\OAuth2\AzureADHeader.cs" /> <Compile Include="Clients\OAuth2\WindowsLiveClient.cs" /> <Compile Include="IAuthenticationClient.cs" /> <Compile Include="Clients\OAuth2\FacebookClient.cs" /> diff --git a/src/DotNetOpenAuth.Core/Messaging/MessagingUtilities.cs b/src/DotNetOpenAuth.Core/Messaging/MessagingUtilities.cs index df11a16..55bc691 100644 --- a/src/DotNetOpenAuth.Core/Messaging/MessagingUtilities.cs +++ b/src/DotNetOpenAuth.Core/Messaging/MessagingUtilities.cs @@ -370,7 +370,8 @@ namespace DotNetOpenAuth.Messaging { if (httpHost != null) { ErrorUtilities.VerifySupported(request.Url.Scheme == Uri.UriSchemeHttps || request.Url.Scheme == Uri.UriSchemeHttp, "Only HTTP and HTTPS are supported protocols."); - string scheme = serverVariables["HTTP_X_FORWARDED_PROTO"] ?? request.Url.Scheme; + string scheme = serverVariables["HTTP_X_FORWARDED_PROTO"] ?? + (string.Equals(serverVariables["HTTP_FRONT_END_HTTPS"], "on", StringComparison.OrdinalIgnoreCase) ? Uri.UriSchemeHttps : request.Url.Scheme); Uri hostAndPort = new Uri(scheme + Uri.SchemeDelimiter + serverVariables["HTTP_HOST"]); UriBuilder publicRequestUri = new UriBuilder(request.Url); publicRequestUri.Scheme = scheme; @@ -603,6 +604,23 @@ namespace DotNetOpenAuth.Messaging { } /// <summary> + /// Assembles the content of the HTTP Authorization or WWW-Authenticate header. + /// </summary> + /// <param name="scheme">The scheme.</param> + /// <param name="fields">The fields to include.</param> + /// <returns>A value prepared for an HTTP header.</returns> + internal static string AssembleAuthorizationHeader(string scheme, IEnumerable<KeyValuePair<string, string>> fields) { + Requires.NotNullOrEmpty(scheme, "scheme"); + Requires.NotNull(fields, "fields"); + + var authorization = new StringBuilder(); + authorization.Append(scheme); + authorization.Append(" "); + authorization.Append(AssembleAuthorizationHeader(fields)); + return authorization.ToString(); + } + + /// <summary> /// Parses the authorization header. /// </summary> /// <param name="scheme">The scheme. Must not be null or empty.</param> @@ -690,11 +708,14 @@ namespace DotNetOpenAuth.Messaging { /// Gets a NON-cryptographically strong random string of base64 characters. /// </summary> /// <param name="binaryLength">The length of the byte sequence to generate.</param> - /// <returns>A base64 encoding of the generated random data, - /// whose length in characters will likely be greater than <paramref name="binaryLength"/>.</returns> - internal static string GetNonCryptoRandomDataAsBase64(int binaryLength) { + /// <param name="useWeb64">A value indicating whether web64 encoding is used to avoid the need to escape characters.</param> + /// <returns> + /// A base64 encoding of the generated random data, + /// whose length in characters will likely be greater than <paramref name="binaryLength" />. + /// </returns> + internal static string GetNonCryptoRandomDataAsBase64(int binaryLength, bool useWeb64 = false) { byte[] uniq_bytes = GetNonCryptoRandomData(binaryLength); - string uniq = Convert.ToBase64String(uniq_bytes); + string uniq = useWeb64 ? ConvertToBase64WebSafeString(uniq_bytes) : Convert.ToBase64String(uniq_bytes); return uniq; } diff --git a/src/DotNetOpenAuth.OAuth2.Client/OAuth2/WebServerClient.cs b/src/DotNetOpenAuth.OAuth2.Client/OAuth2/WebServerClient.cs index 795047f..4225d86 100644 --- a/src/DotNetOpenAuth.OAuth2.Client/OAuth2/WebServerClient.cs +++ b/src/DotNetOpenAuth.OAuth2.Client/OAuth2/WebServerClient.cs @@ -107,7 +107,7 @@ namespace DotNetOpenAuth.OAuth2 { // If the host is implementing the authorization tracker though, they're handling this protection themselves. var cookies = new List<CookieHeaderValue>(); if (this.AuthorizationTracker == null) { - string xsrfKey = MessagingUtilities.GetNonCryptoRandomDataAsBase64(16); + string xsrfKey = MessagingUtilities.GetNonCryptoRandomDataAsBase64(16, useWeb64: true); cookies.Add(new CookieHeaderValue(XsrfCookieName, xsrfKey) { HttpOnly = true, Secure = FormsAuthentication.RequireSSL, diff --git a/src/DotNetOpenAuth.OpenId/OpenId/Extensions/SimpleRegistration/ClaimsResponse.cs b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/SimpleRegistration/ClaimsResponse.cs index 361910d..098f81d 100644 --- a/src/DotNetOpenAuth.OpenId/OpenId/Extensions/SimpleRegistration/ClaimsResponse.cs +++ b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/SimpleRegistration/ClaimsResponse.cs @@ -192,7 +192,7 @@ namespace DotNetOpenAuth.OpenId.Extensions.SimpleRegistration { } /// <summary> - /// Gets or sets a combination o the language and country of the user. + /// Gets or sets a combination of the language and country of the user. /// </summary> [XmlIgnore] public CultureInfo Culture { @@ -203,7 +203,16 @@ namespace DotNetOpenAuth.OpenId.Extensions.SimpleRegistration { if (!string.IsNullOrEmpty(this.Country)) { cultureString += "-" + this.Country; } - this.culture = CultureInfo.GetCultureInfo(cultureString); + + // 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; diff --git a/src/DotNetOpenAuth.OpenId/OpenId/UriIdentifier.cs b/src/DotNetOpenAuth.OpenId/OpenId/UriIdentifier.cs index b9f6ebc..0d020bb 100644 --- a/src/DotNetOpenAuth.OpenId/OpenId/UriIdentifier.cs +++ b/src/DotNetOpenAuth.OpenId/OpenId/UriIdentifier.cs @@ -70,6 +70,11 @@ namespace DotNetOpenAuth.OpenId { /// </remarks> [SuppressMessage("Microsoft.Performance", "CA1810:InitializeReferenceTypeStaticFieldsInline", Justification = "Some things just can't be done in a field initializer.")] static UriIdentifier() { + if (Type.GetType("Mono.Runtime") != null) { + // Uri scheme registration doesn't work on mono. + return; + } + // Our first attempt to handle trailing periods in path segments is to leverage // full trust if it's available to rewrite the rules. // In fact this is the ONLY way in .NET 3.5 (and arguably in .NET 4.0) to send diff --git a/src/DotNetOpenAuth.sln b/src/DotNetOpenAuth.sln index 2def580..049f544 100644 --- a/src/DotNetOpenAuth.sln +++ b/src/DotNetOpenAuth.sln @@ -61,6 +61,7 @@ EndProject Project("{E24C65DC-7377-472B-9ABA-BC803B73C61A}") = "DotNetOpenAuth.TestWeb", "DotNetOpenAuth.TestWeb\", "{47A84EF7-68C3-4D47-926A-9CCEA6518531}" ProjectSection(WebsiteProperties) = preProject TargetFrameworkMoniker = ".NETFramework,Version%3Dv4.5" + TargetFrameworkMoniker = ".NETFramework,Version%3Dv4.0" ProjectReferences = "{f8284738-3b5d-4733-a511-38c23f4a763f}|DotNetOpenAuth.OpenId.Provider.dll;{60426312-6AE5-4835-8667-37EDEA670222}|DotNetOpenAuth.Core.dll;{3896A32A-E876-4C23-B9B8-78E17D134CD3}|DotNetOpenAuth.OpenId.dll;{26DC877F-5987-48DD-9DDB-E62F2DE0E150}|Org.Mentalis.Security.Cryptography.dll;{F4CD3C04-6037-4946-B7A5-34BFC96A75D2}|Mono.Math.dll;" Debug.AspNetCompiler.VirtualPath = "/DotNetOpenAuth.TestWeb" Debug.AspNetCompiler.PhysicalPath = "DotNetOpenAuth.TestWeb\" @@ -88,6 +89,7 @@ EndProject Project("{E24C65DC-7377-472B-9ABA-BC803B73C61A}") = "InfoCardRelyingParty", "..\samples\InfoCardRelyingParty\", "{6EB90284-BD15-461C-BBF2-131CF55F7C8B}" ProjectSection(WebsiteProperties) = preProject TargetFrameworkMoniker = ".NETFramework,Version%3Dv4.5" + TargetFrameworkMoniker = ".NETFramework,Version%3Dv4.0" ProjectReferences = "{60426312-6ae5-4835-8667-37edea670222}|DotNetOpenAuth.Core.dll;{173e7b8d-e751-46e2-a133-f72297c0d2f4}|DotNetOpenAuth.Core.UI.dll;{408d10b8-34ba-4cbd-b7aa-feb1907aba4c}|DotNetOpenAuth.InfoCard.dll;{e040eb58-b4d2-457b-a023-ae6ef3bd34de}|DotNetOpenAuth.InfoCard.UI.dll;" Debug.AspNetCompiler.VirtualPath = "/InfoCardRelyingParty" Debug.AspNetCompiler.PhysicalPath = "..\samples\InfoCardRelyingParty\" diff --git a/src/packages/repositories.config b/src/packages/repositories.config index 0d28c5f..0c71b31 100644 --- a/src/packages/repositories.config +++ b/src/packages/repositories.config @@ -3,6 +3,7 @@ <repository path="..\..\projecttemplates\MvcRelyingParty\packages.config" /> <repository path="..\..\projecttemplates\RelyingPartyLogic\packages.config" /> <repository path="..\..\projecttemplates\WebFormsRelyingParty\packages.config" /> + <repository path="..\..\samples\TestAzureAD\packages.config" /> <repository path="..\..\samples\DotNetOpenAuth.ApplicationBlock\packages.config" /> <repository path="..\..\samples\OAuth2ProtectedWebApi\packages.config" /> <repository path="..\..\samples\OAuthAuthorizationServer\packages.config" /> @@ -45,4 +46,5 @@ <repository path="..\DotNetOpenAuth.OpenIdOAuth\packages.config" /> <repository path="..\DotNetOpenAuth.Test\packages.config" /> <repository path="..\DotNetOpenAuth.TestWeb\packages.config" /> + <repository path="..\TestAD\packages.config" /> </repositories>
\ No newline at end of file |