//----------------------------------------------------------------------- // // 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); } } }