rodauth-oauth 0.0.2 → 0.0.3

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.
@@ -0,0 +1,111 @@
1
+ # frozen-string-literal: true
2
+
3
+ module Rodauth
4
+ Feature.define(:oauth_http_mac) do
5
+ unless String.method_defined?(:delete_prefix)
6
+ module PrefixExtensions
7
+ refine(String) do
8
+ def delete_suffix(suffix)
9
+ suffix = suffix.to_s
10
+ len = suffix.length
11
+ if len.positive? && index(suffix, -len)
12
+ self[0...-len]
13
+ else
14
+ dup
15
+ end
16
+ end
17
+
18
+ def delete_prefix(prefix)
19
+ prefix = prefix.to_s
20
+ if rindex(prefix, 0)
21
+ self[prefix.length..-1]
22
+ else
23
+ dup
24
+ end
25
+ end
26
+ end
27
+ end
28
+ using(PrefixExtensions)
29
+ end
30
+
31
+ depends :oauth
32
+
33
+ auth_value_method :oauth_token_type, "mac"
34
+ auth_value_method :oauth_mac_algorithm, "hmac-sha-256" # hmac-sha-256, hmac-sha-1
35
+ auth_value_method :oauth_tokens_mac_key_column, :mac_key
36
+
37
+ def authorization_token
38
+ return @authorization_token if defined?(@authorization_token)
39
+
40
+ @authorization_token = begin
41
+ value = request.get_header("HTTP_AUTHORIZATION").to_s
42
+
43
+ scheme, token = value.split(/ +/, 2)
44
+
45
+ return unless scheme == "MAC"
46
+
47
+ mac_attributes = parse_mac_authorization_header_props(token)
48
+
49
+ oauth_token = oauth_token_by_token(mac_attributes["id"])
50
+ .where(Sequel[oauth_tokens_expires_in_column] >= Sequel::CURRENT_TIMESTAMP)
51
+ .where(oauth_tokens_revoked_at_column => nil)
52
+ .first
53
+
54
+ return unless oauth_token && mac_signature_matches?(oauth_token, mac_attributes)
55
+
56
+ oauth_token
57
+
58
+ # TODO: set new MAC-KEY for the next request
59
+ end
60
+ end
61
+
62
+ private
63
+
64
+ def generate_oauth_token(params = {}, *args)
65
+ super({ oauth_tokens_mac_key_column => oauth_unique_id_generator }.merge(params), *args)
66
+ end
67
+
68
+ def json_access_token_payload(oauth_token)
69
+ payload = super
70
+
71
+ payload["mac_key"] = oauth_token[oauth_tokens_mac_key_column]
72
+ payload["mac_algorithm"] = oauth_mac_algorithm
73
+
74
+ payload
75
+ end
76
+
77
+ def mac_signature_matches?(oauth_token, mac_attributes)
78
+ nonce = mac_attributes["nonce"]
79
+ uri = URI(request.url)
80
+
81
+ request_signature = [
82
+ nonce,
83
+ request.request_method,
84
+ uri.request_uri,
85
+ uri.host,
86
+ uri.port
87
+ ].join("\n") + ("\n" * 3)
88
+
89
+ mac_algorithm = case oauth_mac_algorithm
90
+ when "hmac-sha-256"
91
+ OpenSSL::Digest::SHA256
92
+ when "hmac-sha-1"
93
+ OpenSSL::Digest::SHA1
94
+ else
95
+ raise ArgumentError, "Unsupported algorithm"
96
+ end
97
+
98
+ mac_signature = Base64.strict_encode64 \
99
+ OpenSSL::HMAC.digest(mac_algorithm.new, oauth_token[oauth_tokens_mac_key_column], request_signature)
100
+
101
+ mac_signature == mac_attributes["mac"]
102
+ end
103
+
104
+ def parse_mac_authorization_header_props(token)
105
+ @mac_authorization_header_props = token.split(/ *, */).each_with_object({}) do |prop, props|
106
+ field, value = prop.split(/ *= */, 2)
107
+ props[field] = value.delete_prefix("\"").delete_suffix("\"")
108
+ end
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,228 @@
1
+ # frozen-string-literal: true
2
+
3
+ module Rodauth
4
+ Feature.define(:oauth_jwt) do
5
+ depends :oauth
6
+
7
+ auth_value_method :oauth_jwt_token_issuer, "Example"
8
+
9
+ auth_value_method :oauth_jwt_key, nil
10
+ auth_value_method :oauth_jwt_public_key, nil
11
+ auth_value_method :oauth_jwt_algorithm, "HS256"
12
+
13
+ auth_value_method :oauth_jwt_jwk_key, nil
14
+ auth_value_method :oauth_jwt_jwk_public_key, nil
15
+ auth_value_method :oauth_jwt_jwk_algorithm, "RS256"
16
+
17
+ auth_value_method :oauth_jwt_jwe_key, nil
18
+ auth_value_method :oauth_jwt_jwe_public_key, nil
19
+ auth_value_method :oauth_jwt_jwe_algorithm, nil
20
+ auth_value_method :oauth_jwt_jwe_encryption_method, nil
21
+
22
+ auth_value_method :oauth_jwt_jwe_copyright, nil
23
+ auth_value_method :oauth_jwt_audience, nil
24
+
25
+ auth_value_methods(
26
+ :generate_jti,
27
+ :jwt_encode,
28
+ :jwt_decode
29
+ )
30
+
31
+ def require_oauth_authorization(*scopes)
32
+ authorization_required unless authorization_token
33
+
34
+ scopes << oauth_application_default_scope if scopes.empty?
35
+
36
+ token_scopes = authorization_token["scopes"].split(",")
37
+
38
+ authorization_required unless scopes.any? { |scope| token_scopes.include?(scope) }
39
+ end
40
+
41
+ private
42
+
43
+ def authorization_token
44
+ return @authorization_token if defined?(@authorization_token)
45
+
46
+ @authorization_token = begin
47
+ value = request.get_header("HTTP_AUTHORIZATION").to_s
48
+
49
+ scheme, token = value.split(/ +/, 2)
50
+
51
+ return unless scheme == "Bearer"
52
+
53
+ jwt_decode(token)
54
+ end
55
+ end
56
+
57
+ def generate_oauth_token(params = {}, should_generate_refresh_token = true)
58
+ create_params = {
59
+ oauth_grants_expires_in_column => Time.now + oauth_token_expires_in
60
+ }.merge(params)
61
+
62
+ if should_generate_refresh_token
63
+ refresh_token = oauth_unique_id_generator
64
+
65
+ if oauth_tokens_refresh_token_hash_column
66
+ create_params[oauth_tokens_refresh_token_hash_column] = generate_token_hash(refresh_token)
67
+ else
68
+ create_params[oauth_tokens_refresh_token_column] = refresh_token
69
+ end
70
+ end
71
+
72
+ oauth_token = _generate_oauth_token(create_params)
73
+
74
+ issued_at = Time.current.utc.to_i
75
+
76
+ payload = {
77
+ sub: oauth_token[oauth_tokens_account_id_column],
78
+ iss: oauth_jwt_token_issuer, # issuer
79
+ iat: issued_at, # issued at
80
+ #
81
+ # sub REQUIRED - as defined in section 4.1.2 of [RFC7519]. In case of
82
+ # access tokens obtained through grants where a resource owner is
83
+ # involved, such as the authorization code grant, the value of "sub"
84
+ # SHOULD correspond to the subject identifier of the resource owner.
85
+ # In case of access tokens obtained through grants where no resource
86
+ # owner is involved, such as the client credentials grant, the value
87
+ # of "sub" SHOULD correspond to an identifier the authorization
88
+ # server uses to indicate the client application.
89
+ client_id: oauth_token[oauth_tokens_oauth_application_id_column],
90
+
91
+ exp: issued_at + oauth_token_expires_in,
92
+ aud: oauth_jwt_audience,
93
+
94
+ # one of the points of using jwt is avoiding database lookups, so we put here all relevant
95
+ # token data.
96
+ scopes: oauth_token[oauth_tokens_scopes_column]
97
+ }
98
+
99
+ token = jwt_encode(payload)
100
+
101
+ oauth_token[oauth_tokens_token_column] = token
102
+ oauth_token
103
+ end
104
+
105
+ def _jwt_key
106
+ @_jwt_key ||= oauth_jwt_key || oauth_application[oauth_applications_client_secret_column]
107
+ end
108
+
109
+ if defined?(JSON::JWT)
110
+ # :nocov:
111
+
112
+ # json-jwt
113
+ def jwt_encode(payload)
114
+ jwt = JSON::JWT.new(payload)
115
+
116
+ jwt = if oauth_jwt_jwk_key
117
+ jwk = JSON::JWK.new(oauth_jwt_jwk_key)
118
+ jwt.kid = jwk.thumbprint
119
+ jwt.sign(oauth_jwt_jwk_key, oauth_jwt_jwk_algorithm)
120
+ else
121
+ jwt.sign(_jwt_key, oauth_jwt_algorithm)
122
+ end
123
+ if oauth_jwt_jwe_key
124
+ algorithm = oauth_jwt_jwe_algorithm.to_sym if oauth_jwt_jwe_algorithm
125
+ jwt = jwt.encrypt(oauth_jwt_jwe_public_key || oauth_jwt_jwe_key,
126
+ algorithm,
127
+ oauth_jwt_jwe_encryption_method.to_sym)
128
+ end
129
+ jwt.to_s
130
+ end
131
+
132
+ def jwt_decode(token)
133
+ token = JSON::JWT.decode(token, oauth_jwt_jwe_key).plain_text if oauth_jwt_jwe_key
134
+ if oauth_jwt_jwk_key
135
+ jwk = JSON::JWK.new(oauth_jwt_jwk_key)
136
+ JSON::JWT.decode(token, jwk)
137
+ else
138
+ JSON::JWT.decode(token, oauth_jwt_public_key || _jwt_key)
139
+ end
140
+ rescue JSON::JWT::Exception
141
+ nil
142
+ end
143
+ # :nocov:
144
+ elsif defined?(JWT)
145
+
146
+ # ruby-jwt
147
+
148
+ def jwt_encode(payload)
149
+ headers = {}
150
+
151
+ key, algorithm = if oauth_jwt_jwk_key
152
+ jwk_key = JWT::JWK.new(oauth_jwt_jwk_key)
153
+ # JWK
154
+ # Currently only supports RSA public keys.
155
+ headers[:kid] = jwk_key.kid
156
+
157
+ [jwk_key.keypair, oauth_jwt_jwk_algorithm]
158
+ else
159
+ # JWS
160
+
161
+ [_jwt_key, oauth_jwt_algorithm]
162
+ end
163
+
164
+ # Use the key and iat to create a unique key per request to prevent replay attacks
165
+ jti_raw = [key, payload[:iat]].join(":").to_s
166
+ jti = Digest::MD5.hexdigest(jti_raw)
167
+
168
+ # @see JWT reserved claims - https://tools.ietf.org/html/draft-jones-json-web-token-07#page-7
169
+ payload[:jti] = jti
170
+ token = JWT.encode(payload, key, algorithm, headers)
171
+
172
+ if oauth_jwt_jwe_key
173
+ params = {
174
+ zip: "DEF",
175
+ copyright: oauth_jwt_jwe_copyright
176
+ }
177
+ params[:enc] = oauth_jwt_jwe_encryption_method if oauth_jwt_jwe_encryption_method
178
+ params[:alg] = oauth_jwt_jwe_algorithm if oauth_jwt_jwe_algorithm
179
+ token = JWE.encrypt(token, oauth_jwt_jwe_public_key || oauth_jwt_jwe_key, **params)
180
+ end
181
+
182
+ token
183
+ end
184
+
185
+ def jwt_decode(token)
186
+ # decrypt jwe
187
+ token = JWE.decrypt(token, oauth_jwt_jwe_key) if oauth_jwt_jwe_key
188
+
189
+ # decode jwt
190
+ headers = { algorithms: [oauth_jwt_algorithm] }
191
+
192
+ key = if oauth_jwt_jwk_key
193
+ jwk_key = JWT::JWK.new(oauth_jwt_jwk_key)
194
+ # JWK
195
+ # The jwk loader would fetch the set of JWKs from a trusted source
196
+ jwk_loader = lambda do |options|
197
+ @cached_keys = nil if options[:invalidate] # need to reload the keys
198
+ @cached_keys ||= { keys: [jwk_key.export] }
199
+ end
200
+
201
+ headers[:algorithms] = [oauth_jwt_jwk_algorithm]
202
+ headers[:jwks] = jwk_loader
203
+
204
+ nil
205
+ else
206
+ # JWS
207
+ # worst case scenario, the key is the application key
208
+ oauth_jwt_public_key || _jwt_key
209
+ end
210
+ token, = JWT.decode(token, key, true, headers)
211
+ token
212
+ rescue JWT::DecodeError
213
+ nil
214
+ end
215
+
216
+ else
217
+ # :nocov:
218
+ def jwt_encode(_token)
219
+ raise "#{__method__} is undefined, redefine it or require either \"jwt\" or \"json-jwt\""
220
+ end
221
+
222
+ def jwt_decode(_token)
223
+ raise "#{__method__} is undefined, redefine it or require either \"jwt\" or \"json-jwt\""
224
+ end
225
+ # :nocov:
226
+ end
227
+ end
228
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Rodauth
4
4
  module OAuth
