diff options
Diffstat (limited to 'src/DotNetOpenAuth.OAuth2/OAuth2')
7 files changed, 300 insertions, 77 deletions
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); + } + } +} |