Index: Src/GoogleApis.Auth.DotNet4/GoogleApis.Auth.DotNet4.csproj =================================================================== --- a/Src/GoogleApis.Auth.DotNet4/GoogleApis.Auth.DotNet4.csproj +++ b/Src/GoogleApis.Auth.DotNet4/GoogleApis.Auth.DotNet4.csproj @@ -75,6 +75,7 @@ + Index: Src/GoogleApis.Auth.DotNet4/OAuth2/GoogleWebAuthenticationBroker.cs =================================================================== new file mode 100644 --- /dev/null +++ b/Src/GoogleApis.Auth.DotNet4/OAuth2/GoogleWebAuthenticationBroker.cs @@ -0,0 +1,104 @@ +/* +Copyright 2013 Google Inc + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +using System.Collections.Generic; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +using Google.Apis.Util.Store; + +namespace Google.Apis.Auth.OAuth2 +{ + /// A helper utility to manage the authorization code flow. + public class GoogleWebAuthenticationBroker + { + /// The folder which is used by the . + public static string Folder = "Google.Apis.Auth"; + + /// Asynchronously authenticates the specified user. + /// + /// In case no data store is specified, will be used by + /// default. + /// + /// The client secrets. + /// + /// The scopes which indicate the Google API access your application is requesting. + /// + /// The user to authenticate. + /// Cancellation token to cancel an operation. + /// The data store, if not specified a file data store will be used. + /// User credential. + public static async Task AuthenticateAsync(ClientSecrets clientSecrets, + IEnumerable scopes, string user, CancellationToken taskCancellationToken, + IDataStore dataStore = null) + { + var initializer = new GoogleAuthorizationCodeFlow.Initializer + { + ClientSecrets = clientSecrets, + }; + return await AuthenticateAsyncCore(initializer, scopes, user, taskCancellationToken, dataStore); + } + + /// Asynchronously authenticates the specified user. + /// + /// In case no data store is specified, will be used by + /// default. + /// + /// + /// The client secrets stream. The authorization code flow constructor is responsible for disposing the stream. + /// + /// + /// The scopes which indicate the Google API access your application is requesting. + /// + /// The user to authenticate. + /// Cancellation token to cancel an operation. + /// The data store, if not specified a file data store will be used. + /// User credential. + public static async Task AuthenticateAsync(Stream clientSecretsStream, + IEnumerable scopes, string user, CancellationToken taskCancellationToken, + IDataStore dataStore = null) + { + var initializer = new GoogleAuthorizationCodeFlow.Initializer + { + ClientSecretsStream = clientSecretsStream, + }; + return await AuthenticateAsyncCore(initializer, scopes, user, taskCancellationToken, dataStore); + } + + /// The core logic for asynchronously authenticating the specified user. + /// The authorization code initializer. + /// + /// The scopes which indicate the Google API access your application is requesting. + /// + /// The user to authenticate. + /// Cancellation token to cancel an operation. + /// The data store, if not specified a file data store will be used. + /// User credential. + private static async Task AuthenticateAsyncCore(AuthorizationCodeFlow.Initializer initializer, + IEnumerable scopes, string user, CancellationToken taskCancellationToken, + IDataStore dataStore = null) + { + initializer.Scopes = scopes; + initializer.DataStore = dataStore ?? new FileDataStore(Folder); + var flow = new GoogleAuthorizationCodeFlow(initializer); + + // Create authorization code installed app instance and authorize the user. + return await new AuthorizationCodeInstalledApp(flow, new LocalServerCodeReceiver()).Authorize + (user, taskCancellationToken); + } + } +} \ No newline at end of file Index: Src/GoogleApis.Auth.Tests/GoogleApis.Auth.Tests.csproj =================================================================== new file mode 100644 --- /dev/null +++ b/Src/GoogleApis.Auth.Tests/GoogleApis.Auth.Tests.csproj @@ -0,0 +1,124 @@ + + + + + Debug + AnyCPU + {548D6C9B-A97B-4316-91AC-9AAD35202884} + Library + Properties + Google.Apis.Auth + Google.Apis.Auth.Tests + v4.0 + 512 + ..\..\ + true + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + ..\..\packages\Microsoft.Bcl.Async.1.0.16\lib\net40\Microsoft.Threading.Tasks.dll + + + ..\..\packages\Microsoft.Bcl.Async.1.0.16\lib\net40\Microsoft.Threading.Tasks.Extensions.dll + + + ..\..\packages\Microsoft.Bcl.Async.1.0.16\lib\net40\Microsoft.Threading.Tasks.Extensions.Desktop.dll + + + ..\..\packages\Moq.4.1.1309.1617\lib\net40\Moq.dll + + + ..\..\packages\Newtonsoft.Json.5.0.6\lib\net40\Newtonsoft.Json.dll + + + ..\..\packages\NUnit.2.6.2\lib\nunit.framework.dll + + + + + + False + ..\..\packages\Microsoft.Net.Http.2.1.10\lib\net40\System.Net.Http.dll + + + ..\..\packages\Microsoft.Net.Http.2.1.10\lib\net40\System.Net.Http.Extensions.dll + + + ..\..\packages\Microsoft.Net.Http.2.1.10\lib\net40\System.Net.Http.Primitives.dll + + + False + ..\..\packages\Microsoft.Net.Http.2.1.10\lib\net40\System.Net.Http.WebRequest.dll + + + ..\..\packages\Microsoft.Bcl.1.0.19\lib\net40\System.Runtime.dll + + + ..\..\packages\Microsoft.Bcl.1.0.19\lib\net40\System.Threading.Tasks.dll + + + + + + + + + + + + + + + + + + + + + {0aaaf32e-2bf0-49c5-bc2d-90874cfb5510} + GoogleApis.Auth + + + {9a8aa9ef-6904-43d8-8a26-0ab62069c2dc} + GoogleApis.Tests + + + {826cf988-eee8-4b75-8f53-b7e851a17baa} + GoogleApis + + + + + + + + + + + + + + \ No newline at end of file Index: Src/GoogleApis.Auth.Tests/OAuth2/AuthorizationCodeFlowTests.cs =================================================================== new file mode 100644 --- /dev/null +++ b/Src/GoogleApis.Auth.Tests/OAuth2/AuthorizationCodeFlowTests.cs @@ -0,0 +1,425 @@ +/* +Copyright 2013 Google Inc + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +using Moq; +using NUnit.Framework; + +using Google.Apis.Auth.OAuth2.Requests; +using Google.Apis.Auth.OAuth2.Responses; +using Google.Apis.Http; +using Google.Apis.Json; +using Google.Apis.Testing; +using Google.Apis.Tests; +using Google.Apis.Util; +using Google.Apis.Util.Store; + +namespace Google.Apis.Auth.OAuth2 +{ + /// Tests for . + [TestFixture] + public class AuthorizationCodeFlowTests + { + private const string TokenUrl = "https://token.com"; + private const string AuthorizationCodeUrl = "https://authorization.com"; + + #region Constructor + + [Test] + public void TestConstructor_ArgumentException() + { + // ClientSecrets are missing. + try + { + new AuthorizationCodeFlow(new AuthorizationCodeFlow.Initializer( + "https://authorization_code.com", "https://token.com")); + Assert.Fail(); + } + catch (ArgumentException ex) + { + Assert.True(ex.Message.Contains("You MUST set ClientSecret or ClientSecretStream")); + } + } + + [Test] + public void TestConstructor_DefaultValues() + { + var flow = CreateFlow(); + Assert.NotNull(flow.AccessMethod); + Assert.That(flow.AccessMethod, Is.InstanceOf()); + Assert.That(flow.AuthorizationServerUrl, Is.EqualTo("https://authorization.com")); + Assert.NotNull(flow.ClientSecrets); + Assert.That(flow.ClientSecrets.ClientId, Is.EqualTo("id")); + Assert.That(flow.ClientSecrets.ClientSecret, Is.EqualTo("secret")); + Assert.That(flow.Clock, Is.InstanceOf()); + Assert.Null(flow.DataStore); + Assert.NotNull(flow.HttpClient); + Assert.NotNull(flow.Scopes); + Assert.That(flow.TokenServerUrl, Is.EqualTo("https://token.com")); + + Assert.That(flow.HttpClient.MessageHandler.UnsuccessfulResponseHandlers.Count(), Is.EqualTo(1)); + Assert.That(flow.HttpClient.MessageHandler.UnsuccessfulResponseHandlers.First(), + Is.InstanceOf()); + } + + #endregion + + #region LoadToken + + [Test] + public void LoadTokenAsync_NoDataStore() + { + var flow = CreateFlow(); + Assert.Null(flow.LoadTokenAsync("user", CancellationToken.None).Result); + } + + [Test] + public void LoadTokenAsync_NullResponse() + { + TaskCompletionSource tcs = new TaskCompletionSource(); + tcs.SetResult(null); + Assert.Null(SubtestLoadTokenAsync(tcs)); + } + + [Test] + public void LoadTokenAsync_TokenResponse() + { + TokenResponse response = new TokenResponse + { + AccessToken = "access" + }; + + TaskCompletionSource tcs = new TaskCompletionSource(); + tcs.SetResult(response); + var result = SubtestLoadTokenAsync(tcs); + Assert.That(result, Is.EqualTo(response)); + } + + private TokenResponse SubtestLoadTokenAsync(TaskCompletionSource tcs) + { + var mock = new Mock(); + mock.Setup(ds => ds.GetAsync("user")).Returns(tcs.Task); + var flow = CreateFlow(dataStore: mock.Object); + var result = flow.LoadTokenAsync("user", CancellationToken.None).Result; + mock.Verify(ds => ds.GetAsync("user")); + return result; + } + + #endregion + + #region CreateAuthorizationCodeRequest + + [Test] + public void TestCreateAuthorizationCodeRequest() + { + var request = CreateFlow(scopes: new[] { "a", "b" }).CreateAuthorizationCodeRequest("redirect"); + Assert.That(request.AuthorizationServerUrl, Is.EqualTo(new Uri(AuthorizationCodeUrl))); + Assert.That(request.ClientId, Is.EqualTo("id")); + Assert.That(request.RedirectUri, Is.EqualTo("redirect")); + Assert.That(request.ResponseType, Is.EqualTo("code")); + Assert.That(request.Scope, Is.EqualTo("a b")); + Assert.Null(request.State); + } + + #endregion + + [Test] + public void TestExchangeCodeForTokenAsync() + { + var mock = new Mock(); + var handler = new FetchTokenMessageHandler(); + handler.AuthorizationCodeTokenRequest = new AuthorizationCodeTokenRequest() + { + Code = "c0de", + RedirectUri = "redIrect", + Scope = "a" + }; + MockHttpClientFactory mockFactory = new MockHttpClientFactory(handler); + + TaskCompletionSource tcs = new TaskCompletionSource(); + tcs.SetResult(null); + mock.Setup(ds => ds.StoreAsync("uSer", It.IsAny())).Returns(tcs.Task); + + var flow = CreateFlow(httpClientFactory: mockFactory, scopes: new[] { "a" }, dataStore: mock.Object); + var response = flow.ExchangeCodeForTokenAsync("uSer", "c0de", "redIrect", CancellationToken.None).Result; + SubtestTokenResponse(response); + + mock.Verify(ds => ds.StoreAsync("uSer", It.IsAny())); + } + + [Test] + public void TestRefreshTokenAsync() + { + var mock = new Mock(); + var handler = new FetchTokenMessageHandler(); + handler.RefreshTokenRequest = new RefreshTokenRequest() + { + RefreshToken = "REFRESH", + Scope = "a" + }; + MockHttpClientFactory mockFactory = new MockHttpClientFactory(handler); + + TaskCompletionSource tcs = new TaskCompletionSource(); + tcs.SetResult(null); + mock.Setup(ds => ds.StoreAsync("uSer", It.IsAny())).Returns(tcs.Task); + + var flow = CreateFlow(httpClientFactory: mockFactory, scopes: new[] { "a" }, dataStore: mock.Object); + var response = flow.RefreshTokenAsync("uSer", "REFRESH", CancellationToken.None).Result; + SubtestTokenResponse(response); + + + mock.Verify(ds => ds.StoreAsync("uSer", It.IsAny())); + } + + #region FetchToken + + /// + /// Fetch token message handler, which expects an authorization code token request or a refresh token request. + /// It verifies all the query parameters are valid and return an error response in case + /// is true. + /// + public class FetchTokenMessageHandler : CountableMessageHandler + { + internal AuthorizationCodeTokenRequest AuthorizationCodeTokenRequest { get; set; } + internal RefreshTokenRequest RefreshTokenRequest { get; set; } + internal bool Error { get; set; } + + protected override async Task SendAsyncCore(HttpRequestMessage request, + CancellationToken taskCancellationToken) + { + Assert.That(request.RequestUri, Is.EqualTo(new Uri(TokenUrl))); + + if (AuthorizationCodeTokenRequest != null) + { + // Verify right parameters. + var content = await request.Content.ReadAsStringAsync(); + foreach (var parameter in content.Split('&')) + { + var keyValue = parameter.Split('='); + switch (keyValue[0]) + { + case "code": + Assert.That(keyValue[1], Is.EqualTo("c0de")); + break; + case "redirect_uri": + Assert.That(keyValue[1], Is.EqualTo("redIrect")); + break; + case "scope": + Assert.That(keyValue[1], Is.EqualTo("a")); + break; + case "grant_type": + Assert.That(keyValue[1], Is.EqualTo("authorization_code")); + break; + case "client_id": + Assert.That(keyValue[1], Is.EqualTo("id")); + break; + case "client_secret": + Assert.That(keyValue[1], Is.EqualTo("secret")); + break; + default: + throw new ArgumentOutOfRangeException("Invalid parameter!"); + } + } + } + else + { + // Verify right parameters. + var content = await request.Content.ReadAsStringAsync(); + foreach (var parameter in content.Split('&')) + { + var keyValue = parameter.Split('='); + switch (keyValue[0]) + { + case "refresh_token": + Assert.That(keyValue[1], Is.EqualTo("REFRESH")); + break; + case "scope": + Assert.That(keyValue[1], Is.EqualTo("a")); + break; + case "grant_type": + Assert.That(keyValue[1], Is.EqualTo("refresh_token")); + break; + case "client_id": + Assert.That(keyValue[1], Is.EqualTo("id")); + break; + case "client_secret": + Assert.That(keyValue[1], Is.EqualTo("secret")); + break; + default: + throw new ArgumentOutOfRangeException("Invalid parameter!"); + } + } + } + + var response = new HttpResponseMessage(); + if (Error) + { + response.StatusCode = System.Net.HttpStatusCode.BadRequest; + var serializedObject = NewtonsoftJsonSerializer.Instance.Serialize(new TokenErrorResponse + { + Error = "error", + ErrorDescription = "desc", + ErrorUri = "uri" + }); + response.Content = new StringContent(serializedObject, Encoding.UTF8); + } + else + { + var serializedObject = NewtonsoftJsonSerializer.Instance.Serialize(new TokenResponse + { + AccessToken = "a", + RefreshToken = "r", + ExpiresInSeconds = 100, + Scope = "b", + }); + response.Content = new StringContent(serializedObject, Encoding.UTF8); + } + + return response; + } + } + + [Test] + public void TestFetchTokenAsync_AuthorizationCodeRequest() + { + var handler = new FetchTokenMessageHandler(); + handler.AuthorizationCodeTokenRequest = new AuthorizationCodeTokenRequest() + { + Code = "c0de", + RedirectUri = "redIrect", + Scope = "a" + }; + MockHttpClientFactory mockFactory = new MockHttpClientFactory(handler); + + var flow = CreateFlow(httpClientFactory: mockFactory); + var response = flow.FetchTokenAsync("user", handler.AuthorizationCodeTokenRequest, + CancellationToken.None).Result; + SubtestTokenResponse(response); + } + + [Test] + public void TestFetchTokenAsync_RefreshTokenRequest() + { + var handler = new FetchTokenMessageHandler(); + handler.RefreshTokenRequest = new RefreshTokenRequest() + { + RefreshToken = "REFRESH", + Scope = "a" + }; + + MockHttpClientFactory mockFactory = new MockHttpClientFactory(handler); + + var flow = CreateFlow(httpClientFactory: mockFactory); + var response = flow.FetchTokenAsync("user", handler.RefreshTokenRequest, CancellationToken.None).Result; + SubtestTokenResponse(response); + } + + [Test] + public void TestFetchTokenAsync_AuthorizationCodeRequest_Error() + { + var handler = new FetchTokenMessageHandler(); + handler.AuthorizationCodeTokenRequest = new AuthorizationCodeTokenRequest() + { + Code = "c0de", + RedirectUri = "redIrect", + Scope = "a" + }; + handler.Error = true; + SubtestFetchTokenAsync_Error(handler); + } + + [Test] + public void TestFetchTokenAsync_RefreshTokenRequest_Error() + { + var handler = new FetchTokenMessageHandler(); + handler.RefreshTokenRequest = new RefreshTokenRequest() + { + RefreshToken = "REFRESH", + Scope = "a" + }; + handler.Error = true; + SubtestFetchTokenAsync_Error(handler); + } + + /// Subtest for receiving an error token response. + /// The message handler. + private void SubtestFetchTokenAsync_Error(FetchTokenMessageHandler handler) + { + MockHttpClientFactory mockFactory = new MockHttpClientFactory(handler); + var flow = CreateFlow(httpClientFactory: mockFactory); + try + { + var request = + (TokenRequest)handler.AuthorizationCodeTokenRequest ?? (TokenRequest)handler.RefreshTokenRequest; + var result = flow.FetchTokenAsync("user", request, CancellationToken.None).Result; + Assert.Fail(); + } + catch (AggregateException aex) + { + var ex = aex.InnerException as TokenResponseException; + Assert.IsNotNull(ex); + var result = ex.Error; + Assert.That(result.Error, Is.EqualTo("error")); + Assert.That(result.ErrorDescription, Is.EqualTo("desc")); + Assert.That(result.ErrorUri, Is.EqualTo("uri")); + } + } + + #endregion + + /// Creates an authorization code flow with the given parameters. + /// The data store. + /// The Scopes. + /// The HTTP client factory. If not set the default will be used. + /// Authorization code flow + private AuthorizationCodeFlow CreateFlow(IDataStore dataStore = null, IEnumerable scopes = null, + IHttpClientFactory httpClientFactory = null) + { + var secrets = new ClientSecrets() { ClientId = "id", ClientSecret = "secret" }; + var initializer = new AuthorizationCodeFlow.Initializer(AuthorizationCodeUrl, TokenUrl) + { + ClientSecrets = secrets, + HttpClientFactory = httpClientFactory + }; + + if (dataStore != null) + { + initializer.DataStore = dataStore; + } + if (scopes != null) + { + initializer.Scopes = scopes; + } + return new AuthorizationCodeFlow(initializer); + } + + /// Verifies that the token response contains the expected data. + /// The token response + private void SubtestTokenResponse(TokenResponse response) + { + Assert.That(response.RefreshToken, Is.EqualTo("r")); + Assert.That(response.ExpiresInSeconds, Is.EqualTo(100)); + Assert.That(response.Scope, Is.EqualTo("b")); + } + } +} Index: Src/GoogleApis.Auth.Tests/OAuth2/BearerTokenTests.cs =================================================================== new file mode 100644 --- /dev/null +++ b/Src/GoogleApis.Auth.Tests/OAuth2/BearerTokenTests.cs @@ -0,0 +1,102 @@ +/* +Copyright 2013 Google Inc + +Licensed under the Apache License, Version 2.0(the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; + +using NUnit.Framework; + +namespace Google.Apis.Auth.OAuth2 +{ + /// Tests for . + [TestFixture] + public class BearerTokenTests + { + [Test] + public void AuthorizationHeaderAccessMethod_Intercept() + { + var request = new HttpRequestMessage(); + new BearerToken.AuthorizationHeaderAccessMethod().Intercept(request, "abc"); + Assert.That(request.Headers.Authorization.ToString(), Is.EqualTo("Bearer abc")); + } + + [Test] + public void AuthorizationHeaderAccessMethod_InterceptOverride() + { + var request = new HttpRequestMessage(); + request.Headers.Authorization = new AuthenticationHeaderValue("a", "1"); + new BearerToken.AuthorizationHeaderAccessMethod().Intercept(request, "abc"); + Assert.That(request.Headers.Authorization.ToString(), Is.EqualTo("Bearer abc")); + } + + [Test] + public void AuthorizationHeaderAccessMethod_GetAccessToken() + { + var request = new HttpRequestMessage(); + request.Headers.Authorization = new AuthenticationHeaderValue("a", "1"); + var accessToken = new BearerToken.AuthorizationHeaderAccessMethod().GetAccessToken(request); + Assert.IsNullOrEmpty(accessToken); + + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", "abc"); + accessToken = new BearerToken.AuthorizationHeaderAccessMethod().GetAccessToken(request); + Assert.That(accessToken, Is.EqualTo("abc")); + } + + [Test] + public void QueryParameterAccessMethod_Intercept() + { + var request = new HttpRequestMessage(HttpMethod.Get, new Uri("https://sample.com/path")); + new BearerToken.QueryParameterAccessMethod().Intercept(request, "abc"); + Assert.That(request.RequestUri, Is.EqualTo(new Uri("https://sample.com/path?access_token=abc"))); + } + + [Test] + public void QueryParameterAccessMethod_Intercept_WithQueryParameters() + { + var request = new HttpRequestMessage(HttpMethod.Get, new Uri("https://sample.com/path?a=1")); + new BearerToken.QueryParameterAccessMethod().Intercept(request, "abc"); + Assert.That(request.RequestUri, Is.EqualTo(new Uri("https://sample.com/path?a=1&access_token=abc"))); + } + + [Test] + public void QueryParameterAccessMethod_GetAccessToken() + { + // No query parameter at all. + var request = new HttpRequestMessage(HttpMethod.Get, new Uri("https://sample.com")); + var accessToken = new BearerToken.QueryParameterAccessMethod().GetAccessToken(request); + Assert.IsNullOrEmpty(accessToken); + + // Different query parameter. + request = new HttpRequestMessage(HttpMethod.Get, new Uri("https://sample.com?a=1")); + accessToken = new BearerToken.QueryParameterAccessMethod().GetAccessToken(request); + Assert.IsNullOrEmpty(accessToken); + + // One query parameter and it's access_token. + request = new HttpRequestMessage(HttpMethod.Get, new Uri("https://sample.com?a=1&access_token=abc")); + accessToken = new BearerToken.QueryParameterAccessMethod().GetAccessToken(request); + Assert.That(accessToken, Is.EqualTo("abc")); + + // 2 query parameters and one of them is access_token. + request = new HttpRequestMessage(HttpMethod.Get, new Uri("https://sample.com?access_token=abc")); + accessToken = new BearerToken.QueryParameterAccessMethod().GetAccessToken(request); + Assert.That(accessToken, Is.EqualTo("abc")); + } + } +} Index: Src/GoogleApis.Auth.Tests/OAuth2/Requests/AuthorizationCodeRequestUrlTests.cs =================================================================== new file mode 100644 --- /dev/null +++ b/Src/GoogleApis.Auth.Tests/OAuth2/Requests/AuthorizationCodeRequestUrlTests.cs @@ -0,0 +1,82 @@ +/* +Copyright 2013 Google Inc + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +using System; + +using NUnit.Framework; + +namespace Google.Apis.Auth.OAuth2.Requests +{ + /// + /// Tests for . + /// It tests that the constructor sets the default values and the building URI method. + /// + [TestFixture] + public class AuthorizationCodeRequestUrlTests + { + private readonly Uri AuthorizationCodeUrl = new Uri("http://www.authorization-server.com"); + + [Test] + public void TestConstructor() + { + var request = new AuthorizationCodeRequestUrl(AuthorizationCodeUrl); + Assert.That(request.AuthorizationServerUrl, Is.EqualTo(AuthorizationCodeUrl)); + Assert.Null(request.ClientId); + Assert.Null(request.RedirectUri); + Assert.That(request.ResponseType, Is.EqualTo("code")); + Assert.Null(request.Scope); + Assert.Null(request.State); + } + + [Test] + public void TestBuild_DefaultValues() + { + var request = new AuthorizationCodeRequestUrl(AuthorizationCodeUrl); + var uri = request.Build(); + Assert.That(uri, Is.EqualTo(new Uri(AuthorizationCodeUrl.ToString() + "?response_type=code"))); + } + + [Test] + public void TestBuild() + { + var request = new AuthorizationCodeRequestUrl(AuthorizationCodeUrl) + { + ClientId = "100", + RedirectUri = "200", + Scope = "SCOPE", + State = "state", + }; + var uri = request.Build(); + Assert.That(uri, Is.EqualTo(new Uri(AuthorizationCodeUrl.ToString() + + "?response_type=code&client_id=100&redirect_uri=200&scope=SCOPE&state=state"))); + } + + [Test] + public void TestBuild_EscapeValues() + { + var request = new AuthorizationCodeRequestUrl(AuthorizationCodeUrl) + { + ClientId = "pa$$word", + RedirectUri = "???", + Scope = "SC@PE", + State = "!", + }; + var uri = request.Build(); + Assert.That(uri, Is.EqualTo(new Uri(AuthorizationCodeUrl.ToString() + + "?response_type=code&client_id=pa%24%24word&redirect_uri=%3F%3F%3F&scope=SC%40PE&state=%21"))); + } + } +} Index: Src/GoogleApis.Auth.Tests/OAuth2/Requests/AuthorizationCodeTokenRequestTests.cs =================================================================== new file mode 100644 --- /dev/null +++ b/Src/GoogleApis.Auth.Tests/OAuth2/Requests/AuthorizationCodeTokenRequestTests.cs @@ -0,0 +1,44 @@ +/* +Copyright 2013 Google Inc + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +using NUnit.Framework; + +namespace Google.Apis.Auth.OAuth2.Requests +{ + /// + /// Tests for . + /// + [TestFixture] + public class AuthorizationCodeTokenRequestTests + { + [Test] + public void TestConstructor() + { + var request = new AuthorizationCodeTokenRequest(); + Assert.Null(request.Code); + Assert.Null(request.ClientId); + Assert.Null(request.ClientSecret); + Assert.Null(request.RedirectUri); + Assert.That(request.GrantType, Is.EqualTo("authorization_code")); + Assert.Null(request.Scope); + } + } +} Index: Src/GoogleApis.Auth.Tests/OAuth2/Requests/GoogleAuthorizationCodeRequestUrlTest.cs =================================================================== new file mode 100644 --- /dev/null +++ b/Src/GoogleApis.Auth.Tests/OAuth2/Requests/GoogleAuthorizationCodeRequestUrlTest.cs @@ -0,0 +1,49 @@ +/* +Copyright 2013 Google Inc + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +using NUnit.Framework; + +namespace Google.Apis.Auth.OAuth2.Requests +{ + /// + /// Tests for . + /// + [TestFixture] + public class GoogleAuthorizationCodeRequestUrlTest + { + private readonly Uri AuthorizationCodeUrl = new Uri("http://www.authorization-server.com"); + + [Test] + public void TestConstructor() + { + var request = new GoogleAuthorizationCodeRequestUrl(AuthorizationCodeUrl); + Assert.That(request.AuthorizationServerUrl, Is.EqualTo(AuthorizationCodeUrl)); + Assert.Null(request.ClientId); + Assert.Null(request.RedirectUri); + Assert.That(request.ResponseType, Is.EqualTo("code")); + Assert.Null(request.Scope); + Assert.Null(request.State); + Assert.That(request.AccessType, Is.EqualTo("offline")); + Assert.Null(request.ApprovalPrompt); + Assert.Null(request.LoginHint); + } + } +} Index: Src/GoogleApis.Auth.Tests/OAuth2/Requests/RefreshTokenRequestTests.cs =================================================================== new file mode 100644 --- /dev/null +++ b/Src/GoogleApis.Auth.Tests/OAuth2/Requests/RefreshTokenRequestTests.cs @@ -0,0 +1,44 @@ +/* +Copyright 2013 Google Inc + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +using NUnit.Framework; + +namespace Google.Apis.Auth.OAuth2.Requests +{ + /// + /// Tests for . + /// + [TestFixture] + public class RefreshTokenRequestTests + { + [Test] + public void TestConstructor() + { + var request = new RefreshTokenRequest(); + Assert.Null(request.Scope); + Assert.Null(request.ClientId); + Assert.Null(request.ClientSecret); + + Assert.Null(request.RefreshToken); + Assert.That(request.GrantType, Is.EqualTo("refresh_token")); + } + } +} Index: Src/GoogleApis.Auth.Tests/OAuth2/Responses/AuthorizationCodeResponseUrlTests.cs =================================================================== new file mode 100644 --- /dev/null +++ b/Src/GoogleApis.Auth.Tests/OAuth2/Responses/AuthorizationCodeResponseUrlTests.cs @@ -0,0 +1,77 @@ +/* +Copyright 2013 Google Inc + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +using NUnit.Framework; + +namespace Google.Apis.Auth.OAuth2.Responses +{ + /// + /// Tests for . + /// + [TestFixture] + public class AuthorizationCodeResponseUrlTests + { + [Test] + public void TestConstructor_Default() + { + var response = new AuthorizationCodeResponseUrl(); + Assert.Null(response.Code); + Assert.Null(response.Error); + Assert.Null(response.ErrorUri); + Assert.Null(response.ErrorDescription); + Assert.Null(response.State); + } + + [Test] + public void TestConstructor_Dictionary() + { + var dic = new Dictionary(); + dic["code"] = "123"; + dic["error"] = "ERR"; + dic["error_uri"] = "URI"; + dic["error_description"] = "DESC"; + dic["state"] = "STATE"; + + // Some other parameter, which is not part of the response. + dic["another_one"] = "BLAH BLAH BLAH"; + + var response = new AuthorizationCodeResponseUrl(dic); + SubtestConstructor(response); + } + + [Test] + public void TestConstructor_Query() + { + string query = "code=123&error=ERR&error_uri=URI&error_description=DESC&state=STATE&other_ine=2222222222"; + var response = new AuthorizationCodeResponseUrl(query); + SubtestConstructor(response); + } + + private void SubtestConstructor(AuthorizationCodeResponseUrl response) + { + Assert.That(response.Code, Is.EqualTo("123")); + Assert.That(response.Error, Is.EqualTo("ERR")); + Assert.That(response.ErrorUri, Is.EqualTo("URI")); + Assert.That(response.ErrorDescription, Is.EqualTo("DESC")); + Assert.That(response.State, Is.EqualTo("STATE")); + } + } +} Index: Src/GoogleApis.Auth.Tests/OAuth2/Responses/TokenResponseTests.cs =================================================================== new file mode 100644 --- /dev/null +++ b/Src/GoogleApis.Auth.Tests/OAuth2/Responses/TokenResponseTests.cs @@ -0,0 +1,109 @@ +/* +Copyright 2013 Google Inc + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +using NUnit.Framework; +using Newtonsoft.Json; + +using Google.Apis.Tests; + +namespace Google.Apis.Auth.OAuth2.Responses +{ + /// + /// Tests for . + /// + [TestFixture] + public class TokenResponseTests + { + [Test] + public void TestConstructor() + { + var response = new TokenResponse(); + Assert.Null(response.AccessToken); + Assert.Null(response.ExpiresInSeconds); + Assert.Null(response.RefreshToken); + Assert.Null(response.Scope); + Assert.AreEqual(DateTime.MinValue, response.Issued); + } + + [Test] + public void TestSerializer() + { + var tokenResponse = JsonConvert.DeserializeObject( + @"{ + 'access_token': '123', + 'expires_in': 1000, + 'refresh_token': '456', + 'scope': '789' + }"); + Assert.That(tokenResponse.AccessToken, Is.EqualTo("123")); + Assert.That(tokenResponse.RefreshToken, Is.EqualTo("456")); + Assert.That(tokenResponse.Scope, Is.EqualTo("789")); + Assert.That(tokenResponse.ExpiresInSeconds, Is.EqualTo(1000)); + } + + [Test] + public void IsExpired_True() + { + var issued = DateTime.Now; + var newNow = DateTime.Now.AddSeconds(200); + + var mockClock = new MockClock() + { + Now = newNow + }; + + // Issued not set. + var response = new TokenResponse(); + Assert.True(response.IsExpired(mockClock)); + + // ExpiresInSeconds is not set. + response = new TokenResponse() { Issued = issued }; + Assert.True(response.IsExpired(mockClock)); + + response = new TokenResponse() { ExpiresInSeconds = 1, Issued = issued }; + Assert.True(response.IsExpired(mockClock)); + + response = new TokenResponse() { ExpiresInSeconds = 100, Issued = issued }; + Assert.True(response.IsExpired(mockClock)); + + response = new TokenResponse() { ExpiresInSeconds = 140, Issued = issued }; + Assert.True(response.IsExpired(mockClock)); + } + + [Test] + public void IsExpired_False() + { + var issued = DateTime.Now; + var newNow = DateTime.Now.AddSeconds(200); + + var mockClock = new MockClock() + { + Now = newNow + }; + + var response = new TokenResponse() { AccessToken = "a", ExpiresInSeconds = 141, Issued = issued }; + Assert.False(response.IsExpired(mockClock)); + + response = new TokenResponse() { AccessToken = "a", ExpiresInSeconds = 142, Issued = issued }; + Assert.False(response.IsExpired(mockClock)); + } + } +} Index: Src/GoogleApis.Auth.Tests/Properties/AssemblyInfo.cs =================================================================== new file mode 100644 --- /dev/null +++ b/Src/GoogleApis.Auth.Tests/Properties/AssemblyInfo.cs @@ -0,0 +1,40 @@ +/* +Copyright 2013 Google Inc + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +using System.Reflection; + +// Information about this assembly is defined by the following attributes. +// Change them to the values specific to your project. + +[assembly: AssemblyTitle("Google.Apis.Auth.Tests")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("Google Inc")] +[assembly: AssemblyProduct("")] +[assembly: AssemblyCopyright("Copyright © Google Inc 2013")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// The assembly version has the format "{Major}.{Minor}.{Build}.{Revision}". +// The form "{Major}.{Minor}.*" will automatically update the build and revision, +// and "{Major}.{Minor}.{Build}.*" will update just the revision. +[assembly: AssemblyVersion("1.5.0.*")] + +// The following attributes are used to specify the signing key for the assembly, +// if desired. See the Mono documentation for more information about signing. + +//[assembly: AssemblyDelaySign(false)] +//[assembly: AssemblyKeyFile("")] Index: Src/GoogleApis.Auth.Tests/packages.config =================================================================== new file mode 100644 --- /dev/null +++ b/Src/GoogleApis.Auth.Tests/packages.config @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file Index: Src/GoogleApis.Auth/GoogleApis.Auth.csproj =================================================================== --- a/Src/GoogleApis.Auth/GoogleApis.Auth.csproj +++ b/Src/GoogleApis.Auth/GoogleApis.Auth.csproj @@ -38,11 +38,16 @@ + - + + + + + Index: Src/GoogleApis.Auth/OAuth2/AuthorizationCodeFlow.cs =================================================================== new file mode 100644 --- /dev/null +++ b/Src/GoogleApis.Auth/OAuth2/AuthorizationCodeFlow.cs @@ -0,0 +1,314 @@ +/* +Copyright 2013 Google Inc + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +using System; +using System.Collections.Generic; +using System.IO; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +using Google.Apis.Auth.OAuth2.Requests; +using Google.Apis.Auth.OAuth2.Responses; +using Google.Apis.Http; +using Google.Apis.Logging; +using Google.Apis.Util; +using Google.Apis.Util.Store; +using Google.Apis.Testing; + +namespace Google.Apis.Auth.OAuth2 +{ + /// + /// Thread-safe OAuth 2.0 authorization code flow that manages and persists end-user credentials. + /// + /// This is designed to simplify the flow in which an end-user authorizes the application to access their protected + /// data, and then the application has access to their data based on an access token and a refresh token to refresh + /// that access token when it expires. + /// + /// + public class AuthorizationCodeFlow : IAuthorizationCodeFlow + { + private static readonly ILogger Logger = ApplicationContext.Logger.ForType(); + + #region Initializer + + /// An initializer class for the authorization code flow. + public class Initializer + { + /// + /// Gets or sets the method for presenting the access token to the resource server. + /// The default value is . + /// + public IAccessMethod AccessMethod { get; set; } + + /// Gets the token server URL. + public string TokenServerUrl { get; private set; } + + /// Gets or sets the authorization server URL. + public string AuthorizationServerUrl { get; private set; } + + /// Gets or sets the client secrets which includes the client identifier and its secret. + public ClientSecrets ClientSecrets { get; set; } + + /// + /// Gets or sets the client secrets stream which contains the client identifier and its secret. + /// + /// The AuthorizationCodeFlow constructor is responsible for disposing the stream. + public Stream ClientSecretsStream { get; set; } + + /// Gets or sets the data store used to store the token response. + public IDataStore DataStore { get; set; } + + /// + /// Gets or sets the scopes which indicate the API access your application is requesting. + /// + public IEnumerable Scopes { get; set; } + + /// + /// Gets or sets the factory for creating instance. + /// + public IHttpClientFactory HttpClientFactory { get; set; } + + /// + /// Get or sets the exponential back-off policy. Default value is UnsuccessfulResponse503, which + /// means that exponential back-off is used on 503 abnormal HTTP responses. + /// If the value is set to None, no exponential back-off policy is used, and it's up to user to + /// configure the in an + /// to set a specific back-off + /// implementation (using ). + /// + public ExponentialBackOffPolicy DefaultExponentialBackOffPolicy { get; set; } + + /// + /// Gets or sets the clock. The clock is used to determine if the token has expired, if so we will try to + /// refresh it. The default value is . + /// + public IClock Clock { get; set; } + + /// Constructs a new initializer. + /// Authorization server URL + /// Token server URL + public Initializer(string authorizationServerUrl, string tokenServerUrl) + { + AuthorizationServerUrl = authorizationServerUrl; + TokenServerUrl = tokenServerUrl; + + Scopes = new List(); + AccessMethod = new BearerToken.AuthorizationHeaderAccessMethod(); + DefaultExponentialBackOffPolicy = ExponentialBackOffPolicy.UnsuccessfulResponse503; + Clock = SystemClock.Default; + } + } + + #endregion + + #region Readonly fields + + private readonly IAccessMethod accessMethod; + private readonly string tokenServerUrl; + private readonly string authorizationServerUrl; + private readonly ClientSecrets clientSecrets; + private readonly IDataStore dataStore; + private readonly IEnumerable scopes; + private readonly ConfigurableHttpClient httpClient; + private readonly IClock clock; + + #endregion + + /// Gets the token server URL. + public string TokenServerUrl { get { return tokenServerUrl; } } + + /// Gets the authorization code server URL. + public string AuthorizationServerUrl { get { return authorizationServerUrl; } } + + /// Gets the client secrets which includes the client identifier and its secret. + public ClientSecrets ClientSecrets { get { return clientSecrets; } } + + /// Gets the data store used to store the credentials. + public IDataStore DataStore { get { return dataStore; } } + + /// Gets the scopes which indicate the API access your application is requesting. + public IEnumerable Scopes { get { return scopes; } } + + /// Gets the HTTP client used to make authentication requests to the server. + public ConfigurableHttpClient HttpClient { get { return httpClient; } } + + /// Constructs a new flow using the initializer's properties. + public AuthorizationCodeFlow(Initializer initializer) + { + clientSecrets = initializer.ClientSecrets; + if (clientSecrets == null) + { + if (initializer.ClientSecretsStream == null) + { + throw new ArgumentException("You MUST set ClientSecret or ClientSecretStream on the initializer"); + } + + using (initializer.ClientSecretsStream) + { + clientSecrets = GoogleClientSecrets.Load(initializer.ClientSecretsStream).Secrets; + } + } + + accessMethod = initializer.AccessMethod.ThrowIfNull("Initializer.AccessMethod"); + clock = initializer.Clock.ThrowIfNull("Initializer.Clock"); + tokenServerUrl = initializer.TokenServerUrl.ThrowIfNullOrEmpty("Initializer.TokenServerUrl"); + authorizationServerUrl = initializer.AuthorizationServerUrl.ThrowIfNullOrEmpty + ("Initializer.AuthorizationServerUrl"); + + dataStore = initializer.DataStore; + if (dataStore == null) + { + Logger.Warning("Datastore is null, as a result the user's credential will not be stored"); + } + scopes = initializer.Scopes; + + // Set the HTTP client. + var httpArgs = new CreateHttpClientArgs(); + + // Add exponential back-off initializer if necessary. + if (initializer.DefaultExponentialBackOffPolicy != ExponentialBackOffPolicy.None) + { + httpArgs.Initializers.Add( + new ExponentialBackOffInitializer(initializer.DefaultExponentialBackOffPolicy, + () => new BackOffHandler(new ExponentialBackOff()))); + } + httpClient = (initializer.HttpClientFactory ?? new HttpClientFactory()).CreateHttpClient(httpArgs); + } + + #region IAuthorizationCodeFlow overrides + + public IAccessMethod AccessMethod { get { return accessMethod; } } + + public IClock Clock { get { return clock; } } + + public async Task LoadTokenAsync(string userId, CancellationToken taskCancellationToken) + { + taskCancellationToken.ThrowIfCancellationRequested(); + if (DataStore == null) + { + return null; + } + return await DataStore.GetAsync(userId).ConfigureAwait(false); + } + + public async Task DeleteTokenAsync(string userId, CancellationToken taskCancellationToken) + { + taskCancellationToken.ThrowIfCancellationRequested(); + if (DataStore != null) + { + await DataStore.DeleteAsync(userId).ConfigureAwait(false); + } + } + + public virtual AuthorizationCodeRequestUrl CreateAuthorizationCodeRequest(string redirectUri) + { + return new AuthorizationCodeRequestUrl(new Uri(AuthorizationServerUrl)) + { + ClientId = ClientSecrets.ClientId, + Scope = string.Join(" ", Scopes), + RedirectUri = redirectUri + }; + } + + public async Task ExchangeCodeForTokenAsync(string userId, string code, string redirectUri, + CancellationToken taskCancellationToken) + { + var authorizationCodeTokenReq = new AuthorizationCodeTokenRequest + { + Scope = string.Join(" ", Scopes), + RedirectUri = redirectUri, + Code = code, + }; + + var token = await FetchTokenAsync(userId, authorizationCodeTokenReq, taskCancellationToken) + .ConfigureAwait(false); + await StoreTokenAsync(userId, token, taskCancellationToken).ConfigureAwait(false); + return token; + } + + public async Task RefreshTokenAsync(string userId, string refreshToken, + CancellationToken taskCancellationToken) + { + var refershTokenReq = new RefreshTokenRequest + { + RefreshToken = refreshToken, + }; + var token = await FetchTokenAsync(userId, refershTokenReq, taskCancellationToken).ConfigureAwait(false); + + // The new token may not contain a refresh token, so set it with the given refresh token. + if (token.RefreshToken == null) + { + token.RefreshToken = refreshToken; + } + + await StoreTokenAsync(userId, token, taskCancellationToken).ConfigureAwait(false); + return token; + } + + #endregion + + /// Stores the token in the . + /// User identifier + /// Token to store + /// Cancellation token to cancel operation + private async Task StoreTokenAsync(string userId, TokenResponse token, CancellationToken taskCancellationToken) + { + taskCancellationToken.ThrowIfCancellationRequested(); + if (DataStore != null) + { + await DataStore.StoreAsync(userId, token).ConfigureAwait(false); + } + } + + /// Retrieve a new token from the server using the specified request. + /// User identifier + /// Token request + /// Cancellation token to cancel operation + /// Token response with the new access token + [VisibleForTestOnly] + internal async Task FetchTokenAsync(string userId, TokenRequest request, + CancellationToken taskCancellationToken) + { + // Add client id and client secret to requests. + request.ClientId = ClientSecrets.ClientId; + request.ClientSecret = ClientSecrets.ClientSecret; + + TokenResponseException tokenException = null; + try + { + var tokenResponse = await request.Execute(httpClient, TokenServerUrl, taskCancellationToken, Clock); + return tokenResponse; + } + catch (TokenResponseException ex) + { + // In case there is an exception during getting the token, we delete any user's token information from + // the data store. + tokenException = ex; + } + await DeleteTokenAsync(userId, taskCancellationToken); + throw tokenException; + } + + public void Dispose() + { + if (HttpClient != null) + { + HttpClient.Dispose(); + } + } + } +} Index: Src/GoogleApis.Auth/OAuth2/AuthorizationCodeInstalledApp.cs =================================================================== new file mode 100644 --- /dev/null +++ b/Src/GoogleApis.Auth/OAuth2/AuthorizationCodeInstalledApp.cs @@ -0,0 +1,95 @@ +/* +Copyright 2013 Google Inc + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +using System.Threading; +using System.Threading.Tasks; + +using Google.Apis.Auth.OAuth2.Responses; +using Google.Apis.Auth.OAuth2.Requests; +using Google.Apis.Logging; + +namespace Google.Apis.Auth.OAuth2 +{ + /// + /// Thread-safe OAuth 2.0 authorization code flow for an installed application that persists end-user credentials. + /// + public class AuthorizationCodeInstalledApp : IAuthorizationCodeInstalledApp + { + private static readonly ILogger Logger = ApplicationContext.Logger.ForType(); + + private readonly IAuthorizationCodeFlow flow; + private readonly ICodeReceiver codeReceiver; + + /// + /// Constructs a new authorization code installed application with the given flow and code receiver. + /// + public AuthorizationCodeInstalledApp(IAuthorizationCodeFlow flow, ICodeReceiver codeReceiver) + { + this.flow = flow; + this.codeReceiver = codeReceiver; + } + + #region IAuthorizationCodeInstalledApp Members + + /// Gets the authorization code flow. + public IAuthorizationCodeFlow Flow + { + get { return flow; } + } + + /// Gets the code receiver which is responsible for receiving the authorization code. + public ICodeReceiver CodeReceiver + { + get { return codeReceiver; } + } + + public async Task Authorize(string userId, CancellationToken taskCancellationToken) + { + // Try to load a token from the data store. + var token = await Flow.LoadTokenAsync(userId, taskCancellationToken).ConfigureAwait(false); + + // If the stored token is null or it doesn't have a refresh token and the access token is expired we need + // to retrieve a new authorization code. + if (token == null || (token.RefreshToken == null && token.IsExpired(flow.Clock))) + { + // Create a authorization code request. + var redirectUri = CodeReceiver.RedirectUri; + AuthorizationCodeRequestUrl codeRequest = Flow.CreateAuthorizationCodeRequest(redirectUri); + + // Receive the code. + var response = await CodeReceiver.ReceiveCodeAsync(codeRequest, taskCancellationToken) + .ConfigureAwait(false); + + if (string.IsNullOrEmpty(response.Code)) + { + var errorResponse = new TokenErrorResponse(response); + Logger.Info("Received an error. The response is: {0}", errorResponse); + throw new TokenResponseException(errorResponse); + } + + Logger.Debug("Received \"{0}\" code", response.Code); + + // Get the token based on the code. + token = await Flow.ExchangeCodeForTokenAsync(userId, response.Code, CodeReceiver.RedirectUri, + taskCancellationToken).ConfigureAwait(false); + } + + return new UserCredential(flow, userId, token); + } + + #endregion + } +} \ No newline at end of file Index: Src/GoogleApis.Auth/OAuth2/AuthorizationCodeWebApp.cs =================================================================== new file mode 100644 --- /dev/null +++ b/Src/GoogleApis.Auth/OAuth2/AuthorizationCodeWebApp.cs @@ -0,0 +1,127 @@ +/* +Copyright 2013 Google Inc + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +using System; +using System.Threading; +using System.Threading.Tasks; + +using Google.Apis.Auth.OAuth2.Requests; + +namespace Google.Apis.Auth.OAuth2 +{ + /// + /// Thread safe OAuth 2.0 authorization code flow for a web application that persists end-user credentials. + /// + public class AuthorizationCodeWebApp + { + /// + /// The state key. As part of making the request for authorization code we save the original request to verify + /// that this server create the original request. + /// + public const string StateKey = "oauth_"; + + /// The length of the random number which will be added to the end of the state parameter. + public const int StateRandomLength = 8; + + /// + /// AuthResult which contains the user's credentials if it was loaded successfully from the store. Otherwise + /// it contains the redirect URI for the authorization server. + /// + public class AuthResult + { + /// + /// Gets or sets the user's credentials or null in case the end user needs to authorize. + /// + public UserCredential Credential { get; set; } + + /// + /// Gets or sets the redirect URI to for the user to authorize against the authorization server or + /// null in case the was loaded from the data + /// store. + /// + public string RedirectUri { get; set; } + } + + private readonly IAuthorizationCodeFlow flow; + private readonly string redirectUri; + private readonly string state; + + /// Gets the authorization code flow. + public IAuthorizationCodeFlow Flow + { + get { return flow; } + } + + /// Gets the OAuth2 callback redirect URI. + public string RedirectUri + { + get { return redirectUri; } + } + + /// Gets the state which is used to navigate back to the page that started the OAuth flow. + public string State + { + get { return state; } + } + + /// + /// Constructs a new authorization code installed application with the given flow and code receiver. + /// + public AuthorizationCodeWebApp(IAuthorizationCodeFlow flow, string redirectUri, string state) + { + // TODO(peleyal): should we provide a way to disable to random number in the end of the state parameter? + this.flow = flow; + this.redirectUri = redirectUri; + this.state = state; + } + + /// Authorizes the web application to access user's protected data. + /// User identifier + /// Cancellation token to cancel an operation + /// + /// Auth result object which contains the user's credential or redirect URI for the authorization server + /// + public async Task Authorize(string userId, CancellationToken taskCancellationToken) + { + // Try to load a token from the data store. + var token = await Flow.LoadTokenAsync(userId, taskCancellationToken).ConfigureAwait(false); + + // If the stored token is null or it doesn't have a refresh token and the access token is expired, we need + // to retrieve a new access token. + if (token == null || (token.RefreshToken == null && token.IsExpired(flow.Clock))) + { + // Create a authorization code request. + AuthorizationCodeRequestUrl codeRequest = Flow.CreateAuthorizationCodeRequest(redirectUri); + + // Add a random number to the end of the state so we can indicate the original request was made by this + // call. + var oauthState = state; + if (Flow.DataStore != null) + { + var rndString = new string('9', StateRandomLength); + var random = new Random().Next(int.Parse(rndString)).ToString("D" + StateRandomLength); + oauthState += random; + await Flow.DataStore.StoreAsync(StateKey + userId, oauthState); + } + codeRequest.State = oauthState; + + return new AuthResult { RedirectUri = codeRequest.Build().ToString() }; + } + + return new AuthResult { Credential = new UserCredential(flow, userId, token) }; + } + } +} Index: Src/GoogleApis.Auth/OAuth2/BearerToken.cs =================================================================== --- a/Src/GoogleApis.Auth/OAuth2/BearerToken.cs +++ b/Src/GoogleApis.Auth/OAuth2/BearerToken.cs @@ -60,8 +60,8 @@ public void Intercept(HttpRequestMessage request, string accessToken) { var uri = request.RequestUri; - request.RequestUri = new Uri(string.Format("{0}&{1}{2}{3}", - uri.ToString(), AccessTokenKey, string.IsNullOrEmpty(uri.Query) ? "?" : "&", + request.RequestUri = new Uri(string.Format("{0}{1}{2}={3}", + uri.ToString(), string.IsNullOrEmpty(uri.Query) ? "?" : "&", AccessTokenKey, Uri.EscapeDataString(accessToken))); } Index: Src/GoogleApis.Auth/OAuth2/Credential.cs =================================================================== --- a/Src/GoogleApis.Auth/OAuth2/Credential.cs +++ b/Src/GoogleApis.Auth/OAuth2/Credential.cs @@ -15,13 +15,125 @@ */ using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; +using System.Net; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +using Google.Apis.Auth.OAuth2.Responses; +using Google.Apis.Http; +using Google.Apis.Logging; namespace Google.Apis.Auth.OAuth2 { - public class Credential + /// + /// OAuth 2.0 credential for accessing protected resources using an access token, as well as optionally refreshing + /// the access token when it expires using a refresh token. + /// + public class Credential : IHttpExecuteInterceptor, IHttpUnsuccessfulResponseHandler, + IConfigurableHttpClientInitializer { + private static readonly ILogger Logger = ApplicationContext.Logger.ForType(); + + private TokenResponse token; + private object lockObject = new object(); + + public TokenResponse Token + { + get + { + lock (lockObject) + { + return token; + } + } + private set + { + lock (lockObject) + { + token = value; + } + } + } + + private readonly IAuthorizationCodeFlow flow; + private readonly string userId; + + /// Constructs a new credential instance. + /// Authorization code flow + /// User identifier + /// An initial token for the user + public Credential(IAuthorizationCodeFlow flow, string userId, TokenResponse token) + { + this.flow = flow; + this.userId = userId; + this.token = token; + } + + /// + /// Default implementation is to try to refresh the access token if there is no access token or if we are 1 + /// minute away from expiration. If token server is unavailable, it will try to use the access token even if + /// has expired. If successful, it will call . + /// + public async Task InterceptAsync(HttpRequestMessage request, CancellationToken taskCancellationToken) + { + if (Token.IsExpired(flow.Clock)) + { + if (!await RefreshTokenAsync(taskCancellationToken).ConfigureAwait(false)) + { + throw new InvalidOperationException("The access token is expired but we can't refresh it"); + } + } + + flow.AccessMethod.Intercept(request, Token.AccessToken); + } + + /// + /// Refreshes the token by calling to . Then it + /// updates the with the new token instance. + /// + /// Cancellation token to cancel an operation + /// true if the token was refreshed + private async Task RefreshTokenAsync(CancellationToken taskCancellationToken) + { + if (Token.RefreshToken == null) + { + Logger.Warning("Refresh token is null, can't refresh the token!"); + return false; + } + + // It's possible that two concurrent calls will be made to refresh the token, in that case the last one + // will win. + var newToken = await flow.RefreshTokenAsync(userId, Token.RefreshToken, taskCancellationToken) + .ConfigureAwait(false); + + Logger.Info("Access token was refreshed"); + + if (newToken.RefreshToken == null) + { + newToken.RefreshToken = Token.RefreshToken; + } + + Token = newToken; + return true; + } + + public async Task HandleResponseAsync(HandleUnsuccessfulResponseArgs args) + { + // TODO(peleyal): check WWW-Authenticate header + if (args.Response.StatusCode == HttpStatusCode.Unauthorized) + { + return !Object.Equals(Token.AccessToken, flow.AccessMethod.GetAccessToken(args.Request)) + || await RefreshTokenAsync(args.CancellationToken).ConfigureAwait(false); + } + + return false; + } + + public void Initialize(ConfigurableHttpClient httpClient) + { + httpClient.MessageHandler.ExecuteInterceptors.Add(this); + httpClient.MessageHandler.UnsuccessfulResponseHandlers.Add(this); + } } } Index: Src/GoogleApis.Auth/OAuth2/GoogleAuthorizationCodeFlow.cs =================================================================== new file mode 100644 --- /dev/null +++ b/Src/GoogleApis.Auth/OAuth2/GoogleAuthorizationCodeFlow.cs @@ -0,0 +1,58 @@ +/* +Copyright 2013 Google Inc + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +using System; + +using Google.Apis.Auth.OAuth2.Requests; + +namespace Google.Apis.Auth.OAuth2 +{ + /// + /// Google specific authorization code flow which inherits from . + /// + public class GoogleAuthorizationCodeFlow : AuthorizationCodeFlow + { + /// Constructs a new Google authorization code flow. + public GoogleAuthorizationCodeFlow(AuthorizationCodeFlow.Initializer initializer) + : base(initializer) + { + } + + public override AuthorizationCodeRequestUrl CreateAuthorizationCodeRequest(string redirectUri) + { + return new GoogleAuthorizationCodeRequestUrl(new Uri(AuthorizationServerUrl)) + { + ClientId = ClientSecrets.ClientId, + Scope = string.Join(" ", Scopes), + RedirectUri = redirectUri + }; + } + + /// An initializer class for Google authorization code flow. + public new class Initializer : AuthorizationCodeFlow.Initializer + { + /// + /// Constructs a new initializer. Sets Authorization server URL to + /// , and Token server URL to + /// . + /// + public Initializer() + : base(GoogleAuthConsts.AuthorizationUrl, GoogleAuthConsts.TokenUrl) + { + } + } + } +} Index: Src/GoogleApis.Auth/OAuth2/IAuthorizationCodeFlow.cs =================================================================== --- a/Src/GoogleApis.Auth/OAuth2/IAuthorizationCodeFlow.cs +++ b/Src/GoogleApis.Auth/OAuth2/IAuthorizationCodeFlow.cs @@ -37,34 +37,35 @@ /// Gets the data store used to store the credentials. IDataStore DataStore { get; } - /// Loads the user's token using the flow's . + /// Asynchronously loads the user's token using the flow's . /// User identifier /// Cancellation token to cancel operation /// Token response - Task LoadToken(string userId, CancellationToken taskCancellationToken); + Task LoadTokenAsync(string userId, CancellationToken taskCancellationToken); - /// Deletes the user's token using the flow's . + /// Asynchronously deletes the user's token using the flow's . /// User identifier /// Cancellation token to cancel operation - Task DeleteToken(string userId, CancellationToken taskCancellationToken); + Task DeleteTokenAsync(string userId, CancellationToken taskCancellationToken); /// Creates an authorization code request with the specified redirect URI. AuthorizationCodeRequestUrl CreateAuthorizationCodeRequest(string redirectUri); - /// Exchanges code with a token. + /// Asynchronously exchanges code with a token. /// User identifier /// Authorization code received from the authorization server /// Redirect URI which is used in the token request /// Cancellation token to cancel operation /// Token response which contains the access token - Task ExchangeCodeForToken(string userId, string code, string redirectUri, + Task ExchangeCodeForTokenAsync(string userId, string code, string redirectUri, CancellationToken taskCancellationToken); - /// Refreshes an access token using a refresh token. + /// Asynchronously refreshes an access token using a refresh token. /// User identifier /// Refresh token which is used to get a new access token /// Cancellation token to cancel operation /// Token response which contains the access token and the input refresh token - Task RefreshToken(string userId, string refreshToken, CancellationToken taskCancellationToken); + Task RefreshTokenAsync(string userId, string refreshToken, + CancellationToken taskCancellationToken); } } Index: Src/GoogleApis.Auth/OAuth2/IAuthorizationCodeInstalledApp.cs =================================================================== --- a/Src/GoogleApis.Auth/OAuth2/IAuthorizationCodeInstalledApp.cs +++ b/Src/GoogleApis.Auth/OAuth2/IAuthorizationCodeInstalledApp.cs @@ -34,6 +34,6 @@ /// User identifier /// Cancellation token to cancel an operation /// The user's credential - Task Authorize(string userId, CancellationToken taskCancellationToken); + Task Authorize(string userId, CancellationToken taskCancellationToken); } } Index: Src/GoogleApis.Auth/OAuth2/Requests/GoogleAssertionTokenRequest.cs =================================================================== --- a/Src/GoogleApis.Auth/OAuth2/Requests/GoogleAssertionTokenRequest.cs +++ b/Src/GoogleApis.Auth/OAuth2/Requests/GoogleAssertionTokenRequest.cs @@ -33,7 +33,7 @@ /// public GoogleAssertionTokenRequest() { - GrantType = @"urn:ietf:params:oauth:grant-type:jwt-bearer"; + GrantType = "urn:ietf:params:oauth:grant-type:jwt-bearer"; } } } Index: Src/GoogleApis.Auth/OAuth2/Responses/TokenResponse.cs =================================================================== --- a/Src/GoogleApis.Auth/OAuth2/Responses/TokenResponse.cs +++ b/Src/GoogleApis.Auth/OAuth2/Responses/TokenResponse.cs @@ -65,29 +65,12 @@ /// public bool IsExpired(IClock clock) { - //TODO(peleyal): consider move it to an extension method - if (!ExpiresInSeconds.HasValue) + if (AccessToken == null || !ExpiresInSeconds.HasValue) { return true; } return Issued.AddSeconds(ExpiresInSeconds.Value + 60) <= clock.Now; } - - /// - /// Copies all properties from the other token, except the other's refresh token property in case it's null. - /// - internal void CopyFrom(TokenResponse other) - { - AccessToken = other.AccessToken; - TokenType = other.TokenType; - ExpiresInSeconds = other.ExpiresInSeconds; - if (other.RefreshToken != null) - { - RefreshToken = other.RefreshToken; - } - Scope = other.Scope; - Issued = other.Issued; - } } } \ No newline at end of file Index: Src/GoogleApis.Auth/OAuth2/TokenRequestExtenstions.cs =================================================================== new file mode 100644 --- /dev/null +++ b/Src/GoogleApis.Auth/OAuth2/TokenRequestExtenstions.cs @@ -0,0 +1,66 @@ +/* +Copyright 2013 Google Inc + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +using Google.Apis.Auth.OAuth2.Requests; +using Google.Apis.Auth.OAuth2.Responses; +using Google.Apis.Json; +using Google.Apis.Requests.Parameters; +using Google.Apis.Util; + +namespace Google.Apis.Auth.OAuth2 +{ + /// + /// Extension methods to . + /// + internal static class TokenRequestExtenstions + { + /// + /// Executes the token request in order to receive a + /// . In case the token server returns an + /// error, a is thrown. + /// + /// The token request. + /// The HTTP client used to create an HTTP request. + /// The token server URL. + /// Cancellation token to cancel operation. + /// The clock which is used to set the property. + /// Token response with the new access token. + public static async Task Execute(this TokenRequest request, HttpClient httpClient, + string tokenServerUrl, CancellationToken taskCancellationToken, IClock clock) + { + var httpRequest = new HttpRequestMessage(HttpMethod.Post, tokenServerUrl); + httpRequest.Content = ParameterUtils.CreateFormUrlEncodedContent(request); + + var response = await httpClient.SendAsync(httpRequest, taskCancellationToken).ConfigureAwait(false); + var content = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + + if (!response.IsSuccessStatusCode) + { + var error = NewtonsoftJsonSerializer.Instance.Deserialize(content); + throw new TokenResponseException(error); + } + + // Gets the token and sets its issued time. + var newToken = NewtonsoftJsonSerializer.Instance.Deserialize(content); + newToken.Issued = clock.Now; + return newToken; + } + } +} Index: Src/GoogleApis.Auth/OAuth2/UserCredential.cs =================================================================== new file mode 100644 --- /dev/null +++ b/Src/GoogleApis.Auth/OAuth2/UserCredential.cs @@ -0,0 +1,140 @@ +/* +Copyright 2013 Google Inc + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +using System; +using System.Net; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +using Google.Apis.Auth.OAuth2.Responses; +using Google.Apis.Http; +using Google.Apis.Logging; + +namespace Google.Apis.Auth.OAuth2 +{ + /// + /// OAuth 2.0 credential for accessing protected resources using an access token, as well as optionally refreshing + /// the access token when it expires using a refresh token. + /// + public class UserCredential : IHttpExecuteInterceptor, IHttpUnsuccessfulResponseHandler, + IConfigurableHttpClientInitializer + { + private static readonly ILogger Logger = ApplicationContext.Logger.ForType(); + + private TokenResponse token; + private object lockObject = new object(); + + public TokenResponse Token + { + get + { + lock (lockObject) + { + return token; + } + } + private set + { + lock (lockObject) + { + token = value; + } + } + } + + private readonly IAuthorizationCodeFlow flow; + private readonly string userId; + + /// Constructs a new credential instance. + /// Authorization code flow + /// User identifier + /// An initial token for the user + public UserCredential(IAuthorizationCodeFlow flow, string userId, TokenResponse token) + { + this.flow = flow; + this.userId = userId; + this.token = token; + } + + /// + /// Default implementation is to try to refresh the access token if there is no access token or if we are 1 + /// minute away from expiration. If token server is unavailable, it will try to use the access token even if + /// has expired. If successful, it will call . + /// + public async Task InterceptAsync(HttpRequestMessage request, CancellationToken taskCancellationToken) + { + if (Token.IsExpired(flow.Clock)) + { + Logger.Debug("Token has expired, trying to refresh it."); + if (!await RefreshTokenAsync(taskCancellationToken).ConfigureAwait(false)) + { + throw new InvalidOperationException("The access token has expired but we can't refresh it"); + } + } + + flow.AccessMethod.Intercept(request, Token.AccessToken); + } + + /// + /// Refreshes the token by calling to . Then it + /// updates the with the new token instance. + /// + /// Cancellation token to cancel an operation + /// true if the token was refreshed + private async Task RefreshTokenAsync(CancellationToken taskCancellationToken) + { + if (Token.RefreshToken == null) + { + Logger.Warning("Refresh token is null, can't refresh the token!"); + return false; + } + + // It's possible that two concurrent calls will be made to refresh the token, in that case the last one + // will win. + var newToken = await flow.RefreshTokenAsync(userId, Token.RefreshToken, taskCancellationToken) + .ConfigureAwait(false); + + Logger.Info("Access token was refreshed successfully"); + + if (newToken.RefreshToken == null) + { + newToken.RefreshToken = Token.RefreshToken; + } + + Token = newToken; + return true; + } + + public async Task HandleResponseAsync(HandleUnsuccessfulResponseArgs args) + { + // TODO(peleyal): check WWW-Authenticate header + if (args.Response.StatusCode == HttpStatusCode.Unauthorized) + { + return !Object.Equals(Token.AccessToken, flow.AccessMethod.GetAccessToken(args.Request)) + || await RefreshTokenAsync(args.CancellationToken).ConfigureAwait(false); + } + + return false; + } + + public void Initialize(ConfigurableHttpClient httpClient) + { + httpClient.MessageHandler.ExecuteInterceptors.Add(this); + httpClient.MessageHandler.UnsuccessfulResponseHandlers.Add(this); + } + } +} Index: Src/GoogleApis.Auth/Properties/AssemblyInfo.cs =================================================================== --- a/Src/GoogleApis.Auth/Properties/AssemblyInfo.cs +++ b/Src/GoogleApis.Auth/Properties/AssemblyInfo.cs @@ -1,4 +1,4 @@ -/* +/* Copyright 2013 Google Inc Licensed under the Apache License, Version 2.0 (the "License"); @@ -30,6 +30,7 @@ [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] [assembly: InternalsVisibleTo("Google.Apis.Auth.Tests")] +[assembly: InternalsVisibleTo("Google.Apis.Auth.PlatformServices")] // The assembly version has the format "{Major}.{Minor}.{Build}.{Revision}". Index: Src/GoogleApis/Apis/Util/IClock.cs =================================================================== --- a/Src/GoogleApis/Apis/Util/IClock.cs +++ b/Src/GoogleApis/Apis/Util/IClock.cs @@ -26,6 +26,12 @@ /// expressed as the local time. /// DateTime Now { get; } + + /// + /// Gets a object that is set to the current date and time on this computer, + /// expressed as UTC time. + /// + DateTime UtcNow { get; } } /// A default clock implementation that wraps the property. @@ -41,5 +47,10 @@ { get { return DateTime.Now; } } + + public DateTime UtcNow + { + get { return DateTime.UtcNow; } + } } } Index: Src/GoogleApis/Apis/Util/Store/IDataStore.cs =================================================================== --- a/Src/GoogleApis/Apis/Util/Store/IDataStore.cs +++ b/Src/GoogleApis/Apis/Util/Store/IDataStore.cs @@ -27,27 +27,27 @@ /// public interface IDataStore { - /// Stores the given value for the given key (replacing any existing value). + /// Asynchronously stores the given value for the given key (replacing any existing value). /// The type to store in the data store /// The key /// The value to store - Task Store(string key, T value); + Task StoreAsync(string key, T value); /// - /// Deletes the given key. The type is provided here as well because the "real" saved key should contain - /// type information as well, so the data store will be able to store the same key for different types. + /// Asynchronously deletes the given key. The type is provided here as well because the "real" saved key should + /// contain type information as well, so the data store will be able to store the same key for different types. /// /// The type to delete from the data store /// The key to delete - Task Delete(string key); + Task DeleteAsync(string key); - /// Returns the stored value for the given key or null if not found. + /// Asynchronously returns the stored value for the given key or null if not found. /// The type to retrieve from the data store /// The key to retrieve its value /// The stored object - Task Get(string key); + Task GetAsync(string key); - /// Clears all values in the data store. - Task Clear(); + /// Asynchronously clears all values in the data store. + Task ClearAsync(); } } \ No newline at end of file Index: Src/GoogleApis/Apis/Util/Utilities.cs =================================================================== --- a/Src/GoogleApis/Apis/Util/Utilities.cs +++ b/Src/GoogleApis/Apis/Util/Utilities.cs @@ -32,12 +32,14 @@ } /// Throws an if the object is null. - internal static void ThrowIfNull(this object obj, string paramName) + internal static T ThrowIfNull(this T obj, string paramName) { if (obj == null) { throw new ArgumentNullException(paramName); } + + return obj; } /// Index: Src/GoogleApis/Properties/AssemblyInfo.cs =================================================================== --- a/Src/GoogleApis/Properties/AssemblyInfo.cs +++ b/Src/GoogleApis/Properties/AssemblyInfo.cs @@ -30,6 +30,7 @@ [assembly: AssemblyCulture("")] [assembly: InternalsVisibleTo("Google.Apis.Tests")] [assembly: InternalsVisibleTo("Google.Apis.Auth")] +[assembly: InternalsVisibleTo("Google.Apis.Auth.PlatformServices")] [assembly: InternalsVisibleTo("Google.Apis.Authentication.OAuth2")] // The assembly version has the format "{Major}.{Minor}.{Build}.{Revision}".