diff options
28 files changed, 452 insertions, 151 deletions
diff --git a/projecttemplates/RelyingPartyLogic/OAuthAuthenticationModule.cs b/projecttemplates/RelyingPartyLogic/OAuthAuthenticationModule.cs index 148af91..0e2618c 100644 --- a/projecttemplates/RelyingPartyLogic/OAuthAuthenticationModule.cs +++ b/projecttemplates/RelyingPartyLogic/OAuthAuthenticationModule.cs @@ -57,7 +57,7 @@ namespace RelyingPartyLogic { IPrincipal principal = resourceServer.GetPrincipal(new HttpRequestWrapper(this.application.Context.Request)); this.application.Context.User = principal; } catch (ProtocolFaultResponseException ex) { - ex.ErrorResponse.Send(); + ex.CreateErrorResponse().Send(); } } } diff --git a/projecttemplates/RelyingPartyLogic/OAuthAuthorizationManager.cs b/projecttemplates/RelyingPartyLogic/OAuthAuthorizationManager.cs index e38d955..6daf56e 100644 --- a/projecttemplates/RelyingPartyLogic/OAuthAuthorizationManager.cs +++ b/projecttemplates/RelyingPartyLogic/OAuthAuthorizationManager.cs @@ -38,7 +38,7 @@ namespace RelyingPartyLogic { var resourceServer = new ResourceServer(tokenAnalyzer); try { - IPrincipal principal = resourceServer.GetPrincipal(httpDetails, requestUri); + IPrincipal principal = resourceServer.GetPrincipal(httpDetails, requestUri, operationContext.IncomingMessageHeaders.Action); var policy = new OAuthPrincipalAuthorizationPolicy(principal); var policies = new List<IAuthorizationPolicy> { policy, @@ -57,13 +57,10 @@ namespace RelyingPartyLogic { principal.Identity, }; - // Only allow this method call if the access token scope permits it. - if (principal.IsInRole(operationContext.IncomingMessageHeaders.Action)) { - return true; - } + return true; } catch (ProtocolFaultResponseException ex) { // Return the appropriate unauthorized response to the client. - ex.ErrorResponse.Send(); + ex.CreateErrorResponse().Send(); } catch (DotNetOpenAuth.Messaging.ProtocolException/* ex*/) { ////Logger.Error("Error processing OAuth messages.", ex); } diff --git a/samples/OAuthClient/SampleWcf2.aspx.cs b/samples/OAuthClient/SampleWcf2.aspx.cs index f4d2dd5..7321ba5 100644 --- a/samples/OAuthClient/SampleWcf2.aspx.cs +++ b/samples/OAuthClient/SampleWcf2.aspx.cs @@ -87,6 +87,8 @@ this.nameLabel.Text = this.CallService(client => client.GetName());
} catch (SecurityAccessDeniedException) {
this.nameLabel.Text = "Access denied!";
+ } catch (MessageSecurityException ex) {
+ this.nameLabel.Text = "Access denied!";
}
}
@@ -96,6 +98,8 @@ this.ageLabel.Text = age.HasValue ? age.Value.ToString(CultureInfo.CurrentCulture) : "not available";
} catch (SecurityAccessDeniedException) {
this.ageLabel.Text = "Access denied!";
+ } catch (MessageSecurityException ex) {
+ this.ageLabel.Text = "Access denied!";
}
}
@@ -105,6 +109,8 @@ this.favoriteSitesLabel.Text = string.Join(", ", favoriteSites);
} catch (SecurityAccessDeniedException) {
this.favoriteSitesLabel.Text = "Access denied!";
+ } catch (MessageSecurityException ex) {
+ this.favoriteSitesLabel.Text = "Access denied!";
}
}
diff --git a/samples/OAuthResourceServer/Code/OAuthAuthorizationManager.cs b/samples/OAuthResourceServer/Code/OAuthAuthorizationManager.cs index 353e838..31371db 100644 --- a/samples/OAuthResourceServer/Code/OAuthAuthorizationManager.cs +++ b/samples/OAuthResourceServer/Code/OAuthAuthorizationManager.cs @@ -7,10 +7,9 @@ using System.ServiceModel; using System.ServiceModel.Channels; using System.ServiceModel.Security; - + using System.ServiceModel.Web; using DotNetOpenAuth.Messaging; using DotNetOpenAuth.OAuth2; - using ProtocolException = System.ServiceModel.ProtocolException; /// <summary> @@ -29,7 +28,7 @@ var requestUri = operationContext.RequestContext.RequestMessage.Properties.Via; try { - var principal = VerifyOAuth2(httpDetails, requestUri); + var principal = VerifyOAuth2(httpDetails, requestUri, operationContext.IncomingMessageHeaders.Action ?? operationContext.IncomingMessageHeaders.To.AbsolutePath); if (principal != null) { var policy = new OAuthPrincipalAuthorizationPolicy(principal); var policies = new List<IAuthorizationPolicy> { @@ -49,8 +48,7 @@ principal.Identity, }; - // Only allow this method call if the access token scope permits it. - return principal.IsInRole(operationContext.IncomingMessageHeaders.Action ?? operationContext.IncomingMessageHeaders.To.AbsolutePath); + return true; } else { return false; } @@ -58,7 +56,8 @@ Global.Logger.Error("Error processing OAuth messages.", ex); // Return the appropriate unauthorized response to the client. - ex.ErrorResponse.Send(); + var outgoingResponse = ex.CreateErrorResponse(); + outgoingResponse.Respond(WebOperationContext.Current.OutgoingResponse); } catch (ProtocolException ex) { Global.Logger.Error("Error processing OAuth messages.", ex); } @@ -66,13 +65,13 @@ return false; } - private static IPrincipal VerifyOAuth2(HttpRequestMessageProperty httpDetails, Uri requestUri) { + private static IPrincipal VerifyOAuth2(HttpRequestMessageProperty httpDetails, Uri requestUri, params string[] requiredScopes) { // for this sample where the auth server and resource server are the same site, // we use the same public/private key. using (var signing = Global.CreateAuthorizationServerSigningServiceProvider()) { using (var encrypting = Global.CreateResourceServerEncryptionServiceProvider()) { var resourceServer = new ResourceServer(new StandardAccessTokenAnalyzer(signing, encrypting)); - return resourceServer.GetPrincipal(httpDetails, requestUri); + return resourceServer.GetPrincipal(httpDetails, requestUri, requiredScopes); } } } diff --git a/src/DotNetOpenAuth.Core/Messaging/OutgoingWebResponse.cs b/src/DotNetOpenAuth.Core/Messaging/OutgoingWebResponse.cs index 67eccce..9ef89e9 100644 --- a/src/DotNetOpenAuth.Core/Messaging/OutgoingWebResponse.cs +++ b/src/DotNetOpenAuth.Core/Messaging/OutgoingWebResponse.cs @@ -12,6 +12,7 @@ namespace DotNetOpenAuth.Messaging { using System.IO; using System.Net; using System.Net.Mime; + using System.ServiceModel.Web; using System.Text; using System.Threading; using System.Web; @@ -213,6 +214,23 @@ namespace DotNetOpenAuth.Messaging { } /// <summary> + /// Submits this response to a WCF response context. Only available when no response body is included. + /// </summary> + /// <param name="responseContext">The response context to apply the response to.</param> + public virtual void Respond(OutgoingWebResponseContext responseContext) { + Requires.NotNull(responseContext, "responseContext"); + if (this.ResponseStream != null) { + throw new NotSupportedException(Strings.ResponseBodyNotSupported); + } + + responseContext.StatusCode = this.Status; + responseContext.SuppressEntityBody = true; + foreach (string header in this.Headers) { + responseContext.Headers[header] = this.Headers[header]; + } + } + + /// <summary> /// Automatically sends the appropriate response to the user agent. /// </summary> /// <param name="response">The response to set to this message.</param> diff --git a/src/DotNetOpenAuth.Core/Messaging/ProtocolFaultResponseException.cs b/src/DotNetOpenAuth.Core/Messaging/ProtocolFaultResponseException.cs index 515414b..f03ebdb 100644 --- a/src/DotNetOpenAuth.Core/Messaging/ProtocolFaultResponseException.cs +++ b/src/DotNetOpenAuth.Core/Messaging/ProtocolFaultResponseException.cs @@ -22,11 +22,6 @@ namespace DotNetOpenAuth.Messaging { private readonly Channel channel; /// <summary> - /// A cached value for the <see cref="ErrorResponse"/> property. - /// </summary> - private OutgoingWebResponse response; - - /// <summary> /// Initializes a new instance of the <see cref="ProtocolFaultResponseException"/> class /// such that it can be sent as a protocol message response to a remote caller. /// </summary> @@ -63,16 +58,11 @@ namespace DotNetOpenAuth.Messaging { public IDirectResponseProtocolMessage ErrorResponseMessage { get; private set; } /// <summary> - /// Gets the HTTP response to forward to the client to report the error. + /// Creates the HTTP response to forward to the client to report the error. /// </summary> - public OutgoingWebResponse ErrorResponse { - get { - if (this.response == null) { - this.response = this.channel.PrepareResponse(this.ErrorResponseMessage); - } - - return this.response; - } + public OutgoingWebResponse CreateErrorResponse() { + var response = this.channel.PrepareResponse(this.ErrorResponseMessage); + return response; } } } diff --git a/src/DotNetOpenAuth.Core/Strings.Designer.cs b/src/DotNetOpenAuth.Core/Strings.Designer.cs index 21411a1..b0e66d2 100644 --- a/src/DotNetOpenAuth.Core/Strings.Designer.cs +++ b/src/DotNetOpenAuth.Core/Strings.Designer.cs @@ -1,7 +1,7 @@ //------------------------------------------------------------------------------ // <auto-generated> // This code was generated by a tool. -// Runtime Version:4.0.30319.17291 +// Runtime Version:4.0.30319.17622 // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. @@ -106,6 +106,24 @@ namespace DotNetOpenAuth { } /// <summary> + /// Looks up a localized string similar to The property {0} must be set before this operation is allowed.. + /// </summary> + internal static string RequiredPropertyNotYetPreset { + get { + return ResourceManager.GetString("RequiredPropertyNotYetPreset", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to This object contains a response body, which is not supported.. + /// </summary> + internal static string ResponseBodyNotSupported { + get { + return ResourceManager.GetString("ResponseBodyNotSupported", resourceCulture); + } + } + + /// <summary> /// Looks up a localized string similar to No current HttpContext was detected, so an {0} instance must be explicitly provided or specified in the .config file. Call the constructor overload that takes an {0}.. /// </summary> internal static string StoreRequiredWhenNoHttpContextAvailable { diff --git a/src/DotNetOpenAuth.Core/Strings.resx b/src/DotNetOpenAuth.Core/Strings.resx index 1c69ef7..f4d61d1 100644 --- a/src/DotNetOpenAuth.Core/Strings.resx +++ b/src/DotNetOpenAuth.Core/Strings.resx @@ -112,10 +112,10 @@ <value>2.0</value> </resheader> <resheader name="reader"> - <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> + <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> </resheader> <resheader name="writer"> - <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> + <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> </resheader> <data name="ConfigurationTypeMustBePublic" xml:space="preserve"> <value>The configuration-specified type {0} must be public, and is not.</value> @@ -135,4 +135,10 @@ <data name="InvalidArgument" xml:space="preserve"> <value>The argument has an unexpected value.</value> </data> + <data name="RequiredPropertyNotYetPreset" xml:space="preserve"> + <value>The property {0} must be set before this operation is allowed.</value> + </data> + <data name="ResponseBodyNotSupported" xml:space="preserve"> + <value>This object contains a response body, which is not supported.</value> + </data> </root>
\ No newline at end of file diff --git a/src/DotNetOpenAuth.OAuth2.AuthorizationServer/OAuth2/AuthorizationServer.cs b/src/DotNetOpenAuth.OAuth2.AuthorizationServer/OAuth2/AuthorizationServer.cs index 59b75bf..ab20971 100644 --- a/src/DotNetOpenAuth.OAuth2.AuthorizationServer/OAuth2/AuthorizationServer.cs +++ b/src/DotNetOpenAuth.OAuth2.AuthorizationServer/OAuth2/AuthorizationServer.cs @@ -23,6 +23,11 @@ namespace DotNetOpenAuth.OAuth2 { /// </summary> public class AuthorizationServer { /// <summary> + /// A reusable instance of the scope satisfied checker. + /// </summary> + private static readonly IScopeSatisfiedCheck DefaultScopeSatisfiedCheck = new StandardScopeSatisfiedCheck(); + + /// <summary> /// The list of modules that verify client authentication data. /// </summary> private readonly List<ClientAuthenticationModule> clientAuthenticationModules = new List<ClientAuthenticationModule>(); @@ -41,6 +46,7 @@ namespace DotNetOpenAuth.OAuth2 { this.aggregatingClientAuthenticationModule = new AggregatingClientCredentialReader(this.clientAuthenticationModules); this.Channel = new OAuth2AuthorizationServerChannel(authorizationServer, this.aggregatingClientAuthenticationModule); this.clientAuthenticationModules.AddRange(OAuth2AuthorizationServerSection.Configuration.ClientAuthenticationModules.CreateInstances(true)); + this.ScopeSatisfiedCheck = DefaultScopeSatisfiedCheck; } /// <summary> @@ -65,6 +71,14 @@ namespace DotNetOpenAuth.OAuth2 { } /// <summary> + /// Gets or sets the service that checks whether a granted set of scopes satisfies a required set of scopes. + /// </summary> + public IScopeSatisfiedCheck ScopeSatisfiedCheck { + get { return ((IOAuth2ChannelWithAuthorizationServer)this.Channel).ScopeSatisfiedCheck; } + set { ((IOAuth2ChannelWithAuthorizationServer)this.Channel).ScopeSatisfiedCheck = value; } + } + + /// <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> diff --git a/src/DotNetOpenAuth.OAuth2.AuthorizationServer/OAuth2/ChannelElements/AuthServerBindingElementBase.cs b/src/DotNetOpenAuth.OAuth2.AuthorizationServer/OAuth2/ChannelElements/AuthServerBindingElementBase.cs index b66088c..9d3a52c 100644 --- a/src/DotNetOpenAuth.OAuth2.AuthorizationServer/OAuth2/ChannelElements/AuthServerBindingElementBase.cs +++ b/src/DotNetOpenAuth.OAuth2.AuthorizationServer/OAuth2/ChannelElements/AuthServerBindingElementBase.cs @@ -38,6 +38,13 @@ namespace DotNetOpenAuth.OAuth2.ChannelElements { public abstract MessageProtections Protection { get; } /// <summary> + /// Gets the channel to which this binding element belongs. + /// </summary> + internal IOAuth2ChannelWithAuthorizationServer AuthServerChannel { + get { return (IOAuth2ChannelWithAuthorizationServer)this.Channel; } + } + + /// <summary> /// Gets the authorization server hosting this channel. /// </summary> /// <value>The authorization server.</value> diff --git a/src/DotNetOpenAuth.OAuth2.AuthorizationServer/OAuth2/ChannelElements/IOAuth2ChannelWithAuthorizationServer.cs b/src/DotNetOpenAuth.OAuth2.AuthorizationServer/OAuth2/ChannelElements/IOAuth2ChannelWithAuthorizationServer.cs index ff6d7d1..5247062 100644 --- a/src/DotNetOpenAuth.OAuth2.AuthorizationServer/OAuth2/ChannelElements/IOAuth2ChannelWithAuthorizationServer.cs +++ b/src/DotNetOpenAuth.OAuth2.AuthorizationServer/OAuth2/ChannelElements/IOAuth2ChannelWithAuthorizationServer.cs @@ -14,6 +14,11 @@ namespace DotNetOpenAuth.OAuth2.ChannelElements { /// Gets the authorization server. /// </summary> /// <value>The authorization server.</value> - IAuthorizationServerHost AuthorizationServer { get; } + IAuthorizationServerHost AuthorizationServer { get; } + + /// <summary> + /// Gets or sets the service that checks whether a granted set of scopes satisfies a required set of scopes. + /// </summary> + IScopeSatisfiedCheck ScopeSatisfiedCheck { get; set; } } } diff --git a/src/DotNetOpenAuth.OAuth2.AuthorizationServer/OAuth2/ChannelElements/MessageValidationBindingElement.cs b/src/DotNetOpenAuth.OAuth2.AuthorizationServer/OAuth2/ChannelElements/MessageValidationBindingElement.cs index ac23e24..4821527 100644 --- a/src/DotNetOpenAuth.OAuth2.AuthorizationServer/OAuth2/ChannelElements/MessageValidationBindingElement.cs +++ b/src/DotNetOpenAuth.OAuth2.AuthorizationServer/OAuth2/ChannelElements/MessageValidationBindingElement.cs @@ -29,21 +29,12 @@ namespace DotNetOpenAuth.OAuth2.ChannelElements { 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) { + internal MessageValidationBindingElement(ClientAuthenticationModule clientAuthenticationModule) { Requires.NotNull(clientAuthenticationModule, "clientAuthenticationModule"); - Requires.NotNull(authorizationServer, "authorizationServer"); - this.clientAuthenticationModule = clientAuthenticationModule; - this.authorizationServer = authorizationServer; } /// <summary> @@ -105,7 +96,7 @@ namespace DotNetOpenAuth.OAuth2.ChannelElements { 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); + var result = this.clientAuthenticationModule.TryAuthenticateClient(this.AuthServerChannel.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; @@ -166,7 +157,7 @@ namespace DotNetOpenAuth.OAuth2.ChannelElements { 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)) { + if (!this.AuthServerChannel.ScopeSatisfiedCheck.IsScopeSatisfied(requiredScope: scopedAccessRequest.Scope, grantedScope: authCarrier.AuthorizationDescription.Scope)) { Logger.OAuth.ErrorFormat("The requested access scope (\"{0}\") exceeds the grant scope (\"{1}\").", scopedAccessRequest.Scope, authCarrier.AuthorizationDescription.Scope); throw new TokenEndpointProtocolException(accessTokenRequest, Protocol.AccessTokenRequestErrorCodes.InvalidScope, AuthServerStrings.AccessScopeExceedsGrantScope); } diff --git a/src/DotNetOpenAuth.OAuth2.AuthorizationServer/OAuth2/ChannelElements/OAuth2AuthorizationServerChannel.cs b/src/DotNetOpenAuth.OAuth2.AuthorizationServer/OAuth2/ChannelElements/OAuth2AuthorizationServerChannel.cs index 53dfb54..7ca4538 100644 --- a/src/DotNetOpenAuth.OAuth2.AuthorizationServer/OAuth2/ChannelElements/OAuth2AuthorizationServerChannel.cs +++ b/src/DotNetOpenAuth.OAuth2.AuthorizationServer/OAuth2/ChannelElements/OAuth2AuthorizationServerChannel.cs @@ -49,6 +49,11 @@ namespace DotNetOpenAuth.OAuth2.ChannelElements { public IAuthorizationServerHost AuthorizationServer { get; private set; } /// <summary> + /// Gets or sets the service that checks whether a granted set of scopes satisfies a required set of scopes. + /// </summary> + public IScopeSatisfiedCheck ScopeSatisfiedCheck { get; set; } + + /// <summary> /// Gets the protocol message that may be in the given HTTP response. /// </summary> /// <param name="response">The response that is anticipated to contain an protocol message.</param> @@ -118,7 +123,7 @@ namespace DotNetOpenAuth.OAuth2.ChannelElements { var bindingElements = new List<IChannelBindingElement>(); // The order they are provided is used for outgoing messgaes, and reversed for incoming messages. - bindingElements.Add(new MessageValidationBindingElement(clientAuthenticationModule, authorizationServer)); + bindingElements.Add(new MessageValidationBindingElement(clientAuthenticationModule)); bindingElements.Add(new TokenCodeSerializationBindingElement()); return bindingElements.ToArray(); diff --git a/src/DotNetOpenAuth.OAuth2.Client/OAuth2/ClientStrings.Designer.cs b/src/DotNetOpenAuth.OAuth2.Client/OAuth2/ClientStrings.Designer.cs index 7a5defd..87acfdf 100644 --- a/src/DotNetOpenAuth.OAuth2.Client/OAuth2/ClientStrings.Designer.cs +++ b/src/DotNetOpenAuth.OAuth2.Client/OAuth2/ClientStrings.Designer.cs @@ -1,7 +1,7 @@ //------------------------------------------------------------------------------ // <auto-generated> // This code was generated by a tool. -// Runtime Version:4.0.30319.17614 +// Runtime Version:4.0.30319.17622 // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. @@ -97,15 +97,6 @@ namespace DotNetOpenAuth.OAuth2 { } /// <summary> - /// Looks up a localized string similar to The property {0} must be set before this operation is allowed.. - /// </summary> - internal static string RequiredPropertyNotYetPreset { - get { - return ResourceManager.GetString("RequiredPropertyNotYetPreset", resourceCulture); - } - } - - /// <summary> /// Looks up a localized string similar to Unexpected response Content-Type {0}. /// </summary> internal static string UnexpectedResponseContentType { diff --git a/src/DotNetOpenAuth.OAuth2.Client/OAuth2/ClientStrings.resx b/src/DotNetOpenAuth.OAuth2.Client/OAuth2/ClientStrings.resx index a1ed7cd..5facbc4 100644 --- a/src/DotNetOpenAuth.OAuth2.Client/OAuth2/ClientStrings.resx +++ b/src/DotNetOpenAuth.OAuth2.Client/OAuth2/ClientStrings.resx @@ -112,10 +112,10 @@ <value>2.0</value> </resheader> <resheader name="reader"> - <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> + <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> </resheader> <resheader name="writer"> - <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> + <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> </resheader> <data name="AccessTokenRefreshFailed" xml:space="preserve"> <value>Access token has expired and cannot be automatically refreshed.</value> @@ -130,9 +130,6 @@ <data name="CannotObtainAccessTokenWithReason" xml:space="preserve"> <value>Failed to obtain access token. Authorization Server reports reason: {0}</value> </data> - <data name="RequiredPropertyNotYetPreset" xml:space="preserve"> - <value>The property {0} must be set before this operation is allowed.</value> - </data> <data name="UnexpectedResponseContentType" xml:space="preserve"> <value>Unexpected response Content-Type {0}</value> </data> diff --git a/src/DotNetOpenAuth.OAuth2.Client/OAuth2/WebServerClient.cs b/src/DotNetOpenAuth.OAuth2.Client/OAuth2/WebServerClient.cs index c19757f..939d1df 100644 --- a/src/DotNetOpenAuth.OAuth2.Client/OAuth2/WebServerClient.cs +++ b/src/DotNetOpenAuth.OAuth2.Client/OAuth2/WebServerClient.cs @@ -81,7 +81,7 @@ namespace DotNetOpenAuth.OAuth2 { public OutgoingWebResponse PrepareRequestUserAuthorization(IAuthorizationState authorization) { Requires.NotNull(authorization, "authorization"); Requires.ValidState(authorization.Callback != null || (HttpContext.Current != null && HttpContext.Current.Request != null), MessagingStrings.HttpContextRequired); - Requires.ValidState(!string.IsNullOrEmpty(this.ClientIdentifier), ClientStrings.RequiredPropertyNotYetPreset, "ClientIdentifier"); + Requires.ValidState(!string.IsNullOrEmpty(this.ClientIdentifier), Strings.RequiredPropertyNotYetPreset, "ClientIdentifier"); Contract.Ensures(Contract.Result<OutgoingWebResponse>() != null); if (authorization.Callback == null) { @@ -118,8 +118,8 @@ namespace DotNetOpenAuth.OAuth2 { /// <param name="request">The incoming HTTP request that may carry an authorization response.</param> /// <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(this.ClientCredentialApplicator != null, ClientStrings.RequiredPropertyNotYetPreset, "ClientCredentialApplicator"); + Requires.ValidState(!string.IsNullOrEmpty(this.ClientIdentifier), Strings.RequiredPropertyNotYetPreset, "ClientIdentifier"); + Requires.ValidState(this.ClientCredentialApplicator != null, Strings.RequiredPropertyNotYetPreset, "ClientCredentialApplicator"); if (request == null) { request = this.Channel.GetRequestFromContext(); diff --git a/src/DotNetOpenAuth.OAuth2.ResourceServer/OAuth2/ChannelElements/OAuth2ResourceServerChannel.cs b/src/DotNetOpenAuth.OAuth2.ResourceServer/OAuth2/ChannelElements/OAuth2ResourceServerChannel.cs index 22514b4..e9d596a 100644 --- a/src/DotNetOpenAuth.OAuth2.ResourceServer/OAuth2/ChannelElements/OAuth2ResourceServerChannel.cs +++ b/src/DotNetOpenAuth.OAuth2.ResourceServer/OAuth2/ChannelElements/OAuth2ResourceServerChannel.cs @@ -101,7 +101,7 @@ namespace DotNetOpenAuth.OAuth2.ChannelElements { protected override OutgoingWebResponse PrepareDirectResponse(IProtocolMessage response) { var webResponse = new OutgoingWebResponse(); - // The only direct response from a resource server is a 401 Unauthorized error. + // The only direct response from a resource server is some authorization error (400, 401, 403). var unauthorizedResponse = response as UnauthorizedResponse; ErrorUtilities.VerifyInternal(unauthorizedResponse != null, "Only unauthorized responses are expected."); @@ -113,7 +113,7 @@ namespace DotNetOpenAuth.OAuth2.ChannelElements { // Now serialize all the message parts into the WWW-Authenticate header. var fields = this.MessageDescriptions.GetAccessor(response); - webResponse.Headers[HttpResponseHeader.WwwAuthenticate] = MessagingUtilities.AssembleAuthorizationHeader(Protocol.BearerHttpAuthorizationScheme, fields); + webResponse.Headers[HttpResponseHeader.WwwAuthenticate] = MessagingUtilities.AssembleAuthorizationHeader(unauthorizedResponse.Scheme, fields); return webResponse; } diff --git a/src/DotNetOpenAuth.OAuth2.ResourceServer/OAuth2/ResourceServer.cs b/src/DotNetOpenAuth.OAuth2.ResourceServer/OAuth2/ResourceServer.cs index ba332fe..be759c4 100644 --- a/src/DotNetOpenAuth.OAuth2.ResourceServer/OAuth2/ResourceServer.cs +++ b/src/DotNetOpenAuth.OAuth2.ResourceServer/OAuth2/ResourceServer.cs @@ -26,6 +26,11 @@ namespace DotNetOpenAuth.OAuth2 { /// </summary> public class ResourceServer { /// <summary> + /// A reusable instance of the scope satisfied checker. + /// </summary> + private static readonly IScopeSatisfiedCheck DefaultScopeSatisfiedCheck = new StandardScopeSatisfiedCheck(); + + /// <summary> /// Initializes a new instance of the <see cref="ResourceServer"/> class. /// </summary> /// <param name="accessTokenAnalyzer">The access token analyzer.</param> @@ -36,6 +41,7 @@ namespace DotNetOpenAuth.OAuth2 { this.Channel = new OAuth2ResourceServerChannel(); this.ResourceOwnerPrincipalPrefix = string.Empty; this.ClientPrincipalPrefix = "client:"; + this.ScopeSatisfiedCheck = DefaultScopeSatisfiedCheck; } /// <summary> @@ -45,6 +51,11 @@ namespace DotNetOpenAuth.OAuth2 { public IAccessTokenAnalyzer AccessTokenAnalyzer { get; private set; } /// <summary> + /// Gets or sets the service that checks whether a granted set of scopes satisfies a required set of scopes. + /// </summary> + public IScopeSatisfiedCheck ScopeSatisfiedCheck { get; set; } + + /// <summary> /// Gets or sets the prefix to apply to a resource owner's username when used as the username in an <see cref="IPrincipal"/>. /// </summary> /// <value>The default value is the empty string.</value> @@ -66,6 +77,7 @@ namespace DotNetOpenAuth.OAuth2 { /// Discovers what access the client should have considering the access token in the current request. /// </summary> /// <param name="httpRequestInfo">The HTTP request info.</param> + /// <param name="requiredScopes">The set of scopes required to approve this request.</param> /// <returns> /// The access token describing the authorization the client has. Never <c>null</c>. /// </returns> @@ -73,7 +85,9 @@ namespace DotNetOpenAuth.OAuth2 { /// Thrown when the client is not authorized. This exception should be caught and the /// <see cref="ProtocolFaultResponseException.ErrorResponse"/> message should be returned to the client. /// </exception> - public virtual AccessToken GetAccessToken(HttpRequestBase httpRequestInfo = null) { + public virtual AccessToken GetAccessToken(HttpRequestBase httpRequestInfo = null, params string[] requiredScopes) { + Requires.NotNull(requiredScopes, "requiredScopes"); + Requires.ValidState(this.ScopeSatisfiedCheck != null, Strings.RequiredPropertyNotYetPreset); if (httpRequestInfo == null) { httpRequestInfo = this.Channel.GetRequestFromContext(); } @@ -89,14 +103,25 @@ namespace DotNetOpenAuth.OAuth2 { ErrorUtilities.ThrowProtocol(ResourceServerStrings.InvalidAccessToken); } + var requiredScopesSet = OAuthUtilities.ParseScopeSet(requiredScopes); + if (!this.ScopeSatisfiedCheck.IsScopeSatisfied(requiredScope: requiredScopesSet, grantedScope: accessToken.Scope)) { + var response = UnauthorizedResponse.InsufficientScope(request, requiredScopesSet); + throw new ProtocolFaultResponseException(this.Channel, response); + } + return accessToken; } else { var ex = new ProtocolException(ResourceServerStrings.MissingAccessToken); - var response = new UnauthorizedResponse(ex); + var response = UnauthorizedResponse.InvalidRequest(ex); throw new ProtocolFaultResponseException(this.Channel, response, innerException: ex); } } catch (ProtocolException ex) { - var response = request != null ? new UnauthorizedResponse(request, ex) : new UnauthorizedResponse(ex); + if (ex is ProtocolFaultResponseException) { + // This doesn't need to be wrapped again. + throw; + } + + var response = request != null ? UnauthorizedResponse.InvalidToken(request, ex) : UnauthorizedResponse.InvalidRequest(ex); throw new ProtocolFaultResponseException(this.Channel, response, innerException: ex); } } @@ -105,6 +130,7 @@ namespace DotNetOpenAuth.OAuth2 { /// Discovers what access the client should have considering the access token in the current request. /// </summary> /// <param name="httpRequestInfo">The HTTP request info.</param> + /// <param name="requiredScopes">The set of scopes required to approve this request.</param> /// <returns> /// The principal that contains the user and roles that the access token is authorized for. Never <c>null</c>. /// </returns> @@ -113,8 +139,8 @@ namespace DotNetOpenAuth.OAuth2 { /// <see cref="ProtocolFaultResponseException.ErrorResponse"/> message should be returned to the client. /// </exception> [SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "1#", Justification = "Try pattern")] - public virtual IPrincipal GetPrincipal(HttpRequestBase httpRequestInfo = null) { - AccessToken accessToken = this.GetAccessToken(httpRequestInfo); + public virtual IPrincipal GetPrincipal(HttpRequestBase httpRequestInfo = null, params string[] requiredScopes) { + AccessToken accessToken = this.GetAccessToken(httpRequestInfo, requiredScopes); // Mitigates attacks on this approach of differentiating clients from resource owners // by checking that a username doesn't look suspiciously engineered to appear like the other type. @@ -135,6 +161,7 @@ namespace DotNetOpenAuth.OAuth2 { /// </summary> /// <param name="request">HTTP details from an incoming WCF message.</param> /// <param name="requestUri">The URI of the WCF service endpoint.</param> + /// <param name="requiredScopes">The set of scopes required to approve this request.</param> /// <returns> /// The principal that contains the user and roles that the access token is authorized for. Never <c>null</c>. /// </returns> @@ -144,11 +171,11 @@ namespace DotNetOpenAuth.OAuth2 { /// </exception> [SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "1#", Justification = "Try pattern")] [SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "2#", Justification = "Try pattern")] - public virtual IPrincipal GetPrincipal(HttpRequestMessageProperty request, Uri requestUri) { + public virtual IPrincipal GetPrincipal(HttpRequestMessageProperty request, Uri requestUri, params string[] requiredScopes) { Requires.NotNull(request, "request"); Requires.NotNull(requestUri, "requestUri"); - return this.GetPrincipal(new HttpRequestInfo(request, requestUri)); + return this.GetPrincipal(new HttpRequestInfo(request, requestUri), requiredScopes); } } } diff --git a/src/DotNetOpenAuth.OAuth2/DotNetOpenAuth.OAuth2.csproj b/src/DotNetOpenAuth.OAuth2/DotNetOpenAuth.OAuth2.csproj index b359508..696d8a9 100644 --- a/src/DotNetOpenAuth.OAuth2/DotNetOpenAuth.OAuth2.csproj +++ b/src/DotNetOpenAuth.OAuth2/DotNetOpenAuth.OAuth2.csproj @@ -24,12 +24,14 @@ <Compile Include="OAuth2\ChannelElements\AuthorizationDataBag.cs" /> <Compile Include="OAuth2\ChannelElements\ClientAuthenticationResult.cs" /> <Compile Include="OAuth2\ChannelElements\IAccessTokenCarryingRequest.cs" /> + <Compile Include="OAuth2\IScopeSatisfiedCheck.cs" /> <Compile Include="OAuth2\ChannelElements\ScopeEncoder.cs" /> <Compile Include="OAuth2\ChannelElements\IAuthorizationDescription.cs" /> <Compile Include="OAuth2\ChannelElements\IAuthorizationCarryingRequest.cs" /> <Compile Include="OAuth2\Messages\AccessProtectedResourceRequest.cs" /> <Compile Include="OAuth2\Messages\UnauthorizedResponse.cs" /> <Compile Include="OAuth2\OAuthUtilities.cs" /> + <Compile Include="OAuth2\StandardScopeSatisfiedCheck.cs" /> <Compile Include="Properties\AssemblyInfo.cs" /> <Compile Include="OAuth2\Messages\MessageBase.cs" /> <Compile Include="OAuth2\Protocol.cs" /> diff --git a/src/DotNetOpenAuth.OAuth2/OAuth2/IScopeSatisfiedCheck.cs b/src/DotNetOpenAuth.OAuth2/OAuth2/IScopeSatisfiedCheck.cs new file mode 100644 index 0000000..c1fe3e4 --- /dev/null +++ b/src/DotNetOpenAuth.OAuth2/OAuth2/IScopeSatisfiedCheck.cs @@ -0,0 +1,37 @@ +//----------------------------------------------------------------------- +// <copyright file="IScopeSatisfiedCheck.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OAuth2 { + using System.Collections.Generic; + + /// <summary> + /// An extensibility point that allows authorization servers and resource servers to customize how scopes may be considered + /// supersets of each other. + /// </summary> + /// <remarks> + /// Implementations must be thread-safe. + /// </remarks> + public interface IScopeSatisfiedCheck { + /// <summary> + /// Checks whether the granted scope is a superset of the required scope. + /// </summary> + /// <param name="requiredScope">The set of strings that the resource server demands in an access token's scope in order to complete some operation.</param> + /// <param name="grantedScope">The set of strings that define the scope within an access token that the client is authorized to.</param> + /// <returns><c>true</c> if <paramref name="grantedScope"/> is a superset of <paramref name="requiredScope"/> to allow the request to proceed; <c>false</c> otherwise.</returns> + /// <remarks> + /// The default reasonable implementation of this is: + /// <code> + /// return <paramref name="grantedScope"/>.IsSupersetOf(<paramref name="requiredScope"/>); + /// </code> + /// <para>In some advanced cases it may not be so simple. One case is that there may be a string that aggregates the capabilities of several others + /// in order to simplify common scenarios. For example, the scope "ReadAll" may represent the same authorization as "ReadProfile", "ReadEmail", and + /// "ReadFriends". + /// </para> + /// <para>Great care should be taken in implementing this method as this is a critical security module for the authorization and resource servers.</para> + /// </remarks> + bool IsScopeSatisfied(ISet<string> requiredScope, ISet<string> grantedScope); + } +} diff --git a/src/DotNetOpenAuth.OAuth2/OAuth2/Messages/UnauthorizedResponse.cs b/src/DotNetOpenAuth.OAuth2/OAuth2/Messages/UnauthorizedResponse.cs index 3f4bb5b..e73f3cf 100644 --- a/src/DotNetOpenAuth.OAuth2/OAuth2/Messages/UnauthorizedResponse.cs +++ b/src/DotNetOpenAuth.OAuth2/OAuth2/Messages/UnauthorizedResponse.cs @@ -6,110 +6,204 @@ namespace DotNetOpenAuth.OAuth2.Messages { using System; + using System.Collections.Generic; using System.Diagnostics.Contracts; + using System.Globalization; using System.Net; using System.Text; using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.OAuth2.ChannelElements; /// <summary> - /// A direct response that is simply a 401 Unauthorized with an - /// WWW-Authenticate: OAuth header. + /// A direct response sent in response to a rejected Bearer access token. /// </summary> - internal class UnauthorizedResponse : MessageBase, IHttpDirectResponse { + /// <remarks> + /// This satisfies the spec in: http://self-issued.info/docs/draft-ietf-oauth-v2-bearer.html#authn-header + /// </remarks> + public class UnauthorizedResponse : MessageBase, IHttpDirectResponse { /// <summary> - /// Initializes a new instance of the <see cref="UnauthorizedResponse"/> class. + /// The headers in the response message. /// </summary> - /// <param name="exception">The exception.</param> - /// <param name="version">The protocol version.</param> - internal UnauthorizedResponse(ProtocolException exception, Version version = null) - : base(version ?? Protocol.Default.Version) { - Requires.NotNull(exception, "exception"); - this.ErrorMessage = exception.Message; - } + private readonly WebHeaderCollection headers = new WebHeaderCollection(); /// <summary> /// Initializes a new instance of the <see cref="UnauthorizedResponse"/> class. /// </summary> - /// <param name="request">The request.</param> - internal UnauthorizedResponse(IDirectedProtocolMessage request) - : base(request) { - this.Realm = "Service"; + /// <param name="version">The protocol version.</param> + protected UnauthorizedResponse(Version version = null) + : base(version ?? Protocol.Default.Version) { } /// <summary> /// Initializes a new instance of the <see cref="UnauthorizedResponse"/> class. /// </summary> /// <param name="request">The request.</param> - /// <param name="exception">The exception.</param> - internal UnauthorizedResponse(IDirectedProtocolMessage request, ProtocolException exception) - : this(request) { - Requires.NotNull(exception, "exception"); - this.ErrorMessage = exception.Message; + protected UnauthorizedResponse(IDirectedProtocolMessage request) + : base(request) { } #region IHttpDirectResponse Members /// <summary> - /// Gets the HTTP status code that the direct response should be sent with. + /// Gets or sets the HTTP status code that the direct response should be sent with. /// </summary> - HttpStatusCode IHttpDirectResponse.HttpStatusCode { - get { return HttpStatusCode.Unauthorized; } - } + public HttpStatusCode HttpStatusCode { get; set; } /// <summary> /// 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() { - { HttpResponseHeader.WwwAuthenticate, Protocol.BearerHttpAuthorizationScheme }, - }; - } + public WebHeaderCollection Headers { + get { return this.headers; } } #endregion /// <summary> - /// Gets or sets the error message. + /// Gets or sets the well known error code. /// </summary> - /// <value>The error message.</value> - [MessagePart("error")] - internal string ErrorMessage { get; set; } + /// <value>One of the values from <see cref="Protocol.BearerTokenErrorCodes"/>.</value> + [MessagePart(Protocol.BearerTokenUnauthorizedResponseParameters.ErrorCode)] + public string ErrorCode { get; set; } + + /// <summary> + /// Gets or sets a human-readable explanation for developers that is not meant to be displayed to end users. + /// </summary> + [MessagePart(Protocol.BearerTokenUnauthorizedResponseParameters.ErrorDescription)] + public string ErrorDescription { get; set; } + + /// <summary> + /// Gets or sets an absolute URI identifying a human-readable web page explaining the error. + /// </summary> + [MessagePart(Protocol.BearerTokenUnauthorizedResponseParameters.ErrorUri)] + public Uri ErrorUri { get; set; } /// <summary> /// Gets or sets the realm. /// </summary> /// <value>The realm.</value> - [MessagePart("realm")] - internal string Realm { get; set; } + [MessagePart(Protocol.BearerTokenUnauthorizedResponseParameters.Realm)] + public string Realm { get; set; } /// <summary> /// Gets or sets the scope. /// </summary> /// <value>The scope.</value> - [MessagePart("scope")] - internal string Scope { get; set; } + [MessagePart(Protocol.BearerTokenUnauthorizedResponseParameters.Scope, Encoder = typeof(ScopeEncoder))] + public ISet<string> Scope { get; set; } + + /// <summary> + /// Gets the scheme to use in the WWW-Authenticate header. + /// </summary> + internal virtual string Scheme { + get { return Protocol.BearerHttpAuthorizationScheme; } + } + + /// <summary> + /// Initializes a new instance of the <see cref="UnauthorizedResponse"/> class + /// to inform the client that the request was invalid. + /// </summary> + /// <param name="exception">The exception.</param> + /// <param name="version">The version of OAuth 2 that is in use.</param> + /// <returns>The error message.</returns> + internal static UnauthorizedResponse InvalidRequest(ProtocolException exception, Version version = null) { + Requires.NotNull(exception, "exception"); + var message = new UnauthorizedResponse(version) { + ErrorCode = Protocol.BearerTokenErrorCodes.InvalidRequest, + ErrorDescription = exception.Message, + HttpStatusCode = System.Net.HttpStatusCode.BadRequest, + }; + + return message; + } + + /// <summary> + /// Initializes a new instance of the <see cref="UnauthorizedResponse"/> class + /// to inform the client that the bearer token included in the request was rejected. + /// </summary> + /// <param name="request">The request.</param> + /// <param name="exception">The exception.</param> + /// <returns>The error message.</returns> + internal static UnauthorizedResponse InvalidToken(IDirectedProtocolMessage request, ProtocolException exception) { + Requires.NotNull(request, "request"); + Requires.NotNull(exception, "exception"); + var message = new UnauthorizedResponse(request) { + ErrorCode = Protocol.BearerTokenErrorCodes.InvalidToken, + ErrorDescription = exception.Message, + HttpStatusCode = System.Net.HttpStatusCode.Unauthorized, + }; + + return message; + } /// <summary> - /// Gets or sets the algorithms. + /// Initializes a new instance of the <see cref="UnauthorizedResponse"/> class + /// to inform the client of the required set of scopes required to perform this operation. /// </summary> - /// <value>The algorithms.</value> - [MessagePart("algorithms")] - internal string Algorithms { get; set; } + /// <param name="request">The request.</param> + /// <param name="requiredScopes">The set of scopes required to perform this operation.</param> + /// <returns>The error message.</returns> + internal static UnauthorizedResponse InsufficientScope(IDirectedProtocolMessage request, ISet<string> requiredScopes) { + Requires.NotNull(request, "request"); + Requires.NotNull(requiredScopes, "requiredScopes"); + var message = new UnauthorizedResponse(request) { + HttpStatusCode = System.Net.HttpStatusCode.Forbidden, + ErrorCode = Protocol.BearerTokenErrorCodes.InsufficientScope, + Scope = requiredScopes, + }; + return message; + } /// <summary> - /// Gets or sets the user endpoint. + /// Ensures the message is valid. /// </summary> - /// <value>The user endpoint.</value> - [MessagePart("user-uri")] - internal Uri UserEndpoint { get; set; } + protected override void EnsureValidMessage() { + base.EnsureValidMessage(); + + // Make sure the characters used in the supplied parameters satisfy requirements. + VerifyErrorCodeOrDescription(this.ErrorCode, Protocol.BearerTokenUnauthorizedResponseParameters.ErrorCode); + VerifyErrorCodeOrDescription(this.ErrorDescription, Protocol.BearerTokenUnauthorizedResponseParameters.ErrorDescription); + VerifyErrorUri(this.ErrorUri); + + // Ensure that at least one parameter is specified, as required in the spec. + ErrorUtilities.VerifyProtocol( + this.ErrorCode != null || this.ErrorDescription != null || this.ErrorUri != null || this.Realm != null || this.Scope != null, + OAuthStrings.BearerTokenUnauthorizedAtLeastOneParameterRequired); + } /// <summary> - /// Gets or sets the token endpoint. + /// Ensures the error or error_description parameters contain only allowed characters. /// </summary> - /// <value>The token endpoint.</value> - [MessagePart("token-uri")] - internal Uri TokenEndpoint { get; set; } + /// <param name="value">The argument.</param> + /// <param name="parameterName">The name of the parameter being validated. Used when errors are reported.</param> + private static void VerifyErrorCodeOrDescription(string value, string parameterName) { + if (value != null) { + for (int i = 0; i < value.Length; i++) { + // The allowed set of characters comes from http://self-issued.info/docs/draft-ietf-oauth-v2-bearer.html#authn-header + char ch = value[i]; + if (!((ch >= '\x20' && ch <= '\x21') || (ch >= '\x23' && ch <= '\x5B') || (ch >= '\x5D' && ch <= '\x7E'))) { + ErrorUtilities.ThrowProtocol(OAuthStrings.ParameterContainsIllegalCharacters, parameterName, ch); + } + } + } + } + + /// <summary> + /// Ensures the error_uri parameter contains only allowed characters and is an absolute URI. + /// </summary> + /// <param name="valueUri">The absolute URI.</param> + private static void VerifyErrorUri(Uri valueUri) { + if (valueUri != null) { + ErrorUtilities.VerifyProtocol(valueUri.IsAbsoluteUri, OAuthStrings.AbsoluteUriRequired); + string value = valueUri.AbsoluteUri; + for (int i = 0; i < value.Length; i++) { + // The allowed set of characters comes from http://self-issued.info/docs/draft-ietf-oauth-v2-bearer.html#authn-header + char ch = value[i]; + if (!(ch == '\x21' || (ch >= '\x23' && ch <= '\x5B') || (ch >= '\x5D' && ch <= '\x7E'))) { + ErrorUtilities.ThrowProtocol(OAuthStrings.ParameterContainsIllegalCharacters, Protocol.BearerTokenUnauthorizedResponseParameters.ErrorUri, ch); + } + } + } + } } } diff --git a/src/DotNetOpenAuth.OAuth2/OAuth2/OAuthStrings.Designer.cs b/src/DotNetOpenAuth.OAuth2/OAuth2/OAuthStrings.Designer.cs index 051d0d5..b440c1f 100644 --- a/src/DotNetOpenAuth.OAuth2/OAuth2/OAuthStrings.Designer.cs +++ b/src/DotNetOpenAuth.OAuth2/OAuth2/OAuthStrings.Designer.cs @@ -1,7 +1,7 @@ //------------------------------------------------------------------------------ // <auto-generated> // This code was generated by a tool. -// Runtime Version:4.0.30319.17614 +// Runtime Version:4.0.30319.17622 // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. @@ -79,6 +79,15 @@ namespace DotNetOpenAuth.OAuth2 { } /// <summary> + /// Looks up a localized string similar to At least one parameter is required for the Bearer scheme in its WWW-Authenticate header.. + /// </summary> + internal static string BearerTokenUnauthorizedAtLeastOneParameterRequired { + get { + return ResourceManager.GetString("BearerTokenUnauthorizedAtLeastOneParameterRequired", resourceCulture); + } + } + + /// <summary> /// Looks up a localized string similar to This message can only be sent over HTTPS.. /// </summary> internal static string HttpsRequired { @@ -106,6 +115,15 @@ namespace DotNetOpenAuth.OAuth2 { } /// <summary> + /// Looks up a localized string similar to The '{0}' parameter contains the illegal character '{1}'.. + /// </summary> + internal static string ParameterContainsIllegalCharacters { + get { + return ResourceManager.GetString("ParameterContainsIllegalCharacters", resourceCulture); + } + } + + /// <summary> /// Looks up a localized string similar to The return value of {0}.{1} should never be null.. /// </summary> internal static string ResultShouldNotBeNull { diff --git a/src/DotNetOpenAuth.OAuth2/OAuth2/OAuthStrings.resx b/src/DotNetOpenAuth.OAuth2/OAuth2/OAuthStrings.resx index 4d9d248..4298af6 100644 --- a/src/DotNetOpenAuth.OAuth2/OAuth2/OAuthStrings.resx +++ b/src/DotNetOpenAuth.OAuth2/OAuth2/OAuthStrings.resx @@ -112,10 +112,10 @@ <value>2.0</value> </resheader> <resheader name="reader"> - <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> + <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> </resheader> <resheader name="writer"> - <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> + <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> </resheader> <data name="AbsoluteUriRequired" xml:space="preserve"> <value>The value for message part "{0}" must be an absolute URI.</value> @@ -123,6 +123,9 @@ <data name="AccessTokenInvalidForHttpAuthorizationHeader" xml:space="preserve"> <value>The access token contains characters that must not appear in the HTTP Authorization header.</value> </data> + <data name="BearerTokenUnauthorizedAtLeastOneParameterRequired" xml:space="preserve"> + <value>At least one parameter is required for the Bearer scheme in its WWW-Authenticate header.</value> + </data> <data name="HttpsRequired" xml:space="preserve"> <value>This message can only be sent over HTTPS.</value> </data> @@ -132,6 +135,9 @@ <data name="NoGrantNoRefreshToken" xml:space="preserve"> <value>Refresh tokens should not be granted without the request including an access grant.</value> </data> + <data name="ParameterContainsIllegalCharacters" xml:space="preserve"> + <value>The '{0}' parameter contains the illegal character '{1}'.</value> + </data> <data name="ResultShouldNotBeNull" xml:space="preserve"> <value>The return value of {0}.{1} should never be null.</value> </data> diff --git a/src/DotNetOpenAuth.OAuth2/OAuth2/OAuthUtilities.cs b/src/DotNetOpenAuth.OAuth2/OAuth2/OAuthUtilities.cs index 661d102..4c46f75 100644 --- a/src/DotNetOpenAuth.OAuth2/OAuth2/OAuthUtilities.cs +++ b/src/DotNetOpenAuth.OAuth2/OAuth2/OAuthUtilities.cs @@ -55,29 +55,6 @@ namespace DotNetOpenAuth.OAuth2 { @"!#$%&'()*+-./:<=>?@[]^_`{|}~\,;"; /// <summary> - /// Determines whether one given scope is a subset of another scope. - /// </summary> - /// <param name="requestedScope">The requested scope, which may be a subset of <paramref name="grantedScope"/>.</param> - /// <param name="grantedScope">The granted scope, the suspected superset.</param> - /// <returns> - /// <c>true</c> if all the elements that appear in <paramref name="requestedScope"/> also appear in <paramref name="grantedScope"/>; - /// <c>false</c> otherwise. - /// </returns> - public static bool IsScopeSubset(string requestedScope, string grantedScope) { - if (string.IsNullOrEmpty(requestedScope)) { - return true; - } - - if (string.IsNullOrEmpty(grantedScope)) { - return false; - } - - var requestedScopes = new HashSet<string>(requestedScope.Split(scopeDelimiter, StringSplitOptions.RemoveEmptyEntries)); - var grantedScopes = new HashSet<string>(grantedScope.Split(scopeDelimiter, StringSplitOptions.RemoveEmptyEntries)); - return requestedScopes.IsSubsetOf(grantedScopes); - } - - /// <summary> /// Identifies individual scope elements /// </summary> /// <param name="scope">The space-delimited list of scopes.</param> @@ -97,13 +74,33 @@ namespace DotNetOpenAuth.OAuth2 { /// </summary> /// <param name="scopes">The scopes to serialize.</param> /// <returns>A space-delimited list.</returns> - public static string JoinScopes(HashSet<string> scopes) { + public static string JoinScopes(ISet<string> scopes) { Requires.NotNull(scopes, "scopes"); VerifyValidScopeTokens(scopes); return string.Join(" ", scopes.ToArray()); } /// <summary> + /// Parses a space-delimited list of scopes into a set. + /// </summary> + /// <param name="scopes">The space-delimited string.</param> + /// <returns>A set.</returns> + internal static ISet<string> ParseScopeSet(string scopes) { + Requires.NotNull(scopes, "scopes"); + return ParseScopeSet(scopes.Split(scopeDelimiter, StringSplitOptions.RemoveEmptyEntries)); + } + + /// <summary> + /// Creates a set out of an array of strings. + /// </summary> + /// <param name="scopes">The array of strings.</param> + /// <returns>A set.</returns> + internal static ISet<string> ParseScopeSet(string[] scopes) { + Requires.NotNull(scopes, "scopes"); + return new HashSet<string>(scopes, StringComparer.Ordinal); + } + + /// <summary> /// Verifies that a sequence of scope tokens are all valid. /// </summary> /// <param name="scopes">The scopes.</param> diff --git a/src/DotNetOpenAuth.OAuth2/OAuth2/Protocol.cs b/src/DotNetOpenAuth.OAuth2/OAuth2/Protocol.cs index 986af13..d780a81 100644 --- a/src/DotNetOpenAuth.OAuth2/OAuth2/Protocol.cs +++ b/src/DotNetOpenAuth.OAuth2/OAuth2/Protocol.cs @@ -297,5 +297,39 @@ namespace DotNetOpenAuth.OAuth2 { /// </summary> internal const string Bearer = "bearer"; } + + internal static class BearerTokenUnauthorizedResponseParameters { + internal const string Realm = "realm"; + internal const string ErrorCode = "error"; + internal const string ErrorDescription = "error_description"; + internal const string ErrorUri = "error_uri"; + internal const string Scope = "scope"; + } + + /// <summary> + /// The error codes prescribed in http://self-issued.info/docs/draft-ietf-oauth-v2-bearer.html#resource-error-codes + /// </summary> + internal static class BearerTokenErrorCodes { + /// <summary> + /// The request is missing a required parameter, includes an unsupported parameter or parameter value, + /// repeats the same parameter, uses more than one method for including an access token, or is otherwise + /// malformed. The resource server SHOULD respond with the HTTP 400 (Bad Request) status code. + /// </summary> + internal const string InvalidRequest = "invalid_request"; + + /// <summary> + /// The access token provided is expired, revoked, malformed, or invalid for other reasons. + /// The resource SHOULD respond with the HTTP 401 (Unauthorized) status code. The client MAY request + /// a new access token and retry the protected resource request. + /// </summary> + internal const string InvalidToken = "invalid_token"; + + /// <summary> + /// The request requires higher privileges than provided by the access token. The resource server + /// SHOULD respond with the HTTP 403 (Forbidden) status code and MAY include the scope attribute + /// with the scope necessary to access the protected resource. + /// </summary> + internal const string InsufficientScope = "insufficient_scope"; + } } } diff --git a/src/DotNetOpenAuth.OAuth2/OAuth2/StandardScopeSatisfiedCheck.cs b/src/DotNetOpenAuth.OAuth2/OAuth2/StandardScopeSatisfiedCheck.cs new file mode 100644 index 0000000..1370057 --- /dev/null +++ b/src/DotNetOpenAuth.OAuth2/OAuth2/StandardScopeSatisfiedCheck.cs @@ -0,0 +1,37 @@ +//----------------------------------------------------------------------- +// <copyright file="StandardScopeSatisfiedCheck.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OAuth2 { + using System.Collections.Generic; + + /// <summary> + /// The default scope superset checker, which assumes that no scopes overlap. + /// </summary> + internal class StandardScopeSatisfiedCheck : IScopeSatisfiedCheck { + /// <summary> + /// Checks whether the granted scope is a superset of the required scope. + /// </summary> + /// <param name="requiredScope">The set of strings that the resource server demands in an access token's scope in order to complete some operation.</param> + /// <param name="grantedScope">The set of strings that define the scope within an access token that the client is authorized to.</param> + /// <returns><c>true</c> if <paramref name="grantedScope"/> is a superset of <paramref name="requiredScope"/> to allow the request to proceed; <c>false</c> otherwise.</returns> + /// <remarks> + /// The default reasonable implementation of this is: + /// <code> + /// return <paramref name="grantedScope"/>.IsSupersetOf(<paramref name="requiredScope"/>); + /// </code> + /// <para>In some advanced cases it may not be so simple. One case is that there may be a string that aggregates the capabilities of several others + /// in order to simplify common scenarios. For example, the scope "ReadAll" may represent the same authorization as "ReadProfile", "ReadEmail", and + /// "ReadFriends". + /// </para> + /// <para>Great care should be taken in implementing this method as this is a critical security module for the authorization and resource servers.</para> + /// </remarks> + public bool IsScopeSatisfied(ISet<string> requiredScope, ISet<string> grantedScope) { + Requires.NotNull(requiredScope, "requiredScope"); + Requires.NotNull(grantedScope, "grantedScope"); + return grantedScope.IsSupersetOf(requiredScope); + } + } +} diff --git a/src/DotNetOpenAuth.Test/Messaging/ProtocolExceptionTests.cs b/src/DotNetOpenAuth.Test/Messaging/ProtocolExceptionTests.cs index 4d107c8..c519680 100644 --- a/src/DotNetOpenAuth.Test/Messaging/ProtocolExceptionTests.cs +++ b/src/DotNetOpenAuth.Test/Messaging/ProtocolExceptionTests.cs @@ -37,7 +37,7 @@ namespace DotNetOpenAuth.Test.Messaging { Assert.AreSame(message, ex.FaultedMessage); } - [Test, ExpectedException(typeof(ArgumentNullException))] + [Test] public void CtorWithNullProtocolMessage() { new ProtocolException("message", (IProtocolMessage)null); } diff --git a/src/DotNetOpenAuth.Test/Mocks/CoordinatingOAuth2AuthServerChannel.cs b/src/DotNetOpenAuth.Test/Mocks/CoordinatingOAuth2AuthServerChannel.cs index 9a5d05e..463b149 100644 --- a/src/DotNetOpenAuth.Test/Mocks/CoordinatingOAuth2AuthServerChannel.cs +++ b/src/DotNetOpenAuth.Test/Mocks/CoordinatingOAuth2AuthServerChannel.cs @@ -24,5 +24,10 @@ namespace DotNetOpenAuth.Test.Mocks { public IAuthorizationServerHost AuthorizationServer { get { return this.wrappedChannel.AuthorizationServer; } } + + public IScopeSatisfiedCheck ScopeSatisfiedCheck { + get { return this.wrappedChannel.ScopeSatisfiedCheck; } + set { this.wrappedChannel.ScopeSatisfiedCheck = value; } + } } } |