diff options
Diffstat (limited to 'src/DotNetOpenAuth.AspNet')
27 files changed, 2692 insertions, 0 deletions
diff --git a/src/DotNetOpenAuth.AspNet/AuthenticationResult.cs b/src/DotNetOpenAuth.AspNet/AuthenticationResult.cs new file mode 100644 index 0000000..d5fb2d1 --- /dev/null +++ b/src/DotNetOpenAuth.AspNet/AuthenticationResult.cs @@ -0,0 +1,114 @@ +//----------------------------------------------------------------------- +// <copyright file="AuthenticationResult.cs" company="Microsoft"> +// Copyright (c) Microsoft. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.AspNet { + using System; + using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; + using DotNetOpenAuth.Messaging; + + /// <summary> + /// Represents the result of OAuth or OpenID authentication. + /// </summary> + public class AuthenticationResult { + /// <summary> + /// Returns an instance which indicates failed authentication. + /// </summary> + [SuppressMessage("Microsoft.Security", "CA2104:DoNotDeclareReadOnlyMutableReferenceTypes", + Justification = "This type is immutable.")] + public static readonly AuthenticationResult Failed = new AuthenticationResult(isSuccessful: false); + + /// <summary> + /// Initializes a new instance of the <see cref="AuthenticationResult"/> class. + /// </summary> + /// <param name="isSuccessful"> + /// if set to <c>true</c> [is successful]. + /// </param> + public AuthenticationResult(bool isSuccessful) + : this(isSuccessful, provider: null, providerUserId: null, userName: null, extraData: null) { } + + /// <summary> + /// Initializes a new instance of the <see cref="AuthenticationResult"/> class. + /// </summary> + /// <param name="exception"> + /// The exception. + /// </param> + public AuthenticationResult(Exception exception) + : this(isSuccessful: false) { + if (exception == null) { + throw new ArgumentNullException("exception"); + } + + this.Error = exception; + } + + /// <summary> + /// Initializes a new instance of the <see cref="AuthenticationResult"/> class. + /// </summary> + /// <param name="isSuccessful"> + /// if set to <c>true</c> [is successful]. + /// </param> + /// <param name="provider"> + /// The provider. + /// </param> + /// <param name="providerUserId"> + /// The provider user id. + /// </param> + /// <param name="userName"> + /// Name of the user. + /// </param> + /// <param name="extraData"> + /// The extra data. + /// </param> + public AuthenticationResult( + bool isSuccessful, string provider, string providerUserId, string userName, IDictionary<string, string> extraData) { + this.IsSuccessful = isSuccessful; + this.Provider = provider; + this.ProviderUserId = providerUserId; + this.UserName = userName; + if (extraData != null) { + // wrap extraData in a read-only dictionary + this.ExtraData = new ReadOnlyDictionary<string, string>(extraData); + } + } + + /// <summary> + /// Gets the error that may have occured during the authentication process + /// </summary> + public Exception Error { get; private set; } + + /// <summary> + /// Gets the optional extra data that may be returned from the provider + /// </summary> + public IDictionary<string, string> ExtraData { get; private set; } + + /// <summary> + /// Gets a value indicating whether the authentication step is successful. + /// </summary> + /// <value> <c>true</c> if authentication is successful; otherwise, <c>false</c> . </value> + public bool IsSuccessful { get; private set; } + + /// <summary> + /// Gets the provider's name. + /// </summary> + public string Provider { get; private set; } + + /// <summary> + /// Gets the user id that is returned from the provider. It is unique only within the Provider's namespace. + /// </summary> + public string ProviderUserId { get; private set; } + + /// <summary> + /// Gets an (insecure, non-unique) alias for the user that the user should recognize as himself/herself. + /// </summary> + /// <value>This may take the form of an email address, a URL, or any other value that the user may recognize.</value> + /// <remarks> + /// This alias may come from the Provider or may be derived by the relying party if the Provider does not supply one. + /// It is not guaranteed to be unique and certainly does not merit any trust in any suggested authenticity. + /// </remarks> + public string UserName { get; private set; } + } +} diff --git a/src/DotNetOpenAuth.AspNet/Clients/DictionaryExtensions.cs b/src/DotNetOpenAuth.AspNet/Clients/DictionaryExtensions.cs new file mode 100644 index 0000000..f441c07 --- /dev/null +++ b/src/DotNetOpenAuth.AspNet/Clients/DictionaryExtensions.cs @@ -0,0 +1,58 @@ +//----------------------------------------------------------------------- +// <copyright file="DictionaryExtensions.cs" company="Microsoft"> +// Copyright (c) Microsoft. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.AspNet.Clients { + using System; + using System.Collections.Generic; + using System.Xml.Linq; + + /// <summary> + /// The dictionary extensions. + /// </summary> + internal static class DictionaryExtensions { + /// <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); + } + } + + /// <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; + } + } + } +} diff --git a/src/DotNetOpenAuth.AspNet/Clients/OAuth/DotNetOpenAuthWebConsumer.cs b/src/DotNetOpenAuth.AspNet/Clients/OAuth/DotNetOpenAuthWebConsumer.cs new file mode 100644 index 0000000..7eda8e4 --- /dev/null +++ b/src/DotNetOpenAuth.AspNet/Clients/OAuth/DotNetOpenAuthWebConsumer.cs @@ -0,0 +1,111 @@ +//----------------------------------------------------------------------- +// <copyright file="DotNetOpenAuthWebConsumer.cs" company="Microsoft"> +// Copyright (c) Microsoft. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.AspNet.Clients { + using System; + using System.Collections.Generic; + using System.Net; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.OAuth; + using DotNetOpenAuth.OAuth.ChannelElements; + using DotNetOpenAuth.OAuth.Messages; + + /// <summary> + /// The dot net open auth web consumer. + /// </summary> + public class DotNetOpenAuthWebConsumer : IOAuthWebWorker, IDisposable { + #region Constants and Fields + + /// <summary> + /// The _web consumer. + /// </summary> + private readonly WebConsumer webConsumer; + + #endregion + + #region Constructors and Destructors + + /// <summary> + /// Initializes a new instance of the <see cref="DotNetOpenAuthWebConsumer"/> class. + /// </summary> + /// <param name="serviceDescription"> + /// The service description. + /// </param> + /// <param name="tokenManager"> + /// The token manager. + /// </param> + public DotNetOpenAuthWebConsumer(ServiceProviderDescription serviceDescription, IConsumerTokenManager tokenManager) { + Requires.NotNull(serviceDescription, "serviceDescription"); + Requires.NotNull(tokenManager, "tokenManager"); + + this.webConsumer = new WebConsumer(serviceDescription, tokenManager); + } + + #endregion + + #region Public Methods and Operators + + /// <summary> + /// The prepare authorized request. + /// </summary> + /// <param name="profileEndpoint"> + /// The profile endpoint. + /// </param> + /// <param name="accessToken"> + /// The access token. + /// </param> + /// <returns>An HTTP request.</returns> + public HttpWebRequest PrepareAuthorizedRequest(MessageReceivingEndpoint profileEndpoint, string accessToken) { + return this.webConsumer.PrepareAuthorizedRequest(profileEndpoint, accessToken); + } + + /// <summary> + /// The process user authorization. + /// </summary> + /// <returns>The response message.</returns> + public AuthorizedTokenResponse ProcessUserAuthorization() { + return this.webConsumer.ProcessUserAuthorization(); + } + + /// <summary> + /// The request authentication. + /// </summary> + /// <param name="callback"> + /// The callback. + /// </param> + public void RequestAuthentication(Uri callback) { + var redirectParameters = new Dictionary<string, string> { { "force_login", "false" } }; + UserAuthorizationRequest request = this.webConsumer.PrepareRequestUserAuthorization( + callback, null, redirectParameters); + this.webConsumer.Channel.PrepareResponse(request).Send(); + } + + #endregion + + #region IDisposable members + + /// <summary> + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// </summary> + /// <filterpriority>2</filterpriority> + public void Dispose() { + this.Dispose(true); + GC.SuppressFinalize(this); + } + + #endregion + + /// <summary> + /// Releases unmanaged and - optionally - managed resources + /// </summary> + /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param> + protected virtual void Dispose(bool disposing) { + if (disposing) { + this.webConsumer.Dispose(); + } + } + } +} diff --git a/src/DotNetOpenAuth.AspNet/Clients/OAuth/IOAuthWebWorker.cs b/src/DotNetOpenAuth.AspNet/Clients/OAuth/IOAuthWebWorker.cs new file mode 100644 index 0000000..a054a1c --- /dev/null +++ b/src/DotNetOpenAuth.AspNet/Clients/OAuth/IOAuthWebWorker.cs @@ -0,0 +1,47 @@ +//----------------------------------------------------------------------- +// <copyright file="IOAuthWebWorker.cs" company="Microsoft"> +// Copyright (c) Microsoft. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.AspNet.Clients { + using System; + using System.Net; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.OAuth.Messages; + + /// <summary> + /// The io auth web worker. + /// </summary> + public interface IOAuthWebWorker { + #region Public Methods and Operators + + /// <summary> + /// The prepare authorized request. + /// </summary> + /// <param name="profileEndpoint"> + /// The profile endpoint. + /// </param> + /// <param name="accessToken"> + /// The access token. + /// </param> + /// <returns>An HTTP request.</returns> + HttpWebRequest PrepareAuthorizedRequest(MessageReceivingEndpoint profileEndpoint, string accessToken); + + /// <summary> + /// The process user authorization. + /// </summary> + /// <returns>The response message.</returns> + AuthorizedTokenResponse ProcessUserAuthorization(); + + /// <summary> + /// The request authentication. + /// </summary> + /// <param name="callback"> + /// The callback. + /// </param> + void RequestAuthentication(Uri callback); + + #endregion + } +} diff --git a/src/DotNetOpenAuth.AspNet/Clients/OAuth/InMemoryOAuthTokenManager.cs b/src/DotNetOpenAuth.AspNet/Clients/OAuth/InMemoryOAuthTokenManager.cs new file mode 100644 index 0000000..b45a65b --- /dev/null +++ b/src/DotNetOpenAuth.AspNet/Clients/OAuth/InMemoryOAuthTokenManager.cs @@ -0,0 +1,157 @@ +//----------------------------------------------------------------------- +// <copyright file="InMemoryOAuthTokenManager.cs" company="Microsoft"> +// Copyright (c) Microsoft. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.AspNet.Clients { + using System; + using System.Collections.Generic; + using DotNetOpenAuth.OAuth; + using DotNetOpenAuth.OAuth.ChannelElements; + using DotNetOpenAuth.OAuth.Messages; + + /// <summary> + /// An implementation of IOAuthTokenManager which stores keys in memory. + /// </summary> + public sealed class InMemoryOAuthTokenManager : IConsumerTokenManager { + #region Constants and Fields + + /// <summary> + /// The _tokens and secrets. + /// </summary> + private readonly Dictionary<string, string> tokensAndSecrets = new Dictionary<string, string>(); + + #endregion + + #region Constructors and Destructors + + /// <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) { + Requires.NotNull(consumerKey, "consumerKey"); + Requires.NotNull(consumerSecret, "consumerSecret"); + + this.ConsumerKey = consumerKey; + this.ConsumerSecret = consumerSecret; + } + + #endregion + + #region Public Properties + + /// <summary> + /// Gets the consumer key. + /// </summary> + public string ConsumerKey { get; private set; } + + /// <summary> + /// Gets the consumer secret. + /// </summary> + public string ConsumerSecret { get; private set; } + + #endregion + + #region Public Methods and Operators + + /// <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) { + this.tokensAndSecrets.Remove(requestToken); + this.tokensAndSecrets[accessToken] = accessTokenSecret; + } + + /// <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 this.tokensAndSecrets[token]; + } + + /// <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(); + } + + /// <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) { + this.tokensAndSecrets[response.Token] = response.TokenSecret; + } + + #endregion + } +} diff --git a/src/DotNetOpenAuth.AspNet/Clients/OAuth/LinkedInClient.cs b/src/DotNetOpenAuth.AspNet/Clients/OAuth/LinkedInClient.cs new file mode 100644 index 0000000..631636b --- /dev/null +++ b/src/DotNetOpenAuth.AspNet/Clients/OAuth/LinkedInClient.cs @@ -0,0 +1,114 @@ +//----------------------------------------------------------------------- +// <copyright file="LinkedInClient.cs" company="Microsoft"> +// Copyright (c) Microsoft. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.AspNet.Clients { + using System; + using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; + using System.IO; + using System.Net; + using System.Xml.Linq; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.OAuth; + using DotNetOpenAuth.OAuth.ChannelElements; + using DotNetOpenAuth.OAuth.Messages; + + /// <summary> + /// Represents LinkedIn authentication client. + /// </summary> + public sealed class LinkedInClient : OAuthClient { + #region Constants and Fields + + /// <summary> + /// Describes the OAuth service provider endpoints for LinkedIn. + /// </summary> + 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() }, + }; + + #endregion + + #region Constructors and Destructors + + /// <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> + [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) { } + + #endregion + + #region Methods + + /// <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> + [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 = this.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("accesstoken", accessToken); + extraData.Add("name", userName); + extraData.AddDataIfNotEmpty(document, "headline"); + extraData.AddDataIfNotEmpty(document, "summary"); + extraData.AddDataIfNotEmpty(document, "industry"); + + return new AuthenticationResult( + isSuccessful: true, provider: this.ProviderName, providerUserId: userId, userName: userName, extraData: extraData); + } + } + } catch (Exception exception) { + return new AuthenticationResult(exception); + } + } + + #endregion + } +} diff --git a/src/DotNetOpenAuth.AspNet/Clients/OAuth/OAuthClient.cs b/src/DotNetOpenAuth.AspNet/Clients/OAuth/OAuthClient.cs new file mode 100644 index 0000000..89cefad --- /dev/null +++ b/src/DotNetOpenAuth.AspNet/Clients/OAuth/OAuthClient.cs @@ -0,0 +1,167 @@ +//----------------------------------------------------------------------- +// <copyright file="OAuthClient.cs" company="Microsoft"> +// Copyright (c) Microsoft. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.AspNet.Clients { + using System; + using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; + using System.Web; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.OAuth; + using DotNetOpenAuth.OAuth.ChannelElements; + using DotNetOpenAuth.OAuth.Messages; + + /// <summary> + /// Represents base class for OAuth 1.0 clients + /// </summary> + public abstract class OAuthClient : IAuthenticationClient { + #region Constructors and Destructors + + /// <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> + /// <param name="serviceDescription"> + /// The service Description. + /// </param> + /// <param name="tokenManager"> + /// The token Manager. + /// </param> + [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "I don't know how to ensure this rule is followed given this API")] + protected OAuthClient( + string providerName, ServiceProviderDescription serviceDescription, IConsumerTokenManager tokenManager) + : this(providerName, new DotNetOpenAuthWebConsumer(serviceDescription, tokenManager)) { } + + /// <summary> + /// Initializes a new instance of the <see cref="OAuthClient"/> class. + /// </summary> + /// <param name="providerName"> + /// The provider name. + /// </param> + /// <param name="webWorker"> + /// The web worker. + /// </param> + protected OAuthClient(string providerName, IOAuthWebWorker webWorker) { + Requires.NotNull(providerName, "providerName"); + Requires.NotNull(webWorker, "webWorker"); + + this.ProviderName = providerName; + this.WebWorker = webWorker; + } + + #endregion + + #region Public Properties + + /// <summary> + /// Gets the name of the provider which provides authentication service. + /// </summary> + public string ProviderName { get; private set; } + + #endregion + + #region Properties + + /// <summary> + /// Gets the OAuthWebConsumer instance which handles constructing requests to the OAuth providers. + /// </summary> + protected IOAuthWebWorker WebWorker { get; private set; } + + #endregion + + #region Public Methods and Operators + + /// <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. + /// </param> + /// <param name="returnUrl"> + /// The return url after users have completed authenticating against external website. + /// </param> + public virtual void RequestAuthentication(HttpContextBase context, Uri returnUrl) { + Requires.NotNull(returnUrl, "returnUrl"); + Requires.NotNull(context, "context"); + + Uri callback = returnUrl.StripQueryArgumentsWithPrefix("oauth_"); + this.WebWorker.RequestAuthentication(callback); + } + + /// <summary> + /// Check if authentication succeeded after user is redirected back from the service provider. + /// </summary> + /// <param name="context"> + /// The context. + /// </param> + /// <returns> + /// An instance of <see cref="AuthenticationResult"/> containing authentication result. + /// </returns> + public virtual AuthenticationResult VerifyAuthentication(HttpContextBase context) { + AuthorizedTokenResponse response = this.WebWorker.ProcessUserAuthorization(); + if (response == null) { + return AuthenticationResult.Failed; + } + + AuthenticationResult result = this.VerifyAuthenticationCore(response); + if (result.IsSuccessful && result.ExtraData != null) { + // add the access token to the user data dictionary just in case page developers want to use it + var wrapExtraData = result.ExtraData.IsReadOnly + ? new Dictionary<string, string>(result.ExtraData) + : result.ExtraData; + wrapExtraData["accesstoken"] = response.AccessToken; + + AuthenticationResult wrapResult = new AuthenticationResult( + result.IsSuccessful, + result.Provider, + result.ProviderUserId, + result.UserName, + wrapExtraData); + + result = wrapResult; + } + + return result; + } + + #endregion + + #region Methods + + /// <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); + #endregion + } +} diff --git a/src/DotNetOpenAuth.AspNet/Clients/OAuth/TwitterClient.cs b/src/DotNetOpenAuth.AspNet/Clients/OAuth/TwitterClient.cs new file mode 100644 index 0000000..ceaffd4 --- /dev/null +++ b/src/DotNetOpenAuth.AspNet/Clients/OAuth/TwitterClient.cs @@ -0,0 +1,111 @@ +//----------------------------------------------------------------------- +// <copyright file="TwitterClient.cs" company="Microsoft"> +// Copyright (c) Microsoft. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.AspNet.Clients { + using System; + using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; + using System.IO; + using System.Net; + using System.Xml.Linq; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.OAuth; + using DotNetOpenAuth.OAuth.ChannelElements; + using DotNetOpenAuth.OAuth.Messages; + + /// <summary> + /// Represents a Twitter client + /// </summary> + public class TwitterClient : OAuthClient { + #region Constants and Fields + + /// <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() }, + }; + + #endregion + + #region Constructors and Destructors + + /// <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> + [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) { } + + #endregion + + #region Methods + + /// <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> + [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"]; + + var profileRequestUrl = new Uri("http://api.twitter.com/1/users/show.xml?user_id=" + + MessagingUtilities.EscapeUriDataStringRfc3986(userId)); + var profileEndpoint = new MessageReceivingEndpoint(profileRequestUrl, HttpDeliveryMethods.GetRequest); + HttpWebRequest request = this.WebWorker.PrepareAuthorizedRequest(profileEndpoint, accessToken); + + var extraData = new Dictionary<string, string>(); + extraData.Add("accesstoken", accessToken); + 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: this.ProviderName, providerUserId: userId, userName: userName, extraData: extraData); + } + + #endregion + } +} diff --git a/src/DotNetOpenAuth.AspNet/Clients/OAuth2/FacebookClient.cs b/src/DotNetOpenAuth.AspNet/Clients/OAuth2/FacebookClient.cs new file mode 100644 index 0000000..f4ad20b --- /dev/null +++ b/src/DotNetOpenAuth.AspNet/Clients/OAuth2/FacebookClient.cs @@ -0,0 +1,148 @@ +//----------------------------------------------------------------------- +// <copyright file="FacebookClient.cs" company="Microsoft"> +// Copyright (c) Microsoft. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.AspNet.Clients { + using System; + using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; + using System.Net; + using System.Web; + using DotNetOpenAuth.Messaging; + + /// <summary> + /// The facebook client. + /// </summary> + [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Facebook", Justification = "Brand name")] + public sealed class FacebookClient : OAuth2Client { + #region Constants and Fields + + /// <summary> + /// The authorization endpoint. + /// </summary> + private const string AuthorizationEndpoint = "https://www.facebook.com/dialog/oauth"; + + /// <summary> + /// The token endpoint. + /// </summary> + private const string TokenEndpoint = "https://graph.facebook.com/oauth/access_token"; + + /// <summary> + /// The _app id. + /// </summary> + private readonly string appId; + + /// <summary> + /// The _app secret. + /// </summary> + private readonly string appSecret; + + #endregion + + #region Constructors and Destructors + + /// <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> + public FacebookClient(string appId, string appSecret) + : base("facebook") { + Requires.NotNullOrEmpty(appId, "appId"); + Requires.NotNullOrEmpty(appSecret, "appSecret"); + + this.appId = appId; + this.appSecret = appSecret; + } + + #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) { + // Note: Facebook doesn't like us to url-encode the redirect_uri value + var builder = new UriBuilder(AuthorizationEndpoint); + builder.AppendQueryArgs( + new Dictionary<string, string> { { "client_id", this.appId }, { "redirect_uri", returnUrl.AbsoluteUri }, }); + 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 IDictionary<string, string> GetUserData(string accessToken) { + FacebookGraphData graphData; + var request = + WebRequest.Create( + "https://graph.facebook.com/me?access_token=" + MessagingUtilities.EscapeUriDataStringRfc3986(accessToken)); + using (var response = request.GetResponse()) { + using (var responseStream = response.GetResponseStream()) { + graphData = JsonHelper.Deserialize<FacebookGraphData>(responseStream); + } + } + + // this dictionary must contains + var userData = new Dictionary<string, string>(); + userData.AddItemIfNotEmpty("id", graphData.Id); + userData.AddItemIfNotEmpty("username", graphData.Email); + userData.AddItemIfNotEmpty("name", graphData.Name); + userData.AddItemIfNotEmpty("link", graphData.Link == null ? null : graphData.Link.AbsoluteUri); + userData.AddItemIfNotEmpty("gender", graphData.Gender); + userData.AddItemIfNotEmpty("birthday", graphData.Birthday); + 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) { + // Note: Facebook doesn't like us to url-encode the redirect_uri value + var builder = new UriBuilder(TokenEndpoint); + builder.AppendQueryArgs( + new Dictionary<string, string> { + { "client_id", this.appId }, + { "redirect_uri", returnUrl.AbsoluteUri }, + { "client_secret", this.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); + return parsedQueryString["access_token"]; + } + } + + #endregion + } +} diff --git a/src/DotNetOpenAuth.AspNet/Clients/OAuth2/FacebookGraphData.cs b/src/DotNetOpenAuth.AspNet/Clients/OAuth2/FacebookGraphData.cs new file mode 100644 index 0000000..9ad3eff --- /dev/null +++ b/src/DotNetOpenAuth.AspNet/Clients/OAuth2/FacebookGraphData.cs @@ -0,0 +1,68 @@ +//----------------------------------------------------------------------- +// <copyright file="FacebookGraphData.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 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] + [EditorBrowsable(EditorBrowsableState.Never)] + [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Facebook", Justification = "Brand name")] + public class FacebookGraphData { + #region Public Properties + + /// <summary> + /// Gets or sets the birthday. + /// </summary> + /// <value> The birthday. </value> + [DataMember(Name = "birthday")] + public string Birthday { get; set; } + + /// <summary> + /// Gets or sets the email. + /// </summary> + /// <value> The email. </value> + [DataMember(Name = "email")] + public string Email { get; set; } + + /// <summary> + /// Gets or sets the gender. + /// </summary> + /// <value> The gender. </value> + [DataMember(Name = "gender")] + public string Gender { get; set; } + + /// <summary> + /// Gets or sets the id. + /// </summary> + /// <value> The id. </value> + [DataMember(Name = "id")] + public string Id { get; set; } + + /// <summary> + /// Gets or sets the link. + /// </summary> + /// <value> The link. </value> + [DataMember(Name = "link")] + public Uri Link { get; set; } + + /// <summary> + /// Gets or sets the name. + /// </summary> + /// <value> The name. </value> + [DataMember(Name = "name")] + public string Name { get; set; } + #endregion + } +} diff --git a/src/DotNetOpenAuth.AspNet/Clients/OAuth2/JsonHelper.cs b/src/DotNetOpenAuth.AspNet/Clients/OAuth2/JsonHelper.cs new file mode 100644 index 0000000..a7ff79e --- /dev/null +++ b/src/DotNetOpenAuth.AspNet/Clients/OAuth2/JsonHelper.cs @@ -0,0 +1,37 @@ +//----------------------------------------------------------------------- +// <copyright file="JsonHelper.cs" company="Microsoft"> +// Copyright (c) Microsoft. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.AspNet.Clients { + using System; + using System.IO; + using System.Runtime.Serialization.Json; + + /// <summary> + /// The json helper. + /// </summary> + internal static class JsonHelper { + #region Public Methods and Operators + + /// <summary> + /// The deserialize. + /// </summary> + /// <param name="stream"> + /// The stream. + /// </param> + /// <typeparam name="T">The type of the value to deserialize.</typeparam> + /// <returns> + /// The deserialized value. + /// </returns> + public static T Deserialize<T>(Stream stream) where T : class { + Requires.NotNull(stream, "stream"); + + var serializer = new DataContractJsonSerializer(typeof(T)); + return (T)serializer.ReadObject(stream); + } + + #endregion + } +} diff --git a/src/DotNetOpenAuth.AspNet/Clients/OAuth2/OAuth2AccessTokenData.cs b/src/DotNetOpenAuth.AspNet/Clients/OAuth2/OAuth2AccessTokenData.cs new file mode 100644 index 0000000..5da24dd --- /dev/null +++ b/src/DotNetOpenAuth.AspNet/Clients/OAuth2/OAuth2AccessTokenData.cs @@ -0,0 +1,46 @@ +//----------------------------------------------------------------------- +// <copyright file="OAuth2AccessTokenData.cs" company="Microsoft"> +// Copyright (c) Microsoft. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.AspNet.Clients { + using System.Runtime.Serialization; + + /// <summary> + /// Captures the result of an access token request, including an optional refresh token. + /// </summary> + [DataContract] + public class OAuth2AccessTokenData { + #region Public Properties + + /// <summary> + /// Gets or sets the access token. + /// </summary> + /// <value> The access token. </value> + [DataMember(Name = "access_token")] + public string AccessToken { get; set; } + + /// <summary> + /// Gets or sets the refresh token. + /// </summary> + /// <value> The refresh token. </value> + [DataMember(Name = "refresh_token")] + public string RefreshToken { get; set; } + + /// <summary> + /// Gets or sets the scope. + /// </summary> + /// <value> The scope. </value> + [DataMember(Name = "scope")] + public string Scope { get; set; } + + /// <summary> + /// Gets or sets the type of the token. + /// </summary> + /// <value> The type of the token. </value> + [DataMember(Name = "token_type")] + public string TokenType { get; set; } + #endregion + } +} diff --git a/src/DotNetOpenAuth.AspNet/Clients/OAuth2/OAuth2Client.cs b/src/DotNetOpenAuth.AspNet/Clients/OAuth2/OAuth2Client.cs new file mode 100644 index 0000000..016d92e --- /dev/null +++ b/src/DotNetOpenAuth.AspNet/Clients/OAuth2/OAuth2Client.cs @@ -0,0 +1,166 @@ +//----------------------------------------------------------------------- +// <copyright file="OAuth2Client.cs" company="Microsoft"> +// Copyright (c) Microsoft. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.AspNet.Clients { + using System; + using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; + using System.Web; + + /// <summary> + /// Represents the base class for OAuth 2.0 clients + /// </summary> + public abstract class OAuth2Client : IAuthenticationClient { + #region Constants and Fields + + /// <summary> + /// The provider name. + /// </summary> + private readonly string providerName; + + /// <summary> + /// The return url. + /// </summary> + private Uri returnUrl; + + #endregion + + #region Constructors and Destructors + + /// <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) { + Requires.NotNull(providerName, "providerName"); + this.providerName = providerName; + } + + #endregion + + #region Public Properties + + /// <summary> + /// Gets the name of the provider which provides authentication service. + /// </summary> + public string ProviderName { + get { + return this.providerName; + } + } + + #endregion + + #region Public Methods and Operators + + /// <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. + /// </param> + /// <param name="returnUrl"> + /// The return url after users have completed authenticating against external website. + /// </param> + public virtual void RequestAuthentication(HttpContextBase context, Uri returnUrl) { + Requires.NotNull(context, "context"); + Requires.NotNull(returnUrl, "returnUrl"); + + this.returnUrl = returnUrl; + + string redirectUrl = this.GetServiceLoginUrl(returnUrl).AbsoluteUri; + context.Response.Redirect(redirectUrl, endResponse: true); + } + + /// <summary> + /// Check if authentication succeeded after user is redirected back from the service provider. + /// </summary> + /// <param name="context"> + /// The context. + /// </param> + /// <returns> + /// An instance of <see cref="AuthenticationResult"/> containing authentication result. + /// </returns> + public virtual AuthenticationResult VerifyAuthentication(HttpContextBase context) { + Requires.NotNull(context, "context"); + + string code = context.Request.QueryString["code"]; + if (string.IsNullOrEmpty(code)) { + return AuthenticationResult.Failed; + } + + string accessToken = this.QueryAccessToken(this.returnUrl, code); + if (accessToken == null) { + return AuthenticationResult.Failed; + } + + IDictionary<string, string> userData = this.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; + } + + // add the access token to the user data dictionary just in case page developers want to use it + userData["accesstoken"] = accessToken; + + return new AuthenticationResult( + isSuccessful: true, provider: this.ProviderName, providerUserId: id, userName: name, extraData: userData); + } + + #endregion + + #region Methods + + /// <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> + /// An absolute URL. + /// </returns> + [SuppressMessage("Microsoft.Naming", "CA1726:UsePreferredTerms", MessageId = "Login", + Justification = "Login is used more consistently in ASP.Net")] + protected abstract Uri GetServiceLoginUrl(Uri returnUrl); + + /// <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); + + /// <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> + /// The access token + /// </returns> + protected abstract string QueryAccessToken(Uri returnUrl, string authorizationCode); + #endregion + } +} diff --git a/src/DotNetOpenAuth.AspNet/Clients/OAuth2/WindowsLiveClient.cs b/src/DotNetOpenAuth.AspNet/Clients/OAuth2/WindowsLiveClient.cs new file mode 100644 index 0000000..5e396a1 --- /dev/null +++ b/src/DotNetOpenAuth.AspNet/Clients/OAuth2/WindowsLiveClient.cs @@ -0,0 +1,166 @@ +//----------------------------------------------------------------------- +// <copyright file="WindowsLiveClient.cs" company="Microsoft"> +// Copyright (c) Microsoft. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.AspNet.Clients { + using System; + using System.Collections.Generic; + using System.IO; + using System.Net; + using DotNetOpenAuth.Messaging; + + /// <summary> + /// The windows live client. + /// </summary> + public sealed class WindowsLiveClient : OAuth2Client { + #region Constants and Fields + + /// <summary> + /// The authorization endpoint. + /// </summary> + private const string AuthorizationEndpoint = "https://oauth.live.com/authorize"; + + /// <summary> + /// The token endpoint. + /// </summary> + private const string TokenEndpoint = "https://oauth.live.com/token"; + + /// <summary> + /// The _app id. + /// </summary> + private readonly string appId; + + /// <summary> + /// The _app secret. + /// </summary> + private readonly string appSecret; + + #endregion + + #region Constructors and Destructors + + /// <summary> + /// Initializes a new instance of the <see cref="WindowsLiveClient"/> class. + /// </summary> + /// <param name="appId"> + /// The app id. + /// </param> + /// <param name="appSecret"> + /// The app secret. + /// </param> + public WindowsLiveClient(string appId, string appSecret) + : base("windowslive") { + Requires.NotNullOrEmpty(appId, "appId"); + Requires.NotNullOrEmpty(appSecret, "appSecret"); + + this.appId = appId; + this.appSecret = appSecret; + } + + #endregion + + #region Methods + + /// <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> + /// An absolute URL. + /// </returns> + protected override Uri GetServiceLoginUrl(Uri returnUrl) { + var builder = new UriBuilder(AuthorizationEndpoint); + builder.AppendQueryArgs( + new Dictionary<string, string> { + { "client_id", this.appId }, + { "scope", "wl.basic" }, + { "response_type", "code" }, + { "redirect_uri", returnUrl.AbsoluteUri }, + }); + + return builder.Uri; + } + + /// <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 override IDictionary<string, string> GetUserData(string accessToken) { + WindowsLiveUserData graph; + var request = + WebRequest.Create( + "https://apis.live.net/v5.0/me?access_token=" + MessagingUtilities.EscapeUriDataStringRfc3986(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.AbsoluteUri); + userData.AddItemIfNotEmpty("gender", graph.Gender); + userData.AddItemIfNotEmpty("firstname", graph.FirstName); + userData.AddItemIfNotEmpty("lastname", graph.LastName); + return userData; + } + + /// <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> + /// The query access token. + /// </returns> + protected override string QueryAccessToken(Uri returnUrl, string authorizationCode) { + 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" }, + }); + + 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) { + return tokenData.AccessToken; + } + } + } + + return null; + } + + #endregion + } +} diff --git a/src/DotNetOpenAuth.AspNet/Clients/OAuth2/WindowsLiveUserData.cs b/src/DotNetOpenAuth.AspNet/Clients/OAuth2/WindowsLiveUserData.cs new file mode 100644 index 0000000..52192c3 --- /dev/null +++ b/src/DotNetOpenAuth.AspNet/Clients/OAuth2/WindowsLiveUserData.cs @@ -0,0 +1,66 @@ +//----------------------------------------------------------------------- +// <copyright file="WindowsLiveUserData.cs" company="Microsoft"> +// Copyright (c) Microsoft. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.AspNet.Clients { + using System; + using System.ComponentModel; + using System.Runtime.Serialization; + + /// <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] + [EditorBrowsable(EditorBrowsableState.Never)] + public class WindowsLiveUserData { + #region Public Properties + + /// <summary> + /// Gets or sets the first name. + /// </summary> + /// <value> The first name. </value> + [DataMember(Name = "first_name")] + public string FirstName { get; set; } + + /// <summary> + /// Gets or sets the gender. + /// </summary> + /// <value> The gender. </value> + [DataMember(Name = "gender")] + public string Gender { get; set; } + + /// <summary> + /// Gets or sets the id. + /// </summary> + /// <value> The id. </value> + [DataMember(Name = "id")] + public string Id { get; set; } + + /// <summary> + /// Gets or sets the last name. + /// </summary> + /// <value> The last name. </value> + [DataMember(Name = "last_name")] + public string LastName { get; set; } + + /// <summary> + /// Gets or sets the link. + /// </summary> + /// <value> The link. </value> + [DataMember(Name = "link")] + public Uri Link { get; set; } + + /// <summary> + /// Gets or sets the name. + /// </summary> + /// <value> The name. </value> + [DataMember(Name = "name")] + public string Name { get; set; } + #endregion + } +} diff --git a/src/DotNetOpenAuth.AspNet/Clients/OpenID/GoogleOpenIdClient.cs b/src/DotNetOpenAuth.AspNet/Clients/OpenID/GoogleOpenIdClient.cs new file mode 100644 index 0000000..aedcb80 --- /dev/null +++ b/src/DotNetOpenAuth.AspNet/Clients/OpenID/GoogleOpenIdClient.cs @@ -0,0 +1,70 @@ +//----------------------------------------------------------------------- +// <copyright file="GoogleOpenIdClient.cs" company="Microsoft"> +// Copyright (c) Microsoft. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.AspNet.Clients { + using System.Collections.Generic; + using DotNetOpenAuth.OpenId.Extensions.AttributeExchange; + using DotNetOpenAuth.OpenId.RelyingParty; + + /// <summary> + /// Represents Google OpenID client. + /// </summary> + public sealed class GoogleOpenIdClient : OpenIdClient { + #region Constructors and Destructors + + /// <summary> + /// Initializes a new instance of the <see cref="GoogleOpenIdClient"/> class. + /// </summary> + public GoogleOpenIdClient() + : base("google", WellKnownProviders.Google) { } + + #endregion + + #region Methods + + /// <summary> + /// Gets the extra data obtained from the response message when authentication is successful. + /// </summary> + /// <param name="response"> + /// The response message. + /// </param> + /// <returns>A dictionary of profile data; or null if no data is available.</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(WellKnownAttributes.Name.First)); + extraData.AddItemIfNotEmpty("lastName", fetchResponse.GetAttributeValue(WellKnownAttributes.Name.Last)); + + return extraData; + } + + return null; + } + + /// <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.AddRequired(WellKnownAttributes.Contact.Email); + fetchRequest.Attributes.AddOptional(WellKnownAttributes.Contact.HomeAddress.Country); + fetchRequest.Attributes.AddOptional(WellKnownAttributes.Name.First); + fetchRequest.Attributes.AddOptional(WellKnownAttributes.Name.Last); + + request.AddExtension(fetchRequest); + } + + #endregion + } +} diff --git a/src/DotNetOpenAuth.AspNet/Clients/OpenID/OpenIDClient.cs b/src/DotNetOpenAuth.AspNet/Clients/OpenID/OpenIDClient.cs new file mode 100644 index 0000000..6ced1a6 --- /dev/null +++ b/src/DotNetOpenAuth.AspNet/Clients/OpenID/OpenIDClient.cs @@ -0,0 +1,156 @@ +//----------------------------------------------------------------------- +// <copyright file="OpenIdClient.cs" company="Microsoft"> +// Copyright (c) Microsoft. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.AspNet.Clients { + using System; + using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; + using System.Web; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.OpenId; + using DotNetOpenAuth.OpenId.RelyingParty; + + /// <summary> + /// Base classes for OpenID clients. + /// </summary> + public class OpenIdClient : IAuthenticationClient { + #region Constants and Fields + + /// <summary> + /// The _openid relaying party. + /// </summary> + private static readonly OpenIdRelyingParty RelyingParty = + new OpenIdRelyingParty(new StandardRelyingPartyApplicationStore()); + + /// <summary> + /// The _provider identifier. + /// </summary> + private readonly Identifier providerIdentifier; + + /// <summary> + /// The _provider name. + /// </summary> + private readonly string providerName; + + #endregion + + #region Constructors and Destructors + + /// <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, Identifier providerIdentifier) { + Requires.NotNullOrEmpty(providerName, "providerName"); + Requires.NotNull(providerIdentifier, "providerIdentifier"); + + this.providerName = providerName; + this.providerIdentifier = providerIdentifier; + } + + #endregion + + #region Public Properties + + /// <summary> + /// Gets the name of the provider which provides authentication service. + /// </summary> + public string ProviderName { + get { + return this.providerName; + } + } + + #endregion + + #region Public Methods and Operators + + /// <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> + [SuppressMessage("Microsoft.Usage", "CA2234:PassSystemUriObjectsInsteadOfStrings", + Justification = "We don't have a Uri object handy.")] + public virtual void RequestAuthentication(HttpContextBase context, Uri returnUrl) { + Requires.NotNull(returnUrl, "returnUrl"); + + var realm = new Realm(returnUrl.GetComponents(UriComponents.SchemeAndServer, UriFormat.Unescaped)); + IAuthenticationRequest request = RelyingParty.CreateRequest(this.providerIdentifier, realm, returnUrl); + + // give subclasses a chance to modify request message, e.g. add extension attributes, etc. + this.OnBeforeSendingAuthenticationRequest(request); + + request.RedirectToProvider(); + } + + /// <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> + public virtual AuthenticationResult VerifyAuthentication(HttpContextBase context) { + IAuthenticationResponse response = RelyingParty.GetResponse(); + if (response == null) { + throw new InvalidOperationException(WebResources.OpenIDFailedToGetResponse); + } + + if (response.Status == AuthenticationStatus.Authenticated) { + string id = response.ClaimedIdentifier; + string username; + + Dictionary<string, string> extraData = this.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, this.ProviderName, id, username, extraData); + } + + return AuthenticationResult.Failed; + } + + #endregion + + #region Methods + + /// <summary> + /// Gets the extra data obtained from the response message when authentication is successful. + /// </summary> + /// <param name="response"> + /// The response message. + /// </param> + /// <returns>Always null.</returns> + protected virtual Dictionary<string, string> GetExtraData(IAuthenticationResponse response) { + return null; + } + + /// <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) { } + + #endregion + } +} diff --git a/src/DotNetOpenAuth.AspNet/Clients/OpenID/YahooOpenIdClient.cs b/src/DotNetOpenAuth.AspNet/Clients/OpenID/YahooOpenIdClient.cs new file mode 100644 index 0000000..bd420fc --- /dev/null +++ b/src/DotNetOpenAuth.AspNet/Clients/OpenID/YahooOpenIdClient.cs @@ -0,0 +1,65 @@ +//----------------------------------------------------------------------- +// <copyright file="YahooOpenIdClient.cs" company="Microsoft"> +// Copyright (c) Microsoft. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.AspNet.Clients { + using System.Collections.Generic; + using DotNetOpenAuth.OpenId.Extensions.AttributeExchange; + using DotNetOpenAuth.OpenId.RelyingParty; + + /// <summary> + /// The yahoo open id client. + /// </summary> + public sealed class YahooOpenIdClient : OpenIdClient { + #region Constructors and Destructors + + /// <summary> + /// Initializes a new instance of the <see cref="YahooOpenIdClient"/> class. + /// </summary> + public YahooOpenIdClient() + : base("yahoo", WellKnownProviders.Yahoo) { } + + #endregion + + #region Methods + + /// <summary> + /// Gets the extra data obtained from the response message when authentication is successful. + /// </summary> + /// <param name="response"> + /// The response message. + /// </param> + /// <returns>A dictionary of profile data; or null if no data is available.</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(WellKnownAttributes.Name.FullName)); + + return extraData; + } + + return null; + } + + /// <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.AddRequired(WellKnownAttributes.Contact.Email); + fetchRequest.Attributes.AddOptional(WellKnownAttributes.Name.FullName); + + request.AddExtension(fetchRequest); + } + + #endregion + } +} diff --git a/src/DotNetOpenAuth.AspNet/DotNetOpenAuth.AspNet.csproj b/src/DotNetOpenAuth.AspNet/DotNetOpenAuth.AspNet.csproj new file mode 100644 index 0000000..f28f96f --- /dev/null +++ b/src/DotNetOpenAuth.AspNet/DotNetOpenAuth.AspNet.csproj @@ -0,0 +1,104 @@ +<?xml version="1.0" encoding="utf-8"?> +<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> + <Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildProjectDirectory), EnlistmentInfo.props))\EnlistmentInfo.props" Condition=" '$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildProjectDirectory), EnlistmentInfo.props))' != '' " /> + <PropertyGroup> + <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration> + <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform> + <SchemaVersion>2.0</SchemaVersion> + <ProjectGuid>{51835086-9611-4C53-819B-F2D5C9320873}</ProjectGuid> + <OutputType>Library</OutputType> + <AppDesignerFolder>Properties</AppDesignerFolder> + <RootNamespace>DotNetOpenAuth.AspNet</RootNamespace> + <AssemblyName>DotNetOpenAuth.AspNet</AssemblyName> + <TargetFrameworkVersion>v4.0</TargetFrameworkVersion> + <CodeAnalysisRuleSet>ExtendedDesignGuidelineRules.ruleset</CodeAnalysisRuleSet> + </PropertyGroup> + <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' "> + <DebugSymbols>true</DebugSymbols> + <DebugType>full</DebugType> + <Optimize>false</Optimize> + <OutputPath>..\..\bin\v4.0\Debug\</OutputPath> + <DefineConstants>DEBUG;TRACE</DefineConstants> + </PropertyGroup> + <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' "> + <DebugType>pdbonly</DebugType> + <Optimize>true</Optimize> + <OutputPath>..\..\bin\v4.0\Release\</OutputPath> + <DefineConstants>TRACE</DefineConstants> + </PropertyGroup> + <Import Project="$(ProjectRoot)tools\DotNetOpenAuth.props" /> + <Import Project="$(ProjectRoot)tools\DotNetOpenAuth.Product.props" /> + <ItemGroup> + <Reference Include="System" /> + <Reference Include="System.Core" /> + <Reference Include="System.Runtime.Serialization" /> + <Reference Include="System.Web" /> + <Reference Include="System.Xml.Linq" /> + <Reference Include="System.Data.DataSetExtensions" /> + <Reference Include="Microsoft.CSharp" /> + <Reference Include="System.Data" /> + <Reference Include="System.Xml" /> + </ItemGroup> + <ItemGroup> + <Compile Include="AuthenticationResult.cs" /> + <Compile Include="Clients\DictionaryExtensions.cs" /> + <Compile Include="IAuthenticationClient.cs" /> + <Compile Include="Clients\OAuth2\FacebookClient.cs" /> + <Compile Include="Clients\OAuth2\FacebookGraphData.cs" /> + <Compile Include="Clients\OAuth2\JsonHelper.cs" /> + <Compile Include="Clients\OAuth2\OAuth2AccessTokenData.cs" /> + <Compile Include="Clients\OAuth2\OAuth2Client.cs" /> + <Compile Include="Clients\OAuth2\WindowsLiveClient.cs" /> + <Compile Include="Clients\OAuth2\WindowsLiveUserData.cs" /> + <Compile Include="Clients\OAuth\DotNetOpenAuthWebConsumer.cs" /> + <Compile Include="Clients\OAuth\InMemoryOAuthTokenManager.cs" /> + <Compile Include="Clients\OAuth\IOAuthWebWorker.cs" /> + <Compile Include="Clients\OAuth\LinkedInClient.cs" /> + <Compile Include="Clients\OAuth\OAuthClient.cs" /> + <Compile Include="Clients\OAuth\TwitterClient.cs" /> + <Compile Include="Clients\OpenID\GoogleOpenIdClient.cs" /> + <Compile Include="Clients\OpenID\OpenIdClient.cs" /> + <Compile Include="Clients\OpenID\YahooOpenIdClient.cs" /> + <Compile Include="UriHelper.cs" /> + <Compile Include="IOpenAuthDataProvider.cs" /> + <Compile Include="OpenAuthAuthenticationTicketHelper.cs" /> + <Compile Include="OpenAuthSecurityManager.cs" /> + <Compile Include="Properties\AssemblyInfo.cs" /> + <Compile Include="WebResources.Designer.cs"> + <AutoGen>True</AutoGen> + <DesignTime>True</DesignTime> + <DependentUpon>WebResources.resx</DependentUpon> + </Compile> + </ItemGroup> + <ItemGroup> + <EmbeddedResource Include="WebResources.resx"> + <Generator>ResXFileCodeGenerator</Generator> + <LastGenOutput>WebResources.Designer.cs</LastGenOutput> + </EmbeddedResource> + </ItemGroup> + <ItemGroup> + <ProjectReference Include="..\DotNetOpenAuth.Core\DotNetOpenAuth.Core.csproj"> + <Project>{60426312-6AE5-4835-8667-37EDEA670222}</Project> + <Name>DotNetOpenAuth.Core</Name> + </ProjectReference> + <ProjectReference Include="..\DotNetOpenAuth.OAuth.Consumer\DotNetOpenAuth.OAuth.Consumer.csproj"> + <Project>{B202E40D-4663-4A2B-ACDA-865F88FF7CAA}</Project> + <Name>DotNetOpenAuth.OAuth.Consumer</Name> + </ProjectReference> + <ProjectReference Include="..\DotNetOpenAuth.OAuth\DotNetOpenAuth.OAuth.csproj"> + <Project>{A288FCC8-6FCF-46DA-A45E-5F9281556361}</Project> + <Name>DotNetOpenAuth.OAuth</Name> + </ProjectReference> + <ProjectReference Include="..\DotNetOpenAuth.OpenId.RelyingParty\DotNetOpenAuth.OpenId.RelyingParty.csproj"> + <Project>{F458AB60-BA1C-43D9-8CEF-EC01B50BE87B}</Project> + <Name>DotNetOpenAuth.OpenId.RelyingParty</Name> + </ProjectReference> + <ProjectReference Include="..\DotNetOpenAuth.OpenId\DotNetOpenAuth.OpenId.csproj"> + <Project>{3896A32A-E876-4C23-B9B8-78E17D134CD3}</Project> + <Name>DotNetOpenAuth.OpenId</Name> + </ProjectReference> + </ItemGroup> + <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> + <Import Project="$(ProjectRoot)tools\DotNetOpenAuth.targets" /> + <Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildProjectDirectory), EnlistmentInfo.targets))\EnlistmentInfo.targets" Condition=" '$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildProjectDirectory), EnlistmentInfo.targets))' != '' " /> +</Project>
\ No newline at end of file diff --git a/src/DotNetOpenAuth.AspNet/IAuthenticationClient.cs b/src/DotNetOpenAuth.AspNet/IAuthenticationClient.cs new file mode 100644 index 0000000..4d9acde --- /dev/null +++ b/src/DotNetOpenAuth.AspNet/IAuthenticationClient.cs @@ -0,0 +1,42 @@ +//----------------------------------------------------------------------- +// <copyright file="IAuthenticationClient.cs" company="Microsoft"> +// Copyright (c) Microsoft. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.AspNet { + using System; + using System.Web; + + /// <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.AspNet/IOpenAuthDataProvider.cs b/src/DotNetOpenAuth.AspNet/IOpenAuthDataProvider.cs new file mode 100644 index 0000000..12d929d --- /dev/null +++ b/src/DotNetOpenAuth.AspNet/IOpenAuthDataProvider.cs @@ -0,0 +1,30 @@ +//----------------------------------------------------------------------- +// <copyright file="IOpenAuthDataProvider.cs" company="Microsoft"> +// Copyright (c) Microsoft. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.AspNet { + /// <summary> + /// Common methods available on identity issuers. + /// </summary> + public interface IOpenAuthDataProvider { + #region Public Methods and Operators + + /// <summary> + /// Get a user name from an identity provider and their own assigned user ID. + /// </summary> + /// <param name="openAuthProvider"> + /// The identity provider. + /// </param> + /// <param name="openAuthId"> + /// The issuer's ID for the user. + /// </param> + /// <returns> + /// The username of the user. + /// </returns> + string GetUserNameFromOpenAuth(string openAuthProvider, string openAuthId); + + #endregion + } +} diff --git a/src/DotNetOpenAuth.AspNet/OpenAuthAuthenticationTicketHelper.cs b/src/DotNetOpenAuth.AspNet/OpenAuthAuthenticationTicketHelper.cs new file mode 100644 index 0000000..3fc3a21 --- /dev/null +++ b/src/DotNetOpenAuth.AspNet/OpenAuthAuthenticationTicketHelper.cs @@ -0,0 +1,126 @@ +//----------------------------------------------------------------------- +// <copyright file="OpenAuthAuthenticationTicketHelper.cs" company="Microsoft"> +// Copyright (c) Microsoft. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.AspNet { + using System; + using System.Diagnostics; + using System.Web; + using System.Web.Security; + + /// <summary> + /// Helper methods for setting and retrieving a custom forms authentication ticket for delegation protocols. + /// </summary> + internal static class OpenAuthAuthenticationTicketHelper { + #region Constants and Fields + + /// <summary> + /// The open auth cookie token. + /// </summary> + private const string OpenAuthCookieToken = "OAuth"; + + #endregion + + #region Public Methods and Operators + + /// <summary> + /// Checks whether the specified HTTP request comes from an authenticated user. + /// </summary> + /// <param name="context"> + /// The context. + /// </param> + /// <returns>True if the reuest is authenticated; false otherwise.</returns> + public static bool IsValidAuthenticationTicket(HttpContextBase context) { + HttpCookie cookie = context.Request.Cookies[FormsAuthentication.FormsCookieName]; + if (cookie == null) { + return false; + } + + string encryptedCookieData = cookie.Value; + if (string.IsNullOrEmpty(encryptedCookieData)) { + return false; + } + + try { + FormsAuthenticationTicket authTicket = FormsAuthentication.Decrypt(encryptedCookieData); + return authTicket != null && !authTicket.Expired && authTicket.UserData == OpenAuthCookieToken; + } catch (ArgumentException) { + return false; + } + } + + /// <summary> + /// Adds an authentication cookie to the user agent in the next HTTP response. + /// </summary> + /// <param name="context"> + /// The context. + /// </param> + /// <param name="userName"> + /// The user name. + /// </param> + /// <param name="createPersistentCookie"> + /// A value indicating whether the cookie should persist across sessions. + /// </param> + public static void SetAuthenticationTicket(HttpContextBase context, string userName, bool createPersistentCookie) { + if (!context.Request.IsSecureConnection && FormsAuthentication.RequireSSL) { + throw new HttpException(WebResources.ConnectionNotSecure); + } + + HttpCookie cookie = GetAuthCookie(userName, createPersistentCookie); + context.Response.Cookies.Add(cookie); + } + + #endregion + + #region Methods + + /// <summary> + /// Creates an HTTP authentication cookie. + /// </summary> + /// <param name="userName"> + /// The user name. + /// </param> + /// <param name="createPersistentCookie"> + /// A value indicating whether the cookie should last across sessions. + /// </param> + /// <returns>An authentication cookie.</returns> + private static HttpCookie GetAuthCookie(string userName, bool createPersistentCookie) { + Requires.NotNullOrEmpty(userName, "userName"); + + var ticket = new FormsAuthenticationTicket( + /* version */ + 2, + userName, + DateTime.Now, + DateTime.Now.Add(FormsAuthentication.Timeout), + createPersistentCookie, + OpenAuthCookieToken, + FormsAuthentication.FormsCookiePath); + + string encryptedTicket = FormsAuthentication.Encrypt(ticket); + if (encryptedTicket == null || encryptedTicket.Length < 1) { + throw new HttpException(WebResources.FailedToEncryptTicket); + } + + var cookie = new HttpCookie(FormsAuthentication.FormsCookieName, encryptedTicket) { + HttpOnly = true, + Path = FormsAuthentication.FormsCookiePath, + Secure = FormsAuthentication.RequireSSL + }; + + if (FormsAuthentication.CookieDomain != null) { + cookie.Domain = FormsAuthentication.CookieDomain; + } + + if (ticket.IsPersistent) { + cookie.Expires = ticket.Expiration; + } + + return cookie; + } + + #endregion + } +} diff --git a/src/DotNetOpenAuth.AspNet/OpenAuthSecurityManager.cs b/src/DotNetOpenAuth.AspNet/OpenAuthSecurityManager.cs new file mode 100644 index 0000000..463f056 --- /dev/null +++ b/src/DotNetOpenAuth.AspNet/OpenAuthSecurityManager.cs @@ -0,0 +1,173 @@ +//----------------------------------------------------------------------- +// <copyright file="OpenAuthSecurityManager.cs" company="Microsoft"> +// Copyright (c) Microsoft. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.AspNet { + using System; + using System.Diagnostics.CodeAnalysis; + using System.Web; + using DotNetOpenAuth.Messaging; + + /// <summary> + /// Manage authenticating with an external OAuth or OpenID provider + /// </summary> + public class OpenAuthSecurityManager { + #region Constants and Fields + + /// <summary> + /// The provider query string name. + /// </summary> + private const string ProviderQueryStringName = "__provider__"; + + /// <summary> + /// The _authentication provider. + /// </summary> + private readonly IAuthenticationClient authenticationProvider; + + /// <summary> + /// The _data provider. + /// </summary> + private readonly IOpenAuthDataProvider dataProvider; + + /// <summary> + /// The _request context. + /// </summary> + private readonly HttpContextBase requestContext; + + #endregion + + #region Constructors and Destructors + + /// <summary> + /// Initializes a new instance of the <see cref="OpenAuthSecurityManager"/> class. + /// </summary> + /// <param name="requestContext"> + /// The request context. + /// </param> + public OpenAuthSecurityManager(HttpContextBase requestContext) + : this(requestContext, provider: null, dataProvider: null) { } + + /// <summary> + /// Initializes a new instance of the <see cref="OpenAuthSecurityManager"/> class. + /// </summary> + /// <param name="requestContext"> + /// The request context. + /// </param> + /// <param name="provider"> + /// The provider. + /// </param> + /// <param name="dataProvider"> + /// The data provider. + /// </param> + public OpenAuthSecurityManager( + HttpContextBase requestContext, IAuthenticationClient provider, IOpenAuthDataProvider dataProvider) { + if (requestContext == null) { + throw new ArgumentNullException("requestContext"); + } + + this.requestContext = requestContext; + this.dataProvider = dataProvider; + this.authenticationProvider = provider; + } + + #endregion + + #region Public Properties + + /// <summary> + /// Gets a value indicating whether IsAuthenticatedWithOpenAuth. + /// </summary> + public bool IsAuthenticatedWithOpenAuth { + get { + return this.requestContext.Request.IsAuthenticated + && OpenAuthAuthenticationTicketHelper.IsValidAuthenticationTicket(this.requestContext); + } + } + + #endregion + + #region Public Methods and Operators + + /// <summary> + /// Gets the provider that is responding to an authentication request. + /// </summary> + /// <param name="context"> + /// The HTTP request context. + /// </param> + /// <returns> + /// The provider name, if one is available. + /// </returns> + public static string GetProviderName(HttpContextBase context) { + return context.Request.QueryString[ProviderQueryStringName]; + } + + /// <summary> + /// Checks if the specified provider user id represents a valid account. If it does, log user in. + /// </summary> + /// <param name="providerUserId"> + /// The provider user id. + /// </param> + /// <param name="createPersistentCookie"> + /// if set to <c>true</c> create persistent cookie. + /// </param> + /// <returns> + /// <c>true</c> if the login is successful. + /// </returns> + [SuppressMessage("Microsoft.Naming", "CA1726:UsePreferredTerms", MessageId = "Login", + Justification = "Login is used more consistently in ASP.Net")] + public bool Login(string providerUserId, bool createPersistentCookie) { + string userName = this.dataProvider.GetUserNameFromOpenAuth( + this.authenticationProvider.ProviderName, providerUserId); + if (string.IsNullOrEmpty(userName)) { + return false; + } + + OpenAuthAuthenticationTicketHelper.SetAuthenticationTicket(this.requestContext, userName, createPersistentCookie); + return true; + } + + /// <summary> + /// Requests the specified provider to start the authentication by directing users to an external website + /// </summary> + /// <param name="returnUrl"> + /// The return url after user is authenticated. + /// </param> + public void RequestAuthentication(string returnUrl) { + // convert returnUrl to an absolute path + Uri uri; + if (!string.IsNullOrEmpty(returnUrl)) { + uri = UriHelper.ConvertToAbsoluteUri(returnUrl, this.requestContext); + } else { + uri = this.requestContext.Request.GetPublicFacingUrl(); + } + + // attach the provider parameter so that we know which provider initiated + // the login when user is redirected back to this page + uri = uri.AttachQueryStringParameter(ProviderQueryStringName, this.authenticationProvider.ProviderName); + this.authenticationProvider.RequestAuthentication(this.requestContext, uri); + } + + /// <summary> + /// Checks if user is successfully authenticated when user is redirected back to this user. + /// </summary> + /// <returns>The result of the authentication.</returns> + public AuthenticationResult VerifyAuthentication() { + AuthenticationResult result = this.authenticationProvider.VerifyAuthentication(this.requestContext); + if (!result.IsSuccessful) { + // if the result is a Failed result, creates a new Failed response which has providerName info. + result = new AuthenticationResult( + isSuccessful: false, + provider: this.authenticationProvider.ProviderName, + providerUserId: null, + userName: null, + extraData: null); + } + + return result; + } + + #endregion + } +} diff --git a/src/DotNetOpenAuth.AspNet/Properties/AssemblyInfo.cs b/src/DotNetOpenAuth.AspNet/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..15b7d9e --- /dev/null +++ b/src/DotNetOpenAuth.AspNet/Properties/AssemblyInfo.cs @@ -0,0 +1,40 @@ +//----------------------------------------------------------------------- +// <copyright file="AssemblyInfo.cs" company="Microsoft"> +// Copyright (c) Microsoft. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +using System; +using System.Reflection; +using System.Resources; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("DotNetOpenAuth.AspNet")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("Microsoft")] +[assembly: AssemblyProduct("DotNetOpenAuth.AspNet")] +[assembly: AssemblyCopyright("Copyright © Microsoft 2011")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] +[assembly: NeutralResourcesLanguage("en-US")] +[assembly: CLSCompliant(true)] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("c89b7e57-2735-4407-bcb9-dfe9bb9493a2")] + +#if StrongNameSigned + +[assembly: InternalsVisibleTo("DotNetOpenAuth.AspNet.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100AD093C3765257C89A7010E853F2C7C741FF92FA8ACE06D7B8254702CAD5CF99104447F63AB05F8BB6F51CE0D81C8C93D2FCE8C20AAFF7042E721CBA16EAAE98778611DED11C0ABC8900DC5667F99B50A9DADEC24DBD8F2C91E3E8AD300EF64F1B4B9536CEB16FB440AF939F57624A9B486F867807C649AE4830EAB88C6C03998")] +#else +[assembly: InternalsVisibleTo("DotNetOpenAuth.AspNet.Test")] +#endif diff --git a/src/DotNetOpenAuth.AspNet/UriHelper.cs b/src/DotNetOpenAuth.AspNet/UriHelper.cs new file mode 100644 index 0000000..602f00c --- /dev/null +++ b/src/DotNetOpenAuth.AspNet/UriHelper.cs @@ -0,0 +1,79 @@ +//----------------------------------------------------------------------- +// <copyright file="UriHelper.cs" company="Microsoft"> +// Copyright (c) Microsoft. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.AspNet { + using System; + using System.Text.RegularExpressions; + using System.Web; + using DotNetOpenAuth.Messaging; + + /// <summary> + /// The uri helper. + /// </summary> + internal static class UriHelper { + /// <summary> + /// The attach query string parameter. + /// </summary> + /// <param name="url"> + /// The url. + /// </param> + /// <param name="parameterName"> + /// The parameter name. + /// </param> + /// <param name="parameterValue"> + /// The parameter value. + /// </param> + /// <returns>An absolute URI.</returns> + 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> + /// 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> + /// <param name="context"> + /// The context. + /// </param> + /// <returns>An absolute URI.</returns> + public static Uri ConvertToAbsoluteUri(string returnUrl, HttpContextBase context) { + if (Uri.IsWellFormedUriString(returnUrl, UriKind.Absolute)) { + return new Uri(returnUrl, UriKind.Absolute); + } + + if (!VirtualPathUtility.IsAbsolute(returnUrl)) { + returnUrl = VirtualPathUtility.ToAbsolute(returnUrl); + } + + Uri publicUrl = context.Request.GetPublicFacingUrl(); + return new Uri(publicUrl, returnUrl); + } + } +} diff --git a/src/DotNetOpenAuth.AspNet/WebResources.Designer.cs b/src/DotNetOpenAuth.AspNet/WebResources.Designer.cs new file mode 100644 index 0000000..23a51be --- /dev/null +++ b/src/DotNetOpenAuth.AspNet/WebResources.Designer.cs @@ -0,0 +1,99 @@ +//------------------------------------------------------------------------------ +// <auto-generated> +// This code was generated by a tool. +// Runtime Version:4.0.30319.261 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// </auto-generated> +//------------------------------------------------------------------------------ + +namespace DotNetOpenAuth.AspNet { + using System; + + + /// <summary> + /// A strongly-typed resource class, for looking up localized strings, etc. + /// </summary> + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class WebResources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal WebResources() { + } + + /// <summary> + /// Returns the cached ResourceManager instance used by this class. + /// </summary> + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("DotNetOpenAuth.AspNet.WebResources", typeof(WebResources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// <summary> + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// </summary> + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// <summary> + /// Looks up a localized string similar to A setting in web.config requires a secure connection for this request but the current connection is not secured.. + /// </summary> + internal static string ConnectionNotSecure { + get { + return ResourceManager.GetString("ConnectionNotSecure", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Unable to encrypt the authentication ticket.. + /// </summary> + internal static string FailedToEncryptTicket { + get { + return ResourceManager.GetString("FailedToEncryptTicket", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to An OAuth data provider has already been registered for this application.. + /// </summary> + internal static string OAuthDataProviderRegistered { + get { + return ResourceManager.GetString("OAuthDataProviderRegistered", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Failed to obtain the authentication response from service provider.. + /// </summary> + internal static string OpenIDFailedToGetResponse { + get { + return ResourceManager.GetString("OpenIDFailedToGetResponse", resourceCulture); + } + } + } +} diff --git a/src/DotNetOpenAuth.AspNet/WebResources.resx b/src/DotNetOpenAuth.AspNet/WebResources.resx new file mode 100644 index 0000000..321c097 --- /dev/null +++ b/src/DotNetOpenAuth.AspNet/WebResources.resx @@ -0,0 +1,132 @@ +<?xml version="1.0" encoding="utf-8"?> +<root> + <!-- + Microsoft ResX Schema + + Version 2.0 + + The primary goals of this format is to allow a simple XML format + that is mostly human readable. The generation and parsing of the + various data types are done through the TypeConverter classes + associated with the data types. + + Example: + + ... ado.net/XML headers & schema ... + <resheader name="resmimetype">text/microsoft-resx</resheader> + <resheader name="version">2.0</resheader> + <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader> + <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader> + <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data> + <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data> + <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64"> + <value>[base64 mime encoded serialized .NET Framework object]</value> + </data> + <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64"> + <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value> + <comment>This is a comment</comment> + </data> + + There are any number of "resheader" rows that contain simple + name/value pairs. + + Each data row contains a name, and value. The row also contains a + type or mimetype. Type corresponds to a .NET class that support + text/value conversion through the TypeConverter architecture. + Classes that don't support this are serialized and stored with the + mimetype set. + + The mimetype is used for serialized objects, and tells the + ResXResourceReader how to depersist the object. This is currently not + extensible. For a given mimetype the value must be set accordingly: + + Note - application/x-microsoft.net.object.binary.base64 is the format + that the ResXResourceWriter will generate, however the reader can + read any of the formats listed below. + + mimetype: application/x-microsoft.net.object.binary.base64 + value : The object must be serialized with + : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter + : and then encoded with base64 encoding. + + mimetype: application/x-microsoft.net.object.soap.base64 + value : The object must be serialized with + : System.Runtime.Serialization.Formatters.Soap.SoapFormatter + : and then encoded with base64 encoding. + + mimetype: application/x-microsoft.net.object.bytearray.base64 + value : The object must be serialized into a byte array + : using a System.ComponentModel.TypeConverter + : and then encoded with base64 encoding. + --> + <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata"> + <xsd:import namespace="http://www.w3.org/XML/1998/namespace" /> + <xsd:element name="root" msdata:IsDataSet="true"> + <xsd:complexType> + <xsd:choice maxOccurs="unbounded"> + <xsd:element name="metadata"> + <xsd:complexType> + <xsd:sequence> + <xsd:element name="value" type="xsd:string" minOccurs="0" /> + </xsd:sequence> + <xsd:attribute name="name" use="required" type="xsd:string" /> + <xsd:attribute name="type" type="xsd:string" /> + <xsd:attribute name="mimetype" type="xsd:string" /> + <xsd:attribute ref="xml:space" /> + </xsd:complexType> + </xsd:element> + <xsd:element name="assembly"> + <xsd:complexType> + <xsd:attribute name="alias" type="xsd:string" /> + <xsd:attribute name="name" type="xsd:string" /> + </xsd:complexType> + </xsd:element> + <xsd:element name="data"> + <xsd:complexType> + <xsd:sequence> + <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" /> + <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" /> + </xsd:sequence> + <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" /> + <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" /> + <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" /> + <xsd:attribute ref="xml:space" /> + </xsd:complexType> + </xsd:element> + <xsd:element name="resheader"> + <xsd:complexType> + <xsd:sequence> + <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" /> + </xsd:sequence> + <xsd:attribute name="name" type="xsd:string" use="required" /> + </xsd:complexType> + </xsd:element> + </xsd:choice> + </xsd:complexType> + </xsd:element> + </xsd:schema> + <resheader name="resmimetype"> + <value>text/microsoft-resx</value> + </resheader> + <resheader name="version"> + <value>2.0</value> + </resheader> + <resheader name="reader"> + <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> + </resheader> + <resheader name="writer"> + <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> + </resheader> + <data name="ConnectionNotSecure" xml:space="preserve"> + <value>A setting in web.config requires a secure connection for this request but the current connection is not secured.</value> + </data> + <data name="FailedToEncryptTicket" xml:space="preserve"> + <value>Unable to encrypt the authentication ticket.</value> + </data> + <data name="OAuthDataProviderRegistered" xml:space="preserve"> + <value>An OAuth data provider has already been registered for this application.</value> + </data> + <data name="OpenIDFailedToGetResponse" xml:space="preserve"> + <value>Failed to obtain the authentication response from service provider.</value> + </data> +</root>
\ No newline at end of file |