seraph-grape 0.0.2 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: d55a4857daeb4971ba60fc79425fb416c3266160
4
- data.tar.gz: 277ca87e4207dfe43077c25b1067e14b13c949b0
3
+ metadata.gz: 03a4b653da5a18ec6b73dbf456a85a02c28b686d
4
+ data.tar.gz: 68b5a3a130382c8cefb255661d557100a4583f1c
5
5
  SHA512:
6
- metadata.gz: 79892107698a485403c3d1b905ebcb6fcc33e9f09c8f9a22d335e4267522b0c6952b18a2e88f260d0f1cbf426f7198d2ffb0cc79e47663459b663b0054748dd3
7
- data.tar.gz: 0b1ad221976c4cd0d00edb52095449728a797da6583456b098cbdc2d22fe44473ee99e62edd59d5ecf9e03a275df16084c5de872900e57374b11e7afaa48590f
6
+ metadata.gz: 4e50ec8975c0ad5511945c00027bcb881f7a5974c6df9a03c7aa486ecf127aa52bb18410d112c4b8c082935d2d6b17f3f9de3af09226a516d8a4fd3ff3c62b04
7
+ data.tar.gz: 17871012d507a09b41ecdad49061f3aa21e7a1b7b1daa539ee0dabbe5222b7488a2229341fdca406aef3ea2881dafd92de6b15500768b86b34a39fe943ad4001
data/.travis.yml CHANGED
@@ -6,6 +6,6 @@ rvm:
6
6
  - 2.1.9
7
7
  - 2.2.5
8
8
  - 2.3.1
9
- # addons:
10
- # code_climate:
11
- # repo_token: 6194b4057a3a760a2b37afe44119bb0ecb7d34f34087c076a8cc84c38e48826a
9
+ addons:
10
+ code_climate:
11
+ repo_token: ce8cd9bca51ed24656d96784bfaf46e8dd0f9a9b18ae0bacc9fa2b0411ced09b
data/README.md CHANGED
@@ -1,7 +1,15 @@
1
1
  # Seraph Grape integration
