Confidential Client and Network Errors
Table of contents
- Confidential Client and Network Errors
- Dynamic Client Registration Errors
DiscoveryError: Server does not advertise a registration endpointValidationError: UDAP registration metadata or certifications are invalidCertificateError: UDAP client signing identity cannot support registrationRegistrationError: Server rejected the registration requestRegistrationError: 2xx response has a missing or invalidclient_idRegistrationError: UDAP cancellation response does not confirm cancellation
- Confidential Symmetric Client Errors
- Confidential Asymmetric Client Errors
- Backend Services Errors
- Network Errors
- Dynamic Client Registration Errors
Dynamic Client Registration Errors
DiscoveryError: Server does not advertise a registration endpoint
Safire::Errors::DiscoveryError: Failed to discover SMART configuration from https://...: server does not advertise a 'registration_endpoint'
The server either does not support Dynamic Client Registration or its registration endpoint is not published in /.well-known/smart-configuration. Pass the endpoint explicitly or register manually through the server’s developer portal:
registration = client.register_client(
metadata,
registration_endpoint: 'https://auth.example.com/register'
)
For UDAP, the registration endpoint is always discovery-bound. Safire raises DiscoveryError before POSTing when discovery fails, signed_metadata cannot be validated, the server does not advertise usable udap_dcr capability, or the registration algorithm and certification-requirement fields cannot be used safely. Full structural conformance remains an explicit UdapMetadata#valid? diagnostic and is not an automatic DCR gate:
registration = udap_client.register_client(
metadata,
client_uri: 'https://client.example.com',
trusted_anchors: [ca_cert],
crls: [ca_crl]
)
A missing RS256 advertisement produces a warning because STU2 requires that server baseline, but Safire can proceed with another advertised, supported, key-compatible algorithm.
Scope handling depends on what the request needs from scopes_supported. Registration and modification reject an unadvertised requested wildcard with ValidationError, because STU2 permits requesting a wildcard only when it is advertised. When a wildcard is requested but scopes_supported provides no usable scope strings, Safire raises DiscoveryError: it is obligated to evaluate the wildcard and the server’s advertisement makes that impossible. A malformed entry beside usable ones does not trigger this; Safire decides against the usable entries and leaves the defect to UdapMetadata#valid?. The same unusable metadata only warns when every requested scope is non-wildcard, because no client-side rule depends on the advertisement there and the authorization server remains free to grant or reject the request. Cancellation warns and proceeds in every scope case, so scope-advertisement drift alone never blocks cleanup.
ValidationError: UDAP registration metadata or certifications are invalid
Safire::Errors::ValidationError: Validation failed for certifications: must be nil or an array of compact JWS strings
UDAP registration signs caller-controlled metadata before sending it. Safire therefore validates the metadata and certification collection locally and raises ValidationError before building a software statement when input is malformed.
Common causes:
- missing required metadata such as
client_name,contacts,grant_types, orscope - unsupported grant combinations
- non-HTTPS redirect or logo URIs, except HTTP localhost when explicitly enabled for development
- reserved claims such as
iss,sub,aud,exp,jti,software_statement,certifications, orudap certifications:is notnilor an array of compact JWS strings- discovery declares required certification policy URIs, but
certifications:is omitted or empty; the server still decides whether supplied JWTs satisfy those policies
CertificateError: UDAP client signing identity cannot support registration
Safire::Errors::CertificateError: Certificate error — client_uri does not match any URI SAN in the leaf certificate
UDAP registration requires a private key and leaf-first certificate chain. The private key must match the leaf certificate, every certificate must be currently valid, and the client_uri: must exactly match a URI Subject Alternative Name in the leaf certificate. Case, port, and trailing slash differences are significant.
RegistrationError: Server rejected the registration request
Safire::Errors::RegistrationError: Client registration failed — HTTP 400 — invalid_redirect_uri — Redirect URI must use HTTPS
The server returned an OAuth 2.0 error response. Check e.error_code and e.error_description for the specific reason, correct the metadata, and retry:
rescue Safire::Errors::RegistrationError => e
puts e.status # 400
puts e.error_code # "invalid_redirect_uri"
puts e.error_description # "Redirect URI must use HTTPS"
UDAP servers may return UDAP-specific error codes such as invalid_software_statement or unapproved_software_statement; Safire preserves them in e.error_code.
RegistrationError: 2xx response has a missing or invalid client_id
Safire::Errors::RegistrationError: Registration response missing client_id; received fields: error, error_description
The server returned a successful HTTP status without a valid client_id. A successful registration response must contain a non-blank string client_id.
If the response omitted the key, check e.received_fields to see what the server returned:
rescue Safire::Errors::RegistrationError => e
puts e.received_fields # ["error", "error_description"]
end
If the key was present but its value was nil, blank, or not a string, e.error_description reports the validation failure and e.received_fields is nil:
rescue Safire::Errors::RegistrationError => e
puts e.error_description # "response client_id must be a non-blank string"
end
Either response is unusable as a confirmed registration. Safire does not retry automatically: the authorization server may already have committed a registration or modification before returning a response Safire cannot safely consume. Use the server’s registration-management mechanism or operator portal before submitting another request.
UDAP assigns completed registration meanings to 201 Created and the update-style 200 OK response. Safire returns registration metadata only for those outcomes. A 202 Accepted response means processing is pending under HTTP semantics and does not establish completion, even when its body contains a valid-looking client_id. Safire raises RegistrationError with e.status == 202; callers remain responsible for any server-specific status or management workflow.
RegistrationError: UDAP cancellation response does not confirm cancellation
Safire::Errors::RegistrationError: Client registration failed — HTTP 202 — cancellation response did not confirm cancellation: expected an empty grant_types array
UDAP registration cancellation is confirmed by the response body, not by a specific success status such as 200 or 201. Safire accepts a cancellation response only when the final status is 2xx and the body contains a non-blank string client_id plus an empty grant_types array:
cancellation = udap_client.cancel_registration(
metadata,
client_uri: 'https://client.example.com',
trusted_anchors: [ca_cert],
crls: [ca_crl]
)
cancellation['grant_types'] # => []
A non-2xx response does not confirm cancellation; inspect any preserved OAuth error code and description to determine whether the server explicitly rejected the request. A non-2xx response without an OAuth error is reported as an unconfirmed outcome; Safire does not infer rejection from status alone. If a final 2xx response omits grant_types, returns a non-array value, or returns a non-empty array, the outcome is also unconfirmed rather than rejected. Safire does not retry automatically; preserve the stored registration state and inspect the authorization server before retrying or discarding the client_id.
Confidential Symmetric Client Errors
ConfigurationError: Missing client_secret
Safire::Errors::ConfigurationError: Configuration missing: client_secret
client_secret must be present when using :confidential_symmetric:
config = Safire::ClientConfig.new(
client_secret: ENV.fetch('SMART_CLIENT_SECRET'),
# ...
)
client = Safire::Client.new(config, client_type: :confidential_symmetric)
You can also pass it as an override directly to the token call — useful when rotating secrets:
tokens = client.request_access_token(
code: code, code_verifier: verifier,
client_secret: ENV.fetch('SMART_CLIENT_SECRET')
)
401 Unauthorized with Basic Auth
Causes: incorrect credentials, or the server does not support client_secret_basic.
Verify the server supports Basic Auth before debugging credentials:
metadata = client.server_metadata
unless metadata.token_endpoint_auth_methods_supported.include?('client_secret_basic')
raise 'Server does not support client_secret_basic'
end
Safire encodes credentials with Base64.strict_encode64 — special characters in secrets are handled automatically.
Confidential Asymmetric Client Errors
ConfigurationError: Missing private_key or kid
Safire::Errors::ConfigurationError: Configuration missing: private_key, kid
Both are required for :confidential_asymmetric:
config = Safire::ClientConfig.new(
private_key: OpenSSL::PKey::RSA.new(File.read(ENV['SMART_PRIVATE_KEY_PATH'])),
kid: ENV.fetch('SMART_KEY_ID'),
# ...
)
client = Safire::Client.new(config, client_type: :confidential_asymmetric)
401 Unauthorized with JWT assertion
Causes: key mismatch, wrong kid, clock skew, or server does not support private_key_jwt.
Verify private_key_jwt is supported:
metadata = client.server_metadata
unless metadata.token_endpoint_auth_methods_supported.include?('private_key_jwt')
raise 'Server does not support private_key_jwt'
end
Verify the public key registered with the server matches the private key you are using, and that the kid value matches the key ID the server expects. Safire sets JWT exp to 5 minutes from iat — if your system clock is significantly skewed from the server, assertions will be rejected.
Backend Services Errors
Deprecated implicit scope fallback
SMART requires the Backend Services token request to carry the scopes selected by the client. Configure them on ClientConfig or pass them per request:
client.request_backend_token(scopes: ['system/Patient.rs'])
Safire v0.4.x temporarily falls back to system/*.rs and logs a deprecation warning when both sources are absent. This compatibility behavior is removed in v0.6.0, when the call will raise ConfigurationError instead.
Safire does not treat discovery scopes_supported as an exhaustive allow-list. The server may support scopes it does not advertise, so explicit client scope requests are submitted for server-side authorization.
The usual Backend Services request contains system/ scopes. If explicit non-system scopes are submitted, Safire proceeds without warning because SMART permits user/ and patient/ scopes when context is coordinated out of band. Confirm that coordination and the server’s registration policy when diagnosing an authorization-server rejection.
ConfigurationError: Missing private_key or kid
Safire::Errors::ConfigurationError: Configuration missing: private_key, kid
request_backend_token validates private_key and kid when building the JWT assertion. Ensure both are in config or passed as overrides:
# In config (preferred)
config = Safire::ClientConfig.new(
private_key: OpenSSL::PKey::RSA.new(File.read(ENV['SMART_PRIVATE_KEY_PATH'])),
kid: ENV.fetch('SMART_KEY_ID'),
# ...
)
client = Safire::Client.new(config)
# Or override per call
client.request_backend_token(
private_key: OpenSSL::PKey::RSA.new(File.read(ENV['SMART_PRIVATE_KEY_PATH'])),
kid: ENV.fetch('SMART_KEY_ID')
)
See Backend Services — Prerequisites for key generation steps.
Network Errors
NetworkError: Connection refused or timeout
Safire::Errors::NetworkError: HTTP request failed: Connection refused
Verify server connectivity before debugging Safire configuration:
curl -v https://fhir.example.com/.well-known/smart-configuration
For transient network failures, implement retry with exponential backoff in your application — see Advanced Examples for a reusable pattern.
NetworkError: Blocked redirect to non-HTTPS URL
Safire::Errors::NetworkError: Blocked redirect to non-HTTPS URL: http://fhir.example.com/...
Safire blocks redirects to non-HTTPS URLs (except localhost). Configure base_url with the final HTTPS URL directly, bypassing any HTTP-to-HTTPS redirect the server may use:
# ✅ Use the final HTTPS URL directly
base_url: 'https://fhir.example.com/r4'
# ❌ Will fail if the server redirects HTTP → HTTPS
base_url: 'http://fhir.example.com/r4'