LEFT | RIGHT |
1 /* | 1 /* |
2 Copyright 2013 Google Inc | 2 Copyright 2013 Google Inc |
3 | 3 |
4 Licensed under the Apache License, Version 2.0 (the "License"); | 4 Licensed under the Apache License, Version 2.0 (the "License"); |
5 you may not use this file except in compliance with the License. | 5 you may not use this file except in compliance with the License. |
6 You may obtain a copy of the License at | 6 You may obtain a copy of the License at |
7 | 7 |
8 http://www.apache.org/licenses/LICENSE-2.0 | 8 http://www.apache.org/licenses/LICENSE-2.0 |
9 | 9 |
10 Unless required by applicable law or agreed to in writing, software | 10 Unless required by applicable law or agreed to in writing, software |
11 distributed under the License is distributed on an "AS IS" BASIS, | 11 distributed under the License is distributed on an "AS IS" BASIS, |
12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
13 See the License for the specific language governing permissions and | 13 See the License for the specific language governing permissions and |
14 limitations under the License. | 14 limitations under the License. |
15 */ | 15 */ |
16 | 16 |
17 using System; | 17 using System; |
| 18 using System.Net; |
18 using System.Net.Http; | 19 using System.Net.Http; |
19 using System.Threading; | 20 using System.Threading; |
20 using System.Threading.Tasks; | 21 using System.Threading.Tasks; |
21 | 22 |
22 using Google.Apis.Auth.OAuth2.Responses; | 23 using Google.Apis.Auth.OAuth2.Responses; |
23 using Google.Apis.Http; | 24 using Google.Apis.Http; |
24 using Google.Apis.Logging; | 25 using Google.Apis.Logging; |
25 | 26 |
26 namespace Google.Apis.Auth.OAuth2 | 27 namespace Google.Apis.Auth.OAuth2 |
27 { | 28 { |
28 /// <summary> | 29 /// <summary> |
29 /// OAuth 2.0 credential for accessing protected resources using an access t
oken, as well as optionally refreshing· | 30 /// OAuth 2.0 credential for accessing protected resources using an access t
oken, as well as optionally refreshing· |
30 /// the access token when it expires using a refresh token. | 31 /// the access token when it expires using a refresh token. |
31 /// </summary> | 32 /// </summary> |
32 public class Credential : IHttpExecuteInterceptor, IHttpUnsuccessfulResponse
Handler, | 33 public class Credential : IHttpExecuteInterceptor, IHttpUnsuccessfulResponse
Handler, |
33 IConfigurableHttpClientInitializer | 34 IConfigurableHttpClientInitializer |
34 { | 35 { |
35 private static readonly ILogger Logger = ApplicationContext.Logger.ForTy
pe<Credential>(); | 36 private static readonly ILogger Logger = ApplicationContext.Logger.ForTy
pe<Credential>(); |
36 | 37 |
37 private readonly TokenResponse token; | 38 private TokenResponse token; |
| 39 private object lockObject = new object(); |
| 40 |
| 41 public TokenResponse Token |
| 42 { |
| 43 get |
| 44 { |
| 45 lock (lockObject) |
| 46 { |
| 47 return token; |
| 48 } |
| 49 } |
| 50 private set |
| 51 { |
| 52 lock (lockObject) |
| 53 { |
| 54 token = value; |
| 55 } |
| 56 } |
| 57 } |
| 58 |
38 private readonly IAuthorizationCodeFlow flow; | 59 private readonly IAuthorizationCodeFlow flow; |
39 private readonly string userId; | 60 private readonly string userId; |
40 | |
41 private readonly object lockObject = new object(); | |
42 | 61 |
43 /// <summary>Constructs a new credential instance.</summary> | 62 /// <summary>Constructs a new credential instance.</summary> |
44 /// <param name="flow">Authorization code flow</param> | 63 /// <param name="flow">Authorization code flow</param> |
45 /// <param name="userId">User identifier</param> | 64 /// <param name="userId">User identifier</param> |
46 /// <param name="token">An initial token for the user</param> | 65 /// <param name="token">An initial token for the user</param> |
47 public Credential(IAuthorizationCodeFlow flow, string userId, TokenRespo
nse token) | 66 public Credential(IAuthorizationCodeFlow flow, string userId, TokenRespo
nse token) |
48 { | 67 { |
49 this.flow = flow; | 68 this.flow = flow; |
| 69 this.userId = userId; |
50 this.token = token; | 70 this.token = token; |
51 this.userId = userId; | |
52 } | 71 } |
53 | 72 |
54 /// <summary> | 73 /// <summary> |
55 /// Default implementation is to try to refresh the access token if ther
e is no access token or if we are 1· | 74 /// Default implementation is to try to refresh the access token if ther
e is no access token or if we are 1· |
56 /// minute away from expiration. If token server is unavailable, it will
try to use the access token even if· | 75 /// minute away from expiration. If token server is unavailable, it will
try to use the access token even if· |
57 /// has expired. If successful, it will call <seealso cref="IAccessMetho
d.Intercept"/>. | 76 /// has expired. If successful, it will call <seealso cref="IAccessMetho
d.Intercept"/>. |
58 /// </summary> | 77 /// </summary> |
59 public async Task InterceptAsync(HttpRequestMessage request, Cancellatio
nToken taskCancellationToken) | 78 public async Task InterceptAsync(HttpRequestMessage request, Cancellatio
nToken taskCancellationToken) |
60 { | 79 { |
61 var isExpired = false; | 80 if (Token.IsExpired(flow.Clock)) |
62 lock (lockObject) | |
63 { | 81 { |
64 isExpired = token.AccessToken == null || token.IsExpired(flow.Cl
ock); | 82 if (!await RefreshTokenAsync(taskCancellationToken).ConfigureAwa
it(false)) |
65 } | 83 { |
66 if (isExpired) | 84 throw new InvalidOperationException("The access token is exp
ired but we can't refresh it"); |
67 { | 85 } |
68 await RefreshToken(taskCancellationToken).ConfigureAwait(false); | |
69 } | 86 } |
70 | 87 |
71 lock (lockObject) | 88 flow.AccessMethod.Intercept(request, Token.AccessToken); |
72 { | |
73 flow.AccessMethod.Intercept(request, token.AccessToken); | |
74 } | |
75 } | 89 } |
76 | 90 |
77 /// <summary> | 91 /// <summary> |
78 /// Refreshes the token by calling to <seealso cref="IAuthorizationCodeF
low.RefreshToken"/>. Then it updates· | 92 /// Refreshes the token by calling to <seealso cref="IAuthorizationCodeF
low.RefreshTokenAsync"/>. Then it· |
79 /// the <see cref="TokenResponse"/> with the new token instance. | 93 /// updates the <see cref="TokenResponse"/> with the new token instance. |
80 /// </summary> | 94 /// </summary> |
81 /// <param name="taskCancellationToken">Cancellation token to cancel an
operation</param> | 95 /// <param name="taskCancellationToken">Cancellation token to cancel an
operation</param> |
82 /// <returns><c>true</c> if the token was refreshed</returns> | 96 /// <returns><c>true</c> if the token was refreshed</returns> |
83 private async Task<bool> RefreshToken(CancellationToken taskCancellation
Token) | 97 private async Task<bool> RefreshTokenAsync(CancellationToken taskCancell
ationToken) |
84 { | 98 { |
85 // We don't need to lock - refresh token isn't going to be changed. | 99 if (Token.RefreshToken == null) |
86 if (token.RefreshToken == null) | |
87 { | 100 { |
88 Logger.Warning("Refresh token is null, can't refresh the token!"
); | 101 Logger.Warning("Refresh token is null, can't refresh the token!"
); |
89 return false; | 102 return false; |
90 } | 103 } |
91 | 104 |
92 // Race condition is possible. we can't lock the following method be
cause there is a use in async-await.· | 105 // It's possible that two concurrent calls will be made to refresh t
he token, in that case the last one· |
93 // It's bad practice to use lockers with async-await calls. As a res
ult this class isn't thread safe and we | 106 // will win. |
94 // may refreshing the same token more than once in a short periods (
apparently number of seconds) | 107 var newToken = await flow.RefreshTokenAsync(userId, Token.RefreshTok
en, taskCancellationToken) |
95 var newToken = await flow.RefreshToken(userId, token.RefreshToken, t
askCancellationToken) | |
96 .ConfigureAwait(false); | 108 .ConfigureAwait(false); |
97 | 109 |
98 Logger.Info("Access token was refreshed"); | 110 Logger.Info("Access token was refreshed"); |
99 | 111 |
100 lock (lockObject) | 112 if (newToken.RefreshToken == null) |
101 { | 113 { |
102 token.CopyFrom(newToken); | 114 newToken.RefreshToken = Token.RefreshToken; |
103 } | 115 } |
104 | 116 |
| 117 Token = newToken; |
105 return true; | 118 return true; |
106 } | 119 } |
107 | 120 |
108 public async Task<bool> HandleResponseAsync(HandleUnsuccessfulResponseAr
gs args) | 121 public async Task<bool> HandleResponseAsync(HandleUnsuccessfulResponseAr
gs args) |
109 { | 122 { |
110 // TODO(peleyal): check WWW-Authenticate header | 123 // TODO(peleyal): check WWW-Authenticate header |
111 if (args.Response.StatusCode == System.Net.HttpStatusCode.Unauthoriz
ed) | 124 if (args.Response.StatusCode == HttpStatusCode.Unauthorized) |
112 { | 125 { |
113 var sameAccessToken = false; | 126 return !Object.Equals(Token.AccessToken, flow.AccessMethod.GetAc
cessToken(args.Request)) |
114 lock (lockObject) | 127 || await RefreshTokenAsync(args.CancellationToken).Configure
Await(false); |
115 { | |
116 sameAccessToken = Object.Equals(token.AccessToken, flow.Acce
ssMethod.GetAccessToken(args.Request)); | |
117 } | |
118 return !sameAccessToken || await RefreshToken(args.CancellationT
oken).ConfigureAwait(false); | |
119 } | 128 } |
120 | 129 |
121 return false; | 130 return false; |
122 } | 131 } |
123 | 132 |
124 public void Initialize(ConfigurableHttpClient httpClient) | 133 public void Initialize(ConfigurableHttpClient httpClient) |
125 { | 134 { |
126 httpClient.MessageHandler.ExecuteInterceptors.Add(this); | 135 httpClient.MessageHandler.ExecuteInterceptors.Add(this); |
127 httpClient.MessageHandler.UnsuccessfulResponseHandlers.Add(this); | 136 httpClient.MessageHandler.UnsuccessfulResponseHandlers.Add(this); |
128 } | 137 } |
129 } | 138 } |
130 } | 139 } |
LEFT | RIGHT |