2
2
  [![Build Status](https://secure.travis-ci.org/Szeliga/seraph-grape.svg?branch=master)](https://travis-ci.org/Szeliga/seraph-grape)
3
+ [![Code Climate](https://codeclimate.com/github/Szeliga/seraph-grape/badges/gpa.svg)](https://codeclimate.com/github/Szeliga/seraph-grape)
4
+ [![Test Coverage](https://codeclimate.com/github/Szeliga/seraph-grape/badges/coverage.svg)](https://codeclimate.com/github/Szeliga/seraph-grape/coverage)
3
5
 
4
- [Seraph](https://github.com/Szeliga/seraph) helpers for Grape.
6
+ [Seraph](https://github.com/Szeliga/seraph) helpers for Grape. Provides several simple helpers for implementing a JWT token authentication.
7
+
8
+ A sample usage in Grape can be found in [DummyApi Grape Endpoint](spec/support/dummy_api.rb).
9
+
10
+ Just like Seraph, this integration doesn't make any assumptions about your stuck. It does however make a small assumption about the authenticated User resource. It requires that this resource responds to two messages - `id` and `encrypted_password`. This is required for authentications and generating of JWT tokens. This resource can be anything from a `Struct`, `OpenStruct` to an `ActiveRecord` model.
11
+
12
+ For more information on what Seraph offers, visit the [projects repo](https://github.com/Szeliga/seraph).
5
13
 
6
14
  ## Installation
7
15
 
@@ -15,6 +23,48 @@ gem 'seraph-grape'
15
23
  ```
16
24
  inside your `Gemfile`
17
25
 
26
+ ## Configuration
27
+
28
+ In addition to the configuration fields that [Seraph](https://github.com/Szeliga/seraph#configuration) provides, the Seraph Grape integration gem adds another option called `api_secret`. This option is used by the JWT library as a secret passed to the hashing algorithm (HS256) to encode the token. **It is strongly recommended that this is set!**
29
+
30
+ ``` ruby
31
+ Seraph.configure do |config|
32
+ config.api_secret = 'GENERATED-SECRET'
33
+ end
34
+ ```
35
+
36
+ The secret can be generated in the same manner, the pepper is generated in [Seraph](https://github.com/Szeliga/seraph#configuration).
37
+
38
+ ## What do you get?
39
+
40
+ The library provides several things.
41
+
42
+ ### Grape helpers
43
+
44
+ 1. `authenticate!` - checks the `Authorization` header of the incoming request for a JWT token, if it finds a non-emtpy string in the header, it attempts to decode the token. If the decoding fails, a 401 error is returned instead.
45
+ 2. `auth_info` - contains the `user_id` to whom the token was issued. Returns a Hash of the form `{ user_id: 1 }`.
46
+ 3. `sign_in(user, password)` - takes a user resource (previously found by the e-mail, or other login information, posted to the sign in endpoint), and a plaintext password (also provided in the information posted to the sign in endpoint). Checks if the plaintext password matches the encrypted one in the `user` resource, using [Seraph's Password Comparator class](https://github.com/Szeliga/seraph#comparing-a-provided-password-with-the-encrypted-one). If the passwords match, it returns a JWT token for that `user` (using it's `id` method). If they do not, it raises a 401 error.
47
+
48
+ ### Authenticator
49
+
50
+ The integration offers also `Seraph::Grape::Authenticator`. The `sign_in` Grape helper delegates to this class in order to do the authentication, as described above. You can invoke the class like this:
51
+
52
+ ``` ruby
53
+ Seraph::Grape::Authenticator.call(user, password)
54
+ ```
55
+
56
+ As stated above, if the passwords match, it returns a JWT token for the passed in `user`. If they do not, false is returned instead.
57
+
58
+ **Note** - this class will be probably moved to Seraph core in the near future, as it isn't grape-specific and could be used in other setups (`rails-api` for example).
59
+
60
+ ## Roadmap
61
+
62
+ ### Version 1.0.0
63
+ * move out `Seraph::Grape::Authenticator` to the core gem
64
+ * expose basic JWT options via configuration - algorithm, etc.
65
+ * implement JWT token [Expiration Time Claim](https://github.com/jwt/ruby-jwt#expiration-time-claim)
66
+ * implement JWT [Subject Claim](https://github.com/jwt/ruby-jwt#subject-claim) alongside subject verification option for decoding
67
+
18
68
  ## Copyright
19
69
 
20
70
  Copyright (c) 2016 Szymon Szeliga
data/lib/seraph-grape.rb CHANGED
@@ -1 +1,2 @@
1
+ require 'seraph'
1
2
  require 'seraph/grape'
data/lib/seraph/grape.rb CHANGED
@@ -1,2 +1,5 @@
1
1
  require 'seraph/grape/version'
2
2
  require 'seraph/grape/configuration'
3
+ require 'seraph/grape/helpers'
4
+ require 'seraph/grape/jwt'
5
+ require 'seraph/grape/authenticator'
@@ -0,0 +1,33 @@
1
+ module Seraph
2
+ module Grape
3
+ class Authenticator
4
+ private_class_method :new
5
+
6
+ def self.call(user, password)
7
+ new(user, password).call
8
+ end
9
+
10
+ def initialize(user, password)
11
+ @user = user
12
+ @password = password
13
+ end
14
+
15
+ def call
16
+ return jwt_token if password_valid?
17
+ false
18
+ end
19
+
20
+ private
21
+
22
+ attr_reader :user, :password
23
+
24
+ def jwt_token
25
+ Seraph::Grape::JWT.encode(user_id: user.id)
26
+ end
27
+
28
+ def password_valid?
29
+ Seraph::PasswordComparator.call(user.encrypted_password, password)
30
+ end
31
+ end
32
+ end
33
+ end
@@ -1,7 +1,10 @@
1
- require 'seraph/configuration'
2
-
3
1
  module Seraph
4
2
  class Configuration
5
- attr_accessor :authenticator
3
+ attr_accessor :api_secret
4
+
5
+ def reset
6
+ @api_secret = nil
7
+ @pepper = nil
8
+ end
6
9
  end
7
10
  end
@@ -0,0 +1,23 @@
1
+ module Seraph
2
+ module Grape
3
+ module Helpers
4
+ def authenticate!
5
+ unauthorized! if auth_info.nil?
6
+ end
7
+
8
+ def auth_info
9
+ @auth_info ||= Seraph::Grape::JWT.decode(headers['Authorization'])
10
+ end
11
+
12
+ def sign_in(user, password)
13
+ result = Seraph::Grape::Authenticator.call(user, password)
14
+ unauthorized! unless result
15
+ result
16
+ end
17
+
18
+ def unauthorized!
19
+ error!('Unauthorized', 401)
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,26 @@
1
+ require 'jwt'
2
+
3
+ module Seraph
4
+ module Grape
5
+ module JWT
6
+ def encode(payload)
7
+ ::JWT.encode(payload, secret)
8
+ end
9
+ module_function :encode
10
+
11
+ def decode(token)
12
+ ::JWT.decode(token, secret).first
13
+ rescue
14
+ nil
15
+ end
16
+ module_function :decode
17
+
18
+ private
19
+
20
+ def secret
21
+ String(Seraph.configuration.api_secret)
22
+ end
23
+ module_function :secret
24
+ end
25
+ end
26
+ end
@@ -1,5 +1,5 @@
1
1
  module Seraph
2
2
  module Grape
3
- VERSION = '0.0.2'.freeze
3
+ VERSION = '0.1.0'.freeze
4
4
  end
5
5
  end
data/seraph-grape.gemspec CHANGED
@@ -8,8 +8,8 @@ require 'English'
8
8
  Gem::Specification.new do |gem|
9
9
  gem.name = 'seraph-grape'
10
10
  gem.version = Seraph::Grape::VERSION
11
- gem.summary = ''
12
- gem.description = ''
11
+ gem.summary = 'Seraph helpers for Grape'
12
+ gem.description = 'Integrate Seraph with your Grape API'
13
13
  gem.license = 'MIT'
14
14
  gem.authors = ['Szymon Szeliga']
15
15
  gem.email = 'szeliga.szymon@gmail.com'
@@ -32,7 +32,9 @@ Gem::Specification.new do |gem|
32
32
 
33
33
  gem.add_runtime_dependency 'grape', '~> 0.16'
34
34
  gem.add_runtime_dependency 'seraph', '~> 0.0'
35
- gem.add_development_dependency 'rake', '~> 10.0'
35
+ gem.add_runtime_dependency 'jwt', '~> 1.5'
36
+ gem.add_development_dependency 'rake', '~> 11.2'
37
+ gem.add_development_dependency 'rack-test', '~> 0.6'
36
38
  gem.add_development_dependency 'rspec', '~> 3.0'
37
39
  gem.add_development_dependency 'rubygems-tasks', '~> 0.2'
38
40
  gem.add_development_dependency 'fuubar', '~> 2.0'
@@ -0,0 +1,24 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe Seraph::Grape::Authenticator do
4
+ describe '#call' do
5
+ subject(:authenticator) { described_class.call(user, password) }
6
+ let(:user) do
7
+ DummyUser.new(1, Seraph::PasswordEncryptor.call('foobar12'))
8
+ end
9
+
10
+ context 'when password is valid' do
11
+ let(:password) { 'foobar12' }
12
+
13
+ it 'returns a JWT token with the user id in the payload' do
14
+ expect(authenticator).to eq Seraph::Grape::JWT.encode(user_id: user.id)
15
+ end
16
+ end
17
+
18
+ context 'when password is invalid' do
19
+ let(:password) { 'invalid_password' }
20
+
21
+ it { is_expected.to be_falsey }
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,66 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe Seraph::Grape::Helpers do
4
+ include Rack::Test::Methods
5
+
6
+ def app
7
+ DummyApi.new
8
+ end
9
+
10
+ describe '#authenticate!' do
11
+ before do
12
+ Seraph.configure do |config|
13
+ config.api_secret = 'secret'
14
+ end
15
+ header('Authorization', jwt)
16
+ end
17
+ let(:user) { DummyUser.new(1, '123') }
18
+
19
+ context 'when authentication is successful' do
20
+ let(:jwt) { Seraph::Grape::JWT.encode(user_id: user.id) }
21
+
22
+ it 'returns a 200 response' do
23
+ get '/dummy_api/private'
24
+ expect(last_response.status).to eq(200)
25
+ expect(JSON.parse(last_response.body)).to eq 'status' => 'authenticated'
26
+ end
27
+
28
+ it 'assigns the payload decoded from the Authorization header' do
29
+ get '/dummy_api/private_info'
30
+ expect(last_response.status).to eq(200)
31
+ expect(JSON.parse(last_response.body)).to eq 'user_id' => user.id
32
+ end
33
+ end
34
+
35
+ context 'when authentication is unsuccessful' do
36
+ let(:jwt) { '' }
37
+
38
+ it 'returns a 401 response' do
39
+ get '/dummy_api/private'
40
+ expect(last_response.status).to eq(401)
41
+ end
42
+ end
43
+ end
44
+
45
+ describe '#sign_in' do
46
+ let(:user) { DummyUser.new(1, Seraph::PasswordEncryptor.call('foobar12')) }
47
+
48
+ context 'when valid credentials are passed' do
49
+ it 'returns the JWT token' do
50
+ post '/dummy_api/sign_in', email: 'dummyuser@gmail.com', password: 'foobar12'
51
+
52
+ expect(last_response.status).to eq(200)
53
+ expect(JSON.parse(last_response.body)).to eq(
54
+ 'jwt' => Seraph::Grape::JWT.encode(user_id: user.id)
55
+ )
56
+ end
57
+ end
58
+
59
+ context 'when invalid credentials are passed' do
60
+ it 'returns status 401' do
61
+ post '/dummy_api/sign_in', email: 'dummyuser@gmail.com', password: 'invalid'
62
+ expect(last_response.status).to eq(401)
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,29 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe Seraph::Grape::JWT do
4
+ describe '#encode' do
5
+ let(:payload) { {} }
6
+ subject(:token) { described_class.encode(payload) }
7
+
8
+ context 'when api_secret configuration option is set' do
9
+ let(:api_secret) { 'secret' }
10
+ before do
11
+ Seraph.configure do |config|
12
+ config.api_secret = api_secret
13
+ end
14
+ end
15
+
16
+ it 'returns an encoded JWT token' do
17
+ expect(token).to eq JWT.encode(payload, api_secret)
18
+ end
19
+ end
20
+
21
+ context 'when api_secret configuration option is not set' do
22
+ let(:api_secret) { nil }
23
+
24
+ it 'returns an encoded JWT token' do
25
+ expect(token).to eq JWT.encode(payload, '')
26
+ end
27
+ end
28
+ end
29
+ end
data/spec/spec_helper.rb CHANGED
@@ -1,6 +1,8 @@
1
+ require 'pry'
1
2
  require 'rspec'
2
- require 'seraph/grape/version'
3
3
  require 'codeclimate-test-reporter'
4
+ require 'seraph'
5
+ require 'seraph/grape'
4
6
 
5
7
  Dir['./spec/support/**/*.rb'].sort.each { |f| require f }
6
8
 
@@ -15,6 +17,10 @@ RSpec.configure do |config|
15
17
  mocks.verify_partial_doubles = true
16
18
  end
17
19
 
20
+ config.before(:each) do
21
+ Seraph.configuration.reset
22
+ end
23
+
18
24
  config.order = :random
19
25
  Kernel.srand config.seed
20
26
  end
@@ -0,0 +1,38 @@
1
+ require 'rack/test'
2
+ require 'grape'
3
+ require 'seraph/grape/helpers'
4
+
5
+ class DummyApi < Grape::API
6
+ helpers Seraph::Grape::Helpers
7
+ prefix :dummy_api
8
+ format :json
9
+
10
+ get :private do
11
+ authenticate!
12
+ { status: 'authenticated' }
13
+ end
14
+
15
+ get :private_info do
16
+ authenticate!
17
+ auth_info
18
+ end
19
+
20
+ params do
21
+ requires :email, type: String
22
+ requires :password, type: String
23
+ end
24
+ post :sign_in do
25
+ declared_params = declared(params)
26
+
27
+ # Perform logic to find user by posted e-mail
28
+ # - usually user = User.find_by_email(declared_params[:email])
29
+ #
30
+ # Here we just create a new instance of DummyUser
31
+ # the same way we created it in the test
32
+ user = DummyUser.new(1, Seraph::PasswordEncryptor.call('foobar12'))
33
+
34
+ token = sign_in(user, declared_params[:password])
35
+ status 200
36
+ { jwt: token }
37
+ end
38
+ end
@@ -0,0 +1 @@
1
+ DummyUser = Struct.new(:id, :encrypted_password)
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: seraph-grape
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.2
4
+ version: 0.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Szymon Szeliga
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-06-17 00:00:00.000000000 Z
11
+ date: 2016-06-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: grape
@@ -38,20 +38,48 @@ dependencies:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
40
  version: '0.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: jwt
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '1.5'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '1.5'
41
55
  - !ruby/object:Gem::Dependency
42
56
  name: rake
43
57
  requirement: !ruby/object:Gem::Requirement
44
58
  requirements:
45
59
  - - "~>"
46
60
  - !ruby/object:Gem::Version
47
- version: '10.0'
61
+ version: '11.2'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '11.2'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rack-test
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '0.6'
48
76
  type: :development
49
77
  prerelease: false
50
78
  version_requirements: !ruby/object:Gem::Requirement
51
79
  requirements:
52
80
  - - "~>"
53
81
  - !ruby/object:Gem::Version
54
- version: '10.0'
82
+ version: '0.6'
55
83
  - !ruby/object:Gem::Dependency
56
84
  name: rspec
57
85
  requirement: !ruby/object:Gem::Requirement
@@ -122,7 +150,7 @@ dependencies:
122
150
  - - "~>"
123
151
  - !ruby/object:Gem::Version
124
152
  version: '0.1'
125
- description: ''
153
+ description: Integrate Seraph with your Grape API
126
154
  email: szeliga.szymon@gmail.com
127
155
  executables: []
128
156
  extensions: []
@@ -134,18 +162,25 @@ files:
134
162
  - ".rubocop.yml"
135
163
  - ".ruby-version"
136
164
  - ".travis.yml"
137
- - ChangeLog.md
138
165
  - Gemfile
139
166
  - LICENSE.txt
140
167
  - README.md
141
168
  - Rakefile
142
169
  - lib/seraph-grape.rb
143
170
  - lib/seraph/grape.rb
171
+ - lib/seraph/grape/authenticator.rb
144
172
  - lib/seraph/grape/configuration.rb
173
+ - lib/seraph/grape/helpers.rb
174
+ - lib/seraph/grape/jwt.rb
145
175
  - lib/seraph/grape/version.rb
146
176
  - seraph-grape.gemspec
177
+ - spec/seraph/grape/authenticator_spec.rb
178
+ - spec/seraph/grape/helpers_spec.rb
179
+ - spec/seraph/grape/jwt_spec.rb
147
180
  - spec/seraph/grape_spec.rb
148
181
  - spec/spec_helper.rb
182
+ - spec/support/dummy_api.rb
183
+ - spec/support/dummy_user.rb
149
184
  homepage: https://rubygems.org/gems/seraph-grape
150
185
  licenses:
151
186
  - MIT
@@ -169,7 +204,12 @@ rubyforge_project:
169
204
  rubygems_version: 2.5.1
170
205
  signing_key:
171
206
  specification_version: 4
172
- summary: ''
207
+ summary: Seraph helpers for Grape
173
208
  test_files:
209
+ - spec/seraph/grape/authenticator_spec.rb
210
+ - spec/seraph/grape/helpers_spec.rb
211
+ - spec/seraph/grape/jwt_spec.rb
174
212
  - spec/seraph/grape_spec.rb
175
213
  - spec/spec_helper.rb
214
+ - spec/support/dummy_api.rb
215
+ - spec/support/dummy_user.rb
data/ChangeLog.md DELETED
@@ -1,4 +0,0 @@
1
- ### 0.1.0 / 2016-06-16
2
-
3
- * Initial release:
4
-