Class: Safire::Protocols::SmartMetadata
- Defined in:
- lib/safire/protocols/smart_metadata.rb
Overview
SMART Metadata obtained from SMART discovery endpoint. Attributes are defined as per SMART App Launch specification
Constant Summary collapse
- REQUIRED_ATTRIBUTES =
%i[ grant_types_supported token_endpoint capabilities code_challenge_methods_supported ].freeze
- OPTIONAL_ATTRIBUTES =
%i[ issuer jwks_uri authorization_endpoint token_endpoint_auth_methods_supported token_endpoint_auth_signing_alg_values_supported registration_endpoint associated_endpoints user_access_brand_bundle user_access_brand_identifier scopes_supported response_types_supported management_endpoint introspection_endpoint revocation_endpoint ].freeze
- ATTRIBUTES =
(REQUIRED_ATTRIBUTES | OPTIONAL_ATTRIBUTES).freeze
- SUPPORTED_ASYMMETRIC_ALGORITHMS =
Supported asymmetric signing algorithms (required by SMART spec)
%w[RS384 ES384].freeze
Instance Attribute Summary collapse
-
#associated_endpoints ⇒ Array<Hash>
readonly
List of objects for endpoints that share the same authorization mechanism as this FHIR endpoint, each with a “url” and “capabilities” array.
-
#authorization_endpoint ⇒ String
readonly
URL of the server’s OAuth2 Authorization Endpoint.
-
#capabilities ⇒ Array<String>
readonly
List of SMART capabilities supported by the server.
-
#code_challenge_methods_supported ⇒ Array<String>
readonly
List of PKCE code challenge methods supported.
-
#grant_types_supported ⇒ Array<String>
readonly
List of OAuth2 grant types supported at the token endpoint.
-
#introspection_endpoint ⇒ String
readonly
URL to a server’s introspection endpoint that can be used to validate a token.
-
#issuer ⇒ String
readonly
Conveying this system’s OpenID Connect Issuer URL.
-
#jwks_uri ⇒ String
readonly
URL of the server’s JSON Web Key Set endpoint.
-
#management_endpoint ⇒ String
readonly
URL where an end-user can view which applications currently have access to data and can make adjustments to these access rights.
-
#registration_endpoint ⇒ String
readonly
URL of the server’s OAuth2 Dynamic Client Registration Endpoint.
-
#response_types_supported ⇒ Array<String>
readonly
List of OAuth2 response types supported.
-
#revocation_endpoint ⇒ String
readonly
URL to a server’s revocation endpoint that can be used to revoke a token.
-
#scopes_supported ⇒ Array<String>
readonly
List of scopes a client may request.
-
#token_endpoint ⇒ String
readonly
URL of the server’s OAuth2 Token Endpoint.
-
#token_endpoint_auth_methods_supported ⇒ Array<String>
readonly
List of client authentication methods supported at the token endpoint.
-
#token_endpoint_auth_signing_alg_values_supported ⇒ Array<String>
readonly
List of signing algorithms supported for JWT-based client authentication.
-
#user_access_brand_bundle ⇒ String
readonly
URL for a Brand Bundle for user-facing applications.
-
#user_access_brand_identifier ⇒ String
readonly
Identifier for the primary entry in a Brand Bundle.
Instance Method Summary collapse
-
#asymmetric_signing_algorithms_supported ⇒ Array<String>
Returns the asymmetric signing algorithms supported by both client and server.
-
#ehr_launch_capability? ⇒ Boolean
Capability-only checks (does not verify required fields are present).
-
#initialize(metadata) ⇒ SmartMetadata
constructor
A new instance of SmartMetadata.
- #openid_connect_capability? ⇒ Boolean
- #standalone_launch_capability? ⇒ Boolean
-
#supports_asymmetric_auth? ⇒ Boolean
Checks if the server supports confidential asymmetric authentication.
-
#supports_backend_services? ⇒ Boolean
Checks if the server supports the SMART Backend Services workflow.
-
#supports_dynamic_registration? ⇒ Boolean
Checks whether the server supports Dynamic Client Registration (RFC 7591).
-
#supports_ehr_launch? ⇒ Boolean
Launch type support checks - requires both capability and authorization_endpoint.
- #supports_openid_connect? ⇒ Boolean
- #supports_post_based_authorization? ⇒ Boolean
-
#supports_public_auth? ⇒ Boolean
Checks if the server supports public client authentication.
- #supports_standalone_launch? ⇒ Boolean
-
#supports_symmetric_auth? ⇒ Boolean
Checks if the server supports confidential symmetric authentication.
-
#valid? ⇒ Boolean
Runs Safire's selected SMART App Launch 2.2.0 structural diagnostics.
Methods inherited from Entity
Constructor Details
#initialize(metadata) ⇒ SmartMetadata
Returns a new instance of SmartMetadata.
94 95 96 |
# File 'lib/safire/protocols/smart_metadata.rb', line 94 def initialize() super(, ATTRIBUTES) end |
Instance Attribute Details
#associated_endpoints ⇒ Array<Hash> (readonly)
Returns list of objects for endpoints that share the same authorization mechanism as this FHIR endpoint, each with a “url” and “capabilities” array. Optionally provided.
52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 |
# File 'lib/safire/protocols/smart_metadata.rb', line 52 class SmartMetadata < Safire::Entity REQUIRED_ATTRIBUTES = %i[ grant_types_supported token_endpoint capabilities code_challenge_methods_supported ].freeze OPTIONAL_ATTRIBUTES = %i[ issuer jwks_uri authorization_endpoint token_endpoint_auth_methods_supported token_endpoint_auth_signing_alg_values_supported registration_endpoint associated_endpoints user_access_brand_bundle user_access_brand_identifier scopes_supported response_types_supported management_endpoint introspection_endpoint revocation_endpoint ].freeze ATTRIBUTES = (REQUIRED_ATTRIBUTES | OPTIONAL_ATTRIBUTES).freeze ARRAY_ATTRIBUTE_ELEMENT_TYPES = { grant_types_supported: String, token_endpoint_auth_methods_supported: String, token_endpoint_auth_signing_alg_values_supported: String, associated_endpoints: Hash, scopes_supported: String, response_types_supported: String, capabilities: String, code_challenge_methods_supported: String }.freeze private_constant :ARRAY_ATTRIBUTE_ELEMENT_TYPES # Supported asymmetric signing algorithms (required by SMART spec) SUPPORTED_ASYMMETRIC_ALGORITHMS = %w[RS384 ES384].freeze attr_reader(*ATTRIBUTES) def initialize() super(, ATTRIBUTES) end # Runs Safire's selected SMART App Launch 2.2.0 structural diagnostics. # # This is a user-callable helper. Safire performs discovery without automatically # asserting server compliance — it is the caller's responsibility to invoke this # method when they wish to inspect these conformance signals. This helper is # not a complete server certification suite. # # Checks performed: # - All required fields are present # (token_endpoint, grant_types_supported, capabilities, code_challenge_methods_supported) # - Conditional fields present when their capability is advertised # (issuer + jwks_uri for sso-openid-connect; authorization_endpoint for launch types) # - Array-valued fields are arrays containing values of the documented type # - `code_challenge_methods_supported` includes 'S256' # (SMART App Launch 2.2.0, §Conformance — SHALL be included) # - `code_challenge_methods_supported` does NOT include 'plain' # (SMART App Launch 2.2.0, §Conformance — SHALL NOT be included) # # A warning is logged for each SMART 2.2.0 violation detected. # # @return [Boolean] true if all checks pass, false if any violation is found def valid? required_attrs = [*REQUIRED_ATTRIBUTES] required_attrs.push(:issuer, :jwks_uri) if issuer_and_jwks_uri_required? required_attrs.push(:authorization_endpoint) if missing_attrs = required_attrs.reject { |attr| public_send(attr) } missing_attrs.each do |attr| Safire.logger.warn("SMART metadata non-compliance: required field '#{attr}' is missing") end array_fields_valid = validate_array_fields! pkce_valid = validate_pkce_methods! missing_attrs.empty? && array_fields_valid && pkce_valid end # Launch type support checks - requires both capability and authorization_endpoint def supports_ehr_launch? ehr_launch_capability? && .present? end def supports_standalone_launch? standalone_launch_capability? && .present? end # Authentication type support checks # Checks if the server supports public client authentication. # @return [Boolean] true if server has client-public capability def supports_public_auth? capability?('client-public') end # Checks if the server supports confidential symmetric authentication. # @return [Boolean] true if server has capability and auth methods not advertised or includes client_secret_basic def supports_symmetric_auth? capability?('client-confidential-symmetric') && auth_method_supported?('client_secret_basic') end # Checks if the server supports the SMART Backend Services workflow. # @return [Boolean] true if the server advertises the client_credentials grant type # and supports private_key_jwt authentication (via {#supports_asymmetric_auth?}) def supports_backend_services? array_includes?(:grant_types_supported, 'client_credentials') && supports_asymmetric_auth? end # Checks if the server supports confidential asymmetric authentication. # @return [Boolean] true if server has capability, auth methods not advertised or includes private_key_jwt, # and has supported algorithms def supports_asymmetric_auth? capability?('client-confidential-asymmetric') && auth_method_supported?('private_key_jwt') && asymmetric_signing_algorithms_supported.any? end # Returns the asymmetric signing algorithms supported by both client and server. # If the server doesn't advertise algorithms, assumes it supports the required ones (RS384, ES384). # @return [Array<String>] list of supported algorithms def asymmetric_signing_algorithms_supported server_algs = token_endpoint_auth_signing_alg_values_supported return SUPPORTED_ASYMMETRIC_ALGORITHMS.dup if server_algs.nil? || server_algs == [] return [] unless array_of?(server_algs, String) server_algs & SUPPORTED_ASYMMETRIC_ALGORITHMS end # Feature support checks # Checks whether the server supports Dynamic Client Registration (RFC 7591). # @return [Boolean] true if the server advertises a +registration_endpoint+ def supports_dynamic_registration? registration_endpoint.present? end def capability?('authorize-post') end def supports_openid_connect? openid_connect_capability? && issuer.present? && jwks_uri.present? end # Capability-only checks (does not verify required fields are present) def ehr_launch_capability? capability?('launch-ehr') end def standalone_launch_capability? capability?('launch-standalone') end def openid_connect_capability? capability?('sso-openid-connect') end private def capability?(name) array_includes?(:capabilities, name) end def array_includes?(attribute, value) values = public_send(attribute) array_of?(values, String) && values.include?(value) end def auth_method_supported?(method) methods = token_endpoint_auth_methods_supported methods.nil? || methods == [] || array_includes?(:token_endpoint_auth_methods_supported, method) end def validate_array_fields! valid = true ARRAY_ATTRIBUTE_ELEMENT_TYPES.each do |attribute, element_type| value = public_send(attribute) next if value.nil? next if array_of?(value, element_type) Safire.logger.warn( "SMART metadata non-compliance: field '#{attribute}' must be an array containing only " \ "#{element_type == Hash ? 'objects' : 'strings'}" ) valid = false end valid end def array_of?(value, element_type) value.is_a?(Array) && value.all?(element_type) end def issuer_and_jwks_uri_required? openid_connect_capability? end def ehr_launch_capability? || standalone_launch_capability? end # Validates PKCE code challenge methods per SMART App Launch 2.2.0: # - 'S256' SHALL be included # - 'plain' SHALL NOT be included # # @return [Boolean] true if both conditions are satisfied def validate_pkce_methods! valid = true unless array_includes?(:code_challenge_methods_supported, 'S256') Safire.logger.warn( "SMART metadata non-compliance: 'S256' is missing from code_challenge_methods_supported " \ '(SMART App Launch 2.2.0 requires S256)' ) valid = false end if array_includes?(:code_challenge_methods_supported, 'plain') Safire.logger.warn( "SMART metadata non-compliance: 'plain' is present in code_challenge_methods_supported " \ '(SMART App Launch 2.2.0 prohibits plain)' ) valid = false end valid end end |
#authorization_endpoint ⇒ String (readonly)
Returns URL of the server’s OAuth2 Authorization Endpoint. Required if the server’s capabilities include launch-standalone or launch-ehr-launch.
52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 |
# File 'lib/safire/protocols/smart_metadata.rb', line 52 class SmartMetadata < Safire::Entity REQUIRED_ATTRIBUTES = %i[ grant_types_supported token_endpoint capabilities code_challenge_methods_supported ].freeze OPTIONAL_ATTRIBUTES = %i[ issuer jwks_uri authorization_endpoint token_endpoint_auth_methods_supported token_endpoint_auth_signing_alg_values_supported registration_endpoint associated_endpoints user_access_brand_bundle user_access_brand_identifier scopes_supported response_types_supported management_endpoint introspection_endpoint revocation_endpoint ].freeze ATTRIBUTES = (REQUIRED_ATTRIBUTES | OPTIONAL_ATTRIBUTES).freeze ARRAY_ATTRIBUTE_ELEMENT_TYPES = { grant_types_supported: String, token_endpoint_auth_methods_supported: String, token_endpoint_auth_signing_alg_values_supported: String, associated_endpoints: Hash, scopes_supported: String, response_types_supported: String, capabilities: String, code_challenge_methods_supported: String }.freeze private_constant :ARRAY_ATTRIBUTE_ELEMENT_TYPES # Supported asymmetric signing algorithms (required by SMART spec) SUPPORTED_ASYMMETRIC_ALGORITHMS = %w[RS384 ES384].freeze attr_reader(*ATTRIBUTES) def initialize() super(, ATTRIBUTES) end # Runs Safire's selected SMART App Launch 2.2.0 structural diagnostics. # # This is a user-callable helper. Safire performs discovery without automatically # asserting server compliance — it is the caller's responsibility to invoke this # method when they wish to inspect these conformance signals. This helper is # not a complete server certification suite. # # Checks performed: # - All required fields are present # (token_endpoint, grant_types_supported, capabilities, code_challenge_methods_supported) # - Conditional fields present when their capability is advertised # (issuer + jwks_uri for sso-openid-connect; authorization_endpoint for launch types) # - Array-valued fields are arrays containing values of the documented type # - `code_challenge_methods_supported` includes 'S256' # (SMART App Launch 2.2.0, §Conformance — SHALL be included) # - `code_challenge_methods_supported` does NOT include 'plain' # (SMART App Launch 2.2.0, §Conformance — SHALL NOT be included) # # A warning is logged for each SMART 2.2.0 violation detected. # # @return [Boolean] true if all checks pass, false if any violation is found def valid? required_attrs = [*REQUIRED_ATTRIBUTES] required_attrs.push(:issuer, :jwks_uri) if issuer_and_jwks_uri_required? required_attrs.push(:authorization_endpoint) if missing_attrs = required_attrs.reject { |attr| public_send(attr) } missing_attrs.each do |attr| Safire.logger.warn("SMART metadata non-compliance: required field '#{attr}' is missing") end array_fields_valid = validate_array_fields! pkce_valid = validate_pkce_methods! missing_attrs.empty? && array_fields_valid && pkce_valid end # Launch type support checks - requires both capability and authorization_endpoint def supports_ehr_launch? ehr_launch_capability? && .present? end def supports_standalone_launch? standalone_launch_capability? && .present? end # Authentication type support checks # Checks if the server supports public client authentication. # @return [Boolean] true if server has client-public capability def supports_public_auth? capability?('client-public') end # Checks if the server supports confidential symmetric authentication. # @return [Boolean] true if server has capability and auth methods not advertised or includes client_secret_basic def supports_symmetric_auth? capability?('client-confidential-symmetric') && auth_method_supported?('client_secret_basic') end # Checks if the server supports the SMART Backend Services workflow. # @return [Boolean] true if the server advertises the client_credentials grant type # and supports private_key_jwt authentication (via {#supports_asymmetric_auth?}) def supports_backend_services? array_includes?(:grant_types_supported, 'client_credentials') && supports_asymmetric_auth? end # Checks if the server supports confidential asymmetric authentication. # @return [Boolean] true if server has capability, auth methods not advertised or includes private_key_jwt, # and has supported algorithms def supports_asymmetric_auth? capability?('client-confidential-asymmetric') && auth_method_supported?('private_key_jwt') && asymmetric_signing_algorithms_supported.any? end # Returns the asymmetric signing algorithms supported by both client and server. # If the server doesn't advertise algorithms, assumes it supports the required ones (RS384, ES384). # @return [Array<String>] list of supported algorithms def asymmetric_signing_algorithms_supported server_algs = token_endpoint_auth_signing_alg_values_supported return SUPPORTED_ASYMMETRIC_ALGORITHMS.dup if server_algs.nil? || server_algs == [] return [] unless array_of?(server_algs, String) server_algs & SUPPORTED_ASYMMETRIC_ALGORITHMS end # Feature support checks # Checks whether the server supports Dynamic Client Registration (RFC 7591). # @return [Boolean] true if the server advertises a +registration_endpoint+ def supports_dynamic_registration? registration_endpoint.present? end def capability?('authorize-post') end def supports_openid_connect? openid_connect_capability? && issuer.present? && jwks_uri.present? end # Capability-only checks (does not verify required fields are present) def ehr_launch_capability? capability?('launch-ehr') end def standalone_launch_capability? capability?('launch-standalone') end def openid_connect_capability? capability?('sso-openid-connect') end private def capability?(name) array_includes?(:capabilities, name) end def array_includes?(attribute, value) values = public_send(attribute) array_of?(values, String) && values.include?(value) end def auth_method_supported?(method) methods = token_endpoint_auth_methods_supported methods.nil? || methods == [] || array_includes?(:token_endpoint_auth_methods_supported, method) end def validate_array_fields! valid = true ARRAY_ATTRIBUTE_ELEMENT_TYPES.each do |attribute, element_type| value = public_send(attribute) next if value.nil? next if array_of?(value, element_type) Safire.logger.warn( "SMART metadata non-compliance: field '#{attribute}' must be an array containing only " \ "#{element_type == Hash ? 'objects' : 'strings'}" ) valid = false end valid end def array_of?(value, element_type) value.is_a?(Array) && value.all?(element_type) end def issuer_and_jwks_uri_required? openid_connect_capability? end def ehr_launch_capability? || standalone_launch_capability? end # Validates PKCE code challenge methods per SMART App Launch 2.2.0: # - 'S256' SHALL be included # - 'plain' SHALL NOT be included # # @return [Boolean] true if both conditions are satisfied def validate_pkce_methods! valid = true unless array_includes?(:code_challenge_methods_supported, 'S256') Safire.logger.warn( "SMART metadata non-compliance: 'S256' is missing from code_challenge_methods_supported " \ '(SMART App Launch 2.2.0 requires S256)' ) valid = false end if array_includes?(:code_challenge_methods_supported, 'plain') Safire.logger.warn( "SMART metadata non-compliance: 'plain' is present in code_challenge_methods_supported " \ '(SMART App Launch 2.2.0 prohibits plain)' ) valid = false end valid end end |
#capabilities ⇒ Array<String> (readonly)
Returns list of SMART capabilities supported by the server.
52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 |
# File 'lib/safire/protocols/smart_metadata.rb', line 52 class SmartMetadata < Safire::Entity REQUIRED_ATTRIBUTES = %i[ grant_types_supported token_endpoint capabilities code_challenge_methods_supported ].freeze OPTIONAL_ATTRIBUTES = %i[ issuer jwks_uri authorization_endpoint token_endpoint_auth_methods_supported token_endpoint_auth_signing_alg_values_supported registration_endpoint associated_endpoints user_access_brand_bundle user_access_brand_identifier scopes_supported response_types_supported management_endpoint introspection_endpoint revocation_endpoint ].freeze ATTRIBUTES = (REQUIRED_ATTRIBUTES | OPTIONAL_ATTRIBUTES).freeze ARRAY_ATTRIBUTE_ELEMENT_TYPES = { grant_types_supported: String, token_endpoint_auth_methods_supported: String, token_endpoint_auth_signing_alg_values_supported: String, associated_endpoints: Hash, scopes_supported: String, response_types_supported: String, capabilities: String, code_challenge_methods_supported: String }.freeze private_constant :ARRAY_ATTRIBUTE_ELEMENT_TYPES # Supported asymmetric signing algorithms (required by SMART spec) SUPPORTED_ASYMMETRIC_ALGORITHMS = %w[RS384 ES384].freeze attr_reader(*ATTRIBUTES) def initialize() super(, ATTRIBUTES) end # Runs Safire's selected SMART App Launch 2.2.0 structural diagnostics. # # This is a user-callable helper. Safire performs discovery without automatically # asserting server compliance — it is the caller's responsibility to invoke this # method when they wish to inspect these conformance signals. This helper is # not a complete server certification suite. # # Checks performed: # - All required fields are present # (token_endpoint, grant_types_supported, capabilities, code_challenge_methods_supported) # - Conditional fields present when their capability is advertised # (issuer + jwks_uri for sso-openid-connect; authorization_endpoint for launch types) # - Array-valued fields are arrays containing values of the documented type # - `code_challenge_methods_supported` includes 'S256' # (SMART App Launch 2.2.0, §Conformance — SHALL be included) # - `code_challenge_methods_supported` does NOT include 'plain' # (SMART App Launch 2.2.0, §Conformance — SHALL NOT be included) # # A warning is logged for each SMART 2.2.0 violation detected. # # @return [Boolean] true if all checks pass, false if any violation is found def valid? required_attrs = [*REQUIRED_ATTRIBUTES] required_attrs.push(:issuer, :jwks_uri) if issuer_and_jwks_uri_required? required_attrs.push(:authorization_endpoint) if missing_attrs = required_attrs.reject { |attr| public_send(attr) } missing_attrs.each do |attr| Safire.logger.warn("SMART metadata non-compliance: required field '#{attr}' is missing") end array_fields_valid = validate_array_fields! pkce_valid = validate_pkce_methods! missing_attrs.empty? && array_fields_valid && pkce_valid end # Launch type support checks - requires both capability and authorization_endpoint def supports_ehr_launch? ehr_launch_capability? && .present? end def supports_standalone_launch? standalone_launch_capability? && .present? end # Authentication type support checks # Checks if the server supports public client authentication. # @return [Boolean] true if server has client-public capability def supports_public_auth? capability?('client-public') end # Checks if the server supports confidential symmetric authentication. # @return [Boolean] true if server has capability and auth methods not advertised or includes client_secret_basic def supports_symmetric_auth? capability?('client-confidential-symmetric') && auth_method_supported?('client_secret_basic') end # Checks if the server supports the SMART Backend Services workflow. # @return [Boolean] true if the server advertises the client_credentials grant type # and supports private_key_jwt authentication (via {#supports_asymmetric_auth?}) def supports_backend_services? array_includes?(:grant_types_supported, 'client_credentials') && supports_asymmetric_auth? end # Checks if the server supports confidential asymmetric authentication. # @return [Boolean] true if server has capability, auth methods not advertised or includes private_key_jwt, # and has supported algorithms def supports_asymmetric_auth? capability?('client-confidential-asymmetric') && auth_method_supported?('private_key_jwt') && asymmetric_signing_algorithms_supported.any? end # Returns the asymmetric signing algorithms supported by both client and server. # If the server doesn't advertise algorithms, assumes it supports the required ones (RS384, ES384). # @return [Array<String>] list of supported algorithms def asymmetric_signing_algorithms_supported server_algs = token_endpoint_auth_signing_alg_values_supported return SUPPORTED_ASYMMETRIC_ALGORITHMS.dup if server_algs.nil? || server_algs == [] return [] unless array_of?(server_algs, String) server_algs & SUPPORTED_ASYMMETRIC_ALGORITHMS end # Feature support checks # Checks whether the server supports Dynamic Client Registration (RFC 7591). # @return [Boolean] true if the server advertises a +registration_endpoint+ def supports_dynamic_registration? registration_endpoint.present? end def capability?('authorize-post') end def supports_openid_connect? openid_connect_capability? && issuer.present? && jwks_uri.present? end # Capability-only checks (does not verify required fields are present) def ehr_launch_capability? capability?('launch-ehr') end def standalone_launch_capability? capability?('launch-standalone') end def openid_connect_capability? capability?('sso-openid-connect') end private def capability?(name) array_includes?(:capabilities, name) end def array_includes?(attribute, value) values = public_send(attribute) array_of?(values, String) && values.include?(value) end def auth_method_supported?(method) methods = token_endpoint_auth_methods_supported methods.nil? || methods == [] || array_includes?(:token_endpoint_auth_methods_supported, method) end def validate_array_fields! valid = true ARRAY_ATTRIBUTE_ELEMENT_TYPES.each do |attribute, element_type| value = public_send(attribute) next if value.nil? next if array_of?(value, element_type) Safire.logger.warn( "SMART metadata non-compliance: field '#{attribute}' must be an array containing only " \ "#{element_type == Hash ? 'objects' : 'strings'}" ) valid = false end valid end def array_of?(value, element_type) value.is_a?(Array) && value.all?(element_type) end def issuer_and_jwks_uri_required? openid_connect_capability? end def ehr_launch_capability? || standalone_launch_capability? end # Validates PKCE code challenge methods per SMART App Launch 2.2.0: # - 'S256' SHALL be included # - 'plain' SHALL NOT be included # # @return [Boolean] true if both conditions are satisfied def validate_pkce_methods! valid = true unless array_includes?(:code_challenge_methods_supported, 'S256') Safire.logger.warn( "SMART metadata non-compliance: 'S256' is missing from code_challenge_methods_supported " \ '(SMART App Launch 2.2.0 requires S256)' ) valid = false end if array_includes?(:code_challenge_methods_supported, 'plain') Safire.logger.warn( "SMART metadata non-compliance: 'plain' is present in code_challenge_methods_supported " \ '(SMART App Launch 2.2.0 prohibits plain)' ) valid = false end valid end end |
#code_challenge_methods_supported ⇒ Array<String> (readonly)
Returns list of PKCE code challenge methods supported. Should include "S256". Should not include "plain". See #valid? for compliance checks.
52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 |
# File 'lib/safire/protocols/smart_metadata.rb', line 52 class SmartMetadata < Safire::Entity REQUIRED_ATTRIBUTES = %i[ grant_types_supported token_endpoint capabilities code_challenge_methods_supported ].freeze OPTIONAL_ATTRIBUTES = %i[ issuer jwks_uri authorization_endpoint token_endpoint_auth_methods_supported token_endpoint_auth_signing_alg_values_supported registration_endpoint associated_endpoints user_access_brand_bundle user_access_brand_identifier scopes_supported response_types_supported management_endpoint introspection_endpoint revocation_endpoint ].freeze ATTRIBUTES = (REQUIRED_ATTRIBUTES | OPTIONAL_ATTRIBUTES).freeze ARRAY_ATTRIBUTE_ELEMENT_TYPES = { grant_types_supported: String, token_endpoint_auth_methods_supported: String, token_endpoint_auth_signing_alg_values_supported: String, associated_endpoints: Hash, scopes_supported: String, response_types_supported: String, capabilities: String, code_challenge_methods_supported: String }.freeze private_constant :ARRAY_ATTRIBUTE_ELEMENT_TYPES # Supported asymmetric signing algorithms (required by SMART spec) SUPPORTED_ASYMMETRIC_ALGORITHMS = %w[RS384 ES384].freeze attr_reader(*ATTRIBUTES) def initialize() super(, ATTRIBUTES) end # Runs Safire's selected SMART App Launch 2.2.0 structural diagnostics. # # This is a user-callable helper. Safire performs discovery without automatically # asserting server compliance — it is the caller's responsibility to invoke this # method when they wish to inspect these conformance signals. This helper is # not a complete server certification suite. # # Checks performed: # - All required fields are present # (token_endpoint, grant_types_supported, capabilities, code_challenge_methods_supported) # - Conditional fields present when their capability is advertised # (issuer + jwks_uri for sso-openid-connect; authorization_endpoint for launch types) # - Array-valued fields are arrays containing values of the documented type # - `code_challenge_methods_supported` includes 'S256' # (SMART App Launch 2.2.0, §Conformance — SHALL be included) # - `code_challenge_methods_supported` does NOT include 'plain' # (SMART App Launch 2.2.0, §Conformance — SHALL NOT be included) # # A warning is logged for each SMART 2.2.0 violation detected. # # @return [Boolean] true if all checks pass, false if any violation is found def valid? required_attrs = [*REQUIRED_ATTRIBUTES] required_attrs.push(:issuer, :jwks_uri) if issuer_and_jwks_uri_required? required_attrs.push(:authorization_endpoint) if missing_attrs = required_attrs.reject { |attr| public_send(attr) } missing_attrs.each do |attr| Safire.logger.warn("SMART metadata non-compliance: required field '#{attr}' is missing") end array_fields_valid = validate_array_fields! pkce_valid = validate_pkce_methods! missing_attrs.empty? && array_fields_valid && pkce_valid end # Launch type support checks - requires both capability and authorization_endpoint def supports_ehr_launch? ehr_launch_capability? && .present? end def supports_standalone_launch? standalone_launch_capability? && .present? end # Authentication type support checks # Checks if the server supports public client authentication. # @return [Boolean] true if server has client-public capability def supports_public_auth? capability?('client-public') end # Checks if the server supports confidential symmetric authentication. # @return [Boolean] true if server has capability and auth methods not advertised or includes client_secret_basic def supports_symmetric_auth? capability?('client-confidential-symmetric') && auth_method_supported?('client_secret_basic') end # Checks if the server supports the SMART Backend Services workflow. # @return [Boolean] true if the server advertises the client_credentials grant type # and supports private_key_jwt authentication (via {#supports_asymmetric_auth?}) def supports_backend_services? array_includes?(:grant_types_supported, 'client_credentials') && supports_asymmetric_auth? end # Checks if the server supports confidential asymmetric authentication. # @return [Boolean] true if server has capability, auth methods not advertised or includes private_key_jwt, # and has supported algorithms def supports_asymmetric_auth? capability?('client-confidential-asymmetric') && auth_method_supported?('private_key_jwt') && asymmetric_signing_algorithms_supported.any? end # Returns the asymmetric signing algorithms supported by both client and server. # If the server doesn't advertise algorithms, assumes it supports the required ones (RS384, ES384). # @return [Array<String>] list of supported algorithms def asymmetric_signing_algorithms_supported server_algs = token_endpoint_auth_signing_alg_values_supported return SUPPORTED_ASYMMETRIC_ALGORITHMS.dup if server_algs.nil? || server_algs == [] return [] unless array_of?(server_algs, String) server_algs & SUPPORTED_ASYMMETRIC_ALGORITHMS end # Feature support checks # Checks whether the server supports Dynamic Client Registration (RFC 7591). # @return [Boolean] true if the server advertises a +registration_endpoint+ def supports_dynamic_registration? registration_endpoint.present? end def capability?('authorize-post') end def supports_openid_connect? openid_connect_capability? && issuer.present? && jwks_uri.present? end # Capability-only checks (does not verify required fields are present) def ehr_launch_capability? capability?('launch-ehr') end def standalone_launch_capability? capability?('launch-standalone') end def openid_connect_capability? capability?('sso-openid-connect') end private def capability?(name) array_includes?(:capabilities, name) end def array_includes?(attribute, value) values = public_send(attribute) array_of?(values, String) && values.include?(value) end def auth_method_supported?(method) methods = token_endpoint_auth_methods_supported methods.nil? || methods == [] || array_includes?(:token_endpoint_auth_methods_supported, method) end def validate_array_fields! valid = true ARRAY_ATTRIBUTE_ELEMENT_TYPES.each do |attribute, element_type| value = public_send(attribute) next if value.nil? next if array_of?(value, element_type) Safire.logger.warn( "SMART metadata non-compliance: field '#{attribute}' must be an array containing only " \ "#{element_type == Hash ? 'objects' : 'strings'}" ) valid = false end valid end def array_of?(value, element_type) value.is_a?(Array) && value.all?(element_type) end def issuer_and_jwks_uri_required? openid_connect_capability? end def ehr_launch_capability? || standalone_launch_capability? end # Validates PKCE code challenge methods per SMART App Launch 2.2.0: # - 'S256' SHALL be included # - 'plain' SHALL NOT be included # # @return [Boolean] true if both conditions are satisfied def validate_pkce_methods! valid = true unless array_includes?(:code_challenge_methods_supported, 'S256') Safire.logger.warn( "SMART metadata non-compliance: 'S256' is missing from code_challenge_methods_supported " \ '(SMART App Launch 2.2.0 requires S256)' ) valid = false end if array_includes?(:code_challenge_methods_supported, 'plain') Safire.logger.warn( "SMART metadata non-compliance: 'plain' is present in code_challenge_methods_supported " \ '(SMART App Launch 2.2.0 prohibits plain)' ) valid = false end valid end end |
#grant_types_supported ⇒ Array<String> (readonly)
Returns list of OAuth2 grant types supported at the token endpoint.
52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 |
# File 'lib/safire/protocols/smart_metadata.rb', line 52 class SmartMetadata < Safire::Entity REQUIRED_ATTRIBUTES = %i[ grant_types_supported token_endpoint capabilities code_challenge_methods_supported ].freeze OPTIONAL_ATTRIBUTES = %i[ issuer jwks_uri authorization_endpoint token_endpoint_auth_methods_supported token_endpoint_auth_signing_alg_values_supported registration_endpoint associated_endpoints user_access_brand_bundle user_access_brand_identifier scopes_supported response_types_supported management_endpoint introspection_endpoint revocation_endpoint ].freeze ATTRIBUTES = (REQUIRED_ATTRIBUTES | OPTIONAL_ATTRIBUTES).freeze ARRAY_ATTRIBUTE_ELEMENT_TYPES = { grant_types_supported: String, token_endpoint_auth_methods_supported: String, token_endpoint_auth_signing_alg_values_supported: String, associated_endpoints: Hash, scopes_supported: String, response_types_supported: String, capabilities: String, code_challenge_methods_supported: String }.freeze private_constant :ARRAY_ATTRIBUTE_ELEMENT_TYPES # Supported asymmetric signing algorithms (required by SMART spec) SUPPORTED_ASYMMETRIC_ALGORITHMS = %w[RS384 ES384].freeze attr_reader(*ATTRIBUTES) def initialize() super(, ATTRIBUTES) end # Runs Safire's selected SMART App Launch 2.2.0 structural diagnostics. # # This is a user-callable helper. Safire performs discovery without automatically # asserting server compliance — it is the caller's responsibility to invoke this # method when they wish to inspect these conformance signals. This helper is # not a complete server certification suite. # # Checks performed: # - All required fields are present # (token_endpoint, grant_types_supported, capabilities, code_challenge_methods_supported) # - Conditional fields present when their capability is advertised # (issuer + jwks_uri for sso-openid-connect; authorization_endpoint for launch types) # - Array-valued fields are arrays containing values of the documented type # - `code_challenge_methods_supported` includes 'S256' # (SMART App Launch 2.2.0, §Conformance — SHALL be included) # - `code_challenge_methods_supported` does NOT include 'plain' # (SMART App Launch 2.2.0, §Conformance — SHALL NOT be included) # # A warning is logged for each SMART 2.2.0 violation detected. # # @return [Boolean] true if all checks pass, false if any violation is found def valid? required_attrs = [*REQUIRED_ATTRIBUTES] required_attrs.push(:issuer, :jwks_uri) if issuer_and_jwks_uri_required? required_attrs.push(:authorization_endpoint) if missing_attrs = required_attrs.reject { |attr| public_send(attr) } missing_attrs.each do |attr| Safire.logger.warn("SMART metadata non-compliance: required field '#{attr}' is missing") end array_fields_valid = validate_array_fields! pkce_valid = validate_pkce_methods! missing_attrs.empty? && array_fields_valid && pkce_valid end # Launch type support checks - requires both capability and authorization_endpoint def supports_ehr_launch? ehr_launch_capability? && .present? end def supports_standalone_launch? standalone_launch_capability? && .present? end # Authentication type support checks # Checks if the server supports public client authentication. # @return [Boolean] true if server has client-public capability def supports_public_auth? capability?('client-public') end # Checks if the server supports confidential symmetric authentication. # @return [Boolean] true if server has capability and auth methods not advertised or includes client_secret_basic def supports_symmetric_auth? capability?('client-confidential-symmetric') && auth_method_supported?('client_secret_basic') end # Checks if the server supports the SMART Backend Services workflow. # @return [Boolean] true if the server advertises the client_credentials grant type # and supports private_key_jwt authentication (via {#supports_asymmetric_auth?}) def supports_backend_services? array_includes?(:grant_types_supported, 'client_credentials') && supports_asymmetric_auth? end # Checks if the server supports confidential asymmetric authentication. # @return [Boolean] true if server has capability, auth methods not advertised or includes private_key_jwt, # and has supported algorithms def supports_asymmetric_auth? capability?('client-confidential-asymmetric') && auth_method_supported?('private_key_jwt') && asymmetric_signing_algorithms_supported.any? end # Returns the asymmetric signing algorithms supported by both client and server. # If the server doesn't advertise algorithms, assumes it supports the required ones (RS384, ES384). # @return [Array<String>] list of supported algorithms def asymmetric_signing_algorithms_supported server_algs = token_endpoint_auth_signing_alg_values_supported return SUPPORTED_ASYMMETRIC_ALGORITHMS.dup if server_algs.nil? || server_algs == [] return [] unless array_of?(server_algs, String) server_algs & SUPPORTED_ASYMMETRIC_ALGORITHMS end # Feature support checks # Checks whether the server supports Dynamic Client Registration (RFC 7591). # @return [Boolean] true if the server advertises a +registration_endpoint+ def supports_dynamic_registration? registration_endpoint.present? end def capability?('authorize-post') end def supports_openid_connect? openid_connect_capability? && issuer.present? && jwks_uri.present? end # Capability-only checks (does not verify required fields are present) def ehr_launch_capability? capability?('launch-ehr') end def standalone_launch_capability? capability?('launch-standalone') end def openid_connect_capability? capability?('sso-openid-connect') end private def capability?(name) array_includes?(:capabilities, name) end def array_includes?(attribute, value) values = public_send(attribute) array_of?(values, String) && values.include?(value) end def auth_method_supported?(method) methods = token_endpoint_auth_methods_supported methods.nil? || methods == [] || array_includes?(:token_endpoint_auth_methods_supported, method) end def validate_array_fields! valid = true ARRAY_ATTRIBUTE_ELEMENT_TYPES.each do |attribute, element_type| value = public_send(attribute) next if value.nil? next if array_of?(value, element_type) Safire.logger.warn( "SMART metadata non-compliance: field '#{attribute}' must be an array containing only " \ "#{element_type == Hash ? 'objects' : 'strings'}" ) valid = false end valid end def array_of?(value, element_type) value.is_a?(Array) && value.all?(element_type) end def issuer_and_jwks_uri_required? openid_connect_capability? end def ehr_launch_capability? || standalone_launch_capability? end # Validates PKCE code challenge methods per SMART App Launch 2.2.0: # - 'S256' SHALL be included # - 'plain' SHALL NOT be included # # @return [Boolean] true if both conditions are satisfied def validate_pkce_methods! valid = true unless array_includes?(:code_challenge_methods_supported, 'S256') Safire.logger.warn( "SMART metadata non-compliance: 'S256' is missing from code_challenge_methods_supported " \ '(SMART App Launch 2.2.0 requires S256)' ) valid = false end if array_includes?(:code_challenge_methods_supported, 'plain') Safire.logger.warn( "SMART metadata non-compliance: 'plain' is present in code_challenge_methods_supported " \ '(SMART App Launch 2.2.0 prohibits plain)' ) valid = false end valid end end |
#introspection_endpoint ⇒ String (readonly)
Returns URL to a server’s introspection endpoint that can be used to validate a token. Optionally provided.
52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 |
# File 'lib/safire/protocols/smart_metadata.rb', line 52 class SmartMetadata < Safire::Entity REQUIRED_ATTRIBUTES = %i[ grant_types_supported token_endpoint capabilities code_challenge_methods_supported ].freeze OPTIONAL_ATTRIBUTES = %i[ issuer jwks_uri authorization_endpoint token_endpoint_auth_methods_supported token_endpoint_auth_signing_alg_values_supported registration_endpoint associated_endpoints user_access_brand_bundle user_access_brand_identifier scopes_supported response_types_supported management_endpoint introspection_endpoint revocation_endpoint ].freeze ATTRIBUTES = (REQUIRED_ATTRIBUTES | OPTIONAL_ATTRIBUTES).freeze ARRAY_ATTRIBUTE_ELEMENT_TYPES = { grant_types_supported: String, token_endpoint_auth_methods_supported: String, token_endpoint_auth_signing_alg_values_supported: String, associated_endpoints: Hash, scopes_supported: String, response_types_supported: String, capabilities: String, code_challenge_methods_supported: String }.freeze private_constant :ARRAY_ATTRIBUTE_ELEMENT_TYPES # Supported asymmetric signing algorithms (required by SMART spec) SUPPORTED_ASYMMETRIC_ALGORITHMS = %w[RS384 ES384].freeze attr_reader(*ATTRIBUTES) def initialize() super(, ATTRIBUTES) end # Runs Safire's selected SMART App Launch 2.2.0 structural diagnostics. # # This is a user-callable helper. Safire performs discovery without automatically # asserting server compliance — it is the caller's responsibility to invoke this # method when they wish to inspect these conformance signals. This helper is # not a complete server certification suite. # # Checks performed: # - All required fields are present # (token_endpoint, grant_types_supported, capabilities, code_challenge_methods_supported) # - Conditional fields present when their capability is advertised # (issuer + jwks_uri for sso-openid-connect; authorization_endpoint for launch types) # - Array-valued fields are arrays containing values of the documented type # - `code_challenge_methods_supported` includes 'S256' # (SMART App Launch 2.2.0, §Conformance — SHALL be included) # - `code_challenge_methods_supported` does NOT include 'plain' # (SMART App Launch 2.2.0, §Conformance — SHALL NOT be included) # # A warning is logged for each SMART 2.2.0 violation detected. # # @return [Boolean] true if all checks pass, false if any violation is found def valid? required_attrs = [*REQUIRED_ATTRIBUTES] required_attrs.push(:issuer, :jwks_uri) if issuer_and_jwks_uri_required? required_attrs.push(:authorization_endpoint) if missing_attrs = required_attrs.reject { |attr| public_send(attr) } missing_attrs.each do |attr| Safire.logger.warn("SMART metadata non-compliance: required field '#{attr}' is missing") end array_fields_valid = validate_array_fields! pkce_valid = validate_pkce_methods! missing_attrs.empty? && array_fields_valid && pkce_valid end # Launch type support checks - requires both capability and authorization_endpoint def supports_ehr_launch? ehr_launch_capability? && .present? end def supports_standalone_launch? standalone_launch_capability? && .present? end # Authentication type support checks # Checks if the server supports public client authentication. # @return [Boolean] true if server has client-public capability def supports_public_auth? capability?('client-public') end # Checks if the server supports confidential symmetric authentication. # @return [Boolean] true if server has capability and auth methods not advertised or includes client_secret_basic def supports_symmetric_auth? capability?('client-confidential-symmetric') && auth_method_supported?('client_secret_basic') end # Checks if the server supports the SMART Backend Services workflow. # @return [Boolean] true if the server advertises the client_credentials grant type # and supports private_key_jwt authentication (via {#supports_asymmetric_auth?}) def supports_backend_services? array_includes?(:grant_types_supported, 'client_credentials') && supports_asymmetric_auth? end # Checks if the server supports confidential asymmetric authentication. # @return [Boolean] true if server has capability, auth methods not advertised or includes private_key_jwt, # and has supported algorithms def supports_asymmetric_auth? capability?('client-confidential-asymmetric') && auth_method_supported?('private_key_jwt') && asymmetric_signing_algorithms_supported.any? end # Returns the asymmetric signing algorithms supported by both client and server. # If the server doesn't advertise algorithms, assumes it supports the required ones (RS384, ES384). # @return [Array<String>] list of supported algorithms def asymmetric_signing_algorithms_supported server_algs = token_endpoint_auth_signing_alg_values_supported return SUPPORTED_ASYMMETRIC_ALGORITHMS.dup if server_algs.nil? || server_algs == [] return [] unless array_of?(server_algs, String) server_algs & SUPPORTED_ASYMMETRIC_ALGORITHMS end # Feature support checks # Checks whether the server supports Dynamic Client Registration (RFC 7591). # @return [Boolean] true if the server advertises a +registration_endpoint+ def supports_dynamic_registration? registration_endpoint.present? end def capability?('authorize-post') end def supports_openid_connect? openid_connect_capability? && issuer.present? && jwks_uri.present? end # Capability-only checks (does not verify required fields are present) def ehr_launch_capability? capability?('launch-ehr') end def standalone_launch_capability? capability?('launch-standalone') end def openid_connect_capability? capability?('sso-openid-connect') end private def capability?(name) array_includes?(:capabilities, name) end def array_includes?(attribute, value) values = public_send(attribute) array_of?(values, String) && values.include?(value) end def auth_method_supported?(method) methods = token_endpoint_auth_methods_supported methods.nil? || methods == [] || array_includes?(:token_endpoint_auth_methods_supported, method) end def validate_array_fields! valid = true ARRAY_ATTRIBUTE_ELEMENT_TYPES.each do |attribute, element_type| value = public_send(attribute) next if value.nil? next if array_of?(value, element_type) Safire.logger.warn( "SMART metadata non-compliance: field '#{attribute}' must be an array containing only " \ "#{element_type == Hash ? 'objects' : 'strings'}" ) valid = false end valid end def array_of?(value, element_type) value.is_a?(Array) && value.all?(element_type) end def issuer_and_jwks_uri_required? openid_connect_capability? end def ehr_launch_capability? || standalone_launch_capability? end # Validates PKCE code challenge methods per SMART App Launch 2.2.0: # - 'S256' SHALL be included # - 'plain' SHALL NOT be included # # @return [Boolean] true if both conditions are satisfied def validate_pkce_methods! valid = true unless array_includes?(:code_challenge_methods_supported, 'S256') Safire.logger.warn( "SMART metadata non-compliance: 'S256' is missing from code_challenge_methods_supported " \ '(SMART App Launch 2.2.0 requires S256)' ) valid = false end if array_includes?(:code_challenge_methods_supported, 'plain') Safire.logger.warn( "SMART metadata non-compliance: 'plain' is present in code_challenge_methods_supported " \ '(SMART App Launch 2.2.0 prohibits plain)' ) valid = false end valid end end |
#issuer ⇒ String (readonly)
Returns conveying this system’s OpenID Connect Issuer URL. Required if the server’s capabilities include sso-openid-connect.
52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 |
# File 'lib/safire/protocols/smart_metadata.rb', line 52 class SmartMetadata < Safire::Entity REQUIRED_ATTRIBUTES = %i[ grant_types_supported token_endpoint capabilities code_challenge_methods_supported ].freeze OPTIONAL_ATTRIBUTES = %i[ issuer jwks_uri authorization_endpoint token_endpoint_auth_methods_supported token_endpoint_auth_signing_alg_values_supported registration_endpoint associated_endpoints user_access_brand_bundle user_access_brand_identifier scopes_supported response_types_supported management_endpoint introspection_endpoint revocation_endpoint ].freeze ATTRIBUTES = (REQUIRED_ATTRIBUTES | OPTIONAL_ATTRIBUTES).freeze ARRAY_ATTRIBUTE_ELEMENT_TYPES = { grant_types_supported: String, token_endpoint_auth_methods_supported: String, token_endpoint_auth_signing_alg_values_supported: String, associated_endpoints: Hash, scopes_supported: String, response_types_supported: String, capabilities: String, code_challenge_methods_supported: String }.freeze private_constant :ARRAY_ATTRIBUTE_ELEMENT_TYPES # Supported asymmetric signing algorithms (required by SMART spec) SUPPORTED_ASYMMETRIC_ALGORITHMS = %w[RS384 ES384].freeze attr_reader(*ATTRIBUTES) def initialize() super(, ATTRIBUTES) end # Runs Safire's selected SMART App Launch 2.2.0 structural diagnostics. # # This is a user-callable helper. Safire performs discovery without automatically # asserting server compliance — it is the caller's responsibility to invoke this # method when they wish to inspect these conformance signals. This helper is # not a complete server certification suite. # # Checks performed: # - All required fields are present # (token_endpoint, grant_types_supported, capabilities, code_challenge_methods_supported) # - Conditional fields present when their capability is advertised # (issuer + jwks_uri for sso-openid-connect; authorization_endpoint for launch types) # - Array-valued fields are arrays containing values of the documented type # - `code_challenge_methods_supported` includes 'S256' # (SMART App Launch 2.2.0, §Conformance — SHALL be included) # - `code_challenge_methods_supported` does NOT include 'plain' # (SMART App Launch 2.2.0, §Conformance — SHALL NOT be included) # # A warning is logged for each SMART 2.2.0 violation detected. # # @return [Boolean] true if all checks pass, false if any violation is found def valid? required_attrs = [*REQUIRED_ATTRIBUTES] required_attrs.push(:issuer, :jwks_uri) if issuer_and_jwks_uri_required? required_attrs.push(:authorization_endpoint) if missing_attrs = required_attrs.reject { |attr| public_send(attr) } missing_attrs.each do |attr| Safire.logger.warn("SMART metadata non-compliance: required field '#{attr}' is missing") end array_fields_valid = validate_array_fields! pkce_valid = validate_pkce_methods! missing_attrs.empty? && array_fields_valid && pkce_valid end # Launch type support checks - requires both capability and authorization_endpoint def supports_ehr_launch? ehr_launch_capability? && .present? end def supports_standalone_launch? standalone_launch_capability? && .present? end # Authentication type support checks # Checks if the server supports public client authentication. # @return [Boolean] true if server has client-public capability def supports_public_auth? capability?('client-public') end # Checks if the server supports confidential symmetric authentication. # @return [Boolean] true if server has capability and auth methods not advertised or includes client_secret_basic def supports_symmetric_auth? capability?('client-confidential-symmetric') && auth_method_supported?('client_secret_basic') end # Checks if the server supports the SMART Backend Services workflow. # @return [Boolean] true if the server advertises the client_credentials grant type # and supports private_key_jwt authentication (via {#supports_asymmetric_auth?}) def supports_backend_services? array_includes?(:grant_types_supported, 'client_credentials') && supports_asymmetric_auth? end # Checks if the server supports confidential asymmetric authentication. # @return [Boolean] true if server has capability, auth methods not advertised or includes private_key_jwt, # and has supported algorithms def supports_asymmetric_auth? capability?('client-confidential-asymmetric') && auth_method_supported?('private_key_jwt') && asymmetric_signing_algorithms_supported.any? end # Returns the asymmetric signing algorithms supported by both client and server. # If the server doesn't advertise algorithms, assumes it supports the required ones (RS384, ES384). # @return [Array<String>] list of supported algorithms def asymmetric_signing_algorithms_supported server_algs = token_endpoint_auth_signing_alg_values_supported return SUPPORTED_ASYMMETRIC_ALGORITHMS.dup if server_algs.nil? || server_algs == [] return [] unless array_of?(server_algs, String) server_algs & SUPPORTED_ASYMMETRIC_ALGORITHMS end # Feature support checks # Checks whether the server supports Dynamic Client Registration (RFC 7591). # @return [Boolean] true if the server advertises a +registration_endpoint+ def supports_dynamic_registration? registration_endpoint.present? end def capability?('authorize-post') end def supports_openid_connect? openid_connect_capability? && issuer.present? && jwks_uri.present? end # Capability-only checks (does not verify required fields are present) def ehr_launch_capability? capability?('launch-ehr') end def standalone_launch_capability? capability?('launch-standalone') end def openid_connect_capability? capability?('sso-openid-connect') end private def capability?(name) array_includes?(:capabilities, name) end def array_includes?(attribute, value) values = public_send(attribute) array_of?(values, String) && values.include?(value) end def auth_method_supported?(method) methods = token_endpoint_auth_methods_supported methods.nil? || methods == [] || array_includes?(:token_endpoint_auth_methods_supported, method) end def validate_array_fields! valid = true ARRAY_ATTRIBUTE_ELEMENT_TYPES.each do |attribute, element_type| value = public_send(attribute) next if value.nil? next if array_of?(value, element_type) Safire.logger.warn( "SMART metadata non-compliance: field '#{attribute}' must be an array containing only " \ "#{element_type == Hash ? 'objects' : 'strings'}" ) valid = false end valid end def array_of?(value, element_type) value.is_a?(Array) && value.all?(element_type) end def issuer_and_jwks_uri_required? openid_connect_capability? end def ehr_launch_capability? || standalone_launch_capability? end # Validates PKCE code challenge methods per SMART App Launch 2.2.0: # - 'S256' SHALL be included # - 'plain' SHALL NOT be included # # @return [Boolean] true if both conditions are satisfied def validate_pkce_methods! valid = true unless array_includes?(:code_challenge_methods_supported, 'S256') Safire.logger.warn( "SMART metadata non-compliance: 'S256' is missing from code_challenge_methods_supported " \ '(SMART App Launch 2.2.0 requires S256)' ) valid = false end if array_includes?(:code_challenge_methods_supported, 'plain') Safire.logger.warn( "SMART metadata non-compliance: 'plain' is present in code_challenge_methods_supported " \ '(SMART App Launch 2.2.0 prohibits plain)' ) valid = false end valid end end |
#jwks_uri ⇒ String (readonly)
Returns URL of the server’s JSON Web Key Set endpoint. Required if the server’s capabilities include sso-openid-connect.
52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 |
# File 'lib/safire/protocols/smart_metadata.rb', line 52 class SmartMetadata < Safire::Entity REQUIRED_ATTRIBUTES = %i[ grant_types_supported token_endpoint capabilities code_challenge_methods_supported ].freeze OPTIONAL_ATTRIBUTES = %i[ issuer jwks_uri authorization_endpoint token_endpoint_auth_methods_supported token_endpoint_auth_signing_alg_values_supported registration_endpoint associated_endpoints user_access_brand_bundle user_access_brand_identifier scopes_supported response_types_supported management_endpoint introspection_endpoint revocation_endpoint ].freeze ATTRIBUTES = (REQUIRED_ATTRIBUTES | OPTIONAL_ATTRIBUTES).freeze ARRAY_ATTRIBUTE_ELEMENT_TYPES = { grant_types_supported: String, token_endpoint_auth_methods_supported: String, token_endpoint_auth_signing_alg_values_supported: String, associated_endpoints: Hash, scopes_supported: String, response_types_supported: String, capabilities: String, code_challenge_methods_supported: String }.freeze private_constant :ARRAY_ATTRIBUTE_ELEMENT_TYPES # Supported asymmetric signing algorithms (required by SMART spec) SUPPORTED_ASYMMETRIC_ALGORITHMS = %w[RS384 ES384].freeze attr_reader(*ATTRIBUTES) def initialize() super(, ATTRIBUTES) end # Runs Safire's selected SMART App Launch 2.2.0 structural diagnostics. # # This is a user-callable helper. Safire performs discovery without automatically # asserting server compliance — it is the caller's responsibility to invoke this # method when they wish to inspect these conformance signals. This helper is # not a complete server certification suite. # # Checks performed: # - All required fields are present # (token_endpoint, grant_types_supported, capabilities, code_challenge_methods_supported) # - Conditional fields present when their capability is advertised # (issuer + jwks_uri for sso-openid-connect; authorization_endpoint for launch types) # - Array-valued fields are arrays containing values of the documented type # - `code_challenge_methods_supported` includes 'S256' # (SMART App Launch 2.2.0, §Conformance — SHALL be included) # - `code_challenge_methods_supported` does NOT include 'plain' # (SMART App Launch 2.2.0, §Conformance — SHALL NOT be included) # # A warning is logged for each SMART 2.2.0 violation detected. # # @return [Boolean] true if all checks pass, false if any violation is found def valid? required_attrs = [*REQUIRED_ATTRIBUTES] required_attrs.push(:issuer, :jwks_uri) if issuer_and_jwks_uri_required? required_attrs.push(:authorization_endpoint) if missing_attrs = required_attrs.reject { |attr| public_send(attr) } missing_attrs.each do |attr| Safire.logger.warn("SMART metadata non-compliance: required field '#{attr}' is missing") end array_fields_valid = validate_array_fields! pkce_valid = validate_pkce_methods! missing_attrs.empty? && array_fields_valid && pkce_valid end # Launch type support checks - requires both capability and authorization_endpoint def supports_ehr_launch? ehr_launch_capability? && .present? end def supports_standalone_launch? standalone_launch_capability? && .present? end # Authentication type support checks # Checks if the server supports public client authentication. # @return [Boolean] true if server has client-public capability def supports_public_auth? capability?('client-public') end # Checks if the server supports confidential symmetric authentication. # @return [Boolean] true if server has capability and auth methods not advertised or includes client_secret_basic def supports_symmetric_auth? capability?('client-confidential-symmetric') && auth_method_supported?('client_secret_basic') end # Checks if the server supports the SMART Backend Services workflow. # @return [Boolean] true if the server advertises the client_credentials grant type # and supports private_key_jwt authentication (via {#supports_asymmetric_auth?}) def supports_backend_services? array_includes?(:grant_types_supported, 'client_credentials') && supports_asymmetric_auth? end # Checks if the server supports confidential asymmetric authentication. # @return [Boolean] true if server has capability, auth methods not advertised or includes private_key_jwt, # and has supported algorithms def supports_asymmetric_auth? capability?('client-confidential-asymmetric') && auth_method_supported?('private_key_jwt') && asymmetric_signing_algorithms_supported.any? end # Returns the asymmetric signing algorithms supported by both client and server. # If the server doesn't advertise algorithms, assumes it supports the required ones (RS384, ES384). # @return [Array<String>] list of supported algorithms def asymmetric_signing_algorithms_supported server_algs = token_endpoint_auth_signing_alg_values_supported return SUPPORTED_ASYMMETRIC_ALGORITHMS.dup if server_algs.nil? || server_algs == [] return [] unless array_of?(server_algs, String) server_algs & SUPPORTED_ASYMMETRIC_ALGORITHMS end # Feature support checks # Checks whether the server supports Dynamic Client Registration (RFC 7591). # @return [Boolean] true if the server advertises a +registration_endpoint+ def supports_dynamic_registration? registration_endpoint.present? end def capability?('authorize-post') end def supports_openid_connect? openid_connect_capability? && issuer.present? && jwks_uri.present? end # Capability-only checks (does not verify required fields are present) def ehr_launch_capability? capability?('launch-ehr') end def standalone_launch_capability? capability?('launch-standalone') end def openid_connect_capability? capability?('sso-openid-connect') end private def capability?(name) array_includes?(:capabilities, name) end def array_includes?(attribute, value) values = public_send(attribute) array_of?(values, String) && values.include?(value) end def auth_method_supported?(method) methods = token_endpoint_auth_methods_supported methods.nil? || methods == [] || array_includes?(:token_endpoint_auth_methods_supported, method) end def validate_array_fields! valid = true ARRAY_ATTRIBUTE_ELEMENT_TYPES.each do |attribute, element_type| value = public_send(attribute) next if value.nil? next if array_of?(value, element_type) Safire.logger.warn( "SMART metadata non-compliance: field '#{attribute}' must be an array containing only " \ "#{element_type == Hash ? 'objects' : 'strings'}" ) valid = false end valid end def array_of?(value, element_type) value.is_a?(Array) && value.all?(element_type) end def issuer_and_jwks_uri_required? openid_connect_capability? end def ehr_launch_capability? || standalone_launch_capability? end # Validates PKCE code challenge methods per SMART App Launch 2.2.0: # - 'S256' SHALL be included # - 'plain' SHALL NOT be included # # @return [Boolean] true if both conditions are satisfied def validate_pkce_methods! valid = true unless array_includes?(:code_challenge_methods_supported, 'S256') Safire.logger.warn( "SMART metadata non-compliance: 'S256' is missing from code_challenge_methods_supported " \ '(SMART App Launch 2.2.0 requires S256)' ) valid = false end if array_includes?(:code_challenge_methods_supported, 'plain') Safire.logger.warn( "SMART metadata non-compliance: 'plain' is present in code_challenge_methods_supported " \ '(SMART App Launch 2.2.0 prohibits plain)' ) valid = false end valid end end |
#management_endpoint ⇒ String (readonly)
Returns URL where an end-user can view which applications currently have access to data and can make adjustments to these access rights. Optionally provided.
52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 |
# File 'lib/safire/protocols/smart_metadata.rb', line 52 class SmartMetadata < Safire::Entity REQUIRED_ATTRIBUTES = %i[ grant_types_supported token_endpoint capabilities code_challenge_methods_supported ].freeze OPTIONAL_ATTRIBUTES = %i[ issuer jwks_uri authorization_endpoint token_endpoint_auth_methods_supported token_endpoint_auth_signing_alg_values_supported registration_endpoint associated_endpoints user_access_brand_bundle user_access_brand_identifier scopes_supported response_types_supported management_endpoint introspection_endpoint revocation_endpoint ].freeze ATTRIBUTES = (REQUIRED_ATTRIBUTES | OPTIONAL_ATTRIBUTES).freeze ARRAY_ATTRIBUTE_ELEMENT_TYPES = { grant_types_supported: String, token_endpoint_auth_methods_supported: String, token_endpoint_auth_signing_alg_values_supported: String, associated_endpoints: Hash, scopes_supported: String, response_types_supported: String, capabilities: String, code_challenge_methods_supported: String }.freeze private_constant :ARRAY_ATTRIBUTE_ELEMENT_TYPES # Supported asymmetric signing algorithms (required by SMART spec) SUPPORTED_ASYMMETRIC_ALGORITHMS = %w[RS384 ES384].freeze attr_reader(*ATTRIBUTES) def initialize() super(, ATTRIBUTES) end # Runs Safire's selected SMART App Launch 2.2.0 structural diagnostics. # # This is a user-callable helper. Safire performs discovery without automatically # asserting server compliance — it is the caller's responsibility to invoke this # method when they wish to inspect these conformance signals. This helper is # not a complete server certification suite. # # Checks performed: # - All required fields are present # (token_endpoint, grant_types_supported, capabilities, code_challenge_methods_supported) # - Conditional fields present when their capability is advertised # (issuer + jwks_uri for sso-openid-connect; authorization_endpoint for launch types) # - Array-valued fields are arrays containing values of the documented type # - `code_challenge_methods_supported` includes 'S256' # (SMART App Launch 2.2.0, §Conformance — SHALL be included) # - `code_challenge_methods_supported` does NOT include 'plain' # (SMART App Launch 2.2.0, §Conformance — SHALL NOT be included) # # A warning is logged for each SMART 2.2.0 violation detected. # # @return [Boolean] true if all checks pass, false if any violation is found def valid? required_attrs = [*REQUIRED_ATTRIBUTES] required_attrs.push(:issuer, :jwks_uri) if issuer_and_jwks_uri_required? required_attrs.push(:authorization_endpoint) if missing_attrs = required_attrs.reject { |attr| public_send(attr) } missing_attrs.each do |attr| Safire.logger.warn("SMART metadata non-compliance: required field '#{attr}' is missing") end array_fields_valid = validate_array_fields! pkce_valid = validate_pkce_methods! missing_attrs.empty? && array_fields_valid && pkce_valid end # Launch type support checks - requires both capability and authorization_endpoint def supports_ehr_launch? ehr_launch_capability? && .present? end def supports_standalone_launch? standalone_launch_capability? && .present? end # Authentication type support checks # Checks if the server supports public client authentication. # @return [Boolean] true if server has client-public capability def supports_public_auth? capability?('client-public') end # Checks if the server supports confidential symmetric authentication. # @return [Boolean] true if server has capability and auth methods not advertised or includes client_secret_basic def supports_symmetric_auth? capability?('client-confidential-symmetric') && auth_method_supported?('client_secret_basic') end # Checks if the server supports the SMART Backend Services workflow. # @return [Boolean] true if the server advertises the client_credentials grant type # and supports private_key_jwt authentication (via {#supports_asymmetric_auth?}) def supports_backend_services? array_includes?(:grant_types_supported, 'client_credentials') && supports_asymmetric_auth? end # Checks if the server supports confidential asymmetric authentication. # @return [Boolean] true if server has capability, auth methods not advertised or includes private_key_jwt, # and has supported algorithms def supports_asymmetric_auth? capability?('client-confidential-asymmetric') && auth_method_supported?('private_key_jwt') && asymmetric_signing_algorithms_supported.any? end # Returns the asymmetric signing algorithms supported by both client and server. # If the server doesn't advertise algorithms, assumes it supports the required ones (RS384, ES384). # @return [Array<String>] list of supported algorithms def asymmetric_signing_algorithms_supported server_algs = token_endpoint_auth_signing_alg_values_supported return SUPPORTED_ASYMMETRIC_ALGORITHMS.dup if server_algs.nil? || server_algs == [] return [] unless array_of?(server_algs, String) server_algs & SUPPORTED_ASYMMETRIC_ALGORITHMS end # Feature support checks # Checks whether the server supports Dynamic Client Registration (RFC 7591). # @return [Boolean] true if the server advertises a +registration_endpoint+ def supports_dynamic_registration? registration_endpoint.present? end def capability?('authorize-post') end def supports_openid_connect? openid_connect_capability? && issuer.present? && jwks_uri.present? end # Capability-only checks (does not verify required fields are present) def ehr_launch_capability? capability?('launch-ehr') end def standalone_launch_capability? capability?('launch-standalone') end def openid_connect_capability? capability?('sso-openid-connect') end private def capability?(name) array_includes?(:capabilities, name) end def array_includes?(attribute, value) values = public_send(attribute) array_of?(values, String) && values.include?(value) end def auth_method_supported?(method) methods = token_endpoint_auth_methods_supported methods.nil? || methods == [] || array_includes?(:token_endpoint_auth_methods_supported, method) end def validate_array_fields! valid = true ARRAY_ATTRIBUTE_ELEMENT_TYPES.each do |attribute, element_type| value = public_send(attribute) next if value.nil? next if array_of?(value, element_type) Safire.logger.warn( "SMART metadata non-compliance: field '#{attribute}' must be an array containing only " \ "#{element_type == Hash ? 'objects' : 'strings'}" ) valid = false end valid end def array_of?(value, element_type) value.is_a?(Array) && value.all?(element_type) end def issuer_and_jwks_uri_required? openid_connect_capability? end def ehr_launch_capability? || standalone_launch_capability? end # Validates PKCE code challenge methods per SMART App Launch 2.2.0: # - 'S256' SHALL be included # - 'plain' SHALL NOT be included # # @return [Boolean] true if both conditions are satisfied def validate_pkce_methods! valid = true unless array_includes?(:code_challenge_methods_supported, 'S256') Safire.logger.warn( "SMART metadata non-compliance: 'S256' is missing from code_challenge_methods_supported " \ '(SMART App Launch 2.2.0 requires S256)' ) valid = false end if array_includes?(:code_challenge_methods_supported, 'plain') Safire.logger.warn( "SMART metadata non-compliance: 'plain' is present in code_challenge_methods_supported " \ '(SMART App Launch 2.2.0 prohibits plain)' ) valid = false end valid end end |
#registration_endpoint ⇒ String (readonly)
Returns URL of the server’s OAuth2 Dynamic Client Registration Endpoint. Optionally provided.
52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 |
# File 'lib/safire/protocols/smart_metadata.rb', line 52 class SmartMetadata < Safire::Entity REQUIRED_ATTRIBUTES = %i[ grant_types_supported token_endpoint capabilities code_challenge_methods_supported ].freeze OPTIONAL_ATTRIBUTES = %i[ issuer jwks_uri authorization_endpoint token_endpoint_auth_methods_supported token_endpoint_auth_signing_alg_values_supported registration_endpoint associated_endpoints user_access_brand_bundle user_access_brand_identifier scopes_supported response_types_supported management_endpoint introspection_endpoint revocation_endpoint ].freeze ATTRIBUTES = (REQUIRED_ATTRIBUTES | OPTIONAL_ATTRIBUTES).freeze ARRAY_ATTRIBUTE_ELEMENT_TYPES = { grant_types_supported: String, token_endpoint_auth_methods_supported: String, token_endpoint_auth_signing_alg_values_supported: String, associated_endpoints: Hash, scopes_supported: String, response_types_supported: String, capabilities: String, code_challenge_methods_supported: String }.freeze private_constant :ARRAY_ATTRIBUTE_ELEMENT_TYPES # Supported asymmetric signing algorithms (required by SMART spec) SUPPORTED_ASYMMETRIC_ALGORITHMS = %w[RS384 ES384].freeze attr_reader(*ATTRIBUTES) def initialize() super(, ATTRIBUTES) end # Runs Safire's selected SMART App Launch 2.2.0 structural diagnostics. # # This is a user-callable helper. Safire performs discovery without automatically # asserting server compliance — it is the caller's responsibility to invoke this # method when they wish to inspect these conformance signals. This helper is # not a complete server certification suite. # # Checks performed: # - All required fields are present # (token_endpoint, grant_types_supported, capabilities, code_challenge_methods_supported) # - Conditional fields present when their capability is advertised # (issuer + jwks_uri for sso-openid-connect; authorization_endpoint for launch types) # - Array-valued fields are arrays containing values of the documented type # - `code_challenge_methods_supported` includes 'S256' # (SMART App Launch 2.2.0, §Conformance — SHALL be included) # - `code_challenge_methods_supported` does NOT include 'plain' # (SMART App Launch 2.2.0, §Conformance — SHALL NOT be included) # # A warning is logged for each SMART 2.2.0 violation detected. # # @return [Boolean] true if all checks pass, false if any violation is found def valid? required_attrs = [*REQUIRED_ATTRIBUTES] required_attrs.push(:issuer, :jwks_uri) if issuer_and_jwks_uri_required? required_attrs.push(:authorization_endpoint) if missing_attrs = required_attrs.reject { |attr| public_send(attr) } missing_attrs.each do |attr| Safire.logger.warn("SMART metadata non-compliance: required field '#{attr}' is missing") end array_fields_valid = validate_array_fields! pkce_valid = validate_pkce_methods! missing_attrs.empty? && array_fields_valid && pkce_valid end # Launch type support checks - requires both capability and authorization_endpoint def supports_ehr_launch? ehr_launch_capability? && .present? end def supports_standalone_launch? standalone_launch_capability? && .present? end # Authentication type support checks # Checks if the server supports public client authentication. # @return [Boolean] true if server has client-public capability def supports_public_auth? capability?('client-public') end # Checks if the server supports confidential symmetric authentication. # @return [Boolean] true if server has capability and auth methods not advertised or includes client_secret_basic def supports_symmetric_auth? capability?('client-confidential-symmetric') && auth_method_supported?('client_secret_basic') end # Checks if the server supports the SMART Backend Services workflow. # @return [Boolean] true if the server advertises the client_credentials grant type # and supports private_key_jwt authentication (via {#supports_asymmetric_auth?}) def supports_backend_services? array_includes?(:grant_types_supported, 'client_credentials') && supports_asymmetric_auth? end # Checks if the server supports confidential asymmetric authentication. # @return [Boolean] true if server has capability, auth methods not advertised or includes private_key_jwt, # and has supported algorithms def supports_asymmetric_auth? capability?('client-confidential-asymmetric') && auth_method_supported?('private_key_jwt') && asymmetric_signing_algorithms_supported.any? end # Returns the asymmetric signing algorithms supported by both client and server. # If the server doesn't advertise algorithms, assumes it supports the required ones (RS384, ES384). # @return [Array<String>] list of supported algorithms def asymmetric_signing_algorithms_supported server_algs = token_endpoint_auth_signing_alg_values_supported return SUPPORTED_ASYMMETRIC_ALGORITHMS.dup if server_algs.nil? || server_algs == [] return [] unless array_of?(server_algs, String) server_algs & SUPPORTED_ASYMMETRIC_ALGORITHMS end # Feature support checks # Checks whether the server supports Dynamic Client Registration (RFC 7591). # @return [Boolean] true if the server advertises a +registration_endpoint+ def supports_dynamic_registration? registration_endpoint.present? end def capability?('authorize-post') end def supports_openid_connect? openid_connect_capability? && issuer.present? && jwks_uri.present? end # Capability-only checks (does not verify required fields are present) def ehr_launch_capability? capability?('launch-ehr') end def standalone_launch_capability? capability?('launch-standalone') end def openid_connect_capability? capability?('sso-openid-connect') end private def capability?(name) array_includes?(:capabilities, name) end def array_includes?(attribute, value) values = public_send(attribute) array_of?(values, String) && values.include?(value) end def auth_method_supported?(method) methods = token_endpoint_auth_methods_supported methods.nil? || methods == [] || array_includes?(:token_endpoint_auth_methods_supported, method) end def validate_array_fields! valid = true ARRAY_ATTRIBUTE_ELEMENT_TYPES.each do |attribute, element_type| value = public_send(attribute) next if value.nil? next if array_of?(value, element_type) Safire.logger.warn( "SMART metadata non-compliance: field '#{attribute}' must be an array containing only " \ "#{element_type == Hash ? 'objects' : 'strings'}" ) valid = false end valid end def array_of?(value, element_type) value.is_a?(Array) && value.all?(element_type) end def issuer_and_jwks_uri_required? openid_connect_capability? end def ehr_launch_capability? || standalone_launch_capability? end # Validates PKCE code challenge methods per SMART App Launch 2.2.0: # - 'S256' SHALL be included # - 'plain' SHALL NOT be included # # @return [Boolean] true if both conditions are satisfied def validate_pkce_methods! valid = true unless array_includes?(:code_challenge_methods_supported, 'S256') Safire.logger.warn( "SMART metadata non-compliance: 'S256' is missing from code_challenge_methods_supported " \ '(SMART App Launch 2.2.0 requires S256)' ) valid = false end if array_includes?(:code_challenge_methods_supported, 'plain') Safire.logger.warn( "SMART metadata non-compliance: 'plain' is present in code_challenge_methods_supported " \ '(SMART App Launch 2.2.0 prohibits plain)' ) valid = false end valid end end |
#response_types_supported ⇒ Array<String> (readonly)
Returns list of OAuth2 response types supported. Optionally provided.
52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 |
# File 'lib/safire/protocols/smart_metadata.rb', line 52 class SmartMetadata < Safire::Entity REQUIRED_ATTRIBUTES = %i[ grant_types_supported token_endpoint capabilities code_challenge_methods_supported ].freeze OPTIONAL_ATTRIBUTES = %i[ issuer jwks_uri authorization_endpoint token_endpoint_auth_methods_supported token_endpoint_auth_signing_alg_values_supported registration_endpoint associated_endpoints user_access_brand_bundle user_access_brand_identifier scopes_supported response_types_supported management_endpoint introspection_endpoint revocation_endpoint ].freeze ATTRIBUTES = (REQUIRED_ATTRIBUTES | OPTIONAL_ATTRIBUTES).freeze ARRAY_ATTRIBUTE_ELEMENT_TYPES = { grant_types_supported: String, token_endpoint_auth_methods_supported: String, token_endpoint_auth_signing_alg_values_supported: String, associated_endpoints: Hash, scopes_supported: String, response_types_supported: String, capabilities: String, code_challenge_methods_supported: String }.freeze private_constant :ARRAY_ATTRIBUTE_ELEMENT_TYPES # Supported asymmetric signing algorithms (required by SMART spec) SUPPORTED_ASYMMETRIC_ALGORITHMS = %w[RS384 ES384].freeze attr_reader(*ATTRIBUTES) def initialize() super(, ATTRIBUTES) end # Runs Safire's selected SMART App Launch 2.2.0 structural diagnostics. # # This is a user-callable helper. Safire performs discovery without automatically # asserting server compliance — it is the caller's responsibility to invoke this # method when they wish to inspect these conformance signals. This helper is # not a complete server certification suite. # # Checks performed: # - All required fields are present # (token_endpoint, grant_types_supported, capabilities, code_challenge_methods_supported) # - Conditional fields present when their capability is advertised # (issuer + jwks_uri for sso-openid-connect; authorization_endpoint for launch types) # - Array-valued fields are arrays containing values of the documented type # - `code_challenge_methods_supported` includes 'S256' # (SMART App Launch 2.2.0, §Conformance — SHALL be included) # - `code_challenge_methods_supported` does NOT include 'plain' # (SMART App Launch 2.2.0, §Conformance — SHALL NOT be included) # # A warning is logged for each SMART 2.2.0 violation detected. # # @return [Boolean] true if all checks pass, false if any violation is found def valid? required_attrs = [*REQUIRED_ATTRIBUTES] required_attrs.push(:issuer, :jwks_uri) if issuer_and_jwks_uri_required? required_attrs.push(:authorization_endpoint) if missing_attrs = required_attrs.reject { |attr| public_send(attr) } missing_attrs.each do |attr| Safire.logger.warn("SMART metadata non-compliance: required field '#{attr}' is missing") end array_fields_valid = validate_array_fields! pkce_valid = validate_pkce_methods! missing_attrs.empty? && array_fields_valid && pkce_valid end # Launch type support checks - requires both capability and authorization_endpoint def supports_ehr_launch? ehr_launch_capability? && .present? end def supports_standalone_launch? standalone_launch_capability? && .present? end # Authentication type support checks # Checks if the server supports public client authentication. # @return [Boolean] true if server has client-public capability def supports_public_auth? capability?('client-public') end # Checks if the server supports confidential symmetric authentication. # @return [Boolean] true if server has capability and auth methods not advertised or includes client_secret_basic def supports_symmetric_auth? capability?('client-confidential-symmetric') && auth_method_supported?('client_secret_basic') end # Checks if the server supports the SMART Backend Services workflow. # @return [Boolean] true if the server advertises the client_credentials grant type # and supports private_key_jwt authentication (via {#supports_asymmetric_auth?}) def supports_backend_services? array_includes?(:grant_types_supported, 'client_credentials') && supports_asymmetric_auth? end # Checks if the server supports confidential asymmetric authentication. # @return [Boolean] true if server has capability, auth methods not advertised or includes private_key_jwt, # and has supported algorithms def supports_asymmetric_auth? capability?('client-confidential-asymmetric') && auth_method_supported?('private_key_jwt') && asymmetric_signing_algorithms_supported.any? end # Returns the asymmetric signing algorithms supported by both client and server. # If the server doesn't advertise algorithms, assumes it supports the required ones (RS384, ES384). # @return [Array<String>] list of supported algorithms def asymmetric_signing_algorithms_supported server_algs = token_endpoint_auth_signing_alg_values_supported return SUPPORTED_ASYMMETRIC_ALGORITHMS.dup if server_algs.nil? || server_algs == [] return [] unless array_of?(server_algs, String) server_algs & SUPPORTED_ASYMMETRIC_ALGORITHMS end # Feature support checks # Checks whether the server supports Dynamic Client Registration (RFC 7591). # @return [Boolean] true if the server advertises a +registration_endpoint+ def supports_dynamic_registration? registration_endpoint.present? end def capability?('authorize-post') end def supports_openid_connect? openid_connect_capability? && issuer.present? && jwks_uri.present? end # Capability-only checks (does not verify required fields are present) def ehr_launch_capability? capability?('launch-ehr') end def standalone_launch_capability? capability?('launch-standalone') end def openid_connect_capability? capability?('sso-openid-connect') end private def capability?(name) array_includes?(:capabilities, name) end def array_includes?(attribute, value) values = public_send(attribute) array_of?(values, String) && values.include?(value) end def auth_method_supported?(method) methods = token_endpoint_auth_methods_supported methods.nil? || methods == [] || array_includes?(:token_endpoint_auth_methods_supported, method) end def validate_array_fields! valid = true ARRAY_ATTRIBUTE_ELEMENT_TYPES.each do |attribute, element_type| value = public_send(attribute) next if value.nil? next if array_of?(value, element_type) Safire.logger.warn( "SMART metadata non-compliance: field '#{attribute}' must be an array containing only " \ "#{element_type == Hash ? 'objects' : 'strings'}" ) valid = false end valid end def array_of?(value, element_type) value.is_a?(Array) && value.all?(element_type) end def issuer_and_jwks_uri_required? openid_connect_capability? end def ehr_launch_capability? || standalone_launch_capability? end # Validates PKCE code challenge methods per SMART App Launch 2.2.0: # - 'S256' SHALL be included # - 'plain' SHALL NOT be included # # @return [Boolean] true if both conditions are satisfied def validate_pkce_methods! valid = true unless array_includes?(:code_challenge_methods_supported, 'S256') Safire.logger.warn( "SMART metadata non-compliance: 'S256' is missing from code_challenge_methods_supported " \ '(SMART App Launch 2.2.0 requires S256)' ) valid = false end if array_includes?(:code_challenge_methods_supported, 'plain') Safire.logger.warn( "SMART metadata non-compliance: 'plain' is present in code_challenge_methods_supported " \ '(SMART App Launch 2.2.0 prohibits plain)' ) valid = false end valid end end |
#revocation_endpoint ⇒ String (readonly)
Returns URL to a server’s revocation endpoint that can be used to revoke a token. Optionally provided.
52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 |
# File 'lib/safire/protocols/smart_metadata.rb', line 52 class SmartMetadata < Safire::Entity REQUIRED_ATTRIBUTES = %i[ grant_types_supported token_endpoint capabilities code_challenge_methods_supported ].freeze OPTIONAL_ATTRIBUTES = %i[ issuer jwks_uri authorization_endpoint token_endpoint_auth_methods_supported token_endpoint_auth_signing_alg_values_supported registration_endpoint associated_endpoints user_access_brand_bundle user_access_brand_identifier scopes_supported response_types_supported management_endpoint introspection_endpoint revocation_endpoint ].freeze ATTRIBUTES = (REQUIRED_ATTRIBUTES | OPTIONAL_ATTRIBUTES).freeze ARRAY_ATTRIBUTE_ELEMENT_TYPES = { grant_types_supported: String, token_endpoint_auth_methods_supported: String, token_endpoint_auth_signing_alg_values_supported: String, associated_endpoints: Hash, scopes_supported: String, response_types_supported: String, capabilities: String, code_challenge_methods_supported: String }.freeze private_constant :ARRAY_ATTRIBUTE_ELEMENT_TYPES # Supported asymmetric signing algorithms (required by SMART spec) SUPPORTED_ASYMMETRIC_ALGORITHMS = %w[RS384 ES384].freeze attr_reader(*ATTRIBUTES) def initialize() super(, ATTRIBUTES) end # Runs Safire's selected SMART App Launch 2.2.0 structural diagnostics. # # This is a user-callable helper. Safire performs discovery without automatically # asserting server compliance — it is the caller's responsibility to invoke this # method when they wish to inspect these conformance signals. This helper is # not a complete server certification suite. # # Checks performed: # - All required fields are present # (token_endpoint, grant_types_supported, capabilities, code_challenge_methods_supported) # - Conditional fields present when their capability is advertised # (issuer + jwks_uri for sso-openid-connect; authorization_endpoint for launch types) # - Array-valued fields are arrays containing values of the documented type # - `code_challenge_methods_supported` includes 'S256' # (SMART App Launch 2.2.0, §Conformance — SHALL be included) # - `code_challenge_methods_supported` does NOT include 'plain' # (SMART App Launch 2.2.0, §Conformance — SHALL NOT be included) # # A warning is logged for each SMART 2.2.0 violation detected. # # @return [Boolean] true if all checks pass, false if any violation is found def valid? required_attrs = [*REQUIRED_ATTRIBUTES] required_attrs.push(:issuer, :jwks_uri) if issuer_and_jwks_uri_required? required_attrs.push(:authorization_endpoint) if missing_attrs = required_attrs.reject { |attr| public_send(attr) } missing_attrs.each do |attr| Safire.logger.warn("SMART metadata non-compliance: required field '#{attr}' is missing") end array_fields_valid = validate_array_fields! pkce_valid = validate_pkce_methods! missing_attrs.empty? && array_fields_valid && pkce_valid end # Launch type support checks - requires both capability and authorization_endpoint def supports_ehr_launch? ehr_launch_capability? && .present? end def supports_standalone_launch? standalone_launch_capability? && .present? end # Authentication type support checks # Checks if the server supports public client authentication. # @return [Boolean] true if server has client-public capability def supports_public_auth? capability?('client-public') end # Checks if the server supports confidential symmetric authentication. # @return [Boolean] true if server has capability and auth methods not advertised or includes client_secret_basic def supports_symmetric_auth? capability?('client-confidential-symmetric') && auth_method_supported?('client_secret_basic') end # Checks if the server supports the SMART Backend Services workflow. # @return [Boolean] true if the server advertises the client_credentials grant type # and supports private_key_jwt authentication (via {#supports_asymmetric_auth?}) def supports_backend_services? array_includes?(:grant_types_supported, 'client_credentials') && supports_asymmetric_auth? end # Checks if the server supports confidential asymmetric authentication. # @return [Boolean] true if server has capability, auth methods not advertised or includes private_key_jwt, # and has supported algorithms def supports_asymmetric_auth? capability?('client-confidential-asymmetric') && auth_method_supported?('private_key_jwt') && asymmetric_signing_algorithms_supported.any? end # Returns the asymmetric signing algorithms supported by both client and server. # If the server doesn't advertise algorithms, assumes it supports the required ones (RS384, ES384). # @return [Array<String>] list of supported algorithms def asymmetric_signing_algorithms_supported server_algs = token_endpoint_auth_signing_alg_values_supported return SUPPORTED_ASYMMETRIC_ALGORITHMS.dup if server_algs.nil? || server_algs == [] return [] unless array_of?(server_algs, String) server_algs & SUPPORTED_ASYMMETRIC_ALGORITHMS end # Feature support checks # Checks whether the server supports Dynamic Client Registration (RFC 7591). # @return [Boolean] true if the server advertises a +registration_endpoint+ def supports_dynamic_registration? registration_endpoint.present? end def capability?('authorize-post') end def supports_openid_connect? openid_connect_capability? && issuer.present? && jwks_uri.present? end # Capability-only checks (does not verify required fields are present) def ehr_launch_capability? capability?('launch-ehr') end def standalone_launch_capability? capability?('launch-standalone') end def openid_connect_capability? capability?('sso-openid-connect') end private def capability?(name) array_includes?(:capabilities, name) end def array_includes?(attribute, value) values = public_send(attribute) array_of?(values, String) && values.include?(value) end def auth_method_supported?(method) methods = token_endpoint_auth_methods_supported methods.nil? || methods == [] || array_includes?(:token_endpoint_auth_methods_supported, method) end def validate_array_fields! valid = true ARRAY_ATTRIBUTE_ELEMENT_TYPES.each do |attribute, element_type| value = public_send(attribute) next if value.nil? next if array_of?(value, element_type) Safire.logger.warn( "SMART metadata non-compliance: field '#{attribute}' must be an array containing only " \ "#{element_type == Hash ? 'objects' : 'strings'}" ) valid = false end valid end def array_of?(value, element_type) value.is_a?(Array) && value.all?(element_type) end def issuer_and_jwks_uri_required? openid_connect_capability? end def ehr_launch_capability? || standalone_launch_capability? end # Validates PKCE code challenge methods per SMART App Launch 2.2.0: # - 'S256' SHALL be included # - 'plain' SHALL NOT be included # # @return [Boolean] true if both conditions are satisfied def validate_pkce_methods! valid = true unless array_includes?(:code_challenge_methods_supported, 'S256') Safire.logger.warn( "SMART metadata non-compliance: 'S256' is missing from code_challenge_methods_supported " \ '(SMART App Launch 2.2.0 requires S256)' ) valid = false end if array_includes?(:code_challenge_methods_supported, 'plain') Safire.logger.warn( "SMART metadata non-compliance: 'plain' is present in code_challenge_methods_supported " \ '(SMART App Launch 2.2.0 prohibits plain)' ) valid = false end valid end end |
#scopes_supported ⇒ Array<String> (readonly)
Returns list of scopes a client may request. Optionally provided.
52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 |
# File 'lib/safire/protocols/smart_metadata.rb', line 52 class SmartMetadata < Safire::Entity REQUIRED_ATTRIBUTES = %i[ grant_types_supported token_endpoint capabilities code_challenge_methods_supported ].freeze OPTIONAL_ATTRIBUTES = %i[ issuer jwks_uri authorization_endpoint token_endpoint_auth_methods_supported token_endpoint_auth_signing_alg_values_supported registration_endpoint associated_endpoints user_access_brand_bundle user_access_brand_identifier scopes_supported response_types_supported management_endpoint introspection_endpoint revocation_endpoint ].freeze ATTRIBUTES = (REQUIRED_ATTRIBUTES | OPTIONAL_ATTRIBUTES).freeze ARRAY_ATTRIBUTE_ELEMENT_TYPES = { grant_types_supported: String, token_endpoint_auth_methods_supported: String, token_endpoint_auth_signing_alg_values_supported: String, associated_endpoints: Hash, scopes_supported: String, response_types_supported: String, capabilities: String, code_challenge_methods_supported: String }.freeze private_constant :ARRAY_ATTRIBUTE_ELEMENT_TYPES # Supported asymmetric signing algorithms (required by SMART spec) SUPPORTED_ASYMMETRIC_ALGORITHMS = %w[RS384 ES384].freeze attr_reader(*ATTRIBUTES) def initialize() super(, ATTRIBUTES) end # Runs Safire's selected SMART App Launch 2.2.0 structural diagnostics. # # This is a user-callable helper. Safire performs discovery without automatically # asserting server compliance — it is the caller's responsibility to invoke this # method when they wish to inspect these conformance signals. This helper is # not a complete server certification suite. # # Checks performed: # - All required fields are present # (token_endpoint, grant_types_supported, capabilities, code_challenge_methods_supported) # - Conditional fields present when their capability is advertised # (issuer + jwks_uri for sso-openid-connect; authorization_endpoint for launch types) # - Array-valued fields are arrays containing values of the documented type # - `code_challenge_methods_supported` includes 'S256' # (SMART App Launch 2.2.0, §Conformance — SHALL be included) # - `code_challenge_methods_supported` does NOT include 'plain' # (SMART App Launch 2.2.0, §Conformance — SHALL NOT be included) # # A warning is logged for each SMART 2.2.0 violation detected. # # @return [Boolean] true if all checks pass, false if any violation is found def valid? required_attrs = [*REQUIRED_ATTRIBUTES] required_attrs.push(:issuer, :jwks_uri) if issuer_and_jwks_uri_required? required_attrs.push(:authorization_endpoint) if missing_attrs = required_attrs.reject { |attr| public_send(attr) } missing_attrs.each do |attr| Safire.logger.warn("SMART metadata non-compliance: required field '#{attr}' is missing") end array_fields_valid = validate_array_fields! pkce_valid = validate_pkce_methods! missing_attrs.empty? && array_fields_valid && pkce_valid end # Launch type support checks - requires both capability and authorization_endpoint def supports_ehr_launch? ehr_launch_capability? && .present? end def supports_standalone_launch? standalone_launch_capability? && .present? end # Authentication type support checks # Checks if the server supports public client authentication. # @return [Boolean] true if server has client-public capability def supports_public_auth? capability?('client-public') end # Checks if the server supports confidential symmetric authentication. # @return [Boolean] true if server has capability and auth methods not advertised or includes client_secret_basic def supports_symmetric_auth? capability?('client-confidential-symmetric') && auth_method_supported?('client_secret_basic') end # Checks if the server supports the SMART Backend Services workflow. # @return [Boolean] true if the server advertises the client_credentials grant type # and supports private_key_jwt authentication (via {#supports_asymmetric_auth?}) def supports_backend_services? array_includes?(:grant_types_supported, 'client_credentials') && supports_asymmetric_auth? end # Checks if the server supports confidential asymmetric authentication. # @return [Boolean] true if server has capability, auth methods not advertised or includes private_key_jwt, # and has supported algorithms def supports_asymmetric_auth? capability?('client-confidential-asymmetric') && auth_method_supported?('private_key_jwt') && asymmetric_signing_algorithms_supported.any? end # Returns the asymmetric signing algorithms supported by both client and server. # If the server doesn't advertise algorithms, assumes it supports the required ones (RS384, ES384). # @return [Array<String>] list of supported algorithms def asymmetric_signing_algorithms_supported server_algs = token_endpoint_auth_signing_alg_values_supported return SUPPORTED_ASYMMETRIC_ALGORITHMS.dup if server_algs.nil? || server_algs == [] return [] unless array_of?(server_algs, String) server_algs & SUPPORTED_ASYMMETRIC_ALGORITHMS end # Feature support checks # Checks whether the server supports Dynamic Client Registration (RFC 7591). # @return [Boolean] true if the server advertises a +registration_endpoint+ def supports_dynamic_registration? registration_endpoint.present? end def capability?('authorize-post') end def supports_openid_connect? openid_connect_capability? && issuer.present? && jwks_uri.present? end # Capability-only checks (does not verify required fields are present) def ehr_launch_capability? capability?('launch-ehr') end def standalone_launch_capability? capability?('launch-standalone') end def openid_connect_capability? capability?('sso-openid-connect') end private def capability?(name) array_includes?(:capabilities, name) end def array_includes?(attribute, value) values = public_send(attribute) array_of?(values, String) && values.include?(value) end def auth_method_supported?(method) methods = token_endpoint_auth_methods_supported methods.nil? || methods == [] || array_includes?(:token_endpoint_auth_methods_supported, method) end def validate_array_fields! valid = true ARRAY_ATTRIBUTE_ELEMENT_TYPES.each do |attribute, element_type| value = public_send(attribute) next if value.nil? next if array_of?(value, element_type) Safire.logger.warn( "SMART metadata non-compliance: field '#{attribute}' must be an array containing only " \ "#{element_type == Hash ? 'objects' : 'strings'}" ) valid = false end valid end def array_of?(value, element_type) value.is_a?(Array) && value.all?(element_type) end def issuer_and_jwks_uri_required? openid_connect_capability? end def ehr_launch_capability? || standalone_launch_capability? end # Validates PKCE code challenge methods per SMART App Launch 2.2.0: # - 'S256' SHALL be included # - 'plain' SHALL NOT be included # # @return [Boolean] true if both conditions are satisfied def validate_pkce_methods! valid = true unless array_includes?(:code_challenge_methods_supported, 'S256') Safire.logger.warn( "SMART metadata non-compliance: 'S256' is missing from code_challenge_methods_supported " \ '(SMART App Launch 2.2.0 requires S256)' ) valid = false end if array_includes?(:code_challenge_methods_supported, 'plain') Safire.logger.warn( "SMART metadata non-compliance: 'plain' is present in code_challenge_methods_supported " \ '(SMART App Launch 2.2.0 prohibits plain)' ) valid = false end valid end end |
#token_endpoint ⇒ String (readonly)
Returns URL of the server’s OAuth2 Token Endpoint.
52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 |
# File 'lib/safire/protocols/smart_metadata.rb', line 52 class SmartMetadata < Safire::Entity REQUIRED_ATTRIBUTES = %i[ grant_types_supported token_endpoint capabilities code_challenge_methods_supported ].freeze OPTIONAL_ATTRIBUTES = %i[ issuer jwks_uri authorization_endpoint token_endpoint_auth_methods_supported token_endpoint_auth_signing_alg_values_supported registration_endpoint associated_endpoints user_access_brand_bundle user_access_brand_identifier scopes_supported response_types_supported management_endpoint introspection_endpoint revocation_endpoint ].freeze ATTRIBUTES = (REQUIRED_ATTRIBUTES | OPTIONAL_ATTRIBUTES).freeze ARRAY_ATTRIBUTE_ELEMENT_TYPES = { grant_types_supported: String, token_endpoint_auth_methods_supported: String, token_endpoint_auth_signing_alg_values_supported: String, associated_endpoints: Hash, scopes_supported: String, response_types_supported: String, capabilities: String, code_challenge_methods_supported: String }.freeze private_constant :ARRAY_ATTRIBUTE_ELEMENT_TYPES # Supported asymmetric signing algorithms (required by SMART spec) SUPPORTED_ASYMMETRIC_ALGORITHMS = %w[RS384 ES384].freeze attr_reader(*ATTRIBUTES) def initialize() super(, ATTRIBUTES) end # Runs Safire's selected SMART App Launch 2.2.0 structural diagnostics. # # This is a user-callable helper. Safire performs discovery without automatically # asserting server compliance — it is the caller's responsibility to invoke this # method when they wish to inspect these conformance signals. This helper is # not a complete server certification suite. # # Checks performed: # - All required fields are present # (token_endpoint, grant_types_supported, capabilities, code_challenge_methods_supported) # - Conditional fields present when their capability is advertised # (issuer + jwks_uri for sso-openid-connect; authorization_endpoint for launch types) # - Array-valued fields are arrays containing values of the documented type # - `code_challenge_methods_supported` includes 'S256' # (SMART App Launch 2.2.0, §Conformance — SHALL be included) # - `code_challenge_methods_supported` does NOT include 'plain' # (SMART App Launch 2.2.0, §Conformance — SHALL NOT be included) # # A warning is logged for each SMART 2.2.0 violation detected. # # @return [Boolean] true if all checks pass, false if any violation is found def valid? required_attrs = [*REQUIRED_ATTRIBUTES] required_attrs.push(:issuer, :jwks_uri) if issuer_and_jwks_uri_required? required_attrs.push(:authorization_endpoint) if missing_attrs = required_attrs.reject { |attr| public_send(attr) } missing_attrs.each do |attr| Safire.logger.warn("SMART metadata non-compliance: required field '#{attr}' is missing") end array_fields_valid = validate_array_fields! pkce_valid = validate_pkce_methods! missing_attrs.empty? && array_fields_valid && pkce_valid end # Launch type support checks - requires both capability and authorization_endpoint def supports_ehr_launch? ehr_launch_capability? && .present? end def supports_standalone_launch? standalone_launch_capability? && .present? end # Authentication type support checks # Checks if the server supports public client authentication. # @return [Boolean] true if server has client-public capability def supports_public_auth? capability?('client-public') end # Checks if the server supports confidential symmetric authentication. # @return [Boolean] true if server has capability and auth methods not advertised or includes client_secret_basic def supports_symmetric_auth? capability?('client-confidential-symmetric') && auth_method_supported?('client_secret_basic') end # Checks if the server supports the SMART Backend Services workflow. # @return [Boolean] true if the server advertises the client_credentials grant type # and supports private_key_jwt authentication (via {#supports_asymmetric_auth?}) def supports_backend_services? array_includes?(:grant_types_supported, 'client_credentials') && supports_asymmetric_auth? end # Checks if the server supports confidential asymmetric authentication. # @return [Boolean] true if server has capability, auth methods not advertised or includes private_key_jwt, # and has supported algorithms def supports_asymmetric_auth? capability?('client-confidential-asymmetric') && auth_method_supported?('private_key_jwt') && asymmetric_signing_algorithms_supported.any? end # Returns the asymmetric signing algorithms supported by both client and server. # If the server doesn't advertise algorithms, assumes it supports the required ones (RS384, ES384). # @return [Array<String>] list of supported algorithms def asymmetric_signing_algorithms_supported server_algs = token_endpoint_auth_signing_alg_values_supported return SUPPORTED_ASYMMETRIC_ALGORITHMS.dup if server_algs.nil? || server_algs == [] return [] unless array_of?(server_algs, String) server_algs & SUPPORTED_ASYMMETRIC_ALGORITHMS end # Feature support checks # Checks whether the server supports Dynamic Client Registration (RFC 7591). # @return [Boolean] true if the server advertises a +registration_endpoint+ def supports_dynamic_registration? registration_endpoint.present? end def capability?('authorize-post') end def supports_openid_connect? openid_connect_capability? && issuer.present? && jwks_uri.present? end # Capability-only checks (does not verify required fields are present) def ehr_launch_capability? capability?('launch-ehr') end def standalone_launch_capability? capability?('launch-standalone') end def openid_connect_capability? capability?('sso-openid-connect') end private def capability?(name) array_includes?(:capabilities, name) end def array_includes?(attribute, value) values = public_send(attribute) array_of?(values, String) && values.include?(value) end def auth_method_supported?(method) methods = token_endpoint_auth_methods_supported methods.nil? || methods == [] || array_includes?(:token_endpoint_auth_methods_supported, method) end def validate_array_fields! valid = true ARRAY_ATTRIBUTE_ELEMENT_TYPES.each do |attribute, element_type| value = public_send(attribute) next if value.nil? next if array_of?(value, element_type) Safire.logger.warn( "SMART metadata non-compliance: field '#{attribute}' must be an array containing only " \ "#{element_type == Hash ? 'objects' : 'strings'}" ) valid = false end valid end def array_of?(value, element_type) value.is_a?(Array) && value.all?(element_type) end def issuer_and_jwks_uri_required? openid_connect_capability? end def ehr_launch_capability? || standalone_launch_capability? end # Validates PKCE code challenge methods per SMART App Launch 2.2.0: # - 'S256' SHALL be included # - 'plain' SHALL NOT be included # # @return [Boolean] true if both conditions are satisfied def validate_pkce_methods! valid = true unless array_includes?(:code_challenge_methods_supported, 'S256') Safire.logger.warn( "SMART metadata non-compliance: 'S256' is missing from code_challenge_methods_supported " \ '(SMART App Launch 2.2.0 requires S256)' ) valid = false end if array_includes?(:code_challenge_methods_supported, 'plain') Safire.logger.warn( "SMART metadata non-compliance: 'plain' is present in code_challenge_methods_supported " \ '(SMART App Launch 2.2.0 prohibits plain)' ) valid = false end valid end end |
#token_endpoint_auth_methods_supported ⇒ Array<String> (readonly)
Returns list of client authentication methods supported at the token endpoint. Optionally provided.
52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 |
# File 'lib/safire/protocols/smart_metadata.rb', line 52 class SmartMetadata < Safire::Entity REQUIRED_ATTRIBUTES = %i[ grant_types_supported token_endpoint capabilities code_challenge_methods_supported ].freeze OPTIONAL_ATTRIBUTES = %i[ issuer jwks_uri authorization_endpoint token_endpoint_auth_methods_supported token_endpoint_auth_signing_alg_values_supported registration_endpoint associated_endpoints user_access_brand_bundle user_access_brand_identifier scopes_supported response_types_supported management_endpoint introspection_endpoint revocation_endpoint ].freeze ATTRIBUTES = (REQUIRED_ATTRIBUTES | OPTIONAL_ATTRIBUTES).freeze ARRAY_ATTRIBUTE_ELEMENT_TYPES = { grant_types_supported: String, token_endpoint_auth_methods_supported: String, token_endpoint_auth_signing_alg_values_supported: String, associated_endpoints: Hash, scopes_supported: String, response_types_supported: String, capabilities: String, code_challenge_methods_supported: String }.freeze private_constant :ARRAY_ATTRIBUTE_ELEMENT_TYPES # Supported asymmetric signing algorithms (required by SMART spec) SUPPORTED_ASYMMETRIC_ALGORITHMS = %w[RS384 ES384].freeze attr_reader(*ATTRIBUTES) def initialize() super(, ATTRIBUTES) end # Runs Safire's selected SMART App Launch 2.2.0 structural diagnostics. # # This is a user-callable helper. Safire performs discovery without automatically # asserting server compliance — it is the caller's responsibility to invoke this # method when they wish to inspect these conformance signals. This helper is # not a complete server certification suite. # # Checks performed: # - All required fields are present # (token_endpoint, grant_types_supported, capabilities, code_challenge_methods_supported) # - Conditional fields present when their capability is advertised # (issuer + jwks_uri for sso-openid-connect; authorization_endpoint for launch types) # - Array-valued fields are arrays containing values of the documented type # - `code_challenge_methods_supported` includes 'S256' # (SMART App Launch 2.2.0, §Conformance — SHALL be included) # - `code_challenge_methods_supported` does NOT include 'plain' # (SMART App Launch 2.2.0, §Conformance — SHALL NOT be included) # # A warning is logged for each SMART 2.2.0 violation detected. # # @return [Boolean] true if all checks pass, false if any violation is found def valid? required_attrs = [*REQUIRED_ATTRIBUTES] required_attrs.push(:issuer, :jwks_uri) if issuer_and_jwks_uri_required? required_attrs.push(:authorization_endpoint) if missing_attrs = required_attrs.reject { |attr| public_send(attr) } missing_attrs.each do |attr| Safire.logger.warn("SMART metadata non-compliance: required field '#{attr}' is missing") end array_fields_valid = validate_array_fields! pkce_valid = validate_pkce_methods! missing_attrs.empty? && array_fields_valid && pkce_valid end # Launch type support checks - requires both capability and authorization_endpoint def supports_ehr_launch? ehr_launch_capability? && .present? end def supports_standalone_launch? standalone_launch_capability? && .present? end # Authentication type support checks # Checks if the server supports public client authentication. # @return [Boolean] true if server has client-public capability def supports_public_auth? capability?('client-public') end # Checks if the server supports confidential symmetric authentication. # @return [Boolean] true if server has capability and auth methods not advertised or includes client_secret_basic def supports_symmetric_auth? capability?('client-confidential-symmetric') && auth_method_supported?('client_secret_basic') end # Checks if the server supports the SMART Backend Services workflow. # @return [Boolean] true if the server advertises the client_credentials grant type # and supports private_key_jwt authentication (via {#supports_asymmetric_auth?}) def supports_backend_services? array_includes?(:grant_types_supported, 'client_credentials') && supports_asymmetric_auth? end # Checks if the server supports confidential asymmetric authentication. # @return [Boolean] true if server has capability, auth methods not advertised or includes private_key_jwt, # and has supported algorithms def supports_asymmetric_auth? capability?('client-confidential-asymmetric') && auth_method_supported?('private_key_jwt') && asymmetric_signing_algorithms_supported.any? end # Returns the asymmetric signing algorithms supported by both client and server. # If the server doesn't advertise algorithms, assumes it supports the required ones (RS384, ES384). # @return [Array<String>] list of supported algorithms def asymmetric_signing_algorithms_supported server_algs = token_endpoint_auth_signing_alg_values_supported return SUPPORTED_ASYMMETRIC_ALGORITHMS.dup if server_algs.nil? || server_algs == [] return [] unless array_of?(server_algs, String) server_algs & SUPPORTED_ASYMMETRIC_ALGORITHMS end # Feature support checks # Checks whether the server supports Dynamic Client Registration (RFC 7591). # @return [Boolean] true if the server advertises a +registration_endpoint+ def supports_dynamic_registration? registration_endpoint.present? end def capability?('authorize-post') end def supports_openid_connect? openid_connect_capability? && issuer.present? && jwks_uri.present? end # Capability-only checks (does not verify required fields are present) def ehr_launch_capability? capability?('launch-ehr') end def standalone_launch_capability? capability?('launch-standalone') end def openid_connect_capability? capability?('sso-openid-connect') end private def capability?(name) array_includes?(:capabilities, name) end def array_includes?(attribute, value) values = public_send(attribute) array_of?(values, String) && values.include?(value) end def auth_method_supported?(method) methods = token_endpoint_auth_methods_supported methods.nil? || methods == [] || array_includes?(:token_endpoint_auth_methods_supported, method) end def validate_array_fields! valid = true ARRAY_ATTRIBUTE_ELEMENT_TYPES.each do |attribute, element_type| value = public_send(attribute) next if value.nil? next if array_of?(value, element_type) Safire.logger.warn( "SMART metadata non-compliance: field '#{attribute}' must be an array containing only " \ "#{element_type == Hash ? 'objects' : 'strings'}" ) valid = false end valid end def array_of?(value, element_type) value.is_a?(Array) && value.all?(element_type) end def issuer_and_jwks_uri_required? openid_connect_capability? end def ehr_launch_capability? || standalone_launch_capability? end # Validates PKCE code challenge methods per SMART App Launch 2.2.0: # - 'S256' SHALL be included # - 'plain' SHALL NOT be included # # @return [Boolean] true if both conditions are satisfied def validate_pkce_methods! valid = true unless array_includes?(:code_challenge_methods_supported, 'S256') Safire.logger.warn( "SMART metadata non-compliance: 'S256' is missing from code_challenge_methods_supported " \ '(SMART App Launch 2.2.0 requires S256)' ) valid = false end if array_includes?(:code_challenge_methods_supported, 'plain') Safire.logger.warn( "SMART metadata non-compliance: 'plain' is present in code_challenge_methods_supported " \ '(SMART App Launch 2.2.0 prohibits plain)' ) valid = false end valid end end |
#token_endpoint_auth_signing_alg_values_supported ⇒ Array<String> (readonly)
Returns list of signing algorithms supported for JWT-based client authentication. Optionally provided. Used for confidential asymmetric authentication.
52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 |
# File 'lib/safire/protocols/smart_metadata.rb', line 52 class SmartMetadata < Safire::Entity REQUIRED_ATTRIBUTES = %i[ grant_types_supported token_endpoint capabilities code_challenge_methods_supported ].freeze OPTIONAL_ATTRIBUTES = %i[ issuer jwks_uri authorization_endpoint token_endpoint_auth_methods_supported token_endpoint_auth_signing_alg_values_supported registration_endpoint associated_endpoints user_access_brand_bundle user_access_brand_identifier scopes_supported response_types_supported management_endpoint introspection_endpoint revocation_endpoint ].freeze ATTRIBUTES = (REQUIRED_ATTRIBUTES | OPTIONAL_ATTRIBUTES).freeze ARRAY_ATTRIBUTE_ELEMENT_TYPES = { grant_types_supported: String, token_endpoint_auth_methods_supported: String, token_endpoint_auth_signing_alg_values_supported: String, associated_endpoints: Hash, scopes_supported: String, response_types_supported: String, capabilities: String, code_challenge_methods_supported: String }.freeze private_constant :ARRAY_ATTRIBUTE_ELEMENT_TYPES # Supported asymmetric signing algorithms (required by SMART spec) SUPPORTED_ASYMMETRIC_ALGORITHMS = %w[RS384 ES384].freeze attr_reader(*ATTRIBUTES) def initialize() super(, ATTRIBUTES) end # Runs Safire's selected SMART App Launch 2.2.0 structural diagnostics. # # This is a user-callable helper. Safire performs discovery without automatically # asserting server compliance — it is the caller's responsibility to invoke this # method when they wish to inspect these conformance signals. This helper is # not a complete server certification suite. # # Checks performed: # - All required fields are present # (token_endpoint, grant_types_supported, capabilities, code_challenge_methods_supported) # - Conditional fields present when their capability is advertised # (issuer + jwks_uri for sso-openid-connect; authorization_endpoint for launch types) # - Array-valued fields are arrays containing values of the documented type # - `code_challenge_methods_supported` includes 'S256' # (SMART App Launch 2.2.0, §Conformance — SHALL be included) # - `code_challenge_methods_supported` does NOT include 'plain' # (SMART App Launch 2.2.0, §Conformance — SHALL NOT be included) # # A warning is logged for each SMART 2.2.0 violation detected. # # @return [Boolean] true if all checks pass, false if any violation is found def valid? required_attrs = [*REQUIRED_ATTRIBUTES] required_attrs.push(:issuer, :jwks_uri) if issuer_and_jwks_uri_required? required_attrs.push(:authorization_endpoint) if missing_attrs = required_attrs.reject { |attr| public_send(attr) } missing_attrs.each do |attr| Safire.logger.warn("SMART metadata non-compliance: required field '#{attr}' is missing") end array_fields_valid = validate_array_fields! pkce_valid = validate_pkce_methods! missing_attrs.empty? && array_fields_valid && pkce_valid end # Launch type support checks - requires both capability and authorization_endpoint def supports_ehr_launch? ehr_launch_capability? && .present? end def supports_standalone_launch? standalone_launch_capability? && .present? end # Authentication type support checks # Checks if the server supports public client authentication. # @return [Boolean] true if server has client-public capability def supports_public_auth? capability?('client-public') end # Checks if the server supports confidential symmetric authentication. # @return [Boolean] true if server has capability and auth methods not advertised or includes client_secret_basic def supports_symmetric_auth? capability?('client-confidential-symmetric') && auth_method_supported?('client_secret_basic') end # Checks if the server supports the SMART Backend Services workflow. # @return [Boolean] true if the server advertises the client_credentials grant type # and supports private_key_jwt authentication (via {#supports_asymmetric_auth?}) def supports_backend_services? array_includes?(:grant_types_supported, 'client_credentials') && supports_asymmetric_auth? end # Checks if the server supports confidential asymmetric authentication. # @return [Boolean] true if server has capability, auth methods not advertised or includes private_key_jwt, # and has supported algorithms def supports_asymmetric_auth? capability?('client-confidential-asymmetric') && auth_method_supported?('private_key_jwt') && asymmetric_signing_algorithms_supported.any? end # Returns the asymmetric signing algorithms supported by both client and server. # If the server doesn't advertise algorithms, assumes it supports the required ones (RS384, ES384). # @return [Array<String>] list of supported algorithms def asymmetric_signing_algorithms_supported server_algs = token_endpoint_auth_signing_alg_values_supported return SUPPORTED_ASYMMETRIC_ALGORITHMS.dup if server_algs.nil? || server_algs == [] return [] unless array_of?(server_algs, String) server_algs & SUPPORTED_ASYMMETRIC_ALGORITHMS end # Feature support checks # Checks whether the server supports Dynamic Client Registration (RFC 7591). # @return [Boolean] true if the server advertises a +registration_endpoint+ def supports_dynamic_registration? registration_endpoint.present? end def capability?('authorize-post') end def supports_openid_connect? openid_connect_capability? && issuer.present? && jwks_uri.present? end # Capability-only checks (does not verify required fields are present) def ehr_launch_capability? capability?('launch-ehr') end def standalone_launch_capability? capability?('launch-standalone') end def openid_connect_capability? capability?('sso-openid-connect') end private def capability?(name) array_includes?(:capabilities, name) end def array_includes?(attribute, value) values = public_send(attribute) array_of?(values, String) && values.include?(value) end def auth_method_supported?(method) methods = token_endpoint_auth_methods_supported methods.nil? || methods == [] || array_includes?(:token_endpoint_auth_methods_supported, method) end def validate_array_fields! valid = true ARRAY_ATTRIBUTE_ELEMENT_TYPES.each do |attribute, element_type| value = public_send(attribute) next if value.nil? next if array_of?(value, element_type) Safire.logger.warn( "SMART metadata non-compliance: field '#{attribute}' must be an array containing only " \ "#{element_type == Hash ? 'objects' : 'strings'}" ) valid = false end valid end def array_of?(value, element_type) value.is_a?(Array) && value.all?(element_type) end def issuer_and_jwks_uri_required? openid_connect_capability? end def ehr_launch_capability? || standalone_launch_capability? end # Validates PKCE code challenge methods per SMART App Launch 2.2.0: # - 'S256' SHALL be included # - 'plain' SHALL NOT be included # # @return [Boolean] true if both conditions are satisfied def validate_pkce_methods! valid = true unless array_includes?(:code_challenge_methods_supported, 'S256') Safire.logger.warn( "SMART metadata non-compliance: 'S256' is missing from code_challenge_methods_supported " \ '(SMART App Launch 2.2.0 requires S256)' ) valid = false end if array_includes?(:code_challenge_methods_supported, 'plain') Safire.logger.warn( "SMART metadata non-compliance: 'plain' is present in code_challenge_methods_supported " \ '(SMART App Launch 2.2.0 prohibits plain)' ) valid = false end valid end end |
#user_access_brand_bundle ⇒ String (readonly)
Returns URL for a Brand Bundle for user-facing applications. Optionally provided.
52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 |
# File 'lib/safire/protocols/smart_metadata.rb', line 52 class SmartMetadata < Safire::Entity REQUIRED_ATTRIBUTES = %i[ grant_types_supported token_endpoint capabilities code_challenge_methods_supported ].freeze OPTIONAL_ATTRIBUTES = %i[ issuer jwks_uri authorization_endpoint token_endpoint_auth_methods_supported token_endpoint_auth_signing_alg_values_supported registration_endpoint associated_endpoints user_access_brand_bundle user_access_brand_identifier scopes_supported response_types_supported management_endpoint introspection_endpoint revocation_endpoint ].freeze ATTRIBUTES = (REQUIRED_ATTRIBUTES | OPTIONAL_ATTRIBUTES).freeze ARRAY_ATTRIBUTE_ELEMENT_TYPES = { grant_types_supported: String, token_endpoint_auth_methods_supported: String, token_endpoint_auth_signing_alg_values_supported: String, associated_endpoints: Hash, scopes_supported: String, response_types_supported: String, capabilities: String, code_challenge_methods_supported: String }.freeze private_constant :ARRAY_ATTRIBUTE_ELEMENT_TYPES # Supported asymmetric signing algorithms (required by SMART spec) SUPPORTED_ASYMMETRIC_ALGORITHMS = %w[RS384 ES384].freeze attr_reader(*ATTRIBUTES) def initialize() super(, ATTRIBUTES) end # Runs Safire's selected SMART App Launch 2.2.0 structural diagnostics. # # This is a user-callable helper. Safire performs discovery without automatically # asserting server compliance — it is the caller's responsibility to invoke this # method when they wish to inspect these conformance signals. This helper is # not a complete server certification suite. # # Checks performed: # - All required fields are present # (token_endpoint, grant_types_supported, capabilities, code_challenge_methods_supported) # - Conditional fields present when their capability is advertised # (issuer + jwks_uri for sso-openid-connect; authorization_endpoint for launch types) # - Array-valued fields are arrays containing values of the documented type # - `code_challenge_methods_supported` includes 'S256' # (SMART App Launch 2.2.0, §Conformance — SHALL be included) # - `code_challenge_methods_supported` does NOT include 'plain' # (SMART App Launch 2.2.0, §Conformance — SHALL NOT be included) # # A warning is logged for each SMART 2.2.0 violation detected. # # @return [Boolean] true if all checks pass, false if any violation is found def valid? required_attrs = [*REQUIRED_ATTRIBUTES] required_attrs.push(:issuer, :jwks_uri) if issuer_and_jwks_uri_required? required_attrs.push(:authorization_endpoint) if missing_attrs = required_attrs.reject { |attr| public_send(attr) } missing_attrs.each do |attr| Safire.logger.warn("SMART metadata non-compliance: required field '#{attr}' is missing") end array_fields_valid = validate_array_fields! pkce_valid = validate_pkce_methods! missing_attrs.empty? && array_fields_valid && pkce_valid end # Launch type support checks - requires both capability and authorization_endpoint def supports_ehr_launch? ehr_launch_capability? && .present? end def supports_standalone_launch? standalone_launch_capability? && .present? end # Authentication type support checks # Checks if the server supports public client authentication. # @return [Boolean] true if server has client-public capability def supports_public_auth? capability?('client-public') end # Checks if the server supports confidential symmetric authentication. # @return [Boolean] true if server has capability and auth methods not advertised or includes client_secret_basic def supports_symmetric_auth? capability?('client-confidential-symmetric') && auth_method_supported?('client_secret_basic') end # Checks if the server supports the SMART Backend Services workflow. # @return [Boolean] true if the server advertises the client_credentials grant type # and supports private_key_jwt authentication (via {#supports_asymmetric_auth?}) def supports_backend_services? array_includes?(:grant_types_supported, 'client_credentials') && supports_asymmetric_auth? end # Checks if the server supports confidential asymmetric authentication. # @return [Boolean] true if server has capability, auth methods not advertised or includes private_key_jwt, # and has supported algorithms def supports_asymmetric_auth? capability?('client-confidential-asymmetric') && auth_method_supported?('private_key_jwt') && asymmetric_signing_algorithms_supported.any? end # Returns the asymmetric signing algorithms supported by both client and server. # If the server doesn't advertise algorithms, assumes it supports the required ones (RS384, ES384). # @return [Array<String>] list of supported algorithms def asymmetric_signing_algorithms_supported server_algs = token_endpoint_auth_signing_alg_values_supported return SUPPORTED_ASYMMETRIC_ALGORITHMS.dup if server_algs.nil? || server_algs == [] return [] unless array_of?(server_algs, String) server_algs & SUPPORTED_ASYMMETRIC_ALGORITHMS end # Feature support checks # Checks whether the server supports Dynamic Client Registration (RFC 7591). # @return [Boolean] true if the server advertises a +registration_endpoint+ def supports_dynamic_registration? registration_endpoint.present? end def capability?('authorize-post') end def supports_openid_connect? openid_connect_capability? && issuer.present? && jwks_uri.present? end # Capability-only checks (does not verify required fields are present) def ehr_launch_capability? capability?('launch-ehr') end def standalone_launch_capability? capability?('launch-standalone') end def openid_connect_capability? capability?('sso-openid-connect') end private def capability?(name) array_includes?(:capabilities, name) end def array_includes?(attribute, value) values = public_send(attribute) array_of?(values, String) && values.include?(value) end def auth_method_supported?(method) methods = token_endpoint_auth_methods_supported methods.nil? || methods == [] || array_includes?(:token_endpoint_auth_methods_supported, method) end def validate_array_fields! valid = true ARRAY_ATTRIBUTE_ELEMENT_TYPES.each do |attribute, element_type| value = public_send(attribute) next if value.nil? next if array_of?(value, element_type) Safire.logger.warn( "SMART metadata non-compliance: field '#{attribute}' must be an array containing only " \ "#{element_type == Hash ? 'objects' : 'strings'}" ) valid = false end valid end def array_of?(value, element_type) value.is_a?(Array) && value.all?(element_type) end def issuer_and_jwks_uri_required? openid_connect_capability? end def ehr_launch_capability? || standalone_launch_capability? end # Validates PKCE code challenge methods per SMART App Launch 2.2.0: # - 'S256' SHALL be included # - 'plain' SHALL NOT be included # # @return [Boolean] true if both conditions are satisfied def validate_pkce_methods! valid = true unless array_includes?(:code_challenge_methods_supported, 'S256') Safire.logger.warn( "SMART metadata non-compliance: 'S256' is missing from code_challenge_methods_supported " \ '(SMART App Launch 2.2.0 requires S256)' ) valid = false end if array_includes?(:code_challenge_methods_supported, 'plain') Safire.logger.warn( "SMART metadata non-compliance: 'plain' is present in code_challenge_methods_supported " \ '(SMART App Launch 2.2.0 prohibits plain)' ) valid = false end valid end end |
#user_access_brand_identifier ⇒ String (readonly)
Returns Identifier for the primary entry in a Brand Bundle. Optionally provided.
52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 |
# File 'lib/safire/protocols/smart_metadata.rb', line 52 class SmartMetadata < Safire::Entity REQUIRED_ATTRIBUTES = %i[ grant_types_supported token_endpoint capabilities code_challenge_methods_supported ].freeze OPTIONAL_ATTRIBUTES = %i[ issuer jwks_uri authorization_endpoint token_endpoint_auth_methods_supported token_endpoint_auth_signing_alg_values_supported registration_endpoint associated_endpoints user_access_brand_bundle user_access_brand_identifier scopes_supported response_types_supported management_endpoint introspection_endpoint revocation_endpoint ].freeze ATTRIBUTES = (REQUIRED_ATTRIBUTES | OPTIONAL_ATTRIBUTES).freeze ARRAY_ATTRIBUTE_ELEMENT_TYPES = { grant_types_supported: String, token_endpoint_auth_methods_supported: String, token_endpoint_auth_signing_alg_values_supported: String, associated_endpoints: Hash, scopes_supported: String, response_types_supported: String, capabilities: String, code_challenge_methods_supported: String }.freeze private_constant :ARRAY_ATTRIBUTE_ELEMENT_TYPES # Supported asymmetric signing algorithms (required by SMART spec) SUPPORTED_ASYMMETRIC_ALGORITHMS = %w[RS384 ES384].freeze attr_reader(*ATTRIBUTES) def initialize() super(, ATTRIBUTES) end # Runs Safire's selected SMART App Launch 2.2.0 structural diagnostics. # # This is a user-callable helper. Safire performs discovery without automatically # asserting server compliance — it is the caller's responsibility to invoke this # method when they wish to inspect these conformance signals. This helper is # not a complete server certification suite. # # Checks performed: # - All required fields are present # (token_endpoint, grant_types_supported, capabilities, code_challenge_methods_supported) # - Conditional fields present when their capability is advertised # (issuer + jwks_uri for sso-openid-connect; authorization_endpoint for launch types) # - Array-valued fields are arrays containing values of the documented type # - `code_challenge_methods_supported` includes 'S256' # (SMART App Launch 2.2.0, §Conformance — SHALL be included) # - `code_challenge_methods_supported` does NOT include 'plain' # (SMART App Launch 2.2.0, §Conformance — SHALL NOT be included) # # A warning is logged for each SMART 2.2.0 violation detected. # # @return [Boolean] true if all checks pass, false if any violation is found def valid? required_attrs = [*REQUIRED_ATTRIBUTES] required_attrs.push(:issuer, :jwks_uri) if issuer_and_jwks_uri_required? required_attrs.push(:authorization_endpoint) if missing_attrs = required_attrs.reject { |attr| public_send(attr) } missing_attrs.each do |attr| Safire.logger.warn("SMART metadata non-compliance: required field '#{attr}' is missing") end array_fields_valid = validate_array_fields! pkce_valid = validate_pkce_methods! missing_attrs.empty? && array_fields_valid && pkce_valid end # Launch type support checks - requires both capability and authorization_endpoint def supports_ehr_launch? ehr_launch_capability? && .present? end def supports_standalone_launch? standalone_launch_capability? && .present? end # Authentication type support checks # Checks if the server supports public client authentication. # @return [Boolean] true if server has client-public capability def supports_public_auth? capability?('client-public') end # Checks if the server supports confidential symmetric authentication. # @return [Boolean] true if server has capability and auth methods not advertised or includes client_secret_basic def supports_symmetric_auth? capability?('client-confidential-symmetric') && auth_method_supported?('client_secret_basic') end # Checks if the server supports the SMART Backend Services workflow. # @return [Boolean] true if the server advertises the client_credentials grant type # and supports private_key_jwt authentication (via {#supports_asymmetric_auth?}) def supports_backend_services? array_includes?(:grant_types_supported, 'client_credentials') && supports_asymmetric_auth? end # Checks if the server supports confidential asymmetric authentication. # @return [Boolean] true if server has capability, auth methods not advertised or includes private_key_jwt, # and has supported algorithms def supports_asymmetric_auth? capability?('client-confidential-asymmetric') && auth_method_supported?('private_key_jwt') && asymmetric_signing_algorithms_supported.any? end # Returns the asymmetric signing algorithms supported by both client and server. # If the server doesn't advertise algorithms, assumes it supports the required ones (RS384, ES384). # @return [Array<String>] list of supported algorithms def asymmetric_signing_algorithms_supported server_algs = token_endpoint_auth_signing_alg_values_supported return SUPPORTED_ASYMMETRIC_ALGORITHMS.dup if server_algs.nil? || server_algs == [] return [] unless array_of?(server_algs, String) server_algs & SUPPORTED_ASYMMETRIC_ALGORITHMS end # Feature support checks # Checks whether the server supports Dynamic Client Registration (RFC 7591). # @return [Boolean] true if the server advertises a +registration_endpoint+ def supports_dynamic_registration? registration_endpoint.present? end def capability?('authorize-post') end def supports_openid_connect? openid_connect_capability? && issuer.present? && jwks_uri.present? end # Capability-only checks (does not verify required fields are present) def ehr_launch_capability? capability?('launch-ehr') end def standalone_launch_capability? capability?('launch-standalone') end def openid_connect_capability? capability?('sso-openid-connect') end private def capability?(name) array_includes?(:capabilities, name) end def array_includes?(attribute, value) values = public_send(attribute) array_of?(values, String) && values.include?(value) end def auth_method_supported?(method) methods = token_endpoint_auth_methods_supported methods.nil? || methods == [] || array_includes?(:token_endpoint_auth_methods_supported, method) end def validate_array_fields! valid = true ARRAY_ATTRIBUTE_ELEMENT_TYPES.each do |attribute, element_type| value = public_send(attribute) next if value.nil? next if array_of?(value, element_type) Safire.logger.warn( "SMART metadata non-compliance: field '#{attribute}' must be an array containing only " \ "#{element_type == Hash ? 'objects' : 'strings'}" ) valid = false end valid end def array_of?(value, element_type) value.is_a?(Array) && value.all?(element_type) end def issuer_and_jwks_uri_required? openid_connect_capability? end def ehr_launch_capability? || standalone_launch_capability? end # Validates PKCE code challenge methods per SMART App Launch 2.2.0: # - 'S256' SHALL be included # - 'plain' SHALL NOT be included # # @return [Boolean] true if both conditions are satisfied def validate_pkce_methods! valid = true unless array_includes?(:code_challenge_methods_supported, 'S256') Safire.logger.warn( "SMART metadata non-compliance: 'S256' is missing from code_challenge_methods_supported " \ '(SMART App Launch 2.2.0 requires S256)' ) valid = false end if array_includes?(:code_challenge_methods_supported, 'plain') Safire.logger.warn( "SMART metadata non-compliance: 'plain' is present in code_challenge_methods_supported " \ '(SMART App Launch 2.2.0 prohibits plain)' ) valid = false end valid end end |
Instance Method Details
#asymmetric_signing_algorithms_supported ⇒ Array<String>
Returns the asymmetric signing algorithms supported by both client and server. If the server doesn't advertise algorithms, assumes it supports the required ones (RS384, ES384).
180 181 182 183 184 185 186 |
# File 'lib/safire/protocols/smart_metadata.rb', line 180 def asymmetric_signing_algorithms_supported server_algs = token_endpoint_auth_signing_alg_values_supported return SUPPORTED_ASYMMETRIC_ALGORITHMS.dup if server_algs.nil? || server_algs == [] return [] unless array_of?(server_algs, String) server_algs & SUPPORTED_ASYMMETRIC_ALGORITHMS end |
#ehr_launch_capability? ⇒ Boolean
Capability-only checks (does not verify required fields are present)
206 207 208 |
# File 'lib/safire/protocols/smart_metadata.rb', line 206 def ehr_launch_capability? capability?('launch-ehr') end |
#openid_connect_capability? ⇒ Boolean
214 215 216 |
# File 'lib/safire/protocols/smart_metadata.rb', line 214 def openid_connect_capability? capability?('sso-openid-connect') end |
#standalone_launch_capability? ⇒ Boolean
210 211 212 |
# File 'lib/safire/protocols/smart_metadata.rb', line 210 def standalone_launch_capability? capability?('launch-standalone') end |
#supports_asymmetric_auth? ⇒ Boolean
Checks if the server supports confidential asymmetric authentication.
171 172 173 174 175 |
# File 'lib/safire/protocols/smart_metadata.rb', line 171 def supports_asymmetric_auth? capability?('client-confidential-asymmetric') && auth_method_supported?('private_key_jwt') && asymmetric_signing_algorithms_supported.any? end |
#supports_backend_services? ⇒ Boolean
Checks if the server supports the SMART Backend Services workflow.
163 164 165 166 |
# File 'lib/safire/protocols/smart_metadata.rb', line 163 def supports_backend_services? array_includes?(:grant_types_supported, 'client_credentials') && supports_asymmetric_auth? end |
#supports_dynamic_registration? ⇒ Boolean
Checks whether the server supports Dynamic Client Registration (RFC 7591).
192 193 194 |
# File 'lib/safire/protocols/smart_metadata.rb', line 192 def supports_dynamic_registration? registration_endpoint.present? end |
#supports_ehr_launch? ⇒ Boolean
Launch type support checks - requires both capability and authorization_endpoint
137 138 139 |
# File 'lib/safire/protocols/smart_metadata.rb', line 137 def supports_ehr_launch? ehr_launch_capability? && .present? end |
#supports_openid_connect? ⇒ Boolean
200 201 202 |
# File 'lib/safire/protocols/smart_metadata.rb', line 200 def supports_openid_connect? openid_connect_capability? && issuer.present? && jwks_uri.present? end |
#supports_post_based_authorization? ⇒ Boolean
196 197 198 |
# File 'lib/safire/protocols/smart_metadata.rb', line 196 def capability?('authorize-post') end |
#supports_public_auth? ⇒ Boolean
Checks if the server supports public client authentication.
149 150 151 |
# File 'lib/safire/protocols/smart_metadata.rb', line 149 def supports_public_auth? capability?('client-public') end |
#supports_standalone_launch? ⇒ Boolean
141 142 143 |
# File 'lib/safire/protocols/smart_metadata.rb', line 141 def supports_standalone_launch? standalone_launch_capability? && .present? end |
#supports_symmetric_auth? ⇒ Boolean
Checks if the server supports confidential symmetric authentication.
155 156 157 158 |
# File 'lib/safire/protocols/smart_metadata.rb', line 155 def supports_symmetric_auth? capability?('client-confidential-symmetric') && auth_method_supported?('client_secret_basic') end |
#valid? ⇒ Boolean
Runs Safire's selected SMART App Launch 2.2.0 structural diagnostics.
This is a user-callable helper. Safire performs discovery without automatically asserting server compliance — it is the caller's responsibility to invoke this method when they wish to inspect these conformance signals. This helper is not a complete server certification suite.
Checks performed:
- All required fields are present (token_endpoint, grant_types_supported, capabilities, code_challenge_methods_supported)
- Conditional fields present when their capability is advertised (issuer + jwks_uri for sso-openid-connect; authorization_endpoint for launch types)
- Array-valued fields are arrays containing values of the documented type
code_challenge_methods_supportedincludes 'S256' (SMART App Launch 2.2.0, §Conformance — SHALL be included)code_challenge_methods_supporteddoes NOT include 'plain' (SMART App Launch 2.2.0, §Conformance — SHALL NOT be included)
A warning is logged for each SMART 2.2.0 violation detected.
119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 |
# File 'lib/safire/protocols/smart_metadata.rb', line 119 def valid? required_attrs = [*REQUIRED_ATTRIBUTES] required_attrs.push(:issuer, :jwks_uri) if issuer_and_jwks_uri_required? required_attrs.push(:authorization_endpoint) if missing_attrs = required_attrs.reject { |attr| public_send(attr) } missing_attrs.each do |attr| Safire.logger.warn("SMART metadata non-compliance: required field '#{attr}' is missing") end array_fields_valid = validate_array_fields! pkce_valid = validate_pkce_methods! missing_attrs.empty? && array_fields_valid && pkce_valid end |