//----------------------------------------------------------------------- // // Copyright (c) Outercurve Foundation. All rights reserved. // //----------------------------------------------------------------------- namespace DotNetOpenAuth.OAuth { using System; using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Net.Http; using System.Security.Principal; using System.ServiceModel.Channels; using System.Threading; using System.Threading.Tasks; using System.Web; using DotNetOpenAuth.Configuration; using DotNetOpenAuth.Messaging; using DotNetOpenAuth.Messaging.Bindings; using DotNetOpenAuth.OAuth.ChannelElements; using DotNetOpenAuth.OAuth.Messages; using Validation; /// /// A web application that allows access via OAuth. /// /// /// The Service Provider’s documentation should include: /// /// The URLs (Request URLs) the Consumer will use when making OAuth requests, and the HTTP methods (i.e. GET, POST, etc.) used in the Request Token URL and Access Token URL. /// Signature methods supported by the Service Provider. /// Any additional request parameters that the Service Provider requires in order to obtain a Token. Service Provider specific parameters MUST NOT begin with oauth_. /// /// public class ServiceProvider : IDisposable { /// /// The name of the key to use in the HttpApplication cache to store the /// instance of to use. /// private const string ApplicationStoreKey = "DotNetOpenAuth.OAuth.ServiceProvider.HttpApplicationStore"; /// /// The length of the verifier code (in raw bytes before base64 encoding) to generate. /// private const int VerifierCodeLength = 5; /// /// The field behind the property. /// private OAuthChannel channel; /// /// Initializes a new instance of the class. /// /// The endpoints and behavior on the Service Provider. /// The host's method of storing and recalling tokens and secrets. public ServiceProvider(ServiceProviderHostDescription serviceDescription, IServiceProviderTokenManager tokenManager) : this(serviceDescription, tokenManager, new OAuthServiceProviderMessageFactory(tokenManager)) { } /// /// Initializes a new instance of the class. /// /// The endpoints and behavior on the Service Provider. /// The host's method of storing and recalling tokens and secrets. /// An object that can figure out what type of message is being received for deserialization. public ServiceProvider(ServiceProviderHostDescription serviceDescription, IServiceProviderTokenManager tokenManager, OAuthServiceProviderMessageFactory messageTypeProvider) : this(serviceDescription, tokenManager, OAuthElement.Configuration.ServiceProvider.ApplicationStore.CreateInstance(GetHttpApplicationStore(), null), messageTypeProvider) { Requires.NotNull(serviceDescription, "serviceDescription"); Requires.NotNull(tokenManager, "tokenManager"); Requires.NotNull(messageTypeProvider, "messageTypeProvider"); } /// /// Initializes a new instance of the class. /// /// The endpoints and behavior on the Service Provider. /// The host's method of storing and recalling tokens and secrets. /// The nonce store. public ServiceProvider(ServiceProviderHostDescription serviceDescription, IServiceProviderTokenManager tokenManager, INonceStore nonceStore) : this(serviceDescription, tokenManager, nonceStore, new OAuthServiceProviderMessageFactory(tokenManager)) { } /// /// Initializes a new instance of the class. /// /// The endpoints and behavior on the Service Provider. /// The host's method of storing and recalling tokens and secrets. /// The nonce store. /// An object that can figure out what type of message is being received for deserialization. public ServiceProvider(ServiceProviderHostDescription serviceDescription, IServiceProviderTokenManager tokenManager, INonceStore nonceStore, OAuthServiceProviderMessageFactory messageTypeProvider) { Requires.NotNull(serviceDescription, "serviceDescription"); Requires.NotNull(tokenManager, "tokenManager"); Requires.NotNull(nonceStore, "nonceStore"); Requires.NotNull(messageTypeProvider, "messageTypeProvider"); var signingElement = serviceDescription.CreateTamperProtectionElement(); this.ServiceDescription = serviceDescription; this.SecuritySettings = OAuthElement.Configuration.ServiceProvider.SecuritySettings.CreateSecuritySettings(); this.OAuthChannel = new OAuthServiceProviderChannel(signingElement, nonceStore, tokenManager, this.SecuritySettings, messageTypeProvider); this.TokenGenerator = new StandardTokenGenerator(); OAuthReporting.RecordFeatureAndDependencyUse(this, serviceDescription, tokenManager, nonceStore); } /// /// Gets the description of this Service Provider. /// public ServiceProviderHostDescription ServiceDescription { get; private set; } /// /// Gets or sets the generator responsible for generating new tokens and secrets. /// public ITokenGenerator TokenGenerator { get; set; } /// /// Gets the persistence store for tokens and secrets. /// public IServiceProviderTokenManager TokenManager { get { return (IServiceProviderTokenManager)this.OAuthChannel.TokenManager; } } /// /// Gets the channel to use for sending/receiving messages. /// public Channel Channel { get { return this.OAuthChannel; } } /// /// Gets the security settings for this service provider. /// public ServiceProviderSecuritySettings SecuritySettings { get; private set; } /// /// Gets or sets the channel to use for sending/receiving messages. /// internal OAuthChannel OAuthChannel { get { return this.channel; } set { Requires.NotNull(value, "value"); this.channel = value; } } /// /// Gets the standard state storage mechanism that uses ASP.NET's /// HttpApplication state dictionary to store associations and nonces. /// /// The HTTP context. If null, this method must be called while is non-null. /// The nonce store. public static INonceStore GetHttpApplicationStore(HttpContextBase context = null) { if (context == null) { ErrorUtilities.VerifyOperation(HttpContext.Current != null, Strings.StoreRequiredWhenNoHttpContextAvailable, typeof(INonceStore).Name); context = new HttpContextWrapper(HttpContext.Current); } var store = (INonceStore)context.Application[ApplicationStoreKey]; if (store == null) { context.Application.Lock(); try { if ((store = (INonceStore)context.Application[ApplicationStoreKey]) == null) { context.Application[ApplicationStoreKey] = store = new NonceMemoryStore(StandardExpirationBindingElement.MaximumMessageAge); } } finally { context.Application.UnLock(); } } return store; } /// /// Creates a cryptographically strong random verification code. /// /// The desired format of the verification code. /// The length of the code. /// When is , /// this is the length of the original byte array before base64 encoding rather than the actual /// length of the final string. /// The verification code. public static string CreateVerificationCode(VerificationCodeFormat format, int length) { Requires.Range(length >= 0, "length"); switch (format) { case VerificationCodeFormat.IncludedInCallback: return MessagingUtilities.GetCryptoRandomDataAsBase64(length); case VerificationCodeFormat.AlphaNumericNoLookAlikes: return MessagingUtilities.GetRandomString(length, MessagingUtilities.AlphaNumericNoLookAlikes); case VerificationCodeFormat.AlphaUpper: return MessagingUtilities.GetRandomString(length, MessagingUtilities.UppercaseLetters); case VerificationCodeFormat.AlphaLower: return MessagingUtilities.GetRandomString(length, MessagingUtilities.LowercaseLetters); case VerificationCodeFormat.Numeric: return MessagingUtilities.GetRandomString(length, MessagingUtilities.Digits); default: throw new ArgumentOutOfRangeException("format"); } } /// /// Reads any incoming OAuth message. /// /// The request. /// The cancellation token. /// /// The deserialized message. /// /// /// Requires HttpContext.Current. /// public Task ReadRequestAsync(HttpRequestBase request = null, CancellationToken cancellationToken = default(CancellationToken)) { return this.ReadRequestAsync((request ?? this.Channel.GetRequestFromContext()).AsHttpRequestMessage(), cancellationToken); } /// /// Reads any incoming OAuth message. /// /// The request. /// The cancellation token. /// /// The deserialized message. /// /// /// Requires HttpContext.Current. /// public Task ReadRequestAsync(HttpRequestMessage request, CancellationToken cancellationToken = default(CancellationToken)) { Requires.NotNull(request, "request"); return this.Channel.ReadFromRequestAsync(request, cancellationToken); } /// /// Reads a request for an unauthorized token from the incoming HTTP request. /// /// The HTTP request to read from. /// The cancellation token. /// /// The incoming request, or null if no OAuth message was attached. /// /// Thrown if an unexpected OAuth message is attached to the incoming request. public Task ReadTokenRequestAsync( HttpRequestBase request = null, CancellationToken cancellationToken = default(CancellationToken)) { return this.ReadTokenRequestAsync((request ?? this.channel.GetRequestFromContext()).AsHttpRequestMessage(), cancellationToken); } /// /// Reads a request for an unauthorized token from the incoming HTTP request. /// /// The HTTP request to read from. /// The cancellation token. /// /// The incoming request, or null if no OAuth message was attached. /// /// Thrown if an unexpected OAuth message is attached to the incoming request. public async Task ReadTokenRequestAsync(HttpRequestMessage request, CancellationToken cancellationToken = default(CancellationToken)) { Requires.NotNull(request, "request"); var message = await this.Channel.TryReadFromRequestAsync(request, cancellationToken); if (message != null) { ErrorUtilities.VerifyProtocol(message.Version >= Protocol.Lookup(this.SecuritySettings.MinimumRequiredOAuthVersion).Version, OAuthStrings.MinimumConsumerVersionRequirementNotMet, this.SecuritySettings.MinimumRequiredOAuthVersion, message.Version); } return message; } /// /// Prepares a message containing an unauthorized token for the Consumer to use in a /// user agent redirect for subsequent authorization. /// /// The token request message the Consumer sent that the Service Provider is now responding to. /// The response message to send using the , after optionally adding extra data to it. public UnauthorizedTokenResponse PrepareUnauthorizedTokenMessage(UnauthorizedTokenRequest request) { Requires.NotNull(request, "request"); string token = this.TokenGenerator.GenerateRequestToken(request.ConsumerKey); string secret = this.TokenGenerator.GenerateSecret(); UnauthorizedTokenResponse response = new UnauthorizedTokenResponse(request, token, secret); return response; } /// /// Reads in a Consumer's request for the Service Provider to obtain permission from /// the user to authorize the Consumer's access of some protected resource(s). /// /// The HTTP request to read from. /// The cancellation token. /// /// The incoming request, or null if no OAuth message was attached. /// /// Thrown if an unexpected OAuth message is attached to the incoming request. public Task ReadAuthorizationRequestAsync(HttpRequestBase request = null, CancellationToken cancellationToken = default(CancellationToken)) { request = request ?? this.channel.GetRequestFromContext(); return this.Channel.TryReadFromRequestAsync(request.AsHttpRequestMessage(), cancellationToken); } /// /// Prepares the message to send back to the consumer following proper authorization of /// a token by an interactive user at the Service Provider's web site. /// /// The Consumer's original authorization request. /// /// The message to send to the Consumer using if one is necessary. /// Null if the Consumer did not request a callback as part of the authorization request. /// [SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "Consistent user experience with instance.")] public UserAuthorizationResponse PrepareAuthorizationResponse(UserAuthorizationRequest request) { Requires.NotNull(request, "request"); // It is very important for us to ignore the oauth_callback argument in the // UserAuthorizationRequest if the Consumer is a 1.0a consumer or else we // open up a security exploit. IServiceProviderRequestToken token = this.TokenManager.GetRequestToken(request.RequestToken); Uri callback; if (request.Version >= Protocol.V10a.Version) { // In OAuth 1.0a, we'll prefer the token-specific callback to the pre-registered one. if (token.Callback != null) { callback = token.Callback; } else { IConsumerDescription consumer = this.TokenManager.GetConsumer(token.ConsumerKey); callback = consumer.Callback; } } else { // In OAuth 1.0, we'll prefer the pre-registered callback over the token-specific one // since 1.0 has a security weakness for user-modified callback URIs. IConsumerDescription consumer = this.TokenManager.GetConsumer(token.ConsumerKey); callback = consumer.Callback ?? request.Callback; } return callback != null ? this.PrepareAuthorizationResponse(request, callback) : null; } /// /// Prepares the message to send back to the consumer following proper authorization of /// a token by an interactive user at the Service Provider's web site. /// /// The Consumer's original authorization request. /// The callback URI the consumer has previously registered /// with this service provider or that came in the . /// /// The message to send to the Consumer using . /// [SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "Consistent user experience with instance.")] public UserAuthorizationResponse PrepareAuthorizationResponse(UserAuthorizationRequest request, Uri callback) { Requires.NotNull(request, "request"); Requires.NotNull(callback, "callback"); var authorization = new UserAuthorizationResponse(callback, request.Version) { RequestToken = request.RequestToken, }; if (authorization.Version >= Protocol.V10a.Version) { authorization.VerificationCode = CreateVerificationCode(VerificationCodeFormat.IncludedInCallback, VerifierCodeLength); } return authorization; } /// /// Reads in a Consumer's request to exchange an authorized request token for an access token. /// /// The HTTP request to read from. /// The cancellation token. /// /// The incoming request, or null if no OAuth message was attached. /// /// Thrown if an unexpected OAuth message is attached to the incoming request. public Task ReadAccessTokenRequestAsync(HttpRequestBase request = null, CancellationToken cancellationToken = default(CancellationToken)) { request = request ?? this.Channel.GetRequestFromContext(); return this.Channel.TryReadFromRequestAsync(request.AsHttpRequestMessage(), cancellationToken); } /// /// Prepares and sends an access token to a Consumer, and invalidates the request token. /// /// The Consumer's message requesting an access token. /// The HTTP response to actually send to the Consumer. public AuthorizedTokenResponse PrepareAccessTokenMessage(AuthorizedTokenRequest request) { Requires.NotNull(request, "request"); ErrorUtilities.VerifyProtocol(this.TokenManager.IsRequestTokenAuthorized(request.RequestToken), OAuthStrings.AccessTokenNotAuthorized, request.RequestToken); string accessToken = this.TokenGenerator.GenerateAccessToken(request.ConsumerKey); string tokenSecret = this.TokenGenerator.GenerateSecret(); this.TokenManager.ExpireRequestTokenAndStoreNewAccessToken(request.ConsumerKey, request.RequestToken, accessToken, tokenSecret); var grantAccess = new AuthorizedTokenResponse(request) { AccessToken = accessToken, TokenSecret = tokenSecret, }; return grantAccess; } /// /// Gets the authorization (access token) for accessing some protected resource. /// /// HTTP details from an incoming WCF message. /// The URI of the WCF service endpoint. /// The cancellation token. /// The authorization message sent by the Consumer, or null if no authorization message is attached. /// /// This method verifies that the access token and token secret are valid. /// It falls on the caller to verify that the access token is actually authorized /// to access the resources being requested. /// /// Thrown if an unexpected message is attached to the request. public Task ReadProtectedResourceAuthorizationAsync(HttpRequestMessageProperty request, Uri requestUri, CancellationToken cancellationToken = default(CancellationToken)) { return this.ReadProtectedResourceAuthorizationAsync(new HttpRequestInfo(request, requestUri), cancellationToken); } /// /// Gets the authorization (access token) for accessing some protected resource. /// /// The incoming HTTP request. /// The cancellation token. /// The authorization message sent by the Consumer, or null if no authorization message is attached. /// /// This method verifies that the access token and token secret are valid. /// It falls on the caller to verify that the access token is actually authorized /// to access the resources being requested. /// /// Thrown if an unexpected message is attached to the request. public async Task ReadProtectedResourceAuthorizationAsync(HttpRequestBase request = null, CancellationToken cancellationToken = default(CancellationToken)) { request = request ?? this.Channel.GetRequestFromContext(); var accessMessage = await this.Channel.TryReadFromRequestAsync(request.AsHttpRequestMessage(), cancellationToken); if (accessMessage != null) { if (this.TokenManager.GetTokenType(accessMessage.AccessToken) != TokenType.AccessToken) { throw new ProtocolException( string.Format( CultureInfo.CurrentCulture, OAuthStrings.BadAccessTokenInProtectedResourceRequest, accessMessage.AccessToken)); } } return accessMessage; } /// /// Creates a security principal that may be used. /// /// The request. /// The instance that can be used for access control of resources. public IPrincipal CreatePrincipal(AccessProtectedResourceRequest request) { Requires.NotNull(request, "request"); IServiceProviderAccessToken accessToken = this.TokenManager.GetAccessToken(request.AccessToken); return OAuthPrincipal.CreatePrincipal(accessToken.Username, accessToken.Roles); } #region IDisposable Members /// /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. /// public void Dispose() { this.Dispose(true); GC.SuppressFinalize(this); } /// /// Releases unmanaged and - optionally - managed resources /// /// true to release both managed and unmanaged resources; false to release only unmanaged resources. protected virtual void Dispose(bool disposing) { if (disposing) { this.Channel.Dispose(); } } #endregion } }