diff options
59 files changed, 1347 insertions, 93 deletions
diff --git a/nuget/DotNetOpenAuth.OAuth2.Core.nuspec b/nuget/DotNetOpenAuth.OAuth2.Core.nuspec index f957069..28c2614 100644 --- a/nuget/DotNetOpenAuth.OAuth2.Core.nuspec +++ b/nuget/DotNetOpenAuth.OAuth2.Core.nuspec @@ -26,6 +26,8 @@ <file src="$OutputPath35$DotNetOpenAuth.OAuth2.xml" target="lib\net35-full" /> <file src="$OutputPath40$DotNetOpenAuth.OAuth2.xml" target="lib\net40-full" /> + <file src="content\OAuth2.Core\web.config.transform" target="content\web.config.transform" /> + <file src="..\src\DotNetOpenAuth.OAuth2\**\*.cs" target="src" /> </files> </package>
\ No newline at end of file diff --git a/nuget/content/OAuth2.AuthorizationServer/web.config.transform b/nuget/content/OAuth2.AuthorizationServer/web.config.transform new file mode 100644 index 0000000..c53fb7c --- /dev/null +++ b/nuget/content/OAuth2.AuthorizationServer/web.config.transform @@ -0,0 +1,9 @@ +<configuration> + <configSections> + <sectionGroup name="dotNetOpenAuth" type="DotNetOpenAuth.Configuration.DotNetOpenAuthSection, DotNetOpenAuth.Core"> + <sectionGroup name="oauth2" type="DotNetOpenAuth.Configuration.OAuth2SectionGroup, DotNetOpenAuth.OAuth2"> + <section name="authorizationServer" type="DotNetOpenAuth.Configuration.OAuth2AuthorizationServerSection, DotNetOpenAuth.OAuth2.AuthorizationServer" requirePermission="false" allowLocation="true" /> + </sectionGroup> + </sectionGroup> + </configSections> +</configuration>
\ No newline at end of file diff --git a/nuget/content/OAuth2.Client/web.config.transform b/nuget/content/OAuth2.Client/web.config.transform new file mode 100644 index 0000000..b1f4429 --- /dev/null +++ b/nuget/content/OAuth2.Client/web.config.transform @@ -0,0 +1,9 @@ +<configuration> + <configSections> + <sectionGroup name="dotNetOpenAuth" type="DotNetOpenAuth.Configuration.DotNetOpenAuthSection, DotNetOpenAuth.Core"> + <sectionGroup name="oauth2" type="DotNetOpenAuth.Configuration.OAuth2SectionGroup, DotNetOpenAuth.OAuth2"> + <section name="client" type="DotNetOpenAuth.Configuration.OAuth2ClientSection, DotNetOpenAuth.OAuth2.Client" requirePermission="false" allowLocation="true" /> + </sectionGroup> + </sectionGroup> + </configSections> +</configuration>
\ No newline at end of file diff --git a/nuget/content/OAuth2.Core/web.config.transform b/nuget/content/OAuth2.Core/web.config.transform new file mode 100644 index 0000000..2c47af1 --- /dev/null +++ b/nuget/content/OAuth2.Core/web.config.transform @@ -0,0 +1,7 @@ +<configuration> + <configSections> + <sectionGroup name="dotNetOpenAuth" type="DotNetOpenAuth.Configuration.DotNetOpenAuthSection, DotNetOpenAuth.Core"> + <sectionGroup name="oauth2" type="DotNetOpenAuth.Configuration.OAuth2SectionGroup, DotNetOpenAuth.OAuth2" /> + </sectionGroup> + </configSections> +</configuration>
\ No newline at end of file diff --git a/nuget/content/OAuth2.ResourceServer/web.config.transform b/nuget/content/OAuth2.ResourceServer/web.config.transform new file mode 100644 index 0000000..a92ff7d --- /dev/null +++ b/nuget/content/OAuth2.ResourceServer/web.config.transform @@ -0,0 +1,9 @@ +<configuration> + <configSections> + <sectionGroup name="dotNetOpenAuth" type="DotNetOpenAuth.Configuration.DotNetOpenAuthSection, DotNetOpenAuth.Core"> + <sectionGroup name="oauth2" type="DotNetOpenAuth.Configuration.OAuth2SectionGroup, DotNetOpenAuth.OAuth2"> + <section name="resourceServer" type="DotNetOpenAuth.Configuration.OAuth2ResourceServerSection, DotNetOpenAuth.OAuth2.ResourceServer" requirePermission="false" allowLocation="true" /> + </sectionGroup> + </sectionGroup> + </configSections> +</configuration>
\ No newline at end of file diff --git a/projecttemplates/MvcRelyingParty/MvcRelyingParty.csproj b/projecttemplates/MvcRelyingParty/MvcRelyingParty.csproj index 4db4969..4ef10f6 100644 --- a/projecttemplates/MvcRelyingParty/MvcRelyingParty.csproj +++ b/projecttemplates/MvcRelyingParty/MvcRelyingParty.csproj @@ -81,7 +81,6 @@ <Compile Include="Controllers\HomeController.cs" /> <Compile Include="Default.aspx.cs"> <DependentUpon>Default.aspx</DependentUpon> - <SubType>ASPXCodeBehind</SubType> </Compile> <Compile Include="Global.asax.cs"> <DependentUpon>Global.asax</DependentUpon> @@ -94,7 +93,6 @@ <Compile Include="Properties\AssemblyInfo.cs" /> <Compile Include="Setup.aspx.cs"> <DependentUpon>Setup.aspx</DependentUpon> - <SubType>ASPXCodeBehind</SubType> </Compile> <Compile Include="Setup.aspx.designer.cs"> <DependentUpon>Setup.aspx</DependentUpon> diff --git a/projecttemplates/WebFormsRelyingParty/WebFormsRelyingParty.csproj b/projecttemplates/WebFormsRelyingParty/WebFormsRelyingParty.csproj index 46236ab..92a0b5f 100644 --- a/projecttemplates/WebFormsRelyingParty/WebFormsRelyingParty.csproj +++ b/projecttemplates/WebFormsRelyingParty/WebFormsRelyingParty.csproj @@ -99,42 +99,36 @@ <Compile Include="Code\SiteUtilities.cs" /> <Compile Include="Members\OAuthAuthorize.aspx.cs"> <DependentUpon>OAuthAuthorize.aspx</DependentUpon> - <SubType>ASPXCodeBehind</SubType> </Compile> <Compile Include="Members\OAuthAuthorize.aspx.designer.cs"> <DependentUpon>OAuthAuthorize.aspx</DependentUpon> </Compile> <Compile Include="LoginFrame.aspx.cs"> <DependentUpon>LoginFrame.aspx</DependentUpon> - <SubType>ASPXCodeBehind</SubType> </Compile> <Compile Include="LoginFrame.aspx.designer.cs"> <DependentUpon>LoginFrame.aspx</DependentUpon> </Compile> <Compile Include="Members\AccountInfo.aspx.cs"> <DependentUpon>AccountInfo.aspx</DependentUpon> - <SubType>ASPXCodeBehind</SubType> </Compile> <Compile Include="Members\AccountInfo.aspx.designer.cs"> <DependentUpon>AccountInfo.aspx</DependentUpon> </Compile> <Compile Include="Admin\Admin.Master.cs"> <DependentUpon>Admin.Master</DependentUpon> - <SubType>ASPXCodeBehind</SubType> </Compile> <Compile Include="Admin\Admin.Master.designer.cs"> <DependentUpon>Admin.Master</DependentUpon> </Compile> <Compile Include="Admin\Default.aspx.cs"> <DependentUpon>Default.aspx</DependentUpon> - <SubType>ASPXCodeBehind</SubType> </Compile> <Compile Include="Admin\Default.aspx.designer.cs"> <DependentUpon>Default.aspx</DependentUpon> </Compile> <Compile Include="Default.aspx.cs"> <DependentUpon>Default.aspx</DependentUpon> - <SubType>ASPXCodeBehind</SubType> </Compile> <Compile Include="Default.aspx.designer.cs"> <DependentUpon>Default.aspx</DependentUpon> @@ -144,21 +138,18 @@ </Compile> <Compile Include="Login.aspx.cs"> <DependentUpon>Login.aspx</DependentUpon> - <SubType>ASPXCodeBehind</SubType> </Compile> <Compile Include="Login.aspx.designer.cs"> <DependentUpon>Login.aspx</DependentUpon> </Compile> <Compile Include="Logout.aspx.cs"> <DependentUpon>Logout.aspx</DependentUpon> - <SubType>ASPXCodeBehind</SubType> </Compile> <Compile Include="Logout.aspx.designer.cs"> <DependentUpon>Logout.aspx</DependentUpon> </Compile> <Compile Include="Members\Default.aspx.cs"> <DependentUpon>Default.aspx</DependentUpon> - <SubType>ASPXCodeBehind</SubType> </Compile> <Compile Include="Members\Default.aspx.designer.cs"> <DependentUpon>Default.aspx</DependentUpon> @@ -169,14 +160,12 @@ <Compile Include="Properties\AssemblyInfo.cs" /> <Compile Include="Setup.aspx.cs"> <DependentUpon>Setup.aspx</DependentUpon> - <SubType>ASPXCodeBehind</SubType> </Compile> <Compile Include="Setup.aspx.designer.cs"> <DependentUpon>Setup.aspx</DependentUpon> </Compile> <Compile Include="Site.Master.cs"> <DependentUpon>Site.Master</DependentUpon> - <SubType>ASPXCodeBehind</SubType> </Compile> <Compile Include="Site.Master.designer.cs"> <DependentUpon>Site.Master</DependentUpon> diff --git a/samples/OAuthAuthorizationServer/Web.config b/samples/OAuthAuthorizationServer/Web.config index b68bb88..37157fd 100644 --- a/samples/OAuthAuthorizationServer/Web.config +++ b/samples/OAuthAuthorizationServer/Web.config @@ -11,6 +11,9 @@ <sectionGroup name="dotNetOpenAuth" type="DotNetOpenAuth.Configuration.DotNetOpenAuthSection, DotNetOpenAuth.Core"> <section name="openid" type="DotNetOpenAuth.Configuration.OpenIdElement, DotNetOpenAuth.OpenId" requirePermission="false" allowLocation="true" /> <section name="oauth" type="DotNetOpenAuth.Configuration.OAuthElement, DotNetOpenAuth.OAuth" requirePermission="false" allowLocation="true" /> + <sectionGroup name="oauth2" type="DotNetOpenAuth.Configuration.OAuth2SectionGroup, DotNetOpenAuth.OAuth2"> + <section name="authorizationServer" type="DotNetOpenAuth.Configuration.OAuth2AuthorizationServerSection, DotNetOpenAuth.OAuth2.AuthorizationServer" requirePermission="false" allowLocation="true" /> + </sectionGroup> <section name="messaging" type="DotNetOpenAuth.Configuration.MessagingElement, DotNetOpenAuth.Core" requirePermission="false" allowLocation="true" /> <section name="reporting" type="DotNetOpenAuth.Configuration.ReportingElement, DotNetOpenAuth.Core" requirePermission="false" allowLocation="true" /> </sectionGroup> @@ -38,6 +41,10 @@ <dotNetOpenAuth> <!-- Allow DotNetOpenAuth to publish usage statistics to library authors to improve the library. --> <reporting enabled="true" /> + <oauth2> + <authorizationServer> + </authorizationServer> + </oauth2> <!-- Relaxing SSL requirements is useful for simple samples, but NOT a good idea in production. --> <messaging relaxSslRequirements="true"> @@ -64,7 +71,7 @@ </log4net> <connectionStrings> - <add name="DatabaseConnectionString" connectionString="Data Source=.\SQLEXPRESS;AttachDbFilename=|DataDirectory|\Database2.mdf;Integrated Security=True;User Instance=True" + <add name="DatabaseConnectionString" connectionString="Data Source=.\SQLEXPRESS;AttachDbFilename=|DataDirectory|\Database4.mdf;Integrated Security=True;User Instance=True" providerName="System.Data.SqlClient" /> </connectionStrings> diff --git a/samples/OAuthClient/Facebook.aspx.cs b/samples/OAuthClient/Facebook.aspx.cs index 0f71712..4701d24 100644 --- a/samples/OAuthClient/Facebook.aspx.cs +++ b/samples/OAuthClient/Facebook.aspx.cs @@ -10,7 +10,7 @@ public partial class Facebook : System.Web.UI.Page { private static readonly FacebookClient client = new FacebookClient { ClientIdentifier = ConfigurationManager.AppSettings["facebookAppID"], - ClientSecret = ConfigurationManager.AppSettings["facebookAppSecret"], + ClientCredentialApplicator = ClientCredentialApplicator.PostParameter(ConfigurationManager.AppSettings["facebookAppSecret"]), }; protected void Page_Load(object sender, EventArgs e) { diff --git a/samples/OAuthClient/WindowsLive.aspx.cs b/samples/OAuthClient/WindowsLive.aspx.cs index b550e17..05101a7 100644 --- a/samples/OAuthClient/WindowsLive.aspx.cs +++ b/samples/OAuthClient/WindowsLive.aspx.cs @@ -14,7 +14,7 @@ public partial class WindowsLive : System.Web.UI.Page { private static readonly WindowsLiveClient client = new WindowsLiveClient { ClientIdentifier = ConfigurationManager.AppSettings["windowsLiveAppID"], - ClientSecret = ConfigurationManager.AppSettings["WindowsLiveAppSecret"], + ClientCredentialApplicator = ClientCredentialApplicator.PostParameter(ConfigurationManager.AppSettings["WindowsLiveAppSecret"]), }; protected void Page_Load(object sender, EventArgs e) { diff --git a/samples/OAuthServiceProvider/OAuthServiceProvider.csproj b/samples/OAuthServiceProvider/OAuthServiceProvider.csproj index c96721e..fd2a5bb 100644 --- a/samples/OAuthServiceProvider/OAuthServiceProvider.csproj +++ b/samples/OAuthServiceProvider/OAuthServiceProvider.csproj @@ -114,19 +114,15 @@ </Compile> <Compile Include="Default.aspx.cs"> <DependentUpon>Default.aspx</DependentUpon> - <SubType>ASPXCodeBehind</SubType> </Compile> <Compile Include="Members\Authorize.aspx.cs"> <DependentUpon>Authorize.aspx</DependentUpon> - <SubType>ASPXCodeBehind</SubType> </Compile> <Compile Include="Members\AuthorizedConsumers.aspx.cs"> <DependentUpon>AuthorizedConsumers.aspx</DependentUpon> - <SubType>ASPXCodeBehind</SubType> </Compile> <Compile Include="TracePage.aspx.cs"> <DependentUpon>TracePage.aspx</DependentUpon> - <SubType>ASPXCodeBehind</SubType> </Compile> <Compile Include="TracePage.aspx.designer.cs"> <DependentUpon>TracePage.aspx</DependentUpon> diff --git a/src/DotNetOpenAuth.Core/Configuration/DotNetOpenAuth.xsd b/src/DotNetOpenAuth.Core/Configuration/DotNetOpenAuth.xsd index d193776..74d4db4 100644 --- a/src/DotNetOpenAuth.Core/Configuration/DotNetOpenAuth.xsd +++ b/src/DotNetOpenAuth.Core/Configuration/DotNetOpenAuth.xsd @@ -479,12 +479,19 @@ <xs:choice minOccurs="0" maxOccurs="unbounded"> <xs:element name="add"> <xs:complexType> - <xs:attribute name="name" type="xs:string" use="required" /> + <xs:attribute name="type" type="xs:string" use="optional"> + <xs:annotation> + <xs:documentation> + The fully-qualified name of the type that implements the IIdentifierDiscoveryService interface. + </xs:documentation> + </xs:annotation> + </xs:attribute> + <xs:attribute name="xaml" type="xs:string" use="optional" /> </xs:complexType> </xs:element> <xs:element name="remove"> <xs:complexType> - <xs:attribute name="name" type="xs:string" use="required" /> + <xs:attribute name="type" type="xs:string" use="required" /> </xs:complexType> </xs:element> <xs:element name="clear"> @@ -898,6 +905,84 @@ </xs:choice> </xs:complexType> </xs:element> + <xs:element name="oauth2"> + <xs:annotation> + <xs:documentation> + Settings OAuth 2 clients, authorization servers and resource servers. + </xs:documentation> + </xs:annotation> + <xs:complexType> + <xs:choice minOccurs="0" maxOccurs="unbounded"> + <xs:element name="client"> + <xs:annotation> + <xs:documentation> + Settings applicable to OAuth 2 Clients. + </xs:documentation> + </xs:annotation> + <xs:complexType> + <xs:choice minOccurs="0" maxOccurs="unbounded"> + </xs:choice> + </xs:complexType> + </xs:element> + <xs:element name="authorizationServer"> + <xs:annotation> + <xs:documentation> + Settings applicable to OAuth 2 Authorization Servers. + </xs:documentation> + </xs:annotation> + <xs:complexType> + <xs:choice minOccurs="0" maxOccurs="unbounded"> + <xs:element name="clientAuthenticationModules"> + <xs:complexType> + <xs:choice minOccurs="0" maxOccurs="unbounded"> + <xs:element name="add"> + <xs:complexType> + <xs:attribute name="type" type="xs:string" use="optional"> + <xs:annotation> + <xs:documentation> + The fully-qualified name of the ClientAuthenticationModule-derived type. + </xs:documentation> + </xs:annotation> + </xs:attribute> + <xs:attribute name="xaml" type="xs:string" use="optional" /> + </xs:complexType> + </xs:element> + <xs:element name="remove"> + <xs:complexType> + <xs:attribute name="type" type="xs:string" use="required"> + <xs:annotation> + <xs:documentation> + The fully-qualified name of the ClientAuthenticationModule-derived type. + </xs:documentation> + </xs:annotation> + </xs:attribute> + </xs:complexType> + </xs:element> + <xs:element name="clear"> + <xs:complexType> + <!--tag is empty--> + </xs:complexType> + </xs:element> + </xs:choice> + </xs:complexType> + </xs:element> + </xs:choice> + </xs:complexType> + </xs:element> + <xs:element name="resourceServer"> + <xs:annotation> + <xs:documentation> + Settings applicable to OAuth 2 Resource Servers. + </xs:documentation> + </xs:annotation> + <xs:complexType> + <xs:choice minOccurs="0" maxOccurs="unbounded"> + </xs:choice> + </xs:complexType> + </xs:element> + </xs:choice> + </xs:complexType> + </xs:element> <xs:element name="reporting"> <xs:annotation> <xs:documentation> diff --git a/src/DotNetOpenAuth.Core/DotNetOpenAuth.Core.csproj b/src/DotNetOpenAuth.Core/DotNetOpenAuth.Core.csproj index 447a3c5..65dee44 100644 --- a/src/DotNetOpenAuth.Core/DotNetOpenAuth.Core.csproj +++ b/src/DotNetOpenAuth.Core/DotNetOpenAuth.Core.csproj @@ -29,6 +29,8 @@ <Compile Include="Messaging\ChannelContract.cs" /> <Compile Include="Messaging\DataBagFormatterBase.cs" /> <Compile Include="Messaging\HttpRequestHeaders.cs" /> + <Compile Include="Messaging\IHttpDirectRequest.cs" /> + <Compile Include="Messaging\IHttpDirectRequestContract.cs" /> <Compile Include="Messaging\IHttpIndirectResponse.cs" /> <Compile Include="Messaging\IMessageOriginalPayload.cs" /> <Compile Include="Messaging\DirectWebRequestOptions.cs" /> diff --git a/src/DotNetOpenAuth.Core/Messaging/Channel.cs b/src/DotNetOpenAuth.Core/Messaging/Channel.cs index 016a2b6..235c41b 100644 --- a/src/DotNetOpenAuth.Core/Messaging/Channel.cs +++ b/src/DotNetOpenAuth.Core/Messaging/Channel.cs @@ -478,6 +478,14 @@ namespace DotNetOpenAuth.Messaging { IDirectedProtocolMessage requestMessage = this.ReadFromRequestCore(httpRequest); if (requestMessage != null) { Logger.Channel.DebugFormat("Incoming request received: {0}", requestMessage.GetType().Name); + + var directRequest = requestMessage as IHttpDirectRequest; + if (directRequest != null) { + foreach (string header in httpRequest.Headers) { + directRequest.Headers[header] = httpRequest.Headers[header]; + } + } + this.ProcessIncomingMessage(requestMessage); } @@ -717,6 +725,13 @@ namespace DotNetOpenAuth.Messaging { Requires.True(request.Recipient != null, "request", MessagingStrings.DirectedMessageMissingRecipient); HttpWebRequest webRequest = this.CreateHttpRequest(request); + var directRequest = request as IHttpDirectRequest; + if (directRequest != null) { + foreach (string header in directRequest.Headers) { + webRequest.Headers[header] = directRequest.Headers[header]; + } + } + IDictionary<string, string> responseFields; IDirectResponseProtocolMessage responseMessage; @@ -1082,6 +1097,7 @@ namespace DotNetOpenAuth.Messaging { UriBuilder builder = new UriBuilder(requestMessage.Recipient); MessagingUtilities.AppendQueryArgs(builder, fields); HttpWebRequest httpRequest = (HttpWebRequest)WebRequest.Create(builder.Uri); + this.PrepareHttpWebRequest(httpRequest); return httpRequest; } @@ -1122,6 +1138,7 @@ namespace DotNetOpenAuth.Messaging { var fields = messageAccessor.Serialize(); var httpRequest = (HttpWebRequest)WebRequest.Create(requestMessage.Recipient); + this.PrepareHttpWebRequest(httpRequest); httpRequest.CachePolicy = this.CachePolicy; httpRequest.Method = "POST"; @@ -1299,6 +1316,14 @@ namespace DotNetOpenAuth.Messaging { } /// <summary> + /// Performs additional processing on an outgoing web request before it is sent to the remote server. + /// </summary> + /// <param name="request">The request.</param> + protected virtual void PrepareHttpWebRequest(HttpWebRequest request) { + Requires.NotNull(request, "request"); + } + + /// <summary> /// Customizes the binding element order for outgoing and incoming messages. /// </summary> /// <param name="outgoingOrder">The outgoing order.</param> diff --git a/src/DotNetOpenAuth.Core/Messaging/ErrorUtilities.cs b/src/DotNetOpenAuth.Core/Messaging/ErrorUtilities.cs index f499d67..2237cc7 100644 --- a/src/DotNetOpenAuth.Core/Messaging/ErrorUtilities.cs +++ b/src/DotNetOpenAuth.Core/Messaging/ErrorUtilities.cs @@ -193,17 +193,17 @@ namespace DotNetOpenAuth.Messaging { /// Throws a <see cref="ProtocolException"/> if some <paramref name="condition"/> evaluates to false. /// </summary> /// <param name="condition">True to do nothing; false to throw the exception.</param> - /// <param name="message">The error message for the exception.</param> + /// <param name="unformattedMessage">The error message for the exception.</param> /// <param name="args">The string formatting arguments, if any.</param> /// <exception cref="ProtocolException">Thrown if <paramref name="condition"/> evaluates to <c>false</c>.</exception> [Pure] - internal static void VerifyProtocol(bool condition, string message, params object[] args) { + internal static void VerifyProtocol(bool condition, string unformattedMessage, params object[] args) { Requires.NotNull(args, "args"); Contract.Ensures(condition); Contract.EnsuresOnThrow<ProtocolException>(!condition); - Contract.Assume(message != null); + Contract.Assume(unformattedMessage != null); if (!condition) { - var exception = new ProtocolException(string.Format(CultureInfo.CurrentCulture, message, args)); + var exception = new ProtocolException(string.Format(CultureInfo.CurrentCulture, unformattedMessage, args)); if (Logger.Messaging.IsErrorEnabled) { Logger.Messaging.Error( string.Format( @@ -220,7 +220,7 @@ namespace DotNetOpenAuth.Messaging { /// <summary> /// Throws a <see cref="ProtocolException"/>. /// </summary> - /// <param name="message">The message to set in the exception.</param> + /// <param name="unformattedMessage">The message to set in the exception.</param> /// <param name="args">The formatting arguments of the message.</param> /// <returns> /// An InternalErrorException, which may be "thrown" by the caller in order @@ -229,10 +229,10 @@ namespace DotNetOpenAuth.Messaging { /// </returns> /// <exception cref="ProtocolException">Always thrown.</exception> [Pure] - internal static Exception ThrowProtocol(string message, params object[] args) { + internal static Exception ThrowProtocol(string unformattedMessage, params object[] args) { Requires.NotNull(args, "args"); - Contract.Assume(message != null); - VerifyProtocol(false, message, args); + Contract.Assume(unformattedMessage != null); + VerifyProtocol(false, unformattedMessage, args); // we never reach here, but this allows callers to "throw" this method. return new InternalErrorException(); diff --git a/src/DotNetOpenAuth.Core/Messaging/HttpRequestHeaders.cs b/src/DotNetOpenAuth.Core/Messaging/HttpRequestHeaders.cs index 9579a81..dad6bf6 100644 --- a/src/DotNetOpenAuth.Core/Messaging/HttpRequestHeaders.cs +++ b/src/DotNetOpenAuth.Core/Messaging/HttpRequestHeaders.cs @@ -20,6 +20,11 @@ namespace DotNetOpenAuth.Messaging { internal const string Authorization = "Authorization"; /// <summary> + /// The WWW-Authenticate header, which is included in HTTP 401 Unauthorized responses to help the client know which authorization schemes are supported. + /// </summary> + internal const string WwwAuthenticate = "WWW-Authenticate"; + + /// <summary> /// The Content-Type header, which specifies the MIME type of the accompanying body data. /// </summary> internal const string ContentType = "Content-Type"; diff --git a/src/DotNetOpenAuth.Core/Messaging/HttpRequestInfo.cs b/src/DotNetOpenAuth.Core/Messaging/HttpRequestInfo.cs index 0f60e04..f613dc5 100644 --- a/src/DotNetOpenAuth.Core/Messaging/HttpRequestInfo.cs +++ b/src/DotNetOpenAuth.Core/Messaging/HttpRequestInfo.cs @@ -90,7 +90,7 @@ namespace DotNetOpenAuth.Messaging { this.requestUri = requestUri; this.form = form ?? new NameValueCollection(); this.queryString = HttpUtility.ParseQueryString(requestUri.Query); - this.headers = headers ?? new NameValueCollection(); + this.headers = headers ?? new WebHeaderCollection(); this.serverVariables = new NameValueCollection(); } diff --git a/src/DotNetOpenAuth.Core/Messaging/IHttpDirectRequest.cs b/src/DotNetOpenAuth.Core/Messaging/IHttpDirectRequest.cs new file mode 100644 index 0000000..7153334 --- /dev/null +++ b/src/DotNetOpenAuth.Core/Messaging/IHttpDirectRequest.cs @@ -0,0 +1,22 @@ +//----------------------------------------------------------------------- +// <copyright file="IHttpDirectRequest.cs" company="Outercurve Foundation"> +// Copyright (c) Outercurve Foundation. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.Messaging { + using System.Diagnostics.Contracts; + using System.Net; + + /// <summary> + /// An interface that allows direct request messages to capture the details of the HTTP request they arrived on. + /// </summary> + [ContractClass(typeof(IHttpDirectRequestContract))] + public interface IHttpDirectRequest : IMessage { + /// <summary> + /// Gets the HTTP headers of the request. + /// </summary> + /// <value>May be an empty collection, but must not be <c>null</c>.</value> + WebHeaderCollection Headers { get; } + } +} diff --git a/src/DotNetOpenAuth.Core/Messaging/IHttpDirectRequestContract.cs b/src/DotNetOpenAuth.Core/Messaging/IHttpDirectRequestContract.cs new file mode 100644 index 0000000..cfde6cf --- /dev/null +++ b/src/DotNetOpenAuth.Core/Messaging/IHttpDirectRequestContract.cs @@ -0,0 +1,75 @@ +//----------------------------------------------------------------------- +// <copyright file="IHttpDirectRequestContract.cs" company="Outercurve Foundation"> +// Copyright (c) Outercurve Foundation. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.Messaging { + using System; + using System.Collections.Generic; + using System.Diagnostics.Contracts; + using System.Linq; + using System.Net; + using System.Text; + + /// <summary> + /// Contract class for the <see cref="IHttpDirectRequest"/> interface. + /// </summary> + [ContractClassFor(typeof(IHttpDirectRequest))] + public abstract class IHttpDirectRequestContract : IHttpDirectRequest { + #region IHttpDirectRequest Members + + /// <summary> + /// Gets the HTTP headers of the request. + /// </summary> + /// <value>May be an empty collection, but must not be <c>null</c>.</value> + WebHeaderCollection IHttpDirectRequest.Headers { + get { + Contract.Ensures(Contract.Result<WebHeaderCollection>() != null); + throw new NotImplementedException(); + } + } + + #endregion + + #region IMessage Members + + /// <summary> + /// Gets the version of the protocol or extension this message is prepared to implement. + /// </summary> + /// <remarks> + /// Implementations of this interface should ensure that this property never returns null. + /// </remarks> + Version IMessage.Version { + get { throw new NotImplementedException(); } + } + + /// <summary> + /// Gets the extra, non-standard Protocol parameters included in the message. + /// </summary> + /// <remarks> + /// Implementations of this interface should ensure that this property never returns null. + /// </remarks> + IDictionary<string, string> IMessage.ExtraData { + get { throw new NotImplementedException(); } + } + + /// <summary> + /// Checks the message state for conformity to the protocol specification + /// and throws an exception if the message is invalid. + /// </summary> + /// <remarks> + /// <para>Some messages have required fields, or combinations of fields that must relate to each other + /// in specialized ways. After deserializing a message, this method checks the state of the + /// message to see if it conforms to the protocol.</para> + /// <para>Note that this property should <i>not</i> check signatures or perform any state checks + /// outside this scope of this particular message.</para> + /// </remarks> + /// <exception cref="ProtocolException">Thrown if the message is invalid.</exception> + void IMessage.EnsureValidMessage() { + throw new NotImplementedException(); + } + + #endregion + } +} diff --git a/src/DotNetOpenAuth.Core/Messaging/OutgoingWebResponseActionResult.cs b/src/DotNetOpenAuth.Core/Messaging/OutgoingWebResponseActionResult.cs index a5fe782..7691cc4 100644 --- a/src/DotNetOpenAuth.Core/Messaging/OutgoingWebResponseActionResult.cs +++ b/src/DotNetOpenAuth.Core/Messaging/OutgoingWebResponseActionResult.cs @@ -35,6 +35,11 @@ namespace DotNetOpenAuth.Messaging { /// <param name="context">The context in which to set the response.</param> public override void ExecuteResult(ControllerContext context) { this.response.Respond(context.HttpContext); + + // MVC likes to muck with our response. For example, when returning contrived 401 Unauthorized responses + // MVC will rewrite our response and turn it into a redirect, which breaks OAuth 2 authorization server token endpoints. + // It turns out we can prevent this unwanted behavior by flushing the response before returning from this method. + context.HttpContext.Response.Flush(); } } } diff --git a/src/DotNetOpenAuth.Core/Requires.cs b/src/DotNetOpenAuth.Core/Requires.cs index 7a196a3..7d4d5be 100644 --- a/src/DotNetOpenAuth.Core/Requires.cs +++ b/src/DotNetOpenAuth.Core/Requires.cs @@ -43,14 +43,17 @@ namespace DotNetOpenAuth { /// </summary> /// <param name="value">The value.</param> /// <param name="parameterName">Name of the parameter.</param> + /// <returns>The validated value.</returns> #if !CLR4 [ContractArgumentValidator] #endif [Pure, DebuggerStepThrough] - internal static void NotNullOrEmpty(string value, string parameterName) { + internal static string NotNullOrEmpty(string value, string parameterName) { NotNull(value, parameterName); True(value.Length > 0, parameterName, Strings.EmptyStringNotAllowed); + Contract.Ensures(Contract.Result<string>() == value); Contract.EndContractBlock(); + return value; } /// <summary> diff --git a/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/OAuthChannel.cs b/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/OAuthChannel.cs index b04c67e..db131a9 100644 --- a/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/OAuthChannel.cs +++ b/src/DotNetOpenAuth.OAuth/OAuth/ChannelElements/OAuthChannel.cs @@ -297,6 +297,7 @@ namespace DotNetOpenAuth.OAuth.ChannelElements { MessagingUtilities.AppendQueryArgs(recipientBuilder, requestMessage.ExtraData); } httpRequest = (HttpWebRequest)WebRequest.Create(recipientBuilder.Uri); + this.PrepareHttpWebRequest(httpRequest); httpRequest.Method = GetHttpMethod(requestMessage); httpRequest.Headers.Add(HttpRequestHeader.Authorization, MessagingUtilities.AssembleAuthorizationHeader(Protocol.AuthorizationHeaderScheme, fields)); diff --git a/src/DotNetOpenAuth.OAuth2.AuthorizationServer/Configuration/OAuth2AuthorizationServerSection.cs b/src/DotNetOpenAuth.OAuth2.AuthorizationServer/Configuration/OAuth2AuthorizationServerSection.cs new file mode 100644 index 0000000..6511a11 --- /dev/null +++ b/src/DotNetOpenAuth.OAuth2.AuthorizationServer/Configuration/OAuth2AuthorizationServerSection.cs @@ -0,0 +1,70 @@ +//----------------------------------------------------------------------- +// <copyright file="OAuth2AuthorizationServerSection.cs" company="Outercurve Foundation"> +// Copyright (c) Outercurve Foundation. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.Configuration { + using System; + using System.Configuration; + using System.Diagnostics.Contracts; + using DotNetOpenAuth.Messaging.Bindings; + using DotNetOpenAuth.OAuth2.ChannelElements; + + /// <summary> + /// Represents the <oauth2/authorizationServer> section in the host's .config file. + /// </summary> + internal class OAuth2AuthorizationServerSection : ConfigurationSection { + /// <summary> + /// The name of the oauth2/authorizationServer section. + /// </summary> + private const string SectionName = OAuth2SectionGroup.SectionName + "/authorizationServer"; + + /// <summary> + /// The name of the <clientAuthenticationModules> sub-element. + /// </summary> + private const string ClientAuthenticationModulesElementName = "clientAuthenticationModules"; + + /// <summary> + /// The built-in set of client authentication modules. + /// </summary> + private static readonly TypeConfigurationCollection<ClientAuthenticationModule> defaultClientAuthenticationModules = + new TypeConfigurationCollection<ClientAuthenticationModule>(new Type[] { typeof(ClientCredentialHttpBasicReader), typeof(ClientCredentialMessagePartReader) }); + + /// <summary> + /// Initializes a new instance of the <see cref="OAuth2AuthorizationServerSection"/> class. + /// </summary> + internal OAuth2AuthorizationServerSection() { + } + + /// <summary> + /// Gets the configuration section from the .config file. + /// </summary> + internal static OAuth2AuthorizationServerSection Configuration { + get { + Contract.Ensures(Contract.Result<OAuth2AuthorizationServerSection>() != null); + return (OAuth2AuthorizationServerSection)ConfigurationManager.GetSection(SectionName) ?? new OAuth2AuthorizationServerSection(); + } + } + + /// <summary> + /// Gets or sets the services to use for discovering service endpoints for identifiers. + /// </summary> + /// <remarks> + /// If no discovery services are defined in the (web) application's .config file, + /// the default set of discovery services built into the library are used. + /// </remarks> + [ConfigurationProperty(ClientAuthenticationModulesElementName, IsDefaultCollection = false)] + [ConfigurationCollection(typeof(TypeConfigurationCollection<ClientAuthenticationModule>))] + internal TypeConfigurationCollection<ClientAuthenticationModule> ClientAuthenticationModules { + get { + var configResult = (TypeConfigurationCollection<ClientAuthenticationModule>)this[ClientAuthenticationModulesElementName]; + return configResult != null && configResult.Count > 0 ? configResult : defaultClientAuthenticationModules; + } + + set { + this[ClientAuthenticationModulesElementName] = value; + } + } + } +} diff --git a/src/DotNetOpenAuth.OAuth2.AuthorizationServer/DotNetOpenAuth.OAuth2.AuthorizationServer.csproj b/src/DotNetOpenAuth.OAuth2.AuthorizationServer/DotNetOpenAuth.OAuth2.AuthorizationServer.csproj index c1f3124..c6371c0 100644 --- a/src/DotNetOpenAuth.OAuth2.AuthorizationServer/DotNetOpenAuth.OAuth2.AuthorizationServer.csproj +++ b/src/DotNetOpenAuth.OAuth2.AuthorizationServer/DotNetOpenAuth.OAuth2.AuthorizationServer.csproj @@ -18,6 +18,7 @@ <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' "> </PropertyGroup> <ItemGroup> + <Compile Include="Configuration\OAuth2AuthorizationServerSection.cs" /> <Compile Include="OAuth2\AuthorizationServer.cs" /> <Compile Include="OAuth2\AuthServerStrings.Designer.cs"> <AutoGen>True</AutoGen> @@ -25,6 +26,9 @@ <DependentUpon>AuthServerStrings.resx</DependentUpon> </Compile> <Compile Include="OAuth2\AuthServerUtilities.cs" /> + <Compile Include="OAuth2\ChannelElements\AggregatingClientCredentialReader.cs" /> + <Compile Include="OAuth2\ChannelElements\ClientCredentialHttpBasicReader.cs" /> + <Compile Include="OAuth2\ChannelElements\ClientCredentialMessagePartReader.cs" /> <Compile Include="OAuth2\ChannelElements\TokenCodeSerializationBindingElement.cs" /> <Compile Include="OAuth2\ChannelElements\AuthorizationCode.cs" /> <Compile Include="OAuth2\ChannelElements\MessageValidationBindingElement.cs" /> @@ -32,6 +36,7 @@ <Compile Include="OAuth2\ChannelElements\IOAuth2ChannelWithAuthorizationServer.cs" /> <Compile Include="OAuth2\ChannelElements\OAuth2AuthorizationServerChannel.cs" /> <Compile Include="OAuth2\ChannelElements\RefreshToken.cs" /> + <Compile Include="OAuth2\ChannelElements\ClientAuthenticationModule.cs" /> <Compile Include="OAuth2\ClientDescription.cs" /> <Compile Include="OAuth2\Messages\AccessTokenAuthorizationCodeRequestAS.cs" /> <Compile Include="OAuth2\Messages\AccessTokenRefreshRequestAS.cs" /> diff --git a/src/DotNetOpenAuth.OAuth2.AuthorizationServer/OAuth2/AuthServerUtilities.cs b/src/DotNetOpenAuth.OAuth2.AuthorizationServer/OAuth2/AuthServerUtilities.cs index cd222e2..b8a1071 100644 --- a/src/DotNetOpenAuth.OAuth2.AuthorizationServer/OAuth2/AuthServerUtilities.cs +++ b/src/DotNetOpenAuth.OAuth2.AuthorizationServer/OAuth2/AuthServerUtilities.cs @@ -12,6 +12,8 @@ namespace DotNetOpenAuth.OAuth2 { using System.Linq; using System.Text; using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.OAuth2.ChannelElements; + using DotNetOpenAuth.OAuth2.Messages; /// <summary> /// Utility methods for authorization servers. @@ -42,13 +44,21 @@ namespace DotNetOpenAuth.OAuth2 { /// Verifies a condition is true or throws an exception describing the problem. /// </summary> /// <param name="condition">The condition that evaluates to true to avoid an exception.</param> + /// <param name="requestMessage">The request message.</param> /// <param name="error">A single error code from <see cref="Protocol.AccessTokenRequestErrorCodes"/>.</param> + /// <param name="authenticationModule">The authentication module from which to glean the WWW-Authenticate header when applicable.</param> /// <param name="unformattedDescription">A human-readable UTF-8 encoded text providing additional information, used to assist the client developer in understanding the error that occurred.</param> /// <param name="args">The formatting arguments to generate the actual description.</param> - internal static void TokenEndpointVerify(bool condition, string error, string unformattedDescription = null, params object[] args) { + internal static void TokenEndpointVerify(bool condition, AccessTokenRequestBase requestMessage, string error, ClientAuthenticationModule authenticationModule = null, string unformattedDescription = null, params object[] args) { if (!condition) { string description = unformattedDescription != null ? string.Format(CultureInfo.CurrentCulture, unformattedDescription, args) : null; - throw new TokenEndpointProtocolException(error, description); + + string wwwAuthenticateHeader = null; + if (authenticationModule != null) { + wwwAuthenticateHeader = authenticationModule.AuthenticateHeader; + } + + throw new TokenEndpointProtocolException(requestMessage, error, description, authenticateHeader: wwwAuthenticateHeader); } } } diff --git a/src/DotNetOpenAuth.OAuth2.AuthorizationServer/OAuth2/AuthorizationServer.cs b/src/DotNetOpenAuth.OAuth2.AuthorizationServer/OAuth2/AuthorizationServer.cs index 3809c3d..59b75bf 100644 --- a/src/DotNetOpenAuth.OAuth2.AuthorizationServer/OAuth2/AuthorizationServer.cs +++ b/src/DotNetOpenAuth.OAuth2.AuthorizationServer/OAuth2/AuthorizationServer.cs @@ -13,7 +13,7 @@ namespace DotNetOpenAuth.OAuth2 { using System.Security.Cryptography; using System.Text; using System.Web; - + using DotNetOpenAuth.Configuration; using DotNetOpenAuth.Messaging; using DotNetOpenAuth.OAuth2.ChannelElements; using DotNetOpenAuth.OAuth2.Messages; @@ -23,12 +23,24 @@ namespace DotNetOpenAuth.OAuth2 { /// </summary> public class AuthorizationServer { /// <summary> + /// The list of modules that verify client authentication data. + /// </summary> + private readonly List<ClientAuthenticationModule> clientAuthenticationModules = new List<ClientAuthenticationModule>(); + + /// <summary> + /// The lone aggregate client authentication module that uses the <see cref="clientAuthenticationModules"/> and applies aggregating policy. + /// </summary> + private readonly ClientAuthenticationModule aggregatingClientAuthenticationModule; + + /// <summary> /// Initializes a new instance of the <see cref="AuthorizationServer"/> class. /// </summary> /// <param name="authorizationServer">The authorization server.</param> public AuthorizationServer(IAuthorizationServerHost authorizationServer) { Requires.NotNull(authorizationServer, "authorizationServer"); - this.Channel = new OAuth2AuthorizationServerChannel(authorizationServer); + this.aggregatingClientAuthenticationModule = new AggregatingClientCredentialReader(this.clientAuthenticationModules); + this.Channel = new OAuth2AuthorizationServerChannel(authorizationServer, this.aggregatingClientAuthenticationModule); + this.clientAuthenticationModules.AddRange(OAuth2AuthorizationServerSection.Configuration.ClientAuthenticationModules.CreateInstances(true)); } /// <summary> @@ -46,6 +58,13 @@ namespace DotNetOpenAuth.OAuth2 { } /// <summary> + /// Gets the extension modules that can read client authentication data from incoming messages. + /// </summary> + public IList<ClientAuthenticationModule> ClientAuthenticationModules { + get { return this.clientAuthenticationModules; } + } + + /// <summary> /// Reads in a client's request for the Authorization Server to obtain permission from /// the user to authorize the Client's access of some protected resource(s). /// </summary> @@ -129,7 +148,7 @@ namespace DotNetOpenAuth.OAuth2 { responseMessage = new AccessTokenFailedResponse() { Error = Protocol.AccessTokenRequestErrorCodes.InvalidRequest, }; } } catch (TokenEndpointProtocolException ex) { - responseMessage = new AccessTokenFailedResponse() { Error = ex.Error, ErrorDescription = ex.Description, ErrorUri = ex.MoreInformation }; + responseMessage = ex.GetResponse(); } catch (ProtocolException) { responseMessage = new AccessTokenFailedResponse() { Error = Protocol.AccessTokenRequestErrorCodes.InvalidRequest, diff --git a/src/DotNetOpenAuth.OAuth2.AuthorizationServer/OAuth2/ChannelElements/AggregatingClientCredentialReader.cs b/src/DotNetOpenAuth.OAuth2.AuthorizationServer/OAuth2/ChannelElements/AggregatingClientCredentialReader.cs new file mode 100644 index 0000000..ace95b3 --- /dev/null +++ b/src/DotNetOpenAuth.OAuth2.AuthorizationServer/OAuth2/ChannelElements/AggregatingClientCredentialReader.cs @@ -0,0 +1,91 @@ +//----------------------------------------------------------------------- +// <copyright file="AggregatingClientCredentialReader.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OAuth2.ChannelElements { + using System; + using System.Collections.Generic; + using System.Globalization; + using System.Linq; + using System.Text; + using System.Web; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.OAuth2.Messages; + + /// <summary> + /// Applies OAuth 2 spec policy for supporting multiple methods of client authentication. + /// </summary> + internal class AggregatingClientCredentialReader : ClientAuthenticationModule { + /// <summary> + /// The set of authenticators to apply to an incoming request. + /// </summary> + private readonly IEnumerable<ClientAuthenticationModule> authenticators; + + /// <summary> + /// Initializes a new instance of the <see cref="AggregatingClientCredentialReader"/> class. + /// </summary> + /// <param name="authenticators">The set of authentication modules to apply.</param> + internal AggregatingClientCredentialReader(IEnumerable<ClientAuthenticationModule> authenticators) { + Requires.NotNull(authenticators, "readers"); + this.authenticators = authenticators; + } + + /// <summary> + /// Gets this module's contribution to an HTTP 401 WWW-Authenticate header so the client knows what kind of authentication this module supports. + /// </summary> + public override string AuthenticateHeader { + get { + var builder = new StringBuilder(); + foreach (var authenticator in this.authenticators) { + string scheme = authenticator.AuthenticateHeader; + if (scheme != null) { + if (builder.Length > 0) { + builder.Append(", "); + } + + builder.Append(scheme); + } + } + + return builder.Length > 0 ? builder.ToString() : null; + } + } + + /// <summary> + /// Attempts to extract client identification/authentication information from a message. + /// </summary> + /// <param name="authorizationServerHost">The authorization server host.</param> + /// <param name="requestMessage">The incoming message.</param> + /// <param name="clientIdentifier">Receives the client identifier, if one was found.</param> + /// <returns>The level of the extracted client information.</returns> + public override ClientAuthenticationResult TryAuthenticateClient(IAuthorizationServerHost authorizationServerHost, AuthenticatedClientRequestBase requestMessage, out string clientIdentifier) { + Requires.NotNull(authorizationServerHost, "authorizationServerHost"); + Requires.NotNull(requestMessage, "requestMessage"); + + ClientAuthenticationModule authenticator = null; + ClientAuthenticationResult result = ClientAuthenticationResult.NoAuthenticationRecognized; + clientIdentifier = null; + + foreach (var candidateAuthenticator in this.authenticators) { + string candidateClientIdentifier; + var resultCandidate = candidateAuthenticator.TryAuthenticateClient(authorizationServerHost, requestMessage, out candidateClientIdentifier); + + ErrorUtilities.VerifyProtocol( + result == ClientAuthenticationResult.NoAuthenticationRecognized || resultCandidate == ClientAuthenticationResult.NoAuthenticationRecognized, + "Message rejected because multiple forms of client authentication ({0} and {1}) were detected, which is forbidden by the OAuth 2 Protocol Framework specification.", + authenticator, + candidateAuthenticator); + + if (resultCandidate != ClientAuthenticationResult.NoAuthenticationRecognized) { + authenticator = candidateAuthenticator; + result = resultCandidate; + clientIdentifier = candidateClientIdentifier; + } + } + + return result; + } + } +} diff --git a/src/DotNetOpenAuth.OAuth2.AuthorizationServer/OAuth2/ChannelElements/ClientAuthenticationModule.cs b/src/DotNetOpenAuth.OAuth2.AuthorizationServer/OAuth2/ChannelElements/ClientAuthenticationModule.cs new file mode 100644 index 0000000..027929a --- /dev/null +++ b/src/DotNetOpenAuth.OAuth2.AuthorizationServer/OAuth2/ChannelElements/ClientAuthenticationModule.cs @@ -0,0 +1,74 @@ +//----------------------------------------------------------------------- +// <copyright file="ClientAuthenticationModule.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OAuth2.ChannelElements { + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + using System.Threading; + using System.Web; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.OAuth2.Messages; + + /// <summary> + /// A base class for extensions that can read incoming messages and extract the client identifier and + /// possibly authentication information (like a shared secret, signed nonce, etc.) + /// </summary> + public abstract class ClientAuthenticationModule { + /// <summary> + /// Initializes a new instance of the <see cref="ClientAuthenticationModule"/> class. + /// </summary> + protected ClientAuthenticationModule() { + } + + /// <summary> + /// Gets this module's contribution to an HTTP 401 WWW-Authenticate header so the client knows what kind of authentication this module supports. + /// </summary> + public virtual string AuthenticateHeader { + get { return null; } + } + + /// <summary> + /// Attempts to extract client identification/authentication information from a message. + /// </summary> + /// <param name="authorizationServerHost">The authorization server host.</param> + /// <param name="requestMessage">The incoming message.</param> + /// <param name="clientIdentifier">Receives the client identifier, if one was found.</param> + /// <returns>The level of the extracted client information.</returns> + public abstract ClientAuthenticationResult TryAuthenticateClient(IAuthorizationServerHost authorizationServerHost, AuthenticatedClientRequestBase requestMessage, out string clientIdentifier); + + /// <summary> + /// Validates a client identifier and shared secret against the authoriation server's database. + /// </summary> + /// <param name="authorizationServerHost">The authorization server host; cannot be <c>null</c>.</param> + /// <param name="clientIdentifier">The alleged client identifier.</param> + /// <param name="clientSecret">The alleged client secret to be verified.</param> + /// <returns>An indication as to the outcome of the validation.</returns> + protected static ClientAuthenticationResult TryAuthenticateClientBySecret(IAuthorizationServerHost authorizationServerHost, string clientIdentifier, string clientSecret) { + Requires.NotNull(authorizationServerHost, "authorizationServerHost"); + + if (!string.IsNullOrEmpty(clientIdentifier)) { + var client = authorizationServerHost.GetClient(clientIdentifier); + if (client != null) { + if (!string.IsNullOrEmpty(clientSecret)) { + if (client.IsValidClientSecret(clientSecret)) { + return ClientAuthenticationResult.ClientAuthenticated; + } else { // invalid client secret + return ClientAuthenticationResult.ClientAuthenticationRejected; + } + } else { // no client secret provided + return ClientAuthenticationResult.ClientIdNotAuthenticated; + } + } else { // The client identifier is not recognized. + return ClientAuthenticationResult.ClientAuthenticationRejected; + } + } else { // no client id provided. + return ClientAuthenticationResult.NoAuthenticationRecognized; + } + } + } +} diff --git a/src/DotNetOpenAuth.OAuth2.AuthorizationServer/OAuth2/ChannelElements/ClientCredentialHttpBasicReader.cs b/src/DotNetOpenAuth.OAuth2.AuthorizationServer/OAuth2/ChannelElements/ClientCredentialHttpBasicReader.cs new file mode 100644 index 0000000..655d38f --- /dev/null +++ b/src/DotNetOpenAuth.OAuth2.AuthorizationServer/OAuth2/ChannelElements/ClientCredentialHttpBasicReader.cs @@ -0,0 +1,48 @@ +//----------------------------------------------------------------------- +// <copyright file="ClientCredentialHttpBasicReader.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OAuth2.ChannelElements { + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + using System.Web; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.OAuth2.Messages; + + /// <summary> + /// Reads client authentication information from the HTTP Authorization header via Basic authentication. + /// </summary> + public class ClientCredentialHttpBasicReader : ClientAuthenticationModule { + /// <summary> + /// Gets this module's contribution to an HTTP 401 WWW-Authenticate header so the client knows what kind of authentication this module supports. + /// </summary> + public override string AuthenticateHeader { + get { return "Basic"; } + } + + /// <summary> + /// Attempts to extract client identification/authentication information from a message. + /// </summary> + /// <param name="authorizationServerHost">The authorization server host.</param> + /// <param name="requestMessage">The incoming message.</param> + /// <param name="clientIdentifier">Receives the client identifier, if one was found.</param> + /// <returns>The level of the extracted client information.</returns> + public override ClientAuthenticationResult TryAuthenticateClient(IAuthorizationServerHost authorizationServerHost, AuthenticatedClientRequestBase requestMessage, out string clientIdentifier) { + Requires.NotNull(authorizationServerHost, "authorizationServerHost"); + Requires.NotNull(requestMessage, "requestMessage"); + + var credential = OAuthUtilities.ParseHttpBasicAuth(requestMessage.Headers); + if (credential != null) { + clientIdentifier = credential.UserName; + return TryAuthenticateClientBySecret(authorizationServerHost, credential.UserName, credential.Password); + } + + clientIdentifier = null; + return ClientAuthenticationResult.NoAuthenticationRecognized; + } + } +} diff --git a/src/DotNetOpenAuth.OAuth2.AuthorizationServer/OAuth2/ChannelElements/ClientCredentialMessagePartReader.cs b/src/DotNetOpenAuth.OAuth2.AuthorizationServer/OAuth2/ChannelElements/ClientCredentialMessagePartReader.cs new file mode 100644 index 0000000..2afd06e --- /dev/null +++ b/src/DotNetOpenAuth.OAuth2.AuthorizationServer/OAuth2/ChannelElements/ClientCredentialMessagePartReader.cs @@ -0,0 +1,34 @@ +//----------------------------------------------------------------------- +// <copyright file="ClientCredentialMessagePartReader.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OAuth2.ChannelElements { + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + using System.Web; + using DotNetOpenAuth.OAuth2.Messages; + + /// <summary> + /// Reads client authentication information from the message payload itself (POST entity as a URI-encoded parameter). + /// </summary> + public class ClientCredentialMessagePartReader : ClientAuthenticationModule { + /// <summary> + /// Attempts to extract client identification/authentication information from a message. + /// </summary> + /// <param name="authorizationServerHost">The authorization server host.</param> + /// <param name="requestMessage">The incoming message.</param> + /// <param name="clientIdentifier">Receives the client identifier, if one was found.</param> + /// <returns>The level of the extracted client information.</returns> + public override ClientAuthenticationResult TryAuthenticateClient(IAuthorizationServerHost authorizationServerHost, AuthenticatedClientRequestBase requestMessage, out string clientIdentifier) { + Requires.NotNull(authorizationServerHost, "authorizationServerHost"); + Requires.NotNull(requestMessage, "requestMessage"); + + clientIdentifier = requestMessage.ClientIdentifier; + return TryAuthenticateClientBySecret(authorizationServerHost, requestMessage.ClientIdentifier, requestMessage.ClientSecret); + } + } +} diff --git a/src/DotNetOpenAuth.OAuth2.AuthorizationServer/OAuth2/ChannelElements/MessageValidationBindingElement.cs b/src/DotNetOpenAuth.OAuth2.AuthorizationServer/OAuth2/ChannelElements/MessageValidationBindingElement.cs index 7361fb9..ac23e24 100644 --- a/src/DotNetOpenAuth.OAuth2.AuthorizationServer/OAuth2/ChannelElements/MessageValidationBindingElement.cs +++ b/src/DotNetOpenAuth.OAuth2.AuthorizationServer/OAuth2/ChannelElements/MessageValidationBindingElement.cs @@ -24,6 +24,29 @@ namespace DotNetOpenAuth.OAuth2.ChannelElements { /// </remarks> internal class MessageValidationBindingElement : AuthServerBindingElementBase { /// <summary> + /// The aggregating client authentication module. + /// </summary> + private readonly ClientAuthenticationModule clientAuthenticationModule; + + /// <summary> + /// The authorization server host that applies. + /// </summary> + private readonly IAuthorizationServerHost authorizationServer; + + /// <summary> + /// Initializes a new instance of the <see cref="MessageValidationBindingElement"/> class. + /// </summary> + /// <param name="clientAuthenticationModule">The aggregating client authentication module.</param> + /// <param name="authorizationServer">The authorization server host.</param> + internal MessageValidationBindingElement(ClientAuthenticationModule clientAuthenticationModule, IAuthorizationServerHost authorizationServer) { + Requires.NotNull(clientAuthenticationModule, "clientAuthenticationModule"); + Requires.NotNull(authorizationServer, "authorizationServer"); + + this.clientAuthenticationModule = clientAuthenticationModule; + this.authorizationServer = authorizationServer; + } + + /// <summary> /// Gets the protection commonly offered (if any) by this binding element. /// </summary> /// <remarks> @@ -79,10 +102,13 @@ namespace DotNetOpenAuth.OAuth2.ChannelElements { // Check that the client secret is correct for client authenticated messages. var clientCredentialOnly = message as AccessTokenClientCredentialsRequest; var authenticatedClientRequest = message as AuthenticatedClientRequestBase; + var accessTokenRequest = authenticatedClientRequest as AccessTokenRequestBase; // currently the only type of message. if (authenticatedClientRequest != null) { - var client = this.AuthorizationServer.GetClientOrThrow(authenticatedClientRequest.ClientIdentifier); - AuthServerUtilities.TokenEndpointVerify(client.HasNonEmptySecret, Protocol.AccessTokenRequestErrorCodes.UnauthorizedClient); // an empty secret is not allowed for client authenticated calls. - AuthServerUtilities.TokenEndpointVerify(client.IsValidClientSecret(authenticatedClientRequest.ClientSecret), Protocol.AccessTokenRequestErrorCodes.InvalidClient, AuthServerStrings.ClientSecretMismatch); + string clientIdentifier; + var result = this.clientAuthenticationModule.TryAuthenticateClient(this.authorizationServer, authenticatedClientRequest, out clientIdentifier); + AuthServerUtilities.TokenEndpointVerify(result != ClientAuthenticationResult.ClientIdNotAuthenticated, accessTokenRequest, Protocol.AccessTokenRequestErrorCodes.UnauthorizedClient); // an empty secret is not allowed for client authenticated calls. + AuthServerUtilities.TokenEndpointVerify(result == ClientAuthenticationResult.ClientAuthenticated, accessTokenRequest, Protocol.AccessTokenRequestErrorCodes.InvalidClient, this.clientAuthenticationModule, AuthServerStrings.ClientSecretMismatch); + authenticatedClientRequest.ClientIdentifier = clientIdentifier; if (clientCredentialOnly != null) { clientCredentialOnly.CredentialsValidated = true; @@ -104,12 +130,12 @@ namespace DotNetOpenAuth.OAuth2.ChannelElements { Logger.OAuth.ErrorFormat( "Resource owner password credential for user \"{0}\" rejected by authorization server host.", resourceOwnerPasswordCarrier.UserName); - throw new TokenEndpointProtocolException(Protocol.AccessTokenRequestErrorCodes.InvalidGrant, AuthServerStrings.InvalidResourceOwnerPasswordCredential); + throw new TokenEndpointProtocolException(accessTokenRequest, Protocol.AccessTokenRequestErrorCodes.InvalidGrant, AuthServerStrings.InvalidResourceOwnerPasswordCredential); } } catch (NotSupportedException) { - throw new TokenEndpointProtocolException(Protocol.AccessTokenRequestErrorCodes.UnsupportedGrantType); + throw new TokenEndpointProtocolException(accessTokenRequest, Protocol.AccessTokenRequestErrorCodes.UnsupportedGrantType); } catch (NotImplementedException) { - throw new TokenEndpointProtocolException(Protocol.AccessTokenRequestErrorCodes.UnsupportedGrantType); + throw new TokenEndpointProtocolException(accessTokenRequest, Protocol.AccessTokenRequestErrorCodes.UnsupportedGrantType); } } @@ -135,14 +161,14 @@ namespace DotNetOpenAuth.OAuth2.ChannelElements { var accessRequest = authCarrier as AccessTokenRequestBase; if (accessRequest != null) { // Make sure the client sending us this token is the client we issued the token to. - AuthServerUtilities.TokenEndpointVerify(string.Equals(accessRequest.ClientIdentifier, authCarrier.AuthorizationDescription.ClientIdentifier, StringComparison.Ordinal), Protocol.AccessTokenRequestErrorCodes.InvalidClient); + AuthServerUtilities.TokenEndpointVerify(string.Equals(accessRequest.ClientIdentifier, authCarrier.AuthorizationDescription.ClientIdentifier, StringComparison.Ordinal), accessTokenRequest, Protocol.AccessTokenRequestErrorCodes.InvalidClient); var scopedAccessRequest = accessRequest as ScopedAccessTokenRequest; if (scopedAccessRequest != null) { // Make sure the scope the client is requesting does not exceed the scope in the grant. if (!scopedAccessRequest.Scope.IsSubsetOf(authCarrier.AuthorizationDescription.Scope)) { Logger.OAuth.ErrorFormat("The requested access scope (\"{0}\") exceeds the grant scope (\"{1}\").", scopedAccessRequest.Scope, authCarrier.AuthorizationDescription.Scope); - throw new TokenEndpointProtocolException(Protocol.AccessTokenRequestErrorCodes.InvalidScope, AuthServerStrings.AccessScopeExceedsGrantScope); + throw new TokenEndpointProtocolException(accessTokenRequest, Protocol.AccessTokenRequestErrorCodes.InvalidScope, AuthServerStrings.AccessScopeExceedsGrantScope); } } } @@ -150,7 +176,7 @@ namespace DotNetOpenAuth.OAuth2.ChannelElements { // Make sure the authorization this token represents hasn't already been revoked. if (!this.AuthorizationServer.IsAuthorizationValid(authCarrier.AuthorizationDescription)) { Logger.OAuth.Error("Rejecting access token request because the IAuthorizationServerHost.IsAuthorizationValid method returned false."); - throw new TokenEndpointProtocolException(Protocol.AccessTokenRequestErrorCodes.InvalidGrant); + throw new TokenEndpointProtocolException(accessTokenRequest, Protocol.AccessTokenRequestErrorCodes.InvalidGrant); } applied = true; diff --git a/src/DotNetOpenAuth.OAuth2.AuthorizationServer/OAuth2/ChannelElements/OAuth2AuthorizationServerChannel.cs b/src/DotNetOpenAuth.OAuth2.AuthorizationServer/OAuth2/ChannelElements/OAuth2AuthorizationServerChannel.cs index bd154bc..53dfb54 100644 --- a/src/DotNetOpenAuth.OAuth2.AuthorizationServer/OAuth2/ChannelElements/OAuth2AuthorizationServerChannel.cs +++ b/src/DotNetOpenAuth.OAuth2.AuthorizationServer/OAuth2/ChannelElements/OAuth2AuthorizationServerChannel.cs @@ -35,8 +35,9 @@ namespace DotNetOpenAuth.OAuth2.ChannelElements { /// Initializes a new instance of the <see cref="OAuth2AuthorizationServerChannel"/> class. /// </summary> /// <param name="authorizationServer">The authorization server.</param> - protected internal OAuth2AuthorizationServerChannel(IAuthorizationServerHost authorizationServer) - : base(MessageTypes, InitializeBindingElements(authorizationServer)) { + /// <param name="clientAuthenticationModule">The aggregating client authentication module.</param> + protected internal OAuth2AuthorizationServerChannel(IAuthorizationServerHost authorizationServer, ClientAuthenticationModule clientAuthenticationModule) + : base(MessageTypes, InitializeBindingElements(authorizationServer, clientAuthenticationModule)) { Requires.NotNull(authorizationServer, "authorizationServer"); this.AuthorizationServer = authorizationServer; } @@ -106,15 +107,18 @@ namespace DotNetOpenAuth.OAuth2.ChannelElements { /// Initializes the binding elements for the OAuth channel. /// </summary> /// <param name="authorizationServer">The authorization server.</param> + /// <param name="clientAuthenticationModule">The aggregating client authentication module.</param> /// <returns> /// An array of binding elements used to initialize the channel. /// </returns> - private static IChannelBindingElement[] InitializeBindingElements(IAuthorizationServerHost authorizationServer) { + private static IChannelBindingElement[] InitializeBindingElements(IAuthorizationServerHost authorizationServer, ClientAuthenticationModule clientAuthenticationModule) { Requires.NotNull(authorizationServer, "authorizationServer"); + Requires.NotNull(clientAuthenticationModule, "clientAuthenticationModule"); + var bindingElements = new List<IChannelBindingElement>(); // The order they are provided is used for outgoing messgaes, and reversed for incoming messages. - bindingElements.Add(new MessageValidationBindingElement()); + bindingElements.Add(new MessageValidationBindingElement(clientAuthenticationModule, authorizationServer)); bindingElements.Add(new TokenCodeSerializationBindingElement()); return bindingElements.ToArray(); diff --git a/src/DotNetOpenAuth.OAuth2.Client/Configuration/OAuth2ClientSection.cs b/src/DotNetOpenAuth.OAuth2.Client/Configuration/OAuth2ClientSection.cs new file mode 100644 index 0000000..1ee5aa5 --- /dev/null +++ b/src/DotNetOpenAuth.OAuth2.Client/Configuration/OAuth2ClientSection.cs @@ -0,0 +1,36 @@ +//----------------------------------------------------------------------- +// <copyright file="OAuth2ClientSection.cs" company="Outercurve Foundation"> +// Copyright (c) Outercurve Foundation. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.Configuration { + using System.Configuration; + using System.Diagnostics.Contracts; + + /// <summary> + /// Represents the <oauth2/client> section in the host's .config file. + /// </summary> + internal class OAuth2ClientSection : ConfigurationSection { + /// <summary> + /// The name of the oauth2/client section. + /// </summary> + private const string SectionName = OAuth2SectionGroup.SectionName + "/client"; + + /// <summary> + /// Initializes a new instance of the <see cref="OAuth2ClientSection"/> class. + /// </summary> + internal OAuth2ClientSection() { + } + + /// <summary> + /// Gets the configuration section from the .config file. + /// </summary> + internal static OAuth2ClientSection Configuration { + get { + Contract.Ensures(Contract.Result<OAuth2ClientSection>() != null); + return (OAuth2ClientSection)ConfigurationManager.GetSection(SectionName) ?? new OAuth2ClientSection(); + } + } + } +} diff --git a/src/DotNetOpenAuth.OAuth2.Client/DotNetOpenAuth.OAuth2.Client.csproj b/src/DotNetOpenAuth.OAuth2.Client/DotNetOpenAuth.OAuth2.Client.csproj index 662bf86..e72ee1a 100644 --- a/src/DotNetOpenAuth.OAuth2.Client/DotNetOpenAuth.OAuth2.Client.csproj +++ b/src/DotNetOpenAuth.OAuth2.Client/DotNetOpenAuth.OAuth2.Client.csproj @@ -18,9 +18,12 @@ <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' "> </PropertyGroup> <ItemGroup> + <Compile Include="Configuration\OAuth2ClientSection.cs" /> <Compile Include="OAuth2\AuthorizationServerDescription.cs" /> <Compile Include="OAuth2\AuthorizationState.cs" /> + <Compile Include="OAuth2\ChannelElements\IOAuth2ChannelWithClient.cs" /> <Compile Include="OAuth2\ChannelElements\OAuth2ClientChannel.cs" /> + <Compile Include="OAuth2\ClientCredentialApplicator.cs" /> <Compile Include="OAuth2\IAuthorizationState.cs" /> <Compile Include="OAuth2\IClientAuthorizationTracker.cs" /> <Compile Include="OAuth2\Messages\AccessTokenAuthorizationCodeRequestC.cs" /> @@ -60,4 +63,4 @@ <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> +</Project>
\ No newline at end of file diff --git a/src/DotNetOpenAuth.OAuth2.Client/OAuth2/ChannelElements/IOAuth2ChannelWithClient.cs b/src/DotNetOpenAuth.OAuth2.Client/OAuth2/ChannelElements/IOAuth2ChannelWithClient.cs new file mode 100644 index 0000000..c802be6 --- /dev/null +++ b/src/DotNetOpenAuth.OAuth2.Client/OAuth2/ChannelElements/IOAuth2ChannelWithClient.cs @@ -0,0 +1,27 @@ +//----------------------------------------------------------------------- +// <copyright file="IOAuth2ChannelWithClient.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OAuth2.ChannelElements { + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + + /// <summary> + /// An interface that defines the OAuth2 client specific channel additions. + /// </summary> + internal interface IOAuth2ChannelWithClient { + /// <summary> + /// Gets or sets the identifier by which this client is known to the Authorization Server. + /// </summary> + string ClientIdentifier { get; set; } + + /// <summary> + /// Gets or sets the client credentials applicator extension to use. + /// </summary> + ClientCredentialApplicator ClientCredentialApplicator { get; set; } + } +} diff --git a/src/DotNetOpenAuth.OAuth2.Client/OAuth2/ChannelElements/OAuth2ClientChannel.cs b/src/DotNetOpenAuth.OAuth2.Client/OAuth2/ChannelElements/OAuth2ClientChannel.cs index 54fc110..8ad2ed9 100644 --- a/src/DotNetOpenAuth.OAuth2.Client/OAuth2/ChannelElements/OAuth2ClientChannel.cs +++ b/src/DotNetOpenAuth.OAuth2.Client/OAuth2/ChannelElements/OAuth2ClientChannel.cs @@ -18,7 +18,7 @@ namespace DotNetOpenAuth.OAuth2.ChannelElements { /// <summary> /// The messaging channel used by OAuth 2.0 Clients. /// </summary> - internal class OAuth2ClientChannel : OAuth2ChannelBase { + internal class OAuth2ClientChannel : OAuth2ChannelBase, IOAuth2ChannelWithClient { /// <summary> /// The messages receivable by this channel. /// </summary> @@ -34,10 +34,22 @@ namespace DotNetOpenAuth.OAuth2.ChannelElements { /// <summary> /// Initializes a new instance of the <see cref="OAuth2ClientChannel"/> class. /// </summary> - internal OAuth2ClientChannel() : base(MessageTypes) { + internal OAuth2ClientChannel() + : base(MessageTypes) { } /// <summary> + /// Gets or sets the identifier by which this client is known to the Authorization Server. + /// </summary> + public string ClientIdentifier { get; set; } + + /// <summary> + /// Gets or sets the tool to use to apply client credentials to authenticated requests to the Authorization Server. + /// </summary> + /// <value>May be <c>null</c> if this client has no client secret.</value> + public ClientCredentialApplicator ClientCredentialApplicator { get; set; } + + /// <summary> /// Prepares an HTTP request that carries a given message. /// </summary> /// <param name="request">The message to send.</param> @@ -131,5 +143,17 @@ namespace DotNetOpenAuth.OAuth2.ChannelElements { // Clients don't ever send direct responses. throw new NotImplementedException(); } + + /// <summary> + /// Performs additional processing on an outgoing web request before it is sent to the remote server. + /// </summary> + /// <param name="request">The request.</param> + protected override void PrepareHttpWebRequest(HttpWebRequest request) { + base.PrepareHttpWebRequest(request); + + if (this.ClientCredentialApplicator != null) { + this.ClientCredentialApplicator.ApplyClientCredential(this.ClientIdentifier, request); + } + } } } diff --git a/src/DotNetOpenAuth.OAuth2.Client/OAuth2/ClientBase.cs b/src/DotNetOpenAuth.OAuth2.Client/OAuth2/ClientBase.cs index 77b9cc6..5f377ae 100644 --- a/src/DotNetOpenAuth.OAuth2.Client/OAuth2/ClientBase.cs +++ b/src/DotNetOpenAuth.OAuth2.Client/OAuth2/ClientBase.cs @@ -26,13 +26,16 @@ namespace DotNetOpenAuth.OAuth2 { /// </summary> /// <param name="authorizationServer">The token issuer.</param> /// <param name="clientIdentifier">The client identifier.</param> - /// <param name="clientSecret">The client secret.</param> - protected ClientBase(AuthorizationServerDescription authorizationServer, string clientIdentifier = null, string clientSecret = null) { + /// <param name="clientCredentialApplicator"> + /// The tool to use to apply client credentials to authenticated requests to the Authorization Server. + /// May be <c>null</c> for clients with no secret or other means of authentication. + /// </param> + protected ClientBase(AuthorizationServerDescription authorizationServer, string clientIdentifier = null, ClientCredentialApplicator clientCredentialApplicator = null) { Requires.NotNull(authorizationServer, "authorizationServer"); this.AuthorizationServer = authorizationServer; this.Channel = new OAuth2ClientChannel(); this.ClientIdentifier = clientIdentifier; - this.ClientSecret = clientSecret; + this.ClientCredentialApplicator = clientCredentialApplicator; } /// <summary> @@ -50,12 +53,26 @@ namespace DotNetOpenAuth.OAuth2 { /// <summary> /// Gets or sets the identifier by which this client is known to the Authorization Server. /// </summary> - public string ClientIdentifier { get; set; } + public string ClientIdentifier { + get { return this.OAuthChannel.ClientIdentifier; } + set { this.OAuthChannel.ClientIdentifier = value; } + } + + /// <summary> + /// Gets or sets the tool to use to apply client credentials to authenticated requests to the Authorization Server. + /// </summary> + /// <value>May be <c>null</c> if this client has no client secret.</value> + public ClientCredentialApplicator ClientCredentialApplicator { + get { return this.OAuthChannel.ClientCredentialApplicator; } + set { this.OAuthChannel.ClientCredentialApplicator = value; } + } /// <summary> - /// Gets or sets the client secret shared with the Authorization Server. + /// Gets the OAuth client channel. /// </summary> - public string ClientSecret { get; set; } + internal IOAuth2ChannelWithClient OAuthChannel { + get { return (IOAuth2ChannelWithClient)this.Channel; } + } /// <summary> /// Adds the necessary HTTP Authorization header to an HTTP request for protected resources @@ -118,10 +135,11 @@ namespace DotNetOpenAuth.OAuth2 { var request = new AccessTokenRefreshRequestC(this.AuthorizationServer) { ClientIdentifier = this.ClientIdentifier, - ClientSecret = this.ClientSecret, RefreshToken = authorization.RefreshToken, }; + this.ApplyClientCredential(request); + var response = this.Channel.Request<AccessTokenSuccessResponse>(request); UpdateAuthorizationWithResponse(authorization, response); return true; @@ -145,10 +163,11 @@ namespace DotNetOpenAuth.OAuth2 { var request = new AccessTokenRefreshRequestC(this.AuthorizationServer) { ClientIdentifier = this.ClientIdentifier, - ClientSecret = this.ClientSecret, RefreshToken = refreshToken, }; + this.ApplyClientCredential(request); + var response = this.Channel.Request<AccessTokenSuccessResponse>(request); var authorization = new AuthorizationState(); UpdateAuthorizationWithResponse(authorization, response); @@ -250,10 +269,10 @@ namespace DotNetOpenAuth.OAuth2 { var accessTokenRequest = new AccessTokenAuthorizationCodeRequestC(this.AuthorizationServer) { ClientIdentifier = this.ClientIdentifier, - ClientSecret = this.ClientSecret, Callback = authorizationState.Callback, AuthorizationCode = authorizationSuccess.AuthorizationCode, }; + this.ApplyClientCredential(accessTokenRequest); IProtocolMessage accessTokenResponse = this.Channel.Request(accessTokenRequest); var accessTokenSuccess = accessTokenResponse as AccessTokenSuccessResponse; var failedAccessTokenResponse = accessTokenResponse as AccessTokenFailedResponse; @@ -267,6 +286,27 @@ namespace DotNetOpenAuth.OAuth2 { } /// <summary> + /// Applies the default client authentication mechanism given a client secret. + /// </summary> + /// <param name="secret">The client secret. May be <c>null</c></param> + /// <returns>The client credential applicator.</returns> + protected static ClientCredentialApplicator DefaultSecretApplicator(string secret) { + return secret == null ? ClientCredentialApplicator.NoSecret() : ClientCredentialApplicator.NetworkCredential(secret); + } + + /// <summary> + /// Applies any applicable client credential to an authenticated outbound request to the authorization server. + /// </summary> + /// <param name="request">The request to apply authentication information to.</param> + protected void ApplyClientCredential(AuthenticatedClientRequestBase request) { + Requires.NotNull(request, "request"); + + if (this.ClientCredentialApplicator != null) { + this.ClientCredentialApplicator.ApplyClientCredential(this.ClientIdentifier, request); + } + } + + /// <summary> /// Calculates the fraction of life remaining in an access token. /// </summary> /// <param name="authorization">The authorization to measure.</param> @@ -295,7 +335,7 @@ namespace DotNetOpenAuth.OAuth2 { var authorizationState = new AuthorizationState(scopes); request.ClientIdentifier = this.ClientIdentifier; - request.ClientSecret = this.ClientSecret; + this.ApplyClientCredential(request); request.Scope.UnionWith(authorizationState.Scope); var response = this.Channel.Request(request); diff --git a/src/DotNetOpenAuth.OAuth2.Client/OAuth2/ClientCredentialApplicator.cs b/src/DotNetOpenAuth.OAuth2.Client/OAuth2/ClientCredentialApplicator.cs new file mode 100644 index 0000000..2a9a8a7 --- /dev/null +++ b/src/DotNetOpenAuth.OAuth2.Client/OAuth2/ClientCredentialApplicator.cs @@ -0,0 +1,165 @@ +//----------------------------------------------------------------------- +// <copyright file="ClientCredentialApplicator.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OAuth2 { + using System; + using System.Collections.Generic; + using System.Linq; + using System.Net; + using System.Text; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.OAuth2.Messages; + + /// <summary> + /// A base class for extensions that apply client authentication to messages for the authorization server in specific ways. + /// </summary> + public abstract class ClientCredentialApplicator { + /// <summary> + /// Initializes a new instance of the <see cref="ClientCredentialApplicator"/> class. + /// </summary> + protected ClientCredentialApplicator() { + } + + /// <summary> + /// Transmits the secret the client shares with the authorization server as a parameter in the POST entity payload. + /// </summary> + /// <param name="clientSecret">The secret the client shares with the authorization server.</param> + /// <returns>The credential applicator to provide to the <see cref="ClientBase"/> instance.</returns> + public static ClientCredentialApplicator PostParameter(string clientSecret) { + Requires.NotNullOrEmpty(clientSecret, "clientSecret"); + return new PostParameterApplicator(clientSecret); + } + + /// <summary> + /// Transmits the client identifier and secret in the HTTP Authorization header via HTTP Basic authentication. + /// </summary> + /// <param name="credential">The client id and secret.</param> + /// <returns>The credential applicator to provide to the <see cref="ClientBase"/> instance.</returns> + public static ClientCredentialApplicator NetworkCredential(NetworkCredential credential) { + Requires.NotNull(credential, "credential"); + return new NetworkCredentialApplicator(credential); + } + + /// <summary> + /// Transmits the client identifier and secret in the HTTP Authorization header via HTTP Basic authentication. + /// </summary> + /// <param name="clientSecret">The secret the client shares with the authorization server.</param> + /// <returns>The credential applicator to provide to the <see cref="ClientBase"/> instance.</returns> + public static ClientCredentialApplicator NetworkCredential(string clientSecret) { + Requires.NotNullOrEmpty(clientSecret, "clientSecret"); + return new NetworkCredentialApplicator(clientSecret); + } + + /// <summary> + /// Never transmits a secret. Useful for anonymous clients or clients unable to keep a secret. + /// </summary> + /// <returns>The credential applicator to provide to the <see cref="ClientBase"/> instance.</returns> + public static ClientCredentialApplicator NoSecret() { + return null; + } + + /// <summary> + /// Applies the client identifier and (when applicable) the client authentication to an outbound message. + /// </summary> + /// <param name="clientIdentifier">The identifier by which the authorization server should recognize this client.</param> + /// <param name="request">The outbound message to apply authentication information to.</param> + public virtual void ApplyClientCredential(string clientIdentifier, AuthenticatedClientRequestBase request) { + } + + /// <summary> + /// Applies the client identifier and (when applicable) the client authentication to an outbound message. + /// </summary> + /// <param name="clientIdentifier">The identifier by which the authorization server should recognize this client.</param> + /// <param name="request">The outbound message to apply authentication information to.</param> + public virtual void ApplyClientCredential(string clientIdentifier, HttpWebRequest request) { + } + + /// <summary> + /// Authenticates the client via HTTP Basic. + /// </summary> + private class NetworkCredentialApplicator : ClientCredentialApplicator { + /// <summary> + /// The client identifier and secret. + /// </summary> + private readonly NetworkCredential credential; + + /// <summary> + /// The client secret. + /// </summary> + private readonly string clientSecret; + + /// <summary> + /// Initializes a new instance of the <see cref="NetworkCredentialApplicator"/> class. + /// </summary> + /// <param name="clientSecret">The client secret.</param> + internal NetworkCredentialApplicator(string clientSecret) { + Requires.NotNullOrEmpty(clientSecret, "clientSecret"); + this.clientSecret = clientSecret; + } + + /// <summary> + /// Initializes a new instance of the <see cref="NetworkCredentialApplicator"/> class. + /// </summary> + /// <param name="credential">The client credential.</param> + internal NetworkCredentialApplicator(NetworkCredential credential) { + Requires.NotNull(credential, "credential"); + this.credential = credential; + } + + /// <summary> + /// Applies the client identifier and (when applicable) the client authentication to an outbound message. + /// </summary> + /// <param name="clientIdentifier">The identifier by which the authorization server should recognize this client.</param> + /// <param name="request">The outbound message to apply authentication information to.</param> + public override void ApplyClientCredential(string clientIdentifier, AuthenticatedClientRequestBase request) { + // When using network credentials, the client authentication is not done as standard message parts. + request.ClientIdentifier = null; + request.ClientSecret = null; + } + + /// <summary> + /// Applies the client identifier and (when applicable) the client authentication to an outbound message. + /// </summary> + /// <param name="clientIdentifier">The identifier by which the authorization server should recognize this client.</param> + /// <param name="request">The outbound message to apply authentication information to.</param> + public override void ApplyClientCredential(string clientIdentifier, HttpWebRequest request) { + if (this.credential != null && this.credential.UserName == clientIdentifier) { + ErrorUtilities.VerifyHost(false, "Client identifiers \"{0}\" and \"{1}\" do not match.", this.credential.UserName, clientIdentifier); + } + + request.Credentials = this.credential ?? new NetworkCredential(clientIdentifier, this.clientSecret); + } + } + + /// <summary> + /// Authenticates the client via a client_secret parameter in the message. + /// </summary> + private class PostParameterApplicator : ClientCredentialApplicator { + /// <summary> + /// The client secret. + /// </summary> + private readonly string secret; + + /// <summary> + /// Initializes a new instance of the <see cref="PostParameterApplicator"/> class. + /// </summary> + /// <param name="clientSecret">The client secret.</param> + internal PostParameterApplicator(string clientSecret) { + Requires.NotNullOrEmpty(clientSecret, "clientSecret"); + this.secret = clientSecret; + } + + /// <summary> + /// Applies the client identifier and (when applicable) the client authentication to an outbound message. + /// </summary> + /// <param name="clientIdentifier">The identifier by which the authorization server should recognize this client.</param> + /// <param name="request">The outbound message to apply authentication information to.</param> + public override void ApplyClientCredential(string clientIdentifier, AuthenticatedClientRequestBase request) { + request.ClientSecret = this.secret; + } + } + } +} diff --git a/src/DotNetOpenAuth.OAuth2.Client/OAuth2/UserAgentClient.cs b/src/DotNetOpenAuth.OAuth2.Client/OAuth2/UserAgentClient.cs index 9834985..edde2a9 100644 --- a/src/DotNetOpenAuth.OAuth2.Client/OAuth2/UserAgentClient.cs +++ b/src/DotNetOpenAuth.OAuth2.Client/OAuth2/UserAgentClient.cs @@ -27,7 +27,7 @@ namespace DotNetOpenAuth.OAuth2 { /// <param name="clientIdentifier">The client identifier.</param> /// <param name="clientSecret">The client secret.</param> public UserAgentClient(AuthorizationServerDescription authorizationServer, string clientIdentifier = null, string clientSecret = null) - : base(authorizationServer, clientIdentifier, clientSecret) { + : this(authorizationServer, clientIdentifier, DefaultSecretApplicator(clientSecret)) { } /// <summary> @@ -38,12 +38,39 @@ namespace DotNetOpenAuth.OAuth2 { /// <param name="clientIdentifier">The client identifier.</param> /// <param name="clientSecret">The client secret.</param> public UserAgentClient(Uri authorizationEndpoint, Uri tokenEndpoint, string clientIdentifier = null, string clientSecret = null) - : this(new AuthorizationServerDescription { AuthorizationEndpoint = authorizationEndpoint, TokenEndpoint = tokenEndpoint }, clientIdentifier, clientSecret) { + : this(authorizationEndpoint, tokenEndpoint, clientIdentifier, DefaultSecretApplicator(clientSecret)) { + } + + /// <summary> + /// Initializes a new instance of the <see cref="UserAgentClient"/> class. + /// </summary> + /// <param name="authorizationEndpoint">The authorization endpoint.</param> + /// <param name="tokenEndpoint">The token endpoint.</param> + /// <param name="clientIdentifier">The client identifier.</param> + /// <param name="clientCredentialApplicator"> + /// The tool to use to apply client credentials to authenticated requests to the Authorization Server. + /// May be <c>null</c> for clients with no secret or other means of authentication. + /// </param> + public UserAgentClient(Uri authorizationEndpoint, Uri tokenEndpoint, string clientIdentifier, ClientCredentialApplicator clientCredentialApplicator) + : this(new AuthorizationServerDescription { AuthorizationEndpoint = authorizationEndpoint, TokenEndpoint = tokenEndpoint }, clientIdentifier, clientCredentialApplicator) { Requires.NotNull(authorizationEndpoint, "authorizationEndpoint"); Requires.NotNull(tokenEndpoint, "tokenEndpoint"); } /// <summary> + /// Initializes a new instance of the <see cref="UserAgentClient"/> class. + /// </summary> + /// <param name="authorizationServer">The token issuer.</param> + /// <param name="clientIdentifier">The client identifier.</param> + /// <param name="clientCredentialApplicator"> + /// The tool to use to apply client credentials to authenticated requests to the Authorization Server. + /// May be <c>null</c> for clients with no secret or other means of authentication. + /// </param> + public UserAgentClient(AuthorizationServerDescription authorizationServer, string clientIdentifier, ClientCredentialApplicator clientCredentialApplicator) + : base(authorizationServer, clientIdentifier, clientCredentialApplicator) { + } + + /// <summary> /// Generates a URL that the user's browser can be directed to in order to authorize /// this client to access protected data at some resource server. /// </summary> diff --git a/src/DotNetOpenAuth.OAuth2.Client/OAuth2/WebServerClient.cs b/src/DotNetOpenAuth.OAuth2.Client/OAuth2/WebServerClient.cs index 5a86bef..c19757f 100644 --- a/src/DotNetOpenAuth.OAuth2.Client/OAuth2/WebServerClient.cs +++ b/src/DotNetOpenAuth.OAuth2.Client/OAuth2/WebServerClient.cs @@ -26,7 +26,20 @@ namespace DotNetOpenAuth.OAuth2 { /// <param name="clientIdentifier">The client identifier.</param> /// <param name="clientSecret">The client secret.</param> public WebServerClient(AuthorizationServerDescription authorizationServer, string clientIdentifier = null, string clientSecret = null) - : base(authorizationServer, clientIdentifier, clientSecret) { + : this(authorizationServer, clientIdentifier, DefaultSecretApplicator(clientSecret)) { + } + + /// <summary> + /// Initializes a new instance of the <see cref="WebServerClient"/> class. + /// </summary> + /// <param name="authorizationServer">The authorization server.</param> + /// <param name="clientIdentifier">The client identifier.</param> + /// <param name="clientCredentialApplicator"> + /// The tool to use to apply client credentials to authenticated requests to the Authorization Server. + /// May be <c>null</c> for clients with no secret or other means of authentication. + /// </param> + public WebServerClient(AuthorizationServerDescription authorizationServer, string clientIdentifier, ClientCredentialApplicator clientCredentialApplicator) + : base(authorizationServer, clientIdentifier, clientCredentialApplicator) { } /// <summary> @@ -106,7 +119,7 @@ namespace DotNetOpenAuth.OAuth2 { /// <returns>The authorization state that contains the details of the authorization.</returns> public IAuthorizationState ProcessUserAuthorization(HttpRequestBase request = null) { Requires.ValidState(!string.IsNullOrEmpty(this.ClientIdentifier), ClientStrings.RequiredPropertyNotYetPreset, "ClientIdentifier"); - Requires.ValidState(!string.IsNullOrEmpty(this.ClientSecret), ClientStrings.RequiredPropertyNotYetPreset, "ClientSecret"); + Requires.ValidState(this.ClientCredentialApplicator != null, ClientStrings.RequiredPropertyNotYetPreset, "ClientCredentialApplicator"); if (request == null) { request = this.Channel.GetRequestFromContext(); diff --git a/src/DotNetOpenAuth.OAuth2.ClientAuthorization/OAuth2/Messages/AccessTokenFailedResponse.cs b/src/DotNetOpenAuth.OAuth2.ClientAuthorization/OAuth2/Messages/AccessTokenFailedResponse.cs index 8c4b1c3..4aaf928 100644 --- a/src/DotNetOpenAuth.OAuth2.ClientAuthorization/OAuth2/Messages/AccessTokenFailedResponse.cs +++ b/src/DotNetOpenAuth.OAuth2.ClientAuthorization/OAuth2/Messages/AccessTokenFailedResponse.cs @@ -25,6 +25,11 @@ namespace DotNetOpenAuth.OAuth2.Messages { private readonly bool invalidClientCredentialsInAuthorizationHeader; /// <summary> + /// The headers to include in the response. + /// </summary> + private readonly WebHeaderCollection headers = new WebHeaderCollection(); + + /// <summary> /// Initializes a new instance of the <see cref="AccessTokenFailedResponse"/> class. /// </summary> /// <param name="request">The faulty request.</param> @@ -63,8 +68,8 @@ namespace DotNetOpenAuth.OAuth2.Messages { /// Gets the HTTP headers to add to the response. /// </summary> /// <value>May be an empty collection, but must not be <c>null</c>.</value> - WebHeaderCollection IHttpDirectResponse.Headers { - get { return new WebHeaderCollection(); } + public WebHeaderCollection Headers { + get { return this.headers; } } #endregion diff --git a/src/DotNetOpenAuth.OAuth2.ClientAuthorization/OAuth2/Messages/AuthenticatedClientRequestBase.cs b/src/DotNetOpenAuth.OAuth2.ClientAuthorization/OAuth2/Messages/AuthenticatedClientRequestBase.cs index bc4d0ca..4631d83 100644 --- a/src/DotNetOpenAuth.OAuth2.ClientAuthorization/OAuth2/Messages/AuthenticatedClientRequestBase.cs +++ b/src/DotNetOpenAuth.OAuth2.ClientAuthorization/OAuth2/Messages/AuthenticatedClientRequestBase.cs @@ -6,13 +6,18 @@ namespace DotNetOpenAuth.OAuth2.Messages { using System; - + using System.Net; using DotNetOpenAuth.Messaging; /// <summary> /// A direct message from the client to the authorization server that includes the client's credentials. /// </summary> - public abstract class AuthenticatedClientRequestBase : MessageBase { + public abstract class AuthenticatedClientRequestBase : MessageBase, IHttpDirectRequest { + /// <summary> + /// The backing for the <see cref="Headers"/> property. + /// </summary> + private readonly WebHeaderCollection headers = new WebHeaderCollection(); + /// <summary> /// Initializes a new instance of the <see cref="AuthenticatedClientRequestBase"/> class. /// </summary> @@ -26,7 +31,10 @@ namespace DotNetOpenAuth.OAuth2.Messages { /// Gets the client identifier previously obtained from the Authorization Server. /// </summary> /// <value>The client identifier.</value> - [MessagePart(Protocol.client_id, IsRequired = true)] + /// <remarks> + /// Not required, because the client id may be communicate through alternate means like HTTP Basic authentication (the OAuth 2 spec allows a lot of freedom here). + /// </remarks> + [MessagePart(Protocol.client_id, IsRequired = false)] public string ClientIdentifier { get; internal set; } /// <summary> @@ -38,5 +46,13 @@ namespace DotNetOpenAuth.OAuth2.Messages { /// </remarks> [MessagePart(Protocol.client_secret, IsRequired = false)] public string ClientSecret { get; internal set; } + + /// <summary> + /// Gets the HTTP headers of the request. + /// </summary> + /// <value>May be an empty collection, but must not be <c>null</c>.</value> + public WebHeaderCollection Headers { + get { return this.headers; } + } } }
\ No newline at end of file diff --git a/src/DotNetOpenAuth.OAuth2.ClientAuthorization/OAuth2/TokenEndpointProtocolException.cs b/src/DotNetOpenAuth.OAuth2.ClientAuthorization/OAuth2/TokenEndpointProtocolException.cs index 308bfe2..e86c27e 100644 --- a/src/DotNetOpenAuth.OAuth2.ClientAuthorization/OAuth2/TokenEndpointProtocolException.cs +++ b/src/DotNetOpenAuth.OAuth2.ClientAuthorization/OAuth2/TokenEndpointProtocolException.cs @@ -12,24 +12,40 @@ namespace DotNetOpenAuth.OAuth2 { using System.Text; using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.OAuth2.Messages; /// <summary> /// Describes an error generated by an Authorization Server's token endpoint. /// </summary> public class TokenEndpointProtocolException : ProtocolException { /// <summary> + /// The message being processed that caused this exception to be thrown. + /// </summary> + private readonly AccessTokenRequestBase requestMessage; + + /// <summary> + /// The WWW-Authenticate header to add to the response message. + /// </summary> + private readonly string authenticateHeader; + + /// <summary> /// Initializes a new instance of the <see cref="TokenEndpointProtocolException"/> class. /// </summary> + /// <param name="requestMessage">The message whose processing resulted in this error.</param> /// <param name="error">A single error code from <see cref="Protocol.AccessTokenRequestErrorCodes"/>.</param> /// <param name="description">A human-readable UTF-8 encoded text providing additional information, used to assist the client developer in understanding the error that occurred.</param> /// <param name="moreInformation">A URI identifying a human-readable web page with information about the error, used to provide the client developer with additional information about the error.</param> - public TokenEndpointProtocolException(string error, string description = null, Uri moreInformation = null) + /// <param name="authenticateHeader">The WWW-Authenticate header to add to the response.</param> + public TokenEndpointProtocolException(AccessTokenRequestBase requestMessage, string error, string description = null, Uri moreInformation = null, string authenticateHeader = null) : base(string.Format(CultureInfo.CurrentCulture, ClientAuthorizationStrings.TokenEndpointErrorFormat, error, description)) { + Requires.NotNull(requestMessage, "requestMessage"); Requires.NotNullOrEmpty(error, "error"); + this.requestMessage = requestMessage; this.Error = error; this.Description = description; this.MoreInformation = moreInformation; + this.authenticateHeader = authenticateHeader; } /// <summary> @@ -55,5 +71,23 @@ namespace DotNetOpenAuth.OAuth2 { /// Gets a URI identifying a human-readable web page with information about the error, used to provide the client developer with additional information about the error. /// </summary> public Uri MoreInformation { get; private set; } + + /// <summary> + /// Gets the response message to send to the client. + /// </summary> + /// <returns>A message.</returns> + public IDirectResponseProtocolMessage GetResponse() { + var response = this.requestMessage != null + ? new AccessTokenFailedResponse(this.requestMessage, this.authenticateHeader != null) + : new AccessTokenFailedResponse(); + response.Error = this.Error; + response.ErrorDescription = this.Description; + response.ErrorUri = this.MoreInformation; + if (this.authenticateHeader != null) { + response.Headers.Add(HttpRequestHeaders.WwwAuthenticate, this.authenticateHeader); + } + + return response; + } } } diff --git a/src/DotNetOpenAuth.OAuth2.ResourceServer/Configuration/OAuth2ResourceServerSection.cs b/src/DotNetOpenAuth.OAuth2.ResourceServer/Configuration/OAuth2ResourceServerSection.cs new file mode 100644 index 0000000..3e37018 --- /dev/null +++ b/src/DotNetOpenAuth.OAuth2.ResourceServer/Configuration/OAuth2ResourceServerSection.cs @@ -0,0 +1,36 @@ +//----------------------------------------------------------------------- +// <copyright file="OAuth2ResourceServerSection.cs" company="Outercurve Foundation"> +// Copyright (c) Outercurve Foundation. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.Configuration { + using System.Configuration; + using System.Diagnostics.Contracts; + + /// <summary> + /// Represents the <oauth2/resourceServer> section in the host's .config file. + /// </summary> + internal class OAuth2ResourceServerSection : ConfigurationElement { + /// <summary> + /// The name of the oauth2/client section. + /// </summary> + private const string SectionName = OAuth2SectionGroup.SectionName + "/resourceServer"; + + /// <summary> + /// Initializes a new instance of the <see cref="OAuth2ResourceServerSection"/> class. + /// </summary> + internal OAuth2ResourceServerSection() { + } + + /// <summary> + /// Gets the configuration section from the .config file. + /// </summary> + internal static OAuth2ResourceServerSection Configuration { + get { + Contract.Ensures(Contract.Result<OAuth2ResourceServerSection>() != null); + return (OAuth2ResourceServerSection)ConfigurationManager.GetSection(SectionName) ?? new OAuth2ResourceServerSection(); + } + } + } +} diff --git a/src/DotNetOpenAuth.OAuth2.ResourceServer/DotNetOpenAuth.OAuth2.ResourceServer.csproj b/src/DotNetOpenAuth.OAuth2.ResourceServer/DotNetOpenAuth.OAuth2.ResourceServer.csproj index 3aa92f7..eb54fee 100644 --- a/src/DotNetOpenAuth.OAuth2.ResourceServer/DotNetOpenAuth.OAuth2.ResourceServer.csproj +++ b/src/DotNetOpenAuth.OAuth2.ResourceServer/DotNetOpenAuth.OAuth2.ResourceServer.csproj @@ -18,6 +18,7 @@ <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' "> </PropertyGroup> <ItemGroup> + <Compile Include="Configuration\OAuth2ResourceServerSection.cs" /> <Compile Include="OAuth2\ChannelElements\OAuth2ResourceServerChannel.cs" /> <Compile Include="OAuth2\IAccessTokenAnalyzer.cs" /> <Compile Include="OAuth2\ResourceServerStrings.Designer.cs"> @@ -52,4 +53,4 @@ <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> +</Project>
\ No newline at end of file diff --git a/src/DotNetOpenAuth.OAuth2/Configuration/OAuth2SectionGroup.cs b/src/DotNetOpenAuth.OAuth2/Configuration/OAuth2SectionGroup.cs new file mode 100644 index 0000000..112e756 --- /dev/null +++ b/src/DotNetOpenAuth.OAuth2/Configuration/OAuth2SectionGroup.cs @@ -0,0 +1,26 @@ +//----------------------------------------------------------------------- +// <copyright file="OAuth2SectionGroup.cs" company="Outercurve Foundation"> +// Copyright (c) Outercurve Foundation. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.Configuration { + using System.Configuration; + using System.Diagnostics.Contracts; + + /// <summary> + /// Represents the <oauth> element in the host's .config file. + /// </summary> + internal class OAuth2SectionGroup : ConfigurationSectionGroup { + /// <summary> + /// The name of the oauth section. + /// </summary> + internal const string SectionName = DotNetOpenAuthSection.SectionName + "/oauth2"; + + /// <summary> + /// Initializes a new instance of the <see cref="OAuth2SectionGroup"/> class. + /// </summary> + internal OAuth2SectionGroup() { + } + } +} diff --git a/src/DotNetOpenAuth.OAuth2/DotNetOpenAuth.OAuth2.csproj b/src/DotNetOpenAuth.OAuth2/DotNetOpenAuth.OAuth2.csproj index 921cd84..b359508 100644 --- a/src/DotNetOpenAuth.OAuth2/DotNetOpenAuth.OAuth2.csproj +++ b/src/DotNetOpenAuth.OAuth2/DotNetOpenAuth.OAuth2.csproj @@ -18,9 +18,11 @@ <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' "> </PropertyGroup> <ItemGroup> + <Compile Include="Configuration\OAuth2SectionGroup.cs" /> <Compile Include="GlobalSuppressions.cs" /> <Compile Include="OAuth2\AccessToken.cs" /> <Compile Include="OAuth2\ChannelElements\AuthorizationDataBag.cs" /> + <Compile Include="OAuth2\ChannelElements\ClientAuthenticationResult.cs" /> <Compile Include="OAuth2\ChannelElements\IAccessTokenCarryingRequest.cs" /> <Compile Include="OAuth2\ChannelElements\ScopeEncoder.cs" /> <Compile Include="OAuth2\ChannelElements\IAuthorizationDescription.cs" /> diff --git a/src/DotNetOpenAuth.OAuth2/OAuth2/ChannelElements/ClientAuthenticationResult.cs b/src/DotNetOpenAuth.OAuth2/OAuth2/ChannelElements/ClientAuthenticationResult.cs new file mode 100644 index 0000000..b0f86a9 --- /dev/null +++ b/src/DotNetOpenAuth.OAuth2/OAuth2/ChannelElements/ClientAuthenticationResult.cs @@ -0,0 +1,32 @@ +//----------------------------------------------------------------------- +// <copyright file="ClientAuthenticationResult.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OAuth2.ChannelElements { + /// <summary> + /// Describes the various levels at which client information may be extracted from an inbound message. + /// </summary> + public enum ClientAuthenticationResult { + /// <summary> + /// No client identification or authentication was discovered. + /// </summary> + NoAuthenticationRecognized, + + /// <summary> + /// The client identified itself, but did not attempt to authenticate itself. + /// </summary> + ClientIdNotAuthenticated, + + /// <summary> + /// The client authenticated itself (provided compelling evidence that it was who it claims to be). + /// </summary> + ClientAuthenticated, + + /// <summary> + /// The client failed in an attempt to authenticate itself, claimed to be an unrecognized client, or otherwise messed up. + /// </summary> + ClientAuthenticationRejected, + } +} diff --git a/src/DotNetOpenAuth.OAuth2/OAuth2/OAuthUtilities.cs b/src/DotNetOpenAuth.OAuth2/OAuth2/OAuthUtilities.cs index eb5c8e4..661d102 100644 --- a/src/DotNetOpenAuth.OAuth2/OAuth2/OAuthUtilities.cs +++ b/src/DotNetOpenAuth.OAuth2/OAuth2/OAuthUtilities.cs @@ -24,9 +24,24 @@ namespace DotNetOpenAuth.OAuth2 { public static readonly StringComparer ScopeStringComparer = StringComparer.Ordinal; /// <summary> + /// The string "Basic ". + /// </summary> + private const string HttpBasicAuthScheme = "Basic "; + + /// <summary> /// The delimiter between scope elements. /// </summary> - private static char[] scopeDelimiter = new char[] { ' ' }; + private static readonly char[] scopeDelimiter = new char[] { ' ' }; + + /// <summary> + /// A colon, in a 1-length character array. + /// </summary> + private static readonly char[] ColonSeparator = new char[] { ':' }; + + /// <summary> + /// The encoding to use when preparing credentials for transit in HTTP Basic base64 encoding form. + /// </summary> + private static readonly Encoding HttpBasicEncoding = Encoding.UTF8; /// <summary> /// The characters that may appear in an access token that is included in an HTTP Authorization header. @@ -35,9 +50,9 @@ namespace DotNetOpenAuth.OAuth2 { /// This is defined in OAuth 2.0 DRAFT 10, section 5.1.1. (http://tools.ietf.org/id/draft-ietf-oauth-v2-10.html#authz-header) /// </remarks> private static string accessTokenAuthorizationHeaderAllowedCharacters = MessagingUtilities.UppercaseLetters + - MessagingUtilities.LowercaseLetters + - MessagingUtilities.Digits + - @"!#$%&'()*+-./:<=>?@[]^_`{|}~\,;"; + MessagingUtilities.LowercaseLetters + + MessagingUtilities.Digits + + @"!#$%&'()*+-./:<=>?@[]^_`{|}~\,;"; /// <summary> /// Determines whether one given scope is a subset of another scope. @@ -129,5 +144,45 @@ namespace DotNetOpenAuth.OAuth2 { Protocol.BearerHttpAuthorizationHeaderFormat, accessToken); } + + /// <summary> + /// Applies the HTTP Authorization header for HTTP Basic authentication. + /// </summary> + /// <param name="headers">The headers collection to set the authorization header to.</param> + /// <param name="userName">The username. Cannot be empty.</param> + /// <param name="password">The password. Cannot be null.</param> + internal static void ApplyHttpBasicAuth(WebHeaderCollection headers, string userName, string password) { + Requires.NotNull(headers, "headers"); + Requires.NotNullOrEmpty(userName, "userName"); + Requires.NotNull(password, "password"); + + string concat = userName + ":" + password; + byte[] bits = HttpBasicEncoding.GetBytes(concat); + string base64 = Convert.ToBase64String(bits); + string header = HttpBasicAuthScheme + base64; + headers[HttpRequestHeader.Authorization] = header; + } + + /// <summary> + /// Extracts the username and password from an HTTP Basic authorized web header. + /// </summary> + /// <param name="headers">The incoming web headers.</param> + /// <returns>The network credentials; or <c>null</c> if none could be discovered in the request.</returns> + internal static NetworkCredential ParseHttpBasicAuth(WebHeaderCollection headers) { + Requires.NotNull(headers, "headers"); + + string authorizationHeader = headers[HttpRequestHeaders.Authorization]; + if (authorizationHeader != null && authorizationHeader.StartsWith(HttpBasicAuthScheme, StringComparison.Ordinal)) { + string base64 = authorizationHeader.Substring(HttpBasicAuthScheme.Length); + byte[] bits = Convert.FromBase64String(base64); + string usernameColonPassword = HttpBasicEncoding.GetString(bits); + string[] usernameAndPassword = usernameColonPassword.Split(ColonSeparator, 2); + if (usernameAndPassword.Length == 2) { + return new NetworkCredential(usernameAndPassword[0], usernameAndPassword[1]); + } + } + + return null; + } } } diff --git a/src/DotNetOpenAuth.OpenId/Configuration/OpenIdRelyingPartyElement.cs b/src/DotNetOpenAuth.OpenId/Configuration/OpenIdRelyingPartyElement.cs index 749659e..7d8c050 100644 --- a/src/DotNetOpenAuth.OpenId/Configuration/OpenIdRelyingPartyElement.cs +++ b/src/DotNetOpenAuth.OpenId/Configuration/OpenIdRelyingPartyElement.cs @@ -49,7 +49,8 @@ namespace DotNetOpenAuth.Configuration { /// <summary> /// The built-in set of identifier discovery services. /// </summary> - private static readonly TypeConfigurationCollection<IIdentifierDiscoveryService> defaultDiscoveryServices = new TypeConfigurationCollection<IIdentifierDiscoveryService>(new Type[] { typeof(UriDiscoveryService), typeof(XriDiscoveryProxyService) }); + private static readonly TypeConfigurationCollection<IIdentifierDiscoveryService> defaultDiscoveryServices = + new TypeConfigurationCollection<IIdentifierDiscoveryService>(new Type[] { typeof(UriDiscoveryService), typeof(XriDiscoveryProxyService) }); /// <summary> /// Initializes a new instance of the <see cref="OpenIdRelyingPartyElement"/> class. diff --git a/src/DotNetOpenAuth.Test/DotNetOpenAuth.Test.csproj b/src/DotNetOpenAuth.Test/DotNetOpenAuth.Test.csproj index a3edcf6..dae4a65 100644 --- a/src/DotNetOpenAuth.Test/DotNetOpenAuth.Test.csproj +++ b/src/DotNetOpenAuth.Test/DotNetOpenAuth.Test.csproj @@ -219,6 +219,7 @@ <Compile Include="Mocks\CoordinatingChannel.cs" /> <Compile Include="Mocks\CoordinatingHttpRequestInfo.cs" /> <Compile Include="Mocks\CoordinatingOAuth2AuthServerChannel.cs" /> + <Compile Include="Mocks\CoordinatingOAuth2ClientChannel.cs" /> <Compile Include="Mocks\CoordinatingOutgoingWebResponse.cs" /> <Compile Include="Mocks\CoordinatingOAuthConsumerChannel.cs" /> <Compile Include="Mocks\IBaseMessageExplicitMembers.cs" /> diff --git a/src/DotNetOpenAuth.Test/Mocks/CoordinatingChannel.cs b/src/DotNetOpenAuth.Test/Mocks/CoordinatingChannel.cs index d7205d6..2e09943 100644 --- a/src/DotNetOpenAuth.Test/Mocks/CoordinatingChannel.cs +++ b/src/DotNetOpenAuth.Test/Mocks/CoordinatingChannel.cs @@ -9,10 +9,10 @@ namespace DotNetOpenAuth.Test.Mocks { using System.Collections.Generic; using System.Diagnostics.Contracts; using System.Linq; + using System.Net; using System.Text; using System.Threading; using System.Web; - using DotNetOpenAuth.Messaging; using DotNetOpenAuth.Messaging.Reflection; using DotNetOpenAuth.Test.OpenId; @@ -65,9 +65,17 @@ namespace DotNetOpenAuth.Test.Mocks { /// </summary> private IDictionary<string, string> incomingMessage; + /// <summary> + /// The recipient URL of the <see cref="incomingMessage"/>, where applicable. + /// </summary> private MessageReceivingEndpoint incomingMessageRecipient; /// <summary> + /// The headers of the <see cref="incomingMessage"/>, where applicable. + /// </summary> + private WebHeaderCollection incomingMessageHttpHeaders; + + /// <summary> /// A delegate that gets a chance to peak at and fiddle with all /// incoming messages. /// </summary> @@ -145,17 +153,27 @@ namespace DotNetOpenAuth.Test.Mocks { this.incomingMessage = this.MessageDescriptions.GetAccessor(message).Serialize(); var directedMessage = message as IDirectedProtocolMessage; this.incomingMessageRecipient = (directedMessage != null && directedMessage.Recipient != null) ? new MessageReceivingEndpoint(directedMessage.Recipient, directedMessage.HttpMethods) : null; + var httpMessage = message as IHttpDirectRequest; + this.incomingMessageHttpHeaders = (httpMessage != null) ? httpMessage.Headers.Clone() : null; this.incomingMessageSignal.Set(); } protected internal override HttpRequestBase GetRequestFromContext() { MessageReceivingEndpoint recipient; - var messageData = this.AwaitIncomingMessage(out recipient); + WebHeaderCollection headers; + var messageData = this.AwaitIncomingMessage(out recipient, out headers); + CoordinatingHttpRequestInfo result; if (messageData != null) { - return new CoordinatingHttpRequestInfo(this, this.MessageFactory, messageData, recipient); + result = new CoordinatingHttpRequestInfo(this, this.MessageFactory, messageData, recipient); } else { - return new CoordinatingHttpRequestInfo(recipient); + result = new CoordinatingHttpRequestInfo(recipient); + } + + if (headers != null) { + headers.ApplyTo(result.Headers); } + + return result; } protected override IProtocolMessage RequestCore(IDirectedProtocolMessage request) { @@ -166,7 +184,8 @@ namespace DotNetOpenAuth.Test.Mocks { // Now wait for a response... MessageReceivingEndpoint recipient; - IDictionary<string, string> responseData = this.AwaitIncomingMessage(out recipient); + WebHeaderCollection headers; + IDictionary<string, string> responseData = this.AwaitIncomingMessage(out recipient, out headers); ErrorUtilities.VerifyInternal(recipient == null, "The recipient is expected to be null for direct responses."); // And deserialize it. @@ -177,6 +196,10 @@ namespace DotNetOpenAuth.Test.Mocks { var responseAccessor = this.MessageDescriptions.GetAccessor(responseMessage); responseAccessor.Deserialize(responseData); + var responseMessageHttpRequest = responseMessage as IHttpDirectRequest; + if (headers != null && responseMessageHttpRequest != null) { + headers.ApplyTo(responseMessageHttpRequest.Headers); + } this.ProcessMessageFilter(responseMessage, false); return responseMessage; @@ -258,7 +281,7 @@ namespace DotNetOpenAuth.Test.Mocks { return channel.MessageFactoryTestHook; } - private IDictionary<string, string> AwaitIncomingMessage(out MessageReceivingEndpoint recipient) { + private IDictionary<string, string> AwaitIncomingMessage(out MessageReceivingEndpoint recipient, out WebHeaderCollection headers) { // Special care should be taken so that we don't indefinitely // wait for a message that may never come due to a bug in the product // or the test. @@ -284,8 +307,10 @@ namespace DotNetOpenAuth.Test.Mocks { this.waitingForMessage = false; var response = this.incomingMessage; recipient = this.incomingMessageRecipient; + headers = this.incomingMessageHttpHeaders; this.incomingMessage = null; this.incomingMessageRecipient = null; + this.incomingMessageHttpHeaders = null; // Briefly signal to another thread that might be waiting for our inbox to be empty this.messageReceivedSignal.Set(); diff --git a/src/DotNetOpenAuth.Test/Mocks/CoordinatingHttpRequestInfo.cs b/src/DotNetOpenAuth.Test/Mocks/CoordinatingHttpRequestInfo.cs index 9f139f3..a1f5cf5 100644 --- a/src/DotNetOpenAuth.Test/Mocks/CoordinatingHttpRequestInfo.cs +++ b/src/DotNetOpenAuth.Test/Mocks/CoordinatingHttpRequestInfo.cs @@ -6,9 +6,10 @@ namespace DotNetOpenAuth.Test.Mocks { using System; - using System.Collections.Generic; - using System.Diagnostics.Contracts; - using DotNetOpenAuth.Messaging; +using System.Collections.Generic; +using System.Diagnostics.Contracts; +using System.Net; +using DotNetOpenAuth.Messaging; internal class CoordinatingHttpRequestInfo : HttpRequestInfo { private readonly Channel channel; diff --git a/src/DotNetOpenAuth.Test/Mocks/CoordinatingOAuth2ClientChannel.cs b/src/DotNetOpenAuth.Test/Mocks/CoordinatingOAuth2ClientChannel.cs new file mode 100644 index 0000000..52f381d --- /dev/null +++ b/src/DotNetOpenAuth.Test/Mocks/CoordinatingOAuth2ClientChannel.cs @@ -0,0 +1,33 @@ +//----------------------------------------------------------------------- +// <copyright file="CoordinatingOAuth2ClientChannel.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.Test.Mocks { + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.OAuth2.ChannelElements; + + internal class CoordinatingOAuth2ClientChannel : CoordinatingChannel, IOAuth2ChannelWithClient { + private OAuth2ClientChannel wrappedChannel; + + internal CoordinatingOAuth2ClientChannel(Channel wrappedChannel, Action<IProtocolMessage> incomingMessageFilter, Action<IProtocolMessage> outgoingMessageFilter) + : base(wrappedChannel, incomingMessageFilter, outgoingMessageFilter) { + this.wrappedChannel = (OAuth2ClientChannel)wrappedChannel; + } + + public string ClientIdentifier { + get { return this.wrappedChannel.ClientIdentifier; } + set { this.wrappedChannel.ClientIdentifier = value; } + } + + public DotNetOpenAuth.OAuth2.ClientCredentialApplicator ClientCredentialApplicator { + get { return this.wrappedChannel.ClientCredentialApplicator; } + set { this.wrappedChannel.ClientCredentialApplicator = value; } + } + } +}
\ No newline at end of file diff --git a/src/DotNetOpenAuth.Test/OAuth2/MessageFactoryTests.cs b/src/DotNetOpenAuth.Test/OAuth2/MessageFactoryTests.cs index 89271ae..52b5371 100644 --- a/src/DotNetOpenAuth.Test/OAuth2/MessageFactoryTests.cs +++ b/src/DotNetOpenAuth.Test/OAuth2/MessageFactoryTests.cs @@ -28,7 +28,7 @@ namespace DotNetOpenAuth.Test.OAuth2 { public override void SetUp() { base.SetUp(); - var authServerChannel = new OAuth2AuthorizationServerChannel(new Mock<IAuthorizationServerHost>().Object); + var authServerChannel = new OAuth2AuthorizationServerChannel(new Mock<IAuthorizationServerHost>().Object, new Mock<ClientAuthenticationModule>().Object); this.authServerMessageFactory = authServerChannel.MessageFactoryTestHook; var clientChannel = new OAuth2ClientChannel(); diff --git a/src/DotNetOpenAuth.Test/OAuth2/OAuth2Coordinator.cs b/src/DotNetOpenAuth.Test/OAuth2/OAuth2Coordinator.cs index ede3258..6494585 100644 --- a/src/DotNetOpenAuth.Test/OAuth2/OAuth2Coordinator.cs +++ b/src/DotNetOpenAuth.Test/OAuth2/OAuth2Coordinator.cs @@ -8,6 +8,7 @@ namespace DotNetOpenAuth.Test.OAuth2 { using System; using System.Collections.Generic; using System.Linq; + using System.Net; using System.Text; using DotNetOpenAuth.OAuth2; using DotNetOpenAuth.Test.Mocks; @@ -34,13 +35,13 @@ namespace DotNetOpenAuth.Test.OAuth2 { this.client = client; this.client.ClientIdentifier = OAuth2TestBase.ClientId; - this.client.ClientSecret = OAuth2TestBase.ClientSecret; + this.client.ClientCredentialApplicator = ClientCredentialApplicator.PostParameter(OAuth2TestBase.ClientSecret); } internal override void Run() { var authServer = new AuthorizationServer(this.authServerHost); - var rpCoordinatingChannel = new CoordinatingChannel(this.client.Channel, this.IncomingMessageFilter, this.OutgoingMessageFilter); + var rpCoordinatingChannel = new CoordinatingOAuth2ClientChannel(this.client.Channel, this.IncomingMessageFilter, this.OutgoingMessageFilter); var opCoordinatingChannel = new CoordinatingOAuth2AuthServerChannel(authServer.Channel, this.IncomingMessageFilter, this.OutgoingMessageFilter); rpCoordinatingChannel.RemoteChannel = opCoordinatingChannel; opCoordinatingChannel.RemoteChannel = rpCoordinatingChannel; diff --git a/src/DotNetOpenAuth.Test/OAuth2/UserAgentClientAuthorizeTests.cs b/src/DotNetOpenAuth.Test/OAuth2/UserAgentClientAuthorizeTests.cs index 97c0f56..ae03b0c 100644 --- a/src/DotNetOpenAuth.Test/OAuth2/UserAgentClientAuthorizeTests.cs +++ b/src/DotNetOpenAuth.Test/OAuth2/UserAgentClientAuthorizeTests.cs @@ -73,7 +73,7 @@ namespace DotNetOpenAuth.Test.OAuth2 { server.ApproveAuthorizationRequest(request, ResourceOwnerUsername); }); - coordinatorClient.ClientSecret = null; // implicit grant clients don't need a secret. + coordinatorClient.ClientCredentialApplicator = null; // implicit grant clients don't need a secret. coordinator.Run(); } } diff --git a/src/DotNetOpenAuth.Test/OAuth2/WebServerClientAuthorizeTests.cs b/src/DotNetOpenAuth.Test/OAuth2/WebServerClientAuthorizeTests.cs index fe0abd2..6f46271 100644 --- a/src/DotNetOpenAuth.Test/OAuth2/WebServerClientAuthorizeTests.cs +++ b/src/DotNetOpenAuth.Test/OAuth2/WebServerClientAuthorizeTests.cs @@ -8,6 +8,7 @@ namespace DotNetOpenAuth.Test.OAuth2 { using System; using System.Collections.Generic; using System.Linq; + using System.Net; using System.Text; using DotNetOpenAuth.Messaging; using DotNetOpenAuth.OAuth2; diff --git a/src/DotNetOpenAuth.Test/TestUtilities.cs b/src/DotNetOpenAuth.Test/TestUtilities.cs index cf9b5a3..a526f7f 100644 --- a/src/DotNetOpenAuth.Test/TestUtilities.cs +++ b/src/DotNetOpenAuth.Test/TestUtilities.cs @@ -7,16 +7,35 @@ namespace DotNetOpenAuth.Test { using System; using System.Collections.Generic; + using System.Collections.Specialized; using System.Linq; + using System.Net; using log4net; /// <summary> /// An assortment of methods useful for testing. /// </summary> - internal class TestUtilities { + internal static class TestUtilities { /// <summary> /// The logger that tests should use. /// </summary> internal static readonly ILog TestLogger = LogManager.GetLogger("DotNetOpenAuth.Test"); + + internal static void ApplyTo(this NameValueCollection source, NameValueCollection target) { + Requires.NotNull(source, "source"); + Requires.NotNull(target, "target"); + + foreach (string header in source) { + target[header] = source[header]; + } + } + + internal static T Clone<T>(this T source) where T : NameValueCollection, new() { + Requires.NotNull(source, "source"); + + var result = new T(); + ApplyTo(source, result); + return result; + } } } |