rodauth-oauth 1.3.2 → 1.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +17 -10
  3. data/doc/release_notes/1_4_0.md +57 -0
  4. data/doc/release_notes/1_5_0.md +20 -0
  5. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/authorize.html.erb +28 -23
  6. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/frontchannel_logout.html.erb +10 -0
  7. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_applications.html.erb +1 -1
  8. data/lib/generators/rodauth/oauth/templates/db/migrate/create_rodauth_oauth.rb +37 -1
  9. data/lib/generators/rodauth/oauth/views_generator.rb +2 -2
  10. data/lib/rodauth/features/oauth_application_management.rb +1 -1
  11. data/lib/rodauth/features/oauth_assertion_base.rb +1 -1
  12. data/lib/rodauth/features/oauth_authorize_base.rb +1 -1
  13. data/lib/rodauth/features/oauth_base.rb +49 -38
  14. data/lib/rodauth/features/oauth_device_code_grant.rb +2 -2
  15. data/lib/rodauth/features/oauth_dpop.rb +410 -0
  16. data/lib/rodauth/features/oauth_dynamic_client_registration.rb +12 -2
  17. data/lib/rodauth/features/oauth_grant_management.rb +1 -1
  18. data/lib/rodauth/features/oauth_jwt.rb +12 -13
  19. data/lib/rodauth/features/oauth_jwt_base.rb +57 -34
  20. data/lib/rodauth/features/oauth_jwt_bearer_grant.rb +1 -1
  21. data/lib/rodauth/features/oauth_jwt_jwks.rb +1 -1
  22. data/lib/rodauth/features/oauth_resource_indicators.rb +1 -1
  23. data/lib/rodauth/features/oauth_resource_server.rb +1 -1
  24. data/lib/rodauth/features/oauth_saml_bearer_grant.rb +79 -47
  25. data/lib/rodauth/features/oauth_tls_client_auth.rb +2 -4
  26. data/lib/rodauth/features/oauth_token_introspection.rb +3 -3
  27. data/lib/rodauth/features/oauth_token_revocation.rb +1 -1
  28. data/lib/rodauth/features/oidc.rb +32 -11
  29. data/lib/rodauth/features/oidc_backchannel_logout.rb +120 -0
  30. data/lib/rodauth/features/oidc_dynamic_client_registration.rb +25 -0
  31. data/lib/rodauth/features/oidc_frontchannel_logout.rb +134 -0
  32. data/lib/rodauth/features/oidc_logout_base.rb +76 -0
  33. data/lib/rodauth/features/oidc_rp_initiated_logout.rb +29 -6
  34. data/lib/rodauth/features/oidc_session_management.rb +91 -0
  35. data/lib/rodauth/oauth/database_extensions.rb +4 -0
  36. data/lib/rodauth/oauth/http_extensions.rb +1 -1
  37. data/lib/rodauth/oauth/ttl_store.rb +1 -1
  38. data/lib/rodauth/oauth/version.rb +1 -1
  39. data/locales/en.yml +19 -0
  40. data/locales/pt.yml +9 -0
  41. data/templates/authorize.str +1 -0
  42. data/templates/check_session.str +67 -0
  43. data/templates/frontchannel_logout.str +17 -0
  44. metadata +14 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bf721a3632883e34bff44e33ca84c9a5aa2248748ccecf52c180edd25086abad
4
- data.tar.gz: 59966edc773dbee470be46770532ee9159dfe370d346eca936a2ebc48a8fd224
3
+ metadata.gz: 40ff3b3b3de0595eae98f218aec2f8e876f18329061c537e91e5841ab35e67dc
4
+ data.tar.gz: 19692e86d66400a9e7227f655bdc6375e38554cb52b97ee3a5e22cceab582168
5
5
  SHA512:
