The project is in a healthy, maintained state
Smart-ID wrapper library for using Smart-ID in Ruby applications
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
 Dependencies

Runtime

>= 0
~> 2.0
~> 3.0
 Project Readme

Tests codecov License: MIT

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-client

Or install directly:

gem install smart-id-ruby-client

Quick 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.client

Logging

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::DEBUG

At 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_pin
  • confirmation_message
  • confirmation_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_session

Certificate 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::Certificate

Device-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_session

Notification 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 polls

Supported 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
end

Trusting 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_context

Exception handling

Main exception classes:

  • SmartIdRuby::Errors::RequestSetupError
  • SmartIdRuby::Errors::RequestValidationError
  • SmartIdRuby::Errors::UnprocessableResponseError
  • SmartIdRuby::Errors::SessionEndResultError
  • SmartIdRuby::Errors::SessionNotCompleteError
  • SmartIdRuby::Errors::CertificateLevelMismatchError
  • SmartIdRuby::Errors::DocumentUnusableError
  • SmartIdRuby::Errors::UserRefusedDisplayTextAndPinError
  • SmartIdRuby::Errors::UserRefusedConfirmationMessageError
  • SmartIdRuby::Errors::UserRefusedConfirmationMessageWithVerificationChoiceError

Connector/network-related classes:

  • SmartIdRuby::Errors::SessionNotFoundError
  • SmartIdRuby::Errors::UserAccountNotFoundError
  • SmartIdRuby::Errors::RelyingPartyAccountConfigurationError
  • SmartIdRuby::Errors::NoSuitableAccountOfRequestedTypeFoundError
  • SmartIdRuby::Errors::PersonShouldViewSmartIdPortalError
  • SmartIdRuby::Errors::UnsupportedClientApiVersionError
  • SmartIdRuby::Errors::ServerMaintenanceError

Development

After checking out the repo:

bin/setup
bundle exec rake

License

The gem is available as open source under the terms of the MIT License.