token_authority 0.1.0 → 0.2.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.
Files changed (61) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +23 -0
  3. data/README.md +199 -7
  4. data/app/controllers/concerns/token_authority/client_authentication.rb +141 -0
  5. data/app/controllers/concerns/token_authority/controller_event_logging.rb +98 -0
  6. data/app/controllers/concerns/token_authority/initial_access_token_authentication.rb +35 -0
  7. data/app/controllers/concerns/token_authority/token_authentication.rb +128 -0
  8. data/app/controllers/token_authority/authorization_grants_controller.rb +119 -0
  9. data/app/controllers/token_authority/authorizations_controller.rb +105 -0
  10. data/app/controllers/token_authority/clients_controller.rb +99 -0
  11. data/app/controllers/token_authority/metadata_controller.rb +12 -0
  12. data/app/controllers/token_authority/resource_metadata_controller.rb +12 -0
  13. data/app/controllers/token_authority/sessions_controller.rb +228 -0
  14. data/app/helpers/token_authority/authorization_grants_helper.rb +27 -0
  15. data/app/models/concerns/token_authority/claim_validatable.rb +95 -0
  16. data/app/models/concerns/token_authority/event_logging.rb +144 -0
  17. data/app/models/concerns/token_authority/resourceable.rb +111 -0
  18. data/app/models/concerns/token_authority/scopeable.rb +105 -0
  19. data/app/models/concerns/token_authority/session_creatable.rb +101 -0
  20. data/app/models/token_authority/access_token.rb +127 -0
  21. data/app/models/token_authority/access_token_request.rb +193 -0
  22. data/app/models/token_authority/authorization_grant.rb +119 -0
  23. data/app/models/token_authority/authorization_request.rb +276 -0
  24. data/app/models/token_authority/authorization_server_metadata.rb +101 -0
  25. data/app/models/token_authority/client.rb +263 -0
  26. data/app/models/token_authority/client_id_resolver.rb +114 -0
  27. data/app/models/token_authority/client_metadata_document.rb +164 -0
  28. data/app/models/token_authority/client_metadata_document_cache.rb +33 -0
  29. data/app/models/token_authority/client_metadata_document_fetcher.rb +266 -0
  30. data/app/models/token_authority/client_registration_request.rb +214 -0
  31. data/app/models/token_authority/client_registration_response.rb +58 -0
  32. data/app/models/token_authority/jwks_cache.rb +37 -0
  33. data/app/models/token_authority/jwks_fetcher.rb +70 -0
  34. data/app/models/token_authority/protected_resource_metadata.rb +74 -0
  35. data/app/models/token_authority/refresh_token.rb +110 -0
  36. data/app/models/token_authority/refresh_token_request.rb +116 -0
  37. data/app/models/token_authority/session.rb +193 -0
  38. data/app/models/token_authority/software_statement.rb +70 -0
  39. data/app/views/token_authority/authorization_grants/new.html.erb +25 -0
  40. data/app/views/token_authority/client_error.html.erb +8 -0
  41. data/config/locales/token_authority.en.yml +248 -0
  42. data/config/routes.rb +29 -0
  43. data/lib/generators/token_authority/install/install_generator.rb +61 -0
  44. data/lib/generators/token_authority/install/templates/create_token_authority_tables.rb.erb +116 -0
  45. data/lib/generators/token_authority/install/templates/token_authority.rb +247 -0
  46. data/lib/token_authority/configuration.rb +397 -0
  47. data/lib/token_authority/engine.rb +34 -0
  48. data/lib/token_authority/errors.rb +221 -0
  49. data/lib/token_authority/instrumentation.rb +80 -0
  50. data/lib/token_authority/instrumentation_log_subscriber.rb +62 -0
  51. data/lib/token_authority/json_web_token.rb +78 -0
  52. data/lib/token_authority/log_event_subscriber.rb +43 -0
  53. data/lib/token_authority/routing/constraints.rb +71 -0
  54. data/lib/token_authority/routing/routes.rb +39 -0
  55. data/lib/token_authority/version.rb +4 -1
  56. data/lib/token_authority.rb +30 -1
  57. metadata +65 -5
  58. data/app/assets/stylesheets/token_authority/application.css +0 -15
  59. data/app/controllers/token_authority/application_controller.rb +0 -4
  60. data/app/helpers/token_authority/application_helper.rb +0 -4
  61. data/app/views/layouts/token_authority/application.html.erb +0 -17
