Implement GetOrCreateApplicationAuthentication

Implements the endpoint for creating an application authorization token.
This commit is contained in:
Haacked
2013-10-09 16:28:59 -07:00
parent 9d71d406fa
commit 33ad79c0fe
19 changed files with 653 additions and 4 deletions

View File

@@ -25,6 +25,59 @@ namespace Octokit.Reactive.Clients
{ {
return _client.Get(id).ToObservable(); return _client.Get(id).ToObservable();
} }
/// <summary>
/// This method will create a new authorization for the specified OAuth application, only if an authorization
/// for that application doesnt already exist for the user. It returns the users token for the application
/// if one exists. Otherwise, it creates one.
/// </summary>
/// <param name="clientId">Client ID for the OAuth application that is requesting the token.</param>
/// <param name="clientSecret">The client secret</param>
/// <param name="authorization">Definse the scopes and metadata for the token</param>
/// <exception cref="AuthorizationException">Thrown when the user does not have permission to make
/// this request. Check </exception>
/// <returns></returns>
public IObservable<Authorization> 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();
}
/// <summary>
/// This method will create a new authorization for the specified OAuth application, only if an authorization
/// for that application doesnt already exist for the user. It returns the users token for the application
/// if one exists. Otherwise, it creates one.
/// </summary>
/// <param name="clientId">Client ID for the OAuth application that is requesting the token.</param>
/// <param name="clientSecret">The client secret</param>
/// <param name="authorization">Defines the scopes and metadata for the token</param>
/// <param name="twoFactorAuthenticationCode"></param>
/// <exception cref="AuthorizationException">Thrown when the user does not have permission to make
/// this request. Check </exception>
/// <exception cref="TwoFactorChallengeFailedException">Thrown when the two-factor code is not
/// valid.</exception>
/// <returns></returns>
public IObservable<Authorization> 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<Authorization> Update(int id, AuthorizationUpdate authorization) public IObservable<Authorization> Update(int id, AuthorizationUpdate authorization)
{ {

View File

@@ -0,0 +1,37 @@
using System;
using System.Reactive.Linq;
using Octokit.Reactive;
namespace Octokit
{
public static class AuthorizationExtensions
{
public static IObservable<Authorization> GetOrCreateApplicationAuthentication(
this IObservableAuthorizationsClient authorizationsClient,
string clientId,
string clientSecret,
AuthorizationUpdate authorization,
Func<TwoFactorRequiredException, IObservable<TwoFactorChallengeResult>> twoFactorChallengeHandler
)
{
Ensure.ArgumentNotNull(authorizationsClient, "authorizationsClient");
Ensure.ArgumentNotNullOrEmptyString(clientId, "clientId");
Ensure.ArgumentNotNullOrEmptyString(clientSecret, "clientSecret");
Ensure.ArgumentNotNull(authorization, "authorization");
return authorizationsClient.GetOrCreateApplicationAuthentication(clientId, clientSecret, authorization)
.Catch<Authorization, TwoFactorRequiredException>(exception => twoFactorChallengeHandler(exception)
.SelectMany(result =>
result.ResendCodeRequested
? authorizationsClient.GetOrCreateApplicationAuthentication(
clientId,
clientSecret,
authorization,
twoFactorChallengeHandler)
: authorizationsClient.GetOrCreateApplicationAuthentication(clientId,
clientSecret,
authorization,
result.AuthenticationCode)));
}
}
}

View File

@@ -13,6 +13,44 @@ namespace Octokit.Reactive
[SuppressMessage("Microsoft.Naming", "CA1716:IdentifiersShouldNotMatchKeywords", MessageId = "Get", [SuppressMessage("Microsoft.Naming", "CA1716:IdentifiersShouldNotMatchKeywords", MessageId = "Get",
Justification = "It's fiiiine. It's fine. Trust us.")] Justification = "It's fiiiine. It's fine. Trust us.")]
IObservable<Authorization> Get(int id); IObservable<Authorization> Get(int id);
/// <summary>
/// This method will create a new authorization for the specified OAuth application, only if an authorization
/// for that application doesnt already exist for the user. It returns the users token for the application
/// if one exists. Otherwise, it creates one.
/// </summary>
/// <param name="clientId">Client ID for the OAuth application that is requesting the token.</param>
/// <param name="clientSecret">The client secret</param>
/// <param name="authorization">Defines the scopes and metadata for the token</param>
/// <exception cref="AuthorizationException">Thrown when the user does not have permission to make
/// this request. Check </exception>
/// <exception cref="TwoFactorRequiredException">Thrown when the current account has two-factor
/// authentication enabled.</exception>
/// <returns></returns>
IObservable<Authorization> GetOrCreateApplicationAuthentication(
string clientId,
string clientSecret,
AuthorizationUpdate authorization);
/// <summary>
/// This method will create a new authorization for the specified OAuth application, only if an authorization
/// for that application doesnt already exist for the user. It returns the users token for the application
/// if one exists. Otherwise, it creates one.
/// </summary>
/// <param name="clientId">Client ID for the OAuth application that is requesting the token.</param>
/// <param name="clientSecret">The client secret</param>
/// <param name="authorization">Defines the scopes and metadata for the token</param>
/// <param name="twoFactorAuthenticationCode"></param>
/// <exception cref="AuthorizationException">Thrown when the user does not have permission to make
/// this request. Check </exception>
/// <exception cref="TwoFactorChallengeFailedException">Thrown when the two-factor code is not
/// valid.</exception>
/// <returns></returns>
IObservable<Authorization> GetOrCreateApplicationAuthentication(
string clientId,
string clientSecret,
AuthorizationUpdate authorization,
string twoFactorAuthenticationCode);
IObservable<Authorization> Update(int id, AuthorizationUpdate authorization); IObservable<Authorization> Update(int id, AuthorizationUpdate authorization);
IObservable<Authorization> Create(AuthorizationUpdate authorization); IObservable<Authorization> Create(AuthorizationUpdate authorization);
IObservable<Unit> Delete(int id); IObservable<Unit> Delete(int id);

View File

@@ -89,6 +89,7 @@
<Compile Include="Clients\ObservableRepositoriesClient.cs" /> <Compile Include="Clients\ObservableRepositoriesClient.cs" />
<Compile Include="Clients\ObservableSshKeysClient.cs" /> <Compile Include="Clients\ObservableSshKeysClient.cs" />
<Compile Include="Clients\ObservableUsersClient.cs" /> <Compile Include="Clients\ObservableUsersClient.cs" />
<Compile Include="Helpers\AuthorizationExtensions.cs" />
<Compile Include="IObservableAuthorizationsClient.cs" /> <Compile Include="IObservableAuthorizationsClient.cs" />
<Compile Include="IObservableMiscellaneousClient.cs" /> <Compile Include="IObservableMiscellaneousClient.cs" />
<Compile Include="IObservableGitHubClient.cs" /> <Compile Include="IObservableGitHubClient.cs" />

View File

@@ -1,6 +1,8 @@
using System; using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using NSubstitute; using NSubstitute;
using Octokit.Internal; using Octokit.Tests.Helpers;
using Xunit; using Xunit;
namespace Octokit.Tests.Clients namespace Octokit.Tests.Clients
@@ -91,5 +93,93 @@ namespace Octokit.Tests.Clients
client.Received().Delete(Arg.Is<Uri>(u => u.ToString() == "/authorizations/1")); client.Received().Delete(Arg.Is<Uri>(u => u.ToString() == "/authorizations/1"));
} }
} }
public class TheGetOrCreateApplicationAuthenticationMethod
{
[Fact]
public void GetsOrCreatesAuthenticationAtCorrectUrl()
{
var data = new AuthorizationUpdate();
var client = Substitute.For<IApiConnection<Authorization>>();
var authEndpoint = new AuthorizationsClient(client);
authEndpoint.GetOrCreateApplicationAuthentication("clientId", "secret", data);
client.Received().GetOrCreate(Arg.Is<Uri>(u => u.ToString() == "/authorizations/clients/clientId"),
Args.Object);
}
[Fact]
public async Task WrapsTwoFactorFailureWithTwoFactorException()
{
var data = new AuthorizationUpdate();
var client = Substitute.For<IApiConnection<Authorization>>();
client.GetOrCreate(Args.Uri, Args.Object, Args.String).Returns(_ => {throw new AuthorizationException();});
var authEndpoint = new AuthorizationsClient(client);
AssertEx.Throws<TwoFactorChallengeFailedException>(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<IAuthorizationsClient>();
client.GetOrCreateApplicationAuthentication("clientId", "secret", Arg.Any<AuthorizationUpdate>())
.Returns(_ => {throw new TwoFactorRequiredException();});
client.GetOrCreateApplicationAuthentication("clientId",
"secret",
Arg.Any<AuthorizationUpdate>(),
"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<AuthorizationUpdate>(u => u.Note == "note"));
client.Received().GetOrCreateApplicationAuthentication("clientId",
"secret",
Arg.Any<AuthorizationUpdate>(), "two-factor-code");
Assert.Equal("xyz", result.Token);
}
[Fact]
public async Task RetriesWhenResendRequested()
{
var challengeResults = new Queue<TwoFactorChallengeResult>(new []
{
TwoFactorChallengeResult.RequestResendCode,
new TwoFactorChallengeResult("two-factor-code")
});
var data = new AuthorizationUpdate();
var client = Substitute.For<IAuthorizationsClient>();
client.GetOrCreateApplicationAuthentication("clientId", "secret", Arg.Any<AuthorizationUpdate>())
.Returns(_ => { throw new TwoFactorRequiredException(); });
client.GetOrCreateApplicationAuthentication("clientId",
"secret",
Arg.Any<AuthorizationUpdate>(),
"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<AuthorizationUpdate>());
client.Received().GetOrCreateApplicationAuthentication("clientId",
"secret",
Arg.Any<AuthorizationUpdate>(), "two-factor-code");
Assert.Equal("xyz", result.Token);
}
}
} }
} }

