summaryrefslogtreecommitdiffstats
path: root/src/DotNetOpenAuth.OAuth2.ResourceServer
diff options
context:
space:
mode:
Diffstat (limited to 'src/DotNetOpenAuth.OAuth2.ResourceServer')
-rw-r--r--src/DotNetOpenAuth.OAuth2.ResourceServer/Configuration/OAuth2ResourceServerSection.cs36
-rw-r--r--src/DotNetOpenAuth.OAuth2.ResourceServer/DotNetOpenAuth.OAuth2.ResourceServer.csproj4
-rw-r--r--src/DotNetOpenAuth.OAuth2.ResourceServer/OAuth2/ChannelElements/OAuth2ResourceServerChannel.cs153
-rw-r--r--src/DotNetOpenAuth.OAuth2.ResourceServer/OAuth2/IAccessTokenAnalyzer.cs57
-rw-r--r--src/DotNetOpenAuth.OAuth2.ResourceServer/OAuth2/ResourceServer.cs135
-rw-r--r--src/DotNetOpenAuth.OAuth2.ResourceServer/OAuth2/ResourceServerStrings.Designer.cs18
-rw-r--r--src/DotNetOpenAuth.OAuth2.ResourceServer/OAuth2/ResourceServerStrings.resx6
-rw-r--r--src/DotNetOpenAuth.OAuth2.ResourceServer/OAuth2/StandardAccessTokenAnalyzer.cs58
8 files changed, 420 insertions, 47 deletions
diff --git a/src/DotNetOpenAuth.OAuth2.ResourceServer/Configuration/OAuth2ResourceServerSection.cs b/src/DotNetOpenAuth.OAuth2.ResourceServer/Configuration/OAuth2ResourceServerSection.cs
new file mode 100644
index 0000000..3e37018
--- /dev/null
+++ b/src/DotNetOpenAuth.OAuth2.ResourceServer/Configuration/OAuth2ResourceServerSection.cs
@@ -0,0 +1,36 @@
+//-----------------------------------------------------------------------
+// <copyright file="OAuth2ResourceServerSection.cs" company="Outercurve Foundation">
+// Copyright (c) Outercurve Foundation. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.Configuration {
+ using System.Configuration;
+ using System.Diagnostics.Contracts;
+
+ /// <summary>
+ /// Represents the &lt;oauth2/resourceServer&gt; section in the host's .config file.
+ /// </summary>
+ internal class OAuth2ResourceServerSection : ConfigurationElement {
+ /// <summary>
+ /// The name of the oauth2/client section.
+ /// </summary>
+ private const string SectionName = OAuth2SectionGroup.SectionName + "/resourceServer";
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="OAuth2ResourceServerSection"/> class.
+ /// </summary>
+ internal OAuth2ResourceServerSection() {
+ }
+
+ /// <summary>
+ /// Gets the configuration section from the .config file.
+ /// </summary>
+ internal static OAuth2ResourceServerSection Configuration {
+ get {
+ Contract.Ensures(Contract.Result<OAuth2ResourceServerSection>() != null);
+ return (OAuth2ResourceServerSection)ConfigurationManager.GetSection(SectionName) ?? new OAuth2ResourceServerSection();
+ }
+ }
+ }
+}
diff --git a/src/DotNetOpenAuth.OAuth2.ResourceServer/DotNetOpenAuth.OAuth2.ResourceServer.csproj b/src/DotNetOpenAuth.OAuth2.ResourceServer/DotNetOpenAuth.OAuth2.ResourceServer.csproj
index 63806b8..eb54fee 100644
--- a/src/DotNetOpenAuth.OAuth2.ResourceServer/DotNetOpenAuth.OAuth2.ResourceServer.csproj
+++ b/src/DotNetOpenAuth.OAuth2.ResourceServer/DotNetOpenAuth.OAuth2.ResourceServer.csproj
@@ -18,12 +18,16 @@
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
</PropertyGroup>
<ItemGroup>
+ <Compile Include="Configuration\OAuth2ResourceServerSection.cs" />
+ <Compile Include="OAuth2\ChannelElements\OAuth2ResourceServerChannel.cs" />
+ <Compile Include="OAuth2\IAccessTokenAnalyzer.cs" />
<Compile Include="OAuth2\ResourceServerStrings.Designer.cs">
<AutoGen>True</AutoGen>
<DesignTime>True</DesignTime>
<DependentUpon>ResourceServerStrings.resx</DependentUpon>
</Compile>
<Compile Include="OAuth2\ResourceServer.cs" />
+ <Compile Include="OAuth2\StandardAccessTokenAnalyzer.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
</ItemGroup>
<ItemGroup>
diff --git a/src/DotNetOpenAuth.OAuth2.ResourceServer/OAuth2/ChannelElements/OAuth2ResourceServerChannel.cs b/src/DotNetOpenAuth.OAuth2.ResourceServer/OAuth2/ChannelElements/OAuth2ResourceServerChannel.cs
new file mode 100644
index 0000000..e9d596a
--- /dev/null
+++ b/src/DotNetOpenAuth.OAuth2.ResourceServer/OAuth2/ChannelElements/OAuth2ResourceServerChannel.cs
@@ -0,0 +1,153 @@
+//-----------------------------------------------------------------------
+// <copyright file="OAuth2ResourceServerChannel.cs" company="Outercurve Foundation">
+// Copyright (c) Outercurve Foundation. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.OAuth2.ChannelElements {
+ using System;
+ using System.Collections.Generic;
+ using System.Diagnostics.Contracts;
+ using System.Linq;
+ using System.Net;
+ using System.Net.Mime;
+ using System.Text;
+ using System.Web;
+ using DotNetOpenAuth.Messaging;
+ using DotNetOpenAuth.Messaging.Reflection;
+ using DotNetOpenAuth.OAuth2.Messages;
+
+ /// <summary>
+ /// The channel for the OAuth protocol.
+ /// </summary>
+ internal class OAuth2ResourceServerChannel : StandardMessageFactoryChannel {
+ /// <summary>
+ /// The messages receivable by this channel.
+ /// </summary>
+ private static readonly Type[] MessageTypes = new Type[] {
+ typeof(Messages.AccessProtectedResourceRequest),
+ };
+
+ /// <summary>
+ /// The protocol versions supported by this channel.
+ /// </summary>
+ private static readonly Version[] Versions = Protocol.AllVersions.Select(v => v.Version).ToArray();
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="OAuth2ResourceServerChannel"/> class.
+ /// </summary>
+ protected internal OAuth2ResourceServerChannel()
+ : base(MessageTypes, Versions) {
+ // TODO: add signing (authenticated request) binding element.
+ }
+
+ /// <summary>
+ /// Gets the protocol message that may be embedded in the given HTTP request.
+ /// </summary>
+ /// <param name="request">The request to search for an embedded message.</param>
+ /// <returns>
+ /// The deserialized message, if one is found. Null otherwise.
+ /// </returns>
+ protected override IDirectedProtocolMessage ReadFromRequestCore(HttpRequestBase request) {
+ var fields = new Dictionary<string, string>();
+ string accessToken;
+ if ((accessToken = SearchForBearerAccessTokenInRequest(request)) != null) {
+ fields[Protocol.token_type] = Protocol.AccessTokenTypes.Bearer;
+ fields[Protocol.access_token] = accessToken;
+ }
+
+ if (fields.Count > 0) {
+ MessageReceivingEndpoint recipient;
+ try {
+ recipient = request.GetRecipient();
+ } catch (ArgumentException ex) {
+ Logger.OAuth.WarnFormat("Unrecognized HTTP request: " + ex.ToString());
+ return null;
+ }
+
+ // Deserialize the message using all the data we've collected.
+ var message = (IDirectedProtocolMessage)this.Receive(fields, recipient);
+ return message;
+ }
+
+ return null;
+ }
+
+ /// <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>
+ /// <returns>
+ /// The deserialized message parts, if found. Null otherwise.
+ /// </returns>
+ /// <exception cref="ProtocolException">Thrown when the response is not valid.</exception>
+ protected override IDictionary<string, string> ReadFromResponseCore(IncomingWebResponse response) {
+ // We never expect resource servers to send out direct requests,
+ // and therefore won't have direct responses.
+ throw new NotImplementedException();
+ }
+
+ /// <summary>
+ /// Queues a message for sending in the response stream where the fields
+ /// are sent in the response stream in querystring style.
+ /// </summary>
+ /// <param name="response">The message to send as a response.</param>
+ /// <returns>
+ /// The pending user agent redirect based message to be sent as an HttpResponse.
+ /// </returns>
+ /// <remarks>
+ /// This method implements spec OAuth V1.0 section 5.3.
+ /// </remarks>
+ protected override OutgoingWebResponse PrepareDirectResponse(IProtocolMessage response) {
+ var webResponse = new OutgoingWebResponse();
+
+ // 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.");
+
+ // First initialize based on the specifics within the message.
+ ApplyMessageTemplate(response, webResponse);
+ if (!(response is IHttpDirectResponse)) {
+ webResponse.Status = HttpStatusCode.Unauthorized;
+ }
+
+ // Now serialize all the message parts into the WWW-Authenticate header.
+ var fields = this.MessageDescriptions.GetAccessor(response);
+ webResponse.Headers[HttpResponseHeader.WwwAuthenticate] = MessagingUtilities.AssembleAuthorizationHeader(unauthorizedResponse.Scheme, fields);
+ return webResponse;
+ }
+
+ /// <summary>
+ /// Searches for a bearer access token in the request.
+ /// </summary>
+ /// <param name="request">The request.</param>
+ /// <returns>The bearer access token, if one exists. Otherwise <c>null</c>.</returns>
+ private static string SearchForBearerAccessTokenInRequest(HttpRequestBase request) {
+ Requires.NotNull(request, "request");
+
+ // First search the authorization header.
+ string authorizationHeader = request.Headers[HttpRequestHeaders.Authorization];
+ if (!string.IsNullOrEmpty(authorizationHeader) && authorizationHeader.StartsWith(Protocol.BearerHttpAuthorizationSchemeWithTrailingSpace, StringComparison.OrdinalIgnoreCase)) {
+ return authorizationHeader.Substring(Protocol.BearerHttpAuthorizationSchemeWithTrailingSpace.Length);
+ }
+
+ // Failing that, scan the entity
+ if (!string.IsNullOrEmpty(request.Headers[HttpRequestHeaders.ContentType])) {
+ var contentType = new ContentType(request.Headers[HttpRequestHeaders.ContentType]);
+ if (string.Equals(contentType.MediaType, HttpFormUrlEncoded, StringComparison.Ordinal)) {
+ if (request.Form[Protocol.BearerTokenEncodedUrlParameterName] != null) {
+ return request.Form[Protocol.BearerTokenEncodedUrlParameterName];
+ }
+ }
+ }
+
+ // Finally, check the least desirable location: the query string
+ var unrewrittenQuery = request.GetQueryStringBeforeRewriting();
+ if (!string.IsNullOrEmpty(unrewrittenQuery[Protocol.BearerTokenEncodedUrlParameterName])) {
+ return unrewrittenQuery[Protocol.BearerTokenEncodedUrlParameterName];
+ }
+
+ return null;
+ }
+ }
+}
diff --git a/src/DotNetOpenAuth.OAuth2.ResourceServer/OAuth2/IAccessTokenAnalyzer.cs b/src/DotNetOpenAuth.OAuth2.ResourceServer/OAuth2/IAccessTokenAnalyzer.cs
new file mode 100644
index 0000000..5c5a526
--- /dev/null
+++ b/src/DotNetOpenAuth.OAuth2.ResourceServer/OAuth2/IAccessTokenAnalyzer.cs
@@ -0,0 +1,57 @@
+//-----------------------------------------------------------------------
+// <copyright file="IAccessTokenAnalyzer.cs" company="Outercurve Foundation">
+// Copyright (c) Outercurve Foundation. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.OAuth2 {
+ using System;
+ using System.Collections.Generic;
+ using System.Diagnostics.CodeAnalysis;
+ using System.Diagnostics.Contracts;
+ using System.Linq;
+ using System.Text;
+ using DotNetOpenAuth.Messaging;
+
+ /// <summary>
+ /// An interface that resource server hosts should implement if they accept access tokens
+ /// issued by non-DotNetOpenAuth authorization servers.
+ /// </summary>
+ [ContractClass((typeof(IAccessTokenAnalyzerContract)))]
+ public interface IAccessTokenAnalyzer {
+ /// <summary>
+ /// Reads an access token to find out what data it authorizes access to.
+ /// </summary>
+ /// <param name="message">The message carrying the access token.</param>
+ /// <param name="accessToken">The access token's serialized representation.</param>
+ /// <returns>The deserialized, validated token.</returns>
+ /// <exception cref="ProtocolException">Thrown if the access token is expired, invalid, or from an untrusted authorization server.</exception>
+ AccessToken DeserializeAccessToken(IDirectedProtocolMessage message, string accessToken);
+ }
+
+ /// <summary>
+ /// Code contract for the <see cref="IAccessTokenAnalyzer"/> interface.
+ /// </summary>
+ [ContractClassFor(typeof(IAccessTokenAnalyzer))]
+ internal abstract class IAccessTokenAnalyzerContract : IAccessTokenAnalyzer {
+ /// <summary>
+ /// Prevents a default instance of the <see cref="IAccessTokenAnalyzerContract"/> class from being created.
+ /// </summary>
+ private IAccessTokenAnalyzerContract() {
+ }
+
+ /// <summary>
+ /// Reads an access token to find out what data it authorizes access to.
+ /// </summary>
+ /// <param name="message">The message carrying the access token.</param>
+ /// <param name="accessToken">The access token's serialized representation.</param>
+ /// <returns>The deserialized, validated token.</returns>
+ /// <exception cref="ProtocolException">Thrown if the access token is expired, invalid, or from an untrusted authorization server.</exception>
+ AccessToken IAccessTokenAnalyzer.DeserializeAccessToken(IDirectedProtocolMessage message, string accessToken) {
+ Requires.NotNull(message, "message");
+ Requires.NotNullOrEmpty(accessToken, "accessToken");
+ Contract.Ensures(Contract.Result<AccessToken>() != null);
+ throw new NotImplementedException();
+ }
+ }
+}
diff --git a/src/DotNetOpenAuth.OAuth2.ResourceServer/OAuth2/ResourceServer.cs b/src/DotNetOpenAuth.OAuth2.ResourceServer/OAuth2/ResourceServer.cs
index 2830ab8..cd0fb55 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>
@@ -34,6 +39,9 @@ namespace DotNetOpenAuth.OAuth2 {
this.AccessTokenAnalyzer = accessTokenAnalyzer;
this.Channel = new OAuth2ResourceServerChannel();
+ this.ResourceOwnerPrincipalPrefix = string.Empty;
+ this.ClientPrincipalPrefix = "client:";
+ this.ScopeSatisfiedCheck = DefaultScopeSatisfiedCheck;
}
/// <summary>
@@ -43,59 +51,78 @@ namespace DotNetOpenAuth.OAuth2 {
public IAccessTokenAnalyzer AccessTokenAnalyzer { get; private set; }
/// <summary>
- /// Gets the channel.
+ /// Gets or sets the service that checks whether a granted set of scopes satisfies a required set of scopes.
/// </summary>
- /// <value>The channel.</value>
- internal OAuth2ResourceServerChannel Channel { get; private set; }
+ public IScopeSatisfiedCheck ScopeSatisfiedCheck { get; set; }
/// <summary>
- /// Discovers what access the client should have considering the access token in the current request.
+ /// Gets or sets the prefix to apply to a resource owner's username when used as the username in an <see cref="IPrincipal"/>.
/// </summary>
- /// <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>
- [SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "0#", Justification = "Try pattern")]
- [SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "1#", Justification = "Try pattern")]
- public OutgoingWebResponse VerifyAccess(out string userName, out HashSet<string> scope) {
- return this.VerifyAccess(this.Channel.GetRequestFromContext(), out userName, out scope);
- }
+ /// <value>The default value is the empty string.</value>
+ public string ResourceOwnerPrincipalPrefix { get; set; }
+
+ /// <summary>
+ /// Gets or sets the prefix to apply to a client identifier when used as the username in an <see cref="IPrincipal"/>.
+ /// </summary>
+ /// <value>The default value is "client:"</value>
+ public string ClientPrincipalPrefix { get; set; }
+
+ /// <summary>
+ /// Gets the channel.
+ /// </summary>
+ /// <value>The channel.</value>
+ internal OAuth2ResourceServerChannel Channel { get; private set; }
/// <summary>
/// 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="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>
+ /// <param name="requiredScopes">The set of scopes required to approve this request.</param>
/// <returns>
- /// An error to return to the client if access is not authorized; <c>null</c> if access is granted.
+ /// The access token describing the authorization the client has. Never <c>null</c>.
/// </returns>
- [SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "1#", Justification = "Try pattern")]
- [SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "2#", Justification = "Try pattern")]
- public virtual OutgoingWebResponse VerifyAccess(HttpRequestBase httpRequestInfo, out string userName, out HashSet<string> scope) {
- Requires.NotNull(httpRequestInfo, "httpRequestInfo");
+ /// <exception cref="ProtocolFaultResponseException">
+ /// Thrown when the client is not authorized. This exception should be caught and the
+ /// <see cref="ProtocolFaultResponseException.ErrorResponseMessage"/> message should be returned to the client.
+ /// </exception>
+ 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();
+ }
+ AccessToken accessToken;
AccessProtectedResourceRequest request = null;
try {
if (this.Channel.TryReadFromRequest<AccessProtectedResourceRequest>(httpRequestInfo, out request)) {
- if (this.AccessTokenAnalyzer.TryValidateAccessToken(request, request.AccessToken, out userName, out scope)) {
- // No errors to return.
- return null;
+ accessToken = this.AccessTokenAnalyzer.DeserializeAccessToken(request, request.AccessToken);
+ ErrorUtilities.VerifyHost(accessToken != null, "IAccessTokenAnalyzer.DeserializeAccessToken returned a null reslut.");
+ if (string.IsNullOrEmpty(accessToken.User) && string.IsNullOrEmpty(accessToken.ClientIdentifier)) {
+ Logger.OAuth.Error("Access token rejected because both the username and client id properties were null or empty.");
+ ErrorUtilities.ThrowProtocol(ResourceServerStrings.InvalidAccessToken);
}
- throw ErrorUtilities.ThrowProtocol(ResourceServerStrings.InvalidAccessToken);
- } else {
- var response = new UnauthorizedResponse(new ProtocolException(ResourceServerStrings.MissingAccessToken));
+ 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);
+ }
- userName = null;
- scope = null;
- return this.Channel.PrepareResponse(response);
+ return accessToken;
+ } else {
+ var ex = new ProtocolException(ResourceServerStrings.MissingAccessToken);
+ 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;
+ }
- userName = null;
- scope = null;
- return this.Channel.PrepareResponse(response);
+ var response = request != null ? UnauthorizedResponse.InvalidToken(request, ex) : UnauthorizedResponse.InvalidRequest(ex);
+ throw new ProtocolFaultResponseException(this.Channel, response, innerException: ex);
}
}
@@ -103,17 +130,29 @@ 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="principal">The principal that contains the user and roles that the access token is authorized for.</param>
+ /// <param name="requiredScopes">The set of scopes required to approve this request.</param>
/// <returns>
- /// An error to return to the client if access is not authorized; <c>null</c> if access is granted.
+ /// The principal that contains the user and roles that the access token is authorized for. Never <c>null</c>.
/// </returns>
- [SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "1#", Justification = "Try pattern")]
- public virtual OutgoingWebResponse VerifyAccess(HttpRequestBase httpRequestInfo, out IPrincipal principal) {
- string username;
- HashSet<string> scope;
- var result = this.VerifyAccess(httpRequestInfo, out username, out scope);
- principal = result == null ? new OAuthPrincipal(username, scope != null ? scope.ToArray() : new string[0]) : null;
- return result;
+ /// <exception cref="ProtocolFaultResponseException">
+ /// Thrown when the client is not authorized. This exception should be caught and the
+ /// <see cref="ProtocolFaultResponseException.ErrorResponseMessage"/> message should be returned to the client.
+ /// </exception>
+ 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.
+ ErrorUtilities.VerifyProtocol(accessToken.User == null || string.IsNullOrEmpty(this.ClientPrincipalPrefix) || !accessToken.User.StartsWith(this.ClientPrincipalPrefix, StringComparison.OrdinalIgnoreCase), ResourceServerStrings.ResourceOwnerNameLooksLikeClientIdentifier);
+ ErrorUtilities.VerifyProtocol(accessToken.ClientIdentifier == null || string.IsNullOrEmpty(this.ResourceOwnerPrincipalPrefix) || !accessToken.ClientIdentifier.StartsWith(this.ResourceOwnerPrincipalPrefix, StringComparison.OrdinalIgnoreCase), ResourceServerStrings.ClientIdentifierLooksLikeResourceOwnerName);
+
+ string principalUserName = !string.IsNullOrEmpty(accessToken.User)
+ ? this.ResourceOwnerPrincipalPrefix + accessToken.User
+ : this.ClientPrincipalPrefix + accessToken.ClientIdentifier;
+ string[] principalScope = accessToken.Scope != null ? accessToken.Scope.ToArray() : new string[0];
+ var principal = new OAuthPrincipal(principalUserName, principalScope);
+
+ return principal;
}
/// <summary>
@@ -121,17 +160,19 @@ 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="principal">The principal that contains the user and roles that the access token is authorized for.</param>
+ /// <param name="requiredScopes">The set of scopes required to approve this request.</param>
/// <returns>
- /// An error to return to the client if access is not authorized; <c>null</c> if access is granted.
+ /// The principal that contains the user and roles that the access token is authorized for. Never <c>null</c>.
/// </returns>
- [SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "1#", Justification = "Try pattern")]
- [SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "2#", Justification = "Try pattern")]
- public virtual OutgoingWebResponse VerifyAccess(HttpRequestMessageProperty request, Uri requestUri, out IPrincipal principal) {
+ /// <exception cref="ProtocolFaultResponseException">
+ /// Thrown when the client is not authorized. This exception should be caught and the
+ /// <see cref="ProtocolFaultResponseException.ErrorResponseMessage"/> message should be returned to the client.
+ /// </exception>
+ public virtual IPrincipal GetPrincipal(HttpRequestMessageProperty request, Uri requestUri, params string[] requiredScopes) {
Requires.NotNull(request, "request");
Requires.NotNull(requestUri, "requestUri");
- return this.VerifyAccess(new HttpRequestInfo(request, requestUri), out principal);
+ return this.GetPrincipal(new HttpRequestInfo(request, requestUri), requiredScopes);
}
}
}
diff --git a/src/DotNetOpenAuth.OAuth2.ResourceServer/OAuth2/ResourceServerStrings.Designer.cs b/src/DotNetOpenAuth.OAuth2.ResourceServer/OAuth2/ResourceServerStrings.Designer.cs
index 606b072..f97b41b 100644
--- a/src/DotNetOpenAuth.OAuth2.ResourceServer/OAuth2/ResourceServerStrings.Designer.cs
+++ b/src/DotNetOpenAuth.OAuth2.ResourceServer/OAuth2/ResourceServerStrings.Designer.cs
@@ -61,6 +61,15 @@ namespace DotNetOpenAuth.OAuth2 {
}
/// <summary>
+ /// Looks up a localized string similar to Client Identifier starts with a resource owner prefix. Authorization aborted..
+ /// </summary>
+ internal static string ClientIdentifierLooksLikeResourceOwnerName {
+ get {
+ return ResourceManager.GetString("ClientIdentifierLooksLikeResourceOwnerName", resourceCulture);
+ }
+ }
+
+ /// <summary>
/// Looks up a localized string similar to Invalid access token..
/// </summary>
internal static string InvalidAccessToken {
@@ -77,5 +86,14 @@ namespace DotNetOpenAuth.OAuth2 {
return ResourceManager.GetString("MissingAccessToken", resourceCulture);
}
}
+
+ /// <summary>
+ /// Looks up a localized string similar to Resource owner username starts with a client prefix. Authorization aborted..
+ /// </summary>
+ internal static string ResourceOwnerNameLooksLikeClientIdentifier {
+ get {
+ return ResourceManager.GetString("ResourceOwnerNameLooksLikeClientIdentifier", resourceCulture);
+ }
+ }
}
}
diff --git a/src/DotNetOpenAuth.OAuth2.ResourceServer/OAuth2/ResourceServerStrings.resx b/src/DotNetOpenAuth.OAuth2.ResourceServer/OAuth2/ResourceServerStrings.resx
index 175a386..46943c4 100644
--- a/src/DotNetOpenAuth.OAuth2.ResourceServer/OAuth2/ResourceServerStrings.resx
+++ b/src/DotNetOpenAuth.OAuth2.ResourceServer/OAuth2/ResourceServerStrings.resx
@@ -117,10 +117,16 @@
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
+ <data name="ClientIdentifierLooksLikeResourceOwnerName" xml:space="preserve">
+ <value>Client Identifier starts with a resource owner prefix. Authorization aborted.</value>
+ </data>
<data name="InvalidAccessToken" xml:space="preserve">
<value>Invalid access token.</value>
</data>
<data name="MissingAccessToken" xml:space="preserve">
<value>Missing access token.</value>
</data>
+ <data name="ResourceOwnerNameLooksLikeClientIdentifier" xml:space="preserve">
+ <value>Resource owner username starts with a client prefix. Authorization aborted.</value>
+ </data>
</root> \ No newline at end of file
diff --git a/src/DotNetOpenAuth.OAuth2.ResourceServer/OAuth2/StandardAccessTokenAnalyzer.cs b/src/DotNetOpenAuth.OAuth2.ResourceServer/OAuth2/StandardAccessTokenAnalyzer.cs
new file mode 100644
index 0000000..54d86ff
--- /dev/null
+++ b/src/DotNetOpenAuth.OAuth2.ResourceServer/OAuth2/StandardAccessTokenAnalyzer.cs
@@ -0,0 +1,58 @@
+//-----------------------------------------------------------------------
+// <copyright file="StandardAccessTokenAnalyzer.cs" company="Outercurve Foundation">
+// Copyright (c) Outercurve Foundation. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.OAuth2 {
+ using System;
+ using System.Collections.Generic;
+ using System.Diagnostics.Contracts;
+ using System.Security.Cryptography;
+ using DotNetOpenAuth.Messaging;
+ using DotNetOpenAuth.OAuth2.ChannelElements;
+
+ /// <summary>
+ /// An access token reader that understands DotNetOpenAuth authorization server issued tokens.
+ /// </summary>
+ public class StandardAccessTokenAnalyzer : IAccessTokenAnalyzer {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="StandardAccessTokenAnalyzer"/> class.
+ /// </summary>
+ /// <param name="authorizationServerPublicSigningKey">The crypto service provider with the authorization server public signing key.</param>
+ /// <param name="resourceServerPrivateEncryptionKey">The crypto service provider with the resource server private encryption key.</param>
+ public StandardAccessTokenAnalyzer(RSACryptoServiceProvider authorizationServerPublicSigningKey, RSACryptoServiceProvider resourceServerPrivateEncryptionKey) {
+ Requires.NotNull(authorizationServerPublicSigningKey, "authorizationServerPublicSigningKey");
+ Requires.NotNull(resourceServerPrivateEncryptionKey, "resourceServerPrivateEncryptionKey");
+ Requires.True(!resourceServerPrivateEncryptionKey.PublicOnly, "resourceServerPrivateEncryptionKey");
+ this.AuthorizationServerPublicSigningKey = authorizationServerPublicSigningKey;
+ this.ResourceServerPrivateEncryptionKey = resourceServerPrivateEncryptionKey;
+ }
+
+ /// <summary>
+ /// Gets the authorization server public signing key.
+ /// </summary>
+ /// <value>The authorization server public signing key.</value>
+ public RSACryptoServiceProvider AuthorizationServerPublicSigningKey { get; private set; }
+
+ /// <summary>
+ /// Gets the resource server private encryption key.
+ /// </summary>
+ /// <value>The resource server private encryption key.</value>
+ public RSACryptoServiceProvider ResourceServerPrivateEncryptionKey { get; private set; }
+
+ /// <summary>
+ /// Reads an access token to find out what data it authorizes access to.
+ /// </summary>
+ /// <param name="message">The message carrying the access token.</param>
+ /// <param name="accessToken">The access token's serialized representation.</param>
+ /// <returns>The deserialized, validated token.</returns>
+ /// <exception cref="ProtocolException">Thrown if the access token is expired, invalid, or from an untrusted authorization server.</exception>
+ public virtual AccessToken DeserializeAccessToken(IDirectedProtocolMessage message, string accessToken) {
+ var accessTokenFormatter = AccessToken.CreateFormatter(this.AuthorizationServerPublicSigningKey, this.ResourceServerPrivateEncryptionKey);
+ var token = new AccessToken();
+ accessTokenFormatter.Deserialize(token, message, accessToken, Protocol.access_token);
+ return token;
+ }
+ }
+}