seraph-grape 0.0.2 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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
-