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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 957552579a7b93218cdad9471e5687ee985cda61315660e1f49f1d6de250f765
4
- data.tar.gz: 188821e48e11150b811a4a79011fd8989881723012f5fa225d642f0b186ca14a
3
+ metadata.gz: c0e6a0810b0a8015aea5ada0e1bbb87596875c7641042281dac811fb3fc8bf0c
4
+ data.tar.gz: 76eda2d8230aca93850b6e025c0be79cfac68da48159bbcb7f34e9f102229649
5
5
  SHA512:
6
- metadata.gz: 93d1c243a6cc29f607cf614d919b356eb94da68418fe4e5948034241b3e8b520fcf472bf7a02239be9a25738684412d4e05710174e54972c34233083532f407a
7
- data.tar.gz: 915b69065cbc0eb9d7532ae063d637f4be7e99d1639103c6516e0d15939e12c6e71c20a472e92ad08b170c2f4ce68018d25e2d456fe76a92097e73a9879addaf
6
+ metadata.gz: '069ecd5dc4b50ca9929192ea6d22084db1e5019fa4df90ebcacbc900f4385d88452f3abc108deaa3ce29a88a7c212095dbfc4f056968409983189c0f97292130'
7
+ data.tar.gz: 45ae89f8b90abffacc828544b9c910c40801cf93b65ce5a362faa28b01a242a565233d771f1a56b64e82765a89452ec4a45aacab42ce7883d98b120dbc11c3aa
data/CHANGELOG.md ADDED
@@ -0,0 +1,23 @@
1
+ ## [Unreleased]
2
+
3
+ ## [0.2.0] - 2025-01-23
4
+
5
+ - Implemented support for OAuth 2.1 authorization flows and JSON Web Token (JWT) Profile for OAuth 2.0 Access Tokens (RFC 9068).
6
+ - Implemented support for OAuth 2.0 Authorization Server Metadata (RFC 8414).
7
+ - Implemented support for OAuth 2.0 Protected Resource Metadata (RFC 9728).
8
+ - Implemented support for OAuth 2.0 Dynamic Client Registration Protocol (RFC 7591).
9
+ - Implemented support for OAuth Client ID Metadata Documents.
10
+ - Implemented support for OAuth 2.0 Resource Indicators (RFC 8707).
11
+ - Implemented configuration.
12
+ - Implemented install generator with templates.
13
+ - Implemented structured event logging.
14
+ - Implemented instrumentation.
15
+ - Added documentation.
16
+
17
+ ## [0.1.0] - 2026-01-19
18
+
19
+ - Initial release
20
+
21
+ [Unreleased]: https://github.com/dickdavis/token_authority/compare/v0.2.0...HEAD
22
+ [0.2.0]: https://github.com/dickdavis/token_authority/compare/v0.1.0...v0.2.0
23
+ [0.1.0]: https://github.com/dickdavis/token_authority/releases/tag/v0.1.0
data/README.md CHANGED
@@ -1,28 +1,220 @@
1
1
  # TokenAuthority
