smart-id-ruby-client
Ruby client gem for integrating Smart-ID RP API v3.1 into Ruby applications.
This gem follows the same high-level flow model as the Smart-ID Java client:
- start session (authentication / certificate choice / signature)
- poll session status
- validate final session response
Table of contents
- Requirements
- Installation
- Quick setup
- Logging
- Interactions
- Flows
- Notification authentication
- Device-link authentication
- Notification signature
- Device-link signature
- Certificate by document number
- Device-link certificate choice + linked notification signature
- Notification certificate choice
- Generating device link and QR-code
- Session status polling
- Response validation
- Network and SSL configuration
- Exception handling
- Development
- License
Requirements
- Ruby
>= 3.0
Installation
Add to your app:
bundle add smart-id-ruby-clientOr install directly:
gem install smart-id-ruby-clientQuick setup
require "smart-id-ruby-client" # or: require "smart_id_ruby"
require "base64"
require "securerandom"
SmartIdRuby.configure do |config|
config.relying_party_uuid = ENV.fetch("SMART_ID_RP_UUID")
config.relying_party_name = ENV.fetch("SMART_ID_RP_NAME")
config.host_url = ENV.fetch("SMART_ID_HOST_URL") # e.g. https://sid.demo.sk.ee/smart-id-rp/v3/
config.default_certificate_level = "QUALIFIED" # optional app-level default
config.poller_timeout_seconds = 10 # optional
end
client = SmartIdRuby.clientLogging
The library uses SmartIdRuby.logger for connector-level logs.
Default level is WARN.
require "logger"
require "smart-id-ruby-client" # or: require "smart_id_ruby"
SmartIdRuby.logger = Logger.new($stdout)
SmartIdRuby.logger.level = Logger::DEBUGAt DEBUG, logs include method, URL, and status code.
Request and response bodies are not logged by default.
Interactions
You can pass interactions either as hashes or helper objects.
Hash style
[
{ type: "displayTextAndPIN", displayText60: "Log in" },
{ type: "confirmationMessage", displayText200: "Confirm login" }
]Helper style (NotificationInteraction)
[
SmartIdRuby::NotificationInteraction.confirmationMessageAndVerificationCodeChoice("Confirm login"),
SmartIdRuby::NotificationInteraction.confirmationMessage("Confirm login fallback"),
SmartIdRuby::NotificationInteraction.displayTextAndPin("Log in fallback")
]Ruby-style aliases are also available:
display_text_and_pinconfirmation_messageconfirmation_message_and_verification_code_choice
Flows
Notification authentication
rp_challenge = Base64.strict_encode64(SecureRandom.random_bytes(32))
auth_builder = client.create_notification_authentication
.with_document_number("PNOEE-30303039914") # or .with_semantics_identifier("PNOEE-...")
.with_certificate_level("QUALIFIED")
.with_rp_challenge(rp_challenge)
.with_interactions([
SmartIdRuby::NotificationInteraction.displayTextAndPin("Log in")
])
.with_share_md_client_ip_address(true) # optional
auth_init = auth_builder.init_authentication_session
session_id = auth_init["sessionID"] || auth_init[:sessionID]Device-link authentication
rp_challenge = Base64.strict_encode64(SecureRandom.random_bytes(32))
builder = client.create_device_link_authentication
.with_rp_challenge(rp_challenge)
.with_interactions([
SmartIdRuby::NotificationInteraction.confirmationMessage("Log in to MyApp")
])
.with_initial_callback_url("https://example.com/callback") # optional
# Anonymous device-link auth (no identifier)
init = builder.init_authentication_session
# Identified flow variants:
# builder.with_document_number("PNOLT-40504040001-MOCK-Q")
# builder.with_semantics_identifier("PNOEE-30303039914")Notification signature
sig_builder = client.create_notification_signature
.with_semantics_identifier("PNOEE-30303039914") # or .with_document_number(...)
.with_certificate_level("QUALIFIED")
.with_signable_data("data to sign")
.with_interactions([
SmartIdRuby::NotificationInteraction.displayTextAndPin("Please sign")
])
.with_nonce(SecureRandom.hex(8)) # optional
sig_init = sig_builder.init_signature_session
session_id = sig_init["sessionID"] || sig_init[:sessionID]You can use with_signable_hash(...) instead of with_signable_data(...).
Device-link signature
sig_builder = client.create_device_link_signature
.with_document_number("PNOLT-40504040001-MOCK-Q") # or .with_semantics_identifier(...)
.with_certificate_level("QUALIFIED")
.with_signable_data("data to sign")
.with_interactions([
SmartIdRuby::NotificationInteraction.confirmationMessage("Please sign document")
])
.with_initial_callback_url("https://example.com/callback") # optional
sig_init = sig_builder.init_signature_sessionCertificate by document number
result = client.create_certificate_by_document_number
.with_document_number("PNOLT-40504040001-MOCK-Q")
.with_certificate_level("QUALIFIED")
.get_certificate_by_document_number
certificate_level = result[:certificate_level]
certificate = result[:certificate] # OpenSSL::X509::CertificateDevice-link certificate choice + linked notification signature
cert_choice_init = client.create_device_link_certificate_request
.with_certificate_level("QUALIFIED")
.with_nonce(SecureRandom.hex(8))
.init_certificate_choice
cert_choice_session_id = cert_choice_init["sessionID"] || cert_choice_init[:sessionID]
cert_choice_status = client.session_status_poller.fetch_final_session_status(cert_choice_session_id)
cert_choice_response = SmartIdRuby::Validation::CertificateChoiceResponseValidator.new
.validate(cert_choice_status, "QUALIFIED")
linked_sig_init = client.create_linked_notification_signature
.with_document_number(cert_choice_response.document_number)
.with_linked_session_id(cert_choice_session_id)
.with_signable_data("data to sign")
.with_interactions([
SmartIdRuby::NotificationInteraction.displayTextAndPin("Please sign")
])
.init_signature_sessionNotification certificate choice
init = client.create_notification_certificate_choice
.with_semantics_identifier("PNOEE-30303039914")
.with_certificate_level("QUALIFIED")
.with_nonce(SecureRandom.hex(8))
.init_certificate_choice
session_id = init["sessionID"] || init[:sessionID]Generating device link and QR-code
After starting a device-link flow (authentication, signature, or certificate choice), you can create a signed device-link URI and QR image.
# Example: after device-link authentication init
auth_builder = client.create_device_link_authentication
.with_rp_challenge(Base64.strict_encode64(SecureRandom.random_bytes(32)))
.with_interactions([SmartIdRuby::NotificationInteraction.displayTextAndPin("Log in")])
.with_document_number("PNOLT-40504040001-MOCK-Q")
init = auth_builder.init_authentication_session
session_token = init["sessionToken"] || init[:sessionToken]
session_secret = init["sessionSecret"] || init[:sessionSecret]
device_link_base = init["deviceLinkBase"] || init[:deviceLinkBase]
request = auth_builder.get_authentication_session_request
request_interactions = request[:interactions]
request_digest = request.dig(:signatureProtocolParameters, :rpChallenge)Build unprotected and protected device-link:
dynamic = client.create_dynamic_content
.with_device_link_base(device_link_base)
.with_session_type(SmartIdRuby::SessionType::AUTHENTICATION)
.with_device_link_type(SmartIdRuby::DeviceLinkType::QR_CODE)
.with_session_token(session_token)
.with_elapsed_seconds(1)
.with_lang("eng")
.with_digest(request_digest)
.with_interactions(request_interactions)
unprotected_uri = dynamic.create_unprotected_uri
device_link_uri = dynamic.build_device_link(session_secret)Generate QR-code (PNG Data URI by default):
qr_data_uri = SmartIdRuby::QrCodeGenerator.generate_data_uri(device_link_uri.to_s)
# or create image object and convert manually
image = SmartIdRuby::QrCodeGenerator.generate_image(device_link_uri.to_s, 610, 610, 4)
qr_data_uri = SmartIdRuby::QrCodeGenerator.convert_to_data_uri(image, "png")Session status polling
Use SessionStatusPoller for both one-shot and final-status polling.
poller = client.session_status_poller
# Query once
status = poller.get_session_status(session_id)
# Poll until COMPLETE
final_status = poller.fetch_final_session_status(session_id)Tune polling behavior:
client.set_session_status_response_socket_open_time(:seconds, 5) # timeoutMs for each status request
client.set_polling_sleep_timeout(:seconds, 1) # sleep between pollsSupported time units are :milliseconds, :seconds, :minutes, :hours.
Response validation
Builders validate request/initialization responses.
For final session status payloads, use validators:
Notification authentication response validator
identity = SmartIdRuby::Validation::NotificationAuthenticationResponseValidator.new
.validate(
final_status,
auth_builder.get_authentication_session_request,
"SMART_ID", # schema_name
nil # brokered_rp_name (optional)
)Device-link authentication response validator
response = SmartIdRuby::Validation::DeviceLinkAuthenticationResponseValidator.new.validate(
final_status,
auth_builder.get_authentication_session_request,
nil, # user_challenge_verifier (required for Web2App/App2App)
"SMART_ID", # schema_name
nil # brokered_rp_name
)Signature response validator
signature = SmartIdRuby::Validation::SignatureResponseValidator.new
.validate(final_status, "QUALIFIED")Certificate choice response validator
choice = SmartIdRuby::Validation::CertificateChoiceResponseValidator.new
.validate(final_status, "QUALIFIED")Network and SSL configuration
Faraday request options
client.network_connection_config = {
open_timeout: 5,
timeout: 30,
headers: { "X-Request-ID" => "123" }
}You can also nest options under :request:
client.network_connection_config = {
request: {
open_timeout: 5,
timeout: 30
}
}Custom Faraday connection
client.configured_connection = Faraday.new(url: client.host_url) do |f|
f.adapter Faraday.default_adapter
endTrusting custom CA certificates
cert_store = OpenSSL::X509::Store.new
cert_store.set_default_paths
# cert_store.add_cert(OpenSSL::X509::Certificate.new(File.read("ca.pem")))
ssl_context = OpenSSL::SSL::SSLContext.new
ssl_context.cert_store = cert_store
client.trust_ssl_context = ssl_contextException handling
Main exception classes:
SmartIdRuby::Errors::RequestSetupErrorSmartIdRuby::Errors::RequestValidationErrorSmartIdRuby::Errors::UnprocessableResponseErrorSmartIdRuby::Errors::SessionEndResultErrorSmartIdRuby::Errors::SessionNotCompleteErrorSmartIdRuby::Errors::CertificateLevelMismatchErrorSmartIdRuby::Errors::DocumentUnusableErrorSmartIdRuby::Errors::UserRefusedDisplayTextAndPinErrorSmartIdRuby::Errors::UserRefusedConfirmationMessageErrorSmartIdRuby::Errors::UserRefusedConfirmationMessageWithVerificationChoiceError
Connector/network-related classes:
SmartIdRuby::Errors::SessionNotFoundErrorSmartIdRuby::Errors::UserAccountNotFoundErrorSmartIdRuby::Errors::RelyingPartyAccountConfigurationErrorSmartIdRuby::Errors::NoSuitableAccountOfRequestedTypeFoundErrorSmartIdRuby::Errors::PersonShouldViewSmartIdPortalErrorSmartIdRuby::Errors::UnsupportedClientApiVersionErrorSmartIdRuby::Errors::ServerMaintenanceError
Development
After checking out the repo:
bin/setup
bundle exec rakeLicense
The gem is available as open source under the terms of the MIT License.