rodauth-oauth 0.0.2 → 0.0.3

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