fb-jwt-auth 0.1.0 → 0.4.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ff3e7406a923af4e7374f7855ddce3de5bbe9e46267b63a2d06c92a74f3ec13a
4
- data.tar.gz: 1a0db0196134bed15fec85fb83006f0254f5dc0b2e96f02644b4d452499a121c
3
+ metadata.gz: 45ebbebff675bd2afe201e48c9d3d711ae255619511e00318f6cd39aef8094c0
4
+ data.tar.gz: e1d51f15d8b8a4a55be59228edce5aa3723b74bd312d0b87f868e81ecd3bcdb3
5
5
  SHA512:
6
- metadata.gz: 9ef0173a6261f0b8f8377f1987ab3d8c16b7364f25209ab10094240a096e2b9ef24f35944f12ad81a06a85357da1e48c76899285dcd40f8210254993da44a81b
7
- data.tar.gz: aee6abbd74914c5ba92158c5e6eb3ba3b62bd55e36bd5fa57d2877fb76a5ebd806317072a5ee8d6fd91b7318af788ec89da4bf15ad421d66426a908636b8d1c0
6
+ metadata.gz: 56e7552bcdede5d4ce45200ab5160a790128e80ee0a6f276d0e4d7f9f4515de14db7a9efab79554df31593ee801e9a983d1af413327427b1c88e184a80e88042
7
+ data.tar.gz: 395d1d6f094db54c9411bc20d6804c94ea56f7a52e60a64e4496f6ed75d2bb5bd52817ada3fbc7058a404fa47db4b48b850df16ccb9a4ae96236b4591a13d2c6
@@ -0,0 +1,63 @@
1
+ version: 2.1
2
+ orbs:
3
+ slack: circleci/slack@3.4.2
4
+
5
+ jobs:
6
+ test:
7
+ docker:
8
+ - image: cimg/ruby:2.7.2
9
+ steps:
10
+ - checkout
11
+ - run:
12
+ name: Install
13
+ command: bundle install
14
+ - run:
15
+ name: Test
16
+ command: bundle exec rspec
17
+ - slack/status: &slack_status
18
+ fail_only: true
19
+ only_for_branches: main
20
+ failure_message: ":facepalm: Failed job $CIRCLE_JOB :homer-disappear:"
21
+ include_job_number_field: false
22
+ publish:
23
+ docker:
24
+ - image: cimg/ruby:2.7.2
25
+ steps:
26
+ - checkout
27
+ - run:
28
+ name: Install
29
+ command: bundle install
30
+ - run:
31
+ name: Setup Rubygems
32
+ command: |
33
+ mkdir ~/.gem
34
+ echo -e "---\r\n:rubygems_api_key: $RUBYGEMS_API_KEY" > ~/.gem/credentials
35
+ chmod 0600 /home/circleci/.gem/credentials
36
+ - run:
37
+ name: Publish to Rubygems
38
+ command: |
39
+ set -e
40
+
41
+ VERSION=$(ruby -e "require './lib/fb/jwt/auth/version.rb'; puts Fb::Jwt::Auth::VERSION")
42
+ PUBLISHED_VERSION=$(curl https://rubygems.org/api/v1/versions/fb-jwt-auth/latest.json | sed -e 's/[{}]/''/g' | sed s/\"//g | awk -v RS=',' -F: '$1=="version"{print $2}')
43
+
44
+ if [ "$VERSION" != "$PUBLISHED_VERSION" ]
45
+ then
46
+ bundle exec gem build fb-jwt-auth.gemspec
47
+ bundle exec gem push fb-jwt-auth-*.gem
48
+ curl -X POST -H 'Content-type: application/json' --data "{\"text\":\":woohoo: Successfully published ${CIRCLE_PROJECT_REPONAME} ${VERSION} :ship_it_parrot:\"}" "$SLACK_WEBHOOK"
49
+ fi
50
+ - slack/status: *slack_status
51
+
52
+ workflows:
53
+ commit-workflow:
54
+ jobs:
55
+ - test
56
+ - publish:
57
+ requires:
58
+ - test
59
+ filters:
60
+ tags:
61
+ only: /.*/
62
+ branches:
63
+ only: main
data/.gitignore CHANGED
@@ -11,3 +11,4 @@
11
11
  .rspec_status
12
12
  Gemfile.lock
13
13
  .byebug_history
14
+ *.gem
@@ -0,0 +1,18 @@
1
+ # 0.3.0
2
+ * Request non cached version of public key if first validition fails
3
+
4
+ # v0.2.2
5
+ * Add token not present exception when token is empty
6
+
7
+ # v0.2.1
8
+ * Add better error messages
9
+
10
+ # v0.2.0
11
+
12
+ * Add service token cache v3 implementation
13
+ * All necessary information (iat, sub and application, namespace) should be on
14
+ the access token
15
+
16
+ # v0.1.0
17
+
18
+ * Add service token cache v2 implementation
data/README.md CHANGED
@@ -18,11 +18,39 @@ Or install it yourself as:
18
18
 
19
19
  ## Usage
20
20
 
21
- ```
21
+ ```ruby
22
22
  Fb::Jwt::Auth.configure do |config|
23
+ # Service token cache domain
24
+ #
23
25
  config.service_token_cache_root_url = ENV['SERVICE_TOKEN_CACHE_ROOT_URL']
24
26
  end
27
+ ```
28
+ In order to generate the service access token we need to use `Fb::Jwt::Auth::ServiceAccessToken.new.generate` or if you require a subject, `Fb::Jwt::Auth::ServiceAccessToken.new(subject: subject).generate`
29
+
30
+ In the case you need to configure the service access token as a client
31
+ ```ruby
32
+ Fb::Jwt::Auth.configure do |config|
33
+ config.issuer = 'fb-editor'
34
+ config.namespace = 'formbuilder-saas-test'
35
+ config.encoded_private_key = 'base64 encoded private key'
36
+ end
37
+ ```
38
+
39
+ ### Using other endpoint versions
40
+
41
+ Service token cache can have different versions of authenticating a service.
42
+
43
+ You can configure the version:
44
+
45
+ ```ruby
46
+ Fb::Jwt::Auth.configure do |config|
47
+ config.service_token_cache_api_version = :v3
48
+ end
49
+ ```
25
50
 
51
+ ### Verifying the token
52
+
53
+ ```ruby
26
54
  Fb::Jwt::Auth.new(
27
55
  access_token: request.headers['x-access-token-v2'],
28
56
  key: 'fb-editor', # service name
@@ -1,24 +1,23 @@
1
1
  require 'fb/jwt/auth/version'
2
2
  require 'openssl'
3
3
  require 'jwt'
4
- require 'active_support/core_ext'
4
+ require 'active_support/all'
5
5
 
6
6
  module Fb
7
7
  module Jwt
8
8
  class Auth
9
- def self.service_token_cache_root_url=(value)
10
- @@service_token_cache_root_url = value
11
- end
12
-
13
- def self.service_token_cache_root_url
14
- @@service_token_cache_root_url
15
- end
9
+ cattr_accessor :service_token_cache_root_url,
10
+ :service_token_cache_api_version,
11
+ :encoded_private_key,
12
+ :issuer,
13
+ :namespace
16
14
 
17
15
  def self.configure(&block)
18
16
  yield self
19
17
  end
20
18
 
21
19
  autoload :ServiceTokenClient, 'fb/jwt/auth/service_token_client'
20
+ autoload :ServiceAccessToken, 'fb/jwt/auth/service_access_token'
22
21
 
23
22
  class TokenNotPresentError < StandardError
24
23
  end
@@ -29,9 +28,15 @@ module Fb
29
28
  class TokenExpiredError < StandardError
30
29
  end
31
30
 
31
+ class IssuerNotPresentError < StandardError
32
+ end
33
+
34
+ class NamespaceNotPresentError < StandardError
35
+ end
36
+
32
37
  attr_accessor :token, :key, :leeway, :logger
33
38
 
34
- def initialize(token:, key:, leeway:, logger:)
39
+ def initialize(token:, key: nil, leeway:, logger:)
35
40
  @token = token
36
41
  @key = key
37
42
  @leeway = leeway
@@ -39,19 +44,14 @@ module Fb
39
44
  end
40
45
 
41
46
  def verify!
42
- raise TokenNotPresentError if token.nil?
47
+ raise TokenNotPresentError.new('Token is not present') if token.blank?
48
+
49
+ application_details = find_application_info
43
50
 
44
51
  begin
45
- hmac_secret = public_key(key)
46
- payload, _header = JWT.decode(
47
- token,
48
- hmac_secret,
49
- true,
50
- exp_leeway: leeway,
51
- algorithm: 'RS256'
52
- )
52
+ payload, _header = retrieve_and_decode_public_key(application_details)
53
53
  rescue StandardError => e
54
- error_message = "Couldn't parse that token - error #{e}"
54
+ error_message = "Token is not valid: error #{e}"
55
55
  logger.debug(error_message)
56
56
  raise TokenNotValidError.new(error_message)
57
57
  end
@@ -61,7 +61,7 @@ module Fb
61
61
  iat_skew = payload['iat'].to_i - Time.zone.now.to_i
62
62
 
63
63
  if iat_skew.abs > leeway.to_i
64
- error_message = "iat skew is #{iat_skew}, max is #{leeway} - INVALID"
64
+ error_message = "Token has expired: iat skew is #{iat_skew}, max is #{leeway}"
65
65
  logger.debug(error_message)
66
66
 
67
67
  raise TokenExpiredError.new(error_message)
@@ -71,8 +71,42 @@ module Fb
71
71
  payload
72
72
  end
73
73
 
74
- def public_key
75
- OpenSSL::PKey::RSA.new(ServiceTokenClient.new(key).public_key)
74
+ def retrieve_and_decode_public_key(application_details)
75
+ hmac_secret = public_key(application_details)
76
+ decode(hmac_secret: hmac_secret)
77
+ rescue JWT::VerificationError
78
+ logger.debug('First validation failed. Requesting non cached public key')
79
+ hmac_secret = public_key(application_details.merge(ignore_cache: true))
80
+ decode(hmac_secret: hmac_secret)
81
+ end
82
+
83
+ def decode(verify: true, hmac_secret: nil)
84
+ JWT.decode(
85
+ token,
86
+ hmac_secret,
87
+ verify,
88
+ exp_leeway: leeway,
89
+ algorithm: 'RS256'
90
+ )
91
+ end
92
+
93
+ def find_application_info
94
+ return { application: key } if key
95
+
96
+ payload, _header = decode(verify: false)
97
+ application = payload['iss']
98
+ namespace = payload['namespace']
99
+
100
+ raise IssuerNotPresentError.new('Issuer is not present in the token') unless application
101
+ raise NamespaceNotPresentError.new('Namespace is not present in the token') unless namespace
102
+
103
+ { application: application, namespace: namespace}
104
+ end
105
+
106
+ def public_key(attributes)
107
+ OpenSSL::PKey::RSA.new(
108
+ ServiceTokenClient.new(attributes).public_key
109
+ )
76
110
  end
77
111
  end
78
112
  end
@@ -0,0 +1,45 @@
1
+ module Fb
2
+ module Jwt
3
+ class Auth
4
+ class ServiceAccessToken
5
+ attr_reader :encoded_private_key,
6
+ :issuer,
7
+ :subject,
8
+ :namespace
9
+
10
+ def initialize(subject: nil)
11
+ @subject = subject
12
+ @encoded_private_key = Fb::Jwt::Auth.encoded_private_key
13
+ @namespace = Fb::Jwt::Auth.namespace
14
+ @issuer = Fb::Jwt::Auth.issuer
15
+ end
16
+
17
+ def generate
18
+ return '' if encoded_private_key.blank?
19
+
20
+ private_key = OpenSSL::PKey::RSA.new(
21
+ Base64.strict_decode64(encoded_private_key.chomp)
22
+ )
23
+
24
+ JWT.encode(
25
+ token,
26
+ private_key,
27
+ 'RS256'
28
+ )
29
+ end
30
+
31
+ private
32
+
33
+ def token
34
+ payload = {
35
+ iss: issuer,
36
+ iat: Time.current.to_i
37
+ }
38
+ payload[:sub] = subject if subject.present?
39
+ payload[:namespace] = namespace if namespace.present?
40
+ payload
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -5,11 +5,19 @@ require 'base64'
5
5
  class Fb::Jwt::Auth::ServiceTokenClient
6
6
  class ServiceTokenCacheError < StandardError; end
7
7
 
8
- attr_accessor :key, :root_url
8
+ ENDPOINTS = {
9
+ v2: '/service/v2/%{application}',
10
+ v3: '/v3/applications/%{application}/namespaces/%{namespace}'
11
+ }
9
12
 
10
- def initialize(key)
11
- @key = key
13
+ attr_accessor :application, :namespace, :root_url, :api_version
14
+
15
+ def initialize(application:, namespace: nil, ignore_cache: false)
16
+ @application = application
17
+ @namespace = namespace
18
+ @ignore_cache = ignore_cache
12
19
  @root_url = Fb::Jwt::Auth.service_token_cache_root_url
20
+ @api_version = Fb::Jwt::Auth.service_token_cache_api_version || :v2
13
21
  end
14
22
 
15
23
  def public_key
@@ -31,7 +39,21 @@ class Fb::Jwt::Auth::ServiceTokenClient
31
39
 
32
40
  private
33
41
 
42
+ attr_reader :ignore_cache
43
+
34
44
  def public_key_uri
35
- URI.join(@root_url, '/service/v2/', key)
45
+ URI.join(root_url, "#{version_url}#{query_param}")
46
+ end
47
+
48
+ def query_param
49
+ ignore_cache ? '?ignore_cache=true' : ''
50
+ end
51
+
52
+ def version_url
53
+ if api_version == :v3
54
+ ENDPOINTS[api_version] % { application: application, namespace: namespace }
55
+ else
56
+ ENDPOINTS[api_version] % { application: application }
57
+ end
36
58
  end
37
59
  end
@@ -1,7 +1,7 @@
1
1
  module Fb
2
2
  module Jwt
3
3
  class Auth
4
- VERSION = "0.1.0"
4
+ VERSION = "0.4.0"
5
5
  end
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fb-jwt-auth
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Form builder developers
8
- autorequire:
8
+ autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-10-21 00:00:00.000000000 Z
11
+ date: 2021-01-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: jwt
@@ -59,6 +59,7 @@ executables: []
59
59
  extensions: []
60
60
  extra_rdoc_files: []
61
61
  files:
62
+ - ".circleci/config.yml"
62
63
  - ".gitignore"
63
64
  - ".rspec"
64
65
  - ".ruby-version"
@@ -72,6 +73,7 @@ files:
72
73
  - bin/setup
73
74
  - fb-jwt-auth.gemspec
74
75
  - lib/fb/jwt/auth.rb
76
+ - lib/fb/jwt/auth/service_access_token.rb
75
77
  - lib/fb/jwt/auth/service_token_client.rb
76
78
  - lib/fb/jwt/auth/version.rb
77
79
  homepage: https://github.com/ministryofjustice/fb-jwt-auth
@@ -81,7 +83,7 @@ metadata:
81
83
  homepage_uri: https://github.com/ministryofjustice/fb-jwt-auth
82
84
  source_code_uri: https://github.com/ministryofjustice/fb-jwt-auth
83
85
  changelog_uri: https://github.com/ministryofjustice/fb-jwt-auth/blob/main/Changelog.md
84
- post_install_message:
86
+ post_install_message:
85
87
  rdoc_options: []
86
88
  require_paths:
87
89
  - lib
@@ -97,7 +99,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
97
99
  version: '0'
98
100
  requirements: []
99
101
  rubygems_version: 3.1.4
100
- signing_key:
102
+ signing_key:
101
103
  specification_version: 4
102
104
  summary: JWT authentication done in form builder team
103
105
  test_files: []