View File

@@ -9,6 +9,7 @@ using NSubstitute;
using Octokit.Internal; using Octokit.Internal;
using Octokit.Tests.Helpers; using Octokit.Tests.Helpers;
using Xunit; using Xunit;
using Xunit.Extensions;
namespace Octokit.Tests.Http namespace Octokit.Tests.Http
{ {
@@ -106,6 +107,63 @@ namespace Octokit.Tests.Http
"oauth token.", exception.Message); "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<IHttpClient>();
IResponse<string> response = new ApiResponse<string> { StatusCode = HttpStatusCode.Unauthorized };
response.Headers[headerKey] = otpHeaderValue;
httpClient.Send<string>(Args.Request).Returns(Task.FromResult(response));
var connection = new Connection("Test Runner User Agent",
ExampleUri,
Substitute.For<ICredentialStore>(),
httpClient,
Substitute.For<IJsonSerializer>());
var exception = await AssertEx.Throws<AuthorizationException>(
async () => await connection.GetAsync<string>(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<IHttpClient>();
IResponse<string> response = new ApiResponse<string>
{
StatusCode = HttpStatusCode.Unauthorized,
};
response.Headers[headerKey] = otpHeaderValue;
httpClient.Send<string>(Args.Request).Returns(Task.FromResult(response));
var connection = new Connection("Test Runner User Agent",
ExampleUri,
Substitute.For<ICredentialStore>(),
httpClient,
Substitute.For<IJsonSerializer>());
var exception = await AssertEx.Throws<TwoFactorRequiredException>(
async () => await connection.GetAsync<string>(new Uri("/endpoint", UriKind.Relative)));
Assert.Equal("Two-factor authentication required", exception.Message);
Assert.Equal(expectedFactorType, exception.TwoFactorType);
}
[Fact] [Fact]
public async Task ThrowsApiValidationExceptionFor422Response() public async Task ThrowsApiValidationExceptionFor422Response()
{ {

View File

@@ -2,6 +2,7 @@
#if NET_45 #if NET_45
using System.Collections.Generic; using System.Collections.Generic;
#endif #endif
using System.Collections;
using System.Threading.Tasks; using System.Threading.Tasks;
using Octokit.Internal; using Octokit.Internal;
@@ -35,6 +36,69 @@ namespace Octokit
return await Client.Get(endpoint); return await Client.Get(endpoint);
} }
/// <summary>
/// This method will create a new authorization for the specified OAuth application, only if an authorization
/// for that application doesnt already exist for the user. It returns the users token for the application
/// if one exists. Otherwise, it creates one.
/// </summary>
/// <param name="clientId">Client ID for the OAuth application that is requesting the token.</param>
/// <param name="clientSecret">The client secret</param>
/// <param name="authorization">Definse the scopes and metadata for the token</param>
/// <returns></returns>
public async Task<Authorization> 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<Authorization> 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);
}
}
/// <summary> /// <summary>
/// Update the specified <see cref="Authorization"/>. /// Update the specified <see cref="Authorization"/>.
/// </summary> /// </summary>

