authress-sdk 2.0.41.0 → 2.0.45.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +2 -1
- 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 +82 -4
- data/lib/authress-sdk/token_validator.rb +105 -5
- metadata +31 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: af182231afad4b4bc13d5953f3a9bfc0cee00bb69d78e9a6b431284d57692ae3
|
4
|
+
data.tar.gz: 66117157125df99898877a4c4cfee34650199ce6354d1fec711517b8c60bee4a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a6dd701c47674e16e43c77919d8a599bec4874505bde3592128889152ba324c80f5a2c8db05a8df4ddf4291ec5c376f516457adad104ac7caefb466b2be56649
|
7
|
+
data.tar.gz: ee0feaafaa7015383e2068a05657d2e0045acb169577dc6274d1d749b88683bfd97ce399cd298395ada9623670eebd85090e90cd8635bc2b8259313e65ec8403
|
data/README.md
CHANGED
@@ -78,7 +78,7 @@ end
|
|
78
78
|
|
79
79
|
# on api route
|
80
80
|
[route('/resources/<resourceId>')]
|
81
|
-
def getResource(resourceId)
|
81
|
+
def getResource(resourceId)
|
82
82
|
# Check Authress to authorize the user
|
83
83
|
user_identity = AuthressSdk::AuthressClient.verify_token(request.headers.get('authorization'))
|
84
84
|
|
@@ -86,6 +86,7 @@ def getResource(resourceId) {
|
|
86
86
|
user_id = user_identity.sub
|
87
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.
|
88
88
|
permission = 'READ' # String | Permission to check, '*' and scoped permissions can also be checked here.
|
89
|
+
|
89
90
|
begin
|
90
91
|
# Check to see if a user has permissions to a resource.
|
91
92
|
api_instance = AuthressSdk::UserPermissionsApi.new
|
@@ -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,95 @@
|
|
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
|
+
priv_pem = <<~EOF
|
66
|
+
-----BEGIN PRIVATE KEY-----
|
67
|
+
#{decodedAccessKey.privateKey}
|
68
|
+
-----END PRIVATE KEY-----
|
69
|
+
EOF
|
70
|
+
|
71
|
+
privateKey = OpenSSL::PKey.read(priv_pem)
|
72
|
+
result = Base64.encode64(privateKey.raw_private_key).tr('+/', '-_').delete('=')
|
73
|
+
private_key = RbNaCl::Signatures::Ed25519::SigningKey.new(Base64.decode64(result))
|
74
|
+
|
75
|
+
token = JWT.encode(jwt, private_key, 'ED25519', { typ: 'at+jwt', alg: 'EdDSA', kid: decodedAccessKey.keyId })
|
76
|
+
@cachedKeyData = { token: token, expires: jwt['exp'] }
|
77
|
+
return token
|
15
78
|
end
|
16
79
|
end
|
17
80
|
end
|
81
|
+
|
82
|
+
module JWTExtensions
|
83
|
+
# Fixed because https://github.com/jwt/ruby-jwt/issues/334 is still broken
|
84
|
+
def encode_header
|
85
|
+
# https://github.com/jwt/ruby-jwt/blob/main/lib/jwt/encode.rb#L17
|
86
|
+
@headers["alg"] = @headers["alg"].downcase == "ed25519" ? "EdDSA" : @headers["alg"]
|
87
|
+
super
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
module JWT
|
92
|
+
class Encode
|
93
|
+
prepend JWTExtensions
|
94
|
+
end
|
95
|
+
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.45.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Authress
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-
|
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,21 @@ dependencies:
|
|
73
87
|
- !ruby/object:Gem::Version
|
74
88
|
version: '0'
|
75
89
|
- !ruby/object:Gem::Dependency
|
76
|
-
name:
|
90
|
+
name: rbnacl
|
91
|
+
requirement: !ruby/object:Gem::Requirement
|
92
|
+
requirements:
|
93
|
+
- - ">="
|
94
|
+
- !ruby/object:Gem::Version
|
95
|
+
version: '0'
|
96
|
+
type: :runtime
|
97
|
+
prerelease: false
|
98
|
+
version_requirements: !ruby/object:Gem::Requirement
|
99
|
+
requirements:
|
100
|
+
- - ">="
|
101
|
+
- !ruby/object:Gem::Version
|
102
|
+
version: '0'
|
103
|
+
- !ruby/object:Gem::Dependency
|
104
|
+
name: openssl
|
77
105
|
requirement: !ruby/object:Gem::Requirement
|
78
106
|
requirements:
|
79
107
|
- - ">="
|