henlo 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 51d705728f88bb4c77b243d887d05f729d2a2019
4
+ data.tar.gz: 968ea1f50cecc17d754fe04617e117a02e6d9c88
5
+ SHA512:
6
+ metadata.gz: e30c6ffaf47154ffe110b3218d7a4ad7f972ddc1a4d5b6b27940342cd23dd7a98d0bc8ae96fa6a1476d0c215ecbd149a0588662609e4bd41715f4a27de96b6d4
7
+ data.tar.gz: 19986364a62c80e02052f7cdbbbc98043a54ba37351afff6d616275cfa69d53fb05641c92226db268f1bc513527c559466236123d7079d940cccaa41a4aa9dc5
@@ -0,0 +1,15 @@
1
+ require "rubygems"
2
+
3
+ begin
4
+ require 'bundler/setup'
5
+ rescue LoadError
6
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
7
+ end
8
+
9
+ APP_RAKEFILE = File.expand_path("../test/dummy/Rakefile", __FILE__)
10
+ load 'rails/tasks/engine.rake'
11
+
12
+
13
+ Bundler::GemHelper.install_tasks
14
+
15
+ task default: :test
@@ -0,0 +1,17 @@
1
+ require 'rails/generators/base'
2
+
3
+ module Henlo
4
+ module Generators
5
+ class InstallGenerator < Rails::Generators::Base
6
+ source_root File.expand_path("../../templates", __FILE__)
7
+ desc "Installs Henlo."
8
+
9
+ def copy_initializer
10
+ template "henlo_initializer.rb", "config/initializers/henlo.rb"
11
+
12
+ puts "Installation complete."
13
+ end
14
+
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,33 @@
1
+ require 'rails/generators/active_record'
2
+
3
+ module Henlo
4
+ module Generators
5
+ class MigrationsGenerator < ActiveRecord::Generators::Base
6
+ source_root File.expand_path("../../templates", __FILE__)
7
+ desc "Generates the necessary migration for the model"
8
+ argument :table_name, type: :string, default: "User"
9
+
10
+
11
+ def create_migrations
12
+ puts "Generating migrations for model #{table_name}.downcase"
13
+ migration_template "migrations/add_jti_column.rb", "db/migrate/add_jti_column_to_#{table_name.downcase.pluralize}.rb"
14
+ migration_template 'migrations/create_blacklisted_tokens.rb.erb', 'db/migrate/create_blacklisted_tokens.rb'
15
+ end
16
+
17
+ def migration_data
18
+ <<RUBY
19
+ t.string :refresh_token_jti
20
+ t.boolean :blacklist_check, default: false
21
+ RUBY
22
+ end
23
+
24
+ def migration_index_data
25
+ <<RUBY
26
+ add_index "#{table_name.downcase.pluralize.to_sym}", :blacklist_check
27
+ add_index "#{table_name.downcase.pluralize.to_sym}", :refresh_token_jti
28
+ RUBY
29
+ end
30
+
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,23 @@
1
+ Henlo.setup do |config|
2
+
3
+ ## All expiration claims use seconds as unit
4
+
5
+ ## Refresh token expiration claim
6
+ ## ----------------
7
+ ##
8
+ ## How long before a refresh token is expired. If nil is provided, token will
9
+ ## last forever.
10
+ ##
11
+ ## Default is 15 days
12
+ # config.refresh_token_lifetime = 15 * 86400
13
+
14
+ ## Id token expiration claim
15
+ ## ----------------
16
+ ##
17
+ ## How long before an id token is expired. If nil is provided, token will
18
+ ## last forever.
19
+ ## This value is provided in seconds
20
+ ## Default is 15 minutes
21
+ # config.id_token_lifetime = 60 * 15
22
+
23
+ end
@@ -0,0 +1,16 @@
1
+ <%
2
+ parent_class = ActiveRecord::Migration
3
+ parent_class = parent_class[parent_class.current_version] if Rails::VERSION::MAJOR >= 5
4
+ -%>
5
+ class AddJtiColumnTo<%= table_name.camelize %>s < <%= parent_class.to_s %>
6
+ def self.up
7
+ change_table(:<%= table_name.downcase.pluralize %>) do |t|
8
+ <%= migration_data %>
9
+ end
10
+ <%= migration_index_data%>
11
+ end
12
+
13
+ def self.down
14
+ raise ActiveRecord::IrreversibleMigration
15
+ end
16
+ end
@@ -0,0 +1,21 @@
1
+ <%
2
+ parent_class = ActiveRecord::Migration
3
+ parent_class = parent_class[parent_class.current_version] if Rails::VERSION::MAJOR >= 5
4
+ -%>
5
+ class CreateBlacklistedTokens < <%= parent_class.to_s %>
6
+ def self.up
7
+ create_table :blacklisted_tokens do |t|
8
+ t.string :token_jti
9
+ <%- if Rails::VERSION::MAJOR >= 5 -%>
10
+ t.integer :exp_in_unix
11
+ t.timestamps
12
+ <%- else -%>
13
+ t.timestamps null: false
14
+ <%- end -%>
15
+ end
16
+ end
17
+
18
+ def self.down
19
+ drop_table :blacklisted_tokens
20
+ end
21
+ end
@@ -0,0 +1,46 @@
1
+ require "henlo/version"
2
+ require "henlo/refreshable"
3
+ require "henlo/identifiable"
4
+ require "henlo/authenticable"
5
+ require "henlo/revocable"
6
+ require "knock"
7
+
8
+ #
9
+ ## Defines default initializing values and main method for generating tokens
10
+ module Henlo
11
+
12
+ #
13
+ ## Generates refresh and access tokens when method is called, allows the passing in of
14
+ ## additional key value pairs to be encoded in the jwt payload
15
+ ## Returns the jwt identifier of the refresh token, as well as the expiry time in unix
16
+ ## seconds of the id token.
17
+ def self.generate_henlos(options={})
18
+ claim = options || nil
19
+ refresh_token_and_jti = Refreshable.generate_refreshable(options)
20
+ id_token_and_exp = Identifiable.generate_identifiable(options)
21
+ tokens = Hash[
22
+ id_token: id_token_and_exp[:token],
23
+ refresh_token: refresh_token_and_jti[:token]
24
+ ]
25
+ henlos = Hash[
26
+ tokens: tokens,
27
+ jti: refresh_token_and_jti[:jti],
28
+ exp: id_token_and_exp[:exp]
29
+ ]
30
+ end
31
+
32
+ mattr_accessor :refresh_token_lifetime
33
+ self.refresh_token_lifetime = 15 * 86400
34
+
35
+ mattr_accessor :id_token_lifetime
36
+ self.id_token_lifetime = 60 * 15
37
+
38
+ # Default way to setup Henlo. Run `rails generate henlo:install` to create
39
+ ## a fresh initializer with all configuration values.
40
+ def self.setup
41
+ yield self
42
+ end
43
+
44
+
45
+ end
46
+
@@ -0,0 +1,90 @@
1
+ require "henlo/helpers/util"
2
+ require 'active_record/errors'
3
+ #
4
+ ## Module
5
+ module Henlo::Authenticable
6
+
7
+ #
8
+ ## Retrieve the token type from the jwt payload
9
+ def self.parse_token_type(token, options={})
10
+ claim = Knock::AuthToken.new(token: token, verify_options: options).payload
11
+ claim["type"]
12
+ end
13
+
14
+ #
15
+ ## Match the token jwt identifier with what is stored in the database for the resource,
16
+ ## a lack of match indicates suspicious activities
17
+ def self.jti_match?(payload, resource)
18
+ payload["jti"] === resource.refresh_token_jti
19
+ end
20
+
21
+ #
22
+ ## Check the resource to see if it has been flagged for blacklist check
23
+ def self.it_suspicious?(resource)
24
+ resource.blacklist_check?
25
+ end
26
+
27
+ #
28
+ ## Check the blacklisted tokens table to see whether the token's jwt identifier has been
29
+ ## blacklisted
30
+ def self.it_not_fren?(resource)
31
+ BlacklistedToken.where(token_jti: resource.refresh_token_jti).first
32
+ end
33
+
34
+ #
35
+ ## Parse the resource as identified by the id encoded in the jwt with the key "sub"
36
+ def self.parse_resource(payload, model)
37
+ resource = model.capitalize.constantize.where(id: payload["sub"]).first
38
+ if resource.nil?
39
+ raise ActiveRecord::RecordNotFound
40
+ end
41
+ resource
42
+ end
43
+
44
+ #
45
+ ## Authenticates resource by first determining the treatment based on the type of token.
46
+ ## Requests with valid id tokens will be processed.
47
+ ## Requests with refresh tokens will be checked for 1) whether the resource has been flagged
48
+ ## for blacklist check and if yes, 2) whether the token's jwt identifier has been flaglisted.
49
+ ## If neither 1) nor 2) is established, the token will be checked for a match of the jwt identifier
50
+ ## The resource is returned if all these checks are passed.
51
+ def self.it_me?(token, model)
52
+ type = parse_token_type(token)
53
+ payload = Knock::AuthToken.new(token: token).payload
54
+ resource = parse_resource(payload, model)
55
+ case type
56
+ when "id"
57
+ resource
58
+ when "refresh"
59
+ if it_suspicious?(resource) && it_not_fren?(resource)
60
+ nil
61
+ else
62
+ if jti_match?(payload, resource)
63
+ resource
64
+ else
65
+ Henlo::Revocable.token_blockt(payload, resource)
66
+ nil
67
+ end
68
+ end
69
+ else
70
+ nil
71
+ end
72
+ end
73
+
74
+ #
75
+ ## This method is to be called before `it_me?` is called, so that expired tokens are treated before
76
+ ## the authentication begins. Requests made with expired id tokens are rejected with an error.
77
+ ## Requests made with expired refresh tokens are then processed with "reauthentication_strategy.
78
+ ## This method is passed as an argument to `it_expired` by the app. You can define how
79
+ ## users are reauthenticated in your own app.
80
+ def self.it_expired(reauthenticate_strategy, token, model)
81
+ token = Knock::AuthToken.new(token: token, verify_options: {verify_expiration: false}).token
82
+ claim = Knock::AuthToken.new(token: token, verify_options: {verify_expiration: false}).payload
83
+ resource = parse_resource(claim, model)
84
+ if claim["type"] == "id"
85
+ raise ActionController::InvalidAuthenticityToken
86
+ else
87
+ reauthenticate_strategy
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,17 @@
1
+
2
+ #
3
+ ## Helper methods
4
+ module Henlo
5
+ module Helpers
6
+ module Util
7
+
8
+ #
9
+ ## Generates a random string in the format of "a5391f26-1136-46f3-a3d3-ea4e1e558f06"
10
+ ## to use as a unique jwt identifier.
11
+ def self.generate_jti
12
+ SecureRandom.uuid
13
+ end
14
+
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,26 @@
1
+ require "henlo/helpers/util"
2
+
3
+ #
4
+ ## Generates id token. The id token is used to identify and authenticate the user before
5
+ ## responding to a request
6
+ module Henlo::Identifiable
7
+
8
+ # Generates id token and returns both the token, with the optional payload encoded, and the
9
+ ## token expiry time in unix seconds
10
+ def self.generate_identifiable(options={})
11
+ claim = options || nil
12
+
13
+ claim.merge!({
14
+ exp: Time.now.utc.to_i + Henlo.id_token_lifetime,
15
+ jti: Henlo::Helpers::Util.generate_jti,
16
+ type: "id"
17
+ })
18
+
19
+ Hash[
20
+ token: Knock::AuthToken.new(payload: claim).token,
21
+ exp: claim[:exp]
22
+ ]
23
+ end
24
+
25
+
26
+ end
@@ -0,0 +1,33 @@
1
+ require "henlo/helpers/util"
2
+
3
+ #
4
+ ## Module for generating refresh tokens
5
+
6
+ module Henlo::Refreshable
7
+
8
+ #
9
+ ## Generate refreshable token with a unix time for expiry, the type of token
10
+ ## and the jwt identifier (a random string) encoded in the payload in addition to
11
+ ## whatever was passed as payload when `generate_henlos` was called
12
+ def self.generate_refreshable(options={})
13
+ claim = options || nil
14
+
15
+ claim.merge!({
16
+ exp: Time.now.utc.to_i + Henlo.refresh_token_lifetime,
17
+ jti: Henlo::Helpers::Util.generate_jti,
18
+ type: "refresh"
19
+ })
20
+
21
+ Hash[
22
+ token: Knock::AuthToken.new(payload: claim).token,
23
+ jti: claim[:jti]
24
+ ]
25
+ end
26
+
27
+ #
28
+ ## Store jwt identifier in the app's database, in the table of the resource
29
+ def self.store_jti(resource, jti)
30
+ resource.update_attribute(:refresh_token_jti, jti)
31
+ end
32
+
33
+ end
@@ -0,0 +1,28 @@
1
+ #
2
+ ## Module allows the blacklist of tokens as identified by the jti (jwt identifier)
3
+ ## Blacklisted refresh tokens cannot be used to generate new id tokens
4
+ module Henlo::Revocable
5
+
6
+ #
7
+ ## Method called when the identifier as encoded in the token payload does not match what was stored in the database
8
+ ## or when the revoke token route is called by the user in cases of breach such as device loss
9
+ ## the token is blacklisted and the resource is flagged as needing blacklist checks
10
+ def self.token_blockt(payload, resource)
11
+ resource.blacklist_check == true
12
+ resource.save!
13
+
14
+ blacklisted_token = BlacklistedToken.create(
15
+ token_jti: payload["jti"],
16
+ exp_in_unix: payload["exp"]
17
+ )
18
+ end
19
+
20
+ #
21
+ ## Call this period in a scheduled task to clean expired tokens from the database
22
+ def self.token_rekt
23
+ BlacklistedToken.each do |token|
24
+ token.destroy unless Time.now.utc < token.exp_in_unix
25
+ end
26
+ end
27
+
28
+ end
@@ -0,0 +1,3 @@
1
+ module Henlo
2
+ VERSION = "0.1.0"
3
+ end
metadata ADDED
@@ -0,0 +1,156 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: henlo
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - nombiezinja
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2018-01-31 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rails
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '4.2'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '4.2'
27
+ - !ruby/object:Gem::Dependency
28
+ name: jwt
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.5'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.5'
41
+ - !ruby/object:Gem::Dependency
42
+ name: knock
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: 2.1.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.1.1
55
+ - !ruby/object:Gem::Dependency
56
+ name: bundler
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '1.15'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '1.15'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rake
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '10.0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '10.0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rspec
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '3.0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '3.0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: rdoc
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ description: Based on the Knock gem, offers options to further customize secure authentication
112
+ practices
113
+ email:
114
+ - tianyizhang1987@gmail.com
115
+ executables: []
116
+ extensions: []
117
+ extra_rdoc_files: []
118
+ files:
119
+ - Rakefile
120
+ - lib/generators/henlo/install_generator.rb
121
+ - lib/generators/henlo/migrations_generator.rb
122
+ - lib/generators/templates/henlo_initializer.rb
123
+ - lib/generators/templates/migrations/add_jti_column.rb
124
+ - lib/generators/templates/migrations/create_blacklisted_tokens.rb.erb
125
+ - lib/henlo.rb
126
+ - lib/henlo/authenticable.rb
127
+ - lib/henlo/helpers/util.rb
128
+ - lib/henlo/identifiable.rb
129
+ - lib/henlo/refreshable.rb
130
+ - lib/henlo/revocable.rb
131
+ - lib/henlo/version.rb
132
+ homepage: https://github.com/nombiezinja/henlo
133
+ licenses:
134
+ - MIT
135
+ metadata: {}
136
+ post_install_message:
137
+ rdoc_options: []
138
+ require_paths:
139
+ - lib
140
+ required_ruby_version: !ruby/object:Gem::Requirement
141
+ requirements:
142
+ - - ">="
143
+ - !ruby/object:Gem::Version
144
+ version: '0'
145
+ required_rubygems_version: !ruby/object:Gem::Requirement
146
+ requirements:
147
+ - - ">="
148
+ - !ruby/object:Gem::Version
149
+ version: '0'
150
+ requirements: []
151
+ rubyforge_project:
152
+ rubygems_version: 2.4.0
153
+ signing_key:
154
+ specification_version: 4
155
+ summary: JWT based authentication with access and id tokens
156
+ test_files: []