6
- metadata.gz: 6855059fe2d8225e4f3650dca01ad4ff3d82ad4e42623b43e5ea1d42ef3dbec317bf9488a2b1598556c135f9b9bf1d45ac41512d7347e3c5c55b7c5e8bd63305
7
- data.tar.gz: 9a564b8ad6812841f4a2737ee263af188e5231c49b8b475f0c575a5b0496aa6f534709552845caaaacbb73dbe1afabb9079a17265683be5df73f62075a78896e
6
+ metadata.gz: cd6efdeda012c25d83949e7f0ea2aa043238851f95bf1217b8156786f7376d39792caed37a2af68fc7777ed5a892fa2f9e5185efe1f8a3e7168df07de84b954d
7
+ data.tar.gz: 0e435eea239f81ff16db08b187ce7bb06ba3d25359603906e48610e38912162b35c79c3d8b7fbfa74df2c56f52bb653d61b999da76c1cc8d445f2077b876c578
data/README.md CHANGED
@@ -18,8 +18,12 @@ This is an extension to the `rodauth` gem which implements the [OAuth 2.0 framew
18
18
  * Dynamic OP
19
19
  * Form Post OP
20
20
  * 3rd Party-Init OP
21
+ * Session Management OP
22
+ * RP-Initiated Logout OP
23
+ * Front-Channel Logout OP
24
+ * Back-Channel Logout OP
21
25
 
22
- (it also passes the conformance tests for the RP-Initiated Logout OP).
26
+ The certifications were obtained using the [example OIDC server](/examples/oidc/authentication_server.rb) deployed [here](https://rodauth-oauth-oidc.onrender.com/).
23
27
 
24
28
  ## Features
25
29
 
@@ -43,7 +47,6 @@ This gem implements the following RFCs and features of OAuth:
43
47
  * `oauth_jwt_secured_authorization_response_mode` - [JWT Secured Authorization Response_mode](https://openid.net/specs/openid-financial-api-jarm.html);
44
48
  * `oauth_resource_indicators` - [Resource Indicators](https://datatracker.ietf.org/doc/html/rfc8707);
45
49
  * Access Type (Token refresh online and offline);
46
- * `oauth_http_mac` - [MAC Authentication Scheme](https://tools.ietf.org/html/draft-hammer-oauth-v2-mac-token-02);
47
50
  * `oauth_assertion_base` - [Assertion Framework](https://datatracker.ietf.org/doc/html/rfc7521);
48
51
  * `oauth_saml_bearer_grant` - [SAML 2.0 Bearer Assertion](https://datatracker.ietf.org/doc/html/rfc7522);
49
52
  * `oauth_jwt_bearer_grant` - [JWT Bearer Assertion](https://datatracker.ietf.org/doc/html/rfc7523);
@@ -52,13 +55,17 @@ This gem implements the following RFCs and features of OAuth:
52
55
  * OAuth application and token management dashboards;
53
56
  * The recommendations for [Native Apps](https://www.rfc-editor.org/rfc/rfc8252);
54
57
 
55
- It also implements the [OpenID Connect layer](https://openid.net/connect/) (via the `openid` feature) on top of the OAuth features it provides, including:
58
+ It also implements several components of [OpenID Connect](https://openid.net/connect/) on top of the OAuth features it provides, including:
56
59
 
57
- * [OpenID Connect Core](https://gitlab.com/os85/rodauth-oauth/-/wikis/Id-Token-Authentication);
58
- * [OpenID Connect Discovery](https://gitlab.com/os85/rodauth-oauth/-/wikis/OIDC-Dynamic-Client-Registration);
59
- * [OpenID Multiple Response Types](https://gitlab.com/os85/rodauth-oauth/-/wikis/Hybrid-flow);
60
- * [OpenID Connect Dynamic Client Registration](https://gitlab.com/os85/rodauth-oauth/-/wikis/OIDC-Dynamic-Client-Registration);
61
- * [RP Initiated Logout](https://gitlab.com/os85/rodauth-oauth/-/wikis/RP-Initiated-Logout);
60
+ * `oidc` - [OpenID Connect Core](https://gitlab.com/os85/rodauth-oauth/-/wikis/Id-Token-Authentication);
61
+ * `oidc_self_issued` - [Self-Issued OpenID Provider](https://openid.net/specs/openid-connect-core-1_0.html#SelfIssued)
62
+ * [OpenID Multiple Response Types](https://gitlab.com/os85/rodauth-oauth/-/wikis/Hybrid-flow);
63
+ * [OpenID Connect Discovery](https://gitlab.com/os85/rodauth-oauth/-/wikis/OIDC-Dynamic-Client-Registration);
64
+ * `oidc_dynamic_client_registration` - [OpenID Connect Dynamic Client Registration](https://gitlab.com/os85/rodauth-oauth/-/wikis/OIDC-Dynamic-Client-Registration);
65
+ * `oidc_session_management` - [Session Management](https://gitlab.com/os85/rodauth-oauth/-/wikis/Session-Management);
66
+ * `oidc_rp_initiated_logout` - [RP Initiated Logout](https://gitlab.com/os85/rodauth-oauth/-/wikis/RP-Initiated-Logout);
67
+ * `oidc_frontchannel_logout` - [Frontchannel Logout](https://gitlab.com/os85/rodauth-oauth/-/wikis/Frontchannel-Logout);
68
+ * `oidc_backchannel_logout` - [Backchannel Logout](https://gitlab.com/os85/rodauth-oauth/-/wikis/Backchannel-Logout);
62
69
 
63
70
  This gem supports also rails (through [rodauth-rails]((https://github.com/janko/rodauth-rails))).
64
71
 
@@ -83,8 +90,8 @@ Or install it yourself as:
83
90
  ## Resources
84
91
  | | |
85
92
  | ------------- | ----------------------------------------------------------- |
86
- | Website | https://os85.gitlab.io/rodauth-oauth/ |
87
- | Documentation | https://os85.gitlab.io/rodauth-oauth/rdoc/ |
93
+ | Website | https://honeyryderchuck.gitlab.io/rodauth-oauth/ |
94
+ | Documentation | https://honeyryderchuck.gitlab.io/rodauth-oauth/rdoc/ |
88
95
  | Wiki | https://gitlab.com/os85/rodauth-oauth/wikis/home |
89
96
  | CI | https://gitlab.com/os85/rodauth-oauth/pipelines |
90
97
 
@@ -0,0 +1,57 @@
1
+ ## 1.4.0 (08/11/2023)
2
+
3
+ ## Highlights
4
+
5
+ rodauth-oauth is now [OpenID certified](https://openid.net/certification/) for the following logout profiles:
6
+
7
+ * Session Management OP
8
+ * RP-Initiated Logout OP
9
+ * Front-Channel Logout OP
10
+ * Back-Channel Logout OP
11
+
12
+ The OIDC server used to run the test can be found [here](https://gitlab.com/os85/rodauth-oauth/-/blob/master/examples/oidc/authentication_server.rb) and deployed [here](https://rodauth-oauth-oidc.onrender.com).
13
+
14
+ ## Features
15
+
16
+ ### OIDC logout features
17
+
18
+ `rodauth-oauth` ships with the following new features:
19
+
20
+ * `oidc_sesssion_management` - enables [OIDC session management](https://openid.net/specs/openid-connect-session-1_0.html)
21
+ * `oidc_frontchannel_logout` - enables [OIDC frontchannel logout](https://openid.net/specs/openid-connect-frontchannel-1_0.html)
22
+ * `oidc_backchannel_logout` - enables [OIDC backchannel logout](https://openid.net/specs/openid-connect-backchannel-1_0.html)
23
+
24
+ which, along with the existing `oidc_rp_initiated_logout`, implemment all OIDC logout profiles.
25
+
26
+ ## Breaking changes
27
+
28
+ If you're using `oidc`, the dependency on `account_expiration` has been replaced by the `active_sessions` rodauth feature. This change is required because it fixes bugs associated with accounts expiring in order for id token invalidation to work.
29
+
30
+ If you're migrating, it's recommended that you keep depending on `account_expiration` during the transition, add `active_sessions` tables as per [rodauth specs](https://github.com/jeremyevans/rodauth/blob/master/spec/migrate/001_tables.rb#L150), and run them alongside one another for the max period ID tokens should be valid, after which you can remove `account_expiration` and its tables.
31
+
32
+ Some `auth_value_methods` were changed to `auth_methods` everywhere where it made sense. If you were overriding them, you'll have to wrap them in a block:
33
+
34
+ ```ruby
35
+ # in 1.3.2
36
+ oauth_jwt_issuer "http://myissuer.com"
37
+ # in 1.4.0
38
+ oauth_jwt_issuer { "http://myissuer.com" }
39
+ ```
40
+
41
+ ## Improvements
42
+
43
+ ### OAuth SAML Bearer Grant per oauth application settings
44
+
45
+ The `oauth_saml_bearer_grant` feature requires a new table/resource, SAML settings, which enable "per client applicatioon" SAML settings, and therefore, make this feature usable in enterprise/multi-tenancy scenarios.
46
+
47
+ ## Bugfixes
48
+
49
+ * remove `html_safe` usage in rails views to prevent XSS in the authorize form.
50
+ * fixed for OIDC RFC 5.4 when requesting claims using scope values
51
+ * `oauth_rp_initiated_logout` does not crash anymore on logout requests with `id_token_hint`
52
+ * `oauth_rp_initiated_logout` now works with response types other than `code`
53
+ * `oauth_rp_initiated_logout` emits an ID token hint invalid message when not able to decode the `id_token_hint`
54
+
55
+ ## Chore
56
+
57
+ * `oauth_tls_client_auth` is not dependent on the `oauth_jwt` feature, and can therefore be used with non-JWT access tokens, at least with the features which do not require it.
@@ -0,0 +1,20 @@
1
+ # 1.5.0
2
+
3
+ ## Highlights
4
+
5
+ ### OAuth DPoP Support
6
+
7
+ `rodauth-oauth` supports Demonstrating Proof-of-Possession at the Application Layer (also known as DPoP), via the `oauth_dpop` feature. This provides a mechanism to bind access tokens to a particular client based on public key cryptography.
8
+
9
+ More info about the feature [in the docs](https://gitlab.com/os85/rodauth-oauth/-/wikis/DPoP).
10
+
11
+ ## Improvements
12
+
13
+ All features managing cookies are now able to set configure them as "session cookies" (i.e. removed on browser shutdown) by setting the expiration interval auth method to `nil`. This ncludes:
14
+
15
+ * `oauth_prompt_login_interval` (from the `oidc` feature)
16
+ * `oauth_oidc_user_agent_state_cookie_expires_in` (from the `oidc_session_management` feature)
17
+
18
+ ## Bugfixes
19
+
20
+ * when using the `oauth_token_instrospection` feature, the `token_type` has been fixed to show "Bearer" (instead of "access_token").
@@ -4,7 +4,7 @@
4
4
  <% end %>
5
5
  <% application_uri = rodauth.oauth_application[rodauth.oauth_applications_homepage_url_column] %>
6
6
  <% application_name = application_uri ? link_to(rodauth.oauth_application[rodauth.oauth_applications_name_column], application_uri) : rodauth.oauth_application[rodauth.oauth_applications_name_column] %>
7
- <p class="lead"><%= rodauth.authorize_page_lead(name: application_name).html_safe %></p>
7
+ <p class="lead"><%= rodauth.authorize_page_lead(name: application_name) %></p>
8
8
 
9
9
  <div class="list-group">
10
10
  <% if rodauth.oauth_application[rodauth.oauth_applications_tos_uri_column] %>
@@ -37,10 +37,10 @@
37
37
  </div>
38
38
  <% end %>
39
39
  <% end %>
40
- <%= hidden_field_tag :client_id, rodauth.raw_param("client_id") %>
40
+ <%= hidden_field_tag :client_id, rodauth.param_or_nil("client_id") %>
41
41
  <% %w[access_type response_type response_mode state redirect_uri].each do |oauth_param| %>
42
- <% if rodauth.raw_param(oauth_param) %>
43
- <%= hidden_field_tag oauth_param, rodauth.raw_param(oauth_param) %>
42
+ <% if rodauth.param_or_nil(oauth_param) %>
43
+ <%= hidden_field_tag oauth_param, rodauth.param_or_nil(oauth_param) %>
44
44
  <% end %>
45
45
  <% end %>
46
46
  <% if rodauth.features.include?(:oauth_resource_indicators) && rodauth.resource_indicators %>
@@ -49,39 +49,44 @@
49
49
  <% end %>
50
50
  <% end %>
51
51
  <% if rodauth.features.include?(:oauth_pkce) %>
52
- <% if rodauth.raw_param("code_challenge") %>
53
- <%= hidden_field_tag :code_challenge, rodauth.raw_param("code_challenge") %>
52
+ <% if rodauth.param_or_nil("code_challenge") %>
53
+ <%= hidden_field_tag :code_challenge, rodauth.param_or_nil("code_challenge") %>
54
54
  <% end %>
55
- <% if rodauth.raw_param("code_challenge_method") %>
56
- <%= hidden_field_tag :code_challenge_method, rodauth.raw_param("code_challenge_method") %>
55
+ <% if rodauth.param_or_nil("code_challenge_method") %>
56
+ <%= hidden_field_tag :code_challenge_method, rodauth.param_or_nil("code_challenge_method") %>
57
57
  <% end %>
58
58
  <% end %>
59
59
  <% if rodauth.features.include?(:oidc) %>
60
- <% if rodauth.raw_param("prompt") %>
61
- <%= hidden_field_tag :prompt, rodauth.raw_param("prompt") %>
60
+ <% if rodauth.param_or_nil("prompt") %>
61
+ <%= hidden_field_tag :prompt, rodauth.param_or_nil("prompt") %>
62
62
  <% end %>
63
- <% if rodauth.raw_param("nonce") %>
64
- <%= hidden_field_tag :nonce, rodauth.raw_param("nonce") %>
63
+ <% if rodauth.param_or_nil("nonce") %>
64
+ <%= hidden_field_tag :nonce, rodauth.param_or_nil("nonce") %>
65
65
  <% end %>
66
- <% if rodauth.raw_param("ui_locales") %>
67
- <%= hidden_field_tag :ui_locales, rodauth.raw_param("ui_locales") %>
66
+ <% if rodauth.param_or_nil("ui_locales") %>
67
+ <%= hidden_field_tag :ui_locales, rodauth.param_or_nil("ui_locales") %>
68
68
  <% end %>
69
- <% if rodauth.raw_param("claims_locales") %>
70
- <%= hidden_field_tag :claims_locales, rodauth.raw_param("claims_locales") %>
69
+ <% if rodauth.param_or_nil("claims_locales") %>
70
+ <%= hidden_field_tag :claims_locales, rodauth.param_or_nil("claims_locales") %>
71
71
  <% end %>
72
- <% if rodauth.raw_param("claims") %>
73
- <%= hidden_field_tag :claims, sanitize(rodauth.raw_param("claims")) %>
72
+ <% if rodauth.param_or_nil("claims") %>
73
+ <%= hidden_field_tag :claims, sanitize(rodauth.param_or_nil("claims")) %>
74
74
  <% end %>
75
- <% if rodauth.raw_param("acr_values") %>
76
- <%= hidden_field_tag :acr_values, rodauth.raw_param("acr_values") %>
75
+ <% if rodauth.param_or_nil("acr_values") %>
76
+ <%= hidden_field_tag :acr_values, rodauth.param_or_nil("acr_values") %>
77
77
  <% end %>
78
- <% if rodauth.raw_param("registration") %>
79
- <%= hidden_field_tag :registration, rodauth.raw_param("registration") %>
78
+ <% if rodauth.param_or_nil("registration") %>
79
+ <%= hidden_field_tag :registration, rodauth.param_or_nil("registration") %>
80
+ <% end %>
81
+ <% end %>
82
+ <% if rodauth.features.include?(:oidc) %>
83
+ <% if rodauth.param_or_nil("dpop_jkt") %>
84
+ <%= hidden_field_tag :dpop_jkt, rodauth.param_or_nil("dpop_jkt") %>
80
85
  <% end %>
81
86
  <% end %>
82
87
  </div>
83
88
  <p class="text-center">
84
89
  <%= submit_tag rodauth.oauth_authorize_button, class: "btn btn-outline-primary" %>
85
- <%= link_to rodauth.oauth_cancel_button, "#{rodauth.redirect_uri}?error=access_denied&error_description=The+resource+owner+or+authorization+server+denied+the+request#{"&state=\#{CGI.escape(rodauth.state)}" if rodauth.raw_param("state") }", class: "btn btn-outline-danger" %>
90
+ <%= link_to rodauth.oauth_cancel_button, "#{rodauth.redirect_uri}?error=access_denied&error_description=The+resource+owner+or+authorization+server+denied+the+request#{"&state=\#{CGI.escape(rodauth.state)}" if rodauth.param_or_nil("state") }", class: "btn btn-outline-danger" %>
86
91
  </p>
87
92
  <% end %>
@@ -0,0 +1,10 @@
1
+ <div class="mb-3">
2
+ <h1><%= rodauth.oauth_frontchannel_logout_redirecting_lead %></h1>
3
+ <p>
4
+ <%= rodauth.oauth_frontchannel_logout_redirecting_label(link: link_to(rodauth.oauth_frontchannel_logout_redirecting_link_label, rodauth.logout_redirect)) %>
5
+ </p>
6
+ <% rodauth.frontchannel_logout_urls.each do |logout_url| %>
7
+ <iframe src="<%= logout_url %>"></iframe>
8
+ <% end %>
9
+ </div>
10
+ <meta http-equiv="refresh" content="5; URL=<%= rodauth.logout_redirect %>" />
@@ -26,5 +26,5 @@
26
26
  <% end %>
27
27
  </tbody>
28
28
  </table>
29
- <%= rodauth.oauth_management_pagination_links(oauth_applications_ds).html_safe %>
29
+ <%= rodauth.oauth_management_pagination_links(oauth_applications_ds) %>
30
30
  <% end %>
@@ -49,6 +49,9 @@ class CreateRodauthOauth < ActiveRecord::Migration<%= migration_version %>
49
49
  t.boolean :require_signed_request_object, null: true
50
50
  t.boolean :require_pushed_authorization_requests, null: false, default: false
51
51
 
52
+ # :oauth_dpop
53
+ t.string :dpop_bound_access_tokens, null: true
54
+
52
55
  # :oauth_tls_client_auth
53
56
  t.string :tls_client_auth_subject_dn, null: true
54
57
  t.string :tls_client_auth_san_dns, null: true
@@ -59,6 +62,14 @@ class CreateRodauthOauth < ActiveRecord::Migration<%= migration_version %>
59
62
 
60
63
  # :oidc_rp_initiated_logout enabled
61
64
  t.string :post_logout_redirect_uris, null: false
65
+
66
+ # frontchannel logout
67
+ t.string :frontchannel_logout_uri
68
+ t.boolean :frontchannel_logout_session_required, default: false
69
+
70
+ # backchannel logout
71
+ t.string :backchannel_logout_uri
72
+ t.boolean :backchannel_logout_session_required, default: false
62
73
  end
63
74
 
64
75
  create_table :oauth_grants do |t|
@@ -78,6 +89,9 @@ class CreateRodauthOauth < ActiveRecord::Migration<%= migration_version %>
78
89
  t.datetime :created_at, null: false, default: -> { "CURRENT_TIMESTAMP(6)" }
79
90
  t.string :access_type, null: false, default: "offline"
80
91
 
92
+ # :oauth_dpop enabled
93
+ t.string :dpop_jwk, null: true
94
+
81
95
  # :oauth_pkce enabled
82
96
  t.string :code_challenge
83
97
  t.string :code_challenge_method
@@ -97,15 +111,37 @@ class CreateRodauthOauth < ActiveRecord::Migration<%= migration_version %>
97
111
  t.string :acr
98
112
  t.string :claims_locales
99
113
  t.string :claims
114
+
115
+ # :oauth_dpop enabled
116
+ t.string :dpop_jkt
100
117
  end
101
118
 
102
119
  create_table :oauth_pushed_requests do |t|
103
120
  t.bigint :oauth_application_id
104
121
  t.foreign_key :oauth_applications, column: :oauth_application_id
105
122
  t.string :code, null: false, index: { unique: true }
123
+ t.index %i[oauth_application_id code], unique: true
106
124
  t.string :params, null: false
107
125
  t.datetime :expires_in, null: false
108
- t.index %i[oauth_application_id code], unique: true
126
+ # :oauth_dpop
127
+ t.string :dpop_jkt
128
+ end
129
+
130
+ create_table :oauth_saml_settings do |t|
131
+ t.bigint :oauth_application_id
132
+ t.foreign_key :oauth_applications, column: :oauth_application_id
133
+ t.text :idp_cert, null: true
134
+ t.text :idp_cert_fingerprint, null: true
135
+ t.string :idp_cert_fingerprint_algorithm, null: true
136
+ t.boolean :check_idp_cert_expiration, null: true
137
+ t.text :name_identifier_format, null: true
138
+ t.string :audience, null: true
139
+ t.string :issuer, null: false, unique: true
140
+ end
141
+
142
+ create_table :oauth_dpop_proofs, primary_key: :jti do |t|
143
+ t.string :jti, null: false
144
+ t.datetime :first_use, null: false, default: -> { "CURRENT_TIMESTAMP(6)" }
109
145
  end
110
146
  end
111
147
  end
@@ -20,11 +20,11 @@ module Rodauth::OAuth
20
20
  }.freeze
21
21
 
22
22
  class_option :features, type: :array,
23
- desc: "Roda OAuth features to generate views for (oauth_applications etc.)",
23
+ desc: "Rodauth OAuth features to generate views for (oauth_applications etc.)",
24
24
  default: DEFAULT
25
25
 
26
26
  class_option :all, aliases: "-a", type: :boolean,
27
- desc: "Generates views for all Roda OAuth features",
27
+ desc: "Generates views for all Rodauth OAuth features",
28
28
  default: false
29
29
 
30
30
  class_option :directory, aliases: "-d", type: :string,
@@ -57,7 +57,7 @@ module Rodauth
57
57
  translatable_method :oauth_no_applications_text, "No oauth applications yet!"
58
58
  translatable_method :oauth_no_grants_text, "No oauth grants yet!"
59
59
 
60
- auth_value_methods(
60
+ auth_methods(
61
61
  :oauth_application_path
62
62
  )
63
63
 
@@ -6,7 +6,7 @@ module Rodauth
6
6
  Feature.define(:oauth_assertion_base, :OauthAssertionBase) do
7
7
  depends :oauth_base
8
8
 
9
- auth_value_methods(
9
+ auth_methods(
10
10
  :assertion_grant_type?,
11
11
  :client_assertion_type?,
12
12
  :assertion_grant_type,
@@ -28,7 +28,7 @@ module Rodauth
28
28
  translatable_method :oauth_unsupported_response_type_message, "Unsupported response type"
29
29
  translatable_method :oauth_authorize_parameter_required, "Invalid or missing '%<parameter>s'"
30
30
 
31
- auth_value_methods(
31
+ auth_methods(
32
32
  :resource_owner_params,
33
33
  :oauth_grants_resource_owner_columns
34
34
  )
@@ -109,13 +109,16 @@ module Rodauth
109
109
  auth_value_method :oauth_metadata_op_tos_uri, nil
110
110
 
111
111
  auth_value_methods(
112
+ :authorization_server_url,
113
+ :oauth_grants_unique_columns
114
+ )
115
+
116
+ auth_methods(
112
117
  :fetch_access_token,
113
118
  :secret_hash,
114
119
  :generate_token_hash,
115
120
  :secret_matches?,
116
- :authorization_server_url,
117
121
  :oauth_unique_id_generator,
118
- :oauth_grants_unique_columns,
119
122
  :require_authorizable_account,
120
123
  :oauth_account_ds,
121
124
  :oauth_application_ds
@@ -234,16 +237,22 @@ module Rodauth
234
237
  return
235
238
  end
236
239
  else
237
- value = request.env["HTTP_AUTHORIZATION"]
240
+ token = fetch_access_token_from_authorization_header
241
+ end
238
242
 
239
- return unless value && !value.empty?
243
+ return if token.nil? || token.empty?
240
244
 
241
- scheme, token = value.split(" ", 2)
245
+ token
246
+ end
242
247
 
243
- return unless scheme.downcase == oauth_token_type
244
- end
248
+ def fetch_access_token_from_authorization_header(token_type = oauth_token_type)
249
+ value = request.env["HTTP_AUTHORIZATION"]
245
250
 
246
- return if token.nil? || token.empty?
251
+ return unless value && !value.empty?
252
+
253
+ scheme, token = value.split(" ", 2)
254
+
255
+ return unless scheme.downcase == token_type
247
256
 
248
257
  token
249
258
  end
@@ -350,7 +359,7 @@ module Rodauth
350
359
  # parse client id and secret
351
360
  #
352
361
  def require_oauth_application
353
- @oauth_application = if (token = ((v = request.env["HTTP_AUTHORIZATION"]) && v[/\A *Basic (.*)\Z/, 1]))
362
+ @oauth_application = if (token = (v = request.env["HTTP_AUTHORIZATION"]) && v[/\A *Basic (.*)\Z/, 1])
354
363
  # client_secret_basic
355
364
  require_oauth_application_from_client_secret_basic(token)
356
365
  elsif (client_id = param_or_nil("client_id"))
@@ -753,7 +762,7 @@ module Rodauth
753
762
  }
754
763
  end
755
764
 
756
- def redirect_response_error(error_code, redirect_url = redirect_uri || request.referer || default_redirect)
765
+ def redirect_response_error(error_code, message = nil)
757
766
  if accepts_json?
758
767
  status_code = if respond_to?(:"oauth_#{error_code}_response_status")
759
768
  send(:"oauth_#{error_code}_response_status")
@@ -761,37 +770,41 @@ module Rodauth
761
770
  oauth_invalid_response_status
762
771
  end
763
772
 
764
- throw_json_response_error(status_code, error_code)
773
+ throw_json_response_error(status_code, error_code, message)
765
774
  else
775
+ redirect_url = redirect_uri || request.referer || default_redirect
766
776
  redirect_url = URI.parse(redirect_url)
767
- params = []
768
-
769
- params << if respond_to?(:"oauth_#{error_code}_error_code")
770
- ["error", send(:"oauth_#{error_code}_error_code")]
771
- else
772
- ["error", error_code]
773
- end
774
-
775
- if respond_to?(:"oauth_#{error_code}_message")
776
- message = send(:"oauth_#{error_code}_message")
777
- params << ["error_description", CGI.escape(message)]
778
- end
779
-
777
+ params = response_error_params(error_code, message)
780
778
  state = param_or_nil("state")
781
-
782
- params << ["state", state] if state
783
-
779
+ params["state"] = state if state
784
780
  _redirect_response_error(redirect_url, params)
785
781
  end
786
782
  end
787
783
 
788
784
  def _redirect_response_error(redirect_url, params)
789
- params = params.map { |k, v| "#{k}=#{v}" }
790
- params << redirect_url.query if redirect_url.query
791
- redirect_url.query = params.join("&")
785
+ params = URI.encode_www_form(params)
786
+ if redirect_url.query
787
+ params << "&" unless params.empty?
788
+ params << redirect_url.query
789
+ end
790
+ redirect_url.query = params
792
791
  redirect(redirect_url.to_s)
793
792
  end
794
793
 
794
+ def response_error_params(error_code, message = nil)
795
+ code = if respond_to?(:"oauth_#{error_code}_error_code")
796
+ send(:"oauth_#{error_code}_error_code")
797
+ else
798
+ error_code
799
+ end
800
+ payload = { "error" => code }
801
+ error_description = message
802
+ error_description ||= send(:"oauth_#{error_code}_message") if respond_to?(:"oauth_#{error_code}_message")
803
+ payload["error_description"] = error_description if error_description
804
+
805
+ payload
806
+ end
807
+
795
808
  def json_response_success(body, cache = false)
796
809
  response.status = 200
797
810
  response["Content-Type"] ||= json_response_content_type
@@ -809,19 +822,17 @@ module Rodauth
809
822
 
810
823
  def throw_json_response_error(status, error_code, message = nil)
811
824
  set_response_error_status(status)
812
- code = if respond_to?(:"oauth_#{error_code}_error_code")
813
- send(:"oauth_#{error_code}_error_code")
814
- else
815
- error_code
816
- end
817
- payload = { "error" => code }
818
- payload["error_description"] = message || (send(:"oauth_#{error_code}_message") if respond_to?(:"oauth_#{error_code}_message"))
825
+ payload = response_error_params(error_code, message)
819
826
  json_payload = _json_response_body(payload)
820
827
  response["Content-Type"] ||= json_response_content_type
821
- response["WWW-Authenticate"] = oauth_token_type.upcase if status == 401
828
+ response["WWW-Authenticate"] = www_authenticate_header(payload) if status == 401
822
829
  return_response(json_payload)
823
830
  end
824
831
 
832
+ def www_authenticate_header(*)
833
+ oauth_token_type.capitalize
834
+ end
835
+
825
836
  def _json_response_body(hash)
826
837
  return super if features.include?(:json)
827
838
 
@@ -35,7 +35,7 @@ module Rodauth
35
35
  end
36
36
  translatable_method :oauth_grant_user_code_label, "User code"
37
37
 
38
- auth_value_methods(
38
+ auth_methods(
39
39
  :generate_user_code
40
40
  )
41
41
 
@@ -173,7 +173,7 @@ module Rodauth
173
173
  oauth_grants_user_code_column => param("user_code")
174
174
  ).update(oauth_grants_user_code_column => nil, oauth_grants_type_column => "device_code")
175
175
 
176
- return unless rs.positive?
176
+ rs if rs.positive?
177
177
  else
178
178
  super
179
179
  end