diff --git a/lib/octokit/client/users.rb b/lib/octokit/client/users.rb index 8f33bf6a3..34e557910 100644 --- a/lib/octokit/client/users.rb +++ b/lib/octokit/client/users.rb @@ -57,6 +57,33 @@ def exchange_code_for_token(code, app_id = client_id, app_secret = client_secret post "#{web_endpoint}login/oauth/access_token", options end + # Refresh a user's access token with a refresh token. + # + # Applications can refresh an access token without requiring a user to re-authorize using refresh access token. + # + # @param code [String] 40 character GitHub OAuth refresh access token + # + # @return [Sawyer::Resource] + # @see https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/refreshing-user-access-tokens#refreshing-a-user-access-token-with-a-refresh-token + # + # @example + # client = Octokit::Client.new(:client_id => 'abcdefg12345', :client_secret => 'secret') + # client.refresh_access_token('40-character-refresh-token') + def refresh_access_token(code, app_id = client_id, app_secret = client_secret, options = {}) + options = options.merge({ + refresh_token: code, + client_id: app_id, + client_secret: app_secret, + grant_type: 'refresh_token', + headers: { + content_type: 'application/json', + accept: 'application/json' + } + }) + + post "#{web_endpoint}login/oauth/access_token", options + end + # Validate user username and password # # @param options [Hash] User credentials diff --git a/spec/fixtures/renew_access_token.json b/spec/fixtures/renew_access_token.json new file mode 100644 index 000000000..91fb116ab --- /dev/null +++ b/spec/fixtures/renew_access_token.json @@ -0,0 +1,8 @@ +{ + "scope": "", + "expires_in": 28800, + "token_type": "bearer", + "access_token": "new_access_token", + "refresh_token": "new_refresh_token", + "refresh_token_expires_in": 15811200 +} diff --git a/spec/octokit/client/users_spec.rb b/spec/octokit/client/users_spec.rb index 5c8d779a6..91de8ccc0 100644 --- a/spec/octokit/client/users_spec.rb +++ b/spec/octokit/client/users_spec.rb @@ -267,6 +267,49 @@ end # with credentials passed as parameters end # .exchange_code_for_token + describe '.refresh_access_token' do + context 'with application authenticated client' do + it 'returns the access_token' do + request_body = { + refresh_token: 'code', + client_id: '123', + client_secret: '345', + grant_type: 'refresh_token' + } + + client = Octokit::Client.new( + client_id: request_body[:client_id], + client_secret: request_body[:client_secret] + ) + + request = stub_post('https://github.com/login/oauth/access_token') + .with( + basic_auth: [request_body[:client_id], request_body[:client_secret]], + body: request_body.to_json + ).to_return(json_response('renew_access_token.json')) + + response = client.refresh_access_token('code') + + expect(response.access_token).to eq 'new_access_token' + expect(response.refresh_token).to eq 'new_refresh_token' + assert_requested request + end + end # with application authenticated client + + context 'with credentials passed as parameters by unauthed client' do + it 'returns the access_token' do + client = Octokit::Client.new + post = stub_request(:post, 'https://github.com/login/oauth/access_token') + .with(body: { refresh_token: 'code', client_id: 'id', client_secret: 'secret', grant_type: 'refresh_token' }.to_json) + .to_return(json_response('renew_access_token.json')) + response = client.refresh_access_token('code', 'id', 'secret') + expect(response.access_token).to eq 'new_access_token' + expect(response.refresh_token).to eq 'new_refresh_token' + assert_requested post + end # with credentials passed as parameters + end # .refresh_access_token + end + describe '.migrations', :vcr do it 'starts a migration for a user' do result = @client.start_user_migration(['snakeoil-ceo/the-insecure'], lock_repositories: true)