@@ -0,0 +1,248 @@
1
+ en:
2
+ activerecord:
3
+ models:
4
+ token_authority/authorization_grant: 'Authorization grant'
5
+ token_authority/challenge: 'Challenge'
6
+ token_authority/client: 'Client'
7
+ token_authority/session: 'Session'
8
+ attributes:
9
+ token_authority/authorization_grant:
10
+ token_authority_client_id: 'TokenAuthority client ID'
11
+ client_id_url: 'Client ID URL'
12
+ redeemed: 'Redeemed'
13
+ user_id: 'User ID'
14
+ token_authority/challenge:
15
+ code_challenge: 'Code challenge'
16
+ code_challenge_method: 'Code challenge method'
17
+ token_authority_authorization_grant_id: 'TokenAuthority authorization grant ID'
18
+ redirect_uri: 'Redirect URI'
19
+ token_authority/client:
20
+ access_token_duration: 'Access token duration'
21
+ client_secret_id: 'Client secret ID'
22
+ client_type: 'Client type'
23
+ name: 'Name'
24
+ redirect_uris: 'Redirect URIs'
25
+ refresh_token_duration: 'Refresh token duration'
26
+ token_endpoint_auth_method: 'Token endpoint auth method'
27
+ grant_types: 'Grant types'
28
+ response_types: 'Response types'
29
+ scope: 'Scope'
30
+ client_uri: 'Client URI'
31
+ logo_uri: 'Logo URI'
32
+ tos_uri: 'Terms of service URI'
33
+ policy_uri: 'Policy URI'
34
+ contacts: 'Contacts'
35
+ jwks_uri: 'JWKS URI'
36
+ jwks: 'JWKS'
37
+ software_id: 'Software ID'
38
+ software_version: 'Software version'
39
+ software_statement: 'Software statement'
40
+ client_id_issued_at: 'Client ID issued at'
41
+ client_secret_expires_at: 'Client secret expires at'
42
+ dynamically_registered: 'Dynamically registered'
43
+ token_authority/session:
44
+ access_token_jti: 'Access token JTI'
45
+ client_id: 'Client ID'
46
+ expires_at: 'Expires at'
47
+ token_authority_authorization_grant_id: 'TokenAuthority authorization grant ID'
48
+ status: 'Status'
49
+ refresh_token_jti: 'Refresh token JTI'
50
+ user_id: 'User ID'
51
+ errors:
52
+ models:
53
+ token_authority/authorization_grant:
54
+ attributes:
55
+ token_authority_client_id:
56
+ blank: 'must be linked to a client'
57
+ user_id:
58
+ blank: 'must be linked to a user'
59
+ base:
60
+ must_have_client_identifier: 'must have either a registered client or a client ID URL'
61
+ token_authority/challenge:
62
+ attributes:
63
+ code_challenge:
64
+ blank: 'is required for PKCE'
65
+ failed_challenge: 'failed validation'
66
+ requires_code_verifier: 'must be validated against a code_verifier'
67
+ code_challenge_method:
68
+ blank: 'is required for PKCE'
69
+ invalid: 'must be a supported code challenge method'
70
+ token_authority_authorization_grant_id:
71
+ blank: 'must be linked to an authorization grant'
72
+ redirect_uri:
73
+ blank: 'must be provided if sent in the original authorization request'
74
+ invalid: 'must be a supported code challenge method'
75
+ token_authority/client:
76
+ attributes:
77
+ redirect_uris:
78
+ invalid_http_scheme: 'must contain valid HTTP(S) scheme in all URIs'
79
+ invalid_uri: 'must contain valid URIs'
80
+ base:
81
+ jwks_required_for_private_key_jwt: 'JWKS or JWKS URI is required when using private_key_jwt authentication'
82
+ token_authority/session:
83
+ access_token_jti:
84
+ invalid: 'must be a valid UUID'
85
+ client_id:
86
+ blank: 'must be linked to a client'
87
+ token_authority_authorization_grant_id:
88
+ blank: 'must be linked to an authorization grant'
89
+ refresh_token_jti:
90
+ invalid: 'must be a valid UUID'
91
+ user_id:
92
+ blank: 'must be linked to a user'
93
+ activemodel:
94
+ models:
95
+ token_authority/access_token: 'Access token'
96
+ token_authority/access_token_request: 'Access token request'
97
+ token_authority/authorization_request: 'Authorization request'
98
+ token_authority/refresh_token: 'Refresh token'
99
+ attributes:
100
+ token_authority/access_token:
101
+ aud: 'Audience (aud)'
102
+ exp: 'Expires at (exp)'
103
+ iat: 'Issued at (iat)'
104
+ iss: 'Issuer (iss)'
105
+ jti: 'JTI (jti)'
106
+ user_id: 'User ID'
107
+ token_authority/access_token_request:
108
+ code_verifier: 'Code verifier'
109
+ token_authority_authorization_grant: 'TokenAuthority authorization grant'
110
+ redirect_uri: 'Redirect URI'
111
+ token_authority/authorization_request:
112
+ client_id: 'Client ID'
113
+ code_challenge: 'Code challenge'
114
+ code_challenge_method: 'Code challenge method'
115
+ token_authority_client: 'TokenAuthority client'
116
+ redirect_uri: 'Redirect URI'
117
+ response_type: 'Response type'
118
+ state: 'State'
119
+ resources: 'Resources'
120
+ scope: 'Scope'
121
+ token_authority/access_token_request:
122
+ resources: 'Resources'
123
+ scope: 'Scope'
124
+ token_authority/refresh_token:
125
+ aud: 'Audience (aud)'
126
+ exp: 'Expires at (exp)'
127
+ iat: 'Issued at (iat)'
128
+ iss: 'Issuer (iss)'
129
+ jti: 'JTI (jti)'
130
+ token_authority/refresh_token_request:
131
+ token: 'Token'
132
+ resources: 'Resources'
133
+ client_id: 'Client ID'
134
+ scope: 'Scope'
135
+ errors:
136
+ models:
137
+ token_authority/access_token:
138
+ attributes:
139
+ aud:
140
+ blank: 'claim not present in token'
141
+ invalid: 'claim does not match server configuration'
142
+ exp:
143
+ blank: 'claim not present in token'
144
+ expired: 'is in the past'
145
+ iss:
146
+ blank: 'claim not present in token'
147
+ invalid: 'claim does not match server configuration'
148
+ jti:
149
+ blank: 'claim not present in token'
150
+ user_id:
151
+ blank: 'claim not present in token'
152
+ token_authority/access_token_request:
153
+ attributes:
154
+ code_verifier:
155
+ blank: 'must be provided as a param in the access token request'
156
+ does_not_validate_code_challenge: 'does not vaidate code_challenge from the authorize request'
157
+ present_in_authorize: 'must be provided if sent in the original authorization request'
158
+ token_authority_authorization_grant:
159
+ invalid: 'must be initialized with an authorization grant'
160
+ redirect_uri:
161
+ blank: 'must be provided as a param in the access token request'
162
+ mismatched: 'does not match the redirect_uri from the authorize request'
163
+ present_in_authorize: 'must be provided if sent in the original authorization request'
164
+ resources:
165
+ invalid_uri: 'must contain valid absolute URIs with http/https scheme and no fragments'
166
+ not_allowed: 'must only contain allowed resource URIs'
167
+ not_subset: 'must be a subset of the originally granted resources'
168
+ scope:
169
+ invalid: 'contains invalid scope tokens'
170
+ not_allowed: 'must only contain allowed scopes'
171
+ not_subset: 'must be a subset of the originally granted scopes'
172
+ token_authority/authorization_request:
173
+ attributes:
174
+ client_id:
175
+ blank: 'must be provided as a param in the request to authorize when client is public'
176
+ mismatched: 'does not match the client ID URL'
177
+ unregistered_client: 'does not map to a registered client'
178
+ code_challenge:
179
+ required_if_other_pkce_params_present: 'must be provided as a param in the request to authorize if other PKCE params were sent'
180
+ required_for_public_clients: 'must be provided as a param in the request to authorize when client is public'
181
+ code_challenge_method:
182
+ invalid: 'must be a supported code challenge method'
183
+ required_if_other_pkce_params_present: 'must be provided as a param in the request to authorize if other PKCE params were sent'
184
+ required_for_public_clients: 'must be provided as a param in the request to authorize when client is public'
185
+ token_authority_client:
186
+ invalid: 'must be initiated by a client'
187
+ redirect_uri:
188
+ blank: 'must be provided as a param in the request to authorize when client is public'
189
+ invalid: 'must match redirect_uri configured for client'
190
+ response_type:
191
+ blank: 'must be provided as a param in the request to authorize'
192
+ invalid: 'must be a supported response type'
193
+ state:
194
+ resources:
195
+ required: 'must be provided when resource indicators are required'
196
+ invalid_uri: 'must contain valid absolute URIs with http/https scheme and no fragments'
197
+ not_allowed: 'must only contain allowed resource URIs'
198
+ not_subset: 'must be a subset of the originally granted resources'
199
+ scope:
200
+ required: 'must be provided when scopes are required'
201
+ invalid: 'contains invalid scope tokens'
202
+ not_allowed: 'must only contain allowed scopes'
203
+ token_authority/refresh_token:
204
+ attributes:
205
+ aud:
206
+ blank: 'claim not present in token'
207
+ invalid: 'claim does not match server configuration'
208
+ exp:
209
+ blank: 'claim not present in token'
210
+ expired: 'is in the past'
211
+ iss:
212
+ blank: 'claim not present in token'
213
+ invalid: 'claim does not match server configuration'
214
+ jti:
215
+ blank: 'claim not present in token'
216
+ token_authority/refresh_token_request:
217
+ attributes:
218
+ token:
219
+ blank: 'must be provided as a param in the refresh token request'
220
+ session_not_found: 'does not map to a valid session'
221
+ resources:
222
+ invalid_uri: 'must contain valid absolute URIs with http/https scheme and no fragments'
223
+ not_allowed: 'must only contain allowed resource URIs'
224
+ not_subset: 'must be a subset of the originally granted resources'
225
+ scope:
226
+ invalid: 'contains invalid scope tokens'
227
+ not_allowed: 'must only contain allowed scopes'
228
+ not_subset: 'must be a subset of the originally granted scopes'
229
+ token_authority:
230
+ authorization_grants:
231
+ new:
232
+ title: 'Authorize this Client'
233
+ lede: 'Confirm that you wish to grant access to this client.'
234
+ resources_heading: 'This application is requesting access to:'
235
+ scopes_heading: 'This application is requesting the following permissions:'
236
+ approve_cta: 'Approve'
237
+ reject_cta: 'Reject'
238
+ client_error:
239
+ title: 'Client Error'
240
+ lede: 'The request provided by the client to this server is malformed. Please report this error through the client support channel.'
241
+ errors:
242
+ invalid_grant: 'The authorization grant is invalid'
243
+ mismatched_refresh_token: 'The provided refresh token JTI does not match the refresh token JTI of the target OAuthSession.'
244
+ oauth_session_failure: 'Failed to create OAuthSession. Errors: %{errors}'
245
+ revoked_session: 'Refresh token replay attack detected. Refreshed OAuthSession: %{refreshed_session_id}, Revoked OAuthSession: %{revoked_session_id}, Client ID: %{client_id}, User ID: %{user_id}'
246
+ missing_auth_header: 'Authorization header is missing or empty'
247
+ invalid_token: 'The access token is invalid or malformed'
248
+ unauthorized_token: 'The access token is expired or unauthorized'
data/config/routes.rb CHANGED
@@ -1,2 +1,31 @@
1
1
  TokenAuthority::Engine.routes.draw do