View File

@@ -34,3 +34,42 @@ namespace Octokit
#endif #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();
}
}
*/

View File

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

View File

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

View File

@@ -0,0 +1,41 @@
using System;
using System.Threading.Tasks;
namespace Octokit
{
public static class AuthorizationExtensions
{
public static async Task<Authorization> GetOrCreateApplicationAuthentication(
this IAuthorizationsClient authorizationsClient,
string clientId,
string clientSecret,
AuthorizationUpdate authorization,
Func<TwoFactorRequiredException, Task<TwoFactorChallengeResult>> 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));
}
}
}

View File

@@ -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; }
}
}

View File

@@ -65,6 +65,27 @@ namespace Octokit
return response.BodyAsObject; return response.BodyAsObject;
} }
public async Task<T> GetOrCreate(Uri endpoint, object data)
{
Ensure.ArgumentNotNull(endpoint, "endpoint");
Ensure.ArgumentNotNull(data, "data");
var response = await Connection.PutAsync<T>(endpoint, data);
return response.BodyAsObject;
}
public async Task<T> GetOrCreate(Uri endpoint, object data, string twoFactorAuthenticationCode)
{
Ensure.ArgumentNotNull(endpoint, "endpoint");
Ensure.ArgumentNotNull(data, "data");
Ensure.ArgumentNotNullOrEmptyString(twoFactorAuthenticationCode, "twoFactorAuthenticationCode");
var response = await Connection.PutAsync<T>(endpoint, data, twoFactorAuthenticationCode);
return response.BodyAsObject;
}
public async Task<T> Update(Uri endpoint, object data) public async Task<T> Update(Uri endpoint, object data)
{ {
Ensure.ArgumentNotNull(endpoint, "endpoint"); Ensure.ArgumentNotNull(endpoint, "endpoint");

View File

@@ -1,6 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.Linq;
using System.Net; using System.Net;
using System.Net.Http; using System.Net.Http;
using System.Threading.Tasks; using System.Threading.Tasks;
@@ -120,12 +121,18 @@ namespace Octokit
return await SendData<T>(endpoint, HttpMethod.Put, body); return await SendData<T>(endpoint, HttpMethod.Put, body);
} }
public async Task<IResponse<T>> PutAsync<T>(Uri endpoint, object body, string twoFactorAuthenticationCode)
{
return await SendData<T>(endpoint, HttpMethod.Put, body, twoFactorAuthenticationCode);
}
async Task<IResponse<T>> SendData<T>( async Task<IResponse<T>> SendData<T>(
Uri endpoint, Uri endpoint,
HttpMethod method, HttpMethod method,
object body, object body,
string contentType = "application/x-www-form-urlencoded", // Per: http://developer.github.com/v3/ 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"); Ensure.ArgumentNotNull(endpoint, "endpoint");
@@ -142,6 +149,11 @@ namespace Octokit
request.Headers["Accept"] = accepts; request.Headers["Accept"] = accepts;
} }
if (!String.IsNullOrEmpty(twoFactorAuthenticationCode))
{
request.Headers["X-GitHub-OTP"] = twoFactorAuthenticationCode;
}
if (body != null) if (body != null)
{ {
request.Body = body; request.Body = body;
@@ -225,8 +237,14 @@ namespace Octokit
static void HandleErrors(IResponse response) static void HandleErrors(IResponse response)
{ {
if (response.StatusCode == HttpStatusCode.Unauthorized) 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) if (response.StatusCode == HttpStatusCode.Forbidden)
{ {
@@ -248,5 +266,29 @@ namespace Octokit
throw new ApiException(response.Body, response.StatusCode); 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;
}
} }
} }

