Skip to content

Commit

Permalink
Support refreshtokens in OAuth flow
Browse files Browse the repository at this point in the history
Fixes #2731
  • Loading branch information
Kencdk committed Jul 18, 2023
1 parent c69182a commit 0c2025b
Show file tree
Hide file tree
Showing 7 changed files with 207 additions and 66 deletions.
7 changes: 7 additions & 0 deletions Octokit.Reactive/Clients/IObservableOauthClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,5 +46,12 @@ public interface IObservableOauthClient
/// <param name="deviceFlowResponse">The response you received from <see cref="InitiateDeviceFlow(OauthDeviceFlowRequest)"/></param>
/// <returns></returns>
IObservable<OauthToken> CreateAccessTokenForDeviceFlow(string clientId, OauthDeviceFlowResponse deviceFlowResponse);

/// <summary>
/// Makes a request to get an access token using the refresh token returned in <see cref="CreateAccessToken(OauthTokenRequest)"/>.
/// </summary>
/// <param name="request">Token renewal request.</param>
/// <returns><see cref="OauthToken"/> with the new token set.</returns>
IObservable<OauthToken> CreateAccessTokenFromRenewalToken(OauthTokenRenewalRequest request);
}
}
45 changes: 10 additions & 35 deletions Octokit.Reactive/Clients/ObservableOauthClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@

namespace Octokit.Reactive
{
/// <summary>
/// Wrapper around <see cref="IOauthClient"/> for use with <see cref="IObservable{T}"/>
/// </summary>
/// <inheritdoc />
public class ObservableOauthClient : IObservableOauthClient
{
readonly IGitHubClient _client;
Expand All @@ -14,59 +18,30 @@ public ObservableOauthClient(IGitHubClient client)
_client = client;
}

/// <summary>
/// Gets the URL used in the first step of the web flow. The Web application should redirect to this URL.
/// </summary>
/// <param name="request">Parameters to the Oauth web flow login url</param>
/// <returns></returns>
public Uri GetGitHubLoginUrl(OauthLoginRequest request)
{
return _client.Oauth.GetGitHubLoginUrl(request);
}

/// <summary>
/// Makes a request to get an access token using the code returned when GitHub.com redirects back from the URL
/// <see cref="GetGitHubLoginUrl">GitHub login url</see> to the application.
/// </summary>
/// <remarks>
/// If the user accepts your request, GitHub redirects back to your site with a temporary code in a code
/// parameter as well as the state you provided in the previous step in a state parameter. If the states don’t
/// match, the request has been created by a third party and the process should be aborted. Exchange this for
/// an access token using this method.
/// </remarks>
/// <param name="request"></param>
/// <returns></returns>
public IObservable<OauthToken> CreateAccessToken(OauthTokenRequest request)
{
return _client.Oauth.CreateAccessToken(request).ToObservable();
}

/// <summary>
/// Makes a request to initiate the device flow authentication.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
/// <param name="request"></param>
/// <returns></returns>
public IObservable<OauthDeviceFlowResponse> InitiateDeviceFlow(OauthDeviceFlowRequest request)
{
return _client.Oauth.InitiateDeviceFlow(request).ToObservable();
}

/// <summary>
/// Makes a request to get an access token using the response from <see cref="InitiateDeviceFlow(OauthDeviceFlowRequest)"/>.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
/// <param name="clientId">The client Id you received from GitHub when you registered the application.</param>
/// <param name="deviceFlowResponse">The response you received from <see cref="InitiateDeviceFlow(OauthDeviceFlowRequest)"/></param>
/// <returns></returns>
public IObservable<OauthToken> CreateAccessTokenForDeviceFlow(string clientId, OauthDeviceFlowResponse deviceFlowResponse)
{
return _client.Oauth.CreateAccessTokenForDeviceFlow(clientId, deviceFlowResponse).ToObservable();
}

public IObservable<OauthToken> CreateAccessTokenFromRenewalToken(OauthTokenRenewalRequest request)
{
return _client.Oauth.CreateAccessTokenFromRenewalToken(request)
.ToObservable();
}
}
}
72 changes: 71 additions & 1 deletion Octokit.Tests/Clients/OauthClientTests.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using System;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using NSubstitute;
Expand Down Expand Up @@ -208,4 +207,75 @@ public async Task DeserializesOAuthScopeFormat()
Assert.Contains("user:email", token.Scope);
}
}

