authress-sdk 2.0.40.0 → 2.0.43.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: 3da3bb4308826d29d25cf3e139f9badc7dc83093639761fdcc1d0e04738a1c3a
4
- data.tar.gz: '0095c6dda00f393821a3aa4aaa109cad0e9ea9fdcb2f5a649e98cbbd24ea9360'
3
+ metadata.gz: 8d32ad65dcb072b5d8a7365e5859fc3342ae2425945380781707650a19f9926c
4
+ data.tar.gz: f81db7d0bc6e7fd9c5d1217f6cd87d02a3ce8dffd8e8e69e0ae95109ab33944c
5
5
  SHA512:
6
- metadata.gz: b77a612ba6dbd1e1fb1b2df5769a30023244c4001327395eb379259f24696a7ccc001a40a4df72a52bf1362fcf07471f5341d8058ee04ad6e7ba970c9ddac8ed
7
- data.tar.gz: 60823e8578a1f7862364ee3a88a6b9429f25c3c6b5e1e01c8a06eea0f0b63beae4a293a8a9852e4570295b42fecf868dfa0d2e6b1fa58eb35c685060a8660dfe
6
+ metadata.gz: 26e8f74c75866011ca9462cf00388401a1d7e402925013e588aaed065cebd8ff6662ce5fe95ce9d98dcdc4d69693e711a862566b1b4c04120b30ab31700b8c62
7
+ data.tar.gz: e7aacd4c91af4346b2c3ae79687335e5394fa811d2ceca575ddd531f9789dc9dddfffe2162cce941e1a4025b1228047247130141a3bb26930387f8325040a16f
data/README.md CHANGED
@@ -1,3 +1,7 @@
1
+ <p id="main" align="center">
2
+ <img src="https://authress.io/static/images/linkedin-banner.png" alt="Authress media banner">
3
+ </p>
4
+
1
5
  # Authress SDK for Ruby
2
6
  This is the Authress SDK used to integrate with the authorization as a service provider Authress at https://authress.io.
3
7
 
@@ -74,7 +78,7 @@ end
74
78
 
75
79
  # on api route
76
80
  [route('/resources/<resourceId>')]