View File

@@ -19,6 +19,8 @@ namespace Octokit
Task<string> GetHtml(Uri endpoint, IDictionary<string, string> parameters); Task<string> GetHtml(Uri endpoint, IDictionary<string, string> parameters);
Task<IReadOnlyList<T>> GetAll(Uri endpoint, IDictionary<string, string> parameters); Task<IReadOnlyList<T>> GetAll(Uri endpoint, IDictionary<string, string> parameters);
Task<T> Create(Uri endpoint, object data); Task<T> Create(Uri endpoint, object data);
Task<T> GetOrCreate(Uri endpoint, object data);
Task<T> GetOrCreate(Uri endpoint, object data, string twoFactorAuthenticationCode);
Task<T> Update(Uri endpoint, object data); Task<T> Update(Uri endpoint, object data);
Task Delete(Uri endpoint); Task Delete(Uri endpoint);
Task<TOther> Upload<TOther>(Uri uri, Stream rawData, string contentType); Task<TOther> Upload<TOther>(Uri uri, Stream rawData, string contentType);

View File

@@ -14,6 +14,7 @@ namespace Octokit
Task<IResponse<T>> PostAsync<T>(Uri endpoint, object body); Task<IResponse<T>> PostAsync<T>(Uri endpoint, object body);
Task<IResponse<T>> PostAsync<T>(Uri endpoint, object body, string contentType, string accepts); Task<IResponse<T>> PostAsync<T>(Uri endpoint, object body, string contentType, string accepts);
Task<IResponse<T>> PutAsync<T>(Uri endpoint, object body); Task<IResponse<T>> PutAsync<T>(Uri endpoint, object body);
Task<IResponse<T>> PutAsync<T>(Uri endpoint, object body, string twoFactorAuthenticationCode);
[SuppressMessage("Microsoft.Design", "CA1004:GenericMethodsShouldProvideTypeParameter")] [SuppressMessage("Microsoft.Design", "CA1004:GenericMethodsShouldProvideTypeParameter")]
Task DeleteAsync<T>(Uri endpoint); Task DeleteAsync<T>(Uri endpoint);

