Module: WorkOS::Actions

Defined in:
lib/workos/actions.rb

Overview

AuthKit Actions request verification + response signing.

action = client.actions.construct_action(
  payload: req.body, sig_header: req.headers["WorkOS-Signature"],
  secret: ENV["WORKOS_ACTIONS_SECRET"]
)
resp = client.actions.sign_response(
  action_type: "authentication", verdict: "Allow",
  secret: ENV["WORKOS_ACTIONS_SECRET"]
)

Constant Summary collapse

DEFAULT_TOLERANCE_SECONDS =
30
ACTION_TYPE_TO_RESPONSE_OBJECT =
{
  "authentication" => "authentication_action_response",
  "user_registration" => "user_registration_action_response"
}.freeze

Class Method Summary collapse

Class Method Details

.compute_signature(payload:, timestamp:, secret:) ⇒ Object

Compute HMAC-SHA256 hex signature for a (timestamp, payload) pair.



78
79
80
# File 'lib/workos/actions.rb', line 78

def compute_signature(payload:, timestamp:, secret:)
  WorkOS::Util::Signature.compute(payload: payload, timestamp: timestamp, secret: secret)
end

.construct_action(payload:, sig_header:, secret:, tolerance: DEFAULT_TOLERANCE_SECONDS) ⇒ Object

Verify and deserialize an Actions request payload.



55
56
57
58
# File 'lib/workos/actions.rb', line 55

def construct_action(payload:, sig_header:, secret:, tolerance: DEFAULT_TOLERANCE_SECONDS)
  verify_header(payload: payload, sig_header: sig_header, secret: secret, tolerance: tolerance)
  JSON.parse(payload)
end

.parse_signature_header(sig_header) ⇒ Object

Parse a “t=<ms>, v1=<sig>” header into [timestamp, signature].



83
84
85
86
87
# File 'lib/workos/actions.rb', line 83

def parse_signature_header(sig_header)
  WorkOS::Util::Signature.parse_header(sig_header)
rescue ArgumentError => e
  raise WorkOS::SignatureVerificationError.new(message: e.message, http_status: nil)
end

.secure_compare(a, b) ⇒ Object



89
90
91
# File 'lib/workos/actions.rb', line 89

def secure_compare(a, b)
  WorkOS::Util::Signature.secure_compare(a, b)
end

.sign_response(action_type:, verdict:, secret:, error_message: nil) ⇒ Object

Build and sign an Actions response. action_type is “authentication” or “user_registration”; verdict is “Allow” or “Deny”.

Raises:

  • (ArgumentError)


62
63
64
65
66
67
68
69
70
71
72
73
74
75
# File 'lib/workos/actions.rb', line 62

def sign_response(action_type:, verdict:, secret:, error_message: nil)
  object_type = ACTION_TYPE_TO_RESPONSE_OBJECT[action_type.to_s]
  raise ArgumentError, "Unknown action_type: #{action_type}" unless object_type
  timestamp_ms = (Time.now.to_f * 1000).to_i
  response_payload = {"timestamp" => timestamp_ms, "verdict" => verdict}
  response_payload["error_message"] = error_message if error_message
  payload_json = JSON.generate(response_payload)
  signed_payload = "#{timestamp_ms}.#{payload_json}"
  {
    "object" => object_type,
    "payload" => response_payload,
    "signature" => OpenSSL::HMAC.hexdigest("SHA256", secret, signed_payload)
  }
end

.verify_header(payload:, sig_header:, secret:, tolerance: DEFAULT_TOLERANCE_SECONDS) ⇒ Object

Verify a request signature; raises on failure.



35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
# File 'lib/workos/actions.rb', line 35

def verify_header(payload:, sig_header:, secret:, tolerance: DEFAULT_TOLERANCE_SECONDS)
  timestamp_ms, signature_hash = parse_signature_header(sig_header)
  issued_at = timestamp_ms.to_i / 1000.0
  if (Time.now.to_f - issued_at) > tolerance
    raise WorkOS::SignatureVerificationError.new(
      message: "Timestamp outside the tolerance zone",
      http_status: nil
    )
  end
  expected = compute_signature(payload: payload, timestamp: timestamp_ms, secret: secret)
  unless secure_compare(signature_hash, expected)
    raise WorkOS::SignatureVerificationError.new(
      message: "Signature hash does not match the expected signature hash for payload",
      http_status: nil
    )
  end
  true
end