rodauth-oauth 0.0.6 → 0.1.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 73f73e4143c1c646b3a6f2a5e87fb925f38f18e157366d7522328b5cbcd2fbb7
4
- data.tar.gz: bce8a4532e365328bb46197e72b05b78beca19b00587630f234cff0c8d51bc35
3
+ metadata.gz: 3eac600d006a2c78509f608575db062b7ba6d67356b890c7d38414b9b82875f9
4
+ data.tar.gz: c37fc18c093f546023481a88cc526c5a0b721b1a3bfeac827c21184e0583071b
5
5
  SHA512:
6
- metadata.gz: 46053f71e35baad7b3c217bfbca0a259d07fd6909713805db23ff92c0503c0907903483e659fe988af774b26a87b274f8b72006983b1acc191bccb7d00919a86
7
- data.tar.gz: 35305a71ea2b4035933d93e3fa5a3e9b388f32b2301ff05b87a86af45defa494cefc4d6d9adb823822c82acb995e57794fb710b1935aaab10430c1898d1a55b0
6
+ metadata.gz: 0a04fdb5ab370ed5736208cbd4ccb1e6da801af52cd68004625a21008c4a10a04bc143d99c9e1a71bccb9fad882fc3cff27d9c0900689dbd5cf6c0616e4d43a0
7
+ data.tar.gz: e6d5cb6e8ff31d64eb588fa39ad6a1e7bb1ae9416adac64e5f9a21bf451ffd4115a8c2d3bb8759f1a32192a88a4a401336e1baca7621a33934dc1b2a873c8402
@@ -2,8 +2,58 @@
2
2
 
3
3
  ## master
4
4
 
