summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--projecttemplates/RelyingPartyLogic/OAuthAuthenticationModule.cs2
-rw-r--r--projecttemplates/RelyingPartyLogic/OAuthAuthorizationManager.cs9
-rw-r--r--samples/OAuthClient/SampleWcf2.aspx.cs6
-rw-r--r--samples/OAuthResourceServer/Code/OAuthAuthorizationManager.cs15
-rw-r--r--src/DotNetOpenAuth.Core/Messaging/OutgoingWebResponse.cs18
-rw-r--r--src/DotNetOpenAuth.Core/Messaging/ProtocolFaultResponseException.cs18
-rw-r--r--src/DotNetOpenAuth.Core/Strings.Designer.cs20
-rw-r--r--src/DotNetOpenAuth.Core/Strings.resx10
-rw-r--r--src/DotNetOpenAuth.OAuth2.AuthorizationServer/OAuth2/AuthorizationServer.cs14
-rw-r--r--src/DotNetOpenAuth.OAuth2.AuthorizationServer/OAuth2/ChannelElements/AuthServerBindingElementBase.cs7
-rw-r--r--src/DotNetOpenAuth.OAuth2.AuthorizationServer/OAuth2/ChannelElements/IOAuth2ChannelWithAuthorizationServer.cs7
-rw-r--r--src/DotNetOpenAuth.OAuth2.AuthorizationServer/OAuth2/ChannelElements/MessageValidationBindingElement.cs15
-rw-r--r--src/DotNetOpenAuth.OAuth2.AuthorizationServer/OAuth2/ChannelElements/OAuth2AuthorizationServerChannel.cs7
-rw-r--r--src/DotNetOpenAuth.OAuth2.Client/OAuth2/ClientStrings.Designer.cs11
-rw-r--r--src/DotNetOpenAuth.OAuth2.Client/OAuth2/ClientStrings.resx7
-rw-r--r--src/DotNetOpenAuth.OAuth2.Client/OAuth2/WebServerClient.cs6
-rw-r--r--src/DotNetOpenAuth.OAuth2.ResourceServer/OAuth2/ChannelElements/OAuth2ResourceServerChannel.cs4
-rw-r--r--src/DotNetOpenAuth.OAuth2.ResourceServer/OAuth2/ResourceServer.cs41
-rw-r--r--src/DotNetOpenAuth.OAuth2/DotNetOpenAuth.OAuth2.csproj2
-rw-r--r--src/DotNetOpenAuth.OAuth2/OAuth2/IScopeSatisfiedCheck.cs37
-rw-r--r--src/DotNetOpenAuth.OAuth2/OAuth2/Messages/UnauthorizedResponse.cs194
-rw-r--r--src/DotNetOpenAuth.OAuth2/OAuth2/OAuthStrings.Designer.cs20
-rw-r--r--src/DotNetOpenAuth.OAuth2/OAuth2/OAuthStrings.resx10
-rw-r--r--src/DotNetOpenAuth.OAuth2/OAuth2/OAuthUtilities.cs45
-rw-r--r--src/DotNetOpenAuth.OAuth2/OAuth2/Protocol.cs34
-rw-r--r--src/DotNetOpenAuth.OAuth2/OAuth2/StandardScopeSatisfiedCheck.cs37
-rw-r--r--src/DotNetOpenAuth.Test/Messaging/ProtocolExceptionTests.cs2
-rw-r--r--src/DotNetOpenAuth.Test/Mocks/CoordinatingOAuth2AuthServerChannel.cs5
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 &apos;{0}&apos; parameter contains the illegal character &apos;{1}&apos;..
+ /// </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; }
+ }
}
}