//----------------------------------------------------------------------- // // Copyright (c) Outercurve Foundation. All rights reserved. // //----------------------------------------------------------------------- namespace DotNetOpenAuth.OAuth2 { using System; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Net; using System.Net.Http; using System.Security; using System.Text; using System.Threading; using System.Threading.Tasks; using System.Xml; using DotNetOpenAuth.Logging; using DotNetOpenAuth.Messaging; using DotNetOpenAuth.OAuth2.ChannelElements; using DotNetOpenAuth.OAuth2.Messages; using Validation; /// /// A base class for common OAuth Client behaviors. /// public class ClientBase { /// /// Initializes a new instance of the class. /// /// The token issuer. /// The client identifier. /// The tool to use to apply client credentials to authenticated requests to the Authorization Server. /// May be null for clients with no secret or other means of authentication. /// The host factories. protected ClientBase(AuthorizationServerDescription authorizationServer, string clientIdentifier = null, ClientCredentialApplicator clientCredentialApplicator = null, IHostFactories hostFactories = null) { Requires.NotNull(authorizationServer, "authorizationServer"); this.AuthorizationServer = authorizationServer; this.Channel = new OAuth2ClientChannel(hostFactories); this.ClientIdentifier = clientIdentifier; this.ClientCredentialApplicator = clientCredentialApplicator; } /// /// Gets the token issuer. /// /// The token issuer. public AuthorizationServerDescription AuthorizationServer { get; private set; } /// /// Gets the OAuth channel. /// /// The channel. public Channel Channel { get; internal set; } /// /// Gets or sets the identifier by which this client is known to the Authorization Server. /// public string ClientIdentifier { get { return this.OAuthChannel.ClientIdentifier; } set { this.OAuthChannel.ClientIdentifier = value; } } /// /// Gets or sets the tool to use to apply client credentials to authenticated requests to the Authorization Server. /// /// May be null if this client has no client secret. public ClientCredentialApplicator ClientCredentialApplicator { get { return this.OAuthChannel.ClientCredentialApplicator; } set { this.OAuthChannel.ClientCredentialApplicator = value; } } /// /// Gets quotas used when deserializing JSON. /// public XmlDictionaryReaderQuotas JsonReaderQuotas { get { return this.OAuthChannel.JsonReaderQuotas; } } /// /// Gets the OAuth client channel. /// internal IOAuth2ChannelWithClient OAuthChannel { get { return (IOAuth2ChannelWithClient)this.Channel; } } /// /// Adds the necessary HTTP Authorization header to an HTTP request for protected resources /// so that the Service Provider will allow the request through. /// /// The request for protected resources from the service provider. /// The access token previously obtained from the Authorization Server. public static void AuthorizeRequest(HttpWebRequest request, string accessToken) { Requires.NotNull(request, "request"); Requires.NotNullOrEmpty(accessToken, "accessToken"); AuthorizeRequest(request.Headers, accessToken); } /// /// Adds the necessary HTTP Authorization header to an HTTP request for protected resources /// so that the Service Provider will allow the request through. /// /// The headers on the request for protected resources from the service provider. /// The access token previously obtained from the Authorization Server. public static void AuthorizeRequest(WebHeaderCollection requestHeaders, string accessToken) { Requires.NotNull(requestHeaders, "requestHeaders"); Requires.NotNullOrEmpty(accessToken, "accessToken"); OAuthUtilities.AuthorizeWithBearerToken(requestHeaders, accessToken); } /// /// Adds the OAuth authorization token to an outgoing HTTP request, renewing a /// (nearly) expired access token if necessary. /// /// The request for protected resources from the service provider. /// The authorization for this request previously obtained via OAuth. /// The cancellation token. /// /// A task that completes with the asynchronous operation. /// public Task AuthorizeRequestAsync(HttpWebRequest request, IAuthorizationState authorization, CancellationToken cancellationToken) { Requires.NotNull(request, "request"); Requires.NotNull(authorization, "authorization"); return this.AuthorizeRequestAsync(request.Headers, authorization, cancellationToken); } /// /// Adds the OAuth authorization token to an outgoing HTTP request, renewing a /// (nearly) expired access token if necessary. /// /// The headers on the request for protected resources from the service provider. /// The authorization for this request previously obtained via OAuth. /// The cancellation token. /// /// A task that completes with the asynchronous operation. /// public async Task AuthorizeRequestAsync(WebHeaderCollection requestHeaders, IAuthorizationState authorization, CancellationToken cancellationToken) { Requires.NotNull(requestHeaders, "requestHeaders"); Requires.NotNull(authorization, "authorization"); Requires.That(!string.IsNullOrEmpty(authorization.AccessToken), "authorization", "AccessToken required."); ErrorUtilities.VerifyProtocol(!authorization.AccessTokenExpirationUtc.HasValue || authorization.AccessTokenExpirationUtc >= DateTime.UtcNow || authorization.RefreshToken != null, ClientStrings.AuthorizationExpired); if (authorization.AccessTokenExpirationUtc.HasValue && authorization.AccessTokenExpirationUtc.Value < DateTime.UtcNow) { ErrorUtilities.VerifyProtocol(authorization.RefreshToken != null, ClientStrings.AccessTokenRefreshFailed); await this.RefreshAuthorizationAsync(authorization, cancellationToken: cancellationToken); } AuthorizeRequest(requestHeaders, authorization.AccessToken); } /// /// Creates an HTTP handler that automatically applies an OAuth 2 (bearer) access token to outbound HTTP requests. /// The result of this method can be supplied to the constructor. /// /// The bearer token to apply to each outbound HTTP message. /// The inner HTTP handler to use. The default uses as the inner handler. /// An instance. public DelegatingHandler CreateAuthorizingHandler(string bearerAccessToken, HttpMessageHandler innerHandler = null) { Requires.NotNullOrEmpty(bearerAccessToken, "bearerAccessToken"); return new BearerTokenHttpMessageHandler(bearerAccessToken, innerHandler ?? new HttpClientHandler()); } /// /// Creates an HTTP handler that automatically applies the OAuth 2 access token to outbound HTTP requests. /// The result of this method can be supplied to the constructor. /// /// The authorization to apply to the message. /// The inner HTTP handler to use. The default uses as the inner handler. /// An instance. public DelegatingHandler CreateAuthorizingHandler(IAuthorizationState authorization, HttpMessageHandler innerHandler = null) { Requires.NotNull(authorization, "authorization"); return new BearerTokenHttpMessageHandler(this, authorization, innerHandler ?? new HttpClientHandler()); } /// /// Refreshes a short-lived access token using a longer-lived refresh token /// with a new access token that has the same scope as the refresh token. /// The refresh token itself may also be refreshed. /// /// The authorization to update. /// If given, the access token will not be refreshed if its remaining lifetime exceeds this value. /// The cancellation token. /// A value indicating whether the access token was actually renewed; true if it was renewed, or false if it still had useful life remaining. /// /// This method may modify the value of the property on /// the parameter if the authorization server has cycled out your refresh token. /// If the parameter value was updated, this method calls on that instance. /// public async Task RefreshAuthorizationAsync(IAuthorizationState authorization, TimeSpan? skipIfUsefulLifeExceeds = null, CancellationToken cancellationToken = default(CancellationToken)) { Requires.NotNull(authorization, "authorization"); Requires.That(!string.IsNullOrEmpty(authorization.RefreshToken), "authorization", "RefreshToken required."); if (skipIfUsefulLifeExceeds.HasValue && authorization.AccessTokenExpirationUtc.HasValue) { TimeSpan usefulLifeRemaining = authorization.AccessTokenExpirationUtc.Value - DateTime.UtcNow; if (usefulLifeRemaining > skipIfUsefulLifeExceeds.Value) { // There is useful life remaining in the access token. Don't refresh. Logger.OAuth.DebugFormat("Skipping token refresh step because access token's remaining life is {0}, which exceeds {1}.", usefulLifeRemaining, skipIfUsefulLifeExceeds.Value); return false; } } var request = new AccessTokenRefreshRequestC(this.AuthorizationServer) { ClientIdentifier = this.ClientIdentifier, RefreshToken = authorization.RefreshToken, }; this.ApplyClientCredential(request); var response = await this.Channel.RequestAsync(request, cancellationToken); UpdateAuthorizationWithResponse(authorization, response); return true; } /// /// Gets an access token that may be used for only a subset of the scope for which a given /// refresh token is authorized. /// /// The refresh token. /// The scope subset desired in the access token. /// The cancellation token. /// A description of the obtained access token, and possibly a new refresh token. /// /// If the return value includes a new refresh token, the old refresh token should be discarded and /// replaced with the new one. /// public async Task GetScopedAccessTokenAsync(string refreshToken, HashSet scope, CancellationToken cancellationToken) { Requires.NotNullOrEmpty(refreshToken, "refreshToken"); Requires.NotNull(scope, "scope"); var request = new AccessTokenRefreshRequestC(this.AuthorizationServer) { ClientIdentifier = this.ClientIdentifier, RefreshToken = refreshToken, }; this.ApplyClientCredential(request); var response = await this.Channel.RequestAsync(request, cancellationToken); var authorization = new AuthorizationState(); UpdateAuthorizationWithResponse(authorization, response); return authorization; } /// /// Exchanges a resource owner's password credential for OAuth 2.0 refresh and access tokens. /// /// The resource owner's username, as it is known by the authorization server. /// The resource owner's account password. /// The desired scope of access. /// The cancellation token. /// /// The result, containing the tokens if successful. /// public Task ExchangeUserCredentialForTokenAsync(string userName, string password, IEnumerable scopes = null, CancellationToken cancellationToken = default(CancellationToken)) { Requires.NotNullOrEmpty(userName, "userName"); Requires.NotNull(password, "password"); var request = new AccessTokenResourceOwnerPasswordCredentialsRequest(this.AuthorizationServer.TokenEndpoint, this.AuthorizationServer.Version) { RequestingUserName = userName, Password = password, }; return this.RequestAccessTokenAsync(request, scopes, cancellationToken); } /// /// Obtains an access token for accessing client-controlled resources on the resource server. /// /// The desired scopes. /// The cancellation token. /// /// The result of the authorization request. /// public Task GetClientAccessTokenAsync(IEnumerable scopes = null, CancellationToken cancellationToken = default(CancellationToken)) { var request = new AccessTokenClientCredentialsRequest(this.AuthorizationServer.TokenEndpoint, this.AuthorizationServer.Version); return this.RequestAccessTokenAsync(request, scopes, cancellationToken); } /// /// Updates the authorization state maintained by the client with the content of an outgoing response. /// /// The authorization state maintained by the client. /// The access token containing response message. internal static void UpdateAuthorizationWithResponse(IAuthorizationState authorizationState, AccessTokenSuccessResponse accessTokenSuccess) { Requires.NotNull(authorizationState, "authorizationState"); Requires.NotNull(accessTokenSuccess, "accessTokenSuccess"); authorizationState.AccessToken = accessTokenSuccess.AccessToken; authorizationState.AccessTokenExpirationUtc = DateTime.UtcNow + accessTokenSuccess.Lifetime; authorizationState.AccessTokenIssueDateUtc = DateTime.UtcNow; // The authorization server MAY choose to renew the refresh token itself. if (accessTokenSuccess.RefreshToken != null) { authorizationState.RefreshToken = accessTokenSuccess.RefreshToken; } // An included scope parameter in the response only describes the access token's scope. // Don't update the whole authorization state object with that scope because that represents // the refresh token's original scope. if ((authorizationState.Scope == null || authorizationState.Scope.Count == 0) && accessTokenSuccess.Scope != null) { authorizationState.Scope.ResetContents(accessTokenSuccess.Scope); } authorizationState.SaveChanges(); } /// /// Updates the authorization state maintained by the client with the content of an outgoing response. /// /// The authorization state maintained by the client. /// The access token containing response message. internal static void UpdateAuthorizationWithResponse(IAuthorizationState authorizationState, EndUserAuthorizationSuccessAccessTokenResponse accessTokenSuccess) { Requires.NotNull(authorizationState, "authorizationState"); Requires.NotNull(accessTokenSuccess, "accessTokenSuccess"); authorizationState.AccessToken = accessTokenSuccess.AccessToken; authorizationState.AccessTokenExpirationUtc = DateTime.UtcNow + accessTokenSuccess.Lifetime; authorizationState.AccessTokenIssueDateUtc = DateTime.UtcNow; 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); } authorizationState.Scope.ResetContents(accessTokenSuccess.Scope); } authorizationState.SaveChanges(); } /// /// Updates authorization state with a success response from the Authorization Server. /// /// The authorization state to update. /// The authorization success message obtained from the authorization server. /// The cancellation token. /// /// A task that completes with the asynchronous operation. /// internal async Task UpdateAuthorizationWithResponseAsync(IAuthorizationState authorizationState, EndUserAuthorizationSuccessAuthCodeResponse authorizationSuccess, CancellationToken cancellationToken) { Requires.NotNull(authorizationState, "authorizationState"); Requires.NotNull(authorizationSuccess, "authorizationSuccess"); var accessTokenRequest = new AccessTokenAuthorizationCodeRequestC(this.AuthorizationServer) { ClientIdentifier = this.ClientIdentifier, Callback = authorizationState.Callback, AuthorizationCode = authorizationSuccess.AuthorizationCode, }; this.ApplyClientCredential(accessTokenRequest); IProtocolMessage accessTokenResponse = await this.Channel.RequestAsync(accessTokenRequest, cancellationToken); var accessTokenSuccess = accessTokenResponse as AccessTokenSuccessResponse; var failedAccessTokenResponse = accessTokenResponse as AccessTokenFailedResponse; if (accessTokenSuccess != null) { UpdateAuthorizationWithResponse(authorizationState, accessTokenSuccess); } else { authorizationState.Delete(); string error = failedAccessTokenResponse != null ? failedAccessTokenResponse.Error : "(unknown)"; ErrorUtilities.ThrowProtocol(ClientStrings.CannotObtainAccessTokenWithReason, error); } } /// /// Applies the default client authentication mechanism given a client secret. /// /// The client secret. May be null /// The client credential applicator. protected static ClientCredentialApplicator DefaultSecretApplicator(string secret) { return secret == null ? ClientCredentialApplicator.NoSecret() : ClientCredentialApplicator.NetworkCredential(secret); } /// /// Applies any applicable client credential to an authenticated outbound request to the authorization server. /// /// The request to apply authentication information to. protected void ApplyClientCredential(AuthenticatedClientRequestBase request) { Requires.NotNull(request, "request"); if (this.ClientCredentialApplicator != null) { this.ClientCredentialApplicator.ApplyClientCredential(this.ClientIdentifier, request); } } /// /// Calculates the fraction of life remaining in an access token. /// /// The authorization to measure. /// A fractional number no greater than 1. Could be negative if the access token has already expired. private static double ProportionalLifeRemaining(IAuthorizationState authorization) { Requires.NotNull(authorization, "authorization"); Requires.That(authorization.AccessTokenIssueDateUtc.HasValue, "authorization", "AccessTokenIssueDateUtc required"); Requires.That(authorization.AccessTokenExpirationUtc.HasValue, "authorization", "AccessTokenExpirationUtc required"); // Calculate what % of the total life this access token has left. TimeSpan totalLifetime = authorization.AccessTokenExpirationUtc.Value - authorization.AccessTokenIssueDateUtc.Value; TimeSpan elapsedLifetime = DateTime.UtcNow - authorization.AccessTokenIssueDateUtc.Value; double proportionLifetimeRemaining = 1 - (elapsedLifetime.TotalSeconds / totalLifetime.TotalSeconds); return proportionLifetimeRemaining; } /// /// Requests an access token using a partially .initialized request message. /// /// The request message. /// The scopes requested by the client. /// The cancellation token. /// /// The result of the request. /// private async Task RequestAccessTokenAsync(ScopedAccessTokenRequest request, IEnumerable scopes, CancellationToken cancellationToken) { Requires.NotNull(request, "request"); var authorizationState = new AuthorizationState(scopes); request.ClientIdentifier = this.ClientIdentifier; this.ApplyClientCredential(request); request.Scope.UnionWith(authorizationState.Scope); var response = await this.Channel.RequestAsync(request, cancellationToken); var success = response as AccessTokenSuccessResponse; var failure = response as AccessTokenFailedResponse; ErrorUtilities.VerifyProtocol(success != null || failure != null, MessagingStrings.UnexpectedMessageReceivedOfMany); if (success != null) { authorizationState.Scope.Clear(); // clear the scope we requested so that the response will repopulate it. UpdateAuthorizationWithResponse(authorizationState, success); } else { // failure Logger.OAuth.Info("Credentials rejected by the Authorization Server."); authorizationState.Delete(); } return authorizationState; } } }