2
+ get "authorize", to: "authorizations#authorize"
3
+ resources :authorization_grants, path: "authorization-grants", only: %i[new create]
4
+
5
+ # Dynamic Client Registration (RFC 7591)
6
+ constraints(TokenAuthority::Routing::DynamicRegistrationEnabledConstraint.new) do
7
+ post "register", to: "clients#create"
8
+ end
9
+
10
+ # Token endpoint with grant_type constraints
11
+ constraints(TokenAuthority::Routing::GrantTypeConstraint.new("refresh_token")) do
12
+ post "token", to: "sessions#refresh", as: :refresh_session
13
+ end
14
+
15
+ constraints(TokenAuthority::Routing::GrantTypeConstraint.new("authorization_code")) do
16
+ post "token", to: "sessions#token", as: :create_session
17
+ end
18
+
19
+ post "token", to: "sessions#unsupported_grant_type", as: :unsupported_grant_type
20
+
21
+ # Revoke endpoint with token_type_hint constraints
22
+ constraints(TokenAuthority::Routing::TokenTypeHintConstraint.new("access_token")) do
23
+ post "revoke", to: "sessions#revoke_access_token", as: :revoke_access_token
24
+ end
25
+
26
+ constraints(TokenAuthority::Routing::TokenTypeHintConstraint.new("refresh_token")) do
27
+ post "revoke", to: "sessions#revoke_refresh_token", as: :revoke_refresh_token
28
+ end
29
+
30
+ post "revoke", to: "sessions#revoke"
2
31
  end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+ require "rails/generators/active_record"