public class TheCreateAccessTokenFromRenewalTokenMethod
{
[Fact]
public async Task PostsWithCorrectBodyAndContentType()
{
var responseToken = new OauthToken("bearer", "someaccesstoken", 3000, "refreshtoken", 10000, Array.Empty<string>(), null, null, null);
var response = Substitute.For<IApiResponse<OauthToken>>();
response.Body.Returns(responseToken);

var connection = Substitute.For<IConnection>();
connection.BaseAddress.Returns(new Uri("https://api.github.com/"));

Uri calledUri = null;
FormUrlEncodedContent calledBody = null;
Uri calledHostAddress = null;
connection.Post<OauthToken>(
Arg.Do<Uri>(uri => calledUri = uri),
Arg.Do<object>(body => calledBody = body as FormUrlEncodedContent),
"application/json",
null,
Arg.Do<Uri>(uri => calledHostAddress = uri))
.Returns(_ => Task.FromResult(response));
var client = new OauthClient(connection);

var token = await client.CreateAccessTokenFromRenewalToken(
new OauthTokenRenewalRequest("secretid", "secretsecret", "refreshToken"));

Assert.Same(responseToken, token);
Assert.Equal("login/oauth/access_token", calledUri.ToString());
Assert.NotNull(calledBody);
Assert.Equal("https://github.com/", calledHostAddress.ToString());
Assert.Equal(
"client_id=secretid&client_secret=secretsecret&grant_type=refresh_token&refresh_token=refreshToken",
await calledBody.ReadAsStringAsync());
}

[Fact]
public async Task PostsWithCorrectBodyAndContentTypeForGHE()
{
var responseToken = new OauthToken("bearer", "someaccesstoken", 3000, "refreshtoken", 10000, Array.Empty<string>(), null, null, null);
var response = Substitute.For<IApiResponse<OauthToken>>();
response.Body.Returns(responseToken);

var connection = Substitute.For<IConnection>();
connection.BaseAddress.Returns(new Uri("https://example.com/api/v3"));

Uri calledUri = null;
FormUrlEncodedContent calledBody = null;
Uri calledHostAddress = null;
connection.Post<OauthToken>(
Arg.Do<Uri>(uri => calledUri = uri),
Arg.Do<object>(body => calledBody = body as FormUrlEncodedContent),
"application/json",
null,
Arg.Do<Uri>(uri => calledHostAddress = uri))
.Returns(_ => Task.FromResult(response));
var client = new OauthClient(connection);

var token = await client.CreateAccessTokenFromRenewalToken(
new OauthTokenRenewalRequest("secretid", "secretsecret", "refreshToken"));

Assert.Same(responseToken, token);
Assert.Equal("login/oauth/access_token", calledUri.ToString());
Assert.NotNull(calledBody);
Assert.Equal("https://example.com/", calledHostAddress.ToString());
Assert.Equal(
"client_id=secretid&client_secret=secretsecret&grant_type=refresh_token&refresh_token=refreshToken",
await calledBody.ReadAsStringAsync());
}
}
}
7 changes: 7 additions & 0 deletions Octokit/Clients/IOAuthClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,5 +50,12 @@ public interface IOauthClient
/// <param name="deviceFlowResponse">The response you received from <see cref="InitiateDeviceFlow(OauthDeviceFlowRequest)"/></param>
/// <returns></returns>
Task<OauthToken> CreateAccessTokenForDeviceFlow(string clientId, OauthDeviceFlowResponse deviceFlowResponse);

