Module: Privy::Authorization

Defined in:
lib/privy/authorization/authorization.rb,
lib/privy/authorization/canonicalization.rb

Defined Under Namespace

Modules: Canonicalization Classes: AuthorizationContext, PreparedRequest, WalletApiRequestSignatureInput

Class Method Summary collapse

Class Method Details

.format_request_for_authorization_signature(input) ⇒ Object



47
48
49
50
51
52
53
54
55
56
# File 'lib/privy/authorization/authorization.rb', line 47

def format_request_for_authorization_signature(input)
  payload = {
    version: input.version,
    method: input.method,
    url: input.url,
    body: input.body.nil? || input.body == {} ? "" : input.body,
    headers: input.headers
  }
  Privy::Authorization::Canonicalization.canonicalize(payload).b
end

.generate_authorization_signature(private_key_base64:, payload:) ⇒ Object



58
59
60
61
62
63
# File 'lib/privy/authorization/authorization.rb', line 58

def generate_authorization_signature(private_key_base64:, payload:)
  key = Privy::Cryptography.import_pkcs8_private_key(private_key_base64)
  digest = OpenSSL::Digest.new("SHA256").digest(payload)
  der = key.dsa_sign_asn1(digest)
  [der].pack("m0")
end

.generate_authorization_signatures(privy_client, input:, context:) ⇒ Object

Generates all authorization signatures for a request.

Signatures come from four sources, combined in this order:

  1. Precomputed signatures passed directly in the context

  2. Explicit private keys provided in authorization_private_keys

  3. Private keys obtained by exchanging user JWTs via HPKE

  4. Sign functions (callbacks that receive the canonicalized payload)



72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
# File 'lib/privy/authorization/authorization.rb', line 72

def generate_authorization_signatures(privy_client, input:, context:)
  if context.user_jwts.any? && privy_client.nil?
    raise ArgumentError,
          "privy_client is required when user_jwts are provided in the authorization context"
  end

  # Canonicalize the request into a deterministic byte string for signing
  payload = format_request_for_authorization_signature(input)

  # Exchange each user JWT for an ephemeral P-256 private key via HPKE.
  # Results are cached by JwtExchangeService so repeated calls are cheap.
  jwt_keys = context.user_jwts.map do |jwt|
    privy_client.jwt_exchange.exchange_jwt_for_authorization_key(jwt)
  end
  all_private_keys = context.authorization_private_keys + jwt_keys

  key_sigs = all_private_keys.map do |pk|
    generate_authorization_signature(private_key_base64: pk, payload: payload)
  end
  fn_sigs = context.sign_fns.map { |fn| fn.call(payload) }

  key_sigs + fn_sigs + context.signatures
end

.merge_prepared_headers!(params, headers) ⇒ Object



38
39
40
41
42
43
44
45
# File 'lib/privy/authorization/authorization.rb', line 38

def merge_prepared_headers!(params, headers)
  if headers["privy-authorization-signature"]
    params[:privy_authorization_signature] =
      headers["privy-authorization-signature"]
  end
  params[:privy_idempotency_key] = headers["privy-idempotency-key"] if headers["privy-idempotency-key"]
  params[:privy_request_expiry] = headers["privy-request-expiry"] if headers["privy-request-expiry"]
end

.prepare_request(privy_client, method:, url:, body:, authorization_context: nil, idempotency_key: nil, request_expiry: nil) ⇒ Object



96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
# File 'lib/privy/authorization/authorization.rb', line 96

def prepare_request(
  privy_client,
  method:,
  url:,
  body:,
  authorization_context: nil,
  idempotency_key: nil,
  request_expiry: nil
)
  ctx = authorization_context || AuthorizationContext.build

  headers = {}
  headers["privy-idempotency-key"] = idempotency_key if idempotency_key
  headers["privy-request-expiry"] = request_expiry.to_s if request_expiry

  signature_input = WalletApiRequestSignatureInput.build(
    method: method,
    url: url,
    body: body,
    headers: {"privy-app-id" => privy_client.app_id}.merge(headers)
  )

  signatures = generate_authorization_signatures(privy_client, input: signature_input, context: ctx)
  headers["privy-authorization-signature"] = signatures.join(",") unless signatures.empty?

  PreparedRequest.new(headers: headers)
end

.signed_url(privy_client, path) ⇒ Object



33
34
35
36
# File 'lib/privy/authorization/authorization.rb', line 33

def signed_url(privy_client, path)
  base = privy_client.api.base_url.to_s.chomp("/")
  "#{base}/#{path}"
end