zaikio-jwt_auth 0.1.3

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