zaikio-jwt_auth 0.1.3

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
+ SHA256:
3
+ metadata.gz: 3e888abf5976a9f6837a1723da1f586f317add69e45caa8527c68c086e1f4c40
4
+ data.tar.gz: 3ccd16afb7cdc1e808b01dab786cf24cdab31e2eff44bffaa695a0c8231afad5
5
+ SHA512:
6
+ metadata.gz: 36540e2d6588c39f994e4ae5dc62c2bb274d505feb32404f3d0dd7568c57f665024ed164686bc3056c5f236b4471f271917d2f42090d62a3685952010bf27ba9
7
+ data.tar.gz: 13447cfaf7386af9cbbfe83f54ccb0ffed13e310c9dd7fab94705ee1b321bf3d82daeb40a4ba9bb273dc313e355aaf822b33f702e5386674c5ce223cd629155f
@@ -0,0 +1,20 @@
1
+ Copyright 2020 Jalyna
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,92 @@
1
+ # Zaikio::JWTAuth
2
+
3
+ Gem for JWT-Based authentication and authorization with zaikio.
4
+
5
+ ## Usage
6
+
7
+ ## Installation
8
+
9
+ 1. Add this line to your application's Gemfile:
10
+
11
+ ```ruby
12
+ gem 'zaikio-jwt_auth'
13
+ ```
14
+
15
+ And then execute:
16
+ ```bash
17
+ $ bundle
18
+ ```
19
+
20
+ Or install it yourself as:
21
+ ```bash
22
+ $ gem install zaikio-jwt_auth
23
+ ```
24
+
25
+ 2. Configure the gem:
26
+
27
+ ```rb
28
+ # config/initializers/zaikio_jwt_auth.rb
29
+
30
+ Zaikio::JWTAuth.configure do |config|
31
+ config.environment = :sandbox # or production
32
+ config.app_name = "test_app" # Your Zaikio App-Name
33
+ config.redis = Redis.new
34
+ end
35
+ ```
36
+
37
+ 3. Extend your API application controller:
38
+
39
+ ```rb
40
+ class API::ApplicationController < ActionController::Base
41
+ include Zaikio::JWTAuth
42
+
43
+ before_action :authenticate_by_jwt
44
+
45
+ def after_jwt_auth(token_data)
46
+ klass = token_data.subject_type == 'Organization' ? Organization : Person
47
+ Current.scope = klass.find(token_data.subject_id)
48
+ end
49
+ end
50
+ ```
51
+
52
+ 4. Update Revoked Access Tokens by Webhook
53
+
54
+ ```rb
55
+ # ENV['ZAIKIO_SHARED_SECRET'] needs to be defined first, you can find it on your
56
+ # app details page in zaikio. Fore more help read:
57
+ # https://docs.zaikio.com/guide/loom/receiving-events.html
58
+ class WebhooksController < ActionController::Base
59
+ include Zaikio::JWTAuth
60
+
61
+ before_action :verify_signature
62
+ before_action :update_blacklisted_access_tokens_by_webhook
63
+
64
+ def create
65
+ case params[:name]
66
+ # Manage other events
67
+ end
68
+ end
69
+
70
+ private
71
+
72
+ def verify_signature
73
+ # Read More: https://docs.zaikio.com/guide/loom/receiving-events.html
74
+ unless ActiveSupport::SecurityUtils.secure_compare(
75
+ OpenSSL::HMAC.hexdigest("SHA256", "shared-secret", request.body.read),
76
+ request.headers["X-Loom-Signature"]
77
+ )
78
+ render status: :unauthorized, json: { errors: ["invalid_signature"] }
79
+ end
80
+ end
81
+ end
82
+ ```
83
+
84
+
85
+ 5. Add more restrictions to your resources:
86
+
87
+ ```rb
88
+ class API::ResourcesController < API::ApplicationController
89
+ authorize_by_jwt_subject_type 'Organization'
90
+ authorize_by_jwt_scopes 'resources'
91
+ end
92
+ ```
@@ -0,0 +1,37 @@
1
+ begin
2
+ require 'bundler/setup'
3
+ rescue LoadError
4
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
5
+ end
6
+
7
+ require 'rdoc/task'
8
+ require 'rubocop/rake_task'
9
+
10
+ RDoc::Task.new(:rdoc) do |rdoc|
11
+ rdoc.rdoc_dir = 'rdoc'
12
+ rdoc.title = 'Zaikio::JWTAuth'
13
+ rdoc.options << '--line-numbers'
14
+ rdoc.rdoc_files.include('README.md')
15
+ rdoc.rdoc_files.include('lib/**/*.rb')
16
+ end
17
+
18
+ require 'bundler/gem_tasks'
19
+
20
+ require 'rake/testtask'
21
+
22
+ Rake::TestTask.new(:test) do |t|
23
+ t.libs << 'test'
24
+ t.pattern = 'test/**/*_test.rb'
25
+ t.verbose = false
26
+ end
27
+
28
+ task default: :test
29
+
30
+ namespace :test do
31
+ desc 'Runs RuboCop on specified directories'
32
+ RuboCop::RakeTask.new(:rubocop) do |task|
33
+ task.fail_on_error = false
34
+ end
35
+ end
36
+
37
+ Rake::Task[:test].enhance ['test:rubocop']
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :zaikio_jwt_auth do
3
+ # # Task goes here
4
+ # end
@@ -0,0 +1,113 @@
1
+ require "jwt"
2
+ require "oj"
3
+ require "zaikio/jwt_auth/railtie"
4
+ require "zaikio/jwt_auth/configuration"
5
+ require "zaikio/jwt_auth/directory_cache"
6
+ require "zaikio/jwt_auth/jwk"
7
+ require "zaikio/jwt_auth/token_data"
8
+
9
+ module Zaikio
10
+ module JWTAuth
11
+ class << self
12
+ attr_accessor :configuration
13
+ end
14
+
15
+ def self.configure
16
+ self.configuration ||= Configuration.new
17
+ yield(configuration)
18
+ end
19
+
20
+ def self.included(base)
21
+ base.send :include, InstanceMethods
22
+ base.send :extend, ClassMethods
23
+ end
24
+
25
+ module ClassMethods
26
+ def authorize_by_jwt_subject_type(type = nil)
27
+ @authorize_by_jwt_subject_type ||= type
28
+ end
29
+
30
+ def authorize_by_jwt_scopes(scopes = nil, options = {})
31
+ @authorize_by_jwt_scopes ||= options.merge(scopes: scopes)
32
+ end
33
+ end
34
+
35
+ module InstanceMethods
36
+ def authenticate_by_jwt
37
+ render_error("no_jwt_passed", status: :unauthorized) && return unless jwt_from_auth_header
38
+
39
+ token_data = TokenData.new(jwt_payload)
40
+
41
+ return if show_error_if_token_is_blacklisted(token_data)
42
+
43
+ return if show_error_if_authorize_by_jwt_subject_type_fails(token_data)
44
+
45
+ return if show_error_if_authorize_by_jwt_scopes_fails(token_data)
46
+
47
+ send(:after_jwt_auth, token_data) if respond_to?(:after_jwt_auth)
48
+ rescue JWT::ExpiredSignature
49
+ render_error("jwt_expired") && (return)
50
+ rescue JWT::DecodeError
51
+ render_error("invalid_jwt") && (return)
52
+ end
53
+
54
+ def update_blacklisted_access_tokens_by_webhook
55
+ return unless params[:name] == "directory.revoked_access_token"
56
+
57
+ DirectoryCache.update("api/v1/blacklisted_token_ids.json", expires_after: 60.minutes) do |data|
58
+ data["blacklisted_token_ids"] << params[:payload][:access_token_id]
59
+ data
60
+ end
61
+
62
+ render json: { received: true }
63
+ end
64
+
65
+ private
66
+
67
+ def jwt_from_auth_header
68
+ auth_header = request.headers["Authorization"]
69
+ auth_header.split("Bearer ").last if /Bearer/.match?(auth_header)
70
+ end
71
+
72
+ def jwt_payload
73
+ payload, = JWT.decode(jwt_from_auth_header, nil, true, algorithms: ["RS256"], jwks: JWK.loader)
74
+
75
+ payload
76
+ end
77
+
78
+ def show_error_if_authorize_by_jwt_scopes_fails(token_data)
79
+ scope_data = self.class.authorize_by_jwt_scopes
80
+ return if !scope_data[:scopes] || token_data.scope?(scope_data[:scopes], action_name, scope_data[:app_name])
81
+
82
+ render_error("unpermitted_scope")
83
+ end
84
+
85
+ def show_error_if_authorize_by_jwt_subject_type_fails(token_data)
86
+ if !self.class.authorize_by_jwt_subject_type ||
87
+ self.class.authorize_by_jwt_subject_type == token_data.subject_type
88
+ return
89
+ end
90
+
91
+ render_error("unpermitted_subject")
92
+ end
93
+
94
+ def show_error_if_token_is_blacklisted(token_data)
95
+ return unless blacklisted_token_ids.include?(token_data.jti)
96
+
97
+ render_error("invalid_jwt")
98
+ end
99
+
100
+ def blacklisted_token_ids
101
+ if Zaikio::JWTAuth.configuration.blacklisted_token_ids
102
+ return Zaikio::JWTAuth.configuration.blacklisted_token_ids
103
+ end
104
+
105
+ DirectoryCache.fetch("api/v1/blacklisted_token_ids.json", expires_after: 60.minutes)["blacklisted_token_ids"]
106
+ end
107
+
108
+ def render_error(error, status: :forbidden)
109
+ render(status: status, json: { "errors" => [error] })
110
+ end
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,49 @@
1
+ require "logger"
2
+
3
+ module Zaikio
4
+ module JWTAuth
5
+ class Configuration
6
+ HOSTS = {
7
+ development: "http://directory.zaikio.test",
8
+ test: "http://directory.zaikio.test",
9
+ staging: "https://directory.staging.zaikio.com",
10
+ sandbox: "https://directory.sandbox.zaikio.com",
11
+ production: "https://directory.zaikio.com"
12
+ }.freeze
13
+
14
+ attr_accessor :app_name
15
+ attr_accessor :redis, :host
16
+ attr_reader :environment
17
+ attr_writer :logger, :blacklisted_token_ids, :keys
18
+
19
+ def initialize
20
+ @environment = :sandbox
21
+ end
22
+
23
+ def logger
24
+ @logger ||= Logger.new(STDOUT)
25
+ end
26
+
27
+ def environment=(env)
28
+ @environment = env.to_sym
29
+ @host = host_for(environment)
30
+ end
31
+
32
+ def keys
33
+ @keys.is_a?(Proc) ? @keys.call : @keys
34
+ end
35
+
36
+ def blacklisted_token_ids
37
+ @blacklisted_token_ids.is_a?(Proc) ? @blacklisted_token_ids.call : @blacklisted_token_ids
38
+ end
39
+
40
+ private
41
+
42
+ def host_for(environment)
43
+ HOSTS.fetch(environment) do
44
+ raise StandardError.new, "Invalid Zaikio::JWTAuth environment '#{environment}'"
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,67 @@
1
+ require "net/http"
2
+ require "json"
3
+ require "logger"
4
+
5
+ module Zaikio
6
+ module JWTAuth
7
+ class DirectoryCache
8
+ class << self
9
+ def fetch(directory_path, options = {})
10
+ cache = Zaikio::JWTAuth.configuration.redis.get("zaikio::jwt_auth::#{directory_path}")
11
+
12
+ json = Oj.load(cache) if cache
13
+
14
+ if !cache || options[:invalidate] || cache_expired?(json, options[:expires_after])
15
+ return reload(directory_path)
16
+ end
17
+
18
+ json["data"]
19
+ end
20
+
21
+ def update(directory_path, options = {})
22
+ data = fetch(directory_path, options)
23
+ data = yield(data)
24
+ Zaikio::JWTAuth.configuration.redis.set("zaikio::jwt_auth::#{directory_path}", {
25
+ fetched_at: Time.now.to_i,
26
+ data: data
27
+ }.to_json)
28
+ end
29
+
30
+ def reset(directory_path)
31
+ Zaikio::JWTAuth.configuration.redis.del("zaikio::jwt_auth::#{directory_path}")
32
+ end
33
+
34
+ private
35
+
36
+ def cache_expired?(json, expires_after)
37
+ DateTime.strptime(json["fetched_at"].to_s, "%s") < Time.now.utc - (expires_after || 1.hour)
38
+ end
39
+
40
+ def reload(directory_path)
41
+ retries = 0
42
+
43
+ begin
44
+ data = fetch_from_directory(directory_path)
45
+ Zaikio::JWTAuth.configuration.redis.set("zaikio::jwt_auth::#{directory_path}", {
46
+ fetched_at: Time.now.to_i,
47
+ data: data
48
+ }.to_json)
49
+
50
+ data
51
+ rescue Errno::ECONNREFUSED, Net::ReadTimeout => e
52
+ raise unless (retries += 1) <= 3
53
+
54
+ Zaikio::JWTAuth.configuration.logger.log("Timeout (#{e}), retrying in 1 second...")
55
+ sleep(1)
56
+ retry
57
+ end
58
+ end
59
+
60
+ def fetch_from_directory(directory_path)
61
+ uri = URI("#{Zaikio::JWTAuth.configuration.host}/#{directory_path}")
62
+ Oj.load(Net::HTTP.get(uri))
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,44 @@
1
+ require "net/http"
2
+ require "json"
3
+ require "logger"
4
+
5
+ module Zaikio
6
+ module JWTAuth
7
+ class JWK
8
+ CACHE_EXPIRES_AFTER = 1.hour.freeze
9
+
10
+ class << self
11
+ def loader
12
+ lambda do |options|
13
+ reload_keys if options[:invalidate]
14
+ {
15
+ keys: keys.map do |key_data|
16
+ JWT::JWK.import(key_data.with_indifferent_access).export
17
+ end
18
+ }
19
+ end
20
+ end
21
+
22
+ private
23
+
24
+ def reload_keys
25
+ return if Zaikio::JWTAuth.configuration.keys
26
+
27
+ fetch_from_cache(invalidate: true)
28
+ end
29
+
30
+ def keys
31
+ return Zaikio::JWTAuth.configuration.keys if Zaikio::JWTAuth.configuration.keys
32
+
33
+ fetch_from_cache["keys"]
34
+ end
35
+
36
+ def fetch_from_cache(options = {})
37
+ DirectoryCache.fetch("api/v1/jwt_public_keys.json", {
38
+ expires_after: CACHE_EXPIRES_AFTER
39
+ }.merge(options))
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,6 @@
1
+ module Zaikio
2
+ module JWTAuth
3
+ class Railtie < ::Rails::Railtie
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,75 @@
1
+ module Zaikio
2
+ module JWTAuth
3
+ class TokenData
4
+ def self.subject_format
5
+ %r{^((\w+)/((\w|-)+)\>)?(\w+)/((\w|-)+)$}
6
+ end
7
+
8
+ def self.actions_by_permission
9
+ {
10
+ "r" => %w[show index],
11
+ "w" => %w[update create destroy],
12
+ "rw" => %w[show index update create destroy]
13
+ }.freeze
14
+ end
15
+
16
+ def initialize(payload)
17
+ @payload = payload
18
+ end
19
+
20
+ def audience
21
+ audiences.first
22
+ end
23
+
24
+ def audiences
25
+ @payload["aud"] || []
26
+ end
27
+
28
+ def scope
29
+ @payload["scope"]
30
+ end
31
+
32
+ def jti
33
+ @payload["jti"]
34
+ end
35
+
36
+ def scope?(allowed_scopes, action_name, app_name = nil)
37
+ app_name ||= Zaikio::JWTAuth.configuration.app_name
38
+ Array(allowed_scopes).map(&:to_s).any? do |allowed_scope|
39
+ scope.any? do |s|
40
+ parts = s.split(".")
41
+ parts[0] == app_name &&
42
+ parts[1] == allowed_scope &&
43
+ action_in_permission?(action_name, parts[2])
44
+ end
45
+ end
46
+ end
47
+
48
+ def subject_id
49
+ subject_match[6]
50
+ end
51
+
52
+ def subject_type
53
+ subject_match[5]
54
+ end
55
+
56
+ def on_behalf_of_id
57
+ subject_match[3]
58
+ end
59
+
60
+ def on_behalf_of_type
61
+ subject_match[2]
62
+ end
63
+
64
+ def subject_match
65
+ self.class.subject_format.match(@payload["sub"]) || []
66
+ end
67
+
68
+ private
69
+
70
+ def action_in_permission?(action_name, permission)
71
+ self.class.actions_by_permission[permission].include?(action_name)
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,5 @@
1
+ module Zaikio
2
+ module JWTAuth
3
+ VERSION = "0.1.3".freeze
4
+ end
5
+ end
metadata ADDED
@@ -0,0 +1,96 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: zaikio-jwt_auth
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.3
5
+ platform: ruby
6
+ authors:
7
+ - Crispy Mountain GmbH
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2020-02-03 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: oj
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: 3.0.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: 3.0.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: rails
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: 6.0.1
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: 6.0.1
41
+ - !ruby/object:Gem::Dependency
42
+ name: jwt
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: 2.2.1
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: 2.2.1
55
+ description: JWT-Based authentication and authorization with zaikio.
56
+ email:
57
+ - js@crispymtn.com
58
+ executables: []
59
+ extensions: []
60
+ extra_rdoc_files: []
61
+ files:
62
+ - MIT-LICENSE
63
+ - README.md
64
+ - Rakefile
65
+ - lib/tasks/zaikio/jwt_auth_tasks.rake
66
+ - lib/zaikio/jwt_auth.rb
67
+ - lib/zaikio/jwt_auth/configuration.rb
68
+ - lib/zaikio/jwt_auth/directory_cache.rb
69
+ - lib/zaikio/jwt_auth/jwk.rb
70
+ - lib/zaikio/jwt_auth/railtie.rb
71
+ - lib/zaikio/jwt_auth/token_data.rb
72
+ - lib/zaikio/jwt_auth/version.rb
73
+ homepage: https://www.zaikio.com/
74
+ licenses:
75
+ - MIT
76
+ metadata: {}
77
+ post_install_message:
78
+ rdoc_options: []
79
+ require_paths:
80
+ - lib
81
+ required_ruby_version: !ruby/object:Gem::Requirement
82
+ requirements:
83
+ - - ">="
84
+ - !ruby/object:Gem::Version
85
+ version: '0'
86
+ required_rubygems_version: !ruby/object:Gem::Requirement
87
+ requirements:
88
+ - - ">="
89
+ - !ruby/object:Gem::Version
90
+ version: '0'
91
+ requirements: []
92
+ rubygems_version: 3.1.2
93
+ signing_key:
94
+ specification_version: 4
95
+ summary: JWT-Based authentication and authorization with zaikio
96
+ test_files: []