Index: GoogleApisClient.sln =================================================================== --- a/GoogleApisClient.sln +++ b/GoogleApisClient.sln @@ -24,6 +24,8 @@ EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GoogleApis.Auth.Tests", "Src\GoogleApis.Auth.Tests\GoogleApis.Auth.Tests.csproj", "{548D6C9B-A97B-4316-91AC-9AAD35202884}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GoogleApis.Auth.Mvc4", "Src\GoogleApis.Auth.Mvc4\GoogleApis.Auth.Mvc4.csproj", "{B700243A-2FAC-454B-8858-47635BFA71C6}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -114,6 +116,16 @@ {548D6C9B-A97B-4316-91AC-9AAD35202884}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {548D6C9B-A97B-4316-91AC-9AAD35202884}.Release|Mixed Platforms.Build.0 = Release|Any CPU {548D6C9B-A97B-4316-91AC-9AAD35202884}.Release|Win32.ActiveCfg = Release|Any CPU + {B700243A-2FAC-454B-8858-47635BFA71C6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B700243A-2FAC-454B-8858-47635BFA71C6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B700243A-2FAC-454B-8858-47635BFA71C6}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {B700243A-2FAC-454B-8858-47635BFA71C6}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {B700243A-2FAC-454B-8858-47635BFA71C6}.Debug|Win32.ActiveCfg = Debug|Any CPU + {B700243A-2FAC-454B-8858-47635BFA71C6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B700243A-2FAC-454B-8858-47635BFA71C6}.Release|Any CPU.Build.0 = Release|Any CPU + {B700243A-2FAC-454B-8858-47635BFA71C6}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {B700243A-2FAC-454B-8858-47635BFA71C6}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {B700243A-2FAC-454B-8858-47635BFA71C6}.Release|Win32.ActiveCfg = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE 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 @@ -78,6 +78,7 @@ + Index: Src/GoogleApis.Auth.DotNet4/OAuth2/GoogleWebAuthenticationBroker.cs =================================================================== --- a/Src/GoogleApis.Auth.DotNet4/OAuth2/GoogleWebAuthenticationBroker.cs +++ b/Src/GoogleApis.Auth.DotNet4/OAuth2/GoogleWebAuthenticationBroker.cs @@ -19,6 +19,7 @@ using System.Threading; using System.Threading.Tasks; +using Google.Apis.Auth.OAuth2.Flows; using Google.Apis.Util.Store; namespace Google.Apis.Auth.OAuth2 @@ -97,7 +98,7 @@ var flow = new GoogleAuthorizationCodeFlow(initializer); // Create authorization code installed app instance and authorize the user. - return await new AuthorizationCodeInstalledApp(flow, new LocalServerCodeReceiver()).Authorize + return await new AuthorizationCodeInstalledApp(flow, new LocalServerCodeReceiver()).AuthorizeAsync (user, taskCancellationToken); } } Index: Src/GoogleApis.Auth.DotNet4/OAuth2/ServiceAccountCredential.cs =================================================================== new file mode 100644 --- /dev/null +++ b/Src/GoogleApis.Auth.DotNet4/OAuth2/ServiceAccountCredential.cs @@ -0,0 +1,338 @@ +/* +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.Net; +using System.Net.Http; +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; +using System.Text; +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.Json; +using Google.Apis.Logging; +using Google.Apis.Util; + +namespace Google.Apis.Auth.OAuth2 +{ + /// + /// Google OAuth 2.0 credential for accessing protected resources using an access token. The Google OAuth 2.0 + /// Authorization Server supports server-to-server interactions such as those between a web application and Google + /// Cloud Storage. The requesting application has to prove its own identity to gain access to an API, and an + /// end-user doesn't have to be involved. + /// + /// Take a look in https://developers.google.com/accounts/docs/OAuth2ServiceAccount for more details. + /// + /// + public class ServiceAccountCredential : IHttpExecuteInterceptor, IHttpUnsuccessfulResponseHandler, + IConfigurableHttpClientInitializer + { + private static readonly ILogger Logger = ApplicationContext.Logger.ForType(); + + /// An initializer class for the service account credential. + public class Initializer + { + /// Gets the service account ID (typically an e-mail address). + public string Id { get; private set; } + + /// Gets the token server URL. + public string TokenServerUrl { get; private set; } + + /// + /// Gets or sets the email address of the user the application is trying to impersonate in the service + /// account flow or null. + /// + public string User { get; set; } + + /// Gets the scopes which indicate API access your application is requesting. + public IEnumerable Scopes { 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; } + + /// + /// Gets or sets the key which is used to sign the request, as specified in + /// https://developers.google.com/accounts/docs/OAuth2ServiceAccount#computingsignature. + /// + public RSACryptoServiceProvider Key { get; set; } + + /// + /// Gets or sets the method for presenting the access token to the resource server. + /// The default value is . + /// + public IAccessMethod AccessMethod { get; set; } + + /// + /// Gets or sets the factory for creating a 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 the user to + /// configure the in an + /// to set a specific back-off + /// implementation (using ). + /// + public ExponentialBackOffPolicy DefaultExponentialBackOffPolicy { get; set; } + + /// Constructs a new initializer using the given id. + public Initializer(string id) + : this(id, GoogleAuthConsts.TokenUrl) { } + + /// Constructs a new initializer using the given id and the token server URL. + public Initializer(string id, string tokenServerUrl) + { + Id = id; + TokenServerUrl = tokenServerUrl; + + AccessMethod = new BearerToken.AuthorizationHeaderAccessMethod(); + Clock = SystemClock.Default; + DefaultExponentialBackOffPolicy = ExponentialBackOffPolicy.UnsuccessfulResponse503; + Scopes = new List(); + } + + /// Extracts a from the given certificate. + public Initializer FromCertificate(X509Certificate2 certificate) + { + // Workaround to correctly cast the private key as a RSACryptoServiceProvider type 24. + RSACryptoServiceProvider rsa = (RSACryptoServiceProvider)certificate.PrivateKey; + byte[] privateKeyBlob = rsa.ExportCspBlob(true); + Key = new RSACryptoServiceProvider(); + Key.ImportCspBlob(privateKeyBlob); + return this; + } + } + + #region Readonly fields + + private readonly string id; + private readonly string tokenServerUrl; + private readonly string user; + private readonly IEnumerable scopes; + private readonly IClock clock; + private readonly IAccessMethod accessMethod; + private readonly ConfigurableHttpClient httpClient; + private readonly RSACryptoServiceProvider key; + + #endregion + + /// Gets the service account ID (typically an e-mail address). + public string Id { get { return id; } } + + /// Gets the token server URL. + public string TokenServerUrl { get { return tokenServerUrl; } } + + /// + /// Gets the email address of the user the application is trying to impersonate in the service account flow + /// or null. + /// + public string User { get { return user; } } + + /// Gets the service account scopes. + public IEnumerable Scopes { get { return scopes; } } + + /// + /// Gets the clock. The clock is used to determine if the token has expired, if so we will try to refresh it. + /// + public IClock Clock { get { return clock; } } + + /// Gets the method for presenting the access token to the resource server. + public IAccessMethod AccessMethod { get { return accessMethod; } } + + /// Gets the HTTP client used to make authentication requests to the server. + public ConfigurableHttpClient HttpClient { get { return httpClient; } } + + /// + /// Gets the key which is used to sign the request, as specified in + /// https://developers.google.com/accounts/docs/OAuth2ServiceAccount#computingsignature. + /// + public RSACryptoServiceProvider Key { get { return key; } } + + private TokenResponse token; + private object lockObject = new object(); + + /// Gets the token response which contains the access token. + public TokenResponse Token + { + get + { + lock (lockObject) + { + return token; + } + } + private set + { + lock (lockObject) + { + token = value; + } + } + } + + /// Constructs a new service account credential using the given initializer. + /// + public ServiceAccountCredential(Initializer initializer) + { + id = initializer.Id.ThrowIfNullOrEmpty("initializer.Id"); + user = initializer.User; + scopes = initializer.Scopes; + tokenServerUrl = initializer.TokenServerUrl; + accessMethod = initializer.AccessMethod.ThrowIfNull("initializer.AccessMethod"); + clock = initializer.Clock.ThrowIfNull("initializer.Clock"); + + // 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); + key = initializer.Key.ThrowIfNull("initializer.Key"); + } + + public void Initialize(ConfigurableHttpClient httpClient) + { + httpClient.MessageHandler.ExecuteInterceptors.Add(this); + httpClient.MessageHandler.UnsuccessfulResponseHandlers.Add(this); + } + + public async Task InterceptAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + if (Token == null || Token.IsExpired(Clock)) + { + Logger.Debug("Token has expired, trying to get a new one."); + if (!await RequestAccessToken(cancellationToken)) + { + throw new InvalidOperationException("The access token has expired but we can't refresh it"); + } + Logger.Info("New access token was received successfully"); + } + + AccessMethod.Intercept(request, Token.AccessToken); + } + + public async Task HandleResponseAsync(HandleUnsuccessfulResponseArgs args) + { + // TODO(peleyal): check WWW-Authenticate header + if (args.Response.StatusCode == HttpStatusCode.Unauthorized) + { + return !Object.Equals(Token.AccessToken, AccessMethod.GetAccessToken(args.Request)) + || await RequestAccessToken(args.CancellationToken).ConfigureAwait(false); + } + + return false; + } + + /// + /// Requests a new token as specified in + /// https://developers.google.com/accounts/docs/OAuth2ServiceAccount#makingrequest. + /// + /// Cancellation token to cancel operation + /// true if a new token was received successfully + private async Task RequestAccessToken(CancellationToken taskCancellationToken) + { + string serializedHeader = CreateSerializedHeader(); + string serializedPayload = GetSerializedPayload(); + + StringBuilder assertion = new StringBuilder(); + assertion.Append(UrlSafeBase64Encode(serializedHeader)) + .Append(".") + .Append(UrlSafeBase64Encode(serializedPayload)); + + // Sign the header and the payload. + var signature = UrlSafeBase64Encode(key.SignData(Encoding.ASCII.GetBytes(assertion.ToString()), "SHA256")); + assertion.Append(".").Append(signature); + + // Create the request. + var request = new GoogleAssertionTokenRequest() + { + Assertion = assertion.ToString() + }; + + Logger.Debug("Request a new access token. Assertion data is: " + request.Assertion); + + var newToken = await request.ExecuteAsync(httpClient, tokenServerUrl, taskCancellationToken, Clock); + Token = newToken; + return true; + } + + /// + /// Creates a serialized header as specified in + /// https://developers.google.com/accounts/docs/OAuth2ServiceAccount#formingheader. + /// + private static string CreateSerializedHeader() + { + var header = new GoogleJsonWebSignature.Header() + { + Algorithm = "RS256", + Type = "JWT" + }; + + return NewtonsoftJsonSerializer.Instance.Serialize(header); + } + + /// + /// Creates a serialized claim set as specified in + /// https://developers.google.com/accounts/docs/OAuth2ServiceAccount#formingclaimset + /// + private string GetSerializedPayload() + { + var issued = (int)(Clock.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc)).TotalSeconds; + var payload = new GoogleJsonWebSignature.Payload() + { + Issuer = Id, + Audience = TokenServerUrl, + IssuedAtTimeSeconds = issued, + ExpirationTimeSeconds = issued + 3600, + Subject = User, + Scope = String.Join(" ", Scopes) + }; + + return NewtonsoftJsonSerializer.Instance.Serialize(payload); + } + + /// Encodes the provided UTF8 string into an URL safe base64 string. + /// Value to encode + /// The URL safe base64 string + private string UrlSafeBase64Encode(string value) + { + return UrlSafeBase64Encode(Encoding.UTF8.GetBytes(value)); + } + + /// Encodes the byte array into an URL safe base64 string. + /// Byte array to encode + /// The URL safe base64 string + private string UrlSafeBase64Encode(byte[] bytes) + { + return Convert.ToBase64String(bytes).Replace("=", String.Empty).Replace('+', '-').Replace('/', '_'); + } + } +} Index: Src/GoogleApis.Auth.DotNet4/Properties/AssemblyInfo.cs =================================================================== --- a/Src/GoogleApis.Auth.DotNet4/Properties/AssemblyInfo.cs +++ b/Src/GoogleApis.Auth.DotNet4/Properties/AssemblyInfo.cs @@ -1,4 +1,4 @@ -/* +/* Copyright 2013 Google Inc Licensed under the Apache License, Version 2.0 (the "License"); Index: Src/GoogleApis.Auth.Mvc4/GoogleApis.Auth.Mvc4.csproj =================================================================== new file mode 100644 --- /dev/null +++ b/Src/GoogleApis.Auth.Mvc4/GoogleApis.Auth.Mvc4.csproj @@ -0,0 +1,144 @@ + + + + + Debug + AnyCPU + {B700243A-2FAC-454B-8858-47635BFA71C6} + Library + Properties + Google.Apis.Auth + Google.Apis.Auth.Mvc4 + v4.0 + 512 + ..\..\ + true + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + Google.Apis.Auth.Mvc4.xml + + + + ..\..\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 + + + True + ..\..\packages\Microsoft.Web.Infrastructure.1.0.0.0\lib\net40\Microsoft.Web.Infrastructure.dll + + + False + ..\..\packages\Newtonsoft.Json.4.5.11\lib\net40\Newtonsoft.Json.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.AspNet.WebApi.Client.4.0.30506.0\lib\net40\System.Net.Http.Formatting.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 + + + + True + ..\..\packages\Microsoft.AspNet.WebPages.2.0.30506.0\lib\net40\System.Web.Helpers.dll + + + True + ..\..\packages\Microsoft.AspNet.Mvc.4.0.30506.0\lib\net40\System.Web.Mvc.dll + + + True + ..\..\packages\Microsoft.AspNet.Razor.2.0.30506.0\lib\net40\System.Web.Razor.dll + + + True + ..\..\packages\Microsoft.AspNet.WebPages.2.0.30506.0\lib\net40\System.Web.WebPages.dll + + + True + ..\..\packages\Microsoft.AspNet.WebPages.2.0.30506.0\lib\net40\System.Web.WebPages.Deployment.dll + + + True + ..\..\packages\Microsoft.AspNet.WebPages.2.0.30506.0\lib\net40\System.Web.WebPages.Razor.dll + + + + + + + + + + + + + + + + + + + + {0aaaf32e-2bf0-49c5-bc2d-90874cfb5510} + GoogleApis.Auth + + + {826cf988-eee8-4b75-8f53-b7e851a17baa} + GoogleApis + + + + + + + + + + + \ No newline at end of file Index: Src/GoogleApis.Auth.Mvc4/OAuth2/Mvc/AuthorizationCodeMvcApp.cs =================================================================== new file mode 100644 --- /dev/null +++ b/Src/GoogleApis.Auth.Mvc4/OAuth2/Mvc/AuthorizationCodeMvcApp.cs @@ -0,0 +1,67 @@ +/* +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 System.Web.Mvc; + +using Google.Apis.Auth.OAuth2.Web; + +namespace Google.Apis.Auth.OAuth2.Mvc +{ + /// + /// Thread-safe OAuth 2.0 authorization code flow for a MVC web application that persists end-user credentials. + /// + public class AuthorizationCodeMvcApp : AuthorizationCodeWebApp + { + // TODO(peleyal): we should also follow the MVC framework Authorize attribute + + private readonly Controller controller; + private readonly FlowMetadata flowData; + + /// Gets the controller which is the owner of this authorization code MVC app instance. + public Controller Controller { get { return controller; } } + + /// Gets the object. + public FlowMetadata FlowData { get { return flowData; } } + + /// Constructs a new authorization code MVC app using the given controller and flow data. + public AuthorizationCodeMvcApp(Controller controller, FlowMetadata flowData) + : base( + flowData.Flow, + new Uri(controller.Request.Url.GetLeftPart(UriPartial.Authority) + flowData.AuthCallback).ToString(), + controller.Request.Url.ToString()) + { + this.controller = controller; + this.flowData = flowData; + } + + /// + /// Asynchronously authorizes the installed application to access user's protected data. It gets the user + /// identifier by calling to and then calls to + /// . + /// + /// Cancellation token to cancel an operation + /// + /// Auth result object which contains the user's credential or redirect URI for the authorization server + /// + public Task AuthorizeAsync(CancellationToken taskCancellationToken) + { + return base.AuthorizeAsync(FlowData.GetUserId(Controller), taskCancellationToken); + } + } +} Index: Src/GoogleApis.Auth.Mvc4/OAuth2/Mvc/Controllers/AuthCallbackController.cs =================================================================== new file mode 100644 --- /dev/null +++ b/Src/GoogleApis.Auth.Mvc4/OAuth2/Mvc/Controllers/AuthCallbackController.cs @@ -0,0 +1,103 @@ +/* +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 System.Web.Mvc; + +using Google.Apis.Auth.OAuth2; +using Google.Apis.Auth.OAuth2.Flows; +using Google.Apis.Auth.OAuth2.Mvc.Filters; +using Google.Apis.Auth.OAuth2.Responses; +using Google.Apis.Auth.OAuth2.Web; +using Google.Apis.Logging; + +namespace Google.Apis.Auth.OAuth2.Mvc.Controllers +{ + /// + /// Auth callback to process the authorization code or error response from the authorization redirect page. + /// + [AuthorizationCodeActionFilter] + public abstract class AuthCallbackController : Controller + { + protected static readonly ILogger Logger = ApplicationContext.Logger.ForType(); + + /// Gets the authorization code flow. + protected IAuthorizationCodeFlow Flow { get { return FlowData.Flow; } } + + /// + /// Gets the user identifier. Potential logic is to use session variables to retrieve that information. + /// + protected string UserId { get { return FlowData.GetUserId(this); } } + + #region Abstract and Virtual Members + + /// Gets the flow data which contains + protected abstract FlowMetadata FlowData { get; } + + /// + /// A callback which gets the error when this controller didn't receive an authorization code. The default + /// implementation throws a . + /// + protected virtual ActionResult OnTokenError(TokenErrorResponse errorResponse) + { + throw new TokenResponseException(errorResponse); + } + + /// + /// The authorization callback which receives an authorization code which contains an error or a code. + /// If a code is available the method exchange the coed with an access token and redirect back to the original + /// page which initialized the auth process (using the state parameter). + /// + /// The current timeout is set to 10 seconds. You can change the default behavior by setting + /// with a different value on your controller. + /// + /// + /// Authorization code response which contains the code or an error. + /// Cancellation token to cancel operation. + /// + /// Redirect action to the state parameter or in case of an error. + /// + [AsyncTimeout(10000)] + public async virtual Task IndexAsync(AuthorizationCodeResponseUrl authorizationCode, + CancellationToken taskCancellationToken) + { + if (string.IsNullOrEmpty(authorizationCode.Code)) + { + var errorResponse = new TokenErrorResponse(authorizationCode); + Logger.Info("Received an error. The response is: {0}", errorResponse); + + return OnTokenError(errorResponse); + } + + Logger.Debug("Received \"{0}\" code", authorizationCode.Code); + + var returnUrl = Request.Url.ToString(); + returnUrl = returnUrl.Substring(0, returnUrl.IndexOf("?")); + + var token = await Flow.ExchangeCodeForTokenAsync(UserId, authorizationCode.Code, returnUrl, + taskCancellationToken); + + // Extract the right state. + var oauthState = await AuthWebUtility.ExtracRedirectFromState(Flow.DataStore, UserId, authorizationCode.State); + + return new RedirectResult(oauthState); + } + + #endregion + } +} Index: Src/GoogleApis.Auth.Mvc4/OAuth2/Mvc/Filters/AuthActionFilter.cs =================================================================== new file mode 100644 --- /dev/null +++ b/Src/GoogleApis.Auth.Mvc4/OAuth2/Mvc/Filters/AuthActionFilter.cs @@ -0,0 +1,55 @@ +/* +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.Web; +using System.Web.Mvc; + +using Google.Apis.Auth.OAuth2.Responses; + +namespace Google.Apis.Auth.OAuth2.Mvc.Filters +{ + /// + /// An action filter which parses the query parameters into . + /// + public class AuthorizationCodeActionFilter : ActionFilterAttribute, IActionFilter + { + /// + /// Parses the request into + /// + public override void OnActionExecuting(ActionExecutingContext actionContext) + { + (actionContext.ActionParameters["authorizationCode"] as AuthorizationCodeResponseUrl) + .ParseRequest(actionContext.RequestContext.HttpContext.Request); + + base.OnActionExecuting(actionContext); + } + } + + /// Auth extensions methods. + public static class AuthExtensions + { + /// Parses the HTTP request query parameters into the Authorization code response. + internal static void ParseRequest(this AuthorizationCodeResponseUrl authorizationCode, HttpRequestBase request) + { + var queryDic = HttpUtility.ParseQueryString(request.Url.Query); + authorizationCode.Code = queryDic["code"]; + authorizationCode.Error = queryDic["error"]; + authorizationCode.ErrorDescription = queryDic["error_description"]; + authorizationCode.ErrorUri = queryDic["error_uri"]; + authorizationCode.State = queryDic["state"]; + } + } +} \ No newline at end of file Index: Src/GoogleApis.Auth.Mvc4/OAuth2/Mvc/FlowMetadata.cs =================================================================== new file mode 100644 --- /dev/null +++ b/Src/GoogleApis.Auth.Mvc4/OAuth2/Mvc/FlowMetadata.cs @@ -0,0 +1,52 @@ +/* +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.Web.Mvc; + +using Google.Apis.Auth.OAuth2.Flows; + +namespace Google.Apis.Auth.OAuth2.Mvc +{ + /// + /// Flow metadata abstract class which contains the reference to the and + /// method to get the user identifier. + /// + public abstract class FlowMetadata + { + /// + /// Gets the user identifier. + /// + /// An example implementation may get the user identifier by retrieving the + /// (from ). + /// + /// + /// The controller + /// User identifier + public abstract string GetUserId(Controller controller); + + /// Gets the authorization code flow. + public abstract IAuthorizationCodeFlow Flow { get; } + + /// + /// Gets the authorization application's call back. That relative URL will handle the authorization code + /// response. + /// + public virtual string AuthCallback + { + get { return @"/AuthCallback/IndexAsync"; } + } + } +} Index: Src/GoogleApis.Auth.Mvc4/Properties/AssemblyInfo.cs =================================================================== new file mode 100644 --- /dev/null +++ b/Src/GoogleApis.Auth.Mvc4/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; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("Google.Apis.Auth.Mvc4")] +[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.Mvc4/packages.config =================================================================== new file mode 100644 --- /dev/null +++ b/Src/GoogleApis.Auth.Mvc4/packages.config @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file Index: Src/GoogleApis.Auth.Tests/GoogleApis.Auth.Tests.csproj =================================================================== --- a/Src/GoogleApis.Auth.Tests/GoogleApis.Auth.Tests.csproj +++ b/Src/GoogleApis.Auth.Tests/GoogleApis.Auth.Tests.csproj @@ -80,7 +80,7 @@ - + Index: Src/GoogleApis.Auth.Tests/OAuth2/Flows/AuthorizationCodeFlowTests.cs =================================================================== rename from Src/GoogleApis.Auth.Tests/OAuth2/AuthorizationCodeFlowTests.cs rename to Src/GoogleApis.Auth.Tests/OAuth2/Flows/AuthorizationCodeFlowTests.cs --- a/Src/GoogleApis.Auth.Tests/OAuth2/AuthorizationCodeFlowTests.cs +++ b/Src/GoogleApis.Auth.Tests/OAuth2/Flows/AuthorizationCodeFlowTests.cs @@ -34,7 +34,7 @@ using Google.Apis.Util; using Google.Apis.Util.Store; -namespace Google.Apis.Auth.OAuth2 +namespace Google.Apis.Auth.OAuth2.Flows { /// Tests for . [TestFixture] Index: Src/GoogleApis.Auth/GoogleApis.Auth.csproj =================================================================== --- a/Src/GoogleApis.Auth/GoogleApis.Auth.csproj +++ b/Src/GoogleApis.Auth/GoogleApis.Auth.csproj @@ -38,16 +38,17 @@ - + - + + - + - + - + Index: Src/GoogleApis.Auth/OAuth2/AuthorizationCodeInstalledApp.cs =================================================================== --- a/Src/GoogleApis.Auth/OAuth2/AuthorizationCodeInstalledApp.cs +++ b/Src/GoogleApis.Auth/OAuth2/AuthorizationCodeInstalledApp.cs @@ -17,6 +17,7 @@ using System.Threading; using System.Threading.Tasks; +using Google.Apis.Auth.OAuth2.Flows; using Google.Apis.Auth.OAuth2.Responses; using Google.Apis.Auth.OAuth2.Requests; using Google.Apis.Logging; @@ -56,7 +57,7 @@ get { return codeReceiver; } } - public async Task Authorize(string userId, CancellationToken taskCancellationToken) + public async Task AuthorizeAsync(string userId, CancellationToken taskCancellationToken) { // Try to load a token from the data store. var token = await Flow.LoadTokenAsync(userId, taskCancellationToken).ConfigureAwait(false); Index: Src/GoogleApis.Auth/OAuth2/Credential.cs =================================================================== deleted file mode 100644 --- a/Src/GoogleApis.Auth/OAuth2/Credential.cs +++ /dev/null @@ -1,139 +0,0 @@ -/* -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 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/Flows/AuthorizationCodeFlow.cs =================================================================== rename from Src/GoogleApis.Auth/OAuth2/AuthorizationCodeFlow.cs rename to Src/GoogleApis.Auth/OAuth2/Flows/AuthorizationCodeFlow.cs --- a/Src/GoogleApis.Auth/OAuth2/AuthorizationCodeFlow.cs +++ b/Src/GoogleApis.Auth/OAuth2/Flows/AuthorizationCodeFlow.cs @@ -29,7 +29,7 @@ using Google.Apis.Util.Store; using Google.Apis.Testing; -namespace Google.Apis.Auth.OAuth2 +namespace Google.Apis.Auth.OAuth2.Flows { /// /// Thread-safe OAuth 2.0 authorization code flow that manages and persists end-user credentials. @@ -290,7 +290,8 @@ TokenResponseException tokenException = null; try { - var tokenResponse = await request.Execute(httpClient, TokenServerUrl, taskCancellationToken, Clock); + var tokenResponse = await request.ExecuteAsync + (httpClient, TokenServerUrl, taskCancellationToken, Clock).ConfigureAwait(false); return tokenResponse; } catch (TokenResponseException ex) @@ -299,7 +300,7 @@ // the data store. tokenException = ex; } - await DeleteTokenAsync(userId, taskCancellationToken); + await DeleteTokenAsync(userId, taskCancellationToken).ConfigureAwait(false); throw tokenException; } Index: Src/GoogleApis.Auth/OAuth2/Flows/GoogleAuthorizationCodeFlow.cs =================================================================== rename from Src/GoogleApis.Auth/OAuth2/GoogleAuthorizationCodeFlow.cs rename to Src/GoogleApis.Auth/OAuth2/Flows/GoogleAuthorizationCodeFlow.cs --- a/Src/GoogleApis.Auth/OAuth2/GoogleAuthorizationCodeFlow.cs +++ b/Src/GoogleApis.Auth/OAuth2/Flows/GoogleAuthorizationCodeFlow.cs @@ -18,7 +18,7 @@ using Google.Apis.Auth.OAuth2.Requests; -namespace Google.Apis.Auth.OAuth2 +namespace Google.Apis.Auth.OAuth2.Flows { /// /// Google specific authorization code flow which inherits from . Index: Src/GoogleApis.Auth/OAuth2/Flows/IAuthorizationCodeFlow.cs =================================================================== rename from Src/GoogleApis.Auth/OAuth2/IAuthorizationCodeFlow.cs rename to Src/GoogleApis.Auth/OAuth2/Flows/IAuthorizationCodeFlow.cs --- a/Src/GoogleApis.Auth/OAuth2/IAuthorizationCodeFlow.cs +++ b/Src/GoogleApis.Auth/OAuth2/Flows/IAuthorizationCodeFlow.cs @@ -23,7 +23,7 @@ using Google.Apis.Util; using Google.Apis.Util.Store; -namespace Google.Apis.Auth.OAuth2 +namespace Google.Apis.Auth.OAuth2.Flows { /// OAuth 2.0 authorization code flow that manages and persists end-user credentials. public interface IAuthorizationCodeFlow : IDisposable Index: Src/GoogleApis.Auth/OAuth2/IAuthorizationCodeInstalledApp.cs =================================================================== --- a/Src/GoogleApis.Auth/OAuth2/IAuthorizationCodeInstalledApp.cs +++ b/Src/GoogleApis.Auth/OAuth2/IAuthorizationCodeInstalledApp.cs @@ -17,6 +17,8 @@ using System.Threading; using System.Threading.Tasks; +using Google.Apis.Auth.OAuth2.Flows; + namespace Google.Apis.Auth.OAuth2 { /// @@ -34,6 +36,6 @@ /// User identifier /// Cancellation token to cancel an operation /// The user's credential - Task Authorize(string userId, CancellationToken taskCancellationToken); + Task AuthorizeAsync(string userId, CancellationToken taskCancellationToken); } } Index: Src/GoogleApis.Auth/OAuth2/Requests/TokenRequestExtenstions.cs =================================================================== rename from Src/GoogleApis.Auth/OAuth2/TokenRequestExtenstions.cs rename to Src/GoogleApis.Auth/OAuth2/Requests/TokenRequestExtenstions.cs --- a/Src/GoogleApis.Auth/OAuth2/TokenRequestExtenstions.cs +++ b/Src/GoogleApis.Auth/OAuth2/Requests/TokenRequestExtenstions.cs @@ -18,13 +18,12 @@ 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 +namespace Google.Apis.Auth.OAuth2.Requests { /// /// Extension methods to . @@ -42,7 +41,7 @@ /// 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, + public static async Task ExecuteAsync(this TokenRequest request, HttpClient httpClient, string tokenServerUrl, CancellationToken taskCancellationToken, IClock clock) { var httpRequest = new HttpRequestMessage(HttpMethod.Post, tokenServerUrl); Index: Src/GoogleApis.Auth/OAuth2/UserCredential.cs =================================================================== --- a/Src/GoogleApis.Auth/OAuth2/UserCredential.cs +++ b/Src/GoogleApis.Auth/OAuth2/UserCredential.cs @@ -20,6 +20,7 @@ using System.Threading; using System.Threading.Tasks; +using Google.Apis.Auth.OAuth2.Flows; using Google.Apis.Auth.OAuth2.Responses; using Google.Apis.Http; using Google.Apis.Logging; Index: Src/GoogleApis.Auth/OAuth2/Web/AuthWebUtility.cs =================================================================== new file mode 100644 --- /dev/null +++ b/Src/GoogleApis.Auth/OAuth2/Web/AuthWebUtility.cs @@ -0,0 +1,62 @@ +/* +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.Tasks; + +using Google.Apis.Auth.OAuth2.Responses; +using Google.Apis.Util.Store; + +namespace Google.Apis.Auth.OAuth2.Web +{ + /// Auth Utility methods for web development. + public class AuthWebUtility + { + /// Extracts the redirect URI from the state OAuth2 parameter. + /// + /// If the data store is not null, this method verifies that the state parameter which was returned + /// from the authorization server is the same as the one we set before redirecting to the authorization server. + /// + /// The data store which contains the original state parameter. + /// User identifier. + /// + /// The authorization state parameter which we got back from the authorization server. + /// + /// Redirect URI to the address which initializes the authorization code flow. + public static async Task ExtracRedirectFromState(IDataStore dataStore, string userId, string state) + { + var oauthState = state; + if (dataStore != null) + { + var userKey = AuthorizationCodeWebApp.StateKey + userId; + var expectedState = await dataStore.GetAsync(userKey); + + // Verify that the stored state is equal to the one we got back from the authorization server. + if (!Object.Equals(oauthState, expectedState)) + { + throw new TokenResponseException(new TokenErrorResponse + { + Error = "State is invalid" + }); + } + await dataStore.DeleteAsync(userKey); + oauthState = oauthState.Substring(0, oauthState.Length - AuthorizationCodeWebApp.StateRandomLength); + } + + return oauthState; + } + } +} Index: Src/GoogleApis.Auth/OAuth2/Web/AuthorizationCodeWebApp.cs =================================================================== rename from Src/GoogleApis.Auth/OAuth2/AuthorizationCodeWebApp.cs rename to Src/GoogleApis.Auth/OAuth2/Web/AuthorizationCodeWebApp.cs --- a/Src/GoogleApis.Auth/OAuth2/AuthorizationCodeWebApp.cs +++ b/Src/GoogleApis.Auth/OAuth2/Web/AuthorizationCodeWebApp.cs @@ -18,9 +18,10 @@ using System.Threading; using System.Threading.Tasks; +using Google.Apis.Auth.OAuth2.Flows; using Google.Apis.Auth.OAuth2.Requests; -namespace Google.Apis.Auth.OAuth2 +namespace Google.Apis.Auth.OAuth2.Web { /// /// Thread safe OAuth 2.0 authorization code flow for a web application that persists end-user credentials. @@ -82,19 +83,19 @@ /// 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? + // TODO(peleyal): 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. + /// Asynchronously 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) + public async Task AuthorizeAsync(string userId, CancellationToken taskCancellationToken) { // Try to load a token from the data store. var token = await Flow.LoadTokenAsync(userId, taskCancellationToken).ConfigureAwait(false); Index: Src/GoogleApis.DotNet4/Apis/Util/Store/FileDataStore.cs =================================================================== --- a/Src/GoogleApis.DotNet4/Apis/Util/Store/FileDataStore.cs +++ b/Src/GoogleApis.DotNet4/Apis/Util/Store/FileDataStore.cs @@ -48,7 +48,7 @@ } /// - /// Stores the given value for the given key. It creates a new file (named ) in + /// Stores the given value for the given key. It creates a new file (named ) in /// . /// /// The type to store in the data store @@ -62,13 +62,13 @@ } var serialized = NewtonsoftJsonSerializer.Instance.Serialize(value); - var filePath = Path.Combine(folderPath, GetStoredKey(key, typeof(T))); + var filePath = Path.Combine(folderPath, GenerateStoredKey(key, typeof(T))); File.WriteAllText(filePath, serialized); return TaskEx.Delay(0); } /// - /// Deletes the given key. It deletes the named file in . + /// Deletes the given key. It deletes the named file in . /// /// The key to delete from the data store public Task DeleteAsync(string key) @@ -78,7 +78,7 @@ throw new ArgumentException("Key MUST have a value"); } - var filePath = Path.Combine(folderPath, GetStoredKey(key, typeof(T))); + var filePath = Path.Combine(folderPath, GenerateStoredKey(key, typeof(T))); if (File.Exists(filePath)) { File.Delete(filePath); @@ -87,7 +87,7 @@ } /// - /// Returns the stored value for the given key or null if the matching file ( + /// Returns the stored value for the given key or null if the matching file ( /// in doesn't exist. /// /// The type to retrieve @@ -101,7 +101,7 @@ } TaskCompletionSource tcs = new TaskCompletionSource(); - var filePath = Path.Combine(folderPath, GetStoredKey(key, typeof(T))); + var filePath = Path.Combine(folderPath, GenerateStoredKey(key, typeof(T))); if (File.Exists(filePath)) { try @@ -138,7 +138,7 @@ /// Creates a unique stored key based on the key and the class type. /// The object key /// The type to store or retrieve - public static string GetStoredKey(string key, Type t) + public static string GenerateStoredKey(string key, Type t) { return string.Format("{0}-{1}", t.FullName, key); } Index: Src/GoogleApis/Apis/Services/BaseClientService.cs =================================================================== --- a/Src/GoogleApis/Apis/Services/BaseClientService.cs +++ b/Src/GoogleApis/Apis/Services/BaseClientService.cs @@ -78,7 +78,7 @@ /// Get or sets the exponential back-off policy used by the service. Default value is /// UnsuccessfulResponse503, which means that exponential back-off is used on 503 abnormal HTTP /// response. - /// If the value is set to None, no exponential back-off policy is used, and it's up to user to + /// If the value is set to None, no exponential back-off policy is used, and it's up to the user to /// configure the in an /// to set a specific back-off /// implementation (using ). @@ -177,8 +177,8 @@ // Add authenticator initializer to intercept a request and add the "Authorization" header and also handle // abnormal 401 responses in case the authenticator is an instance of unsuccessful response handler. args.Initializers.Add(new AuthenticatorMessageHandlerInitializer(Authenticator)); - - var httpClient = factory.CreateHttpClient(args); + + var httpClient = factory.CreateHttpClient(args); if (initializer.MaxUrlLength > 0) { httpClient.MessageHandler.ExecuteInterceptors.Add(