5
+ ### 0.1.0
6
+
7
+ (31/7/2020)
8
+
9
+ #### Features
10
+
11
+ ##### OpenID
12
+
13
+ `rodauth-oauth` now ships with support for [OpenID Connect](https://openid.net/connect/). In order to enable, you have to:
14
+
15
+ ```ruby
16
+ plugin :rodauth do
17
+ enable :oidc
18
+ end
19
+ ```
20
+
21
+ For more info about integrating it, [check the wiki](https://gitlab.com/honeyryderchuck/rodauth-oauth/-/wikis/home#openid-connect-since-v01).
22
+
23
+ It supports omniauth openID integrations out-of-the-box, [check the OpenID example, which integrates with omniauth_openid_connect](https://gitlab.com/honeyryderchuck/rodauth-oauth/-/tree/master/examples).
24
+
25
+ #### Improvements
26
+
27
+ * JWT: `sub` claim now also handles "pairwise" subjects. For that, you have to set the `oauth_jwt_subject_type` option (`"public"` or `"pairwise"`) and `oauth_jwt_subject_secret` (will be used for salting the `sub` when the type is `"pairwise"`).
28
+ * JWT: `auth_time` claim is now supported; if your application uses the `rodauth` feature `:account_expiration`, it'll use the `last_account_login_at` method, otherwise you can set the `last_account_login_at` option:
29
+
30
+ ```ruby
31
+ last_account_login_at do
32
+ convert_timestamp(db[accounts_table].where(account_id_column => account_id).get(:that_column_where_you_keep_the_data))
33
+ end
34
+ ```
35
+ * JWT: `iss` claim now defaults to `authorization_server_url` when not defined;
36
+ * JWT: `aud` claim now defaults to the token application's client ID (`client_id` claim was removed as a result);
37
+
38
+
39
+
40
+ #### Breaking Changes
41
+
42
+ `rodauth-oauth` URLs no longer have the `oauth-` prefix, so make sure you update your integrations accordingly, i.e. where you used to rely on `/oauth-authorize`, you'll have to use `/authorize`.
43
+
44
+ URI schemes for client applications redirect URIs have to be `https`. In order to override this, set the `oauth_valid_uri_schemes` to an array of your expected URI schemes.
45
+
46
+
47
+ #### Bugfixes
48
+
49
+ * Authorization request submission can receive the `scope` as an array of values now, instead of only dealing with receiving a white-space separated list.
50
+ * fixed trailing "/" in the "issuer" value in server metadata (`https://server.com/` -> `https://server.com`).
51
+
52
+
5
53
  ### 0.0.6
6
54
 
55
+ (6/7/2020)
56
+
7
57
  #### Features
8
58
 
9
59
  The `oauth_jwt` feature now supports JWT Secured Authorization Request (JAR) (see https://tools.ietf.org/html/draft-ietf-oauth-jwsreq-20). This means that client applications can send the authorization parameters inside a signed JWT. The client applications keeps the private key, while the authorization server **must** store a public key for the client application. For encrypted JWTs, the client application should use one of the public encryption keys exposed in the JWKs URI, to encrypt the JWT. Remember, **tokens must be signed then encrypted** (or just signed).
@@ -25,7 +75,9 @@ The `oauth_jwt` feature now supports JWT Secured Authorization Request (JAR) (se
25
75
  Removed React Javascript from example applications.
26
76
 
27
77
 
28
- ### 0.0.5 (26/6/2020)
78
+ ### 0.0.5
79
+
80
+ (26/6/2020)
29
81
 
30
82
  #### Features
31
83
 
@@ -62,7 +114,9 @@ It **requires** the authorization to implement the server metadata endpoint (`/.
62
114
  * option `scopes_param` renamed to `scope_param`;
63
115
  *
64
116
 
65
- ## 0.0.4 (13/6/2020)
117
+ ## 0.0.4
118
+
119
+ (13/6/2020)
66
120
 
67
121
  ### Features
68
122
 
@@ -99,7 +153,9 @@ The `oauth_jwt` feature now allows the usage of access tokens to authorize the g
99
153
 
100
154
  * Fixed scope claim of JWT ("scopes" -> "scope");
101
155
 
102
- ## 0.0.3 (5/6/2020)
156
+ ## 0.0.3
157
+
158
+ (5/6/2020)
103
159
 
104
160
  ### Features
105
161
 
@@ -131,7 +187,9 @@ end
131
187
  * renamed the existing `use_oauth_implicit_grant_type` to `use_oauth_implicit_grant_type?`;
132
188
  * It's now usable as JSON API (small caveat: POST authorize will still redirect on success...);
133
189
 
134
- ## 0.0.2 (29/5/2020)
190
+ ## 0.0.2
191
+
192
+ (29/5/2020)
135
193
 
136
194
  ### Features
137
195
 
@@ -147,6 +205,8 @@ end
147
205
 
148
206
  * usage of client secret for authorizing the generation of tokens, as the spec mandates (and refraining from them when doing PKCE).
149
207
 
150
- ## 0.0.1 (14/5/2020)
208
+ ## 0.0.1
209
+
210
+ (14/5/2020)
151
211
 
152
212
  Initial implementation of the Oauth 2.0 framework, with an example app done using roda.
data/README.md CHANGED
@@ -1,13 +1,13 @@
1
1
  # Rodauth::Oauth
2
2
 
3
- [![pipeline status](https://gitlab.com/honeyryderchuck/rodauth-oauth/badges/master/pipeline.svg)](https://gitlab.com/honeyryderchuck/rodauth-oauth/-/commits/master)
4
- [![coverage report](https://gitlab.com/honeyryderchuck/rodauth-oauth/badges/master/coverage.svg)](https://gitlab.com/honeyryderchuck/rodauth-oauth/-/commits/master)
3
+ [![pipeline status](https://gitlab.com/honeyryderchuck/rodauth-oauth/badges/master/pipeline.svg)](https://gitlab.com/honeyryderchuck/rodauth-oauth/-/pipelines?page=1&ref=master)
4
+ [![coverage report](https://gitlab.com/honeyryderchuck/rodauth-oauth/badges/master/coverage.svg)](https://honeyryderchuck.gitlab.io/rodauth-oauth/coverage/#_AllFiles)
5
5
 
6
6
  This is an extension to the `rodauth` gem which implements the [OAuth 2.0 framework](https://tools.ietf.org/html/rfc6749) for an authorization server.
7
7
 
8
8
  ## Features
9
9
 
10
- This gem implements:
10
+ This gem implements the following RFCs and features of OAuth:
11
11
 
12
12
  * [The OAuth 2.0 protocol framework](https://tools.ietf.org/html/rfc6749):
13
13
  * [Authorization grant flow](https://tools.ietf.org/html/rfc6749#section-1.3);
@@ -24,6 +24,8 @@ This gem implements:
24
24
  * [JWT Secured Authorization Requests](https://tools.ietf.org/html/draft-ietf-oauth-jwsreq-20);
25
25
  * OAuth application and token management dashboards;
26
26
 
27
+ It also implements the [OpenID Connect layer](https://openid.net/connect/) on top of the OAuth features it provides.
28
+
27
29
  This gem supports also rails (through [rodauth-rails]((https://github.com/janko/rodauth-rails))).
28
30
 
29
31
 
@@ -43,6 +45,15 @@ Or install it yourself as:
43
45
 
44
46
  $ gem install rodauth-oauth
45
47
 
48
+
49
+ ## Resources
50
+ | | |
51
+ | ------------- | ----------------------------------------------------------- |
52
+ | Website | https://honeyryderchuck.gitlab.io/rodauth-oauth/ |
53
+ | Documentation | https://honeyryderchuck.gitlab.io/rodauth-oauth/rdoc/ |
54
+ | Wiki | https://gitlab.com/honeyryderchuck/rodauth-oauth/wikis/home |
55
+ | CI | https://gitlab.com/honeyryderchuck/rodauth-oauth/pipelines |
56
+
46
57
  ## Usage
47
58
 
48
59
  This tutorial assumes you already read the documentation and know how to set up `rodauth`. After that, integrating `roda-auth` will look like:
@@ -86,7 +97,18 @@ route do |r|
86
97
  end
87
98
  ```
88
99
 
89
- You'll have to do a bit more boilerplate, so here's the instructions.
100
+
101
+ For OpenID, it's very similar to the example above:
102
+
103
+ ```ruby
104
+ plugin :rodauth do
105
+ # enable it in the plugin
106
+ enable :login, :openid
107
+ oauth_application_default_scope %w[openid]
108
+ oauth_application_scopes %w[openid email profile]
109
+ end
110
+ ```
111
+
90
112
 
91
113
  ### Example (TL;DR)
92
114
 
@@ -101,7 +123,7 @@ Generating tokens happens mostly server-to-server, so here's an example using:
101
123
 
102
124
  ```ruby
103
125
  require "httpx"
104
- response = HTTPX.post("https://auth_server/oauth-token",json: {
126
+ response = HTTPX.post("https://auth_server/token",json: {
105
127
  client_id: ENV["OAUTH_CLIENT_ID"],
106
128
  client_secret: ENV["OAUTH_CLIENT_SECRET"],
107
129
  grant_type: "authorization_code",
@@ -115,7 +137,7 @@ puts payload #=> {"access_token" => "awr23f3h8f9d2h89...", "refresh_token" => "2
115
137
  ##### cURL
116
138
 
117
139
  ```
118
- > curl --data '{"client_id":"$OAUTH_CLIENT_ID","client_secret":"$OAUTH_CLIENT_SECRET","grant_type":"authorization_code","code":"oiweicnewdh32fhoi3hf3ihfo2ih3f2o3as"}' https://auth_server/oauth-token
140
+ > curl --data '{"client_id":"$OAUTH_CLIENT_ID","client_secret":"$OAUTH_CLIENT_SECRET","grant_type":"authorization_code","code":"oiweicnewdh32fhoi3hf3ihfo2ih3f2o3as"}' https://auth_server/token
119
141
  ```
120
142
 
121
143
  #### Refresh Token
@@ -126,7 +148,7 @@ Refreshing expired tokens also happens mostly server-to-server, here's an exampl
126
148
 
127
149
  ```ruby
128
150
  require "httpx"
129
- response = HTTPX.post("https://auth_server/oauth-token",json: {
151
+ response = HTTPX.post("https://auth_server/token",json: {
130
152
  client_id: ENV["OAUTH_CLIENT_ID"],
131
153
  client_secret: ENV["OAUTH_CLIENT_SECRET"],
132
154
  grant_type: "refresh_token",
@@ -140,7 +162,7 @@ puts payload #=> {"access_token" => "awr23f3h8f9d2h89...", "token_type" => "Bear
140
162
  ##### cURL
141
163
 
142
164
  ```
143
- > curl -H "X-your-auth-scheme: $SERVER_KEY" --data '{"client_id":"$OAUTH_CLIENT_ID","client_secret":"$OAUTH_CLIENT_SECRET","grant_type":"token","token":"2r89hfef4j9f90d2j2390jf390g"}' https://auth_server/oauth-token
165
+ > curl -H "X-your-auth-scheme: $SERVER_KEY" --data '{"client_id":"$OAUTH_CLIENT_ID","client_secret":"$OAUTH_CLIENT_SECRET","grant_type":"token","token":"2r89hfef4j9f90d2j2390jf390g"}' https://auth_server/token
144
166
  ```
145
167
 
146
168
  #### Revoking tokens
@@ -151,7 +173,7 @@ Token revocation can be done both by the idenntity owner or the application owne
151
173
  require "httpx"
152
174
  httpx = HTTPX.plugin(:basic_authorization)
153
175
  response = httpx.basic_authentication(ENV["CLIENT_ID"], ENV["CLIENT_SECRET"])
154
- .post("https://auth_server/oauth-revoke",json: {
176
+ .post("https://auth_server/revoke",json: {
155
177
  token_type_hint: "access_token", # can also be "refresh:tokn"
156
178
  token: "2r89hfef4j9f90d2j2390jf390g"
157
179
  })
@@ -163,7 +185,7 @@ puts payload #=> {"access_token" => "awr23f3h8f9d2h89...", "token_type" => "Bear
163
185
  ##### cURL
164
186
 
165
187
  ```
166
- > curl -H "X-your-auth-scheme: $SERVER_KEY" --data '{"client_id":"$OAUTH_CLIENT_ID","token_type_hint":"access_token","token":"2r89hfef4j9f90d2j2390jf390g"}' https://auth_server/oauth-revoke
188
+ > curl -H "X-your-auth-scheme: $SERVER_KEY" --data '{"client_id":"$OAUTH_CLIENT_ID","token_type_hint":"access_token","token":"2r89hfef4j9f90d2j2390jf390g"}' https://auth_server/revoke
167
189
  ```
168
190
 
169
191
  #### Token introspection
@@ -174,7 +196,7 @@ Token revocation can be used to determine the state of a token (whether active,
174
196
  require "httpx"
175
197
  httpx = HTTPX.plugin(:basic_authorization)
176
198
  response = httpx.basic_authentication(ENV["CLIENT_ID"], ENV["CLIENT_SECRET"])
177
- .post("https://auth_server/oauth-introspect",json: {
199
+ .post("https://auth_server/introspect",json: {
178
200
  token_type_hint: "access_token", # can also be "refresh:tokn"
179
201
  token: "2r89hfef4j9f90d2j2390jf390g"
180
202
  })
@@ -186,7 +208,7 @@ puts payload #=> {"active" => true, "scope" => "read write" ....
186
208
  ##### cURL
187
209
 
188
210
  ```
189
- > curl -H "X-your-auth-scheme: $SERVER_KEY" --data '{"client_id":"$OAUTH_CLIENT_ID","token_type_hint":"access_token","token":"2r89hfef4j9f90d2j2390jf390g"}' https://auth_server/oauth-revoke
211
+ > curl -H "X-your-auth-scheme: $SERVER_KEY" --data '{"client_id":"$OAUTH_CLIENT_ID","token_type_hint":"access_token","token":"2r89hfef4j9f90d2j2390jf390g"}' https://auth_server/revoke
190
212
  ```
191
213
 
192
214
  ### Authorization Server Metadata
@@ -243,10 +265,10 @@ The rodauth default setup expects the roda `render` plugin to be activated; by d
243
265
 
244
266
  Once you set it up, by default, the following endpoints will be available:
245
267
 
246
- * `GET /oauth-authorize`: Loads the OAuth authorization HTML form;
247
- * `POST /oauth-authorize`: Responds to an OAuth authorization request, as [per the spec](https://tools.ietf.org/html/rfc6749#section-4);
248
- * `POST /oauth-token`: Generates OAuth tokens as [per the spec](https://tools.ietf.org/html/rfc6749#section-4.4.2);
249
- * `POST /oauth-revoke`: Revokes OAuth tokens as [per the spec](https://tools.ietf.org/html/rfc7009);
268
+ * `GET /authorize`: Loads the OAuth authorization HTML form;
269
+ * `POST /authorize`: Responds to an OAuth authorization request, as [per the spec](https://tools.ietf.org/html/rfc6749#section-4);
270
+ * `POST /token`: Generates OAuth tokens as [per the spec](https://tools.ietf.org/html/rfc6749#section-4.4.2);
271
+ * `POST /revoke`: Revokes OAuth tokens as [per the spec](https://tools.ietf.org/html/rfc7009);
250
272
 
251
273
  ### OAuth applications
252
274
 
@@ -426,7 +448,7 @@ The "Proof Key for Code Exchange by OAuth Public Clients" (aka PKCE) flow, which
426
448
  ```ruby
427
449
  # with httpx
428
450
  require "httpx"
429
- response = HTTPX.post("https://auth_server/oauth-token",json: {
451
+ response = HTTPX.post("https://auth_server/token",json: {
430
452
  client_id: ENV["OAUTH_CLIENT_ID"],
431
453
  grant_type: "authorization_code",
432
454
  code: "oiweicnewdh32fhoi3hf3ihfo2ih3f2o3as",
@@ -477,7 +499,7 @@ Generating an access token will deliver the following fields:
477
499
  ```ruby
478
500
  # with httpx
479
501
  require "httpx"
480
- response = httpx.post("https://auth_server/oauth-token",json: {
502
+ response = httpx.post("https://auth_server/token",json: {
481
503
  client_id: env["oauth_client_id"],
482
504
  client_secret: env["oauth_client_secret"],
483
505
  grant_type: "authorization_code",
@@ -576,7 +598,7 @@ which adds an extra layer of protection.
576
598
 
577
599
  #### JWKS URI
578
600
 
579
- A route is defined for getting the JWK Set in a JSON format; this is typically used by client applications, who need the JWK set to decode the JWT token. This URL is typically `https://oauth-server/oauth-jwks`.
601
+ A route is defined for getting the JWK Set in a JSON format; this is typically used by client applications, who need the JWK set to decode the JWT token. This URL is typically `https://oauth-server/jwks`.
580
602
 
581
603
  #### JWT Bearer as authorization grant
582
604
 
@@ -585,7 +607,7 @@ One can emit a new access token by using the bearer access token as grant. This
585
607
  ```ruby
586
608
  # with httpx
587
609
  require "httpx"
588
- response = httpx.post("https://auth_server/oauth-token",json: {
610
+ response = httpx.post("https://auth_server/token",json: {
589
611
  grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer",
590
612
  assertion: "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOjEsImlzcyI6IkV4YW1wbGUiLCJpYXQiOjE1OTIwMDk1MDEsImNsaWVudF9pZCI6IkNMSUVOVF9JRCIsImV4cCI6MTU5MjAxMzEwMSwiYXVkIjpudWxsLCJzY29wZSI6InVzZXIucmVhZCB1c2VyLndyaXRlIiwianRpIjoiOGM1NTVjMjdiOWRjNDdmOTcyNWRkYzBhMjk0NzA1ZTA4NzFkY2JlN2Q5ZTNlMmVkNGE1ZTBiOGZlNTZlYzcxMSJ9.AlxKRtE3ec0mtyBSDx4VseND4eC6cH5ubtv8gfYxxsc"
591
613
  })
@@ -29,7 +29,8 @@ class CreateRodauthOAuth < ActiveRecord::Migration<%= migration_version %>
29
29
  # uncomment to enable PKCE
30
30
  # t.string :code_challenge
31
31
  # t.string :code_challenge_method
32
-
32
+ # uncomment to use OIDC nonce
33
+ # t.string :nonce
33
34
  t.index(%i[oauth_application_id code], unique: true)
34
35
  end
35
36
 
@@ -54,6 +55,8 @@ class CreateRodauthOAuth < ActiveRecord::Migration<%= migration_version %>
54
55
  t.datetime :revoked_at
55
56
  t.string :scopes, null: false
56
57
  t.datetime :created_at, null: false, default: -> { "CURRENT_TIMESTAMP" }
58
+ # uncomment to use OIDC nonce
59
+ # t.string :nonce
57
60
  end
58
61
  end
59
62
  end
@@ -43,7 +43,6 @@ module Rodauth
43
43
 
44
44
  before "authorize"
45
45
  after "authorize"
46
- after "authorize_failure"
47
46
 
48
47
  before "token"
49
48
 
@@ -55,15 +54,13 @@ module Rodauth
55
54
  before "create_oauth_application"
56
55
  after "create_oauth_application"
57
56
 
58
- error_flash "OAuth Authorization invalid parameters", "oauth_grant_valid_parameters"
59
-
60
57
  error_flash "Please authorize to continue", "require_authorization"
61
58
  error_flash "There was an error registering your oauth application", "create_oauth_application"
62
59
  notice_flash "Your oauth application has been registered", "create_oauth_application"
63
60
 
64
61
  notice_flash "The oauth token has been revoked", "revoke_oauth_token"
65
62
 
66
- view "oauth_authorize", "Authorize", "authorize"
63
+ view "authorize", "Authorize", "authorize"
67
64
  view "oauth_applications", "Oauth Applications", "oauth_applications"
68
65
  view "oauth_application", "Oauth Application", "oauth_application"
69
66
  view "new_oauth_application", "New Oauth Application", "new_oauth_application"
@@ -80,7 +77,7 @@ module Rodauth
80
77
  auth_value_method :oauth_require_pkce, false
81
78
  auth_value_method :oauth_pkce_challenge_method, "S256"
82
79
 
83
- auth_value_method :oauth_valid_uri_schemes, %w[http https]
80
+ auth_value_method :oauth_valid_uri_schemes, %w[https]
84
81
 
85
82
  auth_value_method :oauth_scope_separator, " "
86
83
 
@@ -148,9 +145,7 @@ module Rodauth
148
145
  auth_value_method :oauth_application_scopes, SCOPES
149
146
  auth_value_method :oauth_token_type, "bearer"
150
147
 
151
- auth_value_method :invalid_request, "Request is missing a required parameter"
152
- auth_value_method :invalid_client, "Invalid client"
153
- auth_value_method :unauthorized_client, "Unauthorized client"
148
+ auth_value_method :invalid_client_message, "Invalid client"
154
149
  auth_value_method :invalid_grant_type_message, "Invalid grant type"
155
150
  auth_value_method :invalid_grant_message, "Invalid grant"
156
151
  auth_value_method :invalid_scope_message, "Invalid scope"
@@ -195,11 +190,11 @@ module Rodauth
195
190
 
196
191
  def check_csrf?
197
192
  case request.path
198
- when oauth_token_path, oauth_introspect_path
193
+ when token_path, introspect_path
199
194
  false
200
- when oauth_revoke_path
195
+ when revoke_path
201
196
  !json_request?
202
- when oauth_authorize_path, %r{/#{oauth_applications_path}}
197
+ when authorize_path, %r{/#{oauth_applications_path}}
203
198
  only_json? ? false : super
204
199
  else
205
200
  super
@@ -233,7 +228,15 @@ module Rodauth
233
228
  end
234
229
 
235
230
  def scopes
236
- (param_or_nil("scope") || oauth_application_default_scope).split(" ")
231
+ scope = request.params["scope"]
232
+ case scope
233
+ when Array
234
+ scope
235
+ when String
236
+ scope.split(" ")
237
+ when nil
238
+ [oauth_application_default_scope]
239
+ end
237
240
  end
238
241
 
239
242
  def redirect_uri
@@ -266,6 +269,8 @@ module Rodauth
266
269
 
267
270
  return unless scheme.downcase == oauth_token_type
268
271
 
272
+ return if token.empty?
273
+
269
274
  token
270
275
  end
271
276
 
@@ -409,7 +414,7 @@ module Rodauth
409
414
  http = Net::HTTP.new(auth_url.host, auth_url.port)
410
415
  http.use_ssl = auth_url.scheme == "https"
411
416
 
412
- request = Net::HTTP::Post.new(oauth_introspect_path)
417
+ request = Net::HTTP::Post.new(introspect_path)
413
418
  request["content-type"] = json_response_content_type
414
419
  request["accept"] = json_response_content_type
415
420
  request.body = JSON.dump({ "token_type_hint" => token_type_hint, "token" => token })
@@ -694,14 +699,14 @@ module Rodauth
694
699
  request.env["REQUEST_METHOD"] = "POST"
695
700
  end
696
701
 
697
- def create_oauth_grant
698
- create_params = {
702
+ def create_oauth_grant(create_params = {})
703
+ create_params.merge!(
699
704
  oauth_grants_account_id_column => account_id,
700
705
  oauth_grants_oauth_application_id_column => oauth_application[oauth_applications_id_column],
701
706
  oauth_grants_redirect_uri_column => redirect_uri,
702
707
  oauth_grants_expires_in_column => Time.now + oauth_grant_expires_in,
703
708
  oauth_grants_scopes_column => scopes.join(oauth_scope_separator)
704
- }
709
+ )
705
710
 
706
711
  # Access Type flow
707
712
  if use_oauth_access_type?
@@ -735,6 +740,45 @@ module Rodauth
735
740
  end
736
741
  end
737
742
 
743
+ def do_authorize(redirect_url, query_params = [], fragment_params = [])
744
+ case param("response_type")
745
+ when "token"
746
+ redirect_response_error("invalid_request") unless use_oauth_implicit_grant_type?
747
+
748
+ fragment_params.replace(_do_authorize_token.map { |k, v| "#{k}=#{v}" })
749
+ when "code", "", nil
750
+ query_params.replace(_do_authorize_code.map { |k, v| "#{k}=#{v}" })
751
+ end
752
+
753
+ if param_or_nil("state")
754
+ if !fragment_params.empty?
755
+ fragment_params << "state=#{param('state')}"
756
+ else
757
+ query_params << "state=#{param('state')}"
758
+ end
759
+ end
760
+
761
+ query_params << redirect_url.query if redirect_url.query
762
+
763
+ redirect_url.query = query_params.join("&") unless query_params.empty?
764
+ redirect_url.fragment = fragment_params.join("&") unless fragment_params.empty?
765
+ end
766
+
767
+ def _do_authorize_code
768
+ { "code" => create_oauth_grant }
769
+ end
770
+
771
+ def _do_authorize_token
772
+ create_params = {
773
+ oauth_tokens_account_id_column => account_id,
774
+ oauth_tokens_oauth_application_id_column => oauth_application[oauth_applications_id_column],
775
+ oauth_tokens_scopes_column => scopes
776
+ }
777
+ oauth_token = generate_oauth_token(create_params, false)
778
+
779
+ json_access_token_payload(oauth_token)
780
+ end
781
+
738
782
  # Access Tokens
739
783
 
740
784
  def before_token
@@ -760,27 +804,42 @@ module Rodauth
760
804
  def create_oauth_token
761
805
  case param("grant_type")
762
806
  when "authorization_code"
763
- create_oauth_token_from_authorization_code(oauth_application)
807
+ # fetch oauth grant
808
+ oauth_grant = db[oauth_grants_table].where(
809
+ oauth_grants_code_column => param("code"),
810
+ oauth_grants_redirect_uri_column => param("redirect_uri"),
811
+ oauth_grants_oauth_application_id_column => oauth_application[oauth_applications_id_column],
812
+ oauth_grants_revoked_at_column => nil
813
+ ).where(Sequel[oauth_grants_expires_in_column] >= Sequel::CURRENT_TIMESTAMP)
814
+ .for_update
815
+ .first
816
+
817
+ redirect_response_error("invalid_grant") unless oauth_grant
818
+
819
+ create_params = {
820
+ oauth_tokens_account_id_column => oauth_grant[oauth_grants_account_id_column],
821
+ oauth_tokens_oauth_application_id_column => oauth_grant[oauth_grants_oauth_application_id_column],
822
+ oauth_tokens_oauth_grant_id_column => oauth_grant[oauth_grants_id_column],
823
+ oauth_tokens_scopes_column => oauth_grant[oauth_grants_scopes_column]
824
+ }
825
+ create_oauth_token_from_authorization_code(oauth_grant, create_params)
764
826
  when "refresh_token"
765
- create_oauth_token_from_token(oauth_application)
827
+ # fetch oauth token
828
+ oauth_token = oauth_token_by_refresh_token(param("refresh_token"))
829
+
830
+ redirect_response_error("invalid_grant") unless oauth_token
831
+
832
+ update_params = {
833
+ oauth_tokens_oauth_application_id_column => oauth_token[oauth_grants_oauth_application_id_column],
834
+ oauth_tokens_expires_in_column => Time.now + oauth_token_expires_in
835
+ }
836
+ create_oauth_token_from_token(oauth_token, update_params)
766
837
  else
767
838
  redirect_response_error("invalid_grant")
768
839
  end
769
840
  end
770
841
 
771
- def create_oauth_token_from_authorization_code(oauth_application)
772
- # fetch oauth grant
773
- oauth_grant = db[oauth_grants_table].where(
774
- oauth_grants_code_column => param("code"),
775
- oauth_grants_redirect_uri_column => param("redirect_uri"),
776
- oauth_grants_oauth_application_id_column => oauth_application[oauth_applications_id_column],
777
- oauth_grants_revoked_at_column => nil
778
- ).where(Sequel[oauth_grants_expires_in_column] >= Sequel::CURRENT_TIMESTAMP)
779
- .for_update
780
- .first
781
-
782
- redirect_response_error("invalid_grant") unless oauth_grant
783
-
842
+ def create_oauth_token_from_authorization_code(oauth_grant, create_params)
784
843
  # PKCE
785
844
  if use_oauth_pkce?
786
845
  if oauth_grant[oauth_grants_code_challenge_column]
@@ -792,13 +851,6 @@ module Rodauth
792
851
  end
793
852
  end
794
853
 
795
- create_params = {
796
- oauth_tokens_account_id_column => oauth_grant[oauth_grants_account_id_column],
797
- oauth_tokens_oauth_application_id_column => oauth_grant[oauth_grants_oauth_application_id_column],
798
- oauth_tokens_oauth_grant_id_column => oauth_grant[oauth_grants_id_column],
799
- oauth_tokens_scopes_column => oauth_grant[oauth_grants_scopes_column]
800
- }
801
-
802
854
  # revoke oauth grant
803
855
  db[oauth_grants_table].where(oauth_grants_id_column => oauth_grant[oauth_grants_id_column])
804
856
  .update(oauth_grants_revoked_at_column => Sequel::CURRENT_TIMESTAMP)
@@ -809,19 +861,11 @@ module Rodauth
809
861
  generate_oauth_token(create_params, should_generate_refresh_token)
810
862
  end
811
863
 
812
- def create_oauth_token_from_token(oauth_application)
813
- # fetch oauth token
814
- oauth_token = oauth_token_by_refresh_token(param("refresh_token"))
815
-
816
- redirect_response_error("invalid_grant") unless oauth_token && token_from_application?(oauth_token, oauth_application)
864
+ def create_oauth_token_from_token(oauth_token, update_params)
865
+ redirect_response_error("invalid_grant") unless token_from_application?(oauth_token, oauth_application)
817
866
 
818
867
  token = oauth_unique_id_generator
819
868
 
820
- update_params = {
821
- oauth_tokens_oauth_application_id_column => oauth_token[oauth_grants_oauth_application_id_column],
822
- oauth_tokens_expires_in_column => Time.now + oauth_token_expires_in
823
- }
824
-
825
869
  if oauth_tokens_token_hash_column
826
870
  update_params[oauth_tokens_token_hash_column] = generate_token_hash(token)
827
871
  else
@@ -995,7 +1039,7 @@ module Rodauth
995
1039
  throw_json_response_error(authorization_required_error_status, "invalid_client")
996
1040
  else
997
1041
  set_redirect_error_flash(require_authorization_error_flash)
998
- redirect(oauth_authorize_path)
1042
+ redirect(authorize_path)
999
1043
  end
1000
1044
  end
1001
1045
 
@@ -1075,7 +1119,7 @@ module Rodauth
1075
1119
 
1076
1120
  def oauth_server_metadata_body(path)
1077
1121
  issuer = base_url
1078
- issuer += "/#{path}" if issuer
1122
+ issuer += "/#{path}" if path
1079
1123
 
1080
1124
  responses_supported = %w[code]
1081
1125
  response_modes_supported = %w[query]
@@ -1088,8 +1132,8 @@ module Rodauth
1088
1132
  end
1089
1133
  {
1090
1134
  issuer: issuer,
1091
- authorization_endpoint: oauth_authorize_url,
1092
- token_endpoint: oauth_token_url,
1135
+ authorization_endpoint: authorize_url,
1136
+ token_endpoint: token_url,
1093
1137
  registration_endpoint: "#{base_url}/#{oauth_applications_path}",
1094
1138
  scopes_supported: oauth_application_scopes,
1095
1139
  response_types_supported: responses_supported,
@@ -1100,16 +1144,18 @@ module Rodauth
1100
1144
  ui_locales_supported: oauth_metadata_ui_locales_supported,
1101
1145
  op_policy_uri: oauth_metadata_op_policy_uri,
1102
1146
  op_tos_uri: oauth_metadata_op_tos_uri,
1103
- revocation_endpoint: oauth_revoke_url,
1147
+ revocation_endpoint: revoke_url,
1104
1148
  revocation_endpoint_auth_methods_supported: nil, # because it's client_secret_basic
1105
- introspection_endpoint: oauth_introspect_url,
1149
+ introspection_endpoint: introspect_url,
1106
1150
  introspection_endpoint_auth_methods_supported: %w[client_secret_basic],
1107
1151
  code_challenge_methods_supported: (use_oauth_pkce? ? oauth_pkce_challenge_method : nil)
1108
1152
  }
1109
1153
  end
1110
1154
 
1111
- # /oauth-token
1112
- route(:oauth_token) do |r|
1155
+ # /token
1156
+ route(:token) do |r|
1157
+ next unless is_authorization_server?
1158
+
1113
1159
  before_token
1114
1160
 
1115
1161
  r.post do
@@ -1128,8 +1174,10 @@ module Rodauth
1128
1174
  end
1129
1175
  end
1130
1176
 
1131
- # /oauth-introspect
1132
- route(:oauth_introspect) do |r|
1177
+ # /introspect
1178
+ route(:introspect) do |r|
1179
+ next unless is_authorization_server?
1180
+
1133
1181
  before_introspect
1134
1182
 
1135
1183
  r.post do
@@ -1159,8 +1207,10 @@ module Rodauth
1159
1207
  end
1160
1208
  end
1161
1209
 
1162
- # /oauth-revoke
1163
- route(:oauth_revoke) do |r|
1210
+ # /revoke
1211
+ route(:revoke) do |r|
1212
+ next unless is_authorization_server?
1213
+
1164
1214
  before_revoke
1165
1215
 
1166
1216
  r.post do
@@ -1188,8 +1238,10 @@ module Rodauth
1188
1238
  end
1189
1239
  end
1190
1240
 
1191
- # /oauth-authorize
1192
- route(:oauth_authorize) do |r|
1241
+ # /authorize
1242
+ route(:authorize) do |r|
1243
+ next unless is_authorization_server?
1244
+
1193
1245
  require_account
1194
1246
  validate_oauth_grant_params
1195
1247
  try_approval_prompt if use_oauth_access_type? && request.get?
@@ -1201,39 +1253,11 @@ module Rodauth
1201
1253
  end
1202
1254
 
1203
1255
  r.post do
1204
- code = nil
1205
- query_params = []
1206
- fragment_params = []
1256
+ redirect_url = URI.parse(redirect_uri)
1207
1257
 
1208
1258
  transaction do
1209
- case param("response_type")
1210
- when "token"
1211
- redirect_response_error("invalid_request") unless use_oauth_implicit_grant_type?
1212
-
1213
- create_params = {
1214
- oauth_tokens_account_id_column => account_id,
1215
- oauth_tokens_oauth_application_id_column => oauth_application[oauth_applications_id_column],
1216
- oauth_tokens_scopes_column => scopes
1217
- }
1218
- oauth_token = generate_oauth_token(create_params, false)
1219
-
1220
- token_payload = json_access_token_payload(oauth_token)
1221
- fragment_params.replace(token_payload.map { |k, v| "#{k}=#{v}" })
1222
- when "code", "", nil
1223
- code = create_oauth_grant
1224
- query_params << "code=#{code}"
1225
- else
1226
- redirect_response_error("invalid_request")
1227
- end
1228
- after_authorize
1259
+ do_authorize(redirect_url)
1229
1260
  end
1230
-
1231
- redirect_url = URI.parse(redirect_uri)
1232
- query_params << "state=#{param('state')}" if param_or_nil("state")
1233
- query_params << redirect_url.query if redirect_url.query
1234
- redirect_url.query = query_params.join("&") unless query_params.empty?
1235
- redirect_url.fragment = fragment_params.join("&") unless fragment_params.empty?
1236
-
1237
1261
  redirect(redirect_url.to_s)
1238
1262
  end
1239
1263
  end
@@ -2,6 +2,7 @@
2
2
 
3
3
  module Rodauth
4
4
  Feature.define(:oauth_http_mac) do
5
+ # :nocov:
5
6
  unless String.method_defined?(:delete_prefix)
6
7
  module PrefixExtensions
7
8
  refine(String) do
@@ -27,6 +28,7 @@ module Rodauth
27
28
  end
28
29
  using(PrefixExtensions)
29
30
  end
31
+ # :nocov:
30
32
 
31
33
  depends :oauth
32
34
 
@@ -6,7 +6,10 @@ module Rodauth
6
6
  Feature.define(:oauth_jwt) do
7
7
  depends :oauth
8
8
 
9
- auth_value_method :oauth_jwt_token_issuer, "Example"
9
+ auth_value_method :oauth_jwt_subject_type, "public" # public, pairwise
10
+ auth_value_method :oauth_jwt_subject_secret, nil # salt for pairwise generation
11
+
12
+ auth_value_method :oauth_jwt_token_issuer, nil
10
13
 
11
14
  auth_value_method :oauth_application_jws_jwk_column, nil
12
15
 
@@ -28,7 +31,8 @@ module Rodauth
28
31
  auth_value_methods(
29
32
  :jwt_encode,
30
33
  :jwt_decode,
31
- :jwks_set
34
+ :jwks_set,
35
+ :last_account_login_at
32
36
  )
33
37
 
34
38
  JWKS = OAuth::TtlStore.new
@@ -45,6 +49,12 @@ module Rodauth
45
49
 
46
50
  private
47
51
 
52
+ unless method_defined?(:last_account_login_at)
53
+ def last_account_login_at
54
+ nil
55
+ end
56
+ end
57
+
48
58
  def authorization_token
49
59
  return @authorization_token if defined?(@authorization_token)
50
60
 
@@ -57,8 +67,8 @@ module Rodauth
57
67
 
58
68
  return unless jwt_token
59
69
 
60
- return if jwt_token["iss"] != oauth_jwt_token_issuer ||
61
- jwt_token["aud"] != oauth_jwt_audience ||
70
+ return if jwt_token["iss"] != (oauth_jwt_token_issuer || authorization_server_url) ||
71
+ (oauth_jwt_audience && jwt_token["aud"] != oauth_jwt_audience) ||
62
72
  !jwt_token["sub"]
63
73
 
64
74
  jwt_token
@@ -169,11 +179,23 @@ module Rodauth
169
179
 
170
180
  oauth_token = _generate_oauth_token(create_params)
171
181
 
182
+ claims = jwt_claims(oauth_token)
183
+
184
+ # one of the points of using jwt is avoiding database lookups, so we put here all relevant
185
+ # token data.
186
+ claims[:scope] = oauth_token[oauth_tokens_scopes_column]
187
+
188
+ token = jwt_encode(claims)
189
+
190
+ oauth_token[oauth_tokens_token_column] = token
191
+ oauth_token
192
+ end
193
+
194
+ def jwt_claims(oauth_token)
172
195
  issued_at = Time.now.utc.to_i
173
196
 
174
- payload = {
175
- sub: oauth_token[oauth_tokens_account_id_column],
176
- iss: oauth_jwt_token_issuer, # issuer
197
+ claims = {
198
+ iss: (oauth_jwt_token_issuer || authorization_server_url), # issuer
177
199
  iat: issued_at, # issued at
178
200
  #
179
201
  # sub REQUIRED - as defined in section 4.1.2 of [RFC7519]. In case of
@@ -184,20 +206,29 @@ module Rodauth
184
206
  # owner is involved, such as the client credentials grant, the value
185
207
  # of "sub" SHOULD correspond to an identifier the authorization
186
208
  # server uses to indicate the client application.
209
+ sub: jwt_subject(oauth_token),
187
210
  client_id: oauth_application[oauth_applications_client_id_column],
188
211
 
189
212
  exp: issued_at + oauth_token_expires_in,
190
- aud: oauth_jwt_audience,
191
-
192
- # one of the points of using jwt is avoiding database lookups, so we put here all relevant
193
- # token data.
194
- scope: oauth_token[oauth_tokens_scopes_column]
213
+ aud: (oauth_jwt_audience || oauth_application[oauth_applications_client_id_column])
195
214
  }
196
215
 
197
- token = jwt_encode(payload)
216
+ claims[:auth_time] = last_account_login_at.utc.to_i if last_account_login_at
198
217
 
199
- oauth_token[oauth_tokens_token_column] = token
200
- oauth_token
218
+ claims
219
+ end
220
+
221
+ def jwt_subject(oauth_token)
222
+ case oauth_jwt_subject_type
223
+ when "public"
224
+ oauth_token[oauth_tokens_account_id_column]
225
+ when "pairwise"
226
+ id = oauth_token[oauth_tokens_account_id_column]
227
+ application_id = oauth_token[oauth_tokens_oauth_application_id_column]
228
+ Digest::SHA256.hexdigest("#{id}#{application_id}#{oauth_jwt_subject_secret}")
229
+ else
230
+ raise StandardError, "unexpected subject (#{oauth_jwt_subject_type})"
231
+ end
201
232
  end
202
233
 
203
234
  def oauth_token_by_token(token, *)
@@ -228,17 +259,11 @@ module Rodauth
228
259
  def oauth_server_metadata_body(path)
229
260
  metadata = super
230
261
  metadata.merge! \
231
- jwks_uri: oauth_jwks_url,
262
+ jwks_uri: jwks_url,
232
263
  token_endpoint_auth_signing_alg_values_supported: [oauth_jwt_algorithm]
233
264
  metadata
234
265
  end
235
266
 
236
- def token_from_application?(oauth_token, oauth_application)
237
- return super unless oauth_token["sub"] # naive check on whether it's a jwt token
238
-
239
- oauth_token["client_id"] == oauth_application[oauth_applications_client_id_column]
240
- end
241
-
242
267
  def _jwt_key
243
268
  @_jwt_key ||= oauth_jwt_key || (oauth_application[oauth_applications_client_secret_column] if oauth_application)
244
269
  end
@@ -412,7 +437,9 @@ module Rodauth
412
437
  super
413
438
  end
414
439
 
415
- route(:oauth_jwks) do |r|
440
+ route(:jwks) do |r|
441
+ next unless is_authorization_server?
442
+
416
443
  r.get do
417
444
  json_response_success({ keys: jwks_set })
418
445
  end
@@ -0,0 +1,267 @@
1
+ # frozen-string-literal: true
2
+
3
+ module Rodauth
4
+ Feature.define(:oidc) do
5
+ OIDC_SCOPES_MAP = {
6
+ "profile" => %i[name family_name given_name middle_name nickname preferred_username
7
+ profile picture website gender birthdate zoneinfo locale updated_at].freeze,
8
+ "email" => %i[email email_verified].freeze,
9
+ "address" => %i[address].freeze,
10
+ "phone" => %i[phone_number phone_number_verified].freeze
11
+ }.freeze
12
+
13
+ depends :oauth_jwt
14
+
15
+ auth_value_method :oauth_application_default_scope, "openid"
16
+ auth_value_method :oauth_application_scopes, %w[openid]
17
+
18
+ auth_value_method :oauth_grants_nonce_column, :nonce
19
+ auth_value_method :oauth_tokens_nonce_column, :nonce
20
+
21
+ auth_value_method :invalid_scope_message, "The Access Token expired"
22
+
23
+ auth_value_method :webfinger_relation, "http://openid.net/specs/connect/1.0/issuer"
24
+
25
+ auth_value_methods(:get_oidc_param)
26
+
27
+ def openid_configuration(issuer = nil)
28
+ request.on(".well-known/openid-configuration") do
29
+ request.get do
30
+ json_response_success(openid_configuration_body(issuer))
31
+ end
32
+ end
33
+ end
34
+
35
+ def webfinger
36
+ request.on(".well-known/webfinger") do
37
+ request.get do
38
+ resource = param_or_nil("resource")
39
+
40
+ throw_json_response_error(400, "invalid_request") unless resource
41
+
42
+ response.status = 200
43
+ response["Content-Type"] ||= "application/jrd+json"
44
+
45
+ json_payload = JSON.dump({
46
+ subject: resource,
47
+ links: [{
48
+ rel: webfinger_relation,
49
+ href: authorization_server_url
50
+ }]
51
+ })
52
+ response.write(json_payload)
53
+ request.halt
54
+ end
55
+ end
56
+ end
57
+
58
+ private
59
+
60
+ def create_oauth_grant(create_params = {})
61
+ return super unless (nonce = param_or_nil("nonce"))
62
+
63
+ super(oauth_grants_nonce_column => nonce)
64
+ end
65
+
66
+ def create_oauth_token_from_authorization_code(oauth_grant, create_params)
67
+ return super unless oauth_grant[oauth_grants_nonce_column]
68
+
69
+ super(oauth_grant, create_params.merge(oauth_tokens_nonce_column => oauth_grant[oauth_grants_nonce_column]))
70
+ end
71
+
72
+ def create_oauth_token
73
+ oauth_token = super
74
+ generate_id_token(oauth_token)
75
+ oauth_token
76
+ end
77
+
78
+ def generate_id_token(oauth_token)
79
+ oauth_scopes = oauth_token[oauth_tokens_scopes_column].split(oauth_scope_separator)
80
+
81
+ return unless oauth_scopes.include?("openid")
82
+
83
+ id_token_claims = jwt_claims(oauth_token)
84
+ id_token_claims[:nonce] = oauth_token[oauth_tokens_nonce_column] if oauth_token[oauth_tokens_nonce_column]
85
+
86
+ # Time when the End-User authentication occurred.
87
+ #
88
+ # Sounds like the same as issued at claim.
89
+ id_token_claims[:auth_time] = id_token_claims[:iat]
90
+
91
+ account = db[accounts_table].where(account_id_column => oauth_token[oauth_tokens_account_id_column]).first
92
+
93
+ # this should never happen!
94
+ # a newly minted oauth token from a grant should have been assigned to an account
95
+ # who just authorized its generation.
96
+ return unless account
97
+
98
+ fill_with_account_claims(id_token_claims, account, oauth_scopes)
99
+
100
+ oauth_token[:id_token] = jwt_encode(id_token_claims)
101
+ end
102
+
103
+ def fill_with_account_claims(claims, account, scopes)
104
+ scopes_by_oidc = scopes.each_with_object({}) do |scope, by_oidc|
105
+ oidc, param = scope.split(".", 2)
106
+
107
+ by_oidc[oidc] ||= []
108
+
109
+ by_oidc[oidc] << param.to_sym if param
110
+ end
111
+
112
+ oidc_scopes = (OIDC_SCOPES_MAP.keys & scopes_by_oidc.keys)
113
+
114
+ return if oidc_scopes.empty?
115
+
116
+ if respond_to?(:get_oidc_param)
117
+ oidc_scopes.each do |scope|
118
+ params = scopes_by_oidc[scope]
119
+ params = params.empty? ? OIDC_SCOPES_MAP[scope] : (OIDC_SCOPES_MAP[scope] & params)
120
+
121
+ params.each do |param|
122
+ claims[param] = __send__(:get_oidc_param, account, param)
123
+ end
124
+ end
125
+ else
126
+ warn "`get_oidc_param(token, param)` must be implemented to use oidc scopes."
127
+ end
128
+ end
129
+
130
+ def json_access_token_payload(oauth_token)
131
+ payload = super
132
+ payload["id_token"] = oauth_token[:id_token] if oauth_token[:id_token]
133
+ payload
134
+ end
135
+
136
+ # Authorize
137
+
138
+ def check_valid_response_type?
139
+ case param_or_nil("response_type")
140
+ when "none", "id_token",
141
+ "code token", "code id_token", "id_token token", "code id_token token" # multiple
142
+ true
143
+ else
144
+ super
145
+ end
146
+ end
147
+
148
+ def do_authorize(redirect_url, query_params = [], fragment_params = [])
149
+ return super unless use_oauth_implicit_grant_type?
150
+
151
+ case param("response_type")
152
+ when "id_token"
153
+ fragment_params.replace(_do_authorize_id_token.map { |k, v| "#{k}=#{v}" })
154
+ when "code token"
155
+ redirect_response_error("invalid_request") unless use_oauth_implicit_grant_type?
156
+
157
+ params = _do_authorize_code.merge(_do_authorize_token)
158
+
159
+ fragment_params.replace(params.map { |k, v| "#{k}=#{v}" })
160
+ when "code id_token"
161
+ params = _do_authorize_code.merge(_do_authorize_id_token)
162
+
163
+ fragment_params.replace(params.map { |k, v| "#{k}=#{v}" })
164
+ when "id_token token"
165
+ params = _do_authorize_id_token.merge(_do_authorize_token)
166
+
167
+ fragment_params.replace(params.map { |k, v| "#{k}=#{v}" })
168
+ when "code id_token token"
169
+ params = _do_authorize_code.merge(_do_authorize_id_token).merge(_do_authorize_token)
170
+
171
+ fragment_params.replace(params.map { |k, v| "#{k}=#{v}" })
172
+ end
173
+
174
+ super(redirect_url, query_params, fragment_params)
175
+ end
176
+
177
+ def _do_authorize_id_token
178
+ create_params = {
179
+ oauth_tokens_account_id_column => account_id,
180
+ oauth_tokens_oauth_application_id_column => oauth_application[oauth_applications_id_column],
181
+ oauth_tokens_scopes_column => scopes
182
+ }
183
+ oauth_token = generate_oauth_token(create_params, false)
184
+ generate_id_token(oauth_token)
185
+ params = json_access_token_payload(oauth_token)
186
+ params.delete("access_token")
187
+ params
188
+ end
189
+
190
+ # Metadata
191
+
192
+ def openid_configuration_body(path)
193
+ metadata = oauth_server_metadata_body(path)
194
+
195
+ scope_claims = oauth_application_scopes.each_with_object([]) do |scope, claims|
196
+ oidc, param = scope.split(".", 2)
197
+ if param
198
+ claims << param
199
+ else
200
+ oidc_claims = OIDC_SCOPES_MAP[oidc]
201
+ claims.concat(oidc_claims) if oidc_claims
202
+ end
203
+ end
204
+
205
+ scope_claims.unshift("auth_time") if last_account_login_at
206
+
207
+ metadata.merge({
208
+ userinfo_endpoint: userinfo_url,
209
+ response_types_supported: metadata[:response_types_supported] +
210
+ ["none", "id_token", %w[code token], %w[code id_token], %w[id_token token], %w[code id_token token]],
211
+ response_modes_supported: %w[query fragment],
212
+ grant_types_supported: %w[authorization_code implicit],
213
+
214
+ subject_types_supported: [oauth_jwt_subject_type],
215
+
216
+ id_token_signing_alg_values_supported: metadata[:token_endpoint_auth_signing_alg_values_supported],
217
+ id_token_encryption_alg_values_supported: [oauth_jwt_jwe_algorithm].compact,
218
+ id_token_encryption_enc_values_supported: [oauth_jwt_jwe_encryption_method].compact,
219
+
220
+ userinfo_signing_alg_values_supported: [],
221
+ userinfo_encryption_alg_values_supported: [],
222
+ userinfo_encryption_enc_values_supported: [],
223
+
224
+ request_object_signing_alg_values_supported: [],
225
+ request_object_encryption_alg_values_supported: [],
226
+ request_object_encryption_enc_values_supported: [],
227
+
228
+ # These Claim Types are described in Section 5.6 of OpenID Connect Core 1.0 [OpenID.Core].
229
+ # Values defined by this specification are normal, aggregated, and distributed.
230
+ # If omitted, the implementation supports only normal Claims.
231
+ claim_types_supported: %w[normal],
232
+ claims_supported: %w[sub iss iat exp aud] | scope_claims
233
+ })
234
+ end
235
+
236
+ # /userinfo
237
+ route(:userinfo) do |r|
238
+ next unless is_authorization_server?
239
+
240
+ r.on method: %i[get post] do
241
+ catch_error do
242
+ oauth_token = authorization_token
243
+
244
+ throw_json_response_error(authorization_required_error_status, "invalid_token") unless oauth_token
245
+
246
+ oauth_scopes = oauth_token["scope"].split(" ")
247
+
248
+ throw_json_response_error(authorization_required_error_status, "invalid_token") unless oauth_scopes.include?("openid")
249
+
250
+ account = db[accounts_table].where(account_id_column => oauth_token["sub"]).first
251
+
252
+ throw_json_response_error(authorization_required_error_status, "invalid_token") unless account
253
+
254
+ oauth_scopes.delete("openid")
255
+
256
+ oidc_claims = { "sub" => oauth_token["sub"] }
257
+
258
+ fill_with_account_claims(oidc_claims, account, oauth_scopes)
259
+
260
+ json_response_success(oidc_claims)
261
+ end
262
+
263
+ throw_json_response_error(authorization_required_error_status, "invalid_token")
264
+ end
265
+ end
266
+ end
267
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Rodauth
4
4
  module OAuth
5
- VERSION = "0.0.6"
5
+ VERSION = "0.1.0"
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.6
4
+ version: 0.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tiago Cardoso
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-07-06 00:00:00.000000000 Z
11
+ date: 2020-08-01 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:
@@ -32,6 +32,7 @@ files:
32
32
  - lib/rodauth/features/oauth.rb
33
33
  - lib/rodauth/features/oauth_http_mac.rb
34
34
  - lib/rodauth/features/oauth_jwt.rb
35
+ - lib/rodauth/features/oidc.rb
35
36
  - lib/rodauth/oauth.rb
36
37
  - lib/rodauth/oauth/railtie.rb
37
38
  - lib/rodauth/oauth/ttl_store.rb
@@ -42,7 +43,7 @@ metadata:
42
43
  homepage_uri: https://gitlab.com/honeyryderchuck/roda-oauth
43
44
  source_code_uri: https://gitlab.com/honeyryderchuck/roda-oauth
44
45
  changelog_uri: https://gitlab.com/honeyryderchuck/roda-oauth/-/blob/master/CHANGELOG.md
45
- post_install_message:
46
+ post_install_message:
46
47
  rdoc_options: []
47
48
  require_paths:
48
49
  - lib
@@ -58,7 +59,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
58
59
  version: '0'
59
60
  requirements: []
60
61
  rubygems_version: 3.1.2
61
- signing_key:
62
+ signing_key:
62
63
  specification_version: 4
63
64
  summary: Implementation of the OAuth 2.0 protocol on top of rodauth.
64
65
  test_files: []