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 +4 -4
- data/README.md +7 -2
- data/lib/authress-sdk/authress_client.rb +11 -0
- data/lib/authress-sdk/omniauth.rb +1 -1
- data/lib/authress-sdk/service_client_token_provider.rb +77 -4
- data/lib/authress-sdk/token_validator.rb +105 -5
- metadata +17 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8d32ad65dcb072b5d8a7365e5859fc3342ae2425945380781707650a19f9926c
|
4
|
+
data.tar.gz: f81db7d0bc6e7fd9c5d1217f6cd87d02a3ce8dffd8e8e69e0ae95109ab33944c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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::
|
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 '
|
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
|
-
|
14
|
-
|
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
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
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.
|
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:
|
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:
|
90
|
+
name: rbnacl
|
77
91
|
requirement: !ruby/object:Gem::Requirement
|
78
92
|
requirements:
|
79
93
|
- - ">="
|