77
- def getResource(resourceId) {
81
+ def getResource(resourceId)
78
82
  # Check Authress to authorize the user
79
83
  user_identity = AuthressSdk::AuthressClient.verify_token(request.headers.get('authorization'))
80
84
 
@@ -82,6 +86,7 @@ def getResource(resourceId) {
82
86
  user_id = user_identity.sub
83
87
  resource_uri = "resources/#{resourceId}" # String | The uri path of a resource to validate, must be URL encoded, uri segments are allowed, the resource must be a full path, and permissions are not inherited by sub-resources.
84
88
  permission = 'READ' # String | Permission to check, '*' and scoped permissions can also be checked here.
89
+
85
90
  begin
86
91
  # Check to see if a user has permissions to a resource.
87
92
  api_instance = AuthressSdk::UserPermissionsApi.new
@@ -126,4 +131,4 @@ rescue AuthressSdk::ApiError => e
126
131
  puts "Exception when calling AccessRecordsApi->create_record: #{e}"
127
132
  raise
128
133
  end
129
- ```
134
+ ```
@@ -20,6 +20,9 @@ module AuthressSdk
20
20
  # Token Provider
21
21
  attr_accessor :token_provider
22
22
 
23
+ # The Token verifier
24
+ attr_accessor :token_verifier
25
+
23
26
  # Initializes the AuthressClient
24
27
  def initialize()
25
28
  @config = {
@@ -29,6 +32,7 @@ module AuthressSdk
29
32
  }
30
33
 
31
34
  @token_provider = ConstantTokenProvider.new(nil)
35
+ @token_verifier = TokenVerifier.new()
32
36
  end
33
37
 
34
38
  def self.default
@@ -297,5 +301,12 @@ module AuthressSdk
297
301
  obj
298
302
  end
299
303
  end
304
+
305
+ # Verify a JWT token
306
+ # @param [String] The JWT token
307
+ # @return [Object] Returns a Map of user identity properties
308
+ def verify_token(token)
309
+ @token_verifier.verify_token(custom_domain_url, token)
310
+ end
300
311
  end
301
312
  end
@@ -146,7 +146,7 @@ module OmniAuth
146
146
  env['omniauth.auth'] = auth_hash
147
147
  call_app!
148
148
  end
149
- rescue AuthressSdk::TokenValidationError => e
149
+ rescue AuthressSdk::TokenVerificationError => e
150
150
  fail!(:token_validation_error, e)
151
151
  rescue ::OAuth2::Error, CallbackError => e
152
152
  fail!(:invalid_credentials, e)
@@ -1,17 +1,90 @@
1
- require 'date'
1
+ require 'time'
2
2
  require 'json'
3
3
  require 'logger'
4
4
  require 'uri'
5
5
 
6
6
  module AuthressSdk
7
7
  class ServiceClientTokenProvider
8
- def initialize(client_access_key)
8
+ def initialize(client_access_key, custom_domain_url = nil)
9
+ @custom_domain_url = custom_domain_url
9
10
  @client_access_key = client_access_key
11
+ @cachedKeyData = nil
12
+ end
13
+
14
+ def sanitizeUrl(url)
15
+ if url.nil?
16
+ return nil
17
+ end
18
+
19
+ if (url.match(/^http/))
20
+ return url
21
+ end
22
+
23
+ if (url.match(/^localhost/))
24
+ return "http://#{url}"
25
+ end
26
+
27
+ return "https://#{url}"
28
+ end
29
+
30
+ def get_issuer(unsanitizedAuthressCustomDomain, decodedAccessKey)
31
+ authressCustomDomain = sanitizeUrl(@custom_domain_url).gsub(/\/+$/, '')
32
+ return "#{authressCustomDomain}/v1/clients/#{decodedAccessKey.clientId}"
10
33
  end
11
34
 
12
35
  def get_token()
13
- # TODO: This should use the JWT creation strategy and not the client api token one
14
- @client_access_key
36
+ if @cachedKeyData && @cachedKeyData.token && Time.now().to_i() + 3600 < @cachedKeyData.expiresAtInSeconds
37
+ return @cachedKeyData.token
38
+ end
39
+
40
+ accountId = @client_access_key.split('.')[2];
41
+ decodedAccessKeyHash = {
42
+ clientId: @client_access_key.split('.')[0],
43
+ keyId: @client_access_key.split('.')[1],
44
+ audience: "#{accountId}.accounts.authress.io",
45
+ privateKey: @client_access_key.split('.')[3]
46
+ }
47
+ decodedAccessKey = Struct.new(*decodedAccessKeyHash.keys).new(*decodedAccessKeyHash.values)
48
+
49
+ now = Time.now().to_i()
50
+ jwt = {
51
+ aud: decodedAccessKey.audience,
52
+ iss: get_issuer(@custom_domain_url || "#{accountId}.api.authress.io", decodedAccessKey),
53
+ sub: decodedAccessKey.clientId,
54
+ client_id: decodedAccessKey.clientId,
55
+ iat: now,
56
+ # valid for 24 hours
57
+ exp: now + 60 * 60 * 24,
58
+ scope: 'openid'
59
+ }
60
+
61
+ if decodedAccessKey.privateKey.nil?
62
+ raise Exception("Invalid Service Client Access Key")
63
+ end
64
+
65
+ return decodedAccessKey.privateKey
66
+
67
+ # The Ed25519 module is broken right now and doesn't accept valid private keys.
68
+ # private_key = RbNaCl::Signatures::Ed25519::SigningKey.new(Base64.decode64(decodedAccessKey.privateKey)[0, 32])
69
+
70
+ # token = JWT.encode(jwt, private_key, 'ED25519', { typ: 'at+jwt', alg: 'EdDSA', kid: decodedAccessKey.keyId })
71
+ # @cachedKeyData = { token: token, expires: jwt['exp'] }
72
+ # return token
15
73
  end
16
74
  end
17
75
  end
76
+
77
+ module JWTExtensions
78
+ # Fixed because https://github.com/jwt/ruby-jwt/issues/334 is still broken
79
+ def encode_header
80
+ # https://github.com/jwt/ruby-jwt/blob/main/lib/jwt/encode.rb#L17
81
+ @headers["alg"] = @headers["alg"].downcase == "ed25519" ? "EdDSA" : @headers["alg"]
82
+ super
83
+ end
84
+ end
85
+
86
+ module JWT
87
+ class Encode
88
+ prepend JWTExtensions
89
+ end
90
+ end
@@ -1,13 +1,113 @@
1
1
  require 'base64'
2
2
  require 'uri'
3
3
  require 'json'
4
+ require 'jwt'
4
5
 
5
6
  module AuthressSdk
6
- class TokenValidationError < StandardError
7
- attr_reader :error_reason
8
- def initialize(msg)
9
- @error_reason = msg
10
- super(msg)
7
+ class TokenVerifier
8
+
9
+ attr_accessor :key_map
10
+
11
+ def initialize()
12
+ @key_map = {}
13
+ end
14
+
15
+ def verify_token(authressCustomDomain, token)
16
+ sanitized_domain = authressCustomDomain.gsub(/https?:\/\//, '')
17
+ completeIssuerUrl = "https://#{sanitized_domain}"
18
+ if token.nil?
19
+ raise TokenVerificationError.new("Unauthorized: No token specified")
20
+ end
21
+
22
+ begin
23
+ authenticationToken = token
24
+ unverifiedPayload = JWT.decode(authenticationToken, nil, false)
25
+ rescue JWT::DecodeError
26
+ begin
27
+ serviceClient = AuthressSdk::ServiceClientTokenProvider.new(token, completeIssuerUrl)
28
+ authenticationToken = serviceClient.get_token()
29
+ unverifiedPayload = JWT.decode(authenticationToken, nil, false)
30
+ rescue Exception => e
31
+ raise TokenVerificationError.new("Unauthorized: Invalid Token format: #{e}")
32
+ end
33
+ end
34
+
35
+ if unverifiedPayload.nil?
36
+ raise TokenVerificationError.new("Unauthorized: Invalid Token or Token not found")
37
+ end
38
+
39
+ kid = unverifiedPayload[1]["kid"]
40
+ if kid.nil?
41
+ raise TokenVerificationError.new("Unauthorized: No KID found in token")
42
+ end
43
+
44
+ issuer = unverifiedPayload[0]["iss"]
45
+ if issuer.nil?
46
+ raise TokenVerificationError.new("Unauthorized: No Issuer in token")
47
+ end
48
+
49
+ if (URI(issuer).host != URI(completeIssuerUrl).host)
50
+ raise TokenVerificationError.new("Unauthorized: Issuer does not match")
51
+ end
52
+
53
+ # Handle service client checking
54
+ issuerPath = URI(issuer).path
55
+ clientIdMatcher = /^\/v\d\/clients\/([^\/]+)$/.match(issuerPath)
56
+ if clientIdMatcher && clientIdMatcher[1] != unverifiedPayload[0]['sub']
57
+ raise TokenVerificationError.new("Unauthorized: Service ID does not match token sub claim")
58
+ end
59
+
60
+ jwkObject = get_public_key("#{issuer}/.well-known/openid-configuration/jwks", kid)
61
+ jwk = jwkObject.verify_key()
62
+
63
+ begin
64
+ # https://github.com/jwt/ruby-jwt?tab=readme-ov-file
65
+ decodedResult = JWT.decode(authenticationToken, jwk, true, { algorithm: 'EdDSA' })
66
+ return decodedResult[0]
67
+ rescue Exception => e
68
+ raise TokenVerificationError.new("Unauthorized: Token is invalid - #{e}")
69
+ end
70
+ end
71
+
72
+ def get_public_key(jwkKeyListUrl, kid)
73
+ hashKey = "#{jwkKeyListUrl}|#{kid}"
74
+
75
+ if @key_map[hashKey].nil?
76
+ @key_map[hashKey] = get_key_uncached(jwkKeyListUrl, kid)
77
+ end
78
+
79
+ begin
80
+ key = @key_map[hashKey]
81
+ return key
82
+ rescue
83
+ @key_map[hashKey] = get_key_uncached(jwkKeyListUrl, kid)
84
+ return @key_map[hashKey]
85
+ end
11
86
  end
87
+
88
+ def get_key_uncached(jwkKeyListUrl, kid)
89
+ response = Typhoeus::Request.new(jwkKeyListUrl.to_s, { :method => :get, :ssl_verifypeer => true, :ssl_verifyhost => 2, :verbose => false }).run
90
+ unless response.success?
91
+ raise TokenVerificationError.new("Unauthorized: Failed to fetch jwks from: #{jwkKeyListUrl}")
92
+ end
93
+
94
+ jwks = JWT::JWK::Set.new(JSON.parse(response.body))
95
+
96
+ key = jwks.find{|key| key[:kid] == kid }
97
+ if key
98
+ return key
99
+ end
100
+
101
+ raise TokenVerificationError.new("Unauthorized: KID was not found in the list of valid JWKs: #{kid}")
102
+ end
103
+
104
+ class TokenVerificationError < StandardError
105
+ attr_reader :error_reason
106
+ def initialize(msg)
107
+ @error_reason = msg
108
+ super(msg)
109
+ end
110
+ end
111
+
12
112
  end
13
113
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: authress-sdk
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.40.0
4
+ version: 2.0.43.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Authress
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-08-24 00:00:00.000000000 Z
11
+ date: 2024-05-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: typhoeus
@@ -60,6 +60,20 @@ dependencies:
60
60
  version: '0'
61
61
  - !ruby/object:Gem::Dependency
62
62
  name: jwt
63
+ requirement: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: '2.8'
68
+ type: :runtime
69
+ prerelease: false
70
+ version_requirements: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: '2.8'
75
+ - !ruby/object:Gem::Dependency
76
+ name: oauth2
63
77
  requirement: !ruby/object:Gem::Requirement
64
78
  requirements:
65
79
  - - ">="
@@ -73,7 +87,7 @@ dependencies:
73
87
  - !ruby/object:Gem::Version
74
88
  version: '0'
75
89
  - !ruby/object:Gem::Dependency
76
- name: oauth2
90
+ name: rbnacl
77
91
  requirement: !ruby/object:Gem::Requirement
78
92
  requirements:
79
93
  - - ">="