summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorAndrew Arnott <andrewarnott@gmail.com>2013-05-27 09:32:17 -0700
committerAndrew Arnott <andrewarnott@gmail.com>2013-05-27 09:32:17 -0700
commit5a0a8ee4c55f323a6c1fbdb619cd89b7d28a94ba (patch)
tree026bb7a58fc6b80b680f2b5be2a25ddf1efbf0f5 /src
parente4c746826690259eddba106e8a44d1b52b542faf (diff)
parent064220dbab72b00f23abd041bf4a30ea87a00d88 (diff)
downloadDotNetOpenAuth-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')
-rw-r--r--src/.gitignore1
-rw-r--r--src/DotNetOpenAuth.AspNet/Clients/OAuth2/AzureADClaims.cs75
-rw-r--r--src/DotNetOpenAuth.AspNet/Clients/OAuth2/AzureADClient.cs460
-rw-r--r--src/DotNetOpenAuth.AspNet/Clients/OAuth2/AzureADGraph.cs61
-rw-r--r--src/DotNetOpenAuth.AspNet/Clients/OAuth2/AzureADHeader.cs49
-rw-r--r--src/DotNetOpenAuth.AspNet/Clients/OAuth2/FacebookClient.cs28
-rw-r--r--src/DotNetOpenAuth.AspNet/Clients/OAuth2/MicrosoftClient.cs33
-rw-r--r--src/DotNetOpenAuth.AspNet/DotNetOpenAuth.AspNet.csproj4
-rw-r--r--src/DotNetOpenAuth.Core/Messaging/MessagingUtilities.cs31
-rw-r--r--src/DotNetOpenAuth.OAuth2.Client/OAuth2/WebServerClient.cs2
-rw-r--r--src/DotNetOpenAuth.OpenId/OpenId/Extensions/SimpleRegistration/ClaimsResponse.cs13
-rw-r--r--src/DotNetOpenAuth.OpenId/OpenId/UriIdentifier.cs5
-rw-r--r--src/DotNetOpenAuth.sln2
-rw-r--r--src/packages/repositories.config2
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