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,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TokenAuthority
4
+ ##
5
+ # Builds the RFC 9728 OAuth 2.0 Protected Resource Metadata response
6
+ class ProtectedResourceMetadata
7
+ def initialize(mount_path:)
8
+ @mount_path = mount_path
9
+ end
10
+
11
+ def to_h
12
+ metadata = {
13
+ resource: resource,
14
+ authorization_servers: authorization_servers
15
+ }
16
+
17
+ metadata[:scopes_supported] = scopes_supported if scopes_supported.any?
18
+ metadata[:bearer_methods_supported] = bearer_methods_supported if bearer_methods_supported.present?
19
+ metadata[:jwks_uri] = jwks_uri if jwks_uri.present?
20
+ metadata[:resource_name] = resource_name if resource_name.present?
21
+ metadata[:resource_documentation] = resource_documentation if resource_documentation.present?
22
+ metadata[:resource_policy_uri] = resource_policy_uri if resource_policy_uri.present?
23
+ metadata[:resource_tos_uri] = resource_tos_uri if resource_tos_uri.present?
24
+
25
+ metadata
26
+ end
27
+
28
+ private
29
+
30
+ def config
31
+ TokenAuthority.config
32
+ end
33
+
34
+ def issuer
35
+ config.rfc_9068_issuer_url.to_s.chomp("/")
36
+ end
37
+
38
+ def resource
39
+ config.rfc_9728_resource.presence || issuer
40
+ end
41
+
42
+ def authorization_servers
43
+ config.rfc_9728_authorization_servers.presence || [issuer]
44
+ end
45
+
46
+ def scopes_supported
47
+ config.rfc_9728_scopes_supported.presence || config.scopes&.keys || []
48
+ end
49
+
50
+ def bearer_methods_supported
51
+ config.rfc_9728_bearer_methods_supported
52
+ end
53
+
54
+ def jwks_uri
55
+ config.rfc_9728_jwks_uri
56
+ end
57
+
58
+ def resource_name
59
+ config.rfc_9728_resource_name
60
+ end
61
+
62
+ def resource_documentation
63
+ config.rfc_9728_resource_documentation
64
+ end
65
+
66
+ def resource_policy_uri
67
+ config.rfc_9728_resource_policy_uri
68
+ end
69
+
70
+ def resource_tos_uri
71
+ config.rfc_9728_resource_tos_uri
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TokenAuthority
4
+ # Represents an OAuth 2.1 refresh token with JWT format.
5
+ #
6
+ # Refresh tokens are long-lived tokens used to obtain new access tokens without
7
+ # requiring user interaction. Like access tokens, they follow JWT format but have
8
+ # a longer expiration time (default: 14 days).
9
+ #
10
+ # Refresh tokens implement token rotation: each refresh operation creates a new
11
+ # session with new access and refresh tokens, and marks the old session as
12
+ # "refreshed". This prevents replay attacks.
13
+ #
14
+ # Unlike access tokens, refresh tokens do not include a user_id claim since they
15
+ # are looked up by JTI in the Session table which already has the user association.
16
+ #
17
+ # This is an ActiveModel object (not ActiveRecord) that provides validation
18
+ # and serialization of JWT claims without database persistence.
19
+ #
20
+ # @example Creating a refresh token
21
+ # token = TokenAuthority::RefreshToken.default(
22
+ # exp: 14.days.from_now,
23
+ # resources: ["https://api.example.com"],
24
+ # scopes: ["read", "write"]
25
+ # )
26
+ # jwt_string = token.to_encoded_token
27
+ #
28
+ # @example Decoding a refresh token
29
+ # token = TokenAuthority::RefreshToken.from_token(jwt_string)
30
+ # token.valid? # => true/false
31
+ #
32
+ # @since 0.2.0
33
+ class RefreshToken
34
+ include TokenAuthority::ClaimValidatable
35
+
36
+ # @!attribute [rw] scope
37
+ # Space-separated list of OAuth scopes for this refresh token.
38
+ # @return [String, nil]
39
+ attr_accessor :scope
40
+
41
+ # Creates a new refresh token with standard JWT claims.
42
+ #
43
+ # The audience (aud) claim is set from resource indicators if provided,
44
+ # otherwise falls back to the configured default audience URL.
45
+ #
46
+ # @param exp [Time, Integer] token expiration time
47
+ # @param resources [Array<String>] resource indicators (RFC 8707)
48
+ # @param scopes [Array<String>] OAuth scopes to include
49
+ #
50
+ # @return [TokenAuthority::RefreshToken] the new token instance
51
+ #
52
+ # @example
53
+ # token = RefreshToken.default(
54
+ # exp: 14.days.from_now,
55
+ # resources: ["https://api.example.com"],
56
+ # scopes: ["read", "write"]
57
+ # )
58
+ def self.default(exp:, resources: [], scopes: [])
59
+ # Use resources for aud claim if provided, otherwise fall back to config
60
+ aud = if resources.any?
61
+ (resources.size == 1) ? resources.first : resources
62
+ else
63
+ TokenAuthority.config.rfc_9068_audience_url
64
+ end
65
+
66
+ # Only include scope if scopes are provided
67
+ scope_claim = scopes.any? ? scopes.join(" ") : nil
68
+
69
+ new(
70
+ aud:,
71
+ exp:,
72
+ iat: Time.zone.now.to_i,
73
+ iss: TokenAuthority.config.rfc_9068_issuer_url,
74
+ jti: SecureRandom.uuid,
75
+ scope: scope_claim
76
+ )
77
+ end
78
+
79
+ # Decodes a JWT string into a RefreshToken instance.
80
+ #
81
+ # @param token [String] the JWT-encoded refresh token
82
+ #
83
+ # @return [TokenAuthority::RefreshToken] the decoded token instance
84
+ #
85
+ # @raise [JWT::DecodeError] if the token is malformed or signature is invalid
86
+ #
87
+ # @example
88
+ # token = RefreshToken.from_token(jwt_string)
89
+ # jti = token.jti
90
+ def self.from_token(token)
91
+ new(TokenAuthority::JsonWebToken.decode(token))
92
+ end
93
+
94
+ # Converts the token to a hash of JWT claims.
95
+ #
96
+ # Nil values are omitted from the hash to produce a minimal JWT payload.
97
+ #
98
+ # @return [Hash] the JWT claims
99
+ def to_h
100
+ {aud:, exp:, iat:, iss:, jti:, scope:}.compact
101
+ end
102
+
103
+ # Encodes the token as a signed JWT string.
104
+ #
105
+ # @return [String] the JWT-encoded refresh token
106
+ def to_encoded_token
107
+ TokenAuthority::JsonWebToken.encode(to_h, exp)
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,116 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TokenAuthority
4
+ ##
5
+ # Models a refresh token request
6
+ class RefreshTokenRequest
7
+ include ActiveModel::Model
8
+ include ActiveModel::Validations
9
+ include TokenAuthority::Resourceable
10
+ include TokenAuthority::Scopeable
11
+
12
+ attr_accessor :token, :client_id
13
+
14
+ validate :token_authority_session_must_be_valid
15
+ validate :resources_must_be_valid
16
+ validate :scope_must_be_valid
17
+
18
+ # Returns the effective resources (requested or falls back to grant's resources)
19
+ def effective_resources
20
+ return resources if resources.any?
21
+
22
+ token_authority_authorization_grant&.resources || []
23
+ end
24
+
25
+ # Returns the effective scopes (requested or falls back to grant's scopes)
26
+ def effective_scopes
27
+ return scope if scope.present?
28
+
29
+ token_authority_authorization_grant&.scopes || []
30
+ end
31
+
32
+ # Returns the session for use in refresh operation
33
+ def token_authority_session
34
+ return nil unless token
35
+
36
+ @token_authority_session ||= TokenAuthority::Session.find_by(refresh_token_jti: token.jti)
37
+ end
38
+
39
+ # Returns the resolved client_id (from param or from grant's client)
40
+ def resolved_client_id
41
+ client_id.presence || token_authority_session&.token_authority_authorization_grant&.resolved_client&.public_id
42
+ end
43
+
44
+ private
45
+
46
+ def token_authority_session_must_be_valid
47
+ errors.add(:token, :blank) and return if token.blank?
48
+
49
+ errors.add(:token, :session_not_found) if token_authority_session.nil?
50
+ end
51
+
52
+ def token_authority_authorization_grant
53
+ @token_authority_authorization_grant ||= token_authority_session&.token_authority_authorization_grant
54
+ end
55
+
56
+ def resources_must_be_valid
57
+ return unless token_authority_session_valid?
58
+ return if resources.empty?
59
+
60
+ # If resources are provided but feature is disabled, reject them
61
+ unless TokenAuthority.config.rfc_8707_enabled?
62
+ errors.add(:resources, :not_allowed)
63
+ return
64
+ end
65
+
66
+ # Validate all resource URIs
67
+ unless valid_resource_uris?
68
+ errors.add(:resources, :invalid_uri)
69
+ return
70
+ end
71
+
72
+ # Check against allowed resources list
73
+ unless allowed_resources?
74
+ errors.add(:resources, :not_allowed)
75
+ return
76
+ end
77
+
78
+ # Check that requested resources are a subset of granted resources (downscoping)
79
+ granted_resources = token_authority_authorization_grant.resources
80
+ errors.add(:resources, :not_subset) unless resources_subset_of?(granted_resources)
81
+ end
82
+
83
+ def scope_must_be_valid
84
+ return unless token_authority_session_valid?
85
+ return if scope.blank?
86
+
87
+ # If scopes are provided but feature is disabled, reject them
88
+ unless TokenAuthority.config.scopes_enabled?
89
+ errors.add(:scope, :not_allowed)
90
+ return
91
+ end
92
+
93
+ # Validate all scope tokens
94
+ unless valid_scope_tokens?
95
+ errors.add(:scope, :invalid)
96
+ return
97
+ end
98
+
99
+ # Check against allowed scopes list
100
+ unless allowed_scopes?
101
+ errors.add(:scope, :not_allowed)
102
+ return
103
+ end
104
+
105
+ # Check that requested scopes are a subset of granted scopes (downscoping)
106
+ granted_scopes = token_authority_authorization_grant.scopes || []
107
+ errors.add(:scope, :not_subset) unless scopes_subset_of?(granted_scopes)
108
+ end
109
+
110
+ def token_authority_session_valid?
111
+ return @token_authority_session_valid if defined?(@token_authority_session_valid)
112
+
113
+ @token_authority_session_valid = token_authority_session.present?
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,193 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TokenAuthority
4
+ # Represents an OAuth token session containing access and refresh tokens.
5
+ #
6
+ # Sessions track the lifecycle of token pairs from creation through expiration,
7
+ # refresh, or revocation. They implement refresh token rotation (each refresh
8
+ # creates a new session) and revocation detection to prevent replay attacks.
9
+ #
10
+ # The session stores JWT identifiers (jti claims) for both access and refresh
11
+ # tokens, but not the tokens themselves. This allows efficient revocation lookups
12
+ # without storing the full JWT payloads.
13
+ #
14
+ # Session status transitions:
15
+ # - created: Active session with valid tokens
16
+ # - refreshed: Old session that was replaced by a refresh operation
17
+ # - expired: Session that exceeded its lifetime (future use)
18
+ # - revoked: Session that was explicitly revoked or detected as compromised
19
+ #
20
+ # @example Refreshing a session
21
+ # new_session = session.refresh(
22
+ # token: refresh_token,
23
+ # client_id: client.public_id,
24
+ # resources: ["https://api.example.com"]
25
+ # )
26
+ #
27
+ # @example Revoking a session
28
+ # session.revoke_self_and_active_session(
29
+ # reason: "user_logout",
30
+ # request_id: "req-123"
31
+ # )
32
+ #
33
+ # @since 0.2.0
34
+ class Session < ApplicationRecord
35
+ include TokenAuthority::SessionCreatable
36
+ include TokenAuthority::EventLogging
37
+
38
+ STATUS_ENUM_VALUES = {
39
+ created: "created",
40
+ expired: "expired",
41
+ refreshed: "refreshed",
42
+ revoked: "revoked"
43
+ }.freeze
44
+
45
+ VALID_UUID_REGEX = /[0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[089ab][0-9a-f]{3}-[0-9a-f]{12}/i
46
+
47
+ belongs_to :token_authority_authorization_grant, class_name: "TokenAuthority::AuthorizationGrant"
48
+
49
+ enum :status, STATUS_ENUM_VALUES, suffix: true
50
+
51
+ delegate :user_id, to: :token_authority_authorization_grant
52
+
53
+ validates :access_token_jti, presence: true, uniqueness: true, format: {with: VALID_UUID_REGEX}
54
+ validates :refresh_token_jti, presence: true, uniqueness: true, format: {with: VALID_UUID_REGEX}
55
+
56
+ # Refreshes the session by creating a new session with rotated tokens.
57
+ #
58
+ # This implements refresh token rotation as recommended by OAuth 2.1.
59
+ # The current session is marked as "refreshed" and a new session is created
60
+ # with new access and refresh tokens.
61
+ #
62
+ # Includes replay attack detection: if the session is not in "created" status
63
+ # or the client_id doesn't match, the refresh token has been stolen or reused.
64
+ # In this case, both the current session and any active session are revoked.
65
+ #
66
+ # @param token [TokenAuthority::RefreshToken] the refresh token to validate
67
+ # @param client_id [String] the client ID attempting the refresh
68
+ # @param resources [Array<String>] resource indicators for the new tokens
69
+ # @param scopes [Array<String>] scopes for the new tokens
70
+ #
71
+ # @return [TokenAuthority::Session] the newly created session
72
+ #
73
+ # @raise [TokenAuthority::ServerError] if the token JTI doesn't match
74
+ # @raise [TokenAuthority::InvalidGrantError] if the token is invalid
75
+ # @raise [TokenAuthority::RevokedSessionError] if replay attack is detected
76
+ def refresh(token:, client_id:, resources: [], scopes: [])
77
+ instrument("session.refresh") do
78
+ raise TokenAuthority::ServerError, I18n.t("token_authority.errors.mismatched_refresh_token") unless token.jti == refresh_token_jti
79
+ raise TokenAuthority::InvalidGrantError unless token.valid?
80
+
81
+ # Detect stolen refresh token and replay attacks, and then revoke current active token authority session
82
+ unless created_status? && client_id == token_authority_authorization_grant.resolved_client.public_id
83
+ session = token_authority_authorization_grant.active_token_authority_session || self
84
+ session.update(status: "revoked")
85
+ raise TokenAuthority::RevokedSessionError.new(
86
+ client_id:,
87
+ refreshed_session_id: id,
88
+ revoked_session_id: session.id,
89
+ user_id:
90
+ )
91
+ end
92
+
93
+ create_token_authority_session(grant: token_authority_authorization_grant, resources:, scopes:) do
94
+ update(status: "refreshed")
95
+ end
96
+ end
97
+ rescue TokenAuthority::ServerError => error
98
+ raise TokenAuthority::ServerError, error.message
99
+ end
100
+
101
+ # Revokes this session and any active session for the same authorization grant.
102
+ #
103
+ # This ensures that revoking any token (access or refresh) invalidates the
104
+ # entire token family, preventing continued use after revocation. Emits a
105
+ # security event for audit logging.
106
+ #
107
+ # @param reason [String] the reason for revocation (default: "revocation_requested")
108
+ # @param request_id [String, nil] the request ID for correlation in logs
109
+ #
110
+ # @return [void]
111
+ #
112
+ # @example
113
+ # session.revoke_self_and_active_session(
114
+ # reason: "user_logout",
115
+ # request_id: request.uuid
116
+ # )
117
+ def revoke_self_and_active_session(reason: "revocation_requested", request_id: nil)
118
+ instrument("session.revoke") do
119
+ related_session_ids = []
120
+ ActiveRecord::Base.transaction do
121
+ update(status: "revoked")
122
+ active_session = token_authority_authorization_grant.active_token_authority_session
123
+ if active_session && active_session.id != id
124
+ active_session.update(status: "revoked")
125
+ related_session_ids << active_session.id
126
+ end
127
+ end
128
+
129
+ notify_event("security.session.revoked",
130
+ request_id: request_id,
131
+ session_id: id,
132
+ client_id: token_authority_authorization_grant.resolved_client&.public_id,
133
+ reason: reason,
134
+ related_session_ids: related_session_ids)
135
+ end
136
+ end
137
+
138
+ # Revokes the session associated with a token JTI.
139
+ #
140
+ # Searches for a session by either access token or refresh token JTI.
141
+ # This is the generic revocation method used when the token type is unknown.
142
+ #
143
+ # @param jti [String] the JWT identifier from the token
144
+ #
145
+ # @return [void]
146
+ #
147
+ # @note Returns silently if no session is found
148
+ def self.revoke_for_token(jti:)
149
+ # Must use find_by in this manner due to AR encryption
150
+ token_authority_session = find_by(access_token_jti: jti) || find_by(refresh_token_jti: jti)
151
+ execute_revocation(token_authority_session:)
152
+ end
153
+
154
+ # Revokes the session associated with an access token JTI.
155
+ #
156
+ # More efficient than revoke_for_token when the token type is known.
157
+ #
158
+ # @param access_token_jti [String] the access token's JWT identifier
159
+ #
160
+ # @return [void]
161
+ def self.revoke_for_access_token(access_token_jti:)
162
+ token_authority_session = find_by(access_token_jti:)
163
+ execute_revocation(token_authority_session:)
164
+ end
165
+
166
+ # Revokes the session associated with a refresh token JTI.
167
+ #
168
+ # More efficient than revoke_for_token when the token type is known.
169
+ #
170
+ # @param refresh_token_jti [String] the refresh token's JWT identifier
171
+ #
172
+ # @return [void]
173
+ def self.revoke_for_refresh_token(refresh_token_jti:)
174
+ token_authority_session = find_by(refresh_token_jti:)
175
+ execute_revocation(token_authority_session:)
176
+ end
177
+
178
+ class << self
179
+ # Internal helper to execute the revocation of a session.
180
+ #
181
+ # @param token_authority_session [TokenAuthority::Session, nil] the session to revoke
182
+ #
183
+ # @return [void]
184
+ #
185
+ # @api private
186
+ def execute_revocation(token_authority_session:)
187
+ return if token_authority_session.blank?
188
+
189
+ token_authority_session.revoke_self_and_active_session
190
+ end
191
+ end
192
+ end
193
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TokenAuthority
4
+ ##
5
+ # Value object for parsing and validating software statements (signed JWT metadata)
6
+ class SoftwareStatement
7
+ STANDARD_CLAIMS = %i[
8
+ redirect_uris
9
+ token_endpoint_auth_method
10
+ grant_types
11
+ response_types
12
+ client_name
13
+ client_uri
14
+ logo_uri
15
+ scope
16
+ contacts
17
+ tos_uri
18
+ policy_uri
19
+ jwks_uri
20
+ jwks
21
+ software_id
22
+ software_version
23
+ ].freeze
24
+
25
+ attr_reader :raw_jwt, :payload, :header
26
+
27
+ def initialize(raw_jwt:, payload:, header:, verified: false)
28
+ @raw_jwt = raw_jwt
29
+ @payload = payload.with_indifferent_access
30
+ @header = header.with_indifferent_access
31
+ @verified = verified
32
+ end
33
+
34
+ class << self
35
+ def decode(jwt)
36
+ payload, header = JWT.decode(jwt, nil, false)
37
+ new(raw_jwt: jwt, payload: payload, header: header, verified: false)
38
+ rescue JWT::DecodeError => e
39
+ raise TokenAuthority::InvalidSoftwareStatementError, e.message
40
+ end
41
+
42
+ def decode_and_verify(jwt, jwks:)
43
+ jwk_set = jwks.is_a?(JWT::JWK::Set) ? jwks : JWT::JWK::Set.new(jwks)
44
+ algorithms = jwk_set.map { |key| key[:alg] }.compact.uniq
45
+ algorithms = ["RS256"] if algorithms.empty?
46
+
47
+ payload, header = JWT.decode(jwt, nil, true, {algorithms: algorithms, jwks: jwk_set})
48
+ new(raw_jwt: jwt, payload: payload, header: header, verified: true)
49
+ rescue JWT::DecodeError => e
50
+ raise TokenAuthority::InvalidSoftwareStatementError, e.message
51
+ end
52
+ end
53
+
54
+ STANDARD_CLAIMS.each do |claim|
55
+ define_method(claim) { payload[claim] }
56
+ end
57
+
58
+ def claims
59
+ payload.slice(*STANDARD_CLAIMS)
60
+ end
61
+
62
+ def trusted?
63
+ @verified
64
+ end
65
+
66
+ def to_h
67
+ claims
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,25 @@
1
+ <div>
2
+ <h2><%= t('token_authority.authorization_grants.new.title') %></h2>
3
+ <p><%= t('token_authority.authorization_grants.new.lede') %></p>
4
+ <p><strong><%= client_name %></strong></p>
5
+ <% if resources.any? %>
6
+ <p><%= t('token_authority.authorization_grants.new.resources_heading') %></p>
7
+ <ul>
8
+ <% resources.each do |resource| %>
9
+ <li><%= resource_display_name(resource) %></li>
10
+ <% end %>
11
+ </ul>
12
+ <% end %>
13
+ <% if scopes.any? %>
14
+ <p><%= t('token_authority.authorization_grants.new.scopes_heading') %></p>
15
+ <ul>
16
+ <% scopes.each do |scope| %>
17
+ <li><%= scope_display_name(scope) %></li>
18
+ <% end %>
19
+ </ul>
20
+ <% end %>
21
+ <div>
22
+ <%= button_to t('token_authority.authorization_grants.new.reject_cta'), authorization_grants_path, params: { approve: false } %>
23
+ <%= button_to t('token_authority.authorization_grants.new.approve_cta'), authorization_grants_path, params: { approve: true } %>
24
+ </div>
25
+ </div>
@@ -0,0 +1,8 @@
1
+ <div>
2
+ <h2><%= t('token_authority.client_error.title') %></h2>
3
+ <p><%= t('token_authority.client_error.lede') %></p>
4
+ <div>
5
+ <h3><%= error_class %></h3>
6
+ <p><%= error_message %></p>
7
+ </div>
8
+ </div>