View File

@@ -9,9 +9,50 @@ namespace Octokit
[SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate", [SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate",
Justification = "It's an API call, so it's not a property.")] Justification = "It's an API call, so it's not a property.")]
Task<IReadOnlyList<Authorization>> GetAll(); Task<IReadOnlyList<Authorization>> GetAll();
/// <summary>
/// Get a specific <see cref="Authorization"/> for the authenticated user. This method requires basic auth.
/// </summary>
/// <param name="id">The id of the <see cref="Authorization"/>.</param>
/// <returns>An <see cref="Authorization"/></returns>
[SuppressMessage("Microsoft.Naming", "CA1716:IdentifiersShouldNotMatchKeywords", MessageId = "Get", [SuppressMessage("Microsoft.Naming", "CA1716:IdentifiersShouldNotMatchKeywords", MessageId = "Get",
Justification = "It's fiiiine. It's fine. Trust us.")] Justification = "It's fiiiine. It's fine. Trust us.")]
Task<Authorization> Get(int id); Task<Authorization> Get(int id);
/// <summary>
/// This method will create a new authorization for the specified OAuth application, only if an authorization
/// for that application doesnt already exist for the user. It returns the users token for the application
/// if one exists. Otherwise, it creates one.
/// </summary>
/// <param name="clientId">Client ID for the OAuth application that is requesting the token.</param>
/// <param name="clientSecret">The client secret</param>
/// <param name="authorization">Defines the scopes and metadata for the token</param>
/// <exception cref="AuthorizationException">Thrown when the user does not have permission to make
/// this request. Check </exception>
/// <exception cref="TwoFactorRequiredException">Thrown when the current account has two-factor
/// authentication enabled.</exception>
/// <returns></returns>
Task<Authorization> GetOrCreateApplicationAuthentication(
string clientId,
string clientSecret,
AuthorizationUpdate authorization);
/// <summary>
/// This method will create a new authorization for the specified OAuth application, only if an authorization
/// for that application doesnt already exist for the user. It returns the users token for the application
/// if one exists. Otherwise, it creates one.
/// </summary>
/// <param name="clientId">Client ID for the OAuth application that is requesting the token.</param>
/// <param name="clientSecret">The client secret</param>
/// <param name="authorization">Defines the scopes and metadata for the token</param>
/// <param name="twoFactorAuthenticationCode"></param>
/// <exception cref="AuthorizationException">Thrown when the user does not have permission to make
/// this request. Check </exception>
/// <exception cref="TwoFactorChallengeFailedException">Thrown when the two-factor code is not
/// valid.</exception>
/// <returns></returns>
Task<Authorization> GetOrCreateApplicationAuthentication(
string clientId,
string clientSecret,
AuthorizationUpdate authorization,
string twoFactorAuthenticationCode);
Task<Authorization> Update(int id, AuthorizationUpdate authorization); Task<Authorization> Update(int id, AuthorizationUpdate authorization);
Task<Authorization> Create(AuthorizationUpdate authorization); Task<Authorization> Create(AuthorizationUpdate authorization);
Task Delete(int id); Task Delete(int id);