5
- VERSION = "0.0.2"
5
+ VERSION = "0.0.3"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rodauth-oauth
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.2
4
+ version: 0.0.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tiago Cardoso
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-05-29 00:00:00.000000000 Z
11
+ date: 2020-06-05 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: Implementation of the OAuth 2.0 protocol on top of rodauth.
14
14
  email:
@@ -16,10 +16,12 @@ email:
16
16
  executables: []
17
17
  extensions: []
18
18
  extra_rdoc_files:
19
+ - LICENSE.txt
19
20
  - README.md
20
21
  - CHANGELOG.md
21
22
  files:
22
23
  - CHANGELOG.md
24
+ - LICENSE.txt
23
25
  - README.md
24
26
  - lib/generators/roda/oauth/install_generator.rb
25
27
  - lib/generators/roda/oauth/templates/app/models/oauth_application.rb
@@ -28,6 +30,8 @@ files:
28
30
  - lib/generators/roda/oauth/templates/db/migrate/create_rodauth_oauth.rb
29
31
  - lib/generators/roda/oauth/views_generator.rb
30
32
  - lib/rodauth/features/oauth.rb
33
+ - lib/rodauth/features/oauth_http_mac.rb
34
+ - lib/rodauth/features/oauth_jwt.rb
31
35
  - lib/rodauth/oauth.rb
32
36
  - lib/rodauth/oauth/railtie.rb
33
37
  - lib/rodauth/oauth/version.rb