diff options
Diffstat (limited to 'src')
26 files changed, 174 insertions, 52 deletions
diff --git a/src/DotNetOpenAuth/DotNetOpenAuth.csproj b/src/DotNetOpenAuth/DotNetOpenAuth.csproj index 8f33c8d..1287b61 100644 --- a/src/DotNetOpenAuth/DotNetOpenAuth.csproj +++ b/src/DotNetOpenAuth/DotNetOpenAuth.csproj @@ -355,6 +355,7 @@ http://opensource.org/licenses/ms-pl.html <Compile Include="OAuth2\ChannelElements\IDataBagFormatter.cs" /> <Compile Include="OAuth2\ChannelElements\OAuth2ChannelBase.cs" /> <Compile Include="OAuth2\ChannelElements\OAuth2ClientChannel.cs" /> + <Compile Include="OAuth2\ChannelElements\ScopeEncoder.cs" /> <Compile Include="OAuth2\ChannelElements\UriStyleMessageFormatter.cs" /> <Compile Include="OAuth2\ChannelElements\IAuthorizationDescription.cs" /> <Compile Include="OAuth2\ChannelElements\ITokenCarryingRequest.cs" /> diff --git a/src/DotNetOpenAuth/Messaging/MessagingUtilities.cs b/src/DotNetOpenAuth/Messaging/MessagingUtilities.cs index d652051..1860770 100644 --- a/src/DotNetOpenAuth/Messaging/MessagingUtilities.cs +++ b/src/DotNetOpenAuth/Messaging/MessagingUtilities.cs @@ -234,6 +234,36 @@ namespace DotNetOpenAuth.Messaging { } /// <summary> + /// Adds a set of values to a collection. + /// </summary> + /// <typeparam name="T">The type of value kept in the collection.</typeparam> + /// <param name="collection">The collection to add to.</param> + /// <param name="values">The values to add to the collection.</param> + public static void AddRange<T>(this ICollection<T> collection, IEnumerable<T> values) { + Contract.Requires<ArgumentNullException>(collection != null, "collection"); + Contract.Requires<ArgumentNullException>(values != null, "values"); + + foreach (var value in values) { + collection.Add(value); + } + } + + /// <summary> + /// Clears any existing elements in a collection and fills the collection with a given set of values. + /// </summary> + /// <typeparam name="T">The type of value kept in the collection.</typeparam> + /// <param name="collection">The collection to modify.</param> + /// <param name="values">The new values to fill the collection.</param> + internal static void ResetContents<T>(this ICollection<T> collection, IEnumerable<T> values) { + Contract.Requires<ArgumentNullException>(collection != null, "collection"); + + collection.Clear(); + if (values != null) { + AddRange(collection, values); + } + } + + /// <summary> /// Strips any and all URI query parameters that serve as parts of a message. /// </summary> /// <param name="uri">The URI that may contain query parameters to remove.</param> diff --git a/src/DotNetOpenAuth/OAuth2/AuthorizationServer.cs b/src/DotNetOpenAuth/OAuth2/AuthorizationServer.cs index ce4f6bb..37be337 100644 --- a/src/DotNetOpenAuth/OAuth2/AuthorizationServer.cs +++ b/src/DotNetOpenAuth/OAuth2/AuthorizationServer.cs @@ -45,14 +45,14 @@ namespace DotNetOpenAuth.OAuth2 { return message; } - public void ApproveAuthorizationRequest(EndUserAuthorizationRequest authorizationRequest, string username, string scope = null, Uri callback = null) { + public void ApproveAuthorizationRequest(EndUserAuthorizationRequest authorizationRequest, string username, IEnumerable<string> scopes = null, Uri callback = null) { Contract.Requires<ArgumentNullException>(authorizationRequest != null, "authorizationRequest"); var response = this.PrepareApproveAuthorizationRequest(authorizationRequest, username, callback); // Customize the approved scope if the authorization server has decided to do so. - if (scope != null) { - response.Scope = scope; + if (scopes != null) { + response.Scope.ResetContents(scopes); } this.Channel.Send(response); diff --git a/src/DotNetOpenAuth/OAuth2/AuthorizationServerBase.cs b/src/DotNetOpenAuth/OAuth2/AuthorizationServerBase.cs index 93790b6..9078326 100644 --- a/src/DotNetOpenAuth/OAuth2/AuthorizationServerBase.cs +++ b/src/DotNetOpenAuth/OAuth2/AuthorizationServerBase.cs @@ -66,10 +66,10 @@ namespace DotNetOpenAuth.OAuth2 { var accessToken = new AccessToken(tokenRequest.AuthorizationDescription, accessTokenLifetime); var response = new AccessTokenSuccessResponse(request) { - Scope = tokenRequest.AuthorizationDescription.Scope, AccessToken = accessTokenFormatter.Serialize(accessToken), Lifetime = accessToken.Lifetime, }; + response.Scope.ResetContents(tokenRequest.AuthorizationDescription.Scope); if (includeRefreshToken) { var refreshTokenFormatter = RefreshToken.CreateFormatter(this.AuthorizationServer.Secret); diff --git a/src/DotNetOpenAuth/OAuth2/AuthorizationState.cs b/src/DotNetOpenAuth/OAuth2/AuthorizationState.cs index 907f6e7..7682900 100644 --- a/src/DotNetOpenAuth/OAuth2/AuthorizationState.cs +++ b/src/DotNetOpenAuth/OAuth2/AuthorizationState.cs @@ -6,6 +6,9 @@ namespace DotNetOpenAuth.OAuth2 { using System; + using System.Collections.Generic; + + using DotNetOpenAuth.Messaging; /// <summary> /// A simple memory-only copy of an authorization state. @@ -15,7 +18,11 @@ namespace DotNetOpenAuth.OAuth2 { /// <summary> /// Initializes a new instance of the <see cref="AuthorizationState"/> class. /// </summary> - public AuthorizationState() { + public AuthorizationState(IEnumerable<string> scopes = null) { + this.Scope = new HashSet<string>(OAuthUtilities.ScopeStringComparer); + if (scopes != null) { + this.Scope.AddRange(scopes); + } } /// <summary> @@ -52,7 +59,7 @@ namespace DotNetOpenAuth.OAuth2 { /// Gets or sets the scope the token is (to be) authorized for. /// </summary> /// <value>The scope.</value> - public string Scope { get; set; } + public HashSet<string> Scope { get; private set; } /// <summary> /// Gets or sets a value indicating whether this instance is deleted. diff --git a/src/DotNetOpenAuth/OAuth2/ChannelElements/AccessRequestBindingElement.cs b/src/DotNetOpenAuth/OAuth2/ChannelElements/AccessRequestBindingElement.cs index 80d2cd4..2404963 100644 --- a/src/DotNetOpenAuth/OAuth2/ChannelElements/AccessRequestBindingElement.cs +++ b/src/DotNetOpenAuth/OAuth2/ChannelElements/AccessRequestBindingElement.cs @@ -129,7 +129,7 @@ namespace DotNetOpenAuth.OAuth2.ChannelElements { ErrorUtilities.VerifyProtocol(string.Equals(client.Secret, accessRequest.ClientSecret, StringComparison.Ordinal), Protocol.incorrect_client_credentials); // Make sure the scope the client is requesting does not exceed the scope in the grant. - ErrorUtilities.VerifyProtocol(OAuthUtilities.IsScopeSubset(accessRequest.Scope, tokenRequest.AuthorizationDescription.Scope), OAuthStrings.AccessScopeExceedsGrantScope, accessRequest.Scope, tokenRequest.AuthorizationDescription.Scope); + ErrorUtilities.VerifyProtocol(accessRequest.Scope.IsSubsetOf(tokenRequest.AuthorizationDescription.Scope), OAuthStrings.AccessScopeExceedsGrantScope, accessRequest.Scope, tokenRequest.AuthorizationDescription.Scope); } // Make sure the authorization this token represents hasn't already been revoked. diff --git a/src/DotNetOpenAuth/OAuth2/ChannelElements/AccessToken.cs b/src/DotNetOpenAuth/OAuth2/ChannelElements/AccessToken.cs index aba407f..de24428 100644 --- a/src/DotNetOpenAuth/OAuth2/ChannelElements/AccessToken.cs +++ b/src/DotNetOpenAuth/OAuth2/ChannelElements/AccessToken.cs @@ -35,7 +35,7 @@ namespace DotNetOpenAuth.OAuth2.ChannelElements { this.ClientIdentifier = authorization.ClientIdentifier; this.UtcCreationDate = authorization.UtcIssued; this.User = authorization.User; - this.Scope = authorization.Scope; + this.Scope.ResetContents(authorization.Scope); this.Lifetime = lifetime; } diff --git a/src/DotNetOpenAuth/OAuth2/ChannelElements/AuthorizationCode.cs b/src/DotNetOpenAuth/OAuth2/ChannelElements/AuthorizationCode.cs index c4257aa..1ec6b41 100644 --- a/src/DotNetOpenAuth/OAuth2/ChannelElements/AuthorizationCode.cs +++ b/src/DotNetOpenAuth/OAuth2/ChannelElements/AuthorizationCode.cs @@ -6,6 +6,7 @@ namespace DotNetOpenAuth.OAuth2.ChannelElements { using System; + using System.Collections.Generic; using System.Diagnostics.Contracts; using System.Security.Cryptography; using System.Text; @@ -33,15 +34,15 @@ namespace DotNetOpenAuth.OAuth2.ChannelElements { /// </summary> /// <param name="clientIdentifier">The client identifier.</param> /// <param name="callback">The callback the client used to obtain authorization.</param> - /// <param name="scope">The scope.</param> + /// <param name="scopes">The authorized scopes.</param> /// <param name="username">The name on the account that authorized access.</param> - internal AuthorizationCode(string clientIdentifier, Uri callback, string scope, string username) { + internal AuthorizationCode(string clientIdentifier, Uri callback, IEnumerable<string> scopes, string username) { Contract.Requires<ArgumentException>(!String.IsNullOrEmpty(clientIdentifier)); Contract.Requires<ArgumentNullException>(callback != null, "callback"); this.ClientIdentifier = clientIdentifier; this.CallbackHash = this.CalculateCallbackHash(callback); - this.Scope = scope; + this.Scope.ResetContents(scopes); this.User = username; } diff --git a/src/DotNetOpenAuth/OAuth2/ChannelElements/AuthorizationDataBag.cs b/src/DotNetOpenAuth/OAuth2/ChannelElements/AuthorizationDataBag.cs index 90d63eb..2ca8c34 100644 --- a/src/DotNetOpenAuth/OAuth2/ChannelElements/AuthorizationDataBag.cs +++ b/src/DotNetOpenAuth/OAuth2/ChannelElements/AuthorizationDataBag.cs @@ -22,6 +22,7 @@ namespace DotNetOpenAuth.OAuth2.ChannelElements { /// Initializes a new instance of the <see cref="AuthorizationDataBag"/> class. /// </summary> protected AuthorizationDataBag() { + this.Scope = new HashSet<string>(OAuthUtilities.ScopeStringComparer); } /// <summary> @@ -48,7 +49,7 @@ namespace DotNetOpenAuth.OAuth2.ChannelElements { /// <summary> /// Gets or sets the scope of operations the client is allowed to invoke. /// </summary> - [MessagePart] - public string Scope { get; set; } + [MessagePart(Encoder = typeof(ScopeEncoder))] + public HashSet<string> Scope { get; private set; } } } diff --git a/src/DotNetOpenAuth/OAuth2/ChannelElements/IAuthorizationDescription.cs b/src/DotNetOpenAuth/OAuth2/ChannelElements/IAuthorizationDescription.cs index a4aba57..2b3a9ce 100644 --- a/src/DotNetOpenAuth/OAuth2/ChannelElements/IAuthorizationDescription.cs +++ b/src/DotNetOpenAuth/OAuth2/ChannelElements/IAuthorizationDescription.cs @@ -35,7 +35,7 @@ namespace DotNetOpenAuth.OAuth2.ChannelElements { /// <summary> /// Gets the scope of operations the client is allowed to invoke. /// </summary> - string Scope { get; } + HashSet<string> Scope { get; } } /// <summary> @@ -80,8 +80,11 @@ namespace DotNetOpenAuth.OAuth2.ChannelElements { /// <summary> /// Gets the scope of operations the client is allowed to invoke. /// </summary> - string IAuthorizationDescription.Scope { - get { throw new NotImplementedException(); } + HashSet<string> IAuthorizationDescription.Scope { + get { + Contract.Ensures(Contract.Result<HashSet<string>>() != null); + throw new NotImplementedException(); + } } } } diff --git a/src/DotNetOpenAuth/OAuth2/ChannelElements/RefreshToken.cs b/src/DotNetOpenAuth/OAuth2/ChannelElements/RefreshToken.cs index cd6cbf4..a88bf9f 100644 --- a/src/DotNetOpenAuth/OAuth2/ChannelElements/RefreshToken.cs +++ b/src/DotNetOpenAuth/OAuth2/ChannelElements/RefreshToken.cs @@ -33,7 +33,7 @@ namespace DotNetOpenAuth.OAuth2.ChannelElements { this.ClientIdentifier = authorization.ClientIdentifier; this.UtcCreationDate = authorization.UtcIssued; this.User = authorization.User; - this.Scope = authorization.Scope; + this.Scope.ResetContents(authorization.Scope); } internal static IDataBagFormatter<RefreshToken> CreateFormatter(byte[] symmetricSecret) diff --git a/src/DotNetOpenAuth/OAuth2/ChannelElements/ScopeEncoder.cs b/src/DotNetOpenAuth/OAuth2/ChannelElements/ScopeEncoder.cs new file mode 100644 index 0000000..d35f982 --- /dev/null +++ b/src/DotNetOpenAuth/OAuth2/ChannelElements/ScopeEncoder.cs @@ -0,0 +1,51 @@ +//----------------------------------------------------------------------- +// <copyright file="ScopeEncoder.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OAuth2.ChannelElements { + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.Messaging.Reflection; + + /// <summary> + /// Encodes or decodes a set of scopes into the OAuth 2.0 scope message part. + /// </summary> + internal class ScopeEncoder : IMessagePartEncoder { + /// <summary> + /// Initializes a new instance of the <see cref="ScopeEncoder"/> class. + /// </summary> + public ScopeEncoder() { + } + + /// <summary> + /// Encodes the specified value. + /// </summary> + /// <param name="value">The value. Guaranteed to never be null.</param> + /// <returns> + /// The <paramref name="value"/> in string form, ready for message transport. + /// </returns> + public string Encode(object value) { + var scopes = (IEnumerable<string>)value; + ErrorUtilities.VerifyProtocol(!scopes.Any(scope => scope.Contains(" ")), OAuthStrings.ScopesMayNotContainSpaces); + return scopes != null ? string.Join(" ", scopes.ToArray()) : null; + } + + /// <summary> + /// Decodes the specified value. + /// </summary> + /// <param name="value">The string value carried by the transport. Guaranteed to never be null, although it may be empty.</param> + /// <returns> + /// The deserialized form of the given string. + /// </returns> + /// <exception cref="FormatException">Thrown when the string value given cannot be decoded into the required object type.</exception> + public object Decode(string value) { + return OAuthUtilities.SplitScopes(value); + } + } +} diff --git a/src/DotNetOpenAuth/OAuth2/ClientBase.cs b/src/DotNetOpenAuth/OAuth2/ClientBase.cs index ec957b3..d9d9232 100644 --- a/src/DotNetOpenAuth/OAuth2/ClientBase.cs +++ b/src/DotNetOpenAuth/OAuth2/ClientBase.cs @@ -120,7 +120,7 @@ namespace DotNetOpenAuth.OAuth2 { // Just in case the scope has changed... if (response.Scope != null) { - authorization.Scope = response.Scope; + authorization.Scope.ResetContents(response.Scope); } // The authorization server MAY choose to renew the refresh token itself. @@ -147,12 +147,12 @@ namespace DotNetOpenAuth.OAuth2 { if (accessTokenSuccess.Scope != null && accessTokenSuccess.Scope != authorizationState.Scope) { if (authorizationState.Scope != null) { Logger.OAuth.InfoFormat( - "Requested scope of \"{0}\" changed to \"{1}\" by authorization server.", - authorizationState.Scope, - accessTokenSuccess.Scope); + "Requested scope of \"{0}\" changed to \"{1}\" by authorization server.", + authorizationState.Scope, + accessTokenSuccess.Scope); } - authorizationState.Scope = accessTokenSuccess.Scope; + authorizationState.Scope.ResetContents(accessTokenSuccess.Scope); } authorizationState.SaveChanges(); @@ -178,7 +178,7 @@ namespace DotNetOpenAuth.OAuth2 { accessTokenSuccess.Scope); } - authorizationState.Scope = accessTokenSuccess.Scope; + authorizationState.Scope.ResetContents(accessTokenSuccess.Scope); } authorizationState.SaveChanges(); diff --git a/src/DotNetOpenAuth/OAuth2/IAccessTokenAnalyzer.cs b/src/DotNetOpenAuth/OAuth2/IAccessTokenAnalyzer.cs index d82a6ab..ed6cd63 100644 --- a/src/DotNetOpenAuth/OAuth2/IAccessTokenAnalyzer.cs +++ b/src/DotNetOpenAuth/OAuth2/IAccessTokenAnalyzer.cs @@ -26,7 +26,7 @@ namespace DotNetOpenAuth.OAuth2 { /// <param name="user">The user whose data is accessible with this access token.</param> /// <param name="scope">The scope of access authorized by this access token.</param> /// <returns>A value indicating whether this access token is valid.</returns> - bool TryValidateAccessToken(IDirectedProtocolMessage message, string accessToken, out string user, out string scope); + bool TryValidateAccessToken(IDirectedProtocolMessage message, string accessToken, out string user, out HashSet<string> scope); } /// <summary> @@ -50,7 +50,7 @@ namespace DotNetOpenAuth.OAuth2 { /// <returns> /// A value indicating whether this access token is valid. /// </returns> - bool IAccessTokenAnalyzer.TryValidateAccessToken(IDirectedProtocolMessage message, string accessToken, out string user, out string scope) { + bool IAccessTokenAnalyzer.TryValidateAccessToken(IDirectedProtocolMessage message, string accessToken, out string user, out HashSet<string> scope) { Contract.Requires<ArgumentNullException>(message != null, "message"); Contract.Requires<ArgumentException>(!String.IsNullOrEmpty(accessToken)); Contract.Ensures(Contract.Result<bool>() == (Contract.ValueAtReturn<string>(out user) != null)); diff --git a/src/DotNetOpenAuth/OAuth2/IAuthorizationState.cs b/src/DotNetOpenAuth/OAuth2/IAuthorizationState.cs index 3839209..68a109b 100644 --- a/src/DotNetOpenAuth/OAuth2/IAuthorizationState.cs +++ b/src/DotNetOpenAuth/OAuth2/IAuthorizationState.cs @@ -6,6 +6,7 @@ namespace DotNetOpenAuth.OAuth2 { using System; + using System.Collections.Generic; /// <summary> /// Provides access to a persistent object that tracks the state of an authorization. @@ -44,7 +45,7 @@ namespace DotNetOpenAuth.OAuth2 { /// Gets or sets the scope the token is (to be) authorized for. /// </summary> /// <value>The scope.</value> - string Scope { get; set; } + HashSet<string> Scope { get; } /// <summary> /// Deletes this authorization, including access token and refresh token where applicable. diff --git a/src/DotNetOpenAuth/OAuth2/Messages/AccessTokenRequestBase.cs b/src/DotNetOpenAuth/OAuth2/Messages/AccessTokenRequestBase.cs index d9c2144..943308e 100644 --- a/src/DotNetOpenAuth/OAuth2/Messages/AccessTokenRequestBase.cs +++ b/src/DotNetOpenAuth/OAuth2/Messages/AccessTokenRequestBase.cs @@ -25,6 +25,7 @@ namespace DotNetOpenAuth.OAuth2.Messages { protected AccessTokenRequestBase(Uri tokenEndpoint, Version version) : base(tokenEndpoint, version) { this.HttpMethods = HttpDeliveryMethods.PostRequest; + this.Scope = new HashSet<string>(OAuthUtilities.ScopeStringComparer); } /// <summary> @@ -34,8 +35,8 @@ namespace DotNetOpenAuth.OAuth2.Messages { [MessagePart(Protocol.grant_type, IsRequired = true, AllowEmpty = false, Encoder = typeof(GrantTypeEncoder))] internal abstract GrantType GrantType { get; } - [MessagePart(Protocol.scope, IsRequired = false, AllowEmpty = true)] - internal string Scope { get; set; } + [MessagePart(Protocol.scope, IsRequired = false, AllowEmpty = true, Encoder = typeof(ScopeEncoder))] + internal HashSet<string> Scope { get; private set; } /// <summary> /// Checks the message state for conformity to the protocol specification diff --git a/src/DotNetOpenAuth/OAuth2/Messages/AccessTokenSuccessResponse.cs b/src/DotNetOpenAuth/OAuth2/Messages/AccessTokenSuccessResponse.cs index 85db387..37a4684 100644 --- a/src/DotNetOpenAuth/OAuth2/Messages/AccessTokenSuccessResponse.cs +++ b/src/DotNetOpenAuth/OAuth2/Messages/AccessTokenSuccessResponse.cs @@ -6,6 +6,7 @@ namespace DotNetOpenAuth.OAuth2.Messages { using System; + using System.Collections.Generic; using System.Net; using DotNetOpenAuth.Messaging; using DotNetOpenAuth.OAuth2.ChannelElements; @@ -24,6 +25,7 @@ namespace DotNetOpenAuth.OAuth2.Messages { /// <param name="request">The request.</param> internal AccessTokenSuccessResponse(AccessTokenRequestBase request) : base(request) { + this.Scope = new HashSet<string>(OAuthUtilities.ScopeStringComparer); } /// <summary> @@ -75,7 +77,7 @@ namespace DotNetOpenAuth.OAuth2.Messages { /// Gets or sets the scope of access being requested. /// </summary> /// <value>The scope of the access request expressed as a list of space-delimited strings. The value of the scope parameter is defined by the authorization server. If the value contains multiple space-delimited strings, their order does not matter, and each string adds an additional access range to the requested scope.</value> - [MessagePart(Protocol.scope, IsRequired = false, AllowEmpty = true)] - public string Scope { get; set; } + [MessagePart(Protocol.scope, IsRequired = false, AllowEmpty = true, Encoder = typeof(ScopeEncoder))] + public HashSet<string> Scope { get; private set; } } } diff --git a/src/DotNetOpenAuth/OAuth2/Messages/EndUserAuthorizationRequest.cs b/src/DotNetOpenAuth/OAuth2/Messages/EndUserAuthorizationRequest.cs index 108c323..a4e8b89 100644 --- a/src/DotNetOpenAuth/OAuth2/Messages/EndUserAuthorizationRequest.cs +++ b/src/DotNetOpenAuth/OAuth2/Messages/EndUserAuthorizationRequest.cs @@ -6,6 +6,7 @@ namespace DotNetOpenAuth.OAuth2.Messages { using System; + using System.Collections.Generic; using System.Diagnostics.Contracts; using DotNetOpenAuth.Messaging; using DotNetOpenAuth.OAuth2.ChannelElements; @@ -27,6 +28,7 @@ namespace DotNetOpenAuth.OAuth2.Messages { Contract.Requires<ArgumentNullException>(authorizationEndpoint != null); Contract.Requires<ArgumentNullException>(version != null); this.HttpMethods = HttpDeliveryMethods.GetRequest; + this.Scope = new HashSet<string>(OAuthUtilities.ScopeStringComparer); } /// <summary> @@ -85,7 +87,7 @@ namespace DotNetOpenAuth.OAuth2.Messages { /// Gets or sets the scope of access being requested. /// </summary> /// <value>The scope of the access request expressed as a list of space-delimited strings. The value of the scope parameter is defined by the authorization server. If the value contains multiple space-delimited strings, their order does not matter, and each string adds an additional access range to the requested scope.</value> - [MessagePart(Protocol.scope, IsRequired = false, AllowEmpty = true)] - public string Scope { get; set; } + [MessagePart(Protocol.scope, IsRequired = false, AllowEmpty = true, Encoder = typeof(ScopeEncoder))] + public HashSet<string> Scope { get; private set; } } } diff --git a/src/DotNetOpenAuth/OAuth2/Messages/EndUserAuthorizationSuccessResponseBase.cs b/src/DotNetOpenAuth/OAuth2/Messages/EndUserAuthorizationSuccessResponseBase.cs index a02c050..305c0e6 100644 --- a/src/DotNetOpenAuth/OAuth2/Messages/EndUserAuthorizationSuccessResponseBase.cs +++ b/src/DotNetOpenAuth/OAuth2/Messages/EndUserAuthorizationSuccessResponseBase.cs @@ -6,6 +6,7 @@ namespace DotNetOpenAuth.OAuth2.Messages { using System; + using System.Collections.Generic; using System.Diagnostics.Contracts; using System.Security.Cryptography; @@ -27,6 +28,7 @@ namespace DotNetOpenAuth.OAuth2.Messages { : base(version, MessageTransport.Indirect, clientCallback) { Contract.Requires<ArgumentNullException>(version != null); Contract.Requires<ArgumentNullException>(clientCallback != null); + this.Scope = new HashSet<string>(OAuthUtilities.ScopeStringComparer); } /// <summary> @@ -39,6 +41,7 @@ namespace DotNetOpenAuth.OAuth2.Messages { Contract.Requires<ArgumentNullException>(clientCallback != null, "clientCallback"); Contract.Requires<ArgumentNullException>(request != null, "request"); ((IMessageWithClientState)this).ClientState = request.ClientState; + this.Scope = new HashSet<string>(OAuthUtilities.ScopeStringComparer); } /// <summary> @@ -59,11 +62,11 @@ namespace DotNetOpenAuth.OAuth2.Messages { internal TimeSpan? Lifetime { get; set; } /// <summary> - /// Gets or sets the scope of the <see cref="AccessToken"/> if one is given; otherwise the scope of the authorization code. + /// Gets the scope of the <see cref="AccessToken"/> if one is given; otherwise the scope of the authorization code. /// </summary> /// <value>The scope.</value> - [MessagePart(Protocol.scope, IsRequired = false, AllowEmpty = true)] - public string Scope { get; set; } + [MessagePart(Protocol.scope, IsRequired = false, AllowEmpty = true, Encoder = typeof(ScopeEncoder))] + public ICollection<string> Scope { get; private set; } /// <summary> /// Gets or sets the authorizing user's account name. diff --git a/src/DotNetOpenAuth/OAuth2/OAuthStrings.Designer.cs b/src/DotNetOpenAuth/OAuth2/OAuthStrings.Designer.cs index 59099be..f4a74cc 100644 --- a/src/DotNetOpenAuth/OAuth2/OAuthStrings.Designer.cs +++ b/src/DotNetOpenAuth/OAuth2/OAuthStrings.Designer.cs @@ -122,5 +122,14 @@ namespace DotNetOpenAuth.OAuth2 { return ResourceManager.GetString("NoGrantNoRefreshToken", resourceCulture); } } + + /// <summary> + /// Looks up a localized string similar to Individual scopes may not contain spaces.. + /// </summary> + internal static string ScopesMayNotContainSpaces { + get { + return ResourceManager.GetString("ScopesMayNotContainSpaces", resourceCulture); + } + } } } diff --git a/src/DotNetOpenAuth/OAuth2/OAuthStrings.resx b/src/DotNetOpenAuth/OAuth2/OAuthStrings.resx index d88f451..dacd9fc 100644 --- a/src/DotNetOpenAuth/OAuth2/OAuthStrings.resx +++ b/src/DotNetOpenAuth/OAuth2/OAuthStrings.resx @@ -138,4 +138,7 @@ <data name="NoGrantNoRefreshToken" xml:space="preserve"> <value>Refresh tokens should not be granted without the request including an access grant.</value> </data> + <data name="ScopesMayNotContainSpaces" xml:space="preserve"> + <value>Individual scopes may not contain spaces.</value> + </data> </root>
\ No newline at end of file diff --git a/src/DotNetOpenAuth/OAuth2/OAuthUtilities.cs b/src/DotNetOpenAuth/OAuth2/OAuthUtilities.cs index 609e0a6..6b4c42a 100644 --- a/src/DotNetOpenAuth/OAuth2/OAuthUtilities.cs +++ b/src/DotNetOpenAuth/OAuth2/OAuthUtilities.cs @@ -34,6 +34,8 @@ namespace DotNetOpenAuth.OAuth2 { MessagingUtilities.Digits + @"!#$%&'()*+-./:<=>?@[]^_`{|}~\,;"; + public static readonly StringComparer ScopeStringComparer = StringComparer.Ordinal; + /// <summary> /// Determines whether one given scope is a subset of another scope. /// </summary> @@ -64,14 +66,17 @@ namespace DotNetOpenAuth.OAuth2 { /// <param name="scopeComparer">The scope comparer, allowing scopes to be case sensitive or insensitive. /// Usually <see cref="StringComparer.Ordinal"/> or <see cref="StringComparer.OrdinalIgnoreCase"/>.</param> /// <returns></returns> - public static HashSet<string> BreakUpScopes(string scope, StringComparer scopeComparer) { - Contract.Requires<ArgumentNullException>(scopeComparer != null, "scopeComparer"); - + public static HashSet<string> SplitScopes(string scope) { if (string.IsNullOrEmpty(scope)) { return new HashSet<string>(); } - return new HashSet<string>(scope.Split(scopeDelimiter, StringSplitOptions.RemoveEmptyEntries), scopeComparer); + return new HashSet<string>(scope.Split(scopeDelimiter, StringSplitOptions.RemoveEmptyEntries), ScopeStringComparer); + } + + public static string JoinScopes(HashSet<string> scopes) { + Contract.Requires<ArgumentNullException>(scopes != null, "scopes"); + return string.Join(" ", scopes.ToArray()); } /// <summary> diff --git a/src/DotNetOpenAuth/OAuth2/ResourceServer.cs b/src/DotNetOpenAuth/OAuth2/ResourceServer.cs index f013a5e..a2afdae 100644 --- a/src/DotNetOpenAuth/OAuth2/ResourceServer.cs +++ b/src/DotNetOpenAuth/OAuth2/ResourceServer.cs @@ -63,7 +63,7 @@ namespace DotNetOpenAuth.OAuth2 { /// <param name="username">The name on the account the client has access to.</param> /// <param name="scope">The set of operations the client is authorized for.</param> /// <returns>An error to return to the client if access is not authorized; <c>null</c> if access is granted.</returns> - public OutgoingWebResponse VerifyAccess(out string username, out string scope) { + public OutgoingWebResponse VerifyAccess(out string username, out HashSet<string> scope) { return this.VerifyAccess(this.Channel.GetRequestFromContext(), out username, out scope); } @@ -76,7 +76,7 @@ namespace DotNetOpenAuth.OAuth2 { /// <returns> /// An error to return to the client if access is not authorized; <c>null</c> if access is granted. /// </returns> - public virtual OutgoingWebResponse VerifyAccess(HttpRequestInfo httpRequestInfo, out string username, out string scope) { + public virtual OutgoingWebResponse VerifyAccess(HttpRequestInfo httpRequestInfo, out string username, out HashSet<string> scope) { Contract.Requires<ArgumentNullException>(httpRequestInfo != null, "httpRequestInfo"); AccessProtectedResourceRequest request = null; @@ -113,10 +113,11 @@ namespace DotNetOpenAuth.OAuth2 { /// An error to return to the client if access is not authorized; <c>null</c> if access is granted. /// </returns> public virtual OutgoingWebResponse VerifyAccess(HttpRequestInfo httpRequestInfo, out IPrincipal principal) { - string username, scope; + string username; + HashSet<string> scope; var result = this.VerifyAccess(httpRequestInfo, out username, out scope); if (result == null) { - principal = new OAuth.ChannelElements.OAuthPrincipal(username, scope != null ? scope.Split(' ') : new string[0]); + principal = new OAuth.ChannelElements.OAuthPrincipal(username, scope != null ? scope.ToArray() : new string[0]); } else { principal = null; } diff --git a/src/DotNetOpenAuth/OAuth2/StandardAccessTokenAnalyzer.cs b/src/DotNetOpenAuth/OAuth2/StandardAccessTokenAnalyzer.cs index 5e0ea94..d4d4c2f 100644 --- a/src/DotNetOpenAuth/OAuth2/StandardAccessTokenAnalyzer.cs +++ b/src/DotNetOpenAuth/OAuth2/StandardAccessTokenAnalyzer.cs @@ -6,6 +6,7 @@ namespace DotNetOpenAuth.OAuth2 { using System; + using System.Collections.Generic; using System.Security.Cryptography; using DotNetOpenAuth.Messaging; using DotNetOpenAuth.OAuth2.ChannelElements; @@ -50,11 +51,11 @@ namespace DotNetOpenAuth.OAuth2 { /// This method also responsible to throw a <see cref="ProtocolException"/> or return /// <c>false</c> when the access token is expired, invalid, or from an untrusted authorization server. /// </remarks> - public virtual bool TryValidateAccessToken(IDirectedProtocolMessage message, string accessToken, out string user, out string scope) { + public virtual bool TryValidateAccessToken(IDirectedProtocolMessage message, string accessToken, out string user, out HashSet<string> scope) { var accessTokenFormatter = AccessToken.CreateFormatter(this.AuthorizationServerPublicSigningKey, this.ResourceServerPrivateEncryptionKey); var token = accessTokenFormatter.Deserialize(message, accessToken); user = token.User; - scope = token.Scope; + scope = new HashSet<string>(token.Scope, OAuthUtilities.ScopeStringComparer); return true; } } diff --git a/src/DotNetOpenAuth/OAuth2/UserAgentClient.cs b/src/DotNetOpenAuth/OAuth2/UserAgentClient.cs index b848ec4..367a36a 100644 --- a/src/DotNetOpenAuth/OAuth2/UserAgentClient.cs +++ b/src/DotNetOpenAuth/OAuth2/UserAgentClient.cs @@ -42,8 +42,8 @@ namespace DotNetOpenAuth.OAuth2 { /// </summary> /// <param name="scope">The scope of authorized access requested.</param> /// <returns>A fully-qualified URL suitable to initiate the authorization flow.</returns> - public Uri RequestUserAuthorization(string scope = null) { - var authorization = new AuthorizationState { Scope = scope }; + public Uri RequestUserAuthorization(IEnumerable<string> scope = null) { + var authorization = new AuthorizationState(scope); return this.RequestUserAuthorization(authorization); } @@ -63,9 +63,9 @@ namespace DotNetOpenAuth.OAuth2 { var request = new EndUserAuthorizationRequest(this.AuthorizationServer) { ClientIdentifier = this.ClientIdentifier, - Scope = authorization.Scope, Callback = authorization.Callback, }; + request.Scope.ResetContents(authorization.Scope); return this.Channel.PrepareResponse(request).GetDirectUriRequest(this.Channel); } diff --git a/src/DotNetOpenAuth/OAuth2/WebServerClient.cs b/src/DotNetOpenAuth/OAuth2/WebServerClient.cs index 061c58c..091b022 100644 --- a/src/DotNetOpenAuth/OAuth2/WebServerClient.cs +++ b/src/DotNetOpenAuth/OAuth2/WebServerClient.cs @@ -40,7 +40,7 @@ namespace DotNetOpenAuth.OAuth2 { /// </summary> /// <param name="scope">The scope of authorized access requested.</param> /// <returns>The authorization request as an HTTP response that causes a redirect.</returns> - public OutgoingWebResponse RequestUserAuthorization(string scope = null) { + public OutgoingWebResponse RequestUserAuthorization(IEnumerable<string> scope = null) { var response = this.PrepareRequestUserAuthorization(scope); return this.Channel.PrepareResponse(response); } @@ -50,8 +50,8 @@ namespace DotNetOpenAuth.OAuth2 { /// </summary> /// <param name="scope">The scope of authorized access requested.</param> /// <returns>The authorization request.</returns> - public EndUserAuthorizationRequest PrepareRequestUserAuthorization(string scope = null) { - var authorizationState = new AuthorizationState { Scope = scope }; + public EndUserAuthorizationRequest PrepareRequestUserAuthorization(IEnumerable<string> scopes = null) { + var authorizationState = new AuthorizationState(scopes); return this.PrepareRequestUserAuthorization(authorizationState); } @@ -78,8 +78,8 @@ namespace DotNetOpenAuth.OAuth2 { var request = new EndUserAuthorizationRequest(this.AuthorizationServer) { ClientIdentifier = this.ClientIdentifier, Callback = authorization.Callback, - Scope = authorization.Scope, }; + request.Scope.ResetContents(authorization.Scope); return request; } |