/// <summary>
/// Makes a request to get an access token using the refresh token returned in <see cref="CreateAccessToken(OauthTokenRequest)"/>.
/// </summary>
/// <param name="request">Token renewal request.</param>
/// <returns><see cref="OauthToken"/> with the new token set.</returns>
Task<OauthToken> CreateAccessTokenFromRenewalToken(OauthTokenRenewalRequest request);
}
}
43 changes: 13 additions & 30 deletions Octokit/Clients/OAuthClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ namespace Octokit
/// <summary>
/// Provides methods used in the OAuth web flow.
/// </summary>
/// <inheritdoc />
public class OauthClient : IOauthClient
{
readonly IConnection connection;
Expand Down Expand Up @@ -46,18 +47,6 @@ public Uri GetGitHubLoginUrl(OauthLoginRequest request)
.ApplyParameters(request.ToParametersDictionary());
}

/// <summary>
/// Makes a request to get an access token using the code returned when GitHub.com redirects back from the URL
/// <see cref="GetGitHubLoginUrl">GitHub login url</see> to the application.
/// </summary>
/// <remarks>
/// If the user accepts your request, GitHub redirects back to your site with a temporary code in a code
/// parameter as well as the state you provided in the previous step in a state parameter. If the states don’t
/// match, the request has been created by a third party and the process should be aborted. Exchange this for
/// an access token using this method.
/// </remarks>
/// <param name="request"></param>
/// <returns></returns>
[ManualRoute("POST", "/login/oauth/access_token")]
public async Task<OauthToken> CreateAccessToken(OauthTokenRequest request)
{
Expand All @@ -71,15 +60,6 @@ public async Task<OauthToken> CreateAccessToken(OauthTokenRequest request)
return response.Body;
}

/// <summary>
/// Makes a request to initiate the device flow authentication.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
/// <param name="request"></param>
/// <returns></returns>
[ManualRoute("POST", "/login/device/code")]
public async Task<OauthDeviceFlowResponse> InitiateDeviceFlow(OauthDeviceFlowRequest request)
{
Expand All @@ -93,15 +73,6 @@ public async Task<OauthDeviceFlowResponse> InitiateDeviceFlow(OauthDeviceFlowReq
return response.Body;
}

/// <summary>
/// Makes a request to get an access token using the response from <see cref="InitiateDeviceFlow(OauthDeviceFlowRequest)"/>.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
/// <param name="clientId">The client Id you received from GitHub when you registered the application.</param>
/// <param name="deviceFlowResponse">The response you received from <see cref="InitiateDeviceFlow(OauthDeviceFlowRequest)"/></param>
/// <returns></returns>
[ManualRoute("POST", "/login/oauth/access_token")]
public async Task<OauthToken> CreateAccessTokenForDeviceFlow(string clientId, OauthDeviceFlowResponse deviceFlowResponse)
{
Expand Down Expand Up @@ -140,5 +111,17 @@ public async Task<OauthToken> CreateAccessTokenForDeviceFlow(string clientId, Oa
}
}
}

[ManualRoute("POST", "/login/oauth/access_token")]
public async Task<OauthToken> CreateAccessTokenFromRenewalToken(OauthTokenRenewalRequest request)
{
Ensure.ArgumentNotNull(request, nameof(request));

var endPoint = ApiUrls.OauthAccessToken();
var body = new FormUrlEncodedContent(request.ToParametersDictionary());

var response = await connection.Post<OauthToken>(endPoint, body, "application/json", null, hostAddress).ConfigureAwait(false);
return response.Body;
}
}
}
67 changes: 67 additions & 0 deletions Octokit/Models/Request/OauthTokenRenewalRequest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
using System.Diagnostics;
using System.Globalization;
using Octokit.Internal;

