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 +4 -4
- data/CHANGELOG.md +65 -5
- data/README.md +42 -20
- data/lib/generators/roda/oauth/templates/db/migrate/create_rodauth_oauth.rb +4 -1
- data/lib/rodauth/features/oauth.rb +116 -92
- data/lib/rodauth/features/oauth_http_mac.rb +2 -0
- data/lib/rodauth/features/oauth_jwt.rb +50 -23
- data/lib/rodauth/features/oidc.rb +267 -0
- data/lib/rodauth/oauth/version.rb +1 -1
- metadata +6 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3eac600d006a2c78509f608575db062b7ba6d67356b890c7d38414b9b82875f9
|
4
|
+
data.tar.gz: c37fc18c093f546023481a88cc526c5a0b721b1a3bfeac827c21184e0583071b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0a04fdb5ab370ed5736208cbd4ccb1e6da801af52cd68004625a21008c4a10a04bc143d99c9e1a71bccb9fad882fc3cff27d9c0900689dbd5cf6c0616e4d43a0
|
7
|
+
data.tar.gz: e6d5cb6e8ff31d64eb588fa39ad6a1e7bb1ae9416adac64e5f9a21bf451ffd4115a8c2d3bb8759f1a32192a88a4a401336e1baca7621a33934dc1b2a873c8402
|
data/CHANGELOG.md
CHANGED
@@ -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
|
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
|
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
|
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
|
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
|
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
|
-
[](https://gitlab.com/honeyryderchuck/rodauth-oauth/-/
|
4
|
-
[](https://gitlab.
|
3
|
+
[](https://gitlab.com/honeyryderchuck/rodauth-oauth/-/pipelines?page=1&ref=master)
|
4
|
+
[](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
|
-
|
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/
|
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/
|
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/
|
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/
|
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/
|
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/
|
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/
|
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/
|
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 /
|
247
|
-
* `POST /
|
248
|
-
* `POST /
|
249
|
-
* `POST /
|
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/
|
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/
|
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/
|
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/
|
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 "
|
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[
|
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 :
|
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
|
193
|
+
when token_path, introspect_path
|
199
194
|
false
|
200
|
-
when
|
195
|
+
when revoke_path
|
201
196
|
!json_request?
|
202
|
-
when
|
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
|
-
|
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(
|
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
|
-
|
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
|
-
|
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(
|
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(
|
813
|
-
|
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(
|
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
|
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:
|
1092
|
-
token_endpoint:
|
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:
|
1147
|
+
revocation_endpoint: revoke_url,
|
1104
1148
|
revocation_endpoint_auth_methods_supported: nil, # because it's client_secret_basic
|
1105
|
-
introspection_endpoint:
|
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
|
-
# /
|
1112
|
-
route(:
|
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
|
-
# /
|
1132
|
-
route(:
|
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
|
-
# /
|
1163
|
-
route(:
|
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
|
-
# /
|
1192
|
-
route(:
|
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
|
-
|
1205
|
-
query_params = []
|
1206
|
-
fragment_params = []
|
1256
|
+
redirect_url = URI.parse(redirect_uri)
|
1207
1257
|
|
1208
1258
|
transaction do
|
1209
|
-
|
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 :
|
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
|
-
|
175
|
-
|
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
|
-
|
216
|
+
claims[:auth_time] = last_account_login_at.utc.to_i if last_account_login_at
|
198
217
|
|
199
|
-
|
200
|
-
|
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:
|
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(:
|
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
|
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
|
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-
|
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: []
|