zaikio-jwt_auth 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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 6de8bcc2c7ef22d052c626bc463789dec9dba4c6d4e977ee543973736994440b
4
+ data.tar.gz: c3c1b661b388fdef157831bccd203616913456f594762c4d0513967ebde8c468
5
+ SHA512:
6
+ metadata.gz: 694d2a013c3a8e41dc8b03a11b0817fadd8e636cd80809a4d66c04285b2ecfe1e2dcaaf919f7735556a001d6d6feb93c4541a1f61bce16689e117fbabe6bf55f
7
+ data.tar.gz: 44c62f3c08102efb72e0ca8951562d02481bf5882742cd75ff4a27f722d80713f939c17cf05271cd870ef8b5fae3c00ad534b756aa981c32399e634a366b674d
@@ -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,59 @@
1
+ # Zaikio::JWTAuth
2
+
3
+ Gem for JWT-Based authentication and authorization with zaikio.
4
+
5
+ ## Usage
6
+
7
+ ## Installation
8
+
9
+ 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
+ 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
+ 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
+ Add more restrictions to your resources:
53
+
54
+ ```rb
55
+ class API::ResourcesController < API::ApplicationController
56
+ authorize_by_jwt_subject_type 'Organization'
57
+ authorize_by_jwt_scopes 'resources'
58
+ end
59
+ ```
@@ -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,101 @@
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)
31
+ @authorize_by_jwt_scopes ||= scopes
32
+ end
33
+ end
34
+
35
+ module InstanceMethods
36
+ def authenticate_by_jwt
37
+ unless jwt_from_auth_header
38
+ render(status: :unauthorized, plain: "Please authenticate via Zaikio JWT") && return
39
+ end
40
+
41
+ token_data = TokenData.new(jwt_payload)
42
+
43
+ return if show_error_if_token_is_blacklisted(token_data)
44
+
45
+ return if show_error_if_authorize_by_jwt_subject_type_fails(token_data)
46
+
47
+ return if show_error_if_authorize_by_jwt_scopes_fails(token_data)
48
+
49
+ send(:after_jwt_auth, token_data) if respond_to?(:after_jwt_auth)
50
+ rescue JWT::ExpiredSignature
51
+ render(status: :forbidden, plain: "JWT expired") && (return)
52
+ rescue JWT::DecodeError
53
+ render(status: :forbidden, plain: "Invalid JWT") && (return)
54
+ end
55
+
56
+ private
57
+
58
+ def jwt_from_auth_header
59
+ auth_header = request.headers["Authorization"]
60
+ auth_header.split("Bearer ").last if /Bearer/.match?(auth_header)
61
+ end
62
+
63
+ def jwt_payload
64
+ payload, = JWT.decode(jwt_from_auth_header, nil, true, algorithms: ["RS256"], jwks: JWK.loader)
65
+
66
+ payload
67
+ end
68
+
69
+ def show_error_if_authorize_by_jwt_scopes_fails(token_data)
70
+ if !self.class.authorize_by_jwt_scopes || token_data.scope?(self.class.authorize_by_jwt_scopes, action_name)
71
+ return
72
+ end
73
+
74
+ render(status: :forbidden, plain: "Invalid scope")
75
+ end
76
+
77
+ def show_error_if_authorize_by_jwt_subject_type_fails(token_data)
78
+ if !self.class.authorize_by_jwt_subject_type ||
79
+ self.class.authorize_by_jwt_subject_type == token_data.subject_type
80
+ return
81
+ end
82
+
83
+ render(status: :forbidden, plain: "Unallowed subject type")
84
+ end
85
+
86
+ def show_error_if_token_is_blacklisted(token_data)
87
+ return unless blacklisted_token_ids.include?(token_data.jti)
88
+
89
+ render(status: :forbidden, plain: "Invalid token")
90
+ end
91
+
92
+ def blacklisted_token_ids
93
+ if Zaikio::JWTAuth.configuration.blacklisted_token_ids
94
+ return Zaikio::JWTAuth.configuration.blacklisted_token_ids
95
+ end
96
+
97
+ DirectoryCache.fetch("api/v1/blacklisted_token_ids.json", expires_after: 5.minutes)["blacklisted_token_ids"]
98
+ end
99
+ end
100
+ end
101
+ 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,54 @@
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
+ private
22
+
23
+ def cache_expired?(json, expires_after)
24
+ DateTime.strptime(json["fetched_at"].to_s, "%s") < Time.now.utc - (expires_after || 1.hour)
25
+ end
26
+
27
+ def reload(directory_path)
28
+ retries = 0
29
+
30
+ begin
31
+ data = fetch_from_directory(directory_path)
32
+ Zaikio::JWTAuth.configuration.redis.set("zaikio::jwt_auth::#{directory_path}", {
33
+ fetched_at: Time.now.to_i,
34
+ data: data
35
+ }.to_json)
36
+
37
+ data
38
+ rescue Errno::ECONNREFUSED, Net::ReadTimeout => e
39
+ raise unless (retries += 1) <= 3
40
+
41
+ Zaikio::JWTAuth.configuration.logger.log("Timeout (#{e}), retrying in 1 second...")
42
+ sleep(1)
43
+ retry
44
+ end
45
+ end
46
+
47
+ def fetch_from_directory(directory_path)
48
+ uri = URI("#{Zaikio::JWTAuth.configuration.host}/#{directory_path}")
49
+ Oj.load(Net::HTTP.get(uri))
50
+ end
51
+ end
52
+ end
53
+ end
54
+ 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,66 @@
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 scope
21
+ @payload["scope"]
22
+ end
23
+
24
+ def jti
25
+ @payload["jti"]
26
+ end
27
+
28
+ def scope?(allowed_scopes, action_name)
29
+ Array(allowed_scopes).map(&:to_s).any? do |allowed_scope|
30
+ scope.any? do |s|
31
+ parts = s.split(".")
32
+ parts[0] == Zaikio::JWTAuth.configuration.app_name &&
33
+ parts[1] == allowed_scope &&
34
+ action_in_permission?(action_name, parts[2])
35
+ end
36
+ end
37
+ end
38
+
39
+ def subject_id
40
+ subject_match[6]
41
+ end
42
+
43
+ def subject_type
44
+ subject_match[5]
45
+ end
46
+
47
+ def on_behalf_of_id
48
+ subject_match[3]
49
+ end
50
+
51
+ def on_behalf_of_type
52
+ subject_match[2]
53
+ end
54
+
55
+ def subject_match
56
+ self.class.subject_format.match(@payload["sub"]) || []
57
+ end
58
+
59
+ private
60
+
61
+ def action_in_permission?(action_name, permission)
62
+ self.class.actions_by_permission[permission].include?(action_name)
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,5 @@
1
+ module Zaikio
2
+ module JWTAuth
3
+ VERSION = "0.1.0".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.0
5
+ platform: ruby
6
+ authors:
7
+ - Crispy Mountain GmbH
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2020-01-20 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: []