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 @@
+
+
+
+