From 6761c7f47564f72f541a691f6413e247e8924489 Mon Sep 17 00:00:00 2001 From: Haacked Date: Thu, 17 Apr 2014 15:50:32 -0700 Subject: [PATCH 1/9] Add support for OAuth Web Flow This adds support for developers who want to implement the OAuth web flow with GitHub authentication. https://developer.github.com/v3/oauth/ --- Octokit/Clients/IOAuthClient.cs | 32 +++++++++++++ Octokit/Clients/OAuthClient.cs | 46 ++++++++++++++++++ Octokit/Helpers/ApiUrls.cs | 20 ++++++++ Octokit/Models/Request/OauthLoginRequest.cs | 52 +++++++++++++++++++++ Octokit/Models/Request/OauthTokenRequest.cs | 50 ++++++++++++++++++++ Octokit/Models/Response/OauthToken.cs | 11 +++++ Octokit/Octokit-Mono.csproj | 5 ++ Octokit/Octokit-MonoAndroid.csproj | 5 ++ Octokit/Octokit-Monotouch.csproj | 5 ++ Octokit/Octokit-Portable.csproj | 5 ++ Octokit/Octokit-netcore45.csproj | 5 ++ Octokit/Octokit.csproj | 5 ++ 12 files changed, 241 insertions(+) create mode 100644 Octokit/Clients/IOAuthClient.cs create mode 100644 Octokit/Clients/OAuthClient.cs create mode 100644 Octokit/Models/Request/OauthLoginRequest.cs create mode 100644 Octokit/Models/Request/OauthTokenRequest.cs create mode 100644 Octokit/Models/Response/OauthToken.cs diff --git a/Octokit/Clients/IOAuthClient.cs b/Octokit/Clients/IOAuthClient.cs new file mode 100644 index 0000000000..fdbee47d03 --- /dev/null +++ b/Octokit/Clients/IOAuthClient.cs @@ -0,0 +1,32 @@ +using System; +using System.Threading.Tasks; + +namespace Octokit +{ + /// + /// Provides methods used in the OAuth web flow. + /// + public interface IOauthClient + { + /// + /// Gets the URL used in the first step of the web flow. The Web application should redirect to this URL. + /// + /// Parameters to the Oauth web flow login url + /// + Uri GetGitHubLoginUrl(OauthLoginRequest request); + + /// + /// Makes a request to get an access token using the code returned when GitHub.com redirects back from the URL + /// GitHub login url to the application. + /// + /// + /// If the user accepts your request, GitHub redirects back to your site with a temporary code in a code + /// parameter as well as the state you provided in the previous step in a state parameter. If the states don’t + /// match, the request has been created by a third party and the process should be aborted. Exchange this for + /// an access token using this method. + /// + /// + /// + Task GetAccessToken(OauthTokenRequest request); + } +} diff --git a/Octokit/Clients/OAuthClient.cs b/Octokit/Clients/OAuthClient.cs new file mode 100644 index 0000000000..60bd595ae6 --- /dev/null +++ b/Octokit/Clients/OAuthClient.cs @@ -0,0 +1,46 @@ +using System; +using System.Threading.Tasks; + +namespace Octokit +{ + /// + /// Provides methods used in the OAuth web flow. + /// + public class OauthClient : ApiClient, IOauthClient + { + public OauthClient(IApiConnection apiConnection) : base(apiConnection) + { + } + + /// + /// Gets the URL used in the first step of the web flow. The Web application should redirect to this URL. + /// + /// Parameters to the Oauth web flow login url + /// + public Uri GetGitHubLoginUrl(OauthLoginRequest request) + { + Ensure.ArgumentNotNull(request, "request"); + + return ApiUrls.OauthAccessToken().ApplyParameters(request.ToParametersDictionary()); + } + + /// + /// Makes a request to get an access token using the code returned when GitHub.com redirects back from the URL + /// GitHub login url to the application. + /// + /// + /// If the user accepts your request, GitHub redirects back to your site with a temporary code in a code + /// parameter as well as the state you provided in the previous step in a state parameter. If the states don’t + /// match, the request has been created by a third party and the process should be aborted. Exchange this for + /// an access token using this method. + /// + /// + /// + public Task GetAccessToken(OauthTokenRequest request) + { + Ensure.ArgumentNotNull(request, "request"); + + return ApiConnection.Get(ApiUrls.OauthAccessToken(), request.ToParametersDictionary()); + } + } +} diff --git a/Octokit/Helpers/ApiUrls.cs b/Octokit/Helpers/ApiUrls.cs index 2e3fe12929..5e95362179 100644 --- a/Octokit/Helpers/ApiUrls.cs +++ b/Octokit/Helpers/ApiUrls.cs @@ -17,6 +17,8 @@ public static class ApiUrls static readonly Uri _currentUserNotificationsEndpoint = new Uri("notifications", UriKind.Relative); static readonly Uri _currentUserAllIssues = new Uri("issues", UriKind.Relative); static readonly Uri _currentUserOwnedAndMemberIssues = new Uri("user/issues", UriKind.Relative); + static readonly Uri _oauthAuthorize = new Uri("login/oauth/authorize", UriKind.Relative); + static readonly Uri _oauthAccesToken = new Uri("login/oauth/access_token", UriKind.Relative); /// /// Returns the that returns all of the repositories for the currently logged in user in @@ -1176,5 +1178,23 @@ public static Uri IsFollowing(string login, string following) { return "users/{0}/following/{1}".FormatUri(login, following); } + + /// + /// Creates the relative for initiating the OAuth Web login Flow + /// + /// + public static Uri OauthAuthorize() + { + return _oauthAuthorize; + } + + /// + /// Creates the relative to request an OAuth access token. + /// + /// + public static Uri OauthAccessToken() + { + return _oauthAccesToken; + } } } diff --git a/Octokit/Models/Request/OauthLoginRequest.cs b/Octokit/Models/Request/OauthLoginRequest.cs new file mode 100644 index 0000000000..f7d7e53d97 --- /dev/null +++ b/Octokit/Models/Request/OauthLoginRequest.cs @@ -0,0 +1,52 @@ +using System; +using System.Collections.ObjectModel; + +namespace Octokit +{ + public class OauthLoginRequest : RequestParameters + { + /// + /// Creates an instance of the OAuth login request with the required parameter. + /// + /// The client ID you received from GitHub when you registered the application. + public OauthLoginRequest(string clientId) + { + Ensure.ArgumentNotNullOrEmptyString(clientId, "clientId"); + + ClientId = clientId; + Scopes = new Collection(); + } + + /// + /// The client ID you received from GitHub when you registered the application. + /// + public string ClientId { get; private set; } + + /// + /// The URL in your app where users will be sent after authorization. + /// + /// + /// See the documentation about redirect urls + /// for more information. + /// + public Uri RedirectUri { get; set; } + + /// + /// A set of scopes to request. If not provided, scope defaults to an empty list of scopes for users that don’t + /// have a valid token for the app. For users who do already have a valid token for the app, the user won’t be + /// shown the OAuth authorization page with the list of scopes. Instead, this step of the flow will + /// automatically complete with the same scopes that were used last time the user completed the flow. + /// + /// + /// See the scopes documentation for more + /// information about scopes. + /// + public Collection Scopes { get; private set; } + + /// + /// An unguessable random string. It is used to protect against cross-site request forgery attacks. In ASP.NET + /// MVC this would correspond to an anti-forgery token. + /// + public string State { get; set; } + } +} diff --git a/Octokit/Models/Request/OauthTokenRequest.cs b/Octokit/Models/Request/OauthTokenRequest.cs new file mode 100644 index 0000000000..e582192ec7 --- /dev/null +++ b/Octokit/Models/Request/OauthTokenRequest.cs @@ -0,0 +1,50 @@ +using System; + +namespace Octokit +{ + public class OauthTokenRequest : RequestParameters + { + /// + /// Creates an instance of the OAuth login request with the required parameter. + /// + /// The client ID you received from GitHub when you registered the application. + /// The client secret you received from GitHub when you registered. + /// The code you received as a response to making the + /// OAuth login request + public OauthTokenRequest(string clientId, string clientSecret, string code) + { + Ensure.ArgumentNotNullOrEmptyString(clientId, "clientId"); + Ensure.ArgumentNotNullOrEmptyString(clientSecret, "clientSecret"); + Ensure.ArgumentNotNullOrEmptyString(code, "code"); + + ClientId = clientId; + ClientSecret = clientSecret; + Code = code; + } + + /// + /// The client ID you received from GitHub when you registered the application. + /// + public string ClientId { get; private set; } + + /// + /// The client secret you received from GitHub when you registered. + /// + public string ClientSecret { get; private set; } + + /// + /// The code you received as a response to making the OAuth login + /// request. + /// + public string Code { get; private set; } + + /// + /// The URL in your app where users will be sent after authorization. + /// + /// + /// See the documentation about redirect urls + /// for more information. + /// + public Uri RedirectUri { get; set; } + } +} diff --git a/Octokit/Models/Response/OauthToken.cs b/Octokit/Models/Response/OauthToken.cs new file mode 100644 index 0000000000..6c2d46e54e --- /dev/null +++ b/Octokit/Models/Response/OauthToken.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; + +namespace Octokit +{ + public class OauthToken + { + public string TokenType { get; set; } + public string AccessToken { get; set; } + public IReadOnlyCollection Scope { get; set; } + } +} diff --git a/Octokit/Octokit-Mono.csproj b/Octokit/Octokit-Mono.csproj index 7db1a0af42..ac2eded971 100644 --- a/Octokit/Octokit-Mono.csproj +++ b/Octokit/Octokit-Mono.csproj @@ -315,6 +315,11 @@ + + + + + \ No newline at end of file diff --git a/Octokit/Octokit-MonoAndroid.csproj b/Octokit/Octokit-MonoAndroid.csproj index 7a5e1ce18a..bdd41ca38b 100644 --- a/Octokit/Octokit-MonoAndroid.csproj +++ b/Octokit/Octokit-MonoAndroid.csproj @@ -326,6 +326,11 @@ + + + + + \ No newline at end of file diff --git a/Octokit/Octokit-Monotouch.csproj b/Octokit/Octokit-Monotouch.csproj index c04a85cd03..aedc78de30 100644 --- a/Octokit/Octokit-Monotouch.csproj +++ b/Octokit/Octokit-Monotouch.csproj @@ -321,6 +321,11 @@ + + + + + \ No newline at end of file diff --git a/Octokit/Octokit-Portable.csproj b/Octokit/Octokit-Portable.csproj index 0b72c890d0..caf6afa385 100644 --- a/Octokit/Octokit-Portable.csproj +++ b/Octokit/Octokit-Portable.csproj @@ -312,6 +312,11 @@ + + + + + diff --git a/Octokit/Octokit-netcore45.csproj b/Octokit/Octokit-netcore45.csproj index 69754d1dc1..7e11fc82ce 100644 --- a/Octokit/Octokit-netcore45.csproj +++ b/Octokit/Octokit-netcore45.csproj @@ -316,6 +316,11 @@ + + + + + diff --git a/Octokit/Octokit.csproj b/Octokit/Octokit.csproj index 121a66e52f..dd956b6c42 100644 --- a/Octokit/Octokit.csproj +++ b/Octokit/Octokit.csproj @@ -54,7 +54,9 @@ Properties\SolutionInfo.cs + + @@ -66,6 +68,8 @@ + + @@ -140,6 +144,7 @@ + From d7a69e20d77f56613ed7940f7d7c3386537dc313 Mon Sep 17 00:00:00 2001 From: Haacked Date: Sat, 19 Apr 2014 16:01:35 -0700 Subject: [PATCH 2/9] Implement OAuth Web Flow Provide methods to make it easy for developers to implement the web flow. https://developer.github.com/v3/oauth/#web-application-flow --- Octokit.Tests/Clients/OauthClientTests.cs | 87 +++++++++++++++++++++ Octokit.Tests/Helpers/UriExtensionsTests.cs | 4 +- Octokit.Tests/Octokit.Tests.csproj | 1 + Octokit/Clients/IOAuthClient.cs | 2 +- Octokit/Clients/OAuthClient.cs | 29 +++++-- Octokit/GitHubClient.cs | 2 + Octokit/Helpers/UriExtensions.cs | 2 +- Octokit/Http/Connection.cs | 28 +++++-- Octokit/Http/IConnection.cs | 22 ++++++ Octokit/Http/JsonHttpPipeline.cs | 2 +- Octokit/IGitHubClient.cs | 1 + Octokit/Models/Request/OauthLoginRequest.cs | 5 ++ Octokit/Models/Request/OauthTokenRequest.cs | 12 ++- 13 files changed, 177 insertions(+), 20 deletions(-) create mode 100644 Octokit.Tests/Clients/OauthClientTests.cs diff --git a/Octokit.Tests/Clients/OauthClientTests.cs b/Octokit.Tests/Clients/OauthClientTests.cs new file mode 100644 index 0000000000..e96826c71f --- /dev/null +++ b/Octokit.Tests/Clients/OauthClientTests.cs @@ -0,0 +1,87 @@ +using System; +using System.Net.Http; +using System.Threading.Tasks; +using NSubstitute; +using Octokit; +using Octokit.Tests; +using Xunit; +using Xunit.Extensions; + +public class OauthClientTests +{ + public class TheGetGitHubLoginUrlMethod + { + [Theory] + [InlineData("https://api.github.com", "https://github.com/login/oauth/authorize?client_id=secret")] + [InlineData("https://github.com", "https://github.com/login/oauth/authorize?client_id=secret")] + [InlineData("https://example.com", "https://example.com/login/oauth/authorize?client_id=secret")] + [InlineData("https://api.example.com", "https://api.example.com/login/oauth/authorize?client_id=secret")] + public void ReturnsProperAuthorizeUrl(string baseAddress, string expectedUrl) + { + var connection = Substitute.For(); + connection.BaseAddress.Returns(new Uri(baseAddress)); + var client = new OauthClient(connection); + + var result = client.GetGitHubLoginUrl(new OauthLoginRequest("secret")); + + Assert.Equal(new Uri(expectedUrl), result); + } + + [Fact] + public void ReturnsUrlWithAllParameters() + { + var request = new OauthLoginRequest("secret") + { + RedirectUri = new Uri("https://example.com/foo?foo=bar"), + Scopes = { "foo", "bar" }, + State = "canary" + }; + var connection = Substitute.For(); + connection.BaseAddress.Returns(new Uri("https://api.github.com")); + var client = new OauthClient(connection); + + var result = client.GetGitHubLoginUrl(request); + + const string expected = "https://github.com/login/oauth/authorize?client_id=secret&redirect_uri=https://example.com/foo?foo=bar&scope=foo,bar&state=canary"; + Assert.Equal(expected, result.ToString()); + Assert.Equal("?client_id=secret&redirect_uri=https%3A%2F%2Fexample.com%2Ffoo%3Ffoo%3Dbar&scope=foo%2Cbar&state=canary", result.Query); + } + } + + public class TheCreateAccessTokenMethod + { + [Fact] + public async Task PostsWithCorrectBodyAndContentType() + { + var responseToken = new OauthToken(); + var response = Substitute.For>(); + response.BodyAsObject.Returns(responseToken); + var connection = Substitute.For(); + connection.BaseAddress.Returns(new Uri("https://api.github.com/")); + Uri calledUri = null; + FormUrlEncodedContent calledBody = null; + Uri calledHostAddress = null; + connection.PostAsync( + Arg.Do(uri => calledUri = uri), + Arg.Do(body => calledBody = body as FormUrlEncodedContent), + "application/json", + null, + Arg.Do(uri => calledHostAddress = uri)) + .Returns(_ => Task.FromResult(response)); + var client = new OauthClient(connection); + + var token = await client.CreateAccessToken(new OauthTokenRequest("secretid", "secretsecret", "code") + { + RedirectUri = new Uri("https://example.com/foo") + }); + + Assert.Same(responseToken, token); + Assert.Equal("login/oauth/access_token", calledUri.ToString()); + Assert.NotNull(calledBody); + Assert.Equal("https://github.com/", calledHostAddress.ToString()); + Assert.Equal( + "client_id=secretid&client_secret=secretsecret&code=code&redirect_uri=https%3A%2F%2Fexample.com%2Ffoo", + await calledBody.ReadAsStringAsync()); + } + } +} diff --git a/Octokit.Tests/Helpers/UriExtensionsTests.cs b/Octokit.Tests/Helpers/UriExtensionsTests.cs index 2ab07abb82..20bae54b4e 100644 --- a/Octokit.Tests/Helpers/UriExtensionsTests.cs +++ b/Octokit.Tests/Helpers/UriExtensionsTests.cs @@ -15,11 +15,11 @@ public void AppendsParametersAsQueryString() var uriWithParameters = uri.ApplyParameters(new Dictionary { - {"foo", "fooval"}, + {"foo", "foo val"}, {"bar", "barval"} }); - Assert.Equal(new Uri("https://example.com?foo=fooval&bar=barval"), uriWithParameters); + Assert.Equal(new Uri("https://example.com?foo=foo%20val&bar=barval"), uriWithParameters); } [Fact] diff --git a/Octokit.Tests/Octokit.Tests.csproj b/Octokit.Tests/Octokit.Tests.csproj index 64da5f2f4e..312e40adc8 100644 --- a/Octokit.Tests/Octokit.Tests.csproj +++ b/Octokit.Tests/Octokit.Tests.csproj @@ -62,6 +62,7 @@ + diff --git a/Octokit/Clients/IOAuthClient.cs b/Octokit/Clients/IOAuthClient.cs index fdbee47d03..b0bde0191a 100644 --- a/Octokit/Clients/IOAuthClient.cs +++ b/Octokit/Clients/IOAuthClient.cs @@ -27,6 +27,6 @@ public interface IOauthClient /// /// /// - Task GetAccessToken(OauthTokenRequest request); + Task CreateAccessToken(OauthTokenRequest request); } } diff --git a/Octokit/Clients/OAuthClient.cs b/Octokit/Clients/OAuthClient.cs index 60bd595ae6..6c1b092aab 100644 --- a/Octokit/Clients/OAuthClient.cs +++ b/Octokit/Clients/OAuthClient.cs @@ -1,4 +1,5 @@ using System; +using System.Net.Http; using System.Threading.Tasks; namespace Octokit @@ -6,10 +7,22 @@ namespace Octokit /// /// Provides methods used in the OAuth web flow. /// - public class OauthClient : ApiClient, IOauthClient + public class OauthClient : IOauthClient { - public OauthClient(IApiConnection apiConnection) : base(apiConnection) + readonly IConnection connection; + readonly Uri hostAddress; + + public OauthClient(IConnection connection) { + Ensure.ArgumentNotNull(connection, "connection"); + + this.connection = connection; + var baseAddress = connection.BaseAddress ?? GitHubClient.GitHubDotComUrl; + + // The Oauth login stuff uses https://github.com and not the https://api.github.com URLs. + hostAddress = baseAddress.Host.Equals("api.github.com") + ? new Uri("https://github.com") + : baseAddress; } /// @@ -21,7 +34,8 @@ public Uri GetGitHubLoginUrl(OauthLoginRequest request) { Ensure.ArgumentNotNull(request, "request"); - return ApiUrls.OauthAccessToken().ApplyParameters(request.ToParametersDictionary()); + return new Uri(hostAddress, ApiUrls.OauthAuthorize()) + .ApplyParameters(request.ToParametersDictionary()); } /// @@ -36,11 +50,16 @@ public Uri GetGitHubLoginUrl(OauthLoginRequest request) /// /// /// - public Task GetAccessToken(OauthTokenRequest request) + public async Task CreateAccessToken(OauthTokenRequest request) { Ensure.ArgumentNotNull(request, "request"); - return ApiConnection.Get(ApiUrls.OauthAccessToken(), request.ToParametersDictionary()); + var endPoint = ApiUrls.OauthAccessToken(); + + var body = new FormUrlEncodedContent(request.ToParametersDictionary()); + + var response = await connection.PostAsync(endPoint, body, "application/json", null, hostAddress); + return response.BodyAsObject; } } } diff --git a/Octokit/GitHubClient.cs b/Octokit/GitHubClient.cs index 00f7840541..97a663db6d 100644 --- a/Octokit/GitHubClient.cs +++ b/Octokit/GitHubClient.cs @@ -84,6 +84,7 @@ public GitHubClient(IConnection connection) Issue = new IssuesClient(apiConnection); Miscellaneous = new MiscellaneousClient(connection); Notification = new NotificationsClient(apiConnection); + Oauth = new OauthClient(connection); Organization = new OrganizationsClient(apiConnection); Repository = new RepositoriesClient(apiConnection); Gist = new GistsClient(apiConnection); @@ -133,6 +134,7 @@ public Uri BaseAddress public IActivitiesClient Activity { get; private set; } public IIssuesClient Issue { get; private set; } public IMiscellaneousClient Miscellaneous { get; private set; } + public IOauthClient Oauth { get; private set; } public IOrganizationsClient Organization { get; private set; } public IRepositoriesClient Repository { get; private set; } public IGistsClient Gist { get; private set; } diff --git a/Octokit/Helpers/UriExtensions.cs b/Octokit/Helpers/UriExtensions.cs index d3d3c10216..718cc795d9 100644 --- a/Octokit/Helpers/UriExtensions.cs +++ b/Octokit/Helpers/UriExtensions.cs @@ -44,7 +44,7 @@ public static Uri ApplyParameters(this Uri uri, IDictionary para } } - string query = String.Join("&", p.Select(kvp => kvp.Key + "=" + kvp.Value)); + string query = String.Join("&", p.Select(kvp => kvp.Key + "=" + Uri.EscapeDataString(kvp.Value))); if (uri.IsAbsoluteUri) { var uriBuilder = new UriBuilder(uri) diff --git a/Octokit/Http/Connection.cs b/Octokit/Http/Connection.cs index c9ace05734..06dc5ee006 100644 --- a/Octokit/Http/Connection.cs +++ b/Octokit/Http/Connection.cs @@ -4,7 +4,6 @@ using System.Linq; using System.Net; using System.Net.Http; -using System.Net.Http.Headers; using System.Threading; using System.Threading.Tasks; using Octokit.Internal; @@ -22,7 +21,6 @@ public class Connection : IConnection static readonly ICredentialStore _anonymousCredentials = new InMemoryCredentialStore(Credentials.Anonymous); readonly Authenticator _authenticator; - readonly IHttpClient _httpClient; readonly JsonHttpPipeline _jsonPipeline; /// @@ -132,7 +130,7 @@ public Connection( UserAgent = FormatUserAgent(productInformation); BaseAddress = baseAddress; _authenticator = new Authenticator(credentialStore); - _httpClient = httpClient; + HttpClient = httpClient; _jsonPipeline = new JsonHttpPipeline(); } @@ -194,6 +192,14 @@ public Task> PostAsync(Uri uri, object body, string accepts, str return SendData(uri, HttpMethod.Post, body, accepts, contentType, CancellationToken.None); } + public Task> PostAsync(Uri uri, object body, string accepts, string contentType, Uri baseAddress) + { + Ensure.ArgumentNotNull(uri, "uri"); + Ensure.ArgumentNotNull(body, "body"); + + return SendData(uri, HttpMethod.Post, body, accepts, contentType, CancellationToken.None, baseAddress: baseAddress); + } + public Task> PutAsync(Uri uri, object body) { return SendData(uri, HttpMethod.Put, body, null, null, CancellationToken.None); @@ -217,7 +223,8 @@ Task> SendData( string accepts, string contentType, CancellationToken cancellationToken, - string twoFactorAuthenticationCode = null + string twoFactorAuthenticationCode = null, + Uri baseAddress = null ) { Ensure.ArgumentNotNull(uri, "uri"); @@ -225,7 +232,7 @@ Task> SendData( var request = new Request { Method = method, - BaseAddress = BaseAddress, + BaseAddress = baseAddress ?? BaseAddress, Endpoint = uri, }; @@ -327,6 +334,15 @@ public Credentials Credentials } } + /// + /// The Http Client adapter instance used to make the actual request. + /// + public IHttpClient HttpClient + { + get; + private set; + } + Task> GetHtml(IRequest request) { request.Headers.Add("Accept", "application/vnd.github.html"); @@ -346,7 +362,7 @@ async Task> RunRequest(IRequest request, CancellationToken cance { request.Headers.Add("User-Agent", UserAgent); await _authenticator.Apply(request).ConfigureAwait(false); - var response = await _httpClient.Send(request, cancellationToken).ConfigureAwait(false); + var response = await HttpClient.Send(request, cancellationToken).ConfigureAwait(false); ApiInfoParser.ParseApiHttpHeaders(response); HandleErrors(response); return response; diff --git a/Octokit/Http/IConnection.cs b/Octokit/Http/IConnection.cs index 94ed197899..32e6e14698 100644 --- a/Octokit/Http/IConnection.cs +++ b/Octokit/Http/IConnection.cs @@ -76,6 +76,23 @@ public interface IConnection /// representing the received HTTP response Task> PostAsync(Uri uri, object body, string accepts, string contentType); + /// + /// Performs an asynchronous HTTP POST request. + /// Attempts to map the response body to an object of type + /// + /// + /// We have one case where we need to override the BaseAddress. This overload is for that case. + /// https://developer.github.com/v3/oauth/#web-application-flow + /// + /// The type to map the response to + /// URI endpoint to send request to + /// The object to serialize as the body of the request + /// Specifies accepted response media types. + /// Specifies the media type of the request body + /// Allows overriding the base address for a post. + /// representing the received HTTP response + Task> PostAsync(Uri uri, object body, string accepts, string contentType, Uri baseAddress); + /// /// Performs an asynchronous HTTP PUT request. /// Attempts to map the response body to an object of type @@ -131,5 +148,10 @@ public interface IConnection /// the default with just these credentials. /// Credentials Credentials { get; set; } + + /// + /// The Http Client adapter instance used to make the actual request. + /// + IHttpClient HttpClient { get; } } } diff --git a/Octokit/Http/JsonHttpPipeline.cs b/Octokit/Http/JsonHttpPipeline.cs index 3eea0d4e73..8bf6229ca8 100644 --- a/Octokit/Http/JsonHttpPipeline.cs +++ b/Octokit/Http/JsonHttpPipeline.cs @@ -33,7 +33,7 @@ public void SerializeRequest(IRequest request) } if (request.Method == HttpMethod.Get || request.Body == null) return; - if (request.Body is string || request.Body is Stream) return; + if (request.Body is string || request.Body is Stream || request.Body is HttpContent) return; request.Body = _serializer.Serialize(request.Body); } diff --git a/Octokit/IGitHubClient.cs b/Octokit/IGitHubClient.cs index d7b0a222bb..ba5da49043 100644 --- a/Octokit/IGitHubClient.cs +++ b/Octokit/IGitHubClient.cs @@ -13,6 +13,7 @@ public interface IGitHubClient IActivitiesClient Activity { get; } IIssuesClient Issue { get; } IMiscellaneousClient Miscellaneous { get; } + IOauthClient Oauth { get; } IOrganizationsClient Organization { get; } IRepositoriesClient Repository { get; } IGistsClient Gist { get; } diff --git a/Octokit/Models/Request/OauthLoginRequest.cs b/Octokit/Models/Request/OauthLoginRequest.cs index f7d7e53d97..3bfc6d40c1 100644 --- a/Octokit/Models/Request/OauthLoginRequest.cs +++ b/Octokit/Models/Request/OauthLoginRequest.cs @@ -1,5 +1,6 @@ using System; using System.Collections.ObjectModel; +using Octokit.Internal; namespace Octokit { @@ -20,6 +21,7 @@ public OauthLoginRequest(string clientId) /// /// The client ID you received from GitHub when you registered the application. /// + [Parameter(Key = "client_id")] public string ClientId { get; private set; } /// @@ -29,6 +31,7 @@ public OauthLoginRequest(string clientId) /// See the documentation about redirect urls /// for more information. /// + [Parameter(Key = "redirect_uri")] public Uri RedirectUri { get; set; } /// @@ -41,12 +44,14 @@ public OauthLoginRequest(string clientId) /// See the scopes documentation for more /// information about scopes. /// + [Parameter(Key = "scope")] public Collection Scopes { get; private set; } /// /// An unguessable random string. It is used to protect against cross-site request forgery attacks. In ASP.NET /// MVC this would correspond to an anti-forgery token. /// + [Parameter(Key = "state")] public string State { get; set; } } } diff --git a/Octokit/Models/Request/OauthTokenRequest.cs b/Octokit/Models/Request/OauthTokenRequest.cs index e582192ec7..437e0ce290 100644 --- a/Octokit/Models/Request/OauthTokenRequest.cs +++ b/Octokit/Models/Request/OauthTokenRequest.cs @@ -1,16 +1,16 @@ using System; +using Octokit.Internal; namespace Octokit { public class OauthTokenRequest : RequestParameters { - /// + /// /// Creates an instance of the OAuth login request with the required parameter. /// /// The client ID you received from GitHub when you registered the application. /// The client secret you received from GitHub when you registered. - /// The code you received as a response to making the - /// OAuth login request + /// The code you received as a response to making the OAuth login request public OauthTokenRequest(string clientId, string clientSecret, string code) { Ensure.ArgumentNotNullOrEmptyString(clientId, "clientId"); @@ -25,17 +25,20 @@ public OauthTokenRequest(string clientId, string clientSecret, string code) /// /// The client ID you received from GitHub when you registered the application. /// + [Parameter(Key = "client_id")] public string ClientId { get; private set; } /// /// The client secret you received from GitHub when you registered. /// + [Parameter(Key = "client_secret")] public string ClientSecret { get; private set; } /// - /// The code you received as a response to making the OAuth login + /// The code you received as a response to making the OAuth login /// request. /// + [Parameter(Key = "code")] public string Code { get; private set; } /// @@ -45,6 +48,7 @@ public OauthTokenRequest(string clientId, string clientSecret, string code) /// See the documentation about redirect urls /// for more information. /// + [Parameter(Key = "redirect_uri")] public Uri RedirectUri { get; set; } } } From 459299b730908a6012eac0c8d33e679c292606a4 Mon Sep 17 00:00:00 2001 From: Haacked Date: Sun, 20 Apr 2014 21:50:04 -0700 Subject: [PATCH 3/9] We don't need to make this property public after all --- Octokit/Http/Connection.cs | 14 +++----------- Octokit/Http/IConnection.cs | 5 ----- 2 files changed, 3 insertions(+), 16 deletions(-) diff --git a/Octokit/Http/Connection.cs b/Octokit/Http/Connection.cs index 06dc5ee006..45b654067f 100644 --- a/Octokit/Http/Connection.cs +++ b/Octokit/Http/Connection.cs @@ -22,6 +22,7 @@ public class Connection : IConnection readonly Authenticator _authenticator; readonly JsonHttpPipeline _jsonPipeline; + readonly IHttpClient _httpClient; /// /// Creates a new connection instance used to make requests of the GitHub API. @@ -130,7 +131,7 @@ public Connection( UserAgent = FormatUserAgent(productInformation); BaseAddress = baseAddress; _authenticator = new Authenticator(credentialStore); - HttpClient = httpClient; + _httpClient = httpClient; _jsonPipeline = new JsonHttpPipeline(); } @@ -334,15 +335,6 @@ public Credentials Credentials } } - /// - /// The Http Client adapter instance used to make the actual request. - /// - public IHttpClient HttpClient - { - get; - private set; - } - Task> GetHtml(IRequest request) { request.Headers.Add("Accept", "application/vnd.github.html"); @@ -362,7 +354,7 @@ async Task> RunRequest(IRequest request, CancellationToken cance { request.Headers.Add("User-Agent", UserAgent); await _authenticator.Apply(request).ConfigureAwait(false); - var response = await HttpClient.Send(request, cancellationToken).ConfigureAwait(false); + var response = await _httpClient.Send(request, cancellationToken).ConfigureAwait(false); ApiInfoParser.ParseApiHttpHeaders(response); HandleErrors(response); return response; diff --git a/Octokit/Http/IConnection.cs b/Octokit/Http/IConnection.cs index 32e6e14698..7d2d335ab3 100644 --- a/Octokit/Http/IConnection.cs +++ b/Octokit/Http/IConnection.cs @@ -148,10 +148,5 @@ public interface IConnection /// the default with just these credentials. /// Credentials Credentials { get; set; } - - /// - /// The Http Client adapter instance used to make the actual request. - /// - IHttpClient HttpClient { get; } } } From 5d70f885b7026851e51322bd7a961b31636b7f3b Mon Sep 17 00:00:00 2001 From: Haacked Date: Mon, 21 Apr 2014 16:15:44 -0700 Subject: [PATCH 4/9] Do not lower case user supplied value That value might legit need upper and lower case such as the state in an oauth call. --- Octokit.Tests/Clients/OauthClientTests.cs | 6 +++--- Octokit/Models/Request/RequestParameters.cs | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Octokit.Tests/Clients/OauthClientTests.cs b/Octokit.Tests/Clients/OauthClientTests.cs index e96826c71f..d009af682e 100644 --- a/Octokit.Tests/Clients/OauthClientTests.cs +++ b/Octokit.Tests/Clients/OauthClientTests.cs @@ -34,7 +34,7 @@ public void ReturnsUrlWithAllParameters() { RedirectUri = new Uri("https://example.com/foo?foo=bar"), Scopes = { "foo", "bar" }, - State = "canary" + State = "canARY" }; var connection = Substitute.For(); connection.BaseAddress.Returns(new Uri("https://api.github.com")); @@ -42,9 +42,9 @@ public void ReturnsUrlWithAllParameters() var result = client.GetGitHubLoginUrl(request); - const string expected = "https://github.com/login/oauth/authorize?client_id=secret&redirect_uri=https://example.com/foo?foo=bar&scope=foo,bar&state=canary"; + const string expected = "https://github.com/login/oauth/authorize?client_id=secret&redirect_uri=https://example.com/foo?foo=bar&scope=foo,bar&state=canARY"; Assert.Equal(expected, result.ToString()); - Assert.Equal("?client_id=secret&redirect_uri=https%3A%2F%2Fexample.com%2Ffoo%3Ffoo%3Dbar&scope=foo%2Cbar&state=canary", result.Query); + Assert.Equal("?client_id=secret&redirect_uri=https%3A%2F%2Fexample.com%2Ffoo%3Ffoo%3Dbar&scope=foo%2Cbar&state=canARY", result.Query); } } diff --git a/Octokit/Models/Request/RequestParameters.cs b/Octokit/Models/Request/RequestParameters.cs index 62ad0fee8e..ba69623ec3 100644 --- a/Octokit/Models/Request/RequestParameters.cs +++ b/Octokit/Models/Request/RequestParameters.cs @@ -80,7 +80,7 @@ static Func GetValueFunc(Type propertyType) } return (prop, value) => value != null - ? value.ToString().ToLowerInvariant() + ? value.ToString() : null; } From 9e0fb886ca6293359b01f70cef1419b287b51e5a Mon Sep 17 00:00:00 2001 From: Haacked Date: Tue, 22 Apr 2014 08:38:53 -0700 Subject: [PATCH 5/9] Implement observable oauth client Rx will never die --- .../Clients/IObservableOauthClient.cs | 28 +++++++++++++++++++ .../Clients/ObservableOauthClient.cs | 28 +++++++++++++++++++ Octokit.Reactive/IObservableGitHubClient.cs | 1 + Octokit.Reactive/ObservableGitHubClient.cs | 3 +- Octokit.Reactive/Octokit.Reactive.csproj | 3 +- 5 files changed, 61 insertions(+), 2 deletions(-) create mode 100644 Octokit.Reactive/Clients/IObservableOauthClient.cs create mode 100644 Octokit.Reactive/Clients/ObservableOauthClient.cs diff --git a/Octokit.Reactive/Clients/IObservableOauthClient.cs b/Octokit.Reactive/Clients/IObservableOauthClient.cs new file mode 100644 index 0000000000..01f1d6f10a --- /dev/null +++ b/Octokit.Reactive/Clients/IObservableOauthClient.cs @@ -0,0 +1,28 @@ +using System; + +namespace Octokit.Reactive +{ + public interface IObservableOauthClient + { + /// + /// Gets the URL used in the first step of the web flow. The Web application should redirect to this URL. + /// + /// Parameters to the Oauth web flow login url + /// + IObservable GetGitHubLoginUrl(OauthLoginRequest request); + + /// + /// Makes a request to get an access token using the code returned when GitHub.com redirects back from the URL + /// GitHub login url to the application. + /// + /// + /// If the user accepts your request, GitHub redirects back to your site with a temporary code in a code + /// parameter as well as the state you provided in the previous step in a state parameter. If the states don’t + /// match, the request has been created by a third party and the process should be aborted. Exchange this for + /// an access token using this method. + /// + /// + /// + IObservable CreateAccessToken(OauthTokenRequest request); + } +} diff --git a/Octokit.Reactive/Clients/ObservableOauthClient.cs b/Octokit.Reactive/Clients/ObservableOauthClient.cs new file mode 100644 index 0000000000..cd73a94f88 --- /dev/null +++ b/Octokit.Reactive/Clients/ObservableOauthClient.cs @@ -0,0 +1,28 @@ +using System; +using System.Reactive.Linq; +using System.Reactive.Threading.Tasks; + +namespace Octokit.Reactive +{ + public class ObservableOauthClient : IObservableOauthClient + { + readonly IGitHubClient _client; + + public ObservableOauthClient(IGitHubClient client) + { + Ensure.ArgumentNotNull(client, "client"); + + _client = client; + } + + public IObservable GetGitHubLoginUrl(OauthLoginRequest request) + { + return Observable.Return(_client.Oauth.GetGitHubLoginUrl(request)); + } + + public IObservable CreateAccessToken(OauthTokenRequest request) + { + return _client.Oauth.CreateAccessToken(request).ToObservable(); + } + } +} diff --git a/Octokit.Reactive/IObservableGitHubClient.cs b/Octokit.Reactive/IObservableGitHubClient.cs index ecc8dd89dc..c8983ff83f 100644 --- a/Octokit.Reactive/IObservableGitHubClient.cs +++ b/Octokit.Reactive/IObservableGitHubClient.cs @@ -8,6 +8,7 @@ public interface IObservableGitHubClient IObservableActivitiesClient Activity { get; } IObservableIssuesClient Issue { get; } IObservableMiscellaneousClient Miscellaneous { get; } + IObservableOauthClient Oauth { get; } IObservableOrganizationsClient Organization { get; } IObservableRepositoriesClient Repository { get; } IObservableGistsClient Gist { get; } diff --git a/Octokit.Reactive/ObservableGitHubClient.cs b/Octokit.Reactive/ObservableGitHubClient.cs index 8b251ecf84..cba117f23c 100644 --- a/Octokit.Reactive/ObservableGitHubClient.cs +++ b/Octokit.Reactive/ObservableGitHubClient.cs @@ -1,5 +1,4 @@ using System; -using System.Net.Http.Headers; namespace Octokit.Reactive { @@ -37,6 +36,7 @@ public ObservableGitHubClient(IGitHubClient gitHubClient) Issue = new ObservableIssuesClient(gitHubClient); Miscellaneous = new ObservableMiscellaneousClient(gitHubClient.Miscellaneous); Notification = new ObservableNotificationsClient(gitHubClient); + Oauth = new ObservableOauthClient(gitHubClient); Organization = new ObservableOrganizationsClient(gitHubClient); Repository = new ObservableRepositoriesClient(gitHubClient); SshKey = new ObservableSshKeysClient(gitHubClient); @@ -56,6 +56,7 @@ public IConnection Connection public IObservableActivitiesClient Activity { get; private set; } public IObservableIssuesClient Issue { get; private set; } public IObservableMiscellaneousClient Miscellaneous { get; private set; } + public IObservableOauthClient Oauth { get; private set; } public IObservableOrganizationsClient Organization { get; private set; } public IObservableRepositoriesClient Repository { get; private set; } public IObservableGistsClient Gist { get; private set; } diff --git a/Octokit.Reactive/Octokit.Reactive.csproj b/Octokit.Reactive/Octokit.Reactive.csproj index 801fbd704e..453b88aea3 100644 --- a/Octokit.Reactive/Octokit.Reactive.csproj +++ b/Octokit.Reactive/Octokit.Reactive.csproj @@ -73,7 +73,9 @@ Properties\SolutionInfo.cs + + @@ -161,7 +163,6 @@ - From 14d75807658665d911b734929163b14ea95a041c Mon Sep 17 00:00:00 2001 From: Haacked Date: Tue, 22 Apr 2014 14:25:57 -0700 Subject: [PATCH 6/9] Fix up the mono projects --- Octokit.Reactive/Octokit.Reactive-Mono.csproj | 2 ++ Octokit.Reactive/Octokit.Reactive-MonoAndroid.csproj | 2 ++ Octokit.Reactive/Octokit.Reactive-Monotouch.csproj | 2 ++ 3 files changed, 6 insertions(+) diff --git a/Octokit.Reactive/Octokit.Reactive-Mono.csproj b/Octokit.Reactive/Octokit.Reactive-Mono.csproj index 3882f838b4..911ea6eb7d 100644 --- a/Octokit.Reactive/Octokit.Reactive-Mono.csproj +++ b/Octokit.Reactive/Octokit.Reactive-Mono.csproj @@ -143,6 +143,8 @@ + + diff --git a/Octokit.Reactive/Octokit.Reactive-MonoAndroid.csproj b/Octokit.Reactive/Octokit.Reactive-MonoAndroid.csproj index acf6bfb085..c00d82cda0 100644 --- a/Octokit.Reactive/Octokit.Reactive-MonoAndroid.csproj +++ b/Octokit.Reactive/Octokit.Reactive-MonoAndroid.csproj @@ -152,6 +152,8 @@ + + diff --git a/Octokit.Reactive/Octokit.Reactive-Monotouch.csproj b/Octokit.Reactive/Octokit.Reactive-Monotouch.csproj index 5996e3ff11..5c6cf52b50 100644 --- a/Octokit.Reactive/Octokit.Reactive-Monotouch.csproj +++ b/Octokit.Reactive/Octokit.Reactive-Monotouch.csproj @@ -147,6 +147,8 @@ + + From cc404b739c7dbaad4bbc1c6eff0c381c0259bb69 Mon Sep 17 00:00:00 2001 From: Haacked Date: Tue, 22 Apr 2014 15:03:17 -0700 Subject: [PATCH 7/9] Return Uri to appease convention tests --- .../Clients/IObservableOauthClient.cs | 2 +- .../Clients/ObservableOauthClient.cs | 21 +++++++++++++++++-- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/Octokit.Reactive/Clients/IObservableOauthClient.cs b/Octokit.Reactive/Clients/IObservableOauthClient.cs index 01f1d6f10a..2afa944c00 100644 --- a/Octokit.Reactive/Clients/IObservableOauthClient.cs +++ b/Octokit.Reactive/Clients/IObservableOauthClient.cs @@ -9,7 +9,7 @@ public interface IObservableOauthClient /// /// Parameters to the Oauth web flow login url /// - IObservable GetGitHubLoginUrl(OauthLoginRequest request); + Uri GetGitHubLoginUrl(OauthLoginRequest request); /// /// Makes a request to get an access token using the code returned when GitHub.com redirects back from the URL diff --git a/Octokit.Reactive/Clients/ObservableOauthClient.cs b/Octokit.Reactive/Clients/ObservableOauthClient.cs index cd73a94f88..db62a7e13d 100644 --- a/Octokit.Reactive/Clients/ObservableOauthClient.cs +++ b/Octokit.Reactive/Clients/ObservableOauthClient.cs @@ -15,11 +15,28 @@ public ObservableOauthClient(IGitHubClient client) _client = client; } - public IObservable GetGitHubLoginUrl(OauthLoginRequest request) + /// + /// Gets the URL used in the first step of the web flow. The Web application should redirect to this URL. + /// + /// Parameters to the Oauth web flow login url + /// + public Uri GetGitHubLoginUrl(OauthLoginRequest request) { - return Observable.Return(_client.Oauth.GetGitHubLoginUrl(request)); + return _client.Oauth.GetGitHubLoginUrl(request); } + /// + /// Makes a request to get an access token using the code returned when GitHub.com redirects back from the URL + /// GitHub login url to the application. + /// + /// + /// If the user accepts your request, GitHub redirects back to your site with a temporary code in a code + /// parameter as well as the state you provided in the previous step in a state parameter. If the states don’t + /// match, the request has been created by a third party and the process should be aborted. Exchange this for + /// an access token using this method. + /// + /// + /// public IObservable CreateAccessToken(OauthTokenRequest request) { return _client.Oauth.CreateAccessToken(request).ToObservable(); From 54bb948713a2dcb67c7e13cf2b46be82e80d08bb Mon Sep 17 00:00:00 2001 From: Haacked Date: Tue, 22 Apr 2014 15:15:58 -0700 Subject: [PATCH 8/9] Add DebuggerDisplay attributes --- Octokit/Models/Request/OauthLoginRequest.cs | 14 ++++++++++++++ Octokit/Models/Request/OauthTokenRequest.cs | 15 +++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/Octokit/Models/Request/OauthLoginRequest.cs b/Octokit/Models/Request/OauthLoginRequest.cs index 3bfc6d40c1..501d0cbf2f 100644 --- a/Octokit/Models/Request/OauthLoginRequest.cs +++ b/Octokit/Models/Request/OauthLoginRequest.cs @@ -1,9 +1,12 @@ using System; using System.Collections.ObjectModel; +using System.Diagnostics; +using System.Globalization; using Octokit.Internal; namespace Octokit { + [DebuggerDisplay("{DebuggerDisplay,nq}")] public class OauthLoginRequest : RequestParameters { /// @@ -53,5 +56,16 @@ public OauthLoginRequest(string clientId) /// [Parameter(Key = "state")] public string State { get; set; } + + internal string DebuggerDisplay + { + get + { + return String.Format(CultureInfo.InvariantCulture, "ClientId: {0}, RedirectUri: {1}, Scopes: {2}", + ClientId, + RedirectUri, + Scopes); + } + } } } diff --git a/Octokit/Models/Request/OauthTokenRequest.cs b/Octokit/Models/Request/OauthTokenRequest.cs index 437e0ce290..d142462b74 100644 --- a/Octokit/Models/Request/OauthTokenRequest.cs +++ b/Octokit/Models/Request/OauthTokenRequest.cs @@ -1,8 +1,11 @@ using System; +using System.Diagnostics; +using System.Globalization; using Octokit.Internal; namespace Octokit { + [DebuggerDisplay("{DebuggerDisplay,nq}")] public class OauthTokenRequest : RequestParameters { /// @@ -50,5 +53,17 @@ public OauthTokenRequest(string clientId, string clientSecret, string code) /// [Parameter(Key = "redirect_uri")] public Uri RedirectUri { get; set; } + + internal string DebuggerDisplay + { + get + { + return String.Format(CultureInfo.InvariantCulture, "ClientId: {0}, ClientSecret: {1}, Code: {2}, RedirectUri: {3}", + ClientId, + ClientSecret, + Code, + RedirectUri); + } + } } } From 3ddca30acd44c62fba4325c3d90910a470c39af9 Mon Sep 17 00:00:00 2001 From: Haacked Date: Tue, 22 Apr 2014 15:18:31 -0700 Subject: [PATCH 9/9] Add comments and debugger display --- Octokit/Models/Response/OauthToken.cs | 28 ++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/Octokit/Models/Response/OauthToken.cs b/Octokit/Models/Response/OauthToken.cs index 6c2d46e54e..5a4c9244eb 100644 --- a/Octokit/Models/Response/OauthToken.cs +++ b/Octokit/Models/Response/OauthToken.cs @@ -1,11 +1,37 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; namespace Octokit { + [DebuggerDisplay("{DebuggerDisplay,nq}")] public class OauthToken { + /// + /// The type of OAuth token + /// public string TokenType { get; set; } + + /// + /// The secret OAuth access token. Use this to authenticate Octokit.net's client. + /// public string AccessToken { get; set; } + + /// + /// The list of scopes the token includes. + /// public IReadOnlyCollection Scope { get; set; } + + internal string DebuggerDisplay + { + get + { + return String.Format(CultureInfo.InvariantCulture, "TokenType: {0}, AccessToken: {1}, Scopes: {2}", + TokenType, + AccessToken, + Scope); + } + } } }