diff options
author | Microsoft <aspnet@microsoft.com> | 2011-12-08 15:50:14 -0800 |
---|---|---|
committer | Andrew Arnott <andrewarnott@gmail.com> | 2012-03-01 19:35:54 -0800 |
commit | 67e1a42ffe2ed7ac2bf99c703f17e4406cc35921 (patch) | |
tree | b117701274fea4bb5cfb1342c7ba20605fbaf13d /src/DotNetOpenAuth.Web/Clients | |
parent | 8f4165ee515728aca3faaa26e8354a40612e85e4 (diff) | |
download | DotNetOpenAuth-67e1a42ffe2ed7ac2bf99c703f17e4406cc35921.zip DotNetOpenAuth-67e1a42ffe2ed7ac2bf99c703f17e4406cc35921.tar.gz DotNetOpenAuth-67e1a42ffe2ed7ac2bf99c703f17e4406cc35921.tar.bz2 |
Add DotNetOpenAuth.Web and DotNetOpenAut.WebPages projects. Add commands to build nuget packages for DNOA.
Diffstat (limited to 'src/DotNetOpenAuth.Web/Clients')
21 files changed, 1449 insertions, 0 deletions
diff --git a/src/DotNetOpenAuth.Web/Clients/AuthenticationClientCollection.cs b/src/DotNetOpenAuth.Web/Clients/AuthenticationClientCollection.cs new file mode 100644 index 0000000..8f730e1 --- /dev/null +++ b/src/DotNetOpenAuth.Web/Clients/AuthenticationClientCollection.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.ObjectModel; + +namespace DotNetOpenAuth.Web.Clients +{ + /// <summary> + /// A collection to store instances of IAuthenticationClient by keying off ProviderName. + /// </summary> + internal sealed class AuthenticationClientCollection : KeyedCollection<string, IAuthenticationClient> + { + public AuthenticationClientCollection() + : base(StringComparer.OrdinalIgnoreCase) + { + } + + protected override string GetKeyForItem(IAuthenticationClient item) + { + return item.ProviderName; + } + } +} diff --git a/src/DotNetOpenAuth.Web/Clients/DictionaryExtensions.cs b/src/DotNetOpenAuth.Web/Clients/DictionaryExtensions.cs new file mode 100644 index 0000000..517fc47 --- /dev/null +++ b/src/DotNetOpenAuth.Web/Clients/DictionaryExtensions.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Generic; +using System.Xml.Linq; + +namespace DotNetOpenAuth.Web.Clients +{ + internal static class DictionaryExtensions + { + /// <summary> + /// Adds a key/value pair to the specified dictionary if the value is not null or empty. + /// </summary> + /// <param name="dictionary">The dictionary.</param> + /// <param name="key">The key.</param> + /// <param name="value">The value.</param> + public static void AddItemIfNotEmpty(this IDictionary<string, string> dictionary, string key, string value) + { + if (key == null) + { + throw new ArgumentNullException("key"); + } + + if (!String.IsNullOrEmpty(value)) + { + dictionary[key] = value; + } + } + + /// <summary> + /// Adds the value from an XDocument with the specified element name if it's not empty. + /// </summary> + /// <param name="dictionary">The dictionary.</param> + /// <param name="document">The document.</param> + /// <param name="elementName">Name of the element.</param> + public static void AddDataIfNotEmpty( + this Dictionary<string, string> dictionary, + XDocument document, + string elementName) + { + var element = document.Root.Element(elementName); + if (element != null) + { + dictionary.AddItemIfNotEmpty(elementName, element.Value); + } + } + } +} diff --git a/src/DotNetOpenAuth.Web/Clients/IAuthenticationClient.cs b/src/DotNetOpenAuth.Web/Clients/IAuthenticationClient.cs new file mode 100644 index 0000000..526d775 --- /dev/null +++ b/src/DotNetOpenAuth.Web/Clients/IAuthenticationClient.cs @@ -0,0 +1,33 @@ +using System; +using System.Web; + +namespace DotNetOpenAuth.Web.Clients +{ + /// <summary> + /// Represents a client which can authenticate users via an external website/provider. + /// </summary> + public interface IAuthenticationClient + { + /// <summary> + /// Gets the name of the provider which provides authentication service. + /// </summary> + string ProviderName { get; } + + /// <summary> + /// Attempts to authenticate users by forwarding them to an external website, and + /// upon succcess or failure, redirect users back to the specified url. + /// </summary> + /// <param name="context">The context of the current request.</param> + /// <param name="returnUrl">The return url after users have completed authenticating against external website.</param> + void RequestAuthentication(HttpContextBase context, Uri returnUrl); + + /// <summary> + /// Check if authentication succeeded after user is redirected back from the service provider. + /// </summary> + /// <param name="context">The context of the current request.</param> + /// <returns> + /// An instance of <see cref="AuthenticationResult"/> containing authentication result. + /// </returns> + AuthenticationResult VerifyAuthentication(HttpContextBase context); + } +} diff --git a/src/DotNetOpenAuth.Web/Clients/OAuth/DotNetOpenAuthWebConsumer.cs b/src/DotNetOpenAuth.Web/Clients/OAuth/DotNetOpenAuthWebConsumer.cs new file mode 100644 index 0000000..b66f0b1 --- /dev/null +++ b/src/DotNetOpenAuth.Web/Clients/OAuth/DotNetOpenAuthWebConsumer.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Generic; +using System.Net; +using DotNetOpenAuth.Messaging; +using DotNetOpenAuth.OAuth; +using DotNetOpenAuth.OAuth.ChannelElements; +using DotNetOpenAuth.OAuth.Messages; + +namespace DotNetOpenAuth.Web.Clients +{ + public class DotNetOpenAuthWebConsumer : IOAuthWebWorker + { + private readonly WebConsumer _webConsumer; + + public DotNetOpenAuthWebConsumer(ServiceProviderDescription serviceDescription, IConsumerTokenManager tokenManager) + { + if (serviceDescription == null) + { + throw new ArgumentNullException("consumer"); + } + + if (tokenManager == null) + { + throw new ArgumentNullException("tokenManager"); + } + + _webConsumer = new WebConsumer(serviceDescription, tokenManager); + } + + public void RequestAuthentication(Uri callback) + { + var redirectParameters = new Dictionary<string, string>() { { "force_login", "false" } }; + UserAuthorizationRequest request = _webConsumer.PrepareRequestUserAuthorization(callback, null, redirectParameters); + _webConsumer.Channel.PrepareResponse(request).Send(); + } + + public AuthorizedTokenResponse ProcessUserAuthorization() + { + return _webConsumer.ProcessUserAuthorization(); + } + + public HttpWebRequest PrepareAuthorizedRequest(MessageReceivingEndpoint profileEndpoint, string accessToken) + { + return _webConsumer.PrepareAuthorizedRequest(profileEndpoint, accessToken); + } + } +}
\ No newline at end of file diff --git a/src/DotNetOpenAuth.Web/Clients/OAuth/IOAuthWebWorker.cs b/src/DotNetOpenAuth.Web/Clients/OAuth/IOAuthWebWorker.cs new file mode 100644 index 0000000..7ffc6b4 --- /dev/null +++ b/src/DotNetOpenAuth.Web/Clients/OAuth/IOAuthWebWorker.cs @@ -0,0 +1,14 @@ +using System; +using System.Net; +using DotNetOpenAuth.Messaging; +using DotNetOpenAuth.OAuth.Messages; + +namespace DotNetOpenAuth.Web.Clients +{ + public interface IOAuthWebWorker + { + void RequestAuthentication(Uri callback); + AuthorizedTokenResponse ProcessUserAuthorization(); + HttpWebRequest PrepareAuthorizedRequest(MessageReceivingEndpoint profileEndpoint, string accessToken); + } +}
\ No newline at end of file diff --git a/src/DotNetOpenAuth.Web/Clients/OAuth/InMemoryOAuthTokenManager.cs b/src/DotNetOpenAuth.Web/Clients/OAuth/InMemoryOAuthTokenManager.cs new file mode 100644 index 0000000..0cc1fba --- /dev/null +++ b/src/DotNetOpenAuth.Web/Clients/OAuth/InMemoryOAuthTokenManager.cs @@ -0,0 +1,128 @@ +using System; +using System.Collections.Generic; +using DotNetOpenAuth.OAuth.ChannelElements; +using DotNetOpenAuth.OAuth.Messages; + +namespace DotNetOpenAuth.Web.Clients +{ + /// <summary> + /// An implementation of IOAuthTokenManager which stores keys in memory. + /// </summary> + public sealed class InMemoryOAuthTokenManager : IConsumerTokenManager + { + private readonly Dictionary<string, string> _tokensAndSecrets = new Dictionary<string, string>(); + + /// <summary> + /// Initializes a new instance of the <see cref="InMemoryOAuthTokenManager"/> class. + /// </summary> + /// <param name="consumerKey">The consumer key.</param> + /// <param name="consumerSecret">The consumer secret.</param> + public InMemoryOAuthTokenManager(string consumerKey, string consumerSecret) + { + if (consumerKey == null) + { + throw new ArgumentNullException("consumerKey"); + } + + if (consumerSecret == null) + { + throw new ArgumentNullException("consumerSecret"); + } + + ConsumerKey = consumerKey; + ConsumerSecret = consumerSecret; + } + + /// <summary> + /// Gets the consumer key. + /// </summary> + public string ConsumerKey + { + get; + private set; + } + + /// <summary> + /// Gets the consumer secret. + /// </summary> + public string ConsumerSecret + { + get; + private set; + } + + #region ITokenManager Members + + /// <summary> + /// Gets the Token Secret given a request or access token. + /// </summary> + /// <param name="token">The request or access token.</param> + /// <returns> + /// The secret associated with the given token. + /// </returns> + /// <exception cref="ArgumentException">Thrown if the secret cannot be found for the given token.</exception> + public string GetTokenSecret(string token) + { + return _tokensAndSecrets[token]; + } + + /// <summary> + /// Stores a newly generated unauthorized request token, secret, and optional + /// application-specific parameters for later recall. + /// </summary> + /// <param name="request">The request message that resulted in the generation of a new unauthorized request token.</param> + /// <param name="response">The response message that includes the unauthorized request token.</param> + /// <exception cref="ArgumentException">Thrown if the consumer key is not registered, or a required parameter was not found in the parameters collection.</exception> + /// <remarks> + /// Request tokens stored by this method SHOULD NOT associate any user account with this token. + /// It usually opens up security holes in your application to do so. Instead, you associate a user + /// account with access tokens (not request tokens) in the <see cref="ExpireRequestTokenAndStoreNewAccessToken"/> + /// method. + /// </remarks> + public void StoreNewRequestToken(UnauthorizedTokenRequest request, ITokenSecretContainingMessage response) + { + _tokensAndSecrets[response.Token] = response.TokenSecret; + } + + /// <summary> + /// Deletes a request token and its associated secret and stores a new access token and secret. + /// </summary> + /// <param name="consumerKey">The Consumer that is exchanging its request token for an access token.</param> + /// <param name="requestToken">The Consumer's request token that should be deleted/expired.</param> + /// <param name="accessToken">The new access token that is being issued to the Consumer.</param> + /// <param name="accessTokenSecret">The secret associated with the newly issued access token.</param> + /// <remarks> + /// <para> + /// Any scope of granted privileges associated with the request token from the + /// original call to <see cref="StoreNewRequestToken"/> should be carried over + /// to the new Access Token. + /// </para> + /// <para> + /// To associate a user account with the new access token, + /// <see cref="System.Web.HttpContext.User">HttpContext.Current.User</see> may be + /// useful in an ASP.NET web application within the implementation of this method. + /// Alternatively you may store the access token here without associating with a user account, + /// and wait until <see cref="WebConsumer.ProcessUserAuthorization()"/> or + /// <see cref="DesktopConsumer.ProcessUserAuthorization(string, string)"/> return the access + /// token to associate the access token with a user account at that point. + /// </para> + /// </remarks> + public void ExpireRequestTokenAndStoreNewAccessToken(string consumerKey, string requestToken, string accessToken, string accessTokenSecret) + { + _tokensAndSecrets.Remove(requestToken); + _tokensAndSecrets[accessToken] = accessTokenSecret; + } + + /// <summary> + /// Classifies a token as a request token or an access token. + /// </summary> + /// <param name="token">The token to classify.</param> + /// <returns>Request or Access token, or invalid if the token is not recognized.</returns> + public TokenType GetTokenType(string token) + { + throw new NotImplementedException(); + } + + #endregion + } +}
\ No newline at end of file diff --git a/src/DotNetOpenAuth.Web/Clients/OAuth/LinkedInClient.cs b/src/DotNetOpenAuth.Web/Clients/OAuth/LinkedInClient.cs new file mode 100644 index 0000000..ddbc39f --- /dev/null +++ b/src/DotNetOpenAuth.Web/Clients/OAuth/LinkedInClient.cs @@ -0,0 +1,95 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Xml.Linq; +using DotNetOpenAuth.Messaging; +using DotNetOpenAuth.OAuth; +using DotNetOpenAuth.OAuth.ChannelElements; +using DotNetOpenAuth.OAuth.Messages; + +namespace DotNetOpenAuth.Web.Clients +{ + /// <summary> + /// Represents LinkedIn authentication client. + /// </summary> + internal sealed class LinkedInClient : OAuthClient + { + public static readonly ServiceProviderDescription LinkedInServiceDescription = new ServiceProviderDescription + { + RequestTokenEndpoint = new MessageReceivingEndpoint("https://api.linkedin.com/uas/oauth/requestToken", HttpDeliveryMethods.GetRequest | HttpDeliveryMethods.AuthorizationHeaderRequest), + UserAuthorizationEndpoint = new MessageReceivingEndpoint("https://www.linkedin.com/uas/oauth/authenticate", HttpDeliveryMethods.GetRequest | HttpDeliveryMethods.AuthorizationHeaderRequest), + AccessTokenEndpoint = new MessageReceivingEndpoint("https://api.linkedin.com/uas/oauth/accessToken", HttpDeliveryMethods.GetRequest | HttpDeliveryMethods.AuthorizationHeaderRequest), + TamperProtectionElements = new ITamperProtectionChannelBindingElement[] { new HmacSha1SigningBindingElement() }, + }; + + /// <summary> + /// Initializes a new instance of the <see cref="LinkedInClient"/> class. + /// </summary> + /// <param name="consumerKey">The LinkedIn app's consumer key.</param> + /// <param name="consumerSecret">The LinkedIn app's consumer secret.</param> + [System.Diagnostics.CodeAnalysis.SuppressMessage( + "Microsoft.Reliability", + "CA2000:Dispose objects before losing scope", + Justification = "We can't dispose the object because we still need it through the app lifetime.")] + public LinkedInClient(string consumerKey, string consumerSecret) : + base("linkedIn", LinkedInServiceDescription, consumerKey, consumerSecret) + { + } + + /// <summary> + /// Check if authentication succeeded after user is redirected back from the service provider. + /// </summary> + /// <param name="response">The response token returned from service provider</param> + /// <returns> + /// Authentication result. + /// </returns> + [System.Diagnostics.CodeAnalysis.SuppressMessage( + "Microsoft.Design", + "CA1031:DoNotCatchGeneralExceptionTypes", + Justification = "We don't care if the request fails.")] + protected override AuthenticationResult VerifyAuthenticationCore(AuthorizedTokenResponse response) + { + // See here for Field Selectors API http://developer.linkedin.com/docs/DOC-1014 + const string profileRequestUrl = "http://api.linkedin.com/v1/people/~:(id,first-name,last-name,headline,industry,summary)"; + + string accessToken = response.AccessToken; + + var profileEndpoint = new MessageReceivingEndpoint(profileRequestUrl, HttpDeliveryMethods.GetRequest); + HttpWebRequest request = WebWorker.PrepareAuthorizedRequest(profileEndpoint, accessToken); + + try + { + using (WebResponse profileResponse = request.GetResponse()) + { + using (Stream responseStream = profileResponse.GetResponseStream()) + { + XDocument document = XDocument.Load(responseStream); + string userId = document.Root.Element("id").Value; + + string firstName = document.Root.Element("first-name").Value; + string lastName = document.Root.Element("last-name").Value; + string userName = firstName + " " + lastName; + + var extraData = new Dictionary<string, string>(); + extraData.Add("name", userName); + extraData.AddDataIfNotEmpty(document, "headline"); + extraData.AddDataIfNotEmpty(document, "summary"); + extraData.AddDataIfNotEmpty(document, "industry"); + + return new AuthenticationResult( + isSuccessful: true, + provider: ProviderName, + providerUserId: userId, + userName: userName, + extraData: extraData); + } + } + } + catch (Exception exception) + { + return new AuthenticationResult(exception); + } + } + } +}
\ No newline at end of file diff --git a/src/DotNetOpenAuth.Web/Clients/OAuth/OAuthClient.cs b/src/DotNetOpenAuth.Web/Clients/OAuth/OAuthClient.cs new file mode 100644 index 0000000..d0d7751 --- /dev/null +++ b/src/DotNetOpenAuth.Web/Clients/OAuth/OAuthClient.cs @@ -0,0 +1,116 @@ +using System; +using System.Web; +using DotNetOpenAuth.Messaging; +using DotNetOpenAuth.OAuth; +using DotNetOpenAuth.OAuth.ChannelElements; +using DotNetOpenAuth.OAuth.Messages; + +namespace DotNetOpenAuth.Web.Clients +{ + /// <summary> + /// Represents base class for OAuth 1.0 clients + /// </summary> + public abstract class OAuthClient : IAuthenticationClient + { + /// <summary> + /// Initializes a new instance of the <see cref="OAuthClient"/> class. + /// </summary> + /// <param name="providerName">Name of the provider.</param> + /// <param name="serviceDescription">The service description.</param> + /// <param name="consumerKey">The consumer key.</param> + /// <param name="consumerSecret">The consumer secret.</param> + protected OAuthClient(string providerName, ServiceProviderDescription serviceDescription, string consumerKey, string consumerSecret) : + this(providerName, serviceDescription, new InMemoryOAuthTokenManager(consumerKey, consumerSecret)) + { + } + + /// <summary> + /// Initializes a new instance of the <see cref="OAuthClient"/> class. + /// </summary> + /// <param name="providerName">Name of the provider.</param> + protected OAuthClient(string providerName, ServiceProviderDescription serviceDescription, IConsumerTokenManager tokenManager) : + this(providerName, new DotNetOpenAuthWebConsumer(serviceDescription, tokenManager)) + { + } + + protected OAuthClient(string providerName, IOAuthWebWorker webWorker) + { + if (providerName == null) + { + throw new ArgumentNullException("providerName"); + } + + if (webWorker == null) + { + throw new ArgumentNullException("webWorker"); + } + + ProviderName = providerName; + WebWorker = webWorker; + } + + /// <summary> + /// Gets the name of the provider which provides authentication service. + /// </summary> + public string ProviderName + { + get; + private set; + } + + /// <summary> + /// Gets the <see cref="OAuthWebConsumer"/> instance which handles constructing requests + /// to the OAuth providers. + /// </summary> + protected IOAuthWebWorker WebWorker + { + get; + private set; + } + + /// <summary> + /// Attempts to authenticate users by forwarding them to an external website, and + /// upon succcess or failure, redirect users back to the specified url. + /// </summary> + /// <param name="returnUrl">The return url after users have completed authenticating against external website.</param> + public virtual void RequestAuthentication(HttpContextBase context, Uri returnUrl) + { + if (returnUrl == null) + { + throw new ArgumentNullException("returnUrl"); + } + + if (context == null) + { + throw new ArgumentNullException("context"); + } + + Uri callback = returnUrl.StripQueryArgumentsWithPrefix("oauth_"); + WebWorker.RequestAuthentication(callback); + } + + /// <summary> + /// Check if authentication succeeded after user is redirected back from the service provider. + /// </summary> + /// <returns> + /// An instance of <see cref="AuthenticationResult"/> containing authentication result. + /// </returns> + public virtual AuthenticationResult VerifyAuthentication(HttpContextBase context) + { + AuthorizedTokenResponse response = WebWorker.ProcessUserAuthorization(); + if (response == null) + { + return AuthenticationResult.Failed; + } + + return VerifyAuthenticationCore(response); + } + + /// <summary> + /// Check if authentication succeeded after user is redirected back from the service provider. + /// </summary> + /// <param name="response">The response token returned from service provider</param> + /// <returns>Authentication result</returns> + protected abstract AuthenticationResult VerifyAuthenticationCore(AuthorizedTokenResponse response); + } +}
\ No newline at end of file diff --git a/src/DotNetOpenAuth.Web/Clients/OAuth/TwitterClient.cs b/src/DotNetOpenAuth.Web/Clients/OAuth/TwitterClient.cs new file mode 100644 index 0000000..6d82607 --- /dev/null +++ b/src/DotNetOpenAuth.Web/Clients/OAuth/TwitterClient.cs @@ -0,0 +1,94 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Xml.Linq; +using DotNetOpenAuth.Messaging; +using DotNetOpenAuth.OAuth; +using DotNetOpenAuth.OAuth.ChannelElements; +using DotNetOpenAuth.OAuth.Messages; + +namespace DotNetOpenAuth.Web.Clients +{ + /// <summary> + /// Represents a Twitter client + /// </summary> + internal class TwitterClient : OAuthClient + { + /// <summary> + /// The description of Twitter's OAuth protocol URIs for use with their "Sign in with Twitter" feature. + /// </summary> + public static readonly ServiceProviderDescription TwitterServiceDescription = new ServiceProviderDescription + { + RequestTokenEndpoint = new MessageReceivingEndpoint("http://twitter.com/oauth/request_token", HttpDeliveryMethods.GetRequest | HttpDeliveryMethods.AuthorizationHeaderRequest), + UserAuthorizationEndpoint = new MessageReceivingEndpoint("http://twitter.com/oauth/authenticate", HttpDeliveryMethods.GetRequest | HttpDeliveryMethods.AuthorizationHeaderRequest), + AccessTokenEndpoint = new MessageReceivingEndpoint("http://twitter.com/oauth/access_token", HttpDeliveryMethods.GetRequest | HttpDeliveryMethods.AuthorizationHeaderRequest), + TamperProtectionElements = new ITamperProtectionChannelBindingElement[] { new HmacSha1SigningBindingElement() }, + }; + + /// <summary> + /// Initializes a new instance of the <see cref="TwitterClient"/> class with the specified consumer key and consumer secret. + /// </summary> + /// <param name="consumerKey">The consumer key.</param> + /// <param name="consumerSecret">The consumer secret.</param> + [System.Diagnostics.CodeAnalysis.SuppressMessage( + "Microsoft.Reliability", + "CA2000:Dispose objects before losing scope", + Justification = "We can't dispose the object because we still need it through the app lifetime.")] + public TwitterClient(string consumerKey, string consumerSecret) : + base("twitter", TwitterServiceDescription, consumerKey, consumerSecret) + { + } + + /// <summary> + /// Check if authentication succeeded after user is redirected back from the service provider. + /// </summary> + /// <param name="response">The response token returned from service provider</param> + /// <returns> + /// Authentication result + /// </returns> + [System.Diagnostics.CodeAnalysis.SuppressMessage( + "Microsoft.Design", + "CA1031:DoNotCatchGeneralExceptionTypes", + Justification = "We don't care if the request for additional data fails.")] + protected override AuthenticationResult VerifyAuthenticationCore(AuthorizedTokenResponse response) + { + string accessToken = response.AccessToken; + string userId = response.ExtraData["user_id"]; + string userName = response.ExtraData["screen_name"]; + + string profileRequestUrl = "http://api.twitter.com/1/users/show.xml?user_id=" + Uri.EscapeDataString(userId); + var profileEndpoint = new MessageReceivingEndpoint(profileRequestUrl, HttpDeliveryMethods.GetRequest); + HttpWebRequest request = WebWorker.PrepareAuthorizedRequest(profileEndpoint, accessToken); + + var extraData = new Dictionary<string, string>(); + try + { + using (WebResponse profileResponse = request.GetResponse()) + { + using (Stream responseStream = profileResponse.GetResponseStream()) + { + XDocument document = XDocument.Load(responseStream); + extraData.AddDataIfNotEmpty(document, "name"); + extraData.AddDataIfNotEmpty(document, "location"); + extraData.AddDataIfNotEmpty(document, "description"); + extraData.AddDataIfNotEmpty(document, "url"); + } + } + } + catch (Exception) + { + // At this point, the authentication is already successful. + // Here we are just trying to get additional data if we can. + // If it fails, no problem. + } + + return new AuthenticationResult( + isSuccessful: true, + provider: ProviderName, + providerUserId: userId, + userName: userName, + extraData: extraData); + } + } +}
\ No newline at end of file diff --git a/src/DotNetOpenAuth.Web/Clients/OAuth2/FacebookClient.cs b/src/DotNetOpenAuth.Web/Clients/OAuth2/FacebookClient.cs new file mode 100644 index 0000000..e9e0b80 --- /dev/null +++ b/src/DotNetOpenAuth.Web/Clients/OAuth2/FacebookClient.cs @@ -0,0 +1,103 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Net; +using System.Web; +using DotNetOpenAuth.Web.Resources; + +namespace DotNetOpenAuth.Web.Clients +{ + internal sealed class FacebookClient : OAuth2Client + { + private const string AuthorizationEndpoint = "https://www.facebook.com/dialog/oauth"; + private const string TokenEndpoint = "https://graph.facebook.com/oauth/access_token"; + + private readonly string _appId; + private readonly string _appSecret; + + public FacebookClient(string appId, string appSecret) + : base("facebook") + { + if (String.IsNullOrEmpty(appId)) + { + throw new ArgumentException( + String.Format(CultureInfo.CurrentCulture, WebResources.Argument_Cannot_Be_Null_Or_Empty, "appId"), + "appId"); + } + + if (String.IsNullOrEmpty("appSecret")) + { + throw new ArgumentException( + String.Format(CultureInfo.CurrentCulture, WebResources.Argument_Cannot_Be_Null_Or_Empty, "appSecret"), + "appSecret"); + } + + _appId = appId; + _appSecret = appSecret; + } + + protected override Uri GetServiceLoginUrl(Uri returnUrl) + { + // Note: Facebook doesn't like us to url-encode the redirect_uri value + var builder = new UriBuilder(AuthorizationEndpoint); + builder.AppendQueryArguments(new Dictionary<string, string> + { + { "client_id", _appId }, + { "redirect_uri", returnUrl.ToString() } + }); + return builder.Uri; + } + + protected override string QueryAccessToken(Uri returnUrl, string authorizationCode) + { + // Note: Facebook doesn't like us to url-encode the redirect_uri value + var builder = new UriBuilder(TokenEndpoint); + builder.AppendQueryArguments(new Dictionary<string, string> + { + { "client_id", _appId }, + { "redirect_uri", returnUrl.ToString() }, + { "client_secret", _appSecret }, + { "code", authorizationCode } + }); + + using (WebClient client = new WebClient()) + { + string data = client.DownloadString(builder.Uri); + if (String.IsNullOrEmpty(data)) + { + return null; + } + + var parsedQueryString = HttpUtility.ParseQueryString(data); + if (parsedQueryString != null) + { + return parsedQueryString["access_token"]; + } + } + return null; + } + + protected override IDictionary<string, string> GetUserData(string accessToken) + { + FacebookGraph graph; + var request = WebRequest.Create("https://graph.facebook.com/me?access_token=" + Uri.EscapeDataString(accessToken)); + using (var response = request.GetResponse()) + { + using (var responseStream = response.GetResponseStream()) + { + graph = JsonHelper.Deserialize<FacebookGraph>(responseStream); + } + } + + // this dictionary must contains + var userData = new Dictionary<string, string>(); + userData.AddItemIfNotEmpty("id", graph.Id); + userData.AddItemIfNotEmpty("username", graph.Email); + userData.AddItemIfNotEmpty("name", graph.Name); + userData.AddItemIfNotEmpty("link", graph.Link == null ? null : graph.Link.ToString()); + userData.AddItemIfNotEmpty("gender", graph.Gender); + userData.AddItemIfNotEmpty("birthday", graph.Birthday); + return userData; + } + } +} diff --git a/src/DotNetOpenAuth.Web/Clients/OAuth2/FacebookGraph.cs b/src/DotNetOpenAuth.Web/Clients/OAuth2/FacebookGraph.cs new file mode 100644 index 0000000..43f31eb --- /dev/null +++ b/src/DotNetOpenAuth.Web/Clients/OAuth2/FacebookGraph.cs @@ -0,0 +1,34 @@ +using System; +using System.Runtime.Serialization; + +namespace DotNetOpenAuth.Web.Clients +{ + /// <summary> + /// Contains data of a Facebook 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] + public class FacebookGraph + { + [DataMember(Name = "id")] + public string Id { get; set; } + + [DataMember(Name = "email")] + public string Email { get; set; } + + [DataMember(Name = "name")] + public string Name { get; set; } + + [DataMember(Name = "link")] + public Uri Link { get; set; } + + [DataMember(Name = "gender")] + public string Gender { get; set; } + + [DataMember(Name = "birthday")] + public string Birthday { get; set; } + } +} diff --git a/src/DotNetOpenAuth.Web/Clients/OAuth2/JsonHelper.cs b/src/DotNetOpenAuth.Web/Clients/OAuth2/JsonHelper.cs new file mode 100644 index 0000000..26cc61a --- /dev/null +++ b/src/DotNetOpenAuth.Web/Clients/OAuth2/JsonHelper.cs @@ -0,0 +1,20 @@ +using System; +using System.IO; +using System.Runtime.Serialization.Json; + +namespace DotNetOpenAuth.Web.Clients +{ + internal static class JsonHelper + { + public static T Deserialize<T>(Stream stream) where T : class + { + if (stream == null) + { + throw new ArgumentNullException("stream"); + } + + var serializer = new DataContractJsonSerializer(typeof(T)); + return (T)serializer.ReadObject(stream); + } + } +} diff --git a/src/DotNetOpenAuth.Web/Clients/OAuth2/OAuth2AccessTokenData.cs b/src/DotNetOpenAuth.Web/Clients/OAuth2/OAuth2AccessTokenData.cs new file mode 100644 index 0000000..9649ad5 --- /dev/null +++ b/src/DotNetOpenAuth.Web/Clients/OAuth2/OAuth2AccessTokenData.cs @@ -0,0 +1,20 @@ +using System.Runtime.Serialization; + +namespace DotNetOpenAuth.Web.Clients +{ + [DataContract] + public class OAuth2AccessTokenData + { + [DataMember(Name = "access_token")] + public string AccessToken { get; set; } + + [DataMember(Name = "refresh_token")] + public string RefreshToken { get; set; } + + [DataMember(Name = "scope")] + public string Scope { get; set; } + + [DataMember(Name = "token_type")] + public string TokenType { get; set; } + } +} diff --git a/src/DotNetOpenAuth.Web/Clients/OAuth2/OAuth2Client.cs b/src/DotNetOpenAuth.Web/Clients/OAuth2/OAuth2Client.cs new file mode 100644 index 0000000..9920065 --- /dev/null +++ b/src/DotNetOpenAuth.Web/Clients/OAuth2/OAuth2Client.cs @@ -0,0 +1,133 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Web; + +namespace DotNetOpenAuth.Web.Clients +{ + /// <summary> + /// Represents the base class for OAuth 2.0 clients + /// </summary> + public abstract class OAuth2Client : IAuthenticationClient + { + private readonly string _providerName; + private Uri _returnUrl; + + /// <summary> + /// Initializes a new instance of the <see cref="OAuth2Client"/> class with the specified provider name. + /// </summary> + /// <param name="providerName">Name of the provider.</param> + protected OAuth2Client(string providerName) + { + if (providerName == null) + { + throw new ArgumentNullException("providerName"); + } + + _providerName = providerName; + } + + /// <summary> + /// Gets the name of the provider which provides authentication service. + /// </summary> + public string ProviderName + { + get { return _providerName; } + } + + /// <summary> + /// Attempts to authenticate users by forwarding them to an external website, and + /// upon succcess or failure, redirect users back to the specified url. + /// </summary> + /// <param name="returnUrl">The return url after users have completed authenticating against external website.</param> + public virtual void RequestAuthentication(HttpContextBase context, Uri returnUrl) + { + if (context == null) + { + throw new ArgumentNullException("context"); + } + + if (returnUrl == null) + { + throw new ArgumentNullException("returnUrl"); + } + + _returnUrl = returnUrl; + + string redirectUrl = GetServiceLoginUrl(returnUrl).ToString(); + context.Response.Redirect(redirectUrl, endResponse: true); + } + + /// <summary> + /// Check if authentication succeeded after user is redirected back from the service provider. + /// </summary> + /// <returns> + /// An instance of <see cref="AuthenticationResult"/> containing authentication result. + /// </returns> + public virtual AuthenticationResult VerifyAuthentication(HttpContextBase context) + { + if (context == null) + { + throw new ArgumentNullException("context"); + } + + string code = context.Request.QueryString["code"]; + if (String.IsNullOrEmpty(code)) + { + return AuthenticationResult.Failed; + } + + string accessToken = QueryAccessToken(_returnUrl, code); + if (accessToken == null) + { + return AuthenticationResult.Failed; + } + + IDictionary<string, string> userData = GetUserData(accessToken); + if (userData == null) + { + return AuthenticationResult.Failed; + } + string id = userData["id"]; + string name; + // Some oAuth providers do not return value for the 'username' attribute. + // In that case, try the 'name' attribute. If it's still unavailable, fall back to 'id' + if (!userData.TryGetValue("username", out name) && !userData.TryGetValue("name", out name)) + { + name = id; + } + + return new AuthenticationResult( + isSuccessful: true, + provider: ProviderName, + providerUserId: id, + userName: name, + extraData: userData); + } + + /// <summary> + /// Gets the full url pointing to the login page for this client. The url should include the + /// specified return url so that when the login completes, user is redirected back to that url. + /// </summary> + /// <param name="returnUrl">The return URL.</param> + /// <returns></returns> + [SuppressMessage("Microsoft.Naming", "CA1726:UsePreferredTerms", MessageId = "Login", Justification = "Login is used more consistently in ASP.Net")] + protected abstract Uri GetServiceLoginUrl(Uri returnUrl); + + /// <summary> + /// Queries the access token from the specified authorization code. + /// </summary> + /// <param name="returnUrl">The return URL.</param> + /// <param name="authorizationCode">The authorization code.</param> + /// <returns></returns> + protected abstract string QueryAccessToken(Uri returnUrl, string authorizationCode); + + /// <summary> + /// Given the access token, gets the logged-in user's data. The returned dictionary must include + /// two keys 'id', and 'username'. + /// </summary> + /// <param name="accessToken">The access token of the current user.</param> + /// <returns>A dictionary contains key-value pairs of user data</returns> + protected abstract IDictionary<string, string> GetUserData(string accessToken); + } +}
\ No newline at end of file diff --git a/src/DotNetOpenAuth.Web/Clients/OAuth2/WindowsLiveClient.cs b/src/DotNetOpenAuth.Web/Clients/OAuth2/WindowsLiveClient.cs new file mode 100644 index 0000000..9184be6 --- /dev/null +++ b/src/DotNetOpenAuth.Web/Clients/OAuth2/WindowsLiveClient.cs @@ -0,0 +1,107 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Text; + +namespace DotNetOpenAuth.Web.Clients +{ + internal sealed class WindowsLiveClient : OAuth2Client + { + private const string TokenEndpoint = "https://oauth.live.com/token"; + private const string AuthorizationEndpoint = "https://oauth.live.com/authorize"; + private readonly string _appId; + private readonly string _appSecret; + + public WindowsLiveClient(string appId, string appSecret) + : base("windowslive") + { + if (String.IsNullOrEmpty(appId)) + { + throw new ArgumentNullException("appId"); + } + + if (String.IsNullOrEmpty("appSecret")) + { + throw new ArgumentNullException("appSecret"); + } + + _appId = appId; + _appSecret = appSecret; + } + + protected override Uri GetServiceLoginUrl(Uri returnUrl) + { + var builder = new UriBuilder(AuthorizationEndpoint); + builder.AppendQueryArguments(new Dictionary<string, string> + { + { "client_id", _appId }, + { "scope", "wl.basic" }, + { "response_type", "code" }, + { "redirect_uri", returnUrl.ToString() } + }); + + return builder.Uri; + } + + protected override string QueryAccessToken(Uri returnUrl, string authorizationCode) + { + var builder = new StringBuilder(); + builder.AppendFormat("client_id={0}", _appId); + builder.AppendFormat("&redirect_uri={0}", Uri.EscapeDataString(returnUrl.ToString())); + builder.AppendFormat("&client_secret={0}", _appSecret); + builder.AppendFormat("&code={0}", authorizationCode); + builder.Append("&grant_type=authorization_code"); + + WebRequest tokenRequest = WebRequest.Create(TokenEndpoint); + tokenRequest.ContentType = "application/x-www-form-urlencoded"; + tokenRequest.ContentLength = builder.Length; + tokenRequest.Method = "POST"; + + using (Stream requestStream = tokenRequest.GetRequestStream()) + { + var writer = new StreamWriter(requestStream); + writer.Write(builder.ToString()); + 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) + { + return tokenData.AccessToken; + } + } + } + + return null; + } + + protected override IDictionary<string, string> GetUserData(string accessToken) + { + WindowsLiveUserData graph; + var request = WebRequest.Create("https://apis.live.net/v5.0/me?access_token=" + Uri.EscapeDataString(accessToken)); + using (var response = request.GetResponse()) + { + using (var responseStream = response.GetResponseStream()) + { + graph = JsonHelper.Deserialize<WindowsLiveUserData>(responseStream); + } + } + + var userData = new Dictionary<string, string>(); + userData.AddItemIfNotEmpty("id", graph.Id); + userData.AddItemIfNotEmpty("username", graph.Name); + userData.AddItemIfNotEmpty("name", graph.Name); + userData.AddItemIfNotEmpty("link", graph.Link == null ? null : graph.Link.ToString()); + userData.AddItemIfNotEmpty("gender", graph.Gender); + userData.AddItemIfNotEmpty("firstname", graph.FirstName); + userData.AddItemIfNotEmpty("lastname", graph.LastName); + return userData; + } + } +} diff --git a/src/DotNetOpenAuth.Web/Clients/OAuth2/WindowsLiveUserData.cs b/src/DotNetOpenAuth.Web/Clients/OAuth2/WindowsLiveUserData.cs new file mode 100644 index 0000000..cc6fa27 --- /dev/null +++ b/src/DotNetOpenAuth.Web/Clients/OAuth2/WindowsLiveUserData.cs @@ -0,0 +1,34 @@ +using System; +using System.Runtime.Serialization; + +namespace DotNetOpenAuth.Web.Clients +{ + /// <summary> + /// Contains data of a Windows Live 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] + public class WindowsLiveUserData + { + [DataMember(Name = "id")] + public string Id { get; set; } + + [DataMember(Name = "name")] + public string Name { get; set; } + + [DataMember(Name = "link")] + public Uri Link { get; set; } + + [DataMember(Name = "gender")] + public string Gender { get; set; } + + [DataMember(Name = "first_name")] + public string FirstName { get; set; } + + [DataMember(Name = "last_name")] + public string LastName { get; set; } + } +} diff --git a/src/DotNetOpenAuth.Web/Clients/OpenID/AxKnownAttributes.cs b/src/DotNetOpenAuth.Web/Clients/OpenID/AxKnownAttributes.cs new file mode 100644 index 0000000..1afcc65 --- /dev/null +++ b/src/DotNetOpenAuth.Web/Clients/OpenID/AxKnownAttributes.cs @@ -0,0 +1,12 @@ +namespace DotNetOpenAuth.Web.Clients +{ + /// <summary> + /// Contains namespace values of common attributes used for Attribute Exchange extensions + /// </summary> + internal static class AxKnownAttributes + { + public const string FirstName = "http://axschema.org/namePerson/first"; + public const string LastName = "http://axschema.org/namePerson/last"; + public const string FullName = "http://axschema.org/namePerson"; + } +} diff --git a/src/DotNetOpenAuth.Web/Clients/OpenID/GoogleOpenIdClient.cs b/src/DotNetOpenAuth.Web/Clients/OpenID/GoogleOpenIdClient.cs new file mode 100644 index 0000000..61b88ee --- /dev/null +++ b/src/DotNetOpenAuth.Web/Clients/OpenID/GoogleOpenIdClient.cs @@ -0,0 +1,55 @@ +using System.Collections.Generic; +using DotNetOpenAuth.OpenId.Extensions.AttributeExchange; +using DotNetOpenAuth.OpenId.RelyingParty; + +namespace DotNetOpenAuth.Web.Clients +{ + /// <summary> + /// Represents Google OpenID client. + /// </summary> + internal sealed class GoogleOpenIdClient : OpenIDClient + { + public GoogleOpenIdClient() : + base("google", "https://www.google.com/accounts/o8/id") + { + } + + /// <summary> + /// Called just before the authentication request is sent to service provider. + /// </summary> + /// <param name="request">The request.</param> + protected override void OnBeforeSendingAuthenticationRequest(IAuthenticationRequest request) + { + // Attribute Exchange extensions + var fetchRequest = new FetchRequest(); + fetchRequest.Attributes.Add(new AttributeRequest(WellKnownAttributes.Contact.Email, isRequired: true)); + fetchRequest.Attributes.Add(new AttributeRequest(WellKnownAttributes.Contact.HomeAddress.Country, isRequired: false)); + fetchRequest.Attributes.Add(new AttributeRequest(AxKnownAttributes.FirstName, isRequired: false)); + fetchRequest.Attributes.Add(new AttributeRequest(AxKnownAttributes.LastName, isRequired: false)); + + request.AddExtension(fetchRequest); + } + + /// <summary> + /// Gets the extra data obtained from the response message when authentication is successful. + /// </summary> + /// <param name="response">The response message.</param> + /// <returns></returns> + protected override Dictionary<string, string> GetExtraData(IAuthenticationResponse response) + { + FetchResponse fetchResponse = response.GetExtension<FetchResponse>(); + if (fetchResponse != null) + { + var extraData = new Dictionary<string, string>(); + extraData.AddItemIfNotEmpty("email", fetchResponse.GetAttributeValue(WellKnownAttributes.Contact.Email)); + extraData.AddItemIfNotEmpty("country", fetchResponse.GetAttributeValue(WellKnownAttributes.Contact.HomeAddress.Country)); + extraData.AddItemIfNotEmpty("firstName", fetchResponse.GetAttributeValue(AxKnownAttributes.FirstName)); + extraData.AddItemIfNotEmpty("lastName", fetchResponse.GetAttributeValue(AxKnownAttributes.LastName)); + + return extraData; + } + + return null; + } + } +}
\ No newline at end of file diff --git a/src/DotNetOpenAuth.Web/Clients/OpenID/OpenIDClient.cs b/src/DotNetOpenAuth.Web/Clients/OpenID/OpenIDClient.cs new file mode 100644 index 0000000..f0f938e --- /dev/null +++ b/src/DotNetOpenAuth.Web/Clients/OpenID/OpenIDClient.cs @@ -0,0 +1,141 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Web; +using DotNetOpenAuth.OpenId; +using DotNetOpenAuth.OpenId.RelyingParty; +using DotNetOpenAuth.Web.Resources; + +namespace DotNetOpenAuth.Web.Clients +{ + /// <summary> + /// Base classes for OpenID clients. + /// </summary> + internal class OpenIDClient : IAuthenticationClient + { + private readonly Identifier _providerIdentifier; + private readonly string _providerName; + + private static OpenIdRelyingParty _openidRelayingParty = + new OpenIdRelyingParty(new StandardRelyingPartyApplicationStore()); + + /// <summary> + /// Initializes a new instance of the <see cref="OpenIDClient"/> class. + /// </summary> + /// <param name="providerName">Name of the provider.</param> + /// <param name="providerIdentifier">The provider identifier, which is the usually the login url of the specified provider.</param> + public OpenIDClient(string providerName, string providerIdentifier) + { + if (String.IsNullOrEmpty(providerIdentifier)) + { + throw new ArgumentException( + String.Format(CultureInfo.CurrentCulture, WebResources.Argument_Cannot_Be_Null_Or_Empty, "providerIdentifier"), + "providerIdentifier"); + } + + if (String.IsNullOrEmpty(providerName)) + { + throw new ArgumentException( + String.Format(CultureInfo.CurrentCulture, WebResources.Argument_Cannot_Be_Null_Or_Empty, "providerName"), + "providerName"); + } + + _providerName = providerName; + if (!Identifier.TryParse(providerIdentifier, out _providerIdentifier) || _providerIdentifier == null) + { + throw new ArgumentException(WebResources.OpenIDInvalidIdentifier, "providerIdentifier"); + } + } + + /// <summary> + /// Gets the name of the provider which provides authentication service. + /// </summary> + public string ProviderName + { + get + { + return _providerName; + } + } + + /// <summary> + /// Attempts to authenticate users by forwarding them to an external website, and + /// upon succcess or failure, redirect users back to the specified url. + /// </summary> + /// <param name="returnUrl">The return url after users have completed authenticating against external website.</param> + [System.Diagnostics.CodeAnalysis.SuppressMessage( + "Microsoft.Usage", + "CA2234:PassSystemUriObjectsInsteadOfStrings", + Justification = "We don't have a Uri object handy.")] + public virtual void RequestAuthentication(HttpContextBase context, Uri returnUrl) + { + if (returnUrl == null) + { + throw new ArgumentNullException("returnUrl"); + } + + var realm = new Realm(returnUrl.GetComponents(UriComponents.SchemeAndServer, UriFormat.Unescaped)); + IAuthenticationRequest request = _openidRelayingParty.CreateRequest(_providerIdentifier, realm, returnUrl); + + // give subclasses a chance to modify request message, e.g. add extension attributes, etc. + OnBeforeSendingAuthenticationRequest(request); + + request.RedirectToProvider(); + } + + /// <summary> + /// Called just before the authentication request is sent to service provider. + /// </summary> + /// <param name="request">The request.</param> + protected virtual void OnBeforeSendingAuthenticationRequest(IAuthenticationRequest request) + { + } + + /// <summary> + /// Check if authentication succeeded after user is redirected back from the service provider. + /// </summary> + /// <returns> + /// An instance of <see cref="AuthenticationResult"/> containing authentication result. + /// </returns> + public virtual AuthenticationResult VerifyAuthentication(HttpContextBase context) + { + IAuthenticationResponse response = _openidRelayingParty.GetResponse(); + if (response == null) + { + throw new InvalidOperationException(WebResources.OpenIDFailedToGetResponse); + } + + if (response.Status == AuthenticationStatus.Authenticated) + { + string id = response.ClaimedIdentifier; + string username; + + Dictionary<string, string> extraData = GetExtraData(response) ?? new Dictionary<string, string>(); + // try to look up username from the 'username' or 'email' property. If not found, fall back to 'friendly id' + if (!extraData.TryGetValue("username", out username) && !extraData.TryGetValue("email", out username)) + { + username = response.FriendlyIdentifierForDisplay; + } + + return new AuthenticationResult( + true, + ProviderName, + id, + username, + extraData); + } + + return AuthenticationResult.Failed; + } + + /// <summary> + /// Gets the extra data obtained from the response message when authentication is successful. + /// </summary> + /// <param name="response">The response message.</param> + /// <returns></returns> + protected virtual Dictionary<string, string> GetExtraData(IAuthenticationResponse response) + { + return null; + } + } +}
\ No newline at end of file diff --git a/src/DotNetOpenAuth.Web/Clients/OpenID/YahooOpenIdClient.cs b/src/DotNetOpenAuth.Web/Clients/OpenID/YahooOpenIdClient.cs new file mode 100644 index 0000000..2235a2b --- /dev/null +++ b/src/DotNetOpenAuth.Web/Clients/OpenID/YahooOpenIdClient.cs @@ -0,0 +1,48 @@ +using System.Collections.Generic; +using DotNetOpenAuth.OpenId.Extensions.AttributeExchange; +using DotNetOpenAuth.OpenId.RelyingParty; + +namespace DotNetOpenAuth.Web.Clients +{ + internal sealed class YahooOpenIdClient : OpenIDClient + { + public YahooOpenIdClient() : + base("yahoo", "http://me.yahoo.com") + { + } + + /// <summary> + /// Called just before the authentication request is sent to service provider. + /// </summary> + /// <param name="request">The request.</param> + protected override void OnBeforeSendingAuthenticationRequest(IAuthenticationRequest request) + { + // Attribute Exchange extensions + var fetchRequest = new FetchRequest(); + fetchRequest.Attributes.Add(new AttributeRequest(WellKnownAttributes.Contact.Email, isRequired: true)); + fetchRequest.Attributes.Add(new AttributeRequest(AxKnownAttributes.FullName, isRequired: false)); + + request.AddExtension(fetchRequest); + } + + /// <summary> + /// Gets the extra data obtained from the response message when authentication is successful. + /// </summary> + /// <param name="response">The response message.</param> + /// <returns></returns> + protected override Dictionary<string, string> GetExtraData(IAuthenticationResponse response) + { + FetchResponse fetchResponse = response.GetExtension<FetchResponse>(); + if (fetchResponse != null) + { + var extraData = new Dictionary<string, string>(); + extraData.AddItemIfNotEmpty("email", fetchResponse.GetAttributeValue(WellKnownAttributes.Contact.Email)); + extraData.AddItemIfNotEmpty("fullName", fetchResponse.GetAttributeValue(AxKnownAttributes.FullName)); + + return extraData; + } + + return null; + } + } +} diff --git a/src/DotNetOpenAuth.Web/Clients/UriHelper.cs b/src/DotNetOpenAuth.Web/Clients/UriHelper.cs new file mode 100644 index 0000000..f8f9723 --- /dev/null +++ b/src/DotNetOpenAuth.Web/Clients/UriHelper.cs @@ -0,0 +1,148 @@ +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using System.Web; +using DotNetOpenAuth.Messaging; + +namespace DotNetOpenAuth.Web.Clients +{ + internal static class UriHelper + { + /// <summary> + /// Attaches the query string '__provider' to an existing url. If the url already + /// contains the __provider query string, it overrides it with the specified provider name. + /// </summary> + /// <param name="url">The original url.</param> + /// <param name="providerName">Name of the provider.</param> + /// <returns>The new url with the provider name query string attached</returns> + /// <example> + /// If the url is: http://contoso.com, and providerName='facebook', the returned value is: http://contoso.com?__provider=facebook + /// If the url is: http://contoso.com?a=1, and providerName='twitter', the returned value is: http://contoso.com?a=1&__provider=twitter + /// If the url is: http://contoso.com?a=1&__provider=twitter, and providerName='linkedin', the returned value is: http://contoso.com?a=1&__provider=linkedin + /// </example> + /// <remarks> + /// The reason we have to do this is so that when the external service provider forwards user + /// back to our site, we know which provider it comes back from. + /// </remarks> + public static Uri AttachQueryStringParameter(this Uri url, string parameterName, string parameterValue) + { + UriBuilder builder = new UriBuilder(url); + string query = builder.Query; + if (query.Length > 1) + { + // remove the '?' character in front of the query string + query = query.Substring(1); + } + + string parameterPrefix = parameterName + "="; + + string encodedParameterValue = Uri.EscapeDataString(parameterValue); + + string newQuery = Regex.Replace(query, parameterPrefix + "[^\\&]*", parameterPrefix + encodedParameterValue); + if (newQuery == query) + { + if (newQuery.Length > 0) + { + newQuery += "&"; + } + newQuery = newQuery + parameterPrefix + encodedParameterValue; + } + builder.Query = newQuery; + + return builder.Uri; + } + + /// <summary> + /// Appends the specified key/value pairs as query string parameters to the builder. + /// </summary> + /// <param name="builder">The builder.</param> + /// <param name="pairs">The pairs.</param> + /// <returns></returns> + public static void AppendQueryArguments(this UriBuilder builder, IDictionary<string, string> pairs) + { + if (pairs == null) + { + throw new ArgumentNullException("pairs"); + } + + if (!pairs.Any()) + { + return; + } + + string query = builder.Query; + if (query.Length > 1) + { + // remove the '?' character in front of the query string and append the '&' + query = query.Substring(1); + } + + var sb = new StringBuilder(query); + foreach (KeyValuePair<string, string> pair in pairs) + { + if (sb.Length > 0) + { + sb.Append('&'); + } + sb.AppendFormat("{0}={1}", pair.Key, pair.Value); + } + + builder.Query = sb.ToString(); + } + + /// <summary> + /// Converts an app-relative url, e.g. ~/Content/Return.cshtml, to a full-blown url, e.g. http://mysite.com/Content/Return.cshtml + /// </summary> + /// <param name="returnUrl">The return URL.</param> + /// <returns></returns> + public static Uri ConvertToAbsoluteUri(string returnUrl) + { + if (Uri.IsWellFormedUriString(returnUrl, UriKind.Absolute)) + { + return new Uri(returnUrl, UriKind.Absolute); + } + + if (HttpContext.Current == null) + { + return null; + } + + if (!VirtualPathUtility.IsAbsolute(returnUrl)) + { + returnUrl = VirtualPathUtility.ToAbsolute(returnUrl); + } + + return new Uri(GetPublicFacingUrl(new HttpRequestWrapper(HttpContext.Current.Request)), returnUrl); + } + + /// <summary> + /// Gets the public facing URL of this request as what clients see it. + /// </summary> + /// <param name="request">The request.</param> + public static Uri GetPublicFacingUrl(HttpRequestBase request) + { + NameValueCollection serverVariables = request.ServerVariables; + if (serverVariables["HTTP_HOST"] != null) + { + string forwardProto = serverVariables["HTTP_X_FORWARDED_PROTO"]; + if (forwardProto == null) + { + string scheme = request.Url.Scheme; + var hostAndPort = new Uri(scheme + Uri.SchemeDelimiter + serverVariables["HTTP_HOST"]); + var publicRequestUri = new UriBuilder(request.Url) + { + Scheme = scheme, + Host = hostAndPort.Host, + Port = hostAndPort.Port + }; + + return publicRequestUri.Uri; + } + } + return new Uri(request.Url, request.RawUrl); + } + } +}
\ No newline at end of file |