diff --git a/Octokit.Reactive/Clients/ObservableAuthorizationsClient.cs b/Octokit.Reactive/Clients/ObservableAuthorizationsClient.cs index 88c26312..20b6d1a3 100644 --- a/Octokit.Reactive/Clients/ObservableAuthorizationsClient.cs +++ b/Octokit.Reactive/Clients/ObservableAuthorizationsClient.cs @@ -25,6 +25,59 @@ namespace Octokit.Reactive.Clients { return _client.Get(id).ToObservable(); } + /// + /// This method will create a new authorization for the specified OAuth application, only if an authorization + /// for that application doesn’t already exist for the user. It returns the user’s token for the application + /// if one exists. Otherwise, it creates one. + /// + /// Client ID for the OAuth application that is requesting the token. + /// The client secret + /// Definse the scopes and metadata for the token + /// Thrown when the user does not have permission to make + /// this request. Check + /// + public IObservable GetOrCreateApplicationAuthentication( + string clientId, + string clientSecret, + AuthorizationUpdate authorization) + { + Ensure.ArgumentNotNullOrEmptyString(clientId, "clientId"); + Ensure.ArgumentNotNullOrEmptyString(clientSecret, "clientSecret"); + Ensure.ArgumentNotNull(authorization, "authorization"); + + return _client.GetOrCreateApplicationAuthentication(clientId, clientSecret, authorization) + .ToObservable(); + } + + /// + /// This method will create a new authorization for the specified OAuth application, only if an authorization + /// for that application doesn’t already exist for the user. It returns the user’s token for the application + /// if one exists. Otherwise, it creates one. + /// + /// Client ID for the OAuth application that is requesting the token. + /// The client secret + /// Defines the scopes and metadata for the token + /// + /// Thrown when the user does not have permission to make + /// this request. Check + /// Thrown when the two-factor code is not + /// valid. + /// + + public IObservable GetOrCreateApplicationAuthentication( + string clientId, + string clientSecret, + AuthorizationUpdate authorization, + string twoFactorAuthenticationCode) + { + Ensure.ArgumentNotNullOrEmptyString(clientId, "clientId"); + Ensure.ArgumentNotNullOrEmptyString(clientSecret, "clientSecret"); + Ensure.ArgumentNotNull(authorization, "authorization"); + Ensure.ArgumentNotNullOrEmptyString(twoFactorAuthenticationCode, "twoFactorAuthenticationCode"); + + return _client.GetOrCreateApplicationAuthentication(clientId, clientSecret, authorization, twoFactorAuthenticationCode) + .ToObservable(); + } public IObservable Update(int id, AuthorizationUpdate authorization) { diff --git a/Octokit.Reactive/Helpers/AuthorizationExtensions.cs b/Octokit.Reactive/Helpers/AuthorizationExtensions.cs new file mode 100644 index 00000000..88bca520 --- /dev/null +++ b/Octokit.Reactive/Helpers/AuthorizationExtensions.cs @@ -0,0 +1,37 @@ +using System; +using System.Reactive.Linq; +using Octokit.Reactive; + +namespace Octokit +{ + public static class AuthorizationExtensions + { + public static IObservable GetOrCreateApplicationAuthentication( + this IObservableAuthorizationsClient authorizationsClient, + string clientId, + string clientSecret, + AuthorizationUpdate authorization, + Func> twoFactorChallengeHandler + ) + { + Ensure.ArgumentNotNull(authorizationsClient, "authorizationsClient"); + Ensure.ArgumentNotNullOrEmptyString(clientId, "clientId"); + Ensure.ArgumentNotNullOrEmptyString(clientSecret, "clientSecret"); + Ensure.ArgumentNotNull(authorization, "authorization"); + + return authorizationsClient.GetOrCreateApplicationAuthentication(clientId, clientSecret, authorization) + .Catch(exception => twoFactorChallengeHandler(exception) + .SelectMany(result => + result.ResendCodeRequested + ? authorizationsClient.GetOrCreateApplicationAuthentication( + clientId, + clientSecret, + authorization, + twoFactorChallengeHandler) + : authorizationsClient.GetOrCreateApplicationAuthentication(clientId, + clientSecret, + authorization, + result.AuthenticationCode))); + } + } +} diff --git a/Octokit.Reactive/IObservableAuthorizationsClient.cs b/Octokit.Reactive/IObservableAuthorizationsClient.cs index 1d46eb9f..62c46acd 100644 --- a/Octokit.Reactive/IObservableAuthorizationsClient.cs +++ b/Octokit.Reactive/IObservableAuthorizationsClient.cs @@ -13,6 +13,44 @@ namespace Octokit.Reactive [SuppressMessage("Microsoft.Naming", "CA1716:IdentifiersShouldNotMatchKeywords", MessageId = "Get", Justification = "It's fiiiine. It's fine. Trust us.")] IObservable Get(int id); + + /// + /// This method will create a new authorization for the specified OAuth application, only if an authorization + /// for that application doesn’t already exist for the user. It returns the user’s token for the application + /// if one exists. Otherwise, it creates one. + /// + /// Client ID for the OAuth application that is requesting the token. + /// The client secret + /// Defines the scopes and metadata for the token + /// Thrown when the user does not have permission to make + /// this request. Check + /// Thrown when the current account has two-factor + /// authentication enabled. + /// + IObservable GetOrCreateApplicationAuthentication( + string clientId, + string clientSecret, + AuthorizationUpdate authorization); + + /// + /// This method will create a new authorization for the specified OAuth application, only if an authorization + /// for that application doesn’t already exist for the user. It returns the user’s token for the application + /// if one exists. Otherwise, it creates one. + /// + /// Client ID for the OAuth application that is requesting the token. + /// The client secret + /// Defines the scopes and metadata for the token + /// + /// Thrown when the user does not have permission to make + /// this request. Check + /// Thrown when the two-factor code is not + /// valid. + /// + IObservable GetOrCreateApplicationAuthentication( + string clientId, + string clientSecret, + AuthorizationUpdate authorization, + string twoFactorAuthenticationCode); IObservable Update(int id, AuthorizationUpdate authorization); IObservable Create(AuthorizationUpdate authorization); IObservable Delete(int id); diff --git a/Octokit.Reactive/Octokit.Reactive.csproj b/Octokit.Reactive/Octokit.Reactive.csproj index 10141099..637f7c51 100644 --- a/Octokit.Reactive/Octokit.Reactive.csproj +++ b/Octokit.Reactive/Octokit.Reactive.csproj @@ -89,6 +89,7 @@ + diff --git a/Octokit.Tests/Clients/AuthorizationsClientTests.cs b/Octokit.Tests/Clients/AuthorizationsClientTests.cs index 35f52a70..7d014a1d 100644 --- a/Octokit.Tests/Clients/AuthorizationsClientTests.cs +++ b/Octokit.Tests/Clients/AuthorizationsClientTests.cs @@ -1,6 +1,8 @@ using System; +using System.Collections.Generic; +using System.Threading.Tasks; using NSubstitute; -using Octokit.Internal; +using Octokit.Tests.Helpers; using Xunit; namespace Octokit.Tests.Clients @@ -91,5 +93,93 @@ namespace Octokit.Tests.Clients client.Received().Delete(Arg.Is(u => u.ToString() == "/authorizations/1")); } } + + public class TheGetOrCreateApplicationAuthenticationMethod + { + [Fact] + public void GetsOrCreatesAuthenticationAtCorrectUrl() + { + var data = new AuthorizationUpdate(); + var client = Substitute.For>(); + var authEndpoint = new AuthorizationsClient(client); + + authEndpoint.GetOrCreateApplicationAuthentication("clientId", "secret", data); + + client.Received().GetOrCreate(Arg.Is(u => u.ToString() == "/authorizations/clients/clientId"), + Args.Object); + } + + [Fact] + public async Task WrapsTwoFactorFailureWithTwoFactorException() + { + var data = new AuthorizationUpdate(); + var client = Substitute.For>(); + client.GetOrCreate(Args.Uri, Args.Object, Args.String).Returns(_ => {throw new AuthorizationException();}); + var authEndpoint = new AuthorizationsClient(client); + + AssertEx.Throws(async () => + await authEndpoint.GetOrCreateApplicationAuthentication("clientId", "secret", data)); + } + + [Fact] + public async Task UsesCallbackToRetrieveTwoFactorCode() + { + var twoFactorChallengeResult = new TwoFactorChallengeResult("two-factor-code"); + var data = new AuthorizationUpdate { Note = "note" }; + var client = Substitute.For(); + client.GetOrCreateApplicationAuthentication("clientId", "secret", Arg.Any()) + .Returns(_ => {throw new TwoFactorRequiredException();}); + client.GetOrCreateApplicationAuthentication("clientId", + "secret", + Arg.Any(), + "two-factor-code") + .Returns(Task.Factory.StartNew(() => new Authorization {Token = "xyz"})); + + var result = await client.GetOrCreateApplicationAuthentication("clientId", + "secret", + data, + e => Task.Factory.StartNew(() => twoFactorChallengeResult)); + + client.Received().GetOrCreateApplicationAuthentication("clientId", + "secret", + Arg.Is(u => u.Note == "note")); + client.Received().GetOrCreateApplicationAuthentication("clientId", + "secret", + Arg.Any(), "two-factor-code"); + Assert.Equal("xyz", result.Token); + } + + [Fact] + public async Task RetriesWhenResendRequested() + { + var challengeResults = new Queue(new [] + { + TwoFactorChallengeResult.RequestResendCode, + new TwoFactorChallengeResult("two-factor-code") + }); + var data = new AuthorizationUpdate(); + var client = Substitute.For(); + client.GetOrCreateApplicationAuthentication("clientId", "secret", Arg.Any()) + .Returns(_ => { throw new TwoFactorRequiredException(); }); + client.GetOrCreateApplicationAuthentication("clientId", + "secret", + Arg.Any(), + "two-factor-code") + .Returns(Task.Factory.StartNew(() => new Authorization { Token = "xyz" })); + + var result = await client.GetOrCreateApplicationAuthentication("clientId", + "secret", + data, + e => Task.Factory.StartNew(() => challengeResults.Dequeue())); + + client.Received().GetOrCreateApplicationAuthentication("clientId", + "secret", + Arg.Any()); + client.Received().GetOrCreateApplicationAuthentication("clientId", + "secret", + Arg.Any(), "two-factor-code"); + Assert.Equal("xyz", result.Token); + } + } } } diff --git a/Octokit.Tests/Http/ConnectionTests.cs b/Octokit.Tests/Http/ConnectionTests.cs index ef8d3378..f005f37d 100644 --- a/Octokit.Tests/Http/ConnectionTests.cs +++ b/Octokit.Tests/Http/ConnectionTests.cs @@ -9,6 +9,7 @@ using NSubstitute; using Octokit.Internal; using Octokit.Tests.Helpers; using Xunit; +using Xunit.Extensions; namespace Octokit.Tests.Http { @@ -106,6 +107,63 @@ namespace Octokit.Tests.Http "oauth token.", exception.Message); } + [Theory] + [InlineData("missing", "")] + [InlineData("missing", "required; sms")] + [InlineData("X-GitHub-OTP", "blah")] + [InlineData("X-GitHub-OTP", "foo; sms")] + public async Task ThrowsUnauthorizedExceptionExceptionWhenChallengedWithBadHeader( + string headerKey, + string otpHeaderValue) + { + var httpClient = Substitute.For(); + IResponse response = new ApiResponse { StatusCode = HttpStatusCode.Unauthorized }; + response.Headers[headerKey] = otpHeaderValue; + httpClient.Send(Args.Request).Returns(Task.FromResult(response)); + var connection = new Connection("Test Runner User Agent", + ExampleUri, + Substitute.For(), + httpClient, + Substitute.For()); + + var exception = await AssertEx.Throws( + async () => await connection.GetAsync(new Uri("/endpoint", UriKind.Relative))); + Assert.Equal("You must be authenticated to call this method. Either supply a login/password or an " + + "oauth token.", exception.Message); + } + + [Theory] + [InlineData("X-GitHub-OTP", "required", TwoFactorType.Unknown)] + [InlineData("X-GitHub-OTP", "required;", TwoFactorType.Unknown)] + [InlineData("X-GitHub-OTP", "required; poo", TwoFactorType.Unknown)] + [InlineData("X-GitHub-OTP", "required; app", TwoFactorType.AuthenticatorApp)] + [InlineData("X-GitHub-OTP", "required; sms", TwoFactorType.Sms)] + [InlineData("x-github-otp", "required; sms", TwoFactorType.Sms)] + public async Task ThrowsTwoFactorExceptionExceptionWhenChallenged( + string headerKey, + string otpHeaderValue, + TwoFactorType expectedFactorType) + { + var httpClient = Substitute.For(); + IResponse response = new ApiResponse + { + StatusCode = HttpStatusCode.Unauthorized, + }; + response.Headers[headerKey] = otpHeaderValue; + httpClient.Send(Args.Request).Returns(Task.FromResult(response)); + var connection = new Connection("Test Runner User Agent", + ExampleUri, + Substitute.For(), + httpClient, + Substitute.For()); + + var exception = await AssertEx.Throws( + async () => await connection.GetAsync(new Uri("/endpoint", UriKind.Relative))); + + Assert.Equal("Two-factor authentication required", exception.Message); + Assert.Equal(expectedFactorType, exception.TwoFactorType); + } + [Fact] public async Task ThrowsApiValidationExceptionFor422Response() { diff --git a/Octokit/Clients/AuthorizationsClient.cs b/Octokit/Clients/AuthorizationsClient.cs index 99f912d0..019933e3 100644 --- a/Octokit/Clients/AuthorizationsClient.cs +++ b/Octokit/Clients/AuthorizationsClient.cs @@ -2,6 +2,7 @@ #if NET_45 using System.Collections.Generic; #endif +using System.Collections; using System.Threading.Tasks; using Octokit.Internal; @@ -35,6 +36,69 @@ namespace Octokit return await Client.Get(endpoint); } + /// + /// This method will create a new authorization for the specified OAuth application, only if an authorization + /// for that application doesn’t already exist for the user. It returns the user’s token for the application + /// if one exists. Otherwise, it creates one. + /// + /// Client ID for the OAuth application that is requesting the token. + /// The client secret + /// Definse the scopes and metadata for the token + /// + public async Task GetOrCreateApplicationAuthentication( + string clientId, + string clientSecret, + AuthorizationUpdate authorization) + { + Ensure.ArgumentNotNullOrEmptyString(clientId, "clientId"); + Ensure.ArgumentNotNullOrEmptyString(clientSecret, "clientSecret"); + Ensure.ArgumentNotNull(authorization, "authorization"); + + var endpoint = "/authorizations/clients/{0}".FormatUri(clientId); + var requestData = new + { + client_secret = clientSecret, + scopes = authorization.Scopes, + note = authorization.Note, + note_url = authorization.NoteUrl + }; + + return await Client.GetOrCreate(endpoint, requestData); + } + + public async Task GetOrCreateApplicationAuthentication( + string clientId, + string clientSecret, + AuthorizationUpdate authorization, + string twoFactorAuthenticationCode) + { + Ensure.ArgumentNotNullOrEmptyString(clientId, "clientId"); + Ensure.ArgumentNotNullOrEmptyString(clientSecret, "clientSecret"); + Ensure.ArgumentNotNull(authorization, "authorization"); + Ensure.ArgumentNotNullOrEmptyString(twoFactorAuthenticationCode, "twoFactorAuthenticationCode"); + + var endpoint = "/authorizations/clients/{0}".FormatUri(clientId); + var requestData = new + { + client_secret = clientSecret, + scopes = authorization.Scopes, + note = authorization.Note, + note_url = authorization.NoteUrl + }; + + try + { + return await Client.GetOrCreate( + endpoint, + requestData, + twoFactorAuthenticationCode); + } + catch (AuthorizationException e) + { + throw new TwoFactorChallengeFailedException("Two-Factor Authentication code is not valid", e); + } + } + /// /// Update the specified . /// diff --git a/Octokit/Exceptions/AuthorizationException.cs b/Octokit/Exceptions/AuthorizationException.cs index 55e3e616..910435e0 100644 --- a/Octokit/Exceptions/AuthorizationException.cs +++ b/Octokit/Exceptions/AuthorizationException.cs @@ -34,3 +34,42 @@ namespace Octokit #endif } } + + +/* + [Fact] + public void CreatesGitHubErrorFromJsonResponse() + { + var exception = new ApiUnauthorizedWebException("{\"message\":\"Bad credentials.\"}"); + + exception.ApiUnauthorizedError.Message.ShouldEqual("Bad credentials."); + exception.ApiUnauthorizedError.Errors.ShouldBeNull(); + } + + [Theory] + [InlineData("")] + [InlineData(null)] + [InlineData("{{{{{")] + public void CreatesGitHubErrorIfResponseMessageIsNotValidJson(string responseContent) + { + var exception = new ApiUnauthorizedWebException(responseContent); + + exception.ApiUnauthorizedError.Message.ShouldEqual(responseContent); + Assert.False(exception.RequiresSecondFactor); + } + + [Fact] + public void CanPopulateObjectFromSerializedData() + { + var exception = new ApiUnauthorizedWebException("{message:\"Bad credentials.\"}"); + using (var stream = new MemoryStream()) + { + var formatter = new BinaryFormatter(); + formatter.Serialize(stream, exception); + stream.Position = 0; + var deserialized = (ApiUnauthorizedWebException)formatter.Deserialize(stream); + deserialized.ApiUnauthorizedError.Message.ShouldEqual("Bad credentials."); + exception.ApiUnauthorizedError.Errors.ShouldBeNull(); + } + } +*/ \ No newline at end of file diff --git a/Octokit/Exceptions/TwoFactorChallengeFailedException.cs b/Octokit/Exceptions/TwoFactorChallengeFailedException.cs new file mode 100644 index 00000000..d870590f --- /dev/null +++ b/Octokit/Exceptions/TwoFactorChallengeFailedException.cs @@ -0,0 +1,31 @@ +using System; +using System.Runtime.Serialization; + +namespace Octokit +{ +#if !NETFX_CORE + [Serializable] +#endif + public class TwoFactorChallengeFailedException : AuthorizationException + { + public TwoFactorChallengeFailedException() + { + } + + public TwoFactorChallengeFailedException(string message) : base(message) + { + } + + public TwoFactorChallengeFailedException(string message, Exception innerException) + : base(message, innerException) + { + } + +#if !NETFX_CORE + protected TwoFactorChallengeFailedException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + } +#endif + } +} diff --git a/Octokit/Exceptions/TwoFactorRequiredException.cs b/Octokit/Exceptions/TwoFactorRequiredException.cs new file mode 100644 index 00000000..b2b83585 --- /dev/null +++ b/Octokit/Exceptions/TwoFactorRequiredException.cs @@ -0,0 +1,55 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.Serialization; + +namespace Octokit +{ +#if !NETFX_CORE + [Serializable] +#endif + public class TwoFactorRequiredException : AuthorizationException + { + public TwoFactorRequiredException() + { + } + + public TwoFactorRequiredException(string message) : base(message) + { + } + + public TwoFactorRequiredException(string message, Exception innerException) : base(message, innerException) + { + } + + public TwoFactorRequiredException(string message, TwoFactorType twoFactorType) + : base(message) + { + TwoFactorType = twoFactorType; + } + +#if !NETFX_CORE + protected TwoFactorRequiredException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + if (info == null) return; + TwoFactorType = (TwoFactorType) (info.GetInt32("TwoFactorType")); + } + + public override void GetObjectData(SerializationInfo info, StreamingContext context) + { + base.GetObjectData(info, context); + info.AddValue("TwoFactorType", TwoFactorType); + } +#endif + + public TwoFactorType TwoFactorType { get; private set; } + } + + public enum TwoFactorType + { + None, + Unknown, + [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Sms")] Sms, + AuthenticatorApp + } +} diff --git a/Octokit/Helpers/AuthorizationExtensions.cs b/Octokit/Helpers/AuthorizationExtensions.cs new file mode 100644 index 00000000..191b4a4e --- /dev/null +++ b/Octokit/Helpers/AuthorizationExtensions.cs @@ -0,0 +1,41 @@ +using System; +using System.Threading.Tasks; + +namespace Octokit +{ + public static class AuthorizationExtensions + { + public static async Task GetOrCreateApplicationAuthentication( + this IAuthorizationsClient authorizationsClient, + string clientId, + string clientSecret, + AuthorizationUpdate authorization, + Func> twoFactorChallengeHandler + ) + { + TwoFactorRequiredException twoFactorException = null; + try + { + return await authorizationsClient.GetOrCreateApplicationAuthentication(clientId, clientSecret, authorization); + + } + catch (TwoFactorRequiredException exception) + { + twoFactorException = exception; + } + var twoFactorChallengeResult = await twoFactorChallengeHandler(twoFactorException); + + return await (twoFactorChallengeResult.ResendCodeRequested + ? authorizationsClient.GetOrCreateApplicationAuthentication( + clientId, + clientSecret, + authorization, + twoFactorChallengeHandler) + : authorizationsClient.GetOrCreateApplicationAuthentication(clientId, + clientSecret, + authorization, + twoFactorChallengeResult.AuthenticationCode)); + + } + } +} diff --git a/Octokit/Helpers/TwoFactorChallengeResult.cs b/Octokit/Helpers/TwoFactorChallengeResult.cs new file mode 100644 index 00000000..7b0d60a6 --- /dev/null +++ b/Octokit/Helpers/TwoFactorChallengeResult.cs @@ -0,0 +1,27 @@ +using System.Diagnostics.CodeAnalysis; + +namespace Octokit +{ + public class TwoFactorChallengeResult + { + [SuppressMessage("Microsoft.Security", "CA2104:DoNotDeclareReadOnlyMutableReferenceTypes", + Justification = "It really is immutable yo!")] + public static readonly TwoFactorChallengeResult RequestResendCode = new TwoFactorChallengeResult(null, true); + + public TwoFactorChallengeResult(string authenticationCode) + : this(authenticationCode, false) + { + Ensure.ArgumentNotNull(authenticationCode, "authenticationCode"); + } + + TwoFactorChallengeResult(string authenticationCode, bool resendCodeRequested) + { + AuthenticationCode = authenticationCode; + ResendCodeRequested = resendCodeRequested; + } + + public bool ResendCodeRequested { get; private set; } + + public string AuthenticationCode { get; private set; } + } +} diff --git a/Octokit/Http/ApiConnection.cs b/Octokit/Http/ApiConnection.cs index 49a95a8d..71f5a0e8 100644 --- a/Octokit/Http/ApiConnection.cs +++ b/Octokit/Http/ApiConnection.cs @@ -65,6 +65,27 @@ namespace Octokit return response.BodyAsObject; } + public async Task GetOrCreate(Uri endpoint, object data) + { + Ensure.ArgumentNotNull(endpoint, "endpoint"); + Ensure.ArgumentNotNull(data, "data"); + + var response = await Connection.PutAsync(endpoint, data); + + return response.BodyAsObject; + } + + public async Task GetOrCreate(Uri endpoint, object data, string twoFactorAuthenticationCode) + { + Ensure.ArgumentNotNull(endpoint, "endpoint"); + Ensure.ArgumentNotNull(data, "data"); + Ensure.ArgumentNotNullOrEmptyString(twoFactorAuthenticationCode, "twoFactorAuthenticationCode"); + + var response = await Connection.PutAsync(endpoint, data, twoFactorAuthenticationCode); + + return response.BodyAsObject; + } + public async Task Update(Uri endpoint, object data) { Ensure.ArgumentNotNull(endpoint, "endpoint"); diff --git a/Octokit/Http/Connection.cs b/Octokit/Http/Connection.cs index f3b6d5e5..d45fb62b 100644 --- a/Octokit/Http/Connection.cs +++ b/Octokit/Http/Connection.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Globalization; +using System.Linq; using System.Net; using System.Net.Http; using System.Threading.Tasks; @@ -120,12 +121,18 @@ namespace Octokit return await SendData(endpoint, HttpMethod.Put, body); } + public async Task> PutAsync(Uri endpoint, object body, string twoFactorAuthenticationCode) + { + return await SendData(endpoint, HttpMethod.Put, body, twoFactorAuthenticationCode); + } + async Task> SendData( Uri endpoint, HttpMethod method, object body, string contentType = "application/x-www-form-urlencoded", // Per: http://developer.github.com/v3/ - string accepts = null + string accepts = null, + string twoFactorAuthenticationCode = null ) { Ensure.ArgumentNotNull(endpoint, "endpoint"); @@ -142,6 +149,11 @@ namespace Octokit request.Headers["Accept"] = accepts; } + if (!String.IsNullOrEmpty(twoFactorAuthenticationCode)) + { + request.Headers["X-GitHub-OTP"] = twoFactorAuthenticationCode; + } + if (body != null) { request.Body = body; @@ -225,8 +237,14 @@ namespace Octokit static void HandleErrors(IResponse response) { if (response.StatusCode == HttpStatusCode.Unauthorized) - throw new AuthorizationException("You must be authenticated to call this method. Either supply a " + - "login/password or an oauth token."); + { + var twoFactorType = ParseTwoFactorType(response); + + throw twoFactorType == TwoFactorType.None + ? new AuthorizationException("You must be authenticated to call this method. Either supply a " + + "login/password or an oauth token.") + : new TwoFactorRequiredException("Two-factor authentication required", twoFactorType); + } if (response.StatusCode == HttpStatusCode.Forbidden) { @@ -248,5 +266,29 @@ namespace Octokit throw new ApiException(response.Body, response.StatusCode); } } + + static TwoFactorType ParseTwoFactorType(IResponse restResponse) + { + if (restResponse.Headers == null || !restResponse.Headers.Any()) return TwoFactorType.None; + var otpHeader = restResponse.Headers.FirstOrDefault(header => + header.Key.Equals("X-GitHub-OTP", StringComparison.OrdinalIgnoreCase)); + if (String.IsNullOrEmpty(otpHeader.Value)) return TwoFactorType.None; + var factorType = otpHeader.Value; + var parts = factorType.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries); + if (parts.Length > 0 && parts[0] == "required") + { + var secondPart = parts.Length > 1 ? parts[1].Trim() : null; + switch (secondPart) + { + case "sms": + return TwoFactorType.Sms; + case "app": + return TwoFactorType.AuthenticatorApp; + default: + return TwoFactorType.Unknown; + } + } + return TwoFactorType.None; + } } } diff --git a/Octokit/Http/IApiConnection.cs b/Octokit/Http/IApiConnection.cs index c84c7afc..cfc4dd26 100644 --- a/Octokit/Http/IApiConnection.cs +++ b/Octokit/Http/IApiConnection.cs @@ -19,6 +19,8 @@ namespace Octokit Task GetHtml(Uri endpoint, IDictionary parameters); Task> GetAll(Uri endpoint, IDictionary parameters); Task Create(Uri endpoint, object data); + Task GetOrCreate(Uri endpoint, object data); + Task GetOrCreate(Uri endpoint, object data, string twoFactorAuthenticationCode); Task Update(Uri endpoint, object data); Task Delete(Uri endpoint); Task Upload(Uri uri, Stream rawData, string contentType); diff --git a/Octokit/Http/IConnection.cs b/Octokit/Http/IConnection.cs index 113a6af3..10a79414 100644 --- a/Octokit/Http/IConnection.cs +++ b/Octokit/Http/IConnection.cs @@ -14,6 +14,7 @@ namespace Octokit Task> PostAsync(Uri endpoint, object body); Task> PostAsync(Uri endpoint, object body, string contentType, string accepts); Task> PutAsync(Uri endpoint, object body); + Task> PutAsync(Uri endpoint, object body, string twoFactorAuthenticationCode); [SuppressMessage("Microsoft.Design", "CA1004:GenericMethodsShouldProvideTypeParameter")] Task DeleteAsync(Uri endpoint); diff --git a/Octokit/IAuthorizationsClient.cs b/Octokit/IAuthorizationsClient.cs index 68c4929e..3af84c7a 100644 --- a/Octokit/IAuthorizationsClient.cs +++ b/Octokit/IAuthorizationsClient.cs @@ -9,9 +9,50 @@ namespace Octokit [SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate", Justification = "It's an API call, so it's not a property.")] Task> GetAll(); + /// + /// Get a specific for the authenticated user. This method requires basic auth. + /// + /// The id of the . + /// An [SuppressMessage("Microsoft.Naming", "CA1716:IdentifiersShouldNotMatchKeywords", MessageId = "Get", Justification = "It's fiiiine. It's fine. Trust us.")] Task Get(int id); + /// + /// This method will create a new authorization for the specified OAuth application, only if an authorization + /// for that application doesn’t already exist for the user. It returns the user’s token for the application + /// if one exists. Otherwise, it creates one. + /// + /// Client ID for the OAuth application that is requesting the token. + /// The client secret + /// Defines the scopes and metadata for the token + /// Thrown when the user does not have permission to make + /// this request. Check + /// Thrown when the current account has two-factor + /// authentication enabled. + /// + Task GetOrCreateApplicationAuthentication( + string clientId, + string clientSecret, + AuthorizationUpdate authorization); + /// + /// This method will create a new authorization for the specified OAuth application, only if an authorization + /// for that application doesn’t already exist for the user. It returns the user’s token for the application + /// if one exists. Otherwise, it creates one. + /// + /// Client ID for the OAuth application that is requesting the token. + /// The client secret + /// Defines the scopes and metadata for the token + /// + /// Thrown when the user does not have permission to make + /// this request. Check + /// Thrown when the two-factor code is not + /// valid. + /// + Task GetOrCreateApplicationAuthentication( + string clientId, + string clientSecret, + AuthorizationUpdate authorization, + string twoFactorAuthenticationCode); Task Update(int id, AuthorizationUpdate authorization); Task Create(AuthorizationUpdate authorization); Task Delete(int id); diff --git a/Octokit/Octokit.csproj b/Octokit/Octokit.csproj index 8ff61081..95c0a5d8 100644 --- a/Octokit/Octokit.csproj +++ b/Octokit/Octokit.csproj @@ -78,6 +78,10 @@ + + + + diff --git a/Octokit/OctokitRT.csproj b/Octokit/OctokitRT.csproj index 87301df8..2f4bfa7a 100644 --- a/Octokit/OctokitRT.csproj +++ b/Octokit/OctokitRT.csproj @@ -117,7 +117,11 @@ + + + +