//-----------------------------------------------------------------------
//
// Copyright (c) Outercurve Foundation. All rights reserved.
//
//-----------------------------------------------------------------------
namespace DotNetOpenAuth.OAuth2 {
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Security.Claims;
using System.Security.Principal;
using System.ServiceModel.Channels;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using System.Web;
using ChannelElements;
using DotNetOpenAuth.Logging;
using DotNetOpenAuth.OAuth.ChannelElements;
using Messages;
using Messaging;
using Validation;
///
/// Provides services for validating OAuth access tokens.
///
public class ResourceServer {
///
/// A reusable instance of the scope satisfied checker.
///
private static readonly IScopeSatisfiedCheck DefaultScopeSatisfiedCheck = new StandardScopeSatisfiedCheck();
///
/// Initializes a new instance of the class.
///
/// The access token analyzer.
public ResourceServer(IAccessTokenAnalyzer accessTokenAnalyzer) {
Requires.NotNull(accessTokenAnalyzer, "accessTokenAnalyzer");
this.AccessTokenAnalyzer = accessTokenAnalyzer;
this.Channel = new OAuth2ResourceServerChannel();
this.ResourceOwnerPrincipalPrefix = string.Empty;
this.ClientPrincipalPrefix = "client:";
this.ScopeSatisfiedCheck = DefaultScopeSatisfiedCheck;
}
///
/// Gets the access token analyzer.
///
/// The access token analyzer.
public IAccessTokenAnalyzer AccessTokenAnalyzer { get; private set; }
///
/// Gets or sets the service that checks whether a granted set of scopes satisfies a required set of scopes.
///
public IScopeSatisfiedCheck ScopeSatisfiedCheck { get; set; }
///
/// Gets or sets the prefix to apply to a resource owner's username when used as the username in an .
///
/// The default value is the empty string.
public string ResourceOwnerPrincipalPrefix { get; set; }
///
/// Gets or sets the prefix to apply to a client identifier when used as the username in an .
///
/// The default value is "client:"
public string ClientPrincipalPrefix { get; set; }
///
/// Gets the channel.
///
/// The channel.
internal OAuth2ResourceServerChannel Channel { get; private set; }
///
/// Discovers what access the client should have considering the access token in the current request.
///
/// The HTTP request info.
/// The cancellation token.
/// The set of scopes required to approve this request.
///
/// The access token describing the authorization the client has. Never null.
///
/// Thrown when the client is not authorized. This exception should be caught and the
/// message should be returned to the client.
public virtual Task GetAccessTokenAsync(HttpRequestBase httpRequestInfo = null, CancellationToken cancellationToken = default(CancellationToken), params string[] requiredScopes) {
Requires.NotNull(requiredScopes, "requiredScopes");
RequiresEx.ValidState(this.ScopeSatisfiedCheck != null, Strings.RequiredPropertyNotYetPreset);
httpRequestInfo = httpRequestInfo ?? this.Channel.GetRequestFromContext();
return this.GetAccessTokenAsync(httpRequestInfo.AsHttpRequestMessage(), cancellationToken, requiredScopes);
}
///
/// Discovers what access the client should have considering the access token in the current request.
///
/// The request message.
/// The cancellation token.
/// The set of scopes required to approve this request.
///
/// The access token describing the authorization the client has. Never null.
///
/// Thrown when the client is not authorized. This exception should be caught and the
/// message should be returned to the client.
public virtual async Task GetAccessTokenAsync(HttpRequestMessage requestMessage, CancellationToken cancellationToken = default(CancellationToken), params string[] requiredScopes) {
Requires.NotNull(requestMessage, "requestMessage");
Requires.NotNull(requiredScopes, "requiredScopes");
RequiresEx.ValidState(this.ScopeSatisfiedCheck != null, Strings.RequiredPropertyNotYetPreset);
AccessToken accessToken;
AccessProtectedResourceRequest request = null;
try {
request = await this.Channel.TryReadFromRequestAsync(requestMessage, cancellationToken);
if (request != null) {
accessToken = this.AccessTokenAnalyzer.DeserializeAccessToken(request, request.AccessToken);
ErrorUtilities.VerifyHost(accessToken != null, "IAccessTokenAnalyzer.DeserializeAccessToken returned a null result.");
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);
}
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 = UnauthorizedResponse.InvalidRequest(ex);
throw new ProtocolFaultResponseException(this.Channel, response, innerException: ex);
}
} catch (ProtocolException 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);
}
}
///
/// Discovers what access the client should have considering the access token in the current request.
///
/// The HTTP request info.
/// The cancellation token.
/// The set of scopes required to approve this request.
///
/// The principal that contains the user and roles that the access token is authorized for. Never null.
///
/// Thrown when the client is not authorized. This exception should be caught and the
/// message should be returned to the client.
public virtual async Task GetPrincipalAsync(HttpRequestBase httpRequestInfo = null, CancellationToken cancellationToken = default(CancellationToken), params string[] requiredScopes) {
AccessToken accessToken = await this.GetAccessTokenAsync(httpRequestInfo, cancellationToken, 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;
return OAuthPrincipal.CreatePrincipal(principalUserName, accessToken.Scope);
}
///
/// Discovers what access the client should have considering the access token in the current request.
///
/// HTTP details from an incoming WCF message.
/// The URI of the WCF service endpoint.
/// The cancellation token.
/// The set of scopes required to approve this request.
///
/// The principal that contains the user and roles that the access token is authorized for. Never null.
///
/// Thrown when the client is not authorized. This exception should be caught and the
/// message should be returned to the client.
public virtual Task GetPrincipalAsync(HttpRequestMessageProperty request, Uri requestUri, CancellationToken cancellationToken = default(CancellationToken), params string[] requiredScopes) {
Requires.NotNull(request, "request");
Requires.NotNull(requestUri, "requestUri");
return this.GetPrincipalAsync(new HttpRequestInfo(request, requestUri), cancellationToken, requiredScopes);
}
///
/// Discovers what access the client should have considering the access token in the current request.
///
/// HTTP details from an incoming HTTP request message.
/// The cancellation token.
/// The set of scopes required to approve this request.
///
/// The principal that contains the user and roles that the access token is authorized for. Never null.
///
/// Thrown when the client is not authorized. This exception should be caught and the
/// message should be returned to the client.
public Task GetPrincipalAsync(HttpRequestMessage request, CancellationToken cancellationToken = default(CancellationToken), params string[] requiredScopes) {
Requires.NotNull(request, "request");
return this.GetPrincipalAsync(new HttpRequestInfo(request), cancellationToken, requiredScopes);
}
}
}