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 +7 -0
- data/.ruby-version +1 -0
- data/README.md +23 -0
- data/Rakefile +12 -0
- data/lib/tasks/jwt_auth.rake +23 -0
- data/lib/usps/jwt_auth/concern.rb +123 -0
- data/lib/usps/jwt_auth/config.rb +36 -0
- data/lib/usps/jwt_auth/decode.rb +47 -0
- data/lib/usps/jwt_auth/encode.rb +80 -0
- data/lib/usps/jwt_auth/version.rb +7 -0
- data/lib/usps/jwt_auth.rb +27 -0
- data/sig/usps_jwt/auth.rbs +6 -0
- metadata +66 -0
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,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,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
|
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: []
|