diff --git a/.gitignore b/.gitignore index c3301b3..d784380 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,4 @@ pkg/* tmp/* .env .env-* +vendor/bundle diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f8a6f5..3c9b757 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ Your contribution here. * [#000](https://github.com/bigcommerce/bigcommerce-api-ruby/pull/000): Brief description here. - [@username](https://github.com/username). +* [#128](https://github.com/bigcommerce/bigcommerce-api-ruby/pull/128): Added support for token generation for storefront login. - [@mattolson](https://github.com/mattolson). ## 1.0.0 Please note that this is the start of a new major release which breaks all backward compatibility. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 12a2d8e..c8f82ab 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,7 +1,6 @@ # Contributing to Bigcommerce - -We would love to see contributors! You're encouraged to submit [pull requests](https://github.com/bigcommerce/bigcommerce-api-ruby/pulls), [propose features, and discuss issues](https://github.com/bigcommerce/bigcommerce-api-ruby/issues). +We welcome your contribution! You're encouraged to submit [pull requests](https://github.com/bigcommerce/bigcommerce-api-ruby/pulls), [propose features, and discuss issues](https://github.com/bigcommerce/bigcommerce-api-ruby/issues). #### Fork the Project diff --git a/DEPENDENCIES.md b/DEPENDENCIES.md index fc8d29b..c0cdf75 100644 --- a/DEPENDENCIES.md +++ b/DEPENDENCIES.md @@ -5,3 +5,4 @@ Many thanks to the contributors and authors of the following libraries! - [Faraday](https://github.com/lostisland/faraday) Simple, but flexible HTTP client library, with support for multiple backends. - [MIT](https://github.com/lostisland/faraday/blob/master/LICENSE.md) - [Faraday Middleware](https://github.com/lostisland/faraday_middleware) Various Faraday middlewares for Faraday-based API wrappers. - [MIT](https://github.com/lostisland/faraday_middleware/blob/master/LICENSE.md) - [Hashie](https://github.com/intridea/hashie) Hashie is a collection of classes and mixins that make hashes more powerful. - [MIT](https://github.com/intridea/hashie/blob/master/LICENSE) +- [JWT](https://github.com/jwt/ruby-jwt) Used to encode and sign JWT tokens for the Customer Login API. - [MIT](https://github.com/jwt/ruby-jwt/blob/master/LICENSE) diff --git a/README.md b/README.md index 7128528..ce0b8f3 100644 --- a/README.md +++ b/README.md @@ -92,6 +92,20 @@ For more information about configuring SSL with Faraday, please see the followin - [Faraday SSL example](https://gist.github.com/mislav/938183) - [Faraday: Setting up SSL certificates](https://github.com/lostisland/faraday/wiki/Setting-up-SSL-certificates) +### Customer Login API +If you want to generate tokens for storefront login using the Customer Login API, you need to configure your app's client secret. + +- ```store_hash```: The store hash of the store you are operating against. +- ```client_id```: Obtained from the on the BigCommerce [Developer Portal's](http://developer.bigcommerce.com) "My Apps" section. +- ```client_secret```: Obtained from the on the BigCommerce [Developer Portal's](http://developer.bigcommerce.com) "My Apps" section. + +```rb +Bigcommerce.configure do |config| + config.store_hash = ENV['BC_STORE_HASH'] + config.client_id = ENV['BC_CLIENT_ID'] + config.client_secret = ENV['BC_CLIENT_SECRET'] +end +``` ## Usage For full examples of using the API client, please see the [examples folder](examples) and refer to BigCommerce's [developer documentation](https://developer.bigcommerce.com/api). @@ -124,8 +138,8 @@ This connection is nothing more than a `Faraday::Connection` – so if you want ```rb connection = Bigcommerce::Connection.build( Bigcommerce::Config.new( - store_hash: ENV['BC_STORE_HASH'], - client_id: ENV['BC_CLIENT_ID'], + store_hash: ENV['BC_STORE_HASH'], + client_id: ENV['BC_CLIENT_ID'], access_token: ENV['BC_ACCESS_TOKEN'] ) ) @@ -143,7 +157,7 @@ Bigcommerce::System.raw_request(:get, 'time', connection: connection) ```rb connection_legacy = Bigcommerce::Connection.build( Bigcommerce::Config.new( - auth: 'legacy', + auth: 'legacy', url: ENV['BC_API_ENDPOINT_LEGACY'], username: ENV['BC_USERNAME'], api_key: ENV['BC_API_KEY'] @@ -159,4 +173,4 @@ Bigcommerce::System.raw_request(:get, 'time', connection: connection_legacy) ``` ## Contributing -See [CONTRIBUTING.md](CONTRIBUTING.md) \ No newline at end of file +See [CONTRIBUTING.md](CONTRIBUTING.md) diff --git a/bigcommerce.gemspec b/bigcommerce.gemspec index b7fbf3f..c687c4c 100644 --- a/bigcommerce.gemspec +++ b/bigcommerce.gemspec @@ -23,4 +23,5 @@ Gem::Specification.new do |s| s.add_dependency 'faraday', '~> 0.9' s.add_dependency 'faraday_middleware', '~> 0.10.0' s.add_dependency 'hashie', '~> 3.4' + s.add_dependency 'jwt', '~> 1.5.4' end diff --git a/examples/customers/customer_login.rb b/examples/customers/customer_login.rb new file mode 100644 index 0000000..b77040c --- /dev/null +++ b/examples/customers/customer_login.rb @@ -0,0 +1,14 @@ +require 'bigcommerce' + +Bigcommerce.configure do |config| + config.store_hash = ENV['BC_STORE_HASH'] + config.client_id = ENV['BC_CLIENT_ID'] + config.client_secret = ENV['BC_CLIENT_SECRET'] + config.access_token = ENV['BC_ACCESS_TOKEN'] +end + +# Get a customer +customer = Bigcommerce::Customer.all(page: 1).first + +# Generate token login url +puts customer.login_token diff --git a/lib/bigcommerce.rb b/lib/bigcommerce.rb index 9cfde3f..67e02c7 100644 --- a/lib/bigcommerce.rb +++ b/lib/bigcommerce.rb @@ -12,7 +12,7 @@ module Bigcommerce Dir.glob(resources, &method(:require)) class << self - attr_reader :api + attr_reader :api, :config def configure @config = Bigcommerce::Config.new.tap { |h| yield(h) } diff --git a/lib/bigcommerce/resources/customers/customer.rb b/lib/bigcommerce/resources/customers/customer.rb index 76eb174..18ed677 100644 --- a/lib/bigcommerce/resources/customers/customer.rb +++ b/lib/bigcommerce/resources/customers/customer.rb @@ -1,3 +1,6 @@ +require 'jwt' +require 'securerandom' + # Customer # Identity and account details for customers shopping at a Bigcommerce store. # https://developer.bigcommerce.com/api/stores/v2/customers @@ -26,5 +29,21 @@ class Customer < Resource def self.count(params = {}) get 'customers/count', params end + + # Generate a token that can be used to log the customer into the storefront. + # This requires your app to have the store_v2_customers_login scope and to + # be installed in the store. + def login_token(config: Bigcommerce.config) + payload = { + 'iss' => config.client_id, + 'iat' => Time.now.to_i, + 'jti' => SecureRandom.uuid, + 'operation' => 'customer_login', + 'store_hash' => config.store_hash, + 'customer_id' => id + } + + JWT.encode(payload, config.client_secret, 'HS256') + end end end diff --git a/spec/bigcommerce/unit/resources/customers/customer_spec.rb b/spec/bigcommerce/unit/resources/customers/customer_spec.rb index d6f69fa..3258665 100644 --- a/spec/bigcommerce/unit/resources/customers/customer_spec.rb +++ b/spec/bigcommerce/unit/resources/customers/customer_spec.rb @@ -1,10 +1,35 @@ RSpec.describe Bigcommerce::Customer do - before(:each) { @customer = Bigcommerce::Customer } - describe '.count' do it 'should hit the correct path' do - expect(@customer).to receive(:get).with('customers/count', {}) - @customer.count + expect(described_class).to receive(:get).with('customers/count', {}) + described_class.count + end + end + + describe '.login_token' do + let(:client_id) { SecureRandom.hex(6) } + let(:client_secret) { SecureRandom.hex(6) } + let(:store_hash) { SecureRandom.hex(4) } + let(:customer_id) { Random.rand(1000) } + let(:customer) { described_class.new(id: customer_id) } + subject { customer.login_token } + + before do + Bigcommerce.configure do |config| + config.store_hash = store_hash + config.client_id = client_id + config.client_secret = client_secret + end + end + + it 'should generate a signed token with the right payload' do + payload = JWT.decode(subject, client_secret, true, { :algorithm => 'HS256' })[0] + expect(payload['iss']).to eq(client_id) + expect(payload['store_hash']).to eq(store_hash) + expect(payload['operation']).to eq('customer_login') + expect(payload['customer_id']).to eq(customer_id) + expect(payload['iat']).to be <= Time.now.to_i + expect(payload['jti']).to_not be_empty end end end