usps-jwt_auth 0.0.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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 13fa5947571c35b0d6cea5c6236bb61048f3977dd6c008a0c9a04887f458a2e6
4
+ data.tar.gz: 48572594ff31f88c6f9043c0856b49b9a3b2bcf9faaa380d2dd5ee40561cb614
5
+ SHA512:
6
+ metadata.gz: a8777d6b9c4c146e327aba4eb179d500168b620d3f811b6f76fa23eddace6f3eabb1ce2926158ecd1729145c19025f5579f27a874446853551a1510fbaf37703
7
+ data.tar.gz: 5c436aabb4889f0ef6cd5cc0601b0ec00e7ce434fb373a7e7977c819fd8769ea90d582137bdd10b0b0fb42c6f41d6905fb3f3aa9eede62fe0f8ab2aebe206564
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 3.4.6
data/README.md ADDED
@@ -0,0 +1,23 @@
1
+ # USPS JWT Authentication
2
+
3
+ ## Installation
4
+
5
+ ```sh
6
+ bundle exec rake usps:jwt:install
7
+ ```
8
+
9
+ ## Configuration
10
+
11
+ ```ruby
12
+ Usps::JwtAuth.configure do |config|
13
+ config.environment = Rails.env
14
+ config.keys_path = Rails.root.join('config/keys')
15
+ config.public_keys_path = Rails.root.join('config/public_keys')
16
+
17
+ config.jwt = {
18
+ audience: ENV.fetch('JWT_AUDIENCE'),
19
+ issuer_base: ENV.fetch('JWT_ISSUER_BASE', 'usps:1'),
20
+ issuers: ENV.fetch('JWT_ISSUERS', 'admin:1').split(','),
21
+ }
22
+ end
23
+ ```
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ require 'rspec/core/rake_task'
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ require 'rubocop/rake_task'
9
+
10
+ RuboCop::RakeTask.new
11
+
12
+ task default: %i[spec rubocop]
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ namespace :usps do
4
+ namespace :jwt do
5
+ desc 'Setup JWT Authentication'
6
+ task install: :environment do
7
+ # Ensure keys directories exist
8
+ FileUtils.mkdir_p(Usps::JwtAuth.configuration.keys_path)
9
+ FileUtils.touch(Usps::JwtAuth.configuration.keys_path.join('/.keep'))
10
+ FileUtils.mkdir_p(Usps::JwtAuth.configuration.public_keys_path)
11
+ FileUtils.touch(Usps::JwtAuth.configuration.public_keys_path.join('/.keep'))
12
+
13
+ # Ignore keys directories from git
14
+ File.open('.gitignore', 'a') do |file|
15
+ file.puts "\n"
16
+ file.puts Usps::JwtAuth.configuration.keys_path
17
+ file.puts "!#{Usps::JwtAuth.configuration.keys_path.join('/.keep')}"
18
+ file.puts Usps::JwtAuth.configuration.public_keys_path
19
+ file.puts "!#{Usps::JwtAuth.configuration.public_keys_path.join('/.keep')}"
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,123 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Usps
4
+ module JwtAuth
5
+ # Controller helpers for handling JWT authentication
6
+ #
7
+ module Concern
8
+ extend ActiveSupport::Concern
9
+
10
+ included do
11
+ helper_method :current_user
12
+ helper_method :jwt_user
13
+ helper_method :user_signed_in?
14
+ end
15
+
16
+ private
17
+
18
+ def current_user
19
+ return @current_user if defined?(@current_user)
20
+
21
+ stub_jwt! if JwtAuth.configuration.environment.test? && fetch_jwt.nil?
22
+
23
+ current_user_from_jwt
24
+ end
25
+
26
+ # Validate JWT, or else redirect to old location
27
+ # Old location will check for login, and either generate a JWT and redirect back, or
28
+ # redirect to login page
29
+ def authenticate_user_from_jwt!
30
+ user_signed_in? || redirect_to_login
31
+ end
32
+
33
+ def user_signed_in?
34
+ current_user.present?
35
+ end
36
+
37
+ ###############
38
+ # Gem Private #
39
+ ###############
40
+
41
+ def current_user_from_jwt
42
+ reset_session if params[:logout].present?
43
+ return if set_new_jwt
44
+
45
+ load_current_user if fetch_jwt.present?
46
+ end
47
+
48
+ def load_current_user
49
+ @current_user = jwt_user
50
+ return @current_user unless Pundit.policy(@current_user, :admin).admin? && session.key?('impersonate')
51
+
52
+ # Admin has entered impersonation mode -- override current_user
53
+ @current_user = Members::Member.find(session['impersonate']['impersonated'])
54
+ rescue JWT::ExpiredSignature
55
+ clear_jwt
56
+ nil
57
+ rescue ActiveRecord::RecordNotFound => e
58
+ Bugsnag.notify(e)
59
+ clear_jwt
60
+ nil
61
+ end
62
+
63
+ def jwt_user
64
+ @jwt_user ||= Members::Member.find(jwt['certificate']) if fetch_jwt
65
+ end
66
+
67
+ def set_new_jwt
68
+ return if params[:jwt].blank?
69
+
70
+ store_jwt(params[:jwt])
71
+ redirect_to(params[:path] || root_path) # Breaks the login loop
72
+ nil
73
+ end
74
+
75
+ def store_jwt(token)
76
+ session[:jwt] = token
77
+ cookies[:jwt] = { value: token, domain: cookie_domain, httponly: true }
78
+ end
79
+
80
+ def cookie_domain
81
+ JwtAuth.configuration.environment.production? ? '.aws.usps.org' : 'localhost'
82
+ end
83
+
84
+ def clear_jwt
85
+ session[:jwt] = nil
86
+ cookies.delete(:jwt, domain: cookie_domain)
87
+ end
88
+
89
+ def fetch_jwt
90
+ session[:jwt] || cookies[:jwt]
91
+ end
92
+
93
+ def jwt
94
+ Decode.decode(
95
+ fetch_jwt,
96
+ audience: [JwtAuth.configuration.jwt.audience],
97
+ issuer: JwtAuth.configuration.jwt.issuer
98
+ )
99
+ end
100
+
101
+ def redirect_to_login
102
+ url = 'https://www.usps.org/jwt'
103
+ local = "#{url}?local&port=#{ENV.fetch('PORT', '3000')}"
104
+ production = "#{url}?application=#{JwtAuth.configuration.jwt.audience}"
105
+ url = JwtAuth.configuration.environment.development? ? local : production
106
+ url = "#{url}&path=#{request.path}"
107
+
108
+ redirect_to(url, allow_other_host: true)
109
+ end
110
+
111
+ def stub_jwt!
112
+ raise 'Cannot stub JWT outside of test environment!' unless JwtAuth.configuration.environment.test?
113
+
114
+ store_jwt(
115
+ Encode.encode(
116
+ { certificate: ENV['STUB_CERTIFICATE'].presence || 'E123456' },
117
+ audience: [JwtAuth.configuration.jwt.audience], issuer: JwtAuth.configuration.issuers.first
118
+ )
119
+ )
120
+ end
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Usps
4
+ module JwtAuth
5
+ # Configure JWT Authentication
6
+ #
7
+ module Config
8
+ attr_accessor :keys_path, :public_keys_path, :environment
9
+ attr_writer :key_size
10
+ attr_reader :jwt
11
+
12
+ def initialize
13
+ yield self if block_given?
14
+ end
15
+
16
+ def key_size
17
+ @key_size || 4096
18
+ end
19
+
20
+ def jwt=(hash)
21
+ @jwt = ActiveSupport::OrderedOptions.new(
22
+ {
23
+ issuer_base: 'usps:1',
24
+ issuers: [],
25
+ audience: nil,
26
+ algorithm: 'RS512'
27
+ }.merge(hash)
28
+ )
29
+ end
30
+
31
+ def issuer
32
+ /\A#{jwt.issuer_base}(?::#{RegExp.union(jwt.issuers)})?\z/
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Usps
4
+ module JwtAuth
5
+ # Decode and validate data from a JWT
6
+ #
7
+ class Decode
8
+ CONFIG = {
9
+ required_claims: %w[iss exp],
10
+ verify_iss: true,
11
+ verify_aud: true,
12
+ algorithm: JwtAuth.configuration.jwt.algorithm
13
+ }.freeze
14
+
15
+ def self.decode(token, audience: [], issuer: nil)
16
+ new.decode(token, audience: audience, issuer: issuer)
17
+ end
18
+
19
+ def self.issuer_pattern(issuer)
20
+ /\A#{JwtAuth.configuration.jwt.issuer_base}(?:\z|:#{issuer})/
21
+ end
22
+
23
+ def decode(token, audience: [], issuer: nil)
24
+ result = JWT.decode(
25
+ token,
26
+ public_key(token),
27
+ true,
28
+ CONFIG.merge(
29
+ aud: audience,
30
+ iss: self.class.issuer_pattern(issuer)
31
+ )
32
+ )
33
+ result[0]['data']
34
+ end
35
+
36
+ private
37
+
38
+ def public_key(token)
39
+ OpenSSL::PKey::RSA.new(File.read("#{JwtAuth.configuration.public_keys_path}/#{fingerprint(token)}.pub"))
40
+ end
41
+
42
+ def fingerprint(token)
43
+ JWT.decode(token, nil, false)[0]['key']
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Usps
4
+ module JwtAuth
5
+ # Encode data as a JWT
6
+ #
7
+ class Encode
8
+ JwtAuth.config.key_size
9
+
10
+ def self.encode(data, expiration_time: 15 * 60, audience: [], issuer: nil)
11
+ new(expiration_time: expiration_time, audience: audience, issuer: issuer).encode(data)
12
+ end
13
+
14
+ def initialize(expiration_time: 15 * 60, audience: [], issuer: nil)
15
+ private_key
16
+ @expiration_time = expiration_time
17
+ @audience = audience
18
+ @issuer = issuer
19
+ rescue StandardError
20
+ @private_key = generate_private_key
21
+ store_private_key
22
+ retry
23
+ ensure
24
+ store_public_key
25
+ end
26
+
27
+ def encode(data)
28
+ JWT.encode(payload(data), private_key, ALGORITHM)
29
+ end
30
+
31
+ def public_key
32
+ @public_key ||= private_key.public_key
33
+ end
34
+
35
+ def fingerprint
36
+ OpenSSL::Digest::SHA256.new(public_key.to_der).to_s
37
+ end
38
+
39
+ private
40
+
41
+ def expires_at
42
+ DateTime.now.to_i + @expiration_time
43
+ end
44
+
45
+ def payload(data)
46
+ {
47
+ data: data,
48
+ exp: expires_at.to_i,
49
+ iss: [JwtAuth.configuration.jwt.issuer_base, @issuer].join(':'),
50
+ aud: @audience,
51
+ key: fingerprint
52
+ }
53
+ end
54
+
55
+ def private_key
56
+ @private_key ||= OpenSSL::PKey::RSA.new(File.read(keys.first))
57
+ end
58
+
59
+ def keys
60
+ Dir["#{JwtAuth.configuration.keys_path}/*"]
61
+ end
62
+
63
+ def store_private_key
64
+ path = "#{JwtAuth.configuration.keys_path}/#{fingerprint}"
65
+ File.open(path, 'w+') { |f| f.puts(private_key) }
66
+ FileUtils.chmod(0o600, path)
67
+ end
68
+
69
+ def store_public_key
70
+ path = "#{JwtAuth.configuration.public_keys_path}/#{fingerprint}.pub"
71
+ File.open(path, 'w+') { |f| f.puts(public_key) }
72
+ FileUtils.chmod(0o644, path)
73
+ end
74
+
75
+ def generate_private_key
76
+ OpenSSL::PKey::RSA.generate(KEY_SIZE)
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Usps
4
+ module JwtAuth
5
+ VERSION = '0.0.1'
6
+ end
7
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Internal requires
4
+ require_relative 'jwt_auth/version'
5
+ require_relative 'jwt_auth/config'
6
+ require_relative 'jwt_auth/encode'
7
+ require_relative 'jwt_auth/decode'
8
+ require_relative 'jwt_auth/concern'
9
+
10
+ module Usps
11
+ # DOC COMMENT
12
+ module JwtAuth
13
+ class << self
14
+ def configuration
15
+ @configuration ||= Config.new
16
+ end
17
+
18
+ def configure
19
+ yield(configuration) if block_given?
20
+ configuration
21
+ end
22
+
23
+ delegate :encode, to: :Encode
24
+ delegate :decode, to: :Decode
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,6 @@
1
+ module UspsJwt
2
+ module Auth
3
+ VERSION: String
4
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
5
+ end
6
+ end
metadata ADDED
@@ -0,0 +1,66 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: usps-jwt_auth
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Julian Fiander
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: jwt
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '0'
26
+ description: JWT authentication for USPS applications deployed to AWS
27
+ email:
28
+ - jsfiander@gmail.com
29
+ executables: []
30
+ extensions: []
31
+ extra_rdoc_files: []
32
+ files:
33
+ - ".ruby-version"
34
+ - README.md
35
+ - Rakefile
36
+ - lib/tasks/jwt_auth.rake
37
+ - lib/usps/jwt_auth.rb
38
+ - lib/usps/jwt_auth/concern.rb
39
+ - lib/usps/jwt_auth/config.rb
40
+ - lib/usps/jwt_auth/decode.rb
41
+ - lib/usps/jwt_auth/encode.rb
42
+ - lib/usps/jwt_auth/version.rb
43
+ - sig/usps_jwt/auth.rbs
44
+ homepage: https://www.usps.org
45
+ licenses: []
46
+ metadata:
47
+ homepage_uri: https://www.usps.org
48
+ rubygems_mfa_required: 'true'
49
+ rdoc_options: []
50
+ require_paths:
51
+ - lib
52
+ required_ruby_version: !ruby/object:Gem::Requirement
53
+ requirements:
54
+ - - ">="
55
+ - !ruby/object:Gem::Version
56
+ version: 3.4.0
57
+ required_rubygems_version: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ requirements: []
63
+ rubygems_version: 3.6.9
64
+ specification_version: 4
65
+ summary: USPS JWT Authentication
66
+ test_files: []