authress-sdk 2.0.40.0 → 2.0.43.0

Sign up to get free protection for your applications and to get access to all the features.
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
  - - ">="