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,221 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TokenAuthority
4
+ # Raised when the TokenAuthority configuration is invalid or inconsistent.
5
+ # This typically occurs during initialization or validation when required
6
+ # configuration options are missing or conflicting.
7
+ #
8
+ # @since 0.2.0
9
+ class ConfigurationError < StandardError; end
10
+
11
+ # Raised when the client attempting to use a token or grant doesn't match
12
+ # the client that originally requested it. This prevents token theft and
13
+ # unauthorized client access.
14
+ #
15
+ # @since 0.2.0
16
+ class ClientMismatchError < StandardError; end
17
+
18
+ # Raised when a requested OAuth client cannot be found in the database or
19
+ # via client metadata document resolution.
20
+ #
21
+ # @since 0.2.0
22
+ class ClientNotFoundError < StandardError; end
23
+
24
+ # Raised when an access token fails validation (expired, malformed, or invalid signature).
25
+ #
26
+ # @since 0.2.0
27
+ class InvalidAccessTokenError < StandardError; end
28
+
29
+ # Raised when an authorization grant is invalid, expired, already redeemed,
30
+ # or the PKCE code verifier doesn't match the challenge.
31
+ #
32
+ # Uses I18n for the error message to support internationalization.
33
+ #
34
+ # @since 0.2.0
35
+ class InvalidGrantError < StandardError
36
+ # Returns the localized error message.
37
+ # @return [String] the error message
38
+ def message
39
+ I18n.t("token_authority.errors.invalid_grant")
40
+ end
41
+ end
42
+
43
+ # Raised when a client's redirect URI cannot be parsed or is otherwise invalid.
44
+ # This prevents open redirect vulnerabilities.
45
+ #
46
+ # @since 0.2.0
47
+ class InvalidRedirectUrlError < StandardError; end
48
+
49
+ # Raised when a protected endpoint is accessed without the required Authorization header.
50
+ #
51
+ # @since 0.2.0
52
+ class MissingAuthorizationHeaderError < StandardError; end
53
+
54
+ # Raised when an OAuth session cannot be found by token JTI or session ID.
55
+ #
56
+ # @since 0.2.0
57
+ class OAuthSessionNotFound < StandardError; end
58
+
59
+ # Raised when attempting to use a revoked session.
60
+ # This can indicate a refresh token replay attack where a stolen token
61
+ # is used after the legitimate client has already refreshed.
62
+ #
63
+ # Captures context about both the session being refreshed and the session
64
+ # that was revoked to aid in security auditing.
65
+ #
66
+ # @since 0.2.0
67
+ class RevokedSessionError < StandardError
68
+ # @return [String] the client ID that attempted the refresh
69
+ attr_reader :client_id
70
+
71
+ # @return [Integer] the session ID that was being refreshed
72
+ attr_reader :refreshed_session_id
73
+
74
+ # @return [Integer] the session ID that was revoked
75
+ attr_reader :revoked_session_id
76
+
77
+ # @return [Integer] the user ID associated with the session
78
+ attr_reader :user_id
79
+
80
+ # Creates a new RevokedSessionError with security context.
81
+ #
82
+ # @param client_id [String] the client ID
83
+ # @param refreshed_session_id [Integer] the session being refreshed
84
+ # @param revoked_session_id [Integer] the session that was revoked
85
+ # @param user_id [Integer] the user ID
86
+ def initialize(client_id:, refreshed_session_id:, revoked_session_id:, user_id:)
87
+ super()
88
+ @client_id = client_id
89
+ @refreshed_session_id = refreshed_session_id
90
+ @revoked_session_id = revoked_session_id
91
+ @user_id = user_id
92
+ end
93
+
94
+ # Returns the localized error message with context.
95
+ # @return [String] the error message
96
+ def message
97
+ I18n.t("token_authority.errors.revoked_session", client_id:, refreshed_session_id:, revoked_session_id:, user_id:)
98
+ end
99
+ end
100
+
101
+ # Raised for unexpected server-side errors during OAuth flows.
102
+ # This is a catch-all for internal processing errors.
103
+ #
104
+ # @since 0.2.0
105
+ class ServerError < StandardError; end
106
+
107
+ # Raised when an access token is valid but the user is not authorized
108
+ # to access the requested resource.
109
+ #
110
+ # @since 0.2.0
111
+ class UnauthorizedAccessTokenError < StandardError; end
112
+
113
+ # Raised when PKCE code challenge verification fails.
114
+ # This indicates the code_verifier doesn't match the original code_challenge,
115
+ # which could indicate an interception attack.
116
+ #
117
+ # @since 0.2.0
118
+ class UnsuccessfulChallengeError < StandardError; end
119
+
120
+ # Raised when a client requests a grant type that is not supported
121
+ # by the authorization server.
122
+ #
123
+ # @since 0.2.0
124
+ class UnsupportedGrantTypeError < StandardError; end
125
+
126
+ # Raised during client registration (RFC 7591) when one or more
127
+ # redirect URIs are malformed or use an invalid scheme.
128
+ #
129
+ # @since 0.2.0
130
+ class InvalidRedirectUrisError < StandardError
131
+ # Creates a new InvalidRedirectUrisError.
132
+ # @param msg [String] custom error message
133
+ def initialize(msg = "One or more redirect_uris are invalid")
134
+ super
135
+ end
136
+ end
137
+
138
+ # Raised during client registration (RFC 7591) when the submitted
139
+ # client metadata fails validation.
140
+ #
141
+ # @since 0.2.0
142
+ class InvalidClientMetadataError < StandardError
143
+ # Creates a new InvalidClientMetadataError.
144
+ # @param msg [String] custom error message
145
+ def initialize(msg = "Client metadata is invalid")
146
+ super
147
+ end
148
+ end
149
+
150
+ # Raised during client registration (RFC 7591) when a software statement
151
+ # JWT cannot be verified or contains invalid claims.
152
+ #
153
+ # @since 0.2.0
154
+ class InvalidSoftwareStatementError < StandardError
155
+ # Creates a new InvalidSoftwareStatementError.
156
+ # @param msg [String] custom error message
157
+ def initialize(msg = "Software statement is invalid or could not be verified")
158
+ super
159
+ end
160
+ end
161
+
162
+ # Raised during client registration (RFC 7591) when a software statement
163
+ # is valid but not approved for use with this authorization server.
164
+ #
165
+ # @since 0.2.0
166
+ class UnapprovedSoftwareStatementError < StandardError
167
+ # Creates a new UnapprovedSoftwareStatementError.
168
+ # @param msg [String] custom error message
169
+ def initialize(msg = "Software statement is not approved for use with this authorization server")
170
+ super
171
+ end
172
+ end
173
+
174
+ # Raised during client registration (RFC 7591) when an initial access token
175
+ # is required but missing or fails validation.
176
+ #
177
+ # @since 0.2.0
178
+ class InvalidInitialAccessTokenError < StandardError
179
+ # Creates a new InvalidInitialAccessTokenError.
180
+ # @param msg [String] custom error message
181
+ def initialize(msg = "Initial access token is invalid or missing")
182
+ super
183
+ end
184
+ end
185
+
186
+ # Raised when a client_id URL for client metadata documents is invalid.
187
+ # URLs must be HTTPS, not contain fragments, and meet other security requirements.
188
+ #
189
+ # @since 0.2.0
190
+ class InvalidClientMetadataDocumentUrlError < StandardError
191
+ # Creates a new InvalidClientMetadataDocumentUrlError.
192
+ # @param msg [String] custom error message
193
+ def initialize(msg = "Client ID URL is invalid")
194
+ super
195
+ end
196
+ end
197
+
198
+ # Raised when fetching a client metadata document fails due to network errors,
199
+ # timeouts, or HTTP error responses.
200
+ #
201
+ # @since 0.2.0
202
+ class ClientMetadataDocumentFetchError < StandardError
203
+ # Creates a new ClientMetadataDocumentFetchError.
204
+ # @param msg [String] custom error message
205
+ def initialize(msg = "Failed to fetch client metadata document")
206
+ super
207
+ end
208
+ end
209
+
210
+ # Raised when a fetched client metadata document contains invalid JSON
211
+ # or doesn't meet the required schema.
212
+ #
213
+ # @since 0.2.0
214
+ class InvalidClientMetadataDocumentError < StandardError
215
+ # Creates a new InvalidClientMetadataDocumentError.
216
+ # @param msg [String] custom error message
217
+ def initialize(msg = "Client metadata document is invalid")
218
+ super
219
+ end
220
+ end
221
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TokenAuthority
4
+ # Provides instrumentation capabilities using ActiveSupport::Notifications.
5
+ #
6
+ # This module emits performance and timing data that APM tools (New Relic, Datadog,
7
+ # Skylight) can automatically capture. It can be included in classes or extended
8
+ # in modules to add instrumentation to methods.
9
+ #
10
+ # All events are automatically namespaced with "token_authority." prefix to avoid
11
+ # conflicts with other instrumentation in the application.
12
+ #
13
+ # @example Using in a module with class methods
14
+ # module MyModule
15
+ # extend TokenAuthority::Instrumentation
16
+ #
17
+ # def self.process_data
18
+ # instrument("process_data", record_count: 100) do |payload|
19
+ # # Do work here
20
+ # # Can add additional payload data: payload[:rows_processed] = 42
21
+ # end
22
+ # end
23
+ # end
24
+ #
25
+ # @example Using in a class with instance methods
26
+ # class MyClass
27
+ # include TokenAuthority::Instrumentation
28
+ #
29
+ # def process_record
30
+ # instrument("process_record") do
31
+ # # Do work here
32
+ # end
33
+ # end
34
+ # end
35
+ #
36
+ # @since 0.2.0
37
+ module Instrumentation
38
+ # The namespace prefix for all instrumentation events.
39
+ # @return [String]
40
+ NAMESPACE = "token_authority"
41
+
42
+ # Wraps a block of code with instrumentation timing and error tracking.
43
+ #
44
+ # If instrumentation is disabled via configuration, the block is executed
45
+ # without emitting notifications. Errors are captured in the payload before
46
+ # being re-raised.
47
+ #
48
+ # @param event_name [String] the event name (will be prefixed with "token_authority.")
49
+ # @param payload [Hash] initial payload data for the event
50
+ #
51
+ # @yield [payload] the block to instrument; the payload can be modified within the block
52
+ # @yieldparam payload [Hash] the mutable event payload
53
+ #
54
+ # @return the result of the yielded block
55
+ #
56
+ # @raise re-raises any exception from the block after capturing it in the payload
57
+ #
58
+ # @example Basic usage
59
+ # instrument("database.query", table: "users") do |payload|
60
+ # result = run_query
61
+ # payload[:rows] = result.count
62
+ # result
63
+ # end
64
+ #
65
+ # @example Error handling
66
+ # instrument("risky_operation") do
67
+ # raise "Something went wrong" # Error is logged to payload before re-raising
68
+ # end
69
+ def instrument(event_name, **payload, &block)
70
+ return yield(payload) unless TokenAuthority.config.instrumentation_enabled
71
+
72
+ ActiveSupport::Notifications.instrument("#{NAMESPACE}.#{event_name}", payload) do |p|
73
+ yield(p)
74
+ rescue => e
75
+ p[:error] = "#{e.class.name}: #{e.message}"
76
+ raise
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TokenAuthority
4
+ # Subscribes to TokenAuthority instrumentation events and logs them to Rails logger.
5
+ #
6
+ # This subscriber captures all instrumentation events emitted by the TokenAuthority
7
+ # engine and writes them to the Rails log with timing information. This is valuable
8
+ # for development debugging and performance analysis.
9
+ #
10
+ # The subscriber is automatically enabled when `instrumentation_enabled` is true
11
+ # in the configuration. It can also be manually enabled for testing or debugging.
12
+ #
13
+ # Events are logged at INFO level with the format:
14
+ # [TokenAuthority::Instrumentation] event_name (duration_ms) key1=value1 key2=value2
15
+ #
16
+ # @example Automatic subscription via configuration
17
+ # TokenAuthority.configure do |config|
18
+ # config.instrumentation_enabled = true
19
+ # end
20
+ #
21
+ # @example Manual subscription
22
+ # TokenAuthority::InstrumentationLogSubscriber.subscribe!
23
+ #
24
+ # @since 0.2.0
25
+ class InstrumentationLogSubscriber
26
+ class << self
27
+ # Subscribes to all TokenAuthority instrumentation events.
28
+ #
29
+ # Creates a wildcard subscription for all events matching the pattern
30
+ # /^token_authority\./. Each event is logged with its name, duration,
31
+ # and payload data.
32
+ #
33
+ # @return [ActiveSupport::Notifications::Subscription] the subscription object
34
+ #
35
+ # @note This method should only be called once during application initialization
36
+ # to avoid duplicate log entries.
37
+ def subscribe!
38
+ ActiveSupport::Notifications.subscribe(/^token_authority\./) do |name, start, finish, id, payload|
39
+ duration_ms = (finish - start) * 1000
40
+ log_event(name, duration_ms, payload)
41
+ end
42
+ end
43
+
44
+ private
45
+
46
+ # Formats and logs an instrumentation event.
47
+ #
48
+ # Exceptions are excluded from the payload output to avoid cluttering logs,
49
+ # as they're typically logged separately by Rails' exception handling.
50
+ #
51
+ # @param name [String] the full event name (e.g., "token_authority.jwt.encode")
52
+ # @param duration_ms [Float] the event duration in milliseconds
53
+ # @param payload [Hash] the event payload data
54
+ #
55
+ # @return [void]
56
+ def log_event(name, duration_ms, payload)
57
+ payload_str = payload.except(:exception).map { |k, v| "#{k}=#{v.inspect}" }.join(" ")
58
+ Rails.logger.info { "[TokenAuthority::Instrumentation] #{name} (#{duration_ms.round(2)}ms) #{payload_str}" }
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "jwt"
4
+
5
+ module TokenAuthority
6
+ # Provides JWT encoding and decoding functionality for TokenAuthority.
7
+ #
8
+ # This module wraps the ruby-jwt gem and adds instrumentation for monitoring.
9
+ # All JWTs are signed using HMAC-SHA256 with the configured secret key.
10
+ #
11
+ # Token expiration validation is disabled during decoding to allow the application
12
+ # to handle expired tokens gracefully and provide better error messages.
13
+ #
14
+ # @note This module uses ActiveSupport::Notifications for instrumentation when enabled.
15
+ #
16
+ # @example Encoding a token
17
+ # payload = { user_id: 123, scope: "read write" }
18
+ # token = TokenAuthority::JsonWebToken.encode(payload, 1.hour.from_now)
19
+ #
20
+ # @example Decoding a token
21
+ # payload = TokenAuthority::JsonWebToken.decode(token)
22
+ # user_id = payload[:user_id]
23
+ #
24
+ # @since 0.2.0
25
+ module JsonWebToken
26
+ extend TokenAuthority::Instrumentation
27
+
28
+ # Encodes a payload into a signed JWT.
29
+ #
30
+ # The expiration time is added to the payload before encoding.
31
+ # Emits an instrumentation event with the token size.
32
+ #
33
+ # @param payload [Hash] the JWT claims to encode
34
+ # @param expiration [Time, ActiveSupport::TimeWithZone] when the token expires
35
+ # (default: 30 minutes from now)
36
+ #
37
+ # @return [String] the encoded JWT token
38
+ #
39
+ # @example
40
+ # token = JsonWebToken.encode({ user_id: 42 }, 1.hour.from_now)
41
+ def self.encode(payload, expiration = 30.minutes.from_now)
42
+ payload[:exp] = expiration.to_i
43
+
44
+ instrument("jwt.encode") do |p|
45
+ token = JWT.encode(payload, TokenAuthority.config.secret_key)
46
+ p[:token_size] = token.bytesize
47
+ token
48
+ end
49
+ end
50
+
51
+ # Decodes and verifies a JWT signature.
52
+ #
53
+ # Expiration validation is intentionally disabled to allow the application
54
+ # to handle expired tokens with custom error messages and logic.
55
+ # Emits an instrumentation event with the token size.
56
+ #
57
+ # @param token [String] the JWT token to decode
58
+ #
59
+ # @return [ActiveSupport::HashWithIndifferentAccess] the decoded JWT claims
60
+ #
61
+ # @raise [JWT::DecodeError] if the token is malformed or signature is invalid
62
+ #
63
+ # @example
64
+ # payload = JsonWebToken.decode(token_string)
65
+ # user_id = payload[:user_id]
66
+ def self.decode(token)
67
+ instrument("jwt.decode", token_size: token.bytesize) do
68
+ (payload,) = JWT.decode(
69
+ token,
70
+ TokenAuthority.config.secret_key,
71
+ true,
72
+ {verify_expiration: false, algorithm: "HS256"}
73
+ )
74
+ ActiveSupport::HashWithIndifferentAccess.new(payload)
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TokenAuthority
4
+ ##
5
+ # A Rails.event subscriber that logs TokenAuthority events to Rails.logger.
6
+ #
7
+ # This subscriber only processes events with the "token_authority." prefix,
8
+ # ignoring all other Rails events.
9
+ #
10
+ # Usage:
11
+ # # Automatically enabled via configuration:
12
+ # TokenAuthority.configure do |config|
13
+ # config.event_logging_rails_logger = true
14
+ # end
15
+ #
16
+ # # Or manually subscribe:
17
+ # Rails.event.subscribe(TokenAuthority::LogEventSubscriber.new)
18
+ #
19
+ class LogEventSubscriber
20
+ # @param event [Hash] The event hash from Rails.event
21
+ # - :name [String] Event name (e.g., "token_authority.authorization.request.received")
22
+ # - :payload [Hash] Event-specific data
23
+ # - :context [Hash] Request-level context (request_id, user_id, etc.)
24
+ # - :tags [Hash] Domain tags
25
+ # - :timestamp [Integer] Nanosecond timestamp
26
+ # - :source_location [Hash] File, line, and method info
27
+ def emit(event)
28
+ name = event[:name]
29
+
30
+ # Only log token_authority events
31
+ return unless name.start_with?("token_authority.")
32
+
33
+ payload = event[:payload] || {}
34
+ context = event[:context] || {}
35
+
36
+ # Format payload as key=value pairs
37
+ payload_str = payload.map { |k, v| "#{k}=#{v.inspect}" }.join(" ")
38
+ context_str = context.any? ? " context=#{context.inspect}" : ""
39
+
40
+ Rails.logger.info("[TokenAuthority] #{name} #{payload_str}#{context_str}")
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TokenAuthority
4
+ # Provides routing utilities for the TokenAuthority engine.
5
+ #
6
+ # @since 0.2.0
7
+ module Routing
8
+ # Route constraint for matching the grant_type parameter in token requests.
9
+ #
10
+ # This allows routing different grant types (authorization_code, refresh_token)
11
+ # to different controller actions based on the request parameters.
12
+ #
13
+ # @example In routes.rb
14
+ # post "token",
15
+ # to: "sessions#create_from_authorization_code",
16
+ # constraints: GrantTypeConstraint.new("authorization_code")
17
+ #
18
+ # @since 0.2.0
19
+ GrantTypeConstraint = Struct.new(:grant_type) do
20
+ # Determines if the request's grant_type parameter matches the constraint.
21
+ #
22
+ # @param request [ActionDispatch::Request] the Rails request object
23
+ # @return [Boolean] true if the grant_type parameter matches
24
+ def matches?(request)
25
+ request.request_parameters["grant_type"] == grant_type
26
+ end
27
+ end
28
+
29
+ # Route constraint for matching the token_type_hint parameter in revocation requests.
30
+ #
31
+ # This allows routing access token and refresh token revocations to different
32
+ # controller actions for optimized lookups.
33
+ #
34
+ # @example In routes.rb
35
+ # post "revoke",
36
+ # to: "sessions#revoke_access_token",
37
+ # constraints: TokenTypeHintConstraint.new("access_token")
38
+ #
39
+ # @since 0.2.0
40
+ TokenTypeHintConstraint = Struct.new(:token_type_hint) do
41
+ # Determines if the request's token_type_hint parameter matches the constraint.
42
+ #
43
+ # @param request [ActionDispatch::Request] the Rails request object
44
+ # @return [Boolean] true if the token_type_hint parameter matches
45
+ def matches?(request)
46
+ request.request_parameters["token_type_hint"] == token_type_hint
47
+ end
48
+ end
49
+
50
+ # Route constraint that checks if dynamic client registration (RFC 7591) is enabled.
51
+ #
52
+ # This prevents registration endpoints from being accessible when the feature
53
+ # is disabled in configuration.
54
+ #
55
+ # @example In routes.rb
56
+ # post "clients",
57
+ # to: "clients#create",
58
+ # constraints: DynamicRegistrationEnabledConstraint.new
59
+ #
60
+ # @since 0.2.0
61
+ DynamicRegistrationEnabledConstraint = Struct.new(:nothing) do
62
+ # Determines if dynamic client registration is enabled in the configuration.
63
+ #
64
+ # @param _request [ActionDispatch::Request] the Rails request object (unused)
65
+ # @return [Boolean] true if RFC 7591 dynamic registration is enabled
66
+ def matches?(_request)
67
+ TokenAuthority.config.rfc_7591_enabled
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionDispatch
4
+ module Routing
5
+ class Mapper
6
+ ##
7
+ # Adds all TokenAuthority routes including OAuth 2.0 metadata endpoints
8
+ # (RFC 8414, RFC 9728) and mounts the TokenAuthority engine.
9
+ #
10
+ # Both RFC 8414 and RFC 9728 require the metadata endpoints to be at the
11
+ # root level `/.well-known/` path, not under the engine mount path.
12
+ #
13
+ # @param at [String] the path where TokenAuthority engine is mounted (default: "/oauth")
14
+ #
15
+ # @example
16
+ # Rails.application.routes.draw do
17
+ # token_authority_routes # Mounts at default "/oauth"
18
+ # end
19
+ #
20
+ # @example Custom mount path
21
+ # Rails.application.routes.draw do
22
+ # token_authority_routes(at: "/auth")
23
+ # end
24
+ def token_authority_routes(at: "/oauth")
25
+ # RFC 8414: Authorization Server Metadata
26
+ get "/.well-known/oauth-authorization-server",
27
+ to: "token_authority/metadata#show",
28
+ defaults: {mount_path: at}
29
+
30
+ # RFC 9728: Protected Resource Metadata
31
+ get "/.well-known/oauth-protected-resource",
32
+ to: "token_authority/resource_metadata#show",
33
+ defaults: {mount_path: at}
34
+
35
+ mount TokenAuthority::Engine => at
36
+ end
37
+ end
38
+ end
39
+ end
@@ -1,3 +1,6 @@
1
1
  module TokenAuthority
