jwt-bouncer 0.1.1

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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 9d0c3be8cdfa58b6baa7ec47c8df5f3a5efc823e
4
+ data.tar.gz: ebd73773ea8d3feb01635c12a4baa43817e07589
5
+ SHA512:
6
+ metadata.gz: 1fec43fdf1bbf19cebe162727c8ab454ce6f6c1dbc529489f55f11df0da5876f508ead002f1f4ad104bb2fb8806e1b1c75330af45c7ece6259f581d33759f108
7
+ data.tar.gz: 05af534ca792ae6a2e26621f002b31b4697152671f0edc713d99342dad52426d999edfbee4e9909b6040b91c9c6cd1ed387cd0e099845ab8e31d44255c3461c5
@@ -0,0 +1,9 @@
1
+ # Ignore bundler config.
2
+ /.bundle
3
+
4
+ # Ignore all logfiles and tempfiles.
5
+ /log/*
6
+ /tmp/*
7
+
8
+ # Ignore annoying Mac files
9
+ **.DS_Store
@@ -0,0 +1,23 @@
1
+ FROM ruby:2.4-slim
2
+ LABEL maintainer "ryan@ryantownsend.co.uk"
3
+
4
+ # Install basic packages
5
+ RUN apt-get update -qq && apt-get install -y --no-install-recommends \
6
+ build-essential \
7
+ curl \
8
+ git
9
+
10
+ # Configure the main working directory
11
+ ENV app /app
12
+ RUN mkdir $app
13
+ WORKDIR $app
14
+
15
+ # Set the where to install gems
16
+ ENV GEM_HOME /rubygems
17
+ ENV BUNDLE_PATH /rubygems
18
+
19
+ # Link the whole application up
20
+ ADD . $app
21
+
22
+ # Ensure our gems are installed when running commands
23
+ ENTRYPOINT bin/docker/entrypoint $0 $@
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in jwt-bouncer.gemspec
4
+ gemspec
@@ -0,0 +1,59 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ jwt-bouncer (0.1.0)
5
+ jwt (~> 1.5, >= 1.5.6)
6
+ rack (~> 2.0)
7
+
8
+ GEM
9
+ remote: https://rubygems.org/
10
+ specs:
11
+ activesupport (5.0.1)
12
+ concurrent-ruby (~> 1.0, >= 1.0.2)
13
+ i18n (~> 0.7)
14
+ minitest (~> 5.1)
15
+ tzinfo (~> 1.1)
16
+ coderay (1.1.1)
17
+ concurrent-ruby (1.0.4)
18
+ diff-lcs (1.3)
19
+ i18n (0.8.1)
20
+ jwt (1.5.6)
21
+ method_source (0.8.2)
22
+ minitest (5.10.1)
23
+ pry (0.10.4)
24
+ coderay (~> 1.1.0)
25
+ method_source (~> 0.8.1)
26
+ slop (~> 3.4)
27
+ rack (2.0.1)
28
+ rake (10.5.0)
29
+ rspec (3.5.0)
30
+ rspec-core (~> 3.5.0)
31
+ rspec-expectations (~> 3.5.0)
32
+ rspec-mocks (~> 3.5.0)
33
+ rspec-core (3.5.4)
34
+ rspec-support (~> 3.5.0)
35
+ rspec-expectations (3.5.0)
36
+ diff-lcs (>= 1.2.0, < 2.0)
37
+ rspec-support (~> 3.5.0)
38
+ rspec-mocks (3.5.0)
39
+ diff-lcs (>= 1.2.0, < 2.0)
40
+ rspec-support (~> 3.5.0)
41
+ rspec-support (3.5.0)
42
+ slop (3.6.0)
43
+ thread_safe (0.3.5)
44
+ tzinfo (1.2.2)
45
+ thread_safe (~> 0.1)
46
+
47
+ PLATFORMS
48
+ ruby
49
+
50
+ DEPENDENCIES
51
+ activesupport (~> 5.0, >= 5.0.1)
52
+ bundler (~> 1.14)
53
+ jwt-bouncer!
54
+ pry (~> 0.10)
55
+ rake (~> 10.0)
56
+ rspec (~> 3.5)
57
+
58
+ BUNDLED WITH
59
+ 1.14.3
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2017 Shift Commerce Ltd
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
6
+ this software and associated documentation files (the "Software"), to deal in
7
+ the Software without restriction, including without limitation the rights to
8
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9
+ the Software, and to permit persons to whom the Software is furnished to do so,
10
+ subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17
+ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,63 @@
1
+ [![JWT Compatible](https://jwt.io/assets/badge-compatible.svg)](https://jwt.io/)
2
+
3
+ JwtBouncer is an abstraction for JWT-based authentication/authorisation.
4
+
5
+ ### Usage
6
+
7
+ #### Parsing incoming requests
8
+
9
+ JwtBouncer includes a request parser which accepts a standard Rack request object and provides automated decoding and verification of the JWT.
10
+
11
+ You can then call various methods on this for authorisation. It's best demonstrated through an example:
12
+
13
+ ```ruby
14
+ require 'jwt_bouncer/request'
15
+
16
+ r = JwtBouncer::Request.new(request)
17
+
18
+ # is the token valid
19
+ r.authenticated?
20
+
21
+ # checks whether permissions has this key and it's truthy
22
+ r.can?(:update_product)
23
+
24
+ # who is authenticated, returns a hash of data
25
+ r.actor
26
+
27
+ # access the raw permissions hash
28
+ r.permissions
29
+ ```
30
+
31
+ #### Signing outbound requests
32
+
33
+ JwtBouncer isn't currently designed to provide extensive JWT signing, however it does provide a small service object for signing outbound requests. This can be useful for test suites where you may want to fake a JWT signed request.
34
+
35
+ ```ruby
36
+ require 'jwt_bouncer/sign_request'
37
+
38
+ # assuming you're using rspec-mocks here, but this could be a real request
39
+ request = double(:request, headers: {})
40
+
41
+ # some data to pass in
42
+ shared_secret = 'leeroy'
43
+ permissions = { update_product: true }
44
+ actor = { type: 'user', id: 1, name: 'Jenkins' }
45
+
46
+ # expiry is optional
47
+ expiry = Time.now.to_i + 60
48
+
49
+ # sign the request
50
+ JwtBouncer::SignRequest.call request, shared_secret: shared_secret,
51
+ permissions: permissions,
52
+ actor: actor,
53
+ expiry: expiry
54
+
55
+ # the request will now have the token in the header
56
+ request.headers['Authorization'] # => "Bearer ..."
57
+ ```
58
+
59
+ ### Contributing
60
+
61
+ Read the [development documentation](https://github.com/shiftcommerce/jwt-bouncer/blob/master/docs/development.md) to understand how to work on the library locally.
62
+
63
+ We welcome pull requests.
@@ -0,0 +1,2 @@
1
+ require 'bundler/gem_tasks'
2
+ task default: :spec
@@ -0,0 +1,2 @@
1
+ #!/bin/bash
2
+ ./bin/run bundle $@
@@ -0,0 +1,2 @@
1
+ #!/bin/bash
2
+ ./bin/run ./bin/docker/console
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'bundler/setup'
4
+ require 'jwt/bouncer'
5
+ require 'pry'
6
+
7
+ Pry.start
@@ -0,0 +1,12 @@
1
+ #!/bin/bash
2
+ bundle check || {
3
+ echo "Installing gems..."
4
+ {
5
+ bundle install --jobs 4 --retry 5 --quiet && echo "Installed gems."
6
+ } || {
7
+ echo "Gem installation failed."
8
+ exit 1
9
+ }
10
+ }
11
+
12
+ exec "$@"
@@ -0,0 +1,3 @@
1
+ #!/usr/local/bin/ruby
2
+ require 'rake'
3
+ Rake.application.run
@@ -0,0 +1,2 @@
1
+ #!/bin/bash
2
+ ./bin/run bundle exec rake $@
@@ -0,0 +1,2 @@
1
+ #!/bin/bash
2
+ ./bin/run bundle exec rspec $@
data/bin/run ADDED
@@ -0,0 +1,2 @@
1
+ #!/bin/bash
2
+ docker-compose run --rm app $@
@@ -0,0 +1,14 @@
1
+ version: '2'
2
+
3
+ services:
4
+ app:
5
+ build: .
6
+ volumes:
7
+ - .:/app
8
+ - rubygems_cache:/rubygems
9
+ environment:
10
+ GEM_HOME: '/rubygems'
11
+ BUNDLE_PATH: '/rubygems'
12
+
13
+ volumes:
14
+ rubygems_cache:
@@ -0,0 +1,9 @@
1
+ This library is setup using Docker and docker-compose, meaning you don't even need to have Ruby or any of the dependencies installed locally – you only need the [Docker Toolbox](https://www.docker.com/products/docker-toolbox).
2
+
3
+ Once you have the toolbox installed and running, you can boot a Pry console with the library loaded by simply running `./bin/console`. docker-compose will take care of downloading and installing dependencies.
4
+
5
+ For your common commands, you'll need to use the script equivalents in the `bin` folder, e.g. `./bin/bundle exec ...`, `./bin/rake -T` etc.
6
+
7
+ Tests can be run simply by using `./bin/rspec`.
8
+
9
+ If you need to run a custom command, you can use `./bin/run command here`, e.g. `./bin/run bash` will give you a bash console to the Docker instance.
@@ -0,0 +1,29 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'jwt_bouncer/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'jwt-bouncer'
8
+ spec.version = JwtBouncer::VERSION
9
+ spec.authors = ['Ryan Townsend']
10
+ spec.email = ['ryan@ryantownsend.co.uk']
11
+
12
+ spec.summary = 'jwt-bouncer is an abstraction for JWT-based authentication/authorisation'
13
+ spec.homepage = 'https://github.com/ryantownsend/jwt-bouncer'
14
+
15
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
16
+ f.match(%r{^(test|spec|features)/})
17
+ end
18
+ spec.bindir = 'exe'
19
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
20
+ spec.require_paths = ['lib']
21
+
22
+ spec.add_development_dependency 'bundler', '~> 1.14'
23
+ spec.add_development_dependency 'rake', '~> 10.0'
24
+ spec.add_development_dependency 'pry', '~> 0.10'
25
+ spec.add_development_dependency 'rspec', '~> 3.5'
26
+ spec.add_development_dependency 'activesupport', '~> 5.0', '>= 5.0.1'
27
+ spec.add_dependency 'rack', '~> 2.0'
28
+ spec.add_dependency 'jwt', '~> 1.5', '>= 1.5.6'
29
+ end
@@ -0,0 +1,8 @@
1
+ require 'jwt_bouncer/version'
2
+
3
+ module JwtBouncer
4
+ autoload :Request, 'jwt_bouncer/request'
5
+ autoload :SignRequest, 'jwt_bouncer/sign_request'
6
+ autoload :Token, 'jwt_bouncer/token'
7
+ autoload :Permissions, 'jwt_bouncer/permissions'
8
+ end
@@ -0,0 +1,46 @@
1
+ # frozen-string-literal: true
2
+
3
+ require 'jwt'
4
+ require 'zlib'
5
+
6
+ module JwtBouncer
7
+ module Permissions
8
+ def self.compress(permissions)
9
+ stream = StringIO.new
10
+ zip = Zlib::GzipWriter.new(stream)
11
+ begin
12
+ zip.write(permissions.to_json)
13
+ ensure
14
+ zip.close
15
+ end
16
+
17
+ Base64.encode64(stream.string)
18
+ end
19
+
20
+ def self.decompress(permissions)
21
+ unzipped_stream = StringIO.new
22
+ StringIO.open(Base64.decode64(permissions)) do |stream|
23
+ unzip = Zlib::GzipReader.new(stream)
24
+ begin
25
+ unzipped_stream.write(unzip.read)
26
+ ensure
27
+ unzip.close
28
+ end
29
+ end
30
+
31
+ JSON.parse(unzipped_stream.string)
32
+ end
33
+
34
+ def self.destructure(permissions)
35
+ destructured_permissions = []
36
+ permissions.each do |service, resources|
37
+ resources.each do |resource, resource_permissions|
38
+ resource_permissions.each do |permission|
39
+ destructured_permissions << [service, resource, permission].join('_')
40
+ end
41
+ end
42
+ end
43
+ destructured_permissions
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,57 @@
1
+ # frozen-string-literal: true
2
+
3
+ require 'jwt_bouncer/token'
4
+ require 'jwt_bouncer/permissions'
5
+ require 'jwt'
6
+
7
+ module JwtBouncer
8
+ class Request
9
+ HEADER = 'Authorization'.freeze
10
+
11
+ def initialize(request, shared_secret: nil)
12
+ @encoded_token = Request.extract_token(request)
13
+ @shared_secret = shared_secret
14
+ end
15
+
16
+ def authenticated?
17
+ !!decoded_token
18
+ rescue JWT::DecodeError
19
+ false
20
+ end
21
+
22
+ def actor
23
+ decoded_token['actor']
24
+ end
25
+
26
+ def account_reference
27
+ decoded_token['account_reference']
28
+ end
29
+
30
+ def permissions
31
+ @permissions ||= Permissions.decompress(decoded_token['permissions'])
32
+ end
33
+
34
+ def can?(action)
35
+ destructured_action_permissions = Permissions.destructure(action)
36
+ matching_permissions = destructured_action_permissions & destructured_permissions
37
+ matching_permissions == destructured_action_permissions
38
+ end
39
+
40
+ # extracts the encoded token from the given request
41
+ def self.extract_token(request)
42
+ return nil unless request.headers.key?(HEADER)
43
+ matches = request.headers.fetch(HEADER).match(/\ABearer\s(.*)\z/i)
44
+ matches[1] if matches
45
+ end
46
+
47
+ private
48
+
49
+ def decoded_token
50
+ @decoded_token ||= Token.decode(@encoded_token, @shared_secret)
51
+ end
52
+
53
+ def destructured_permissions
54
+ Permissions.destructure(permissions)
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,21 @@
1
+ # frozen-string-literal: true
2
+ require 'jwt_bouncer/token'
3
+ require 'jwt_bouncer/permissions'
4
+
5
+ module JwtBouncer
6
+ module SignRequest
7
+ def self.call(request, **options)
8
+ request.headers['Authorization'] = "Bearer #{generate_token(**options)}"
9
+ request
10
+ end
11
+
12
+ def self.generate_token(permissions: {}, actor: {}, account_reference:, shared_secret:, expiry: nil)
13
+ payload = {
14
+ permissions: Permissions.compress(permissions),
15
+ actor: actor,
16
+ account_reference: account_reference
17
+ }
18
+ Token.encode(payload, shared_secret, expiry: expiry)
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,21 @@
1
+ # frozen-string-literal: true
2
+ require 'jwt'
3
+
4
+ module JwtBouncer
5
+ module Token
6
+ ALGORITHM = 'HS256'
7
+
8
+ def self.encode(data, shared_secret, expiry: nil)
9
+ # setup our base payload
10
+ payload = { data: data, iat: Time.now.utc.to_i }
11
+ # apply expiry, if necessary
12
+ payload[:exp] = expiry.to_i if expiry
13
+ # build the JWT
14
+ JWT.encode(payload, shared_secret, ALGORITHM)
15
+ end
16
+
17
+ def self.decode(token, shared_secret)
18
+ JWT.decode(token, shared_secret, true, algorithm: ALGORITHM, verify_iat: true).dig(0, 'data')
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,3 @@
1
+ module JwtBouncer
2
+ VERSION = '0.1.1'.freeze
3
+ end
metadata ADDED
@@ -0,0 +1,177 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: jwt-bouncer
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1
5
+ platform: ruby
6
+ authors:
7
+ - Ryan Townsend
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2017-04-12 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.14'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.14'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: pry
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '0.10'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '0.10'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rspec
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '3.5'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '3.5'
69
+ - !ruby/object:Gem::Dependency
70
+ name: activesupport
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '5.0'
76
+ - - ">="
77
+ - !ruby/object:Gem::Version
78
+ version: 5.0.1
79
+ type: :development
80
+ prerelease: false
81
+ version_requirements: !ruby/object:Gem::Requirement
82
+ requirements:
83
+ - - "~>"
84
+ - !ruby/object:Gem::Version
85
+ version: '5.0'
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ version: 5.0.1
89
+ - !ruby/object:Gem::Dependency
90
+ name: rack
91
+ requirement: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - "~>"
94
+ - !ruby/object:Gem::Version
95
+ version: '2.0'
96
+ type: :runtime
97
+ prerelease: false
98
+ version_requirements: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - "~>"
101
+ - !ruby/object:Gem::Version
102
+ version: '2.0'
103
+ - !ruby/object:Gem::Dependency
104
+ name: jwt
105
+ requirement: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - "~>"
108
+ - !ruby/object:Gem::Version
109
+ version: '1.5'
110
+ - - ">="
111
+ - !ruby/object:Gem::Version
112
+ version: 1.5.6
113
+ type: :runtime
114
+ prerelease: false
115
+ version_requirements: !ruby/object:Gem::Requirement
116
+ requirements:
117
+ - - "~>"
118
+ - !ruby/object:Gem::Version
119
+ version: '1.5'
120
+ - - ">="
121
+ - !ruby/object:Gem::Version
122
+ version: 1.5.6
123
+ description:
124
+ email:
125
+ - ryan@ryantownsend.co.uk
126
+ executables: []
127
+ extensions: []
128
+ extra_rdoc_files: []
129
+ files:
130
+ - ".gitignore"
131
+ - Dockerfile
132
+ - Gemfile
133
+ - Gemfile.lock
134
+ - LICENSE
135
+ - README.md
136
+ - Rakefile
137
+ - bin/bundle
138
+ - bin/console
139
+ - bin/docker/console
140
+ - bin/docker/entrypoint
141
+ - bin/docker/rake
142
+ - bin/rake
143
+ - bin/rspec
144
+ - bin/run
145
+ - docker-compose.yml
146
+ - docs/development.md
147
+ - jwt-bouncer.gemspec
148
+ - lib/jwt_bouncer.rb
149
+ - lib/jwt_bouncer/permissions.rb
150
+ - lib/jwt_bouncer/request.rb
151
+ - lib/jwt_bouncer/sign_request.rb
152
+ - lib/jwt_bouncer/token.rb
153
+ - lib/jwt_bouncer/version.rb
154
+ homepage: https://github.com/ryantownsend/jwt-bouncer
155
+ licenses: []
156
+ metadata: {}
157
+ post_install_message:
158
+ rdoc_options: []
159
+ require_paths:
160
+ - lib
161
+ required_ruby_version: !ruby/object:Gem::Requirement
162
+ requirements:
163
+ - - ">="
164
+ - !ruby/object:Gem::Version
165
+ version: '0'
166
+ required_rubygems_version: !ruby/object:Gem::Requirement
167
+ requirements:
168
+ - - ">="
169
+ - !ruby/object:Gem::Version
170
+ version: '0'
171
+ requirements: []
172
+ rubyforge_project:
173
+ rubygems_version: 2.6.8
174
+ signing_key:
175
+ specification_version: 4
176
+ summary: jwt-bouncer is an abstraction for JWT-based authentication/authorisation
177
+ test_files: []