2
- Short description and motivation.
2
+
3
+ Rails engine allowing apps to act as their own OAuth 2.1 provider. The goal of this project is to make authorization dead simple for MCP server developers.
4
+
5
+ This project aims to implement the OAuth standards specified in the [MCP Authorization Specification](https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization#standards-compliance).
6
+
7
+ | Status | Standard |
8
+ |--------|----------|
9
+ | ✅ | [OAuth 2.1 IETF DRAFT](https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-13) |
10
+ | ✅ | [JSON Web Token (JWT) Profile for OAuth 2.0 Access Tokens (RFC 9068)](https://datatracker.ietf.org/doc/html/rfc9068) |
11
+ | ✅ | [OAuth 2.0 Authorization Server Metadata (RFC 8414)](https://datatracker.ietf.org/doc/html/rfc8414) |
12
+ | ✅ | [OAuth 2.0 Protected Resource Metadata (RFC 9728)](https://datatracker.ietf.org/doc/html/rfc9728) |
13
+ | ✅ | [OAuth 2.0 Resource Indicators (RFC 8707)](https://datatracker.ietf.org/doc/html/rfc8707) |
14
+ | ✅ | [OAuth 2.0 Dynamic Client Registration Protocol (RFC 7591)](https://datatracker.ietf.org/doc/html/rfc7591) |
15
+ | ✅ | [OAuth Client ID Metadata Documents](https://datatracker.ietf.org/doc/html/draft-ietf-oauth-client-id-metadata-document-00) |
3
16
 
4
17
  ## Usage
5
- How to use my plugin.
6
18
 
7
- ## Installation
19
+ TokenAuthority is simple to install and configure.
20
+
21
+ ### Installation
22
+
8
23
  Add this line to your application's Gemfile:
9
24
 
10
25
  ```ruby
11
26
  gem "token_authority"
12
27
  ```
13
28
 
14
- And then execute:
29
+ Install the gem, generate the required set-up files, and run the migration:
30
+
15
31
  ```bash
16
32
  $ bundle
33
+ $ bin/rails generate token_authority:install
34
+ $ bin/rails db:migrate
17
35
  ```
18
36
 
19
- Or install it yourself as:
37
+ See the [Installation Guide](https://github.com/dickdavis/token_authority/wiki/Installation-Guide) for generator options and custom configurations.
38
+
39
+ ### Configuration
40
+
41
+ Configure TokenAuthority in the generated initializer. The following represents a minimal configuration:
42
+
43
+ ```ruby
44
+ # config/initializers/token_authority.rb
45
+ TokenAuthority.configure do |config|
46
+ # The secret key used for encryption/decryption
47
+ config.secret_key = Rails.application.credentials.secret_key_base
48
+ # The URI for the protected resource (to be included in tokens and metadata)
49
+ config.rfc_9068_audience_url = "https://example.com/api/"
50
+ # The URI for the authorization server (to be included in tokens and metadata)
51
+ config.rfc_9068_issuer_url = "https://example.com/"
52
+ # Define available scopes and their descriptions (shown on consent screen)
53
+ config.scopes = {
54
+ "read" => "Read your data",
55
+ "write" => "Create and modify your data",
56
+ "delete" => "Delete your data"
57
+ }
58
+ end
59
+ ```
60
+
61
+ See the [Configuration Reference](https://github.com/dickdavis/token_authority/wiki/Configuration-Reference) for all available options.
62
+
63
+ ### Mount the Engine
64
+
65
+ Add the engine routes to your `config/routes.rb`:
66
+
67
+ ```ruby
68
+ Rails.application.routes.draw do
69
+ token_authority_routes
70
+ end
71
+ ```
72
+
73
+ This exposes:
74
+ - RFC 8414 Authorization Server Metadata at `/.well-known/oauth-authorization-server`
75
+ - RFC 9728 Protected Resource Metadata at `/.well-known/oauth-protected-resource`
76
+ - OAuth endpoints at `/oauth/authorize`, `/oauth/token`, etc.
77
+
78
+ To mount the engine at a different path, use the `at` option:
79
+
80
+ ```ruby
81
+ Rails.application.routes.draw do
82
+ token_authority_routes(at: "/auth")
83
+ end
84
+ ```
85
+
86
+ ### User Consent
87
+
88
+ Before issuing authorization codes, TokenAuthority displays a consent screen where users can approve or deny access to OAuth clients. The consent views are fully customizable and the layout is configurable—see [Customizing Views](https://github.com/dickdavis/token_authority/wiki/Customizing-Views) for details.
89
+
90
+ The consent screen requires user authentication. Your `authenticatable_controller` must provide two methods:
91
+
92
+ - `authenticate_user!` - Ensures the user is logged in (redirects to login if not)
93
+ - `current_user` - Returns the authenticated user
94
+
95
+ If you use [Devise](https://github.com/heartcombo/devise), these methods are already available on `ApplicationController`. For other authentication systems, see [User Authentication](https://github.com/dickdavis/token_authority/wiki/User-Authentication).
96
+
97
+ ### Protecting API Endpoints
98
+
99
+ Use the `TokenAuthentication` concern to validate access tokens:
100
+
101
+ ```ruby
102
+ class Api::V1::UsersController < ActionController::API
103
+ include TokenAuthority::TokenAuthentication
104
+
105
+ before_action :require_read_scope
106
+
107
+ def current
108
+ render json: { id: token_user.id, email: token_user.email }
109
+ end
110
+
111
+ private
112
+
113
+ def require_read_scope
114
+ return if token_scope.include?("read")
115
+
116
+ render json: { error: "insufficient_scope" }, status: :forbidden
117
+ end
118
+ end
119
+ ```
120
+
121
+ The concern automatically validates the access token on every request and provides:
122
+ - `token_user` - Returns the authenticated user
123
+ - `token_scope` - Returns an array of scope tokens (e.g., `["read", "write"]`), or `[]` if no scopes
124
+
125
+ See [Protecting API Endpoints](https://github.com/dickdavis/token_authority/wiki/Protecting-API-Endpoints) for error handling details.
126
+
127
+ ### Event Logging
128
+
129
+ TokenAuthority emits structured events using Rails 8.1's event reporting system for monitoring, debugging, and auditing. Events cover the full OAuth lifecycle:
130
+
131
+ - Authorization requests and consent
132
+ - Token exchanges, refreshes, and revocations
133
+ - Client and token authentication
134
+ - Security events (e.g., token theft detection)
135
+
136
+ Event logging is enabled by default. Events are automatically logged to `Rails.logger`:
137
+
138
+ ```
139
+ [TokenAuthority] token_authority.authorization.request.received client_id="..." client_type="public" ...
140
+ [TokenAuthority] token_authority.token.exchange.completed client_id="..." user_id=42 session_id=1 ...
141
+ [TokenAuthority] token_authority.security.token.theft_detected client_id="..." user_id=42 ...
142
+ ```
143
+
144
+ See [Event Logging](https://github.com/dickdavis/token_authority/wiki/Event-Logging) for the full event reference and custom subscriber examples.
145
+
146
+ ### Instrumentation
147
+
148
+ TokenAuthority emits `ActiveSupport::Notifications` instrumentation events for performance monitoring. These events provide timing data that APM tools (New Relic, Datadog, Skylight) automatically capture.
149
+
150
+ Instrumentation is enabled by default. Events are automatically logged to `Rails.logger`:
151
+
152
+ ```
153
+ [TokenAuthority::Instrumentation] token_authority.session.create (15.2ms)
154
+ [TokenAuthority::Instrumentation] token_authority.jwt.encode (0.4ms) token_size=312
155
+ [TokenAuthority::Instrumentation] token_authority.client.resolve (0.5ms) client_type="registered"
156
+ ```
157
+
158
+ See [Instrumentation](https://github.com/dickdavis/token_authority/wiki/Instrumentation) for the full event reference and custom subscriber examples.
159
+
160
+ ### Learn More
161
+
162
+ - [Installation Guide](https://github.com/dickdavis/token_authority/wiki/Installation-Guide) - Generator options, custom table names
163
+ - [Configuration Reference](https://github.com/dickdavis/token_authority/wiki/Configuration-Reference) - All configuration options
164
+ - [User Authentication](https://github.com/dickdavis/token_authority/wiki/User-Authentication) - Custom authentication setups
165
+ - [Protecting API Endpoints](https://github.com/dickdavis/token_authority/wiki/Protecting-API-Endpoints) - Error handling, validation details
166
+ - [Customizing Views](https://github.com/dickdavis/token_authority/wiki/Customizing-Views) - Styling consent screens
167
+ - [Event Logging](https://github.com/dickdavis/token_authority/wiki/Event-Logging) - Structured events for monitoring
168
+ - [Instrumentation](https://github.com/dickdavis/token_authority/wiki/Instrumentation) - Performance monitoring with ActiveSupport::Notifications
169
+
170
+ ## Development
171
+
172
+ Clone the repository and install dependencies:
173
+
174
+ ```bash
175
+ git clone https://github.com/dickdavis/token-authority.git
176
+ cd token-authority
177
+ bundle install
178
+ ```
179
+
180
+ Set up git hooks:
181
+
182
+ ```bash
183
+ bundle exec lefthook install
184
+ ```
185
+
186
+ Run the test suite:
187
+
188
+ ```bash
189
+ bundle exec rspec
190
+ ```
191
+
192
+ Run the linter:
193
+
20
194
  ```bash
21
- $ gem install token_authority
195
+ bundle exec standardrb
22
196
  ```
23
197
 
198
+ Generate documentation:
199
+
200
+ ```bash
201
+ bundle exec yard
202
+ ```
203
+
204
+ For manual testing with the dummy app, see [Manual Testing](https://github.com/dickdavis/token_authority/wiki/Manual-Testing).
205
+
206
+ ### Releasing
207
+
208
+ 1. Update the version number in `lib/token_authority/version.rb`
209
+ 2. Commit the version change: `git commit -am "Bump version to X.Y.Z"`
210
+ 3. Run the release task: `rake release`
211
+
212
+ This will create a git tag, push the tag to GitHub, and publish the gem to RubyGems.
213
+
24
214
  ## Contributing
25
- Contribution directions go here.
215
+
216
+ Bug reports and pull requests are welcome on GitHub at https://github.com/dickdavis/token-authority.
26
217
 
27
218
  ## License
219
+
28
220
  The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -0,0 +1,141 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TokenAuthority
4
+ # Provides OAuth client authentication for controllers.
5
+ #
6
+ # This concern handles authentication of OAuth clients during authorization
7
+ # and token requests. It supports both confidential clients (using HTTP Basic
8
+ # authentication with client_id and client_secret) and public clients (using
9
+ # only client_id).
10
+ #
11
+ # The concern automatically:
12
+ # - Resolves client_id to either a registered Client or URL-based ClientMetadataDocument
13
+ # - Validates HTTP Basic credentials for confidential clients
14
+ # - Emits authentication events for monitoring and security auditing
15
+ # - Handles authentication errors with appropriate HTTP responses
16
+ #
17
+ # @example Using in a controller
18
+ # class MyController < ApplicationController
19
+ # include TokenAuthority::ClientAuthentication
20
+ #
21
+ # before_action :authenticate_client
22
+ #
23
+ # def my_action
24
+ # # @token_authority_client is now available
25
+ # end
26
+ # end
27
+ #
28
+ # @since 0.2.0
29
+ module ClientAuthentication
30
+ extend ActiveSupport::Concern
31
+
32
+ included do
33
+ include ActionController::HttpAuthentication::Basic::ControllerMethods
34
+ include TokenAuthority::ControllerEventLogging
35
+
36
+ rescue_from TokenAuthority::ClientMismatchError do
37
+ notify_event("authentication.client.failed",
38
+ client_id: params[:client_id],
39
+ failure_reason: "client_mismatch",
40
+ auth_method_attempted: "http_basic")
41
+
42
+ render plain: "HTTP Basic: Access denied.", status: :unauthorized
43
+ end
44
+
45
+ rescue_from TokenAuthority::ClientNotFoundError do
46
+ notify_event("authentication.client.failed",
47
+ client_id: params[:client_id],
48
+ failure_reason: "client_not_found",
49
+ auth_method_attempted: "http_basic")
50
+
51
+ render plain: "HTTP Basic: Access denied.", status: :unauthorized
52
+ end
53
+ end
54
+
55
+ private
56
+
57
+ # Authenticates an OAuth client.
58
+ #
59
+ # Public clients are authenticated by client_id alone. Confidential clients
60
+ # must provide valid credentials via HTTP Basic authentication.
61
+ #
62
+ # Sets @token_authority_client instance variable on successful authentication.
63
+ #
64
+ # @param id [String, nil] the client_id (uses params[:client_id] if not provided)
65
+ #
66
+ # @return [void]
67
+ #
68
+ # @raise [TokenAuthority::ClientMismatchError] if HTTP Basic credentials don't match params
69
+ # @raise [TokenAuthority::ClientNotFoundError] if client cannot be found
70
+ #
71
+ # @api private
72
+ def authenticate_client(id: nil)
73
+ client_id = id || params[:client_id]
74
+ load_token_authority_client(id: client_id)
75
+
76
+ if @token_authority_client.present? && @token_authority_client.public_client_type?
77
+ notify_event("authentication.client.succeeded",
78
+ client_id: @token_authority_client.public_id,
79
+ client_type: @token_authority_client.client_type,
80
+ auth_method: "public_client")
81
+ return
82
+ end
83
+
84
+ if http_basic_auth_successful?
85
+ notify_event("authentication.client.succeeded",
86
+ client_id: @token_authority_client.public_id,
87
+ client_type: @token_authority_client.client_type,
88
+ auth_method: "http_basic")
89
+ return
90
+ end
91
+
92
+ notify_event("authentication.client.failed",
93
+ client_id: client_id,
94
+ failure_reason: "missing_credentials",
95
+ auth_method_attempted: "none")
96
+
97
+ request_http_basic_authentication
98
+ end
99
+
100
+ # Loads the client by ID using the ClientIdResolver.
101
+ #
102
+ # Sets @token_authority_client to nil if client is not found.
103
+ #
104
+ # @param id [String, nil] the client identifier
105
+ #
106
+ # @return [void]
107
+ # @api private
108
+ def load_token_authority_client(id: nil)
109
+ @token_authority_client = TokenAuthority::ClientIdResolver.resolve(id)
110
+ rescue TokenAuthority::ClientNotFoundError
111
+ @token_authority_client = nil
112
+ end
113
+
114
+ # Attempts to authenticate using HTTP Basic credentials.
115
+ #
116
+ # Verifies that the client_id in Basic auth matches params[:client_id] if present,
117
+ # and validates the client_secret.
118
+ #
119
+ # @return [Boolean] true if authentication succeeds
120
+ #
121
+ # @raise [TokenAuthority::ClientMismatchError] if IDs don't match
122
+ # @raise [TokenAuthority::ClientNotFoundError] if client not found
123
+ # @api private
124
+ def http_basic_auth_successful?
125
+ authenticate_with_http_basic do |public_id, client_secret|
126
+ @token_authority_client = TokenAuthority::Client.find_by(public_id:)
127
+ raise TokenAuthority::ClientNotFoundError if @token_authority_client.blank?
128
+ raise TokenAuthority::ClientMismatchError if params.key?(:client_id) && params[:client_id] != @token_authority_client.public_id
129
+
130
+ authenticated = @token_authority_client.authenticate_with_secret(client_secret)
131
+ unless authenticated
132
+ notify_event("authentication.client.failed",
133
+ client_id: public_id,
134
+ failure_reason: "invalid_secret",
135
+ auth_method_attempted: "http_basic")
136
+ end
137
+ authenticated
138
+ end
139
+ end
140
+ end
141
+ end
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TokenAuthority
4
+ # Provides structured event logging for controller classes.
5
+ #
6
+ # This concern extends event logging for use in controllers by automatically
7
+ # including the request ID in all event payloads. This enables correlation
8
+ # of events across the request lifecycle.
9
+ #
10
+ # Unlike the model EventLogging concern, this version automatically extracts
11
+ # request_id from the controller's request object, eliminating the need to
12
+ # manually pass it.
13
+ #
14
+ # @example Using in a controller
15
+ # class AuthorizationsController < ApplicationController
16
+ # include TokenAuthority::ControllerEventLogging
17
+ #
18
+ # def authorize
19
+ # notify_event("authorization.request.received",
20
+ # client_id: params[:client_id],
21
+ # redirect_uri: params[:redirect_uri])
22
+ # end
23
+ # end
24
+ #
25
+ # @since 0.2.0
26
+ module ControllerEventLogging
27
+ extend ActiveSupport::Concern
28
+
29
+ included do
30
+ class_attribute :_event_logging_enabled, default: true
31
+ end
32
+
33
+ private
34
+
35
+ # Emits a production-level event with automatic request ID.
36
+ #
37
+ # The request_id is automatically extracted from the current request
38
+ # for correlation across the request lifecycle.
39
+ #
40
+ # @param event_name [String] the event name (will be prefixed with "token_authority.")
41
+ # @param payload [Hash] additional event data
42
+ #
43
+ # @return [void]
44
+ #
45
+ # @example
46
+ # notify_event("authorization.request.received",
47
+ # client_id: params[:client_id],
48
+ # has_pkce: params[:code_challenge].present?)
49
+ def notify_event(event_name, **payload)
50
+ return unless event_logging_enabled?
51
+
52
+ full_payload = build_controller_payload(payload)
53
+ Rails.event.notify("token_authority.#{event_name}", **full_payload)
54
+ end
55
+
56
+ # Emits a debug-level event with automatic request ID.
57
+ #
58
+ # Debug events are only emitted when both event_logging_enabled and
59
+ # event_logging_debug_events are true.
60
+ #
61
+ # @param event_name [String] the event name (will be prefixed with "token_authority.")
62
+ # @param payload [Hash] additional event data
63
+ #
64
+ # @return [void]
65
+ def debug_event(event_name, **payload)
66
+ return unless event_logging_enabled?
67
+ return unless debug_events_enabled?
68
+
69
+ full_payload = build_controller_payload(payload)
70
+ Rails.event.debug("token_authority.#{event_name}", **full_payload)
71
+ end
72
+
73
+ # Checks if event logging is enabled.
74
+ # @return [Boolean]
75
+ # @api private
76
+ def event_logging_enabled?
77
+ _event_logging_enabled && TokenAuthority.config.event_logging_enabled
78
+ end
79
+
80
+ # Checks if debug events are enabled.
81
+ # @return [Boolean]
82
+ # @api private
83
+ def debug_events_enabled?
84
+ TokenAuthority.config.event_logging_debug_events
85
+ end
86
+
87
+ # Builds the event payload with timestamp and request_id.
88
+ #
89
+ # @param payload [Hash] the base payload
90
+ # @return [Hash] the enriched payload
91
+ # @api private
92
+ def build_controller_payload(payload)
93
+ base = {timestamp: Time.current.iso8601(6)}
94
+ base[:request_id] = request.request_id if request.respond_to?(:request_id) && request.request_id.present?
95
+ base.merge(payload)
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TokenAuthority
4
+ ##
5
+ # Concern for authenticating initial access tokens in protected registration mode
6
+ module InitialAccessTokenAuthentication
7
+ extend ActiveSupport::Concern
8
+
9
+ included do
10
+ before_action :authenticate_initial_access_token, if: :initial_access_token_required?
11
+ end
12
+
13
+ private
14
+
15
+ def initial_access_token_required?
16
+ TokenAuthority.config.rfc_7591_require_initial_access_token
17
+ end
18
+
19
+ def authenticate_initial_access_token
20
+ token = extract_bearer_token
21
+ raise TokenAuthority::InvalidInitialAccessTokenError if token.blank?
22
+
23
+ validator = TokenAuthority.config.rfc_7591_initial_access_token_validator
24
+ raise TokenAuthority::InvalidInitialAccessTokenError unless validator&.call(token)
25
+ end
26
+
27
+ def extract_bearer_token
28
+ auth_header = request.headers["Authorization"]
29
+ return nil if auth_header.blank?
30
+
31
+ match = auth_header.match(/\ABearer\s+(.+)\z/i)
32
+ match&.captures&.first
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,128 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TokenAuthority
4
+ # Provides JWT access token authentication for protected API endpoints.
5
+ #
6
+ # This concern enables host applications to protect their API endpoints using
7
+ # JWT access tokens issued by TokenAuthority. It validates tokens, checks
8
+ # session status, and provides helper methods to access the authenticated user
9
+ # and token scopes.
10
+ #
11
+ # The concern automatically:
12
+ # - Extracts and validates the Bearer token from the Authorization header
13
+ # - Verifies the token signature and claims
14
+ # - Checks that the session is still active (not revoked or expired)
15
+ # - Emits authentication events for monitoring
16
+ # - Handles authentication errors with JSON error responses
17
+ #
18
+ # @example Protecting an API endpoint
19
+ # class Api::UsersController < ApplicationController
20
+ # include TokenAuthority::TokenAuthentication
21
+ #
22
+ # def show
23
+ # # token_user is the authenticated user
24
+ # # token_scope contains the granted scopes
25
+ # render json: token_user
26
+ # end
27
+ # end
28
+ #
29
+ # @since 0.2.0
30
+ module TokenAuthentication
31
+ extend ActiveSupport::Concern
32
+
33
+ included do
34
+ include TokenAuthority::ControllerEventLogging
35
+
36
+ before_action :decode_token
37
+
38
+ rescue_from TokenAuthority::MissingAuthorizationHeaderError, with: :missing_auth_header_response
39
+ rescue_from TokenAuthority::InvalidAccessTokenError, with: :invalid_token_response
40
+ rescue_from TokenAuthority::UnauthorizedAccessTokenError, with: :unauthorized_token_response
41
+ end
42
+
43
+ private
44
+
45
+ # Extracts, decodes, and validates the JWT access token.
46
+ #
47
+ # Verifies that:
48
+ # - An Authorization header is present
49
+ # - The token can be decoded and has a valid signature
50
+ # - A matching session exists and is still active
51
+ # - The token passes all claim validations
52
+ #
53
+ # Sets @decoded_token instance variable on success.
54
+ #
55
+ # @return [void]
56
+ #
57
+ # @raise [TokenAuthority::MissingAuthorizationHeaderError] if no header present
58
+ # @raise [TokenAuthority::InvalidAccessTokenError] if token is malformed
59
+ # @raise [TokenAuthority::UnauthorizedAccessTokenError] if token is invalid or revoked
60
+ # @api private
61
+ def decode_token
62
+ bearer_token_header = request.headers["AUTHORIZATION"]
63
+ raise TokenAuthority::MissingAuthorizationHeaderError if bearer_token_header.blank?
64
+
65
+ token = bearer_token_header.split.last
66
+ access_token = TokenAuthority::AccessToken.from_token(token)
67
+
68
+ oauth_session = TokenAuthority::Session.find_by(access_token_jti: access_token.jti)
69
+ raise TokenAuthority::UnauthorizedAccessTokenError if oauth_session.blank?
70
+ raise TokenAuthority::UnauthorizedAccessTokenError unless oauth_session.created_status?
71
+ raise TokenAuthority::UnauthorizedAccessTokenError unless access_token.valid?
72
+
73
+ @decoded_token = access_token
74
+
75
+ notify_event("authentication.token.succeeded",
76
+ session_id: oauth_session.id,
77
+ scopes: access_token.scope)
78
+ rescue JWT::DecodeError, ActiveModel::UnknownAttributeError
79
+ raise TokenAuthority::InvalidAccessTokenError
80
+ end
81
+
82
+ # Returns the user associated with the authenticated token.
83
+ #
84
+ # @return [User] the user from the configured user_class
85
+ # @api private
86
+ def token_user
87
+ @token_user ||= TokenAuthority.config.user_class.constantize.find(@decoded_token.user_id)
88
+ end
89
+
90
+ # Returns the scopes granted in the authenticated token.
91
+ #
92
+ # @return [Array<String>] the scope tokens
93
+ # @api private
94
+ def token_scope
95
+ @token_scope ||= @decoded_token.scope&.split || []
96
+ end
97
+
98
+ # Renders error response for missing Authorization header.
99
+ # @return [void]
100
+ # @api private
101
+ def missing_auth_header_response
102
+ notify_event("authentication.token.failed",
103
+ failure_reason: "missing_authorization_header")
104
+
105
+ render json: {error: I18n.t("token_authority.errors.missing_auth_header")}, status: :unauthorized
106
+ end
107
+
108
+ # Renders error response for invalid token format.
109
+ # @return [void]
110
+ # @api private
111
+ def invalid_token_response
112
+ notify_event("authentication.token.failed",
113
+ failure_reason: "invalid_token_format")
114
+
115
+ render json: {error: I18n.t("token_authority.errors.invalid_token")}, status: :unauthorized
116
+ end
117
+
118
+ # Renders error response for unauthorized token.
119
+ # @return [void]
120
+ # @api private
121
+ def unauthorized_token_response
122
+ notify_event("authentication.token.failed",
123
+ failure_reason: "unauthorized_token")
124
+
125
+ render json: {error: I18n.t("token_authority.errors.unauthorized_token")}, status: :unauthorized
126
+ end
127
+ end
128
+ end