Class: Nylas::ServiceAccountSigner

Inherits:
Object
  • Object
show all
Defined in:
lib/nylas/handler/service_account_signer.rb

Overview

Builds Nylas Service Account request signing headers for organization admin APIs.

Constant Summary collapse

NONCE_ALPHABET =
("a".."z").to_a.concat(("A".."Z").to_a, ("0".."9").to_a).freeze
DEFAULT_NONCE_LENGTH =
20
SIGNED_BODY_METHODS =
%w[post put patch].freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(private_key_pem:, private_key_id:) ⇒ ServiceAccountSigner

Returns a new instance of ServiceAccountSigner.

Parameters:

  • private_key_pem (String)

    RSA private key in PEM format.

  • private_key_id (String)

    Value for the X-Nylas-Kid header.



21
22
23
24
# File 'lib/nylas/handler/service_account_signer.rb', line 21

def initialize(private_key_pem:, private_key_id:)
  @private_key = self.class.load_rsa_private_key(private_key_pem)
  @private_key_id = private_key_id
end

Instance Attribute Details

#private_key_idObject (readonly)

Returns the value of attribute private_key_id.



17
18
19
# File 'lib/nylas/handler/service_account_signer.rb', line 17

def private_key_id
  @private_key_id
end

Class Method Details

.canonical_json(data) ⇒ String

Returns deterministic JSON with keys sorted at every object level and no extra whitespace.

Parameters:

  • data (Hash, Array, String, Numeric, true, false, nil)

    Data to serialize.

Returns:

  • (String)

    Canonical JSON string.



30
31
32
# File 'lib/nylas/handler/service_account_signer.rb', line 30

def self.canonical_json(data)
  JSON.generate(canonicalize(data))
end

.generate_nonce(length = DEFAULT_NONCE_LENGTH) ⇒ String

Generates a cryptographically secure alphanumeric nonce.

Parameters:

  • length (Integer) (defaults to: DEFAULT_NONCE_LENGTH)

    Length of the nonce to generate.

Returns:

  • (String)

    Generated nonce.



52
53
54
# File 'lib/nylas/handler/service_account_signer.rb', line 52

def self.generate_nonce(length = DEFAULT_NONCE_LENGTH)
  Array.new(length) { NONCE_ALPHABET[SecureRandom.random_number(NONCE_ALPHABET.length)] }.join
end

.load_rsa_private_key(private_key_pem) ⇒ OpenSSL::PKey::RSA

Loads an RSA private key from a PEM string.

Parameters:

  • private_key_pem (String)

    RSA private key in PEM format.

Returns:

  • (OpenSSL::PKey::RSA)


38
39
40
41
42
43
44
45
46
# File 'lib/nylas/handler/service_account_signer.rb', line 38

def self.load_rsa_private_key(private_key_pem)
  key = OpenSSL::PKey::RSA.new(private_key_pem)
  raise ArgumentError, "Private key must be RSA private key" unless key.private?
  raise ArgumentError, "Private key must be at least 2048 bits" if key.n.num_bits < 2048

  key
rescue OpenSSL::PKey::PKeyError
  raise ArgumentError, "Private key must be RSA PEM"
end

Instance Method Details

#build_headers(method:, path:, body: nil, timestamp: nil, nonce: nil) ⇒ Array(Hash, String)

Builds signed headers and, for JSON body methods, the exact canonical body to send.

Parameters:

  • method (String, Symbol)

    HTTP method.

  • path (String)

    Relative request path, for example "/v3/admin/domains".

  • body (Hash, nil) (defaults to: nil)

    Request body for POST/PUT/PATCH requests.

  • timestamp (Integer, nil) (defaults to: nil)

    Optional Unix timestamp in seconds, mainly for tests.

  • nonce (String, nil) (defaults to: nil)

    Optional nonce, mainly for tests.

Returns:

  • (Array(Hash, String))

    Signed headers and optional serialized JSON body.



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
# File 'lib/nylas/handler/service_account_signer.rb', line 64

def build_headers(method:, path:, body: nil, timestamp: nil, nonce: nil)
  timestamp ||= Time.now.to_i
  nonce ||= self.class.generate_nonce
  method_value = method.to_s.downcase
  serialized_body = nil

  if SIGNED_BODY_METHODS.include?(method_value) && !body.nil?
    serialized_body = self.class.canonical_json(body)
  end

  envelope = {
    method: method_value,
    nonce: nonce,
    path: path,
    timestamp: timestamp
  }
  envelope[:payload] = serialized_body if serialized_body

  signature = @private_key.sign(OpenSSL::Digest.new("SHA256"), self.class.canonical_json(envelope))

  [
    {
      "X-Nylas-Kid" => private_key_id,
      "X-Nylas-Nonce" => nonce,
      "X-Nylas-Timestamp" => timestamp.to_s,
      "X-Nylas-Signature" => Base64.strict_encode64(signature)
    },
    serialized_body
  ]
end