summaryrefslogtreecommitdiffstats
path: root/src/DotNetOpenAuth.OAuth2.Client/OAuth2/ClientBase.cs
blob: bb555f5f130338c24ae832307fe62d489cf4f596 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
//-----------------------------------------------------------------------
// <copyright file="ClientBase.cs" company="Outercurve Foundation">
//     Copyright (c) Outercurve Foundation. All rights reserved.
// </copyright>
//-----------------------------------------------------------------------

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;

	/// <summary>
	/// A base class for common OAuth Client behaviors.
	/// </summary>
	public class ClientBase {
		/// <summary>
		/// Initializes a new instance of the <see cref="ClientBase"/> class.
		/// </summary>
		/// <param name="authorizationServer">The token issuer.</param>
		/// <param name="clientIdentifier">The client identifier.</param>
		/// <param name="clientSecret">The client secret.</param>
		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;
		}

		/// <summary>
		/// Gets the token issuer.
		/// </summary>
		/// <value>The token issuer.</value>
		public AuthorizationServerDescription AuthorizationServer { get; private set; }

		/// <summary>
		/// Gets the OAuth channel.
		/// </summary>
		/// <value>The channel.</value>
		public Channel Channel { get; private set; }

		/// <summary>
		/// Gets or sets the identifier by which this client is known to the Authorization Server.
		/// </summary>
		public string ClientIdentifier { get; set; }

		/// <summary>
		/// Gets or sets the client secret shared with the Authorization Server.
		/// </summary>
		public string ClientSecret { get; set; }

		/// <summary>
		/// Adds the necessary HTTP Authorization header to an HTTP request for protected resources
		/// so that the Service Provider will allow the request through.
		/// </summary>
		/// <param name="request">The request for protected resources from the service provider.</param>
		/// <param name="accessToken">The access token previously obtained from the Authorization Server.</param>
		public static void AuthorizeRequest(HttpWebRequest request, string accessToken) {
			Requires.NotNull(request, "request");
			Requires.NotNullOrEmpty(accessToken, "accessToken");

			OAuthUtilities.AuthorizeWithBearerToken(request, accessToken);
		}

		/// <summary>
		/// Adds the OAuth authorization token to an outgoing HTTP request, renewing a
		/// (nearly) expired access token if necessary.
		/// </summary>
		/// <param name="request">The request for protected resources from the service provider.</param>
		/// <param name="authorization">The authorization for this request previously obtained via OAuth.</param>
		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);
		}

		/// <summary>
		/// 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.
		/// </summary>
		/// <param name="authorization">The authorization to update.</param>
		/// <param name="skipIfUsefulLifeExceeds">If given, the access token will <em>not</em> be refreshed if its remaining lifetime exceeds this value.</param>
		/// <returns>A value indicating whether the access token was actually renewed; <c>true</c> if it was renewed, or <c>false</c> if it still had useful life remaining.</returns>
		/// <remarks>
		/// This method may modify the value of the <see cref="IAuthorizationState.RefreshToken"/> property on
		/// the <paramref name="authorization"/> parameter if the authorization server has cycled out your refresh token.
		/// If the parameter value was updated, this method calls <see cref="IAuthorizationState.SaveChanges"/> on that instance.
		/// </remarks>
		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<AccessTokenSuccessResponse>(request);
			UpdateAuthorizationWithResponse(authorization, response);
			return true;
		}

		/// <summary>
		/// Gets an access token that may be used for only a subset of the scope for which a given
		/// refresh token is authorized.
		/// </summary>
		/// <param name="refreshToken">The refresh token.</param>
		/// <param name="scope">The scope subset desired in the access token.</param>
		/// <returns>A description of the obtained access token, and possibly a new refresh token.</returns>
		/// <remarks>
		/// If the return value includes a new refresh token, the old refresh token should be discarded and
		/// replaced with the new one.
		/// </remarks>
		public IAuthorizationState GetScopedAccessToken(string refreshToken, HashSet<string> scope) {
			Requires.NotNullOrEmpty(refreshToken, "refreshToken");
			Requires.NotNull(scope, "scope");
			Contract.Ensures(Contract.Result<IAuthorizationState>() != null);

			var request = new AccessTokenRefreshRequest(this.AuthorizationServer) {
				ClientIdentifier = this.ClientIdentifier,
				ClientSecret = this.ClientSecret,
				RefreshToken = refreshToken,
			};

			var response = this.Channel.Request<AccessTokenSuccessResponse>(request);
			var authorization = new AuthorizationState();
			UpdateAuthorizationWithResponse(authorization, response);

			return authorization;
		}

		/// <summary>
		/// Updates the authorization state maintained by the client with the content of an outgoing response.
		/// </summary>
		/// <param name="authorizationState">The authorization state maintained by the client.</param>
		/// <param name="accessTokenSuccess">The access token containing response message.</param>
		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();
		}

		/// <summary>
		/// Updates the authorization state maintained by the client with the content of an outgoing response.
		/// </summary>
		/// <param name="authorizationState">The authorization state maintained by the client.</param>
		/// <param name="accessTokenSuccess">The access token containing response message.</param>
		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();
		}

		/// <summary>
		/// Updates authorization state with a success response from the Authorization Server.
		/// </summary>
		/// <param name="authorizationState">The authorization state to update.</param>
		/// <param name="authorizationSuccess">The authorization success message obtained from the authorization server.</param>
		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);
			}
		}

		/// <summary>
		/// Calculates the fraction of life remaining in an access token.
		/// </summary>
		/// <param name="authorization">The authorization to measure.</param>
		/// <returns>A fractional number no greater than 1.  Could be negative if the access token has already expired.</returns>
		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;
		}
	}
}