diff --git a/Octokit.Reactive/Clients/IObservableOauthClient.cs b/Octokit.Reactive/Clients/IObservableOauthClient.cs
index 2afa944c00..88c7a3547d 100644
--- a/Octokit.Reactive/Clients/IObservableOauthClient.cs
+++ b/Octokit.Reactive/Clients/IObservableOauthClient.cs
@@ -24,5 +24,27 @@ public interface IObservableOauthClient
///
///
IObservable CreateAccessToken(OauthTokenRequest request);
+
+ ///
+ /// Makes a request to initiate the device flow authentication.
+ ///
+ ///
+ /// Returns a user verification code and verification URL that the you will use to prompt the user to authenticate.
+ /// This request also returns a device verification code that you must use to receive an access token to check the status of user authentication.
+ ///
+ ///
+ ///
+ IObservable InitiateDeviceFlow(OauthDeviceFlowRequest request);
+
+ ///
+ /// Makes a request to get an access token using the response from .
+ ///
+ ///
+ /// Will poll the access token endpoint, until the device and user codes expire or the user has successfully authorized the app with a valid user code.
+ ///
+ /// The client Id you received from GitHub when you registered the application.
+ /// The response you received from
+ ///
+ IObservable CreateAccessTokenForDeviceFlow(string clientId, OauthDeviceFlowResponse deviceFlowResponse);
}
}
diff --git a/Octokit.Reactive/Clients/ObservableOauthClient.cs b/Octokit.Reactive/Clients/ObservableOauthClient.cs
index 62cce34790..fb51cd5c4d 100644
--- a/Octokit.Reactive/Clients/ObservableOauthClient.cs
+++ b/Octokit.Reactive/Clients/ObservableOauthClient.cs
@@ -40,5 +40,33 @@ public IObservable CreateAccessToken(OauthTokenRequest request)
{
return _client.Oauth.CreateAccessToken(request).ToObservable();
}
+
+ ///
+ /// Makes a request to initiate the device flow authentication.
+ ///
+ ///
+ /// Returns a user verification code and verification URL that the you will use to prompt the user to authenticate.
+ /// This request also returns a device verification code that you must use to receive an access token to check the status of user authentication.
+ ///
+ ///
+ ///
+ public IObservable InitiateDeviceFlow(OauthDeviceFlowRequest request)
+ {
+ return _client.Oauth.InitiateDeviceFlow(request).ToObservable();
+ }
+
+ ///
+ /// Makes a request to get an access token using the response from .
+ ///
+ ///
+ /// Will poll the access token endpoint, until the device and user codes expire or the user has successfully authorized the app with a valid user code.
+ ///
+ /// The client Id you received from GitHub when you registered the application.
+ /// The response you received from
+ ///
+ public IObservable CreateAccessTokenForDeviceFlow(string clientId, OauthDeviceFlowResponse deviceFlowResponse)
+ {
+ return _client.Oauth.CreateAccessTokenForDeviceFlow(clientId, deviceFlowResponse).ToObservable();
+ }
}
}
diff --git a/Octokit.Tests/Clients/OauthClientTests.cs b/Octokit.Tests/Clients/OauthClientTests.cs
index b2bf4228ac..a6f5187024 100644
--- a/Octokit.Tests/Clients/OauthClientTests.cs
+++ b/Octokit.Tests/Clients/OauthClientTests.cs
@@ -65,7 +65,7 @@ public class TheCreateAccessTokenMethod
[Fact]
public async Task PostsWithCorrectBodyAndContentType()
{
- var responseToken = new OauthToken(null, null, null);
+ var responseToken = new OauthToken(null, null, null, null, null, null);
var response = Substitute.For>();
response.Body.Returns(responseToken);
var connection = Substitute.For();
@@ -99,7 +99,7 @@ public async Task PostsWithCorrectBodyAndContentType()
[Fact]
public async Task PostsWithCorrectBodyAndContentTypeForGHE()
{
- var responseToken = new OauthToken(null, null, null);
+ var responseToken = new OauthToken(null, null, null, null, null, null);
var response = Substitute.For>();
response.Body.Returns(responseToken);
var connection = Substitute.For();
@@ -130,6 +130,68 @@ public async Task PostsWithCorrectBodyAndContentTypeForGHE()
await calledBody.ReadAsStringAsync());
}
+ [Fact]
+ public async Task InitiateDeviceFlowPostsWithCorrectBodyAndContentType()
+ {
+ var responseToken = new OauthDeviceFlowResponse("devicecode", "usercode", "uri", 10, 5);
+ var response = Substitute.For>();
+ response.Body.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.Post(
+ 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.InitiateDeviceFlow(new OauthDeviceFlowRequest("clientid"));
+
+ Assert.Same(responseToken, token);
+ Assert.Equal("login/device/code", calledUri.ToString());
+ Assert.NotNull(calledBody);
+ Assert.Equal("https://github.com/", calledHostAddress.ToString());
+ Assert.Equal(
+ "client_id=clientid",
+ await calledBody.ReadAsStringAsync());
+ }
+
+ [Fact]
+ public async Task CreateAccessTokenForDeviceFlowPostsWithCorrectBodyAndContentType()
+ {
+ var responseToken = new OauthToken(null, null, null, null, null, null);
+ var response = Substitute.For>();
+ response.Body.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.Post(
+ 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.CreateAccessTokenForDeviceFlow("clientid", new OauthDeviceFlowResponse("devicecode", "usercode", "uri", 10, 5));
+
+ 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=clientid&device_code=devicecode&grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Adevice_code",
+ await calledBody.ReadAsStringAsync());
+ }
+
[Fact]
public async Task DeserializesOAuthScopeFormat()
{
diff --git a/Octokit/Clients/IOAuthClient.cs b/Octokit/Clients/IOAuthClient.cs
index b0bde0191a..2d8af15299 100644
--- a/Octokit/Clients/IOAuthClient.cs
+++ b/Octokit/Clients/IOAuthClient.cs
@@ -28,5 +28,27 @@ public interface IOauthClient
///
///
Task CreateAccessToken(OauthTokenRequest request);
+
+ ///
+ /// Makes a request to initiate the device flow authentication.
+ ///
+ ///
+ /// Returns a user verification code and verification URL that the you will use to prompt the user to authenticate.
+ /// This request also returns a device verification code that you must use to receive an access token to check the status of user authentication.
+ ///
+ ///
+ ///
+ Task InitiateDeviceFlow(OauthDeviceFlowRequest request);
+
+ ///
+ /// Makes a request to get an access token using the response from .
+ ///
+ ///
+ /// Will poll the access token endpoint, until the device and user codes expire or the user has successfully authorized the app with a valid user code.
+ ///
+ /// The client Id you received from GitHub when you registered the application.
+ /// The response you received from
+ ///
+ Task CreateAccessTokenForDeviceFlow(string clientId, OauthDeviceFlowResponse deviceFlowResponse);
}
}
diff --git a/Octokit/Clients/OAuthClient.cs b/Octokit/Clients/OAuthClient.cs
index 655fe00be9..75b477f988 100644
--- a/Octokit/Clients/OAuthClient.cs
+++ b/Octokit/Clients/OAuthClient.cs
@@ -1,4 +1,5 @@
using System;
+using System.Globalization;
using System.Net.Http;
using System.Threading.Tasks;
@@ -69,5 +70,75 @@ public async Task CreateAccessToken(OauthTokenRequest request)
var response = await connection.Post(endPoint, body, "application/json", null, hostAddress).ConfigureAwait(false);
return response.Body;
}
+
+ ///
+ /// Makes a request to initiate the device flow authentication.
+ ///
+ ///
+ /// Returns a user verification code and verification URL that the you will use to prompt the user to authenticate.
+ /// This request also returns a device verification code that you must use to receive an access token to check the status of user authentication.
+ ///
+ ///
+ ///
+ [ManualRoute("POST", "/login/device/code")]
+ public async Task InitiateDeviceFlow(OauthDeviceFlowRequest request)
+ {
+ Ensure.ArgumentNotNull(request, nameof(request));
+
+ var endPoint = ApiUrls.OauthDeviceCode();
+
+ var body = new FormUrlEncodedContent(request.ToParametersDictionary());
+
+ var response = await connection.Post(endPoint, body, "application/json", null, hostAddress).ConfigureAwait(false);
+ return response.Body;
+ }
+
+ ///
+ /// Makes a request to get an access token using the response from .
+ ///
+ ///
+ /// Will poll the access token endpoint, until the device and user codes expire or the user has successfully authorized the app with a valid user code.
+ ///
+ /// The client Id you received from GitHub when you registered the application.
+ /// The response you received from
+ ///
+ [ManualRoute("POST", "/login/oauth/access_token")]
+ public async Task CreateAccessTokenForDeviceFlow(string clientId, OauthDeviceFlowResponse deviceFlowResponse)
+ {
+ Ensure.ArgumentNotNullOrEmptyString(clientId, nameof(clientId));
+ Ensure.ArgumentNotNull(deviceFlowResponse, nameof(deviceFlowResponse));
+
+ var endPoint = ApiUrls.OauthAccessToken();
+
+ int pollingDelay = deviceFlowResponse.Interval;
+
+ while (true)
+ {
+ var request = new OauthTokenRequestForDeviceFlow(clientId, deviceFlowResponse.DeviceCode);
+ var body = new FormUrlEncodedContent(request.ToParametersDictionary());
+ var response = await connection.Post(endPoint, body, "application/json", null, hostAddress).ConfigureAwait(false);
+
+ if (response.Body.Error != null)
+ {
+ switch (response.Body.Error)
+ {
+ case "authorization_pending":
+ break;
+ case "slow_down":
+ pollingDelay += 5;
+ break;
+ case "expired_token":
+ default:
+ throw new ApiException(string.Format(CultureInfo.InvariantCulture, "{0}: {1}\n{2}", response.Body.Error, response.Body.ErrorDescription, response.Body.ErrorUri), null);
+ }
+
+ await Task.Delay(TimeSpan.FromSeconds(pollingDelay));
+ }
+ else
+ {
+ return response.Body;
+ }
+ }
+ }
}
}
diff --git a/Octokit/Helpers/ApiUrls.cs b/Octokit/Helpers/ApiUrls.cs
index 74f0ff3f9c..48b5660ac5 100644
--- a/Octokit/Helpers/ApiUrls.cs
+++ b/Octokit/Helpers/ApiUrls.cs
@@ -2350,6 +2350,15 @@ public static Uri OauthAuthorize()
return "login/oauth/authorize".FormatUri();
}
+ ///
+ /// Creates the relative for initiating the OAuth device Flow
+ ///
+ ///
+ public static Uri OauthDeviceCode()
+ {
+ return "login/device/code".FormatUri();
+ }
+
///
/// Creates the relative to request an OAuth access token.
///
diff --git a/Octokit/Models/Request/OauthDeviceFlowRequest.cs b/Octokit/Models/Request/OauthDeviceFlowRequest.cs
new file mode 100644
index 0000000000..5e3ba24cbb
--- /dev/null
+++ b/Octokit/Models/Request/OauthDeviceFlowRequest.cs
@@ -0,0 +1,54 @@
+using System.Collections.ObjectModel;
+using System.Diagnostics;
+using System.Globalization;
+using Octokit.Internal;
+
+namespace Octokit
+{
+ ///
+ /// Used to create an Oauth device flow initiation request.
+ ///
+ [DebuggerDisplay("{DebuggerDisplay,nq}")]
+ public class OauthDeviceFlowRequest
+ : 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 OauthDeviceFlowRequest(string clientId)
+ {
+ Ensure.ArgumentNotNullOrEmptyString(clientId, nameof(clientId));
+
+ ClientId = clientId;
+ Scopes = new Collection();
+ }
+
+ ///
+ /// The client Id you received from GitHub when you registered the application.
+ ///
+ [Parameter(Key = "client_id")]
+ public string ClientId { get; private 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.
+ ///
+ [Parameter(Key = "scope")]
+ public Collection Scopes { get; private set; }
+
+ internal string DebuggerDisplay
+ {
+ get
+ {
+ return string.Format(CultureInfo.InvariantCulture, "ClientId: {0}, Scopes: {1}", ClientId, Scopes);
+ }
+ }
+ }
+}
diff --git a/Octokit/Models/Request/OauthTokenRequestForDeviceFlow.cs b/Octokit/Models/Request/OauthTokenRequestForDeviceFlow.cs
new file mode 100644
index 0000000000..b53e1406ac
--- /dev/null
+++ b/Octokit/Models/Request/OauthTokenRequestForDeviceFlow.cs
@@ -0,0 +1,55 @@
+using System.Diagnostics;
+using System.Globalization;
+using Octokit.Internal;
+
+namespace Octokit
+{
+ ///
+ /// Used to create an Oauth login request for the device flow.
+ ///
+ [DebuggerDisplay("{DebuggerDisplay,nq}")]
+ internal class OauthTokenRequestForDeviceFlow : 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 device code you received from the device flow initiation call.
+ public OauthTokenRequestForDeviceFlow(string clientId, string deviceCode)
+ {
+ Ensure.ArgumentNotNullOrEmptyString(clientId, nameof(clientId));
+ Ensure.ArgumentNotNullOrEmptyString(deviceCode, nameof(deviceCode));
+
+ ClientId = clientId;
+ DeviceCode = deviceCode;
+ }
+
+ ///
+ /// The client Id you received from GitHub when you registered the application.
+ ///
+ [Parameter(Key = "client_id")]
+ public string ClientId { get; private set; }
+
+ ///
+ /// The device code you received from the device flow initiation call.
+ ///
+ [Parameter(Key = "device_code")]
+ public string DeviceCode { get; private set; }
+
+ ///
+ /// The authorization grant type.
+ ///
+ [Parameter(Key = "grant_type")]
+ public string GrantType => "urn:ietf:params:oauth:grant-type:device_code";
+
+ internal string DebuggerDisplay
+ {
+ get
+ {
+ return string.Format(CultureInfo.InvariantCulture, "ClientId: {0}, DeviceCode: {1}",
+ ClientId,
+ DeviceCode);
+ }
+ }
+ }
+}
diff --git a/Octokit/Models/Response/OauthDeviceFlowResponse.cs b/Octokit/Models/Response/OauthDeviceFlowResponse.cs
new file mode 100644
index 0000000000..2f3ab0ad94
--- /dev/null
+++ b/Octokit/Models/Response/OauthDeviceFlowResponse.cs
@@ -0,0 +1,61 @@
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Globalization;
+
+namespace Octokit
+{
+ [DebuggerDisplay("{DebuggerDisplay,nq}")]
+ public class OauthDeviceFlowResponse
+ {
+ public OauthDeviceFlowResponse() { }
+
+ public OauthDeviceFlowResponse(string deviceCode, string userCode, string verificationUri, int expiresIn, int interval)
+ {
+ DeviceCode = deviceCode;
+ UserCode = userCode;
+ VerificationUri = verificationUri;
+ ExpiresIn = expiresIn;
+ Interval = interval;
+ }
+
+ ///
+ /// The device verification code is 40 characters and used to verify the device.
+ ///
+ public string DeviceCode { get; private set; }
+
+ ///
+ /// The user verification code is displayed on the device so the user can enter the code in a browser. This code is 8 characters with a hyphen in the middle.
+ ///
+ public string UserCode { get; private set; }
+
+ ///
+ /// The verification URL where users need to enter the UserCode: https://github.com/login/device.
+ ///
+ public string VerificationUri { get; private set; }
+
+ ///
+ /// The number of seconds before the DeviceCode and UserCode expire. The default is 900 seconds or 15 minutes.
+ ///
+ public int ExpiresIn { get; private set; }
+
+ ///
+ /// The minimum number of seconds that must pass before you can make a new access token request (POST https://github.com/login/oauth/access_token) to complete the device authorization.
+ /// For example, if the interval is 5, then you cannot make a new request until 5 seconds pass. If you make more than one request over 5 seconds, then you will hit the rate limit
+ /// and receive a slow_down error.
+ ///
+ public int Interval { get; private set; }
+
+ internal string DebuggerDisplay
+ {
+ get
+ {
+ return string.Format(CultureInfo.InvariantCulture, "DeviceCode: {0}, UserCode: {1}, VerificationUri: {2}, ExpiresIn: {3}, Interval: {4}",
+ DeviceCode,
+ UserCode,
+ VerificationUri,
+ ExpiresIn,
+ Interval);
+ }
+ }
+ }
+}
diff --git a/Octokit/Models/Response/OauthToken.cs b/Octokit/Models/Response/OauthToken.cs
index e61f2b1ea2..b7ecd50b88 100644
--- a/Octokit/Models/Response/OauthToken.cs
+++ b/Octokit/Models/Response/OauthToken.cs
@@ -1,6 +1,7 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
+using Octokit.Internal;
namespace Octokit
{
@@ -9,36 +10,57 @@ public class OauthToken
{
public OauthToken() { }
- public OauthToken(string tokenType, string accessToken, IReadOnlyList scope)
+ public OauthToken(string tokenType, string accessToken, IReadOnlyList scope, string error, string errorDescription, string errorUri)
{
- TokenType = tokenType;
- AccessToken = accessToken;
- Scope = scope;
+ this.TokenType = tokenType;
+ this.AccessToken = accessToken;
+ this.Scope = scope;
+ this.Error = error;
+ this.ErrorDescription = errorDescription;
+ this.ErrorUri = errorUri;
}
///
/// The type of OAuth token
///
- public string TokenType { get; protected set; }
+ public string TokenType { get; private set; }
///
/// The secret OAuth access token. Use this to authenticate Octokit.net's client.
///
- public string AccessToken { get; protected set; }
+ public string AccessToken { get; private set; }
///
/// The list of scopes the token includes.
///
- public IReadOnlyList Scope { get; protected set; }
+ public IReadOnlyList Scope { get; private set; }
+
+ ///
+ /// Gets or sets the error code or the response.
+ ///
+ [Parameter(Key = "error")]
+ public string Error { get; private set; }
+
+ ///
+ /// Gets or sets the error description.
+ ///
+ [Parameter(Key = "error_description")]
+ public string ErrorDescription { get; private set; }
+
+ ///
+ /// Gets or sets the error uri.
+ ///
+ [Parameter(Key = "error_uri")]
+ public string ErrorUri { get; private set; }
internal string DebuggerDisplay
{
get
{
return string.Format(CultureInfo.InvariantCulture, "TokenType: {0}, AccessToken: {1}, Scopes: {2}",
- TokenType,
- AccessToken,
- Scope);
+ this.TokenType,
+ this.AccessToken,
+ this.Scope);
}
}
}