diff options
Diffstat (limited to 'src')
24 files changed, 281 insertions, 34 deletions
diff --git a/src/DotNetOpenAuth.Core/Messaging/Channel.cs b/src/DotNetOpenAuth.Core/Messaging/Channel.cs index fbcb3d6..235c41b 100644 --- a/src/DotNetOpenAuth.Core/Messaging/Channel.cs +++ b/src/DotNetOpenAuth.Core/Messaging/Channel.cs @@ -1097,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; } @@ -1137,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"; @@ -1314,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/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/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.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/DotNetOpenAuth.OAuth2.AuthorizationServer.csproj b/src/DotNetOpenAuth.OAuth2.AuthorizationServer/DotNetOpenAuth.OAuth2.AuthorizationServer.csproj index 628db32..c6371c0 100644 --- a/src/DotNetOpenAuth.OAuth2.AuthorizationServer/DotNetOpenAuth.OAuth2.AuthorizationServer.csproj +++ b/src/DotNetOpenAuth.OAuth2.AuthorizationServer/DotNetOpenAuth.OAuth2.AuthorizationServer.csproj @@ -36,7 +36,7 @@ <Compile Include="OAuth2\ChannelElements\IOAuth2ChannelWithAuthorizationServer.cs" /> <Compile Include="OAuth2\ChannelElements\OAuth2AuthorizationServerChannel.cs" /> <Compile Include="OAuth2\ChannelElements\RefreshToken.cs" /> - <Compile Include="OAuth2\ChannelElements\ClientAuthenticationModuleBase.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 fecc6be..59b75bf 100644 --- a/src/DotNetOpenAuth.OAuth2.AuthorizationServer/OAuth2/AuthorizationServer.cs +++ b/src/DotNetOpenAuth.OAuth2.AuthorizationServer/OAuth2/AuthorizationServer.cs @@ -148,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 index 4f60303..ace95b3 100644 --- a/src/DotNetOpenAuth.OAuth2.AuthorizationServer/OAuth2/ChannelElements/AggregatingClientCredentialReader.cs +++ b/src/DotNetOpenAuth.OAuth2.AuthorizationServer/OAuth2/ChannelElements/AggregatingClientCredentialReader.cs @@ -33,6 +33,27 @@ namespace DotNetOpenAuth.OAuth2.ChannelElements { } /// <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> diff --git a/src/DotNetOpenAuth.OAuth2.AuthorizationServer/OAuth2/ChannelElements/ClientAuthenticationModuleBase.cs b/src/DotNetOpenAuth.OAuth2.AuthorizationServer/OAuth2/ChannelElements/ClientAuthenticationModule.cs index e835e1e..027929a 100644 --- a/src/DotNetOpenAuth.OAuth2.AuthorizationServer/OAuth2/ChannelElements/ClientAuthenticationModuleBase.cs +++ b/src/DotNetOpenAuth.OAuth2.AuthorizationServer/OAuth2/ChannelElements/ClientAuthenticationModule.cs @@ -1,5 +1,5 @@ //----------------------------------------------------------------------- -// <copyright file="ClientAuthenticationModuleBase.cs" company="Andrew Arnott"> +// <copyright file="ClientAuthenticationModule.cs" company="Andrew Arnott"> // Copyright (c) Andrew Arnott. All rights reserved. // </copyright> //----------------------------------------------------------------------- @@ -26,6 +26,13 @@ namespace DotNetOpenAuth.OAuth2.ChannelElements { } /// <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> @@ -41,7 +48,7 @@ namespace DotNetOpenAuth.OAuth2.ChannelElements { /// <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 TryAuthenticateClient(IAuthorizationServerHost authorizationServerHost, string clientIdentifier, string clientSecret) { + protected static ClientAuthenticationResult TryAuthenticateClientBySecret(IAuthorizationServerHost authorizationServerHost, string clientIdentifier, string clientSecret) { Requires.NotNull(authorizationServerHost, "authorizationServerHost"); if (!string.IsNullOrEmpty(clientIdentifier)) { diff --git a/src/DotNetOpenAuth.OAuth2.AuthorizationServer/OAuth2/ChannelElements/ClientCredentialHttpBasicReader.cs b/src/DotNetOpenAuth.OAuth2.AuthorizationServer/OAuth2/ChannelElements/ClientCredentialHttpBasicReader.cs index 44af332..655d38f 100644 --- a/src/DotNetOpenAuth.OAuth2.AuthorizationServer/OAuth2/ChannelElements/ClientCredentialHttpBasicReader.cs +++ b/src/DotNetOpenAuth.OAuth2.AuthorizationServer/OAuth2/ChannelElements/ClientCredentialHttpBasicReader.cs @@ -18,6 +18,13 @@ namespace DotNetOpenAuth.OAuth2.ChannelElements { /// </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> @@ -31,7 +38,7 @@ namespace DotNetOpenAuth.OAuth2.ChannelElements { var credential = OAuthUtilities.ParseHttpBasicAuth(requestMessage.Headers); if (credential != null) { clientIdentifier = credential.UserName; - return TryAuthenticateClient(authorizationServerHost, credential.UserName, credential.Password); + return TryAuthenticateClientBySecret(authorizationServerHost, credential.UserName, credential.Password); } clientIdentifier = null; diff --git a/src/DotNetOpenAuth.OAuth2.AuthorizationServer/OAuth2/ChannelElements/ClientCredentialMessagePartReader.cs b/src/DotNetOpenAuth.OAuth2.AuthorizationServer/OAuth2/ChannelElements/ClientCredentialMessagePartReader.cs index 6579df2..2afd06e 100644 --- a/src/DotNetOpenAuth.OAuth2.AuthorizationServer/OAuth2/ChannelElements/ClientCredentialMessagePartReader.cs +++ b/src/DotNetOpenAuth.OAuth2.AuthorizationServer/OAuth2/ChannelElements/ClientCredentialMessagePartReader.cs @@ -28,7 +28,7 @@ namespace DotNetOpenAuth.OAuth2.ChannelElements { Requires.NotNull(requestMessage, "requestMessage"); clientIdentifier = requestMessage.ClientIdentifier; - return TryAuthenticateClient(authorizationServerHost, requestMessage.ClientIdentifier, requestMessage.ClientSecret); + 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 e114208..ac23e24 100644 --- a/src/DotNetOpenAuth.OAuth2.AuthorizationServer/OAuth2/ChannelElements/MessageValidationBindingElement.cs +++ b/src/DotNetOpenAuth.OAuth2.AuthorizationServer/OAuth2/ChannelElements/MessageValidationBindingElement.cs @@ -37,6 +37,7 @@ namespace DotNetOpenAuth.OAuth2.ChannelElements { /// 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"); @@ -101,11 +102,12 @@ 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) { string clientIdentifier; var result = this.clientAuthenticationModule.TryAuthenticateClient(this.authorizationServer, authenticatedClientRequest, out clientIdentifier); - AuthServerUtilities.TokenEndpointVerify(result != ClientAuthenticationResult.ClientIdNotAuthenticated, Protocol.AccessTokenRequestErrorCodes.UnauthorizedClient); // an empty secret is not allowed for client authenticated calls. - AuthServerUtilities.TokenEndpointVerify(result == ClientAuthenticationResult.ClientAuthenticated, Protocol.AccessTokenRequestErrorCodes.InvalidClient, AuthServerStrings.ClientSecretMismatch); + 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) { @@ -128,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); } } @@ -159,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); } } } @@ -174,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.Client/DotNetOpenAuth.OAuth2.Client.csproj b/src/DotNetOpenAuth.OAuth2.Client/DotNetOpenAuth.OAuth2.Client.csproj index da76ecb..e72ee1a 100644 --- a/src/DotNetOpenAuth.OAuth2.Client/DotNetOpenAuth.OAuth2.Client.csproj +++ b/src/DotNetOpenAuth.OAuth2.Client/DotNetOpenAuth.OAuth2.Client.csproj @@ -21,6 +21,7 @@ <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" /> 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 10a92e0..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> @@ -39,6 +39,17 @@ namespace DotNetOpenAuth.OAuth2.ChannelElements { } /// <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> @@ -132,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 dd34b12..77b0f92 100644 --- a/src/DotNetOpenAuth.OAuth2.Client/OAuth2/ClientBase.cs +++ b/src/DotNetOpenAuth.OAuth2.Client/OAuth2/ClientBase.cs @@ -53,13 +53,23 @@ 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; set; } + public ClientCredentialApplicator ClientCredentialApplicator { + get { return this.OAuthChannel.ClientCredentialApplicator; } + set { this.OAuthChannel.ClientCredentialApplicator = value; } + } + + internal IOAuth2ChannelWithClient OAuthChannel { + get { return (IOAuth2ChannelWithClient)this.Channel; } + } /// <summary> /// Adds the necessary HTTP Authorization header to an HTTP request for protected resources @@ -278,7 +288,7 @@ namespace DotNetOpenAuth.OAuth2 { /// <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.HttpBasic(secret); + return secret == null ? ClientCredentialApplicator.NoSecret() : ClientCredentialApplicator.NetworkCredential(secret); } /// <summary> diff --git a/src/DotNetOpenAuth.OAuth2.Client/OAuth2/ClientCredentialApplicator.cs b/src/DotNetOpenAuth.OAuth2.Client/OAuth2/ClientCredentialApplicator.cs index a95f3d0..2a9a8a7 100644 --- a/src/DotNetOpenAuth.OAuth2.Client/OAuth2/ClientCredentialApplicator.cs +++ b/src/DotNetOpenAuth.OAuth2.Client/OAuth2/ClientCredentialApplicator.cs @@ -36,11 +36,21 @@ namespace DotNetOpenAuth.OAuth2 { /// <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 HttpBasic(string clientSecret) { + public static ClientCredentialApplicator NetworkCredential(string clientSecret) { Requires.NotNullOrEmpty(clientSecret, "clientSecret"); - return new HttpBasicApplicator(clientSecret); + return new NetworkCredentialApplicator(clientSecret); } /// <summary> @@ -56,27 +66,50 @@ namespace DotNetOpenAuth.OAuth2 { /// </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 abstract void ApplyClientCredential(string clientIdentifier, AuthenticatedClientRequestBase request); + 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 HttpBasicApplicator : ClientCredentialApplicator { + 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="HttpBasicApplicator"/> class. + /// Initializes a new instance of the <see cref="NetworkCredentialApplicator"/> class. /// </summary> /// <param name="clientSecret">The client secret.</param> - internal HttpBasicApplicator(string clientSecret) { + 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> @@ -85,7 +118,19 @@ namespace DotNetOpenAuth.OAuth2 { // When using network credentials, the client authentication is not done as standard message parts. request.ClientIdentifier = null; request.ClientSecret = null; - OAuthUtilities.ApplyHttpBasicAuth(request.Headers, clientIdentifier, this.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, 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); } } 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/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/Configuration/OAuth2Element.cs b/src/DotNetOpenAuth.OAuth2/Configuration/OAuth2SectionGroup.cs index 858d27b..112e756 100644 --- a/src/DotNetOpenAuth.OAuth2/Configuration/OAuth2Element.cs +++ b/src/DotNetOpenAuth.OAuth2/Configuration/OAuth2SectionGroup.cs @@ -1,5 +1,5 @@ //----------------------------------------------------------------------- -// <copyright file="OAuth2Element.cs" company="Outercurve Foundation"> +// <copyright file="OAuth2SectionGroup.cs" company="Outercurve Foundation"> // Copyright (c) Outercurve Foundation. All rights reserved. // </copyright> //----------------------------------------------------------------------- diff --git a/src/DotNetOpenAuth.OAuth2/DotNetOpenAuth.OAuth2.csproj b/src/DotNetOpenAuth.OAuth2/DotNetOpenAuth.OAuth2.csproj index 4b1d534..b359508 100644 --- a/src/DotNetOpenAuth.OAuth2/DotNetOpenAuth.OAuth2.csproj +++ b/src/DotNetOpenAuth.OAuth2/DotNetOpenAuth.OAuth2.csproj @@ -18,7 +18,7 @@ <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' "> </PropertyGroup> <ItemGroup> - <Compile Include="Configuration\OAuth2Element.cs" /> + <Compile Include="Configuration\OAuth2SectionGroup.cs" /> <Compile Include="GlobalSuppressions.cs" /> <Compile Include="OAuth2\AccessToken.cs" /> <Compile Include="OAuth2\ChannelElements\AuthorizationDataBag.cs" /> 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/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/OAuth2Coordinator.cs b/src/DotNetOpenAuth.Test/OAuth2/OAuth2Coordinator.cs index afc1335..1223fe7 100644 --- a/src/DotNetOpenAuth.Test/OAuth2/OAuth2Coordinator.cs +++ b/src/DotNetOpenAuth.Test/OAuth2/OAuth2Coordinator.cs @@ -35,13 +35,13 @@ namespace DotNetOpenAuth.Test.OAuth2 { this.client = client; this.client.ClientIdentifier = OAuth2TestBase.ClientId; - this.client.ClientCredentialApplicator = ClientCredentialApplicator.HttpBasic(OAuth2TestBase.ClientSecret); + this.client.ClientCredentialApplicator = ClientCredentialApplicator.NetworkCredential(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; |