Class: Keycardai::OAuth::PrivateKeyManager

Inherits:
Object
  • Object
show all
Defined in:
lib/keycardai/oauth/private_key.rb

Overview

Generates, persists, and loads an RSA-2048 keypair, and signs RFC 7523 private_key_jwt client assertions with it. WebIdentity composes this; it is also usable standalone.

Constant Summary collapse

ASSERTION_TYPE =
"urn:ietf:params:oauth:client-assertion-type:jwt-bearer"
DEFAULT_ASSERTION_LIFETIME =
300

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(key_id:, storage: FilePrivateKeyStorage.new, clock: -> { Time.now }) ⇒ PrivateKeyManager

Returns a new instance of PrivateKeyManager.

Parameters:

  • key_id (String)
  • storage (#load, #store) (defaults to: FilePrivateKeyStorage.new)

    keypair persistence

  • clock (#call) (defaults to: -> { Time.now })

    returns the current Time; override in tests



57
58
59
60
61
62
63
# File 'lib/keycardai/oauth/private_key.rb', line 57

def initialize(key_id:, storage: FilePrivateKeyStorage.new, clock: -> { Time.now })
  @key_id = key_id
  @storage = storage
  @clock = clock
  @key = nil
  @mutex = Mutex.new
end

Instance Attribute Details

#key_idString (readonly)

Returns the key id (the JWT kid).

Returns:

  • (String)

    the key id (the JWT kid)



52
53
54
# File 'lib/keycardai/oauth/private_key.rb', line 52

def key_id
  @key_id
end

Instance Method Details

#create_client_assertion(client_id:, audience:, expiry_seconds: DEFAULT_ASSERTION_LIFETIME) ⇒ String

Sign a short-lived client assertion (RFC 7523 §3).

Parameters:

  • client_id (String)

    becomes iss and sub

  • audience (String)

    the authorization server's token endpoint

  • expiry_seconds (Integer) (defaults to: DEFAULT_ASSERTION_LIFETIME)

Returns:

  • (String)

    the signed assertion



83
84
85
86
87
88
89
90
# File 'lib/keycardai/oauth/private_key.rb', line 83

def create_client_assertion(client_id:, audience:, expiry_seconds: DEFAULT_ASSERTION_LIFETIME)
  now = @clock.call.to_i
  claims = {
    "iss" => client_id, "sub" => client_id, "aud" => audience,
    "jti" => SecureRandom.uuid, "iat" => now, "exp" => now + expiry_seconds
  }
  JWTSigner.new(key: key, kid: @key_id).sign(claims)
end

#keyOpenSSL::PKey::RSA

Load the persisted keypair, generating and storing one on first use.

Returns:

  • (OpenSSL::PKey::RSA)


68
69
70
71
72
73
74
75
# File 'lib/keycardai/oauth/private_key.rb', line 68

def key
  @mutex.synchronize do
    @key ||= begin
      pem = @storage.load(@key_id)
      pem ? OpenSSL::PKey::RSA.new(pem) : generate
    end
  end
end

#public_jwksHash

The public half as a JWKS document, for the authorization server to verify assertions against.

Returns:

  • (Hash)

    => [...]



96
97
98
99
# File 'lib/keycardai/oauth/private_key.rb', line 96

def public_jwks
  jwk = JWT::JWK.new(key.public_key, { kid: @key_id, use: "sig", alg: "RS256" })
  { "keys" => [jwk.export.transform_keys(&:to_s)] }
end