mirror of
https://github.com/zoriya/octokit.net.git
synced 2025-12-06 07:16:09 +00:00
Support refreshtokens in OAuth flow (#2749)
* Support refreshtokens in OAuth flow Fixes #2731 * Added summary to OauthToken.cs constructors * Mark deprecation of non-refreshToken constructor for OauthToken * Remove unnecessary comment --------- Co-authored-by: Keegan Campbell <me@kfcampbell.com>
This commit is contained in:
@@ -46,5 +46,12 @@ namespace Octokit.Reactive
|
||||
/// <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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,10 @@ using System.Reactive.Threading.Tasks;
|
||||
|
||||
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;
|
||||
@@ -14,59 +18,30 @@ namespace Octokit.Reactive
|
||||
_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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using NSubstitute;
|
||||
@@ -208,4 +207,75 @@ public class OauthClientTests
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,5 +50,12 @@ namespace Octokit
|
||||
/// <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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ namespace Octokit
|
||||
/// <summary>
|
||||
/// Provides methods used in the OAuth web flow.
|
||||
/// </summary>
|
||||
/// <inheritdoc />
|
||||
public class OauthClient : IOauthClient
|
||||
{
|
||||
readonly IConnection connection;
|
||||
@@ -46,18 +47,6 @@ namespace Octokit
|
||||
.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)
|
||||
{
|
||||
@@ -71,15 +60,6 @@ namespace Octokit
|
||||
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)
|
||||
{
|
||||
@@ -93,15 +73,6 @@ namespace Octokit
|
||||
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)
|
||||
{
|
||||
@@ -140,5 +111,17 @@ namespace Octokit
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[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
Octokit/Models/Request/OauthTokenRenewalRequest.cs
Normal file
67
Octokit/Models/Request/OauthTokenRenewalRequest.cs
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using Octokit.Internal;
|
||||
@@ -10,6 +11,17 @@ namespace Octokit
|
||||
{
|
||||
public OauthToken() { }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="OauthToken"/> class.
|
||||
/// Use this constructor when you don't have a refreshToken.
|
||||
/// </summary>
|
||||
/// <param name="tokenType">The type of token returned by GitHub.</param>
|
||||
/// <param name="accessToken">The access token returned by GitHub.</param>
|
||||
/// <param name="scope">The auhtorization scope of the returned token.</param>
|
||||
/// <param name="error">The error code returned by the GitHub API.</param>
|
||||
/// <param name="errorDescription">The error message, if any, returned by the GitHub API.</param>
|
||||
/// <param name="errorUri">The GitHub documentation link, detailing the error message.</param>
|
||||
[Obsolete("This constructor is being deprecated and will be removed in the future. Use OauthToken.OauthToken (with refreshToken paramters) instead.")]
|
||||
public OauthToken(string tokenType, string accessToken, IReadOnlyList<string> scope, string error, string errorDescription, string errorUri)
|
||||
{
|
||||
this.TokenType = tokenType;
|
||||
@@ -20,6 +32,32 @@ namespace Octokit
|
||||
this.ErrorUri = errorUri;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="OauthToken"/> class.
|
||||
/// Use this constructor by default.
|
||||
/// </summary>
|
||||
/// <param name="tokenType">The type of token returned by GitHub.</param>
|
||||
/// <param name="accessToken">The access token returned by GitHub.</param>
|
||||
/// <param name="expiresIn">The amount of seconds, before the access token expires.</param>
|
||||
/// <param name="refreshToken">The refresh token returned by GitHub. Use this, to get a new access token if it expires.</param>
|
||||
/// <param name="refreshTokenExpiresIn">The amount of seconds, before the refresh token expires.</param>
|
||||
/// <param name="scope">The auhtorization scope of the returned token.</param>
|
||||
/// <param name="error">The error code returned by the GitHub API.</param>
|
||||
/// <param name="errorDescription">The error message, if any, returned by the GitHub API.</param>
|
||||
/// <param name="errorUri">The GitHub documentation link, detailing the error message.</param>
|
||||
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>
|
||||
@@ -30,6 +68,25 @@ namespace Octokit
|
||||
/// </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>
|
||||
|
||||
Reference in New Issue
Block a user