2
- VERSION = "0.1.0"
2
+ # The current version of the TokenAuthority gem.
3
+ #
4
+ # @return [String] the version string in semantic versioning format
5
+ VERSION = "0.2.0"
3
6
  end
@@ -1,6 +1,35 @@
1
1
  require "token_authority/version"
2
2
  require "token_authority/engine"
3
+ require "token_authority/configuration"
4
+ require "token_authority/errors"
5
+ require "token_authority/instrumentation"
6
+ require "token_authority/json_web_token"
7
+ require "token_authority/routing/constraints"
8
+ require "token_authority/routing/routes"
3
9
 
10
+ # TokenAuthority is a Rails engine that enables Rails applications to act as their own
11
+ # OAuth 2.1 provider. It provides a complete implementation of the OAuth 2.1 authorization
12
+ # framework with support for PKCE, JWT access tokens (RFC 9068), dynamic client registration
13
+ # (RFC 7591), resource indicators (RFC 8707), and client metadata documents.
14
+ #
15
+ # The engine is designed to integrate alongside existing authentication systems (Devise,
16
+ # custom auth, etc.) and provides mountable OAuth endpoints for authorization, token
17
+ # exchange, and token management.
18
+ #
19
+ # @example Basic configuration
20
+ # TokenAuthority.configure do |config|
21
+ # config.secret_key = Rails.application.credentials.secret_key_base
22
+ # config.rfc_9068_audience_url = "https://api.example.com"
23
+ # config.rfc_9068_issuer_url = "https://example.com"
24
+ # end
25
+ #
26
+ # @since 0.2.0
4
27
  module TokenAuthority
5
- # Your code goes here...
28
+ # Returns the table name prefix for all TokenAuthority models.
29
+ # This ensures that all database tables are namespaced with 'token_authority_'.
30
+ #
31
+ # @return [String] the table name prefix
32
+ def self.table_name_prefix
33
+ "token_authority_"
34
+ end
6
35
  end