diff options
Diffstat (limited to 'src/DotNetOpenAuth.OAuth2.AuthorizationServer/OAuth2/ChannelElements')
6 files changed, 290 insertions, 13 deletions
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(); |