//----------------------------------------------------------------------- // // Copyright (c) Outercurve Foundation. All rights reserved. // //----------------------------------------------------------------------- namespace DotNetOpenAuth.OAuth2 { using System; using System.Collections.Generic; using System.Diagnostics.Contracts; using System.Globalization; using System.Linq; using System.Net; using System.Text; using DotNetOpenAuth.Messaging; using DotNetOpenAuth.OAuth2.ChannelElements; using DotNetOpenAuth.OAuth2.Messages; /// /// 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 client secret. protected ClientBase(AuthorizationServerDescription authorizationServer, string clientIdentifier = null, string clientSecret = null) { Requires.NotNull(authorizationServer, "authorizationServer"); this.AuthorizationServer = authorizationServer; this.Channel = new OAuth2ClientChannel(); this.ClientIdentifier = clientIdentifier; this.ClientSecret = clientSecret; } /// /// Gets the token issuer. /// /// The token issuer. public AuthorizationServerDescription AuthorizationServer { get; private set; } /// /// Gets the OAuth channel. /// /// The channel. public Channel Channel { get; private set; } /// /// Gets or sets the identifier by which this client is known to the Authorization Server. /// public string ClientIdentifier { get; set; } /// /// Gets or sets the client secret shared with the Authorization Server. /// public string ClientSecret { get; set; } /// /// 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"); OAuthUtilities.AuthorizeWithBearerToken(request, 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. public void AuthorizeRequest(HttpWebRequest request, IAuthorizationState authorization) { Requires.NotNull(request, "request"); Requires.NotNull(authorization, "authorization"); Requires.True(!string.IsNullOrEmpty(authorization.AccessToken), "authorization"); ErrorUtilities.VerifyProtocol(!authorization.AccessTokenExpirationUtc.HasValue || authorization.AccessTokenExpirationUtc < DateTime.UtcNow || authorization.RefreshToken != null, "authorization has expired"); if (authorization.AccessTokenExpirationUtc.HasValue && authorization.AccessTokenExpirationUtc.Value < DateTime.UtcNow) { ErrorUtilities.VerifyProtocol(authorization.RefreshToken != null, "Access token has expired and cannot be automatically refreshed."); this.RefreshAuthorization(authorization); } AuthorizeRequest(request, authorization.AccessToken); } /// /// 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. /// 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 bool RefreshAuthorization(IAuthorizationState authorization, TimeSpan? skipIfUsefulLifeExceeds = null) { Requires.NotNull(authorization, "authorization"); Requires.True(!string.IsNullOrEmpty(authorization.RefreshToken), "authorization"); 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 AccessTokenRefreshRequest(this.AuthorizationServer) { ClientIdentifier = this.ClientIdentifier, ClientSecret = this.ClientSecret, RefreshToken = authorization.RefreshToken, }; var response = this.Channel.Request(request); 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. /// 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 IAuthorizationState GetScopedAccessToken(string refreshToken, HashSet scope) { Requires.NotNullOrEmpty(refreshToken, "refreshToken"); Requires.NotNull(scope, "scope"); Contract.Ensures(Contract.Result() != null); var request = new AccessTokenRefreshRequest(this.AuthorizationServer) { ClientIdentifier = this.ClientIdentifier, ClientSecret = this.ClientSecret, RefreshToken = refreshToken, }; var response = this.Channel.Request(request); var authorization = new AuthorizationState(); UpdateAuthorizationWithResponse(authorization, response); return authorization; } /// /// 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. internal void UpdateAuthorizationWithResponse(IAuthorizationState authorizationState, EndUserAuthorizationSuccessAuthCodeResponse authorizationSuccess) { Requires.NotNull(authorizationState, "authorizationState"); Requires.NotNull(authorizationSuccess, "authorizationSuccess"); var accessTokenRequest = new AccessTokenAuthorizationCodeRequest(this.AuthorizationServer) { ClientIdentifier = this.ClientIdentifier, ClientSecret = this.ClientSecret, Callback = authorizationState.Callback, AuthorizationCode = authorizationSuccess.AuthorizationCode, }; IProtocolMessage accessTokenResponse = this.Channel.Request(accessTokenRequest); 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(OAuthStrings.CannotObtainAccessTokenWithReason, error); } } /// /// 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.True(authorization.AccessTokenIssueDateUtc.HasValue, "authorization"); Requires.True(authorization.AccessTokenExpirationUtc.HasValue, "authorization"); // 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; } } }