5
+
6
+ module TokenAuthority
7
+ module Generators
8
+ class InstallGenerator < Rails::Generators::Base
9
+ include ActiveRecord::Generators::Migration
10
+
11
+ source_root File.expand_path("templates", __dir__)
12
+
13
+ class_option :user_table_name,
14
+ type: :string,
15
+ default: "users",
16
+ desc: "Name of the user table in your application"
17
+
18
+ class_option :user_foreign_key_type,
19
+ type: :string,
20
+ default: "bigint",
21
+ desc: "Type of the user table's primary key (bigint, uuid, integer)"
22
+
23
+ def create_migration_file
24
+ migration_template(
25
+ "create_token_authority_tables.rb.erb",
26
+ "db/migrate/create_token_authority_tables.rb"
27
+ )
28
+ end
29
+
30
+ def create_initializer_file
31
+ template "token_authority.rb", "config/initializers/token_authority.rb"
32
+ end
33
+
34
+ def copy_views
35
+ directory engine_views_path, "app/views/token_authority"
36
+ end
37
+
38
+ def add_routes
39
+ route "token_authority_routes"
40
+ end
41
+
42
+ private
43
+
44
+ def engine_views_path
45
+ File.expand_path("../../../../app/views/token_authority", __dir__)
46
+ end
47
+
48
+ def user_table_name
49
+ options[:user_table_name]
50
+ end
51
+
52
+ def user_foreign_key_type
53
+ options[:user_foreign_key_type]
54
+ end
55
+
56
+ def migration_version
57
+ "[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]"
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,116 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateTokenAuthorityTables < ActiveRecord::Migration<%= migration_version %>
4
+ def change
5
+ # OAuth Clients - registered applications that can request tokens
6
+ create_table :token_authority_clients do |t|
7
+ t.string :public_id, null: false # Public-facing identifier (UUID)
8
+ t.string :name, null: false
9
+ t.string :client_type, null: false, default: "confidential" # "confidential" or "public"
10
+ t.json :redirect_uris, null: false # Array of redirect URIs
11
+ t.bigint :access_token_duration, null: false
12
+ t.bigint :refresh_token_duration, null: false
13
+ t.string :client_secret_id # Used for HMAC-based secret generation (confidential clients only)
14
+
15
+ # RFC 7591 client metadata
16
+ t.string :token_endpoint_auth_method, null: false, default: "client_secret_basic"
17
+ t.json :grant_types # Array, default: ["authorization_code"]
18
+ t.json :response_types # Array, default: ["code"]
19
+ t.string :scope # space-delimited scope string
20
+
21
+ # Human-readable metadata
22
+ t.string :client_uri
23
+ t.string :logo_uri
24
+ t.string :tos_uri
25
+ t.string :policy_uri
26
+ t.json :contacts # Array of email addresses
27
+
28
+ # Technical metadata (for JWT auth methods)
29
+ t.string :jwks_uri
30
+ t.json :jwks # JWKS object (inline)
31
+ t.string :software_id
32
+ t.string :software_version
33
+ t.text :software_statement # Original software statement JWT (if provided)
34
+
35
+ # Registration metadata
36
+ t.datetime :client_id_issued_at
37
+ t.datetime :client_secret_expires_at
38
+ t.boolean :dynamically_registered, null: false, default: false
39
+
40
+ t.timestamps
41
+ end
42
+
43
+ add_index :token_authority_clients, :public_id, unique: true
44
+ add_index :token_authority_clients, :client_secret_id, unique: true
45
+ add_index :token_authority_clients, :software_id
46
+
47
+ # Authorization Grants - authorization codes issued during OAuth flow
48
+ create_table :token_authority_authorization_grants do |t|
49
+ t.string :public_id, null: false # Public-facing identifier (UUID) - this is the authorization code
50
+ t.datetime :expires_at, null: false
51
+ t.boolean :redeemed, null: false, default: false
52
+ t.references :token_authority_client,
53
+ null: true, # Nullable for URL-based clients (Client Metadata Document)
54
+ foreign_key: { to_table: :token_authority_clients },
55
+ index: { name: "index_ta_auth_grants_on_client_id" }
56
+ t.string :client_id_url # URL for URL-based client IDs (Client Metadata Document)
57
+ t.<%= user_foreign_key_type %> :user_id
58
+
59
+ # PKCE parameters
60
+ t.string :code_challenge
61
+ t.string :code_challenge_method, default: "S256"
62
+ t.string :redirect_uri
63
+ t.json :resources, null: false, default: [] # RFC 8707 resource indicators
64
+ t.json :scopes, null: false, default: [] # OAuth scopes (RFC 6749 Section 3.3)
65
+
66
+ t.timestamps
67
+ end
68
+
69
+ add_index :token_authority_authorization_grants, :public_id, unique: true
70
+ add_index :token_authority_authorization_grants, :user_id, name: "index_ta_auth_grants_on_user_id"
71
+ add_index :token_authority_authorization_grants, :expires_at, name: "index_ta_auth_grants_on_expires_at"
72
+ add_foreign_key :token_authority_authorization_grants, :<%= user_table_name %>, column: :user_id
73
+
74
+ # OAuth Sessions - tracks issued tokens and their status
75
+ create_table :token_authority_sessions do |t|
76
+ t.string :access_token_jti, null: false
77
+ t.string :refresh_token_jti, null: false
78
+ t.string :status, null: false, default: "created" # "created", "expired", "refreshed", "revoked"
79
+ t.references :token_authority_authorization_grant,
80
+ null: true,
81
+ foreign_key: { to_table: :token_authority_authorization_grants },
82
+ index: { name: "index_ta_sessions_on_auth_grant_id" }
83
+
84
+ t.timestamps
85
+ end
86
+
87
+ add_index :token_authority_sessions, :access_token_jti, unique: true
88
+ add_index :token_authority_sessions, :refresh_token_jti, unique: true
89
+
90
+ # JWKS Cache - stores fetched JWKS from remote URIs
91
+ create_table :token_authority_jwks_caches do |t|
92
+ t.string :uri_hash, null: false # SHA256 hash of the URI for lookups
93
+ t.string :uri, null: false # Original URI for debugging/display
94
+ t.json :jwks, null: false # The cached JWKS data
95
+ t.datetime :expires_at, null: false # When this cache entry expires
96
+
97
+ t.timestamps
98
+ end
99
+
100
+ add_index :token_authority_jwks_caches, :uri_hash, unique: true
101
+ add_index :token_authority_jwks_caches, :expires_at
102
+
103
+ # Client Metadata Document Cache - stores fetched metadata documents
104
+ create_table :token_authority_client_metadata_document_caches do |t|
105
+ t.string :uri_hash, null: false # SHA256 hash of the URI for lookups
106
+ t.string :uri, null: false # Original URI for debugging/display
107
+ t.json :metadata, null: false # The cached metadata document
108
+ t.datetime :expires_at, null: false # When this cache entry expires
109
+
110
+ t.timestamps
111
+ end
112
+
113
+ add_index :token_authority_client_metadata_document_caches, :uri_hash, unique: true, name: "index_ta_client_metadata_caches_on_uri_hash"
114
+ add_index :token_authority_client_metadata_document_caches, :expires_at, name: "index_ta_client_metadata_caches_on_expires_at"
115
+ end
116
+ end