View File

@@ -78,6 +78,10 @@
</Reference> </Reference>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Compile Include="Exceptions\TwoFactorChallengeFailedException.cs" />
<Compile Include="Exceptions\TwoFactorRequiredException.cs" />
<Compile Include="Helpers\AuthorizationExtensions.cs" />
<Compile Include="Helpers\TwoFactorChallengeResult.cs" />
<Compile Include="Models\Account.cs" /> <Compile Include="Models\Account.cs" />
<Compile Include="Models\ApiError.cs" /> <Compile Include="Models\ApiError.cs" />
<Compile Include="Models\ApiErrorDetail.cs" /> <Compile Include="Models\ApiErrorDetail.cs" />

View File

@@ -117,7 +117,11 @@
<Compile Include="Exceptions\ApiException.cs" /> <Compile Include="Exceptions\ApiException.cs" />
<Compile Include="Exceptions\ApiValidationException.cs" /> <Compile Include="Exceptions\ApiValidationException.cs" />
<Compile Include="Exceptions\AuthorizationException.cs" /> <Compile Include="Exceptions\AuthorizationException.cs" />
<Compile Include="Exceptions\TwoFactorChallengeFailedException.cs" />
<Compile Include="Exceptions\TwoFactorRequiredException.cs" />
<Compile Include="Helpers\AuthorizationExtensions.cs" />
<Compile Include="Helpers\CollectionExtensions.cs" /> <Compile Include="Helpers\CollectionExtensions.cs" />
<Compile Include="Helpers\TwoFactorChallengeResult.cs" />
<Compile Include="Helpers\UriExtensions.cs" /> <Compile Include="Helpers\UriExtensions.cs" />
<Compile Include="Http\ApiConnection.cs" /> <Compile Include="Http\ApiConnection.cs" />
<Compile Include="Http\ApiResponse.cs" /> <Compile Include="Http\ApiResponse.cs" />