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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +23 -0
- data/README.md +199 -7
- data/app/controllers/concerns/token_authority/client_authentication.rb +141 -0
- data/app/controllers/concerns/token_authority/controller_event_logging.rb +98 -0
- data/app/controllers/concerns/token_authority/initial_access_token_authentication.rb +35 -0
- data/app/controllers/concerns/token_authority/token_authentication.rb +128 -0
- data/app/controllers/token_authority/authorization_grants_controller.rb +119 -0
- data/app/controllers/token_authority/authorizations_controller.rb +105 -0
- data/app/controllers/token_authority/clients_controller.rb +99 -0
- data/app/controllers/token_authority/metadata_controller.rb +12 -0
- data/app/controllers/token_authority/resource_metadata_controller.rb +12 -0
- data/app/controllers/token_authority/sessions_controller.rb +228 -0
- data/app/helpers/token_authority/authorization_grants_helper.rb +27 -0
- data/app/models/concerns/token_authority/claim_validatable.rb +95 -0
- data/app/models/concerns/token_authority/event_logging.rb +144 -0
- data/app/models/concerns/token_authority/resourceable.rb +111 -0
- data/app/models/concerns/token_authority/scopeable.rb +105 -0
- data/app/models/concerns/token_authority/session_creatable.rb +101 -0
- data/app/models/token_authority/access_token.rb +127 -0
- data/app/models/token_authority/access_token_request.rb +193 -0
- data/app/models/token_authority/authorization_grant.rb +119 -0
- data/app/models/token_authority/authorization_request.rb +276 -0
- data/app/models/token_authority/authorization_server_metadata.rb +101 -0
- data/app/models/token_authority/client.rb +263 -0
- data/app/models/token_authority/client_id_resolver.rb +114 -0
- data/app/models/token_authority/client_metadata_document.rb +164 -0
- data/app/models/token_authority/client_metadata_document_cache.rb +33 -0
- data/app/models/token_authority/client_metadata_document_fetcher.rb +266 -0
- data/app/models/token_authority/client_registration_request.rb +214 -0
- data/app/models/token_authority/client_registration_response.rb +58 -0
- data/app/models/token_authority/jwks_cache.rb +37 -0
- data/app/models/token_authority/jwks_fetcher.rb +70 -0
- data/app/models/token_authority/protected_resource_metadata.rb +74 -0
- data/app/models/token_authority/refresh_token.rb +110 -0
- data/app/models/token_authority/refresh_token_request.rb +116 -0
- data/app/models/token_authority/session.rb +193 -0
- data/app/models/token_authority/software_statement.rb +70 -0
- data/app/views/token_authority/authorization_grants/new.html.erb +25 -0
- data/app/views/token_authority/client_error.html.erb +8 -0
- data/config/locales/token_authority.en.yml +248 -0
- data/config/routes.rb +29 -0
- data/lib/generators/token_authority/install/install_generator.rb +61 -0
- data/lib/generators/token_authority/install/templates/create_token_authority_tables.rb.erb +116 -0
- data/lib/generators/token_authority/install/templates/token_authority.rb +247 -0
- data/lib/token_authority/configuration.rb +397 -0
- data/lib/token_authority/engine.rb +34 -0
- data/lib/token_authority/errors.rb +221 -0
- data/lib/token_authority/instrumentation.rb +80 -0
- data/lib/token_authority/instrumentation_log_subscriber.rb +62 -0
- data/lib/token_authority/json_web_token.rb +78 -0
- data/lib/token_authority/log_event_subscriber.rb +43 -0
- data/lib/token_authority/routing/constraints.rb +71 -0
- data/lib/token_authority/routing/routes.rb +39 -0
- data/lib/token_authority/version.rb +4 -1
- data/lib/token_authority.rb +30 -1
- metadata +65 -5
- data/app/assets/stylesheets/token_authority/application.css +0 -15
- data/app/controllers/token_authority/application_controller.rb +0 -4
- data/app/helpers/token_authority/application_helper.rb +0 -4
- 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:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c0e6a0810b0a8015aea5ada0e1bbb87596875c7641042281dac811fb3fc8bf0c
|
|
4
|
+
data.tar.gz: 76eda2d8230aca93850b6e025c0be79cfac68da48159bbcb7f34e9f102229649
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|