namespace Octokit
{
/// <summary>
/// Used to create an Oauth login request.
/// </summary>
[DebuggerDisplay("{DebuggerDisplay,nq}")]
public class OauthTokenRenewalRequest : RequestParameters
{
/// <summary>
/// Creates an instance of the OAuth token refresh request.
/// </summary>
/// <param name="clientId">The client Id you received from GitHub when you registered the application.</param>
/// <param name="clientSecret">The client secret you received from GitHub when you registered.</param>
/// <param name="refreshToken">The refresh token you received when making the original oauth token request.</param>
public OauthTokenRenewalRequest(string clientId, string clientSecret, string refreshToken)
{
Ensure.ArgumentNotNullOrEmptyString(clientId, nameof(clientId));
Ensure.ArgumentNotNullOrEmptyString(clientSecret, nameof(clientSecret));
Ensure.ArgumentNotNullOrEmptyString(refreshToken, nameof(refreshToken));

ClientId = clientId;
ClientSecret = clientSecret;
RefreshToken = refreshToken;
}

/// <summary>
/// The client Id you received from GitHub when you registered the application.
/// </summary>
[Parameter(Key = "client_id")]
public string ClientId { get; private set; }

/// <summary>
/// The client secret you received from GitHub when you registered.
/// </summary>
[Parameter(Key = "client_secret")]
public string ClientSecret { get; private set; }

/// <summary>
/// The type of grant. Should be ommited, unless renewing an access token.
/// </summary>
[Parameter(Key = "grant_type")]
public string GrantType { get; private set; } = "refresh_token";

/// <summary>
/// The refresh token you received as a response to making the <see cref="IOauthClient.CreateAccessToken">OAuth login
/// request</see>.
/// </summary>
[Parameter(Key = "refresh_token")]
public string RefreshToken { get; private set; }

internal string DebuggerDisplay
{
get
{
return string.Format(CultureInfo.InvariantCulture, "ClientId: {0}, ClientSecret: {1}, GrantType: {2}, RefreshToken: {3}",
ClientId,
ClientSecret,
GrantType,
RefreshToken);
}
}
}
}
32 changes: 32 additions & 0 deletions Octokit/Models/Response/OauthToken.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,19 @@ public OauthToken(string tokenType, string accessToken, IReadOnlyList<string> sc
this.ErrorUri = errorUri;
}

public OauthToken(string tokenType, string accessToken, int expiresIn, string refreshToken, int refreshTokenExpiresIn, IReadOnlyList<string> scope, string error, string errorDescription, string errorUri)
{
this.TokenType = tokenType;
this.AccessToken = accessToken;
this.ExpiresIn = expiresIn;
this.RefreshToken = refreshToken;
this.RefreshTokenExpiresIn = refreshTokenExpiresIn;
this.Scope = scope;
this.Error = error;
this.ErrorDescription = errorDescription;
this.ErrorUri = errorUri;
}

/// <summary>
/// The type of OAuth token
/// </summary>
Expand All @@ -30,6 +43,25 @@ public OauthToken(string tokenType, string accessToken, IReadOnlyList<string> sc
/// </summary>
public string AccessToken { get; private set; }

/// <summary>
/// The amount of seconds, until the acces token expires.
/// </summary>
[Parameter(Key = "expires_in")]
public int ExpiresIn { get; private set; }

/// <summary>
/// The secret refresh token.
/// Use this to get a new access token, without going through the OAuth flow again.
/// </summary>
[Parameter(Key = "refresh_token")]
public string RefreshToken { get; private set; }

/// <summary>
/// The amount of seconds, until the refresh token expires.
/// </summary>
[Parameter(Key = "refresh_token_expires_in")]
public int RefreshTokenExpiresIn { get; private set; }

/// <summary>
/// The list of scopes the token includes.
/// </summary>
Expand Down

0 comments on commit 0c2025b

Please sign in to comment.