diff options
Diffstat (limited to 'samples/DotNetOpenAuth.ApplicationBlock')
5 files changed, 554 insertions, 10 deletions
diff --git a/samples/DotNetOpenAuth.ApplicationBlock/DotNetOpenAuth.ApplicationBlock.csproj b/samples/DotNetOpenAuth.ApplicationBlock/DotNetOpenAuth.ApplicationBlock.csproj index 976a325..6739bf9 100644 --- a/samples/DotNetOpenAuth.ApplicationBlock/DotNetOpenAuth.ApplicationBlock.csproj +++ b/samples/DotNetOpenAuth.ApplicationBlock/DotNetOpenAuth.ApplicationBlock.csproj @@ -1,5 +1,5 @@ <?xml version="1.0" encoding="utf-8"?> -<Project ToolsVersion="3.5" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> +<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> <PropertyGroup> <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration> <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform> @@ -12,15 +12,36 @@ <AssemblyName>DotNetOpenAuth.ApplicationBlock</AssemblyName> <TargetFrameworkVersion>v3.5</TargetFrameworkVersion> <FileAlignment>512</FileAlignment> + <FileUpgradeFlags> + </FileUpgradeFlags> + <OldToolsVersion>3.5</OldToolsVersion> + <UpgradeBackupLocation /> + <IsWebBootstrapper>false</IsWebBootstrapper> + <TargetFrameworkProfile /> + <PublishUrl>publish\</PublishUrl> + <Install>true</Install> + <InstallFrom>Disk</InstallFrom> + <UpdateEnabled>false</UpdateEnabled> + <UpdateMode>Foreground</UpdateMode> + <UpdateInterval>7</UpdateInterval> + <UpdateIntervalUnits>Days</UpdateIntervalUnits> + <UpdatePeriodically>false</UpdatePeriodically> + <UpdateRequired>false</UpdateRequired> + <MapFileExtensions>true</MapFileExtensions> + <ApplicationRevision>0</ApplicationRevision> + <ApplicationVersion>1.0.0.%2a</ApplicationVersion> + <UseApplicationTrust>false</UseApplicationTrust> + <BootstrapperEnabled>true</BootstrapperEnabled> </PropertyGroup> <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' "> <DebugSymbols>true</DebugSymbols> <DebugType>full</DebugType> <Optimize>false</Optimize> <OutputPath>bin\Debug\</OutputPath> - <DefineConstants>DEBUG;TRACE</DefineConstants> + <DefineConstants>TRACE;DEBUG</DefineConstants> <ErrorReport>prompt</ErrorReport> <WarningLevel>4</WarningLevel> + <CodeAnalysisRuleSet>AllRules.ruleset</CodeAnalysisRuleSet> </PropertyGroup> <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' "> <DebugType>pdbonly</DebugType> @@ -29,6 +50,7 @@ <DefineConstants>TRACE</DefineConstants> <ErrorReport>prompt</ErrorReport> <WarningLevel>4</WarningLevel> + <CodeAnalysisRuleSet>AllRules.ruleset</CodeAnalysisRuleSet> </PropertyGroup> <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'CodeAnalysis|AnyCPU' "> <DebugSymbols>true</DebugSymbols> @@ -39,12 +61,18 @@ <CodeAnalysisUseTypeNameInSuppression>true</CodeAnalysisUseTypeNameInSuppression> <CodeAnalysisModuleSuppressionsFile>GlobalSuppressions.cs</CodeAnalysisModuleSuppressionsFile> <ErrorReport>prompt</ErrorReport> + <CodeAnalysisRuleSet>AllRules.ruleset</CodeAnalysisRuleSet> + </PropertyGroup> + <PropertyGroup> + <DefineConstants>$(DefineConstants);SAMPLESONLY</DefineConstants> </PropertyGroup> <ItemGroup> <Reference Include="System" /> + <Reference Include="System.configuration" /> <Reference Include="System.Core"> <RequiredTargetFramework>3.5</RequiredTargetFramework> </Reference> + <Reference Include="System.Web" /> <Reference Include="System.Xml.Linq"> <RequiredTargetFramework>3.5</RequiredTargetFramework> </Reference> @@ -59,9 +87,13 @@ <Compile Include="CustomExtensions\AcmeRequest.cs" /> <Compile Include="CustomExtensions\AcmeResponse.cs" /> <Compile Include="GoogleConsumer.cs" /> + <Compile Include="InMemoryTokenManager.cs"> + <SubType>Code</SubType> + </Compile> <Compile Include="Properties\AssemblyInfo.cs" /> <Compile Include="TwitterConsumer.cs" /> <Compile Include="Util.cs" /> + <Compile Include="YubikeyRelyingParty.cs" /> </ItemGroup> <ItemGroup> <ProjectReference Include="..\..\src\DotNetOpenAuth\DotNetOpenAuth.csproj"> @@ -69,6 +101,23 @@ <Name>DotNetOpenAuth</Name> </ProjectReference> </ItemGroup> + <ItemGroup> + <BootstrapperPackage Include="Microsoft.Net.Client.3.5"> + <Visible>False</Visible> + <ProductName>.NET Framework 3.5 SP1 Client Profile</ProductName> + <Install>false</Install> + </BootstrapperPackage> + <BootstrapperPackage Include="Microsoft.Net.Framework.3.5.SP1"> + <Visible>False</Visible> + <ProductName>.NET Framework 3.5 SP1</ProductName> + <Install>true</Install> + </BootstrapperPackage> + <BootstrapperPackage Include="Microsoft.Windows.Installer.3.1"> + <Visible>False</Visible> + <ProductName>Windows Installer 3.1</ProductName> + <Install>true</Install> + </BootstrapperPackage> + </ItemGroup> <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> <!-- To modify your build process, add your task inside one of the targets below and uncomment it. Other similar extension points exist, see Microsoft.Common.targets. diff --git a/samples/DotNetOpenAuth.ApplicationBlock/GoogleConsumer.cs b/samples/DotNetOpenAuth.ApplicationBlock/GoogleConsumer.cs index 4d3ce13..2add642 100644 --- a/samples/DotNetOpenAuth.ApplicationBlock/GoogleConsumer.cs +++ b/samples/DotNetOpenAuth.ApplicationBlock/GoogleConsumer.cs @@ -8,6 +8,7 @@ namespace DotNetOpenAuth.ApplicationBlock { using System; using System.Collections.Generic; using System.Diagnostics; + using System.Globalization; using System.IO; using System.Linq; using System.Net; @@ -208,13 +209,20 @@ namespace DotNetOpenAuth.ApplicationBlock { /// </summary> /// <param name="consumer">The Google consumer previously constructed using <see cref="CreateWebConsumer"/> or <see cref="CreateDesktopConsumer"/>.</param> /// <param name="accessToken">The access token previously retrieved.</param> + /// <param name="maxResults">The maximum number of entries to return. If you want to receive all of the contacts, rather than only the default maximum, you can specify a very large number here.</param> + /// <param name="startIndex">The 1-based index of the first result to be retrieved (for paging).</param> /// <returns>An XML document returned by Google.</returns> - public static XDocument GetContacts(ConsumerBase consumer, string accessToken) { + public static XDocument GetContacts(ConsumerBase consumer, string accessToken, int maxResults = 25, int startIndex = 1) { if (consumer == null) { throw new ArgumentNullException("consumer"); } - var response = consumer.PrepareAuthorizedRequestAndSend(GetContactsEndpoint, accessToken); + var extraData = new Dictionary<string, string>() { + { "start-index", startIndex.ToString(CultureInfo.InvariantCulture) }, + { "max-results", maxResults.ToString(CultureInfo.InvariantCulture) }, + }; + var request = consumer.PrepareAuthorizedRequest(GetContactsEndpoint, accessToken, extraData); + var response = consumer.Channel.WebRequestHandler.GetResponse(request); string body = response.GetResponseReader().ReadToEnd(); XDocument result = XDocument.Parse(body); return result; diff --git a/samples/DotNetOpenAuth.ApplicationBlock/InMemoryTokenManager.cs b/samples/DotNetOpenAuth.ApplicationBlock/InMemoryTokenManager.cs new file mode 100644 index 0000000..b9cc2b8 --- /dev/null +++ b/samples/DotNetOpenAuth.ApplicationBlock/InMemoryTokenManager.cs @@ -0,0 +1,147 @@ +//----------------------------------------------------------------------- +// <copyright file="InMemoryTokenManager.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.ApplicationBlock { + using System; + using System.Collections.Generic; + using System.Diagnostics; + using DotNetOpenAuth.OAuth.ChannelElements; + using DotNetOpenAuth.OAuth.Messages; + using DotNetOpenAuth.OpenId.Extensions.OAuth; + +#if SAMPLESONLY + /// <summary> + /// A token manager that only retains tokens in memory. + /// Meant for SHORT TERM USE TOKENS ONLY. + /// </summary> + /// <remarks> + /// A likely application of this class is for "Sign In With Twitter", + /// where the user only signs in without providing any authorization to access + /// Twitter APIs except to authenticate, since that access token is only useful once. + /// </remarks> + internal class InMemoryTokenManager : IConsumerTokenManager, IOpenIdOAuthTokenManager { + private Dictionary<string, string> tokensAndSecrets = new Dictionary<string, string>(); + + /// <summary> + /// Initializes a new instance of the <see cref="InMemoryTokenManager"/> class. + /// </summary> + /// <param name="consumerKey">The consumer key.</param> + /// <param name="consumerSecret">The consumer secret.</param> + public InMemoryTokenManager(string consumerKey, string consumerSecret) { + if (String.IsNullOrEmpty(consumerKey)) { + throw new ArgumentNullException("consumerKey"); + } + + this.ConsumerKey = consumerKey; + this.ConsumerSecret = consumerSecret; + } + + /// <summary> + /// Gets the consumer key. + /// </summary> + /// <value>The consumer key.</value> + public string ConsumerKey { get; private set; } + + /// <summary> + /// Gets the consumer secret. + /// </summary> + /// <value>The consumer secret.</value> + public string ConsumerSecret { get; private set; } + + #region ITokenManager Members + + /// <summary> + /// Gets the Token Secret given a request or access token. + /// </summary> + /// <param name="token">The request or access token.</param> + /// <returns> + /// The secret associated with the given token. + /// </returns> + /// <exception cref="ArgumentException">Thrown if the secret cannot be found for the given token.</exception> + public string GetTokenSecret(string token) { + return this.tokensAndSecrets[token]; + } + + /// <summary> + /// Stores a newly generated unauthorized request token, secret, and optional + /// application-specific parameters for later recall. + /// </summary> + /// <param name="request">The request message that resulted in the generation of a new unauthorized request token.</param> + /// <param name="response">The response message that includes the unauthorized request token.</param> + /// <exception cref="ArgumentException">Thrown if the consumer key is not registered, or a required parameter was not found in the parameters collection.</exception> + /// <remarks> + /// Request tokens stored by this method SHOULD NOT associate any user account with this token. + /// It usually opens up security holes in your application to do so. Instead, you associate a user + /// account with access tokens (not request tokens) in the <see cref="ExpireRequestTokenAndStoreNewAccessToken"/> + /// method. + /// </remarks> + public void StoreNewRequestToken(UnauthorizedTokenRequest request, ITokenSecretContainingMessage response) { + this.tokensAndSecrets[response.Token] = response.TokenSecret; + } + + /// <summary> + /// Deletes a request token and its associated secret and stores a new access token and secret. + /// </summary> + /// <param name="consumerKey">The Consumer that is exchanging its request token for an access token.</param> + /// <param name="requestToken">The Consumer's request token that should be deleted/expired.</param> + /// <param name="accessToken">The new access token that is being issued to the Consumer.</param> + /// <param name="accessTokenSecret">The secret associated with the newly issued access token.</param> + /// <remarks> + /// <para> + /// Any scope of granted privileges associated with the request token from the + /// original call to <see cref="StoreNewRequestToken"/> should be carried over + /// to the new Access Token. + /// </para> + /// <para> + /// To associate a user account with the new access token, + /// <see cref="System.Web.HttpContext.User">HttpContext.Current.User</see> may be + /// useful in an ASP.NET web application within the implementation of this method. + /// Alternatively you may store the access token here without associating with a user account, + /// and wait until <see cref="WebConsumer.ProcessUserAuthorization()"/> or + /// <see cref="DesktopConsumer.ProcessUserAuthorization(string, string)"/> return the access + /// token to associate the access token with a user account at that point. + /// </para> + /// </remarks> + public void ExpireRequestTokenAndStoreNewAccessToken(string consumerKey, string requestToken, string accessToken, string accessTokenSecret) { + this.tokensAndSecrets.Remove(requestToken); + this.tokensAndSecrets[accessToken] = accessTokenSecret; + } + + /// <summary> + /// Classifies a token as a request token or an access token. + /// </summary> + /// <param name="token">The token to classify.</param> + /// <returns>Request or Access token, or invalid if the token is not recognized.</returns> + public TokenType GetTokenType(string token) { + throw new NotImplementedException(); + } + + #endregion + + #region IOpenIdOAuthTokenManager Members + + /// <summary> + /// Stores a new request token obtained over an OpenID request. + /// </summary> + /// <param name="consumerKey">The consumer key.</param> + /// <param name="authorization">The authorization message carrying the request token and authorized access scope.</param> + /// <remarks> + /// <para>The token secret is the empty string.</para> + /// <para>Tokens stored by this method should be short-lived to mitigate + /// possible security threats. Their lifetime should be sufficient for the + /// relying party to receive the positive authentication assertion and immediately + /// send a follow-up request for the access token.</para> + /// </remarks> + public void StoreOpenIdAuthorizedRequestToken(string consumerKey, AuthorizationApprovedResponse authorization) { + this.tokensAndSecrets[authorization.RequestToken] = String.Empty; + } + + #endregion + } +#else +#error The InMemoryTokenManager class is only for samples as it forgets all tokens whenever the application restarts! You should implement IConsumerTokenManager in your own app that stores tokens in a persistent store (like a SQL database). +#endif +}
\ No newline at end of file diff --git a/samples/DotNetOpenAuth.ApplicationBlock/TwitterConsumer.cs b/samples/DotNetOpenAuth.ApplicationBlock/TwitterConsumer.cs index ecb7d6c..0ebb1db 100644 --- a/samples/DotNetOpenAuth.ApplicationBlock/TwitterConsumer.cs +++ b/samples/DotNetOpenAuth.ApplicationBlock/TwitterConsumer.cs @@ -7,10 +7,14 @@ namespace DotNetOpenAuth.ApplicationBlock { using System; using System.Collections.Generic; + using System.Configuration; + using System.Globalization; using System.IO; using System.Net; + using System.Web; using System.Xml; using System.Xml.Linq; + using System.Xml.XPath; using DotNetOpenAuth.Messaging; using DotNetOpenAuth.OAuth; using DotNetOpenAuth.OAuth.ChannelElements; @@ -20,7 +24,8 @@ namespace DotNetOpenAuth.ApplicationBlock { /// </summary> public static class TwitterConsumer { /// <summary> - /// The description of Twitter's OAuth protocol URIs. + /// The description of Twitter's OAuth protocol URIs for use with actually reading/writing + /// a user's private Twitter data. /// </summary> public static readonly ServiceProviderDescription ServiceDescription = new ServiceProviderDescription { RequestTokenEndpoint = new MessageReceivingEndpoint("http://twitter.com/oauth/request_token", HttpDeliveryMethods.GetRequest | HttpDeliveryMethods.AuthorizationHeaderRequest), @@ -30,6 +35,16 @@ namespace DotNetOpenAuth.ApplicationBlock { }; /// <summary> + /// The description of Twitter's OAuth protocol URIs for use with their "Sign in with Twitter" feature. + /// </summary> + public static readonly ServiceProviderDescription SignInWithTwitterServiceDescription = new ServiceProviderDescription { + RequestTokenEndpoint = new MessageReceivingEndpoint("http://twitter.com/oauth/request_token", HttpDeliveryMethods.GetRequest | HttpDeliveryMethods.AuthorizationHeaderRequest), + UserAuthorizationEndpoint = new MessageReceivingEndpoint("http://twitter.com/oauth/authenticate", HttpDeliveryMethods.GetRequest | HttpDeliveryMethods.AuthorizationHeaderRequest), + AccessTokenEndpoint = new MessageReceivingEndpoint("http://twitter.com/oauth/access_token", HttpDeliveryMethods.GetRequest | HttpDeliveryMethods.AuthorizationHeaderRequest), + TamperProtectionElements = new ITamperProtectionChannelBindingElement[] { new HmacSha1SigningBindingElement() }, + }; + + /// <summary> /// The URI to get a user's favorites. /// </summary> private static readonly MessageReceivingEndpoint GetFavoritesEndpoint = new MessageReceivingEndpoint("http://twitter.com/favorites.xml", HttpDeliveryMethods.GetRequest); @@ -43,6 +58,18 @@ namespace DotNetOpenAuth.ApplicationBlock { private static readonly MessageReceivingEndpoint UpdateProfileImageEndpoint = new MessageReceivingEndpoint("http://twitter.com/account/update_profile_image.xml", HttpDeliveryMethods.PostRequest | HttpDeliveryMethods.AuthorizationHeaderRequest); + private static readonly MessageReceivingEndpoint VerifyCredentialsEndpoint = new MessageReceivingEndpoint("http://api.twitter.com/1/account/verify_credentials.xml", HttpDeliveryMethods.GetRequest | HttpDeliveryMethods.AuthorizationHeaderRequest); + + /// <summary> + /// The consumer used for the Sign in to Twitter feature. + /// </summary> + private static WebConsumer signInConsumer; + + /// <summary> + /// The lock acquired to initialize the <see cref="signInConsumer"/> field. + /// </summary> + private static object signInConsumerInitLock = new object(); + /// <summary> /// Initializes static members of the <see cref="TwitterConsumer"/> class. /// </summary> @@ -51,6 +78,53 @@ namespace DotNetOpenAuth.ApplicationBlock { ServicePointManager.FindServicePoint(GetFavoritesEndpoint.Location).Expect100Continue = false; } + /// <summary> + /// Gets a value indicating whether the Twitter consumer key and secret are set in the web.config file. + /// </summary> + public static bool IsTwitterConsumerConfigured { + get { + return !string.IsNullOrEmpty(ConfigurationManager.AppSettings["twitterConsumerKey"]) && + !string.IsNullOrEmpty(ConfigurationManager.AppSettings["twitterConsumerSecret"]); + } + } + + /// <summary> + /// Gets the consumer to use for the Sign in to Twitter feature. + /// </summary> + /// <value>The twitter sign in.</value> + private static WebConsumer TwitterSignIn { + get { + if (signInConsumer == null) { + lock (signInConsumerInitLock) { + if (signInConsumer == null) { + signInConsumer = new WebConsumer(SignInWithTwitterServiceDescription, ShortTermUserSessionTokenManager); + } + } + } + + return signInConsumer; + } + } + + private static InMemoryTokenManager ShortTermUserSessionTokenManager { + get { + var store = HttpContext.Current.Session; + var tokenManager = (InMemoryTokenManager)store["TwitterShortTermUserSessionTokenManager"]; + if (tokenManager == null) { + string consumerKey = ConfigurationManager.AppSettings["twitterConsumerKey"]; + string consumerSecret = ConfigurationManager.AppSettings["twitterConsumerSecret"]; + if (IsTwitterConsumerConfigured) { + tokenManager = new InMemoryTokenManager(consumerKey, consumerSecret); + store["TwitterShortTermUserSessionTokenManager"] = tokenManager; + } else { + throw new InvalidOperationException("No Twitter OAuth consumer key and secret could be found in web.config AppSettings."); + } + } + + return tokenManager; + } + } + public static XDocument GetUpdates(ConsumerBase twitter, string accessToken) { IncomingWebResponse response = twitter.PrepareAuthorizedRequestAndSend(GetFriendTimelineStatusEndpoint, accessToken); return XDocument.Load(XmlReader.Create(response.GetResponseReader())); @@ -62,13 +136,13 @@ namespace DotNetOpenAuth.ApplicationBlock { } public static XDocument UpdateProfileBackgroundImage(ConsumerBase twitter, string accessToken, string image, bool tile) { - HttpWebRequest request = twitter.PrepareAuthorizedRequest(UpdateProfileBackgroundImageEndpoint, accessToken); - request.ServicePoint.Expect100Continue = false; var parts = new[] { MultipartPostPart.CreateFormFilePart("image", image, "image/" + Path.GetExtension(image).Substring(1).ToLowerInvariant()), MultipartPostPart.CreateFormPart("tile", tile.ToString().ToLowerInvariant()), }; - IncomingWebResponse response = request.PostMultipart(twitter.Channel.WebRequestHandler, parts); + HttpWebRequest request = twitter.PrepareAuthorizedRequest(UpdateProfileBackgroundImageEndpoint, accessToken, parts); + request.ServicePoint.Expect100Continue = false; + IncomingWebResponse response = twitter.Channel.WebRequestHandler.GetResponse(request); string responseString = response.GetResponseReader().ReadToEnd(); return XDocument.Parse(responseString); } @@ -79,13 +153,72 @@ namespace DotNetOpenAuth.ApplicationBlock { } public static XDocument UpdateProfileImage(ConsumerBase twitter, string accessToken, Stream image, string contentType) { - HttpWebRequest request = twitter.PrepareAuthorizedRequest(UpdateProfileImageEndpoint, accessToken); var parts = new[] { MultipartPostPart.CreateFormFilePart("image", "twitterPhoto", contentType, image), }; - IncomingWebResponse response = request.PostMultipart(twitter.Channel.WebRequestHandler, parts); + HttpWebRequest request = twitter.PrepareAuthorizedRequest(UpdateProfileImageEndpoint, accessToken, parts); + IncomingWebResponse response = twitter.Channel.WebRequestHandler.GetResponse(request); string responseString = response.GetResponseReader().ReadToEnd(); return XDocument.Parse(responseString); } + + public static XDocument VerifyCredentials(ConsumerBase twitter, string accessToken) { + IncomingWebResponse response = twitter.PrepareAuthorizedRequestAndSend(VerifyCredentialsEndpoint, accessToken); + return XDocument.Load(XmlReader.Create(response.GetResponseReader())); + } + + public static string GetUsername(ConsumerBase twitter, string accessToken) { + XDocument xml = VerifyCredentials(twitter, accessToken); + XPathNavigator nav = xml.CreateNavigator(); + return nav.SelectSingleNode("/user/screen_name").Value; + } + + /// <summary> + /// Prepares a redirect that will send the user to Twitter to sign in. + /// </summary> + /// <param name="forceNewLogin">if set to <c>true</c> the user will be required to re-enter their Twitter credentials even if already logged in to Twitter.</param> + /// <returns>The redirect message.</returns> + /// <remarks> + /// Call <see cref="OutgoingWebResponse.Send"/> or + /// <c>return StartSignInWithTwitter().<see cref="MessagingUtilities.AsActionResult">AsActionResult()</see></c> + /// to actually perform the redirect. + /// </remarks> + public static OutgoingWebResponse StartSignInWithTwitter(bool forceNewLogin) { + var redirectParameters = new Dictionary<string, string>(); + if (forceNewLogin) { + redirectParameters["force_login"] = "true"; + } + Uri callback = MessagingUtilities.GetRequestUrlFromContext().StripQueryArgumentsWithPrefix("oauth_"); + var request = TwitterSignIn.PrepareRequestUserAuthorization(callback, null, redirectParameters); + return TwitterSignIn.Channel.PrepareResponse(request); + } + + /// <summary> + /// Checks the incoming web request to see if it carries a Twitter authentication response, + /// and provides the user's Twitter screen name and unique id if available. + /// </summary> + /// <param name="screenName">The user's Twitter screen name.</param> + /// <param name="userId">The user's Twitter unique user ID.</param> + /// <returns> + /// A value indicating whether Twitter authentication was successful; + /// otherwise <c>false</c> to indicate that no Twitter response was present. + /// </returns> + public static bool TryFinishSignInWithTwitter(out string screenName, out int userId) { + screenName = null; + userId = 0; + var response = TwitterSignIn.ProcessUserAuthorization(); + if (response == null) { + return false; + } + + screenName = response.ExtraData["screen_name"]; + userId = int.Parse(response.ExtraData["user_id"]); + + // If we were going to make this LOOK like OpenID even though it isn't, + // this seems like a reasonable, secure claimed id to allow the user to assume. + OpenId.Identifier fake_claimed_id = string.Format(CultureInfo.InvariantCulture, "http://twitter.com/{0}#{1}", screenName, userId); + + return true; + } } } diff --git a/samples/DotNetOpenAuth.ApplicationBlock/YubikeyRelyingParty.cs b/samples/DotNetOpenAuth.ApplicationBlock/YubikeyRelyingParty.cs new file mode 100644 index 0000000..6be749e --- /dev/null +++ b/samples/DotNetOpenAuth.ApplicationBlock/YubikeyRelyingParty.cs @@ -0,0 +1,207 @@ +//----------------------------------------------------------------------- +// <copyright file="YubikeyRelyingParty.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.ApplicationBlock { + using System; + using System.Collections.Specialized; + using System.Globalization; + using System.IO; + using System.Net; + using System.Text; + using System.Text.RegularExpressions; + + /// <summary> + /// The set of possible results from verifying a Yubikey token. + /// </summary> + public enum YubikeyResult { + /// <summary> + /// The OTP is valid. + /// </summary> + Ok, + + /// <summary> + /// The OTP is invalid format. + /// </summary> + BadOtp, + + /// <summary> + /// The OTP has already been seen by the service. + /// </summary> + ReplayedOtp, + + /// <summary> + /// The HMAC signature verification failed. + /// </summary> + /// <remarks> + /// This indicates a bug in the relying party code. + /// </remarks> + BadSignature, + + /// <summary> + /// The request lacks a parameter. + /// </summary> + /// <remarks> + /// This indicates a bug in the relying party code. + /// </remarks> + MissingParameter, + + /// <summary> + /// The request id does not exist. + /// </summary> + NoSuchClient, + + /// <summary> + /// The request id is not allowed to verify OTPs. + /// </summary> + OperationNotAllowed, + + /// <summary> + /// Unexpected error in our server. Please contact Yubico if you see this error. + /// </summary> + BackendError, + } + + /// <summary> + /// Provides verification of a Yubikey one-time password (OTP) as a means of authenticating + /// a user at your web site or application. + /// </summary> + /// <remarks> + /// Please visit http://yubico.com/ for more information about this authentication method. + /// </remarks> + public class YubikeyRelyingParty { + /// <summary> + /// The default Yubico authorization server to use for validation and replay protection. + /// </summary> + private const string DefaultYubicoAuthorizationServer = "https://api.yubico.com/wsapi/verify"; + + /// <summary> + /// The format of the lines in the Yubico server response. + /// </summary> + private static readonly Regex ResultLineMatcher = new Regex(@"^(?<key>[^=]+)=(?<value>.*)$"); + + /// <summary> + /// The Yubico authorization server to use for validation and replay protection. + /// </summary> + private readonly string yubicoAuthorizationServer; + + /// <summary> + /// The authorization ID assigned to your individual site by Yubico. + /// </summary> + private readonly int yubicoAuthorizationId; + + /// <summary> + /// Initializes a new instance of the <see cref="YubikeyRelyingParty"/> class + /// that uses the default Yubico server for validation and replay protection. + /// </summary> + /// <param name="authorizationId">The authorization ID assigned to your individual site by Yubico. + /// Get one from https://upgrade.yubico.com/getapikey/</param> + public YubikeyRelyingParty(int authorizationId) + : this(authorizationId, DefaultYubicoAuthorizationServer) { + } + + /// <summary> + /// Initializes a new instance of the <see cref="YubikeyRelyingParty"/> class. + /// </summary> + /// <param name="authorizationId">The authorization ID assigned to your individual site by Yubico. + /// Contact tech@yubico.com if you haven't got an authId for your site.</param> + /// <param name="yubicoAuthorizationServer">The Yubico authorization server to use for validation and replay protection.</param> + public YubikeyRelyingParty(int authorizationId, string yubicoAuthorizationServer) { + if (authorizationId < 0) { + throw new ArgumentOutOfRangeException("authorizationId"); + } + + if (!Uri.IsWellFormedUriString(yubicoAuthorizationServer, UriKind.Absolute)) { + throw new ArgumentException("Invalid authorization server URI", "yubicoAuthorizationServer"); + } + + if (!yubicoAuthorizationServer.StartsWith(Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase)) { + throw new ArgumentException("HTTPS is required for the Yubico server. HMAC response verification not supported.", "yubicoAuthorizationServer"); + } + + this.yubicoAuthorizationId = authorizationId; + this.yubicoAuthorizationServer = yubicoAuthorizationServer; + } + + /// <summary> + /// Extracts the username out of a Yubikey token. + /// </summary> + /// <param name="yubikeyToken">The yubikey token.</param> + /// <returns>A 12 character string that is unique for this particular Yubikey device.</returns> + public static string ExtractUsername(string yubikeyToken) { + EnsureWellFormedToken(yubikeyToken); + return yubikeyToken.Substring(0, 12); + } + + /// <summary> + /// Determines whether the specified yubikey token is valid and has not yet been used. + /// </summary> + /// <param name="yubikeyToken">The yubikey token.</param> + /// <returns> + /// <c>true</c> if the specified yubikey token is valid; otherwise, <c>false</c>. + /// </returns> + /// <exception cref="WebException">Thrown when the validity of the token could not be confirmed due to network issues.</exception> + public YubikeyResult IsValid(string yubikeyToken) { + EnsureWellFormedToken(yubikeyToken); + + StringBuilder authorizationUri = new StringBuilder(this.yubicoAuthorizationServer); + authorizationUri.Append("?id="); + authorizationUri.Append(Uri.EscapeDataString(this.yubicoAuthorizationId.ToString(CultureInfo.InvariantCulture))); + authorizationUri.Append("&otp="); + authorizationUri.Append(Uri.EscapeDataString(yubikeyToken)); + + var request = WebRequest.Create(authorizationUri.ToString()); + using (var response = request.GetResponse()) { + using (var responseReader = new StreamReader(response.GetResponseStream())) { + string line; + var result = new NameValueCollection(); + while ((line = responseReader.ReadLine()) != null) { + Match m = ResultLineMatcher.Match(line); + if (m.Success) { + result[m.Groups["key"].Value] = m.Groups["value"].Value; + } + } + + return ParseResult(result["status"]); + } + } + } + + /// <summary> + /// Parses the Yubico server result. + /// </summary> + /// <param name="status">The status field from the response.</param> + /// <returns>The enum value representing the result.</returns> + private static YubikeyResult ParseResult(string status) { + switch (status) { + case "OK": return YubikeyResult.Ok; + case "BAD_OTP": return YubikeyResult.BadOtp; + case "REPLAYED_OTP": return YubikeyResult.ReplayedOtp; + case "BAD_SIGNATURE": return YubikeyResult.BadSignature; + case "MISSING_PARAMETER": return YubikeyResult.MissingParameter; + case "NO_SUCH_CLIENT": return YubikeyResult.NoSuchClient; + case "OPERATION_NOT_ALLOWED": return YubikeyResult.OperationNotAllowed; + case "BACKEND_ERROR": return YubikeyResult.BackendError; + default: throw new ArgumentOutOfRangeException("status", status, "Unexpected status value."); + } + } + + /// <summary> + /// Ensures the OTP is well formed. + /// </summary> + /// <param name="yubikeyToken">The yubikey token.</param> + private static void EnsureWellFormedToken(string yubikeyToken) { + if (yubikeyToken == null) { + throw new ArgumentNullException("yubikeyToken"); + } + + yubikeyToken = yubikeyToken.Trim(); + + if (yubikeyToken.Length <= 12) { + throw new ArgumentException("Yubikey token has unexpected length."); + } + } + } +} |