Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

support OAuth authentication #19

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,21 @@ api = Kintone::Api.new("example.cybozu.com", "Administrator", "cybozu")

# Use token authentication
api = Kintone::Api.new("example.cybozu.com", "authtoken")

# Use OAuth authentication
api = Kintone::OAuthApi.new("example.cybozu.com", "access_token")
# if set oauth options below, you can refresh the access_token.
oauth_options = {
client_id: 'client_id',
client_secret: 'client_secret',
refresh_token: 'refresh_token',
expires_at: 1599921045
}
api = Kintone::OAuthApi.new("example.cybozu.com", "access_token", oauth_options)
# get new token.
api.refresh!
api.access_token.token
# => "new_access_token"
```

### Supported API
Expand Down
1 change: 1 addition & 0 deletions kintone.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ Gem::Specification.new do |spec|
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
spec.require_paths = ['lib']

spec.add_runtime_dependency 'oauth2', '>=1.4.4'
spec.add_runtime_dependency 'faraday', '>=0.9.2'
spec.add_runtime_dependency 'faraday_middleware', '>=0.10.0'

Expand Down
1 change: 1 addition & 0 deletions lib/kintone.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
require 'kintone/version'
require 'kintone/api'
require 'kintone/oauth_api'
require 'kintone/type'

module Kintone
Expand Down
102 changes: 102 additions & 0 deletions lib/kintone/oauth_api.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
require 'oauth2'

class Kintone::OAuthApi < Kintone::Api
# @return [OAuth2::AccessToken]
attr_reader :access_token

# @param [String] domain the kintone's domain (e.g. 'foobar.cybozu.com').
# @param [String] token the Access Token value.
# @param [Hash] opts the options to create the Access Token with.
# @option opts [String] :client_id (nil) the client_id value.
# @option opts [String] :client_secret (nil) the client_secret value.
# @option opts [String] :refresh_token (nil) the refresh_token value.
# @option opts [FixNum, String] :expires_at (nil) the epoch time in seconds in which AccessToken will expire.
# @yield [builder] The Faraday connection builder
def initialize(domain, token, opts = {}, &block)
client_options = {
site: "https://#{domain}",
token_url: '/oauth2/token',
authorize_url: '/oauth2/authorization',
connection_build: connection_builder(&block)
}
client = ::OAuth2::Client.new(opts[:client_id], opts[:client_secret], client_options)
@access_token = ::OAuth2::AccessToken.new(client, token, refresh_token: opts[:refresh_token], expires_at: opts[:expires_at])
end

def refresh!
@access_token = @access_token.refresh!
end

def get(url, params = {})
opts = request_options(params: params, headers: nil)
request(:get, url, opts)
end

def post(url, body)
json_body = body.to_json if body.respond_to?(:to_json)
opts = request_options(body: json_body)
request(:post, url, opts)
end

def put(url, body)
json_body = body.to_json if body.respond_to?(:to_json)
opts = request_options(body: json_body)
request(:put, url, opts)
end

def delete(url, body = nil)
json_body = body.to_json if body.respond_to?(:to_json)
opts = request_options(body: json_body)
request(:delete, url, opts)
end

def post_file(url, path, content_type, original_filename)
body = { file: Faraday::UploadIO.new(path, content_type, original_filename) }
headers = { 'Content-Type' => 'multipart/form-data' }
opts = request_options(body: body, headers: headers)
res = request(:post, url, opts)
res['fileKey']
end

private

def connection_builder(&block)
lambda { |con|
con.request :url_encoded
con.request :multipart
# NOTE: comment out for avoiding following bug at OAuth2 v1.4.4.
# In 2.x the bug will be fixed.
# refer to https://github.com/oauth-xx/oauth2/pull/380
# con.response :json, content_type: /\bjson$/
block.call(con) if block_given?
}
end

def request(verb, url, opts)
response = @access_token.request(verb, url, opts)
validate_response(response)
rescue OAuth2::Error => e
response = e.response
raise Kintone::KintoneError.new(response.body, response.status)
end

def validate_response(response, expected_status = 200)
if response.status != expected_status
raise Kintone::KintoneError.new(response.body, response.status)
end

JSON.parse(response.body)
end

def request_options(params: nil, body: nil, headers: default_headers)
opts = {}
opts[:headers] = headers
opts[:params] = params if params
opts[:body] = body if body
opts
end

def default_headers
{ 'Content-Type' => 'application/json' }
end
end
191 changes: 191 additions & 0 deletions spec/kintone/oauth_api_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
require 'spec_helper'

describe Kintone::OAuthApi do
let(:domain) { 'www.example.com' }
let(:path) { '/k/v1/path' }
let(:token) { 'access_token' }
let(:request_verb) { nil }
let(:default_request_headers) { { 'Authorization' => "Bearer #{token}" } }
let(:option_request_headers) { nil }
let(:params) { nil }
let(:body) { nil }
let(:response_body) { nil }
let(:response_status) { 200 }

let(:add_stub_request) do
url = "https://#{domain}#{path}"
url += "?#{URI.encode_www_form(params)}" if params && !params.empty?

stub_request(request_verb, url) \
.with do |req|
req.body = body.to_json if body
headers = default_request_headers
headers.merge! option_request_headers if option_request_headers
req.headers = headers
end
.to_return(
body: response_body.to_json,
status: response_status,
headers: { 'Content-type' => 'application/json' }
)
end

context 'specify token argument only' do
let(:target) { Kintone::OAuthApi.new(domain, token) }

describe '#get' do
before(:each) do
add_stub_request
end
let(:request_verb) { :get }
let(:response_body) { { abc: 'def' } }
let(:response_status) { 200 }
subject { target.get(path, params) }

context 'with some params' do
let(:params) { { 'p1' => 'abc', 'p2' => 'def' } }
it { is_expected.to eq 'abc' => 'def' }
end

context 'with empty params' do
let(:params) { {} }
it { is_expected.to eq 'abc' => 'def' }
end

context 'with nil params' do
let(:params) { nil }
it { is_expected.to eq 'abc' => 'def' }
end

context 'fail to request when unexpected response status returns' do
let(:response_status) { 201 }
before(:each) do
add_stub_request
end
it { expect { subject }.to raise_error Kintone::KintoneError }
end

context 'fail to request' do
let(:response_status) { 500 }
let(:response_body) { { message: '不正なJSON文字列です。', id: '1505999166-897850006', code: 'CB_IJ01' } }
before(:each) do
add_stub_request
end
it { expect { subject }.to raise_error Kintone::KintoneError }
end
end

describe '#post' do
before(:each) do
add_stub_request
end
let(:request_verb) { :post }
let(:response_status) { 200 }
let(:response_body) { { abc: 'def' } }
let(:params) { nil }
let(:body) { { p1: 'abc', p2: 'def' } }
let(:option_headers) { { 'Content-Type' => 'application/json' } }
subject { target.post(path, body) }

it { is_expected.to eq 'abc' => 'def' }
end

describe '#put' do
before(:each) do
add_stub_request
end
let(:request_verb) { :put }
let(:response_status) { 200 }
let(:response_body) { { abc: 'def' } }
let(:params) { nil }
let(:body) { { p1: 'abc', p2: 'def' } }
let(:option_headers) { { 'Content-Type' => 'application/json' } }
subject { target.put(path, body) }

it { is_expected.to eq 'abc' => 'def' }
end

describe '#delete' do
before(:each) do
add_stub_request
end
let(:request_verb) { :delete }
let(:response_status) { 200 }
let(:response_body) { { abc: 'def' } }
let(:params) { nil }
let(:body) { { p1: 'abc', p2: 'def' } }
let(:option_headers) { { 'Content-Type' => 'application/json' } }
subject { target.delete(path, body) }

it { is_expected.to eq 'abc' => 'def' }
end

describe '#post_file' do
before(:each) do
add_stub_request
expect(Faraday::UploadIO).to receive(:new).with(file_path, content_type, original_filename).and_return(attachment)
end

subject { target.post_file(path, file_path, content_type, original_filename) }
let(:file_path) { '/path/to/file.txt' }
let(:content_type) { 'text/plain' }
let(:original_filename) { 'fileName.txt' }
let(:attachment) { double('attachment') }
let(:request_verb) { :post }
let(:response_status) { 200 }
let(:response_body) { { fileKey: 'abc' } }
let(:option_headers) { { 'Content-Type' => %r{multipart/form-data} } }

it { is_expected.to eq 'abc' }
end

describe '#refresh!' do
subject { target.refresh! }
it { expect { subject }.to raise_error RuntimeError }
end
end

context 'specify token and oauth_options arguments' do
let(:target) { Kintone::OAuthApi.new(domain, token, oauth_options) }
let(:oauth_options) do
{
client_id: 'client_id',
client_secret: 'client_secret',
refresh_token: 'refresh_token',
expires_at: 1_598_886_000
}
end
let(:access_token_response_body) do
{
access_token: 'new_access_token',
token_type: 'bearer',
expires_in: 3600,
scope: 'k:app_record:read k:app_record:write k:app_settings:read k:app_settings:write k:file:read k:file:write'
}
end
describe '#refresh!' do
before(:each) do
url = 'https://www.example.com/oauth2/token'
stub_request(:post, url)\
.with(
body: {
client_id: oauth_options[:client_id],
client_secret: oauth_options[:client_secret],
grant_type: 'refresh_token',
refresh_token: oauth_options[:refresh_token]
},
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
)
.to_return(
body: access_token_response_body.to_json,
status: 200,
headers: { 'Content-type' => 'application/json' }
)
end
subject { target.refresh!.token }
it { is_expected.to eq 'new_access_token' }
end
end
end