Module: Aladin

Defined in:
lib/aladin.rb,
lib/aladin/user.rb,
lib/aladin/version.rb

Defined Under Namespace

Classes: InvalidAuthResponse, User

Constant Summary collapse

USER_AGENT =
"aladin-ruby #{VERSION}"
ALGORITHM =
'ES256K'
REQUIRED_CLAIMS =
%w(iss iat jti exp username profile public_keys)
DEFAULT_LEEWAY =

seconds

30
DEFAULT_VALID_WITHIN =

seconds

30
DEFAULT_API =
'https://core.aladin.org'
VERSION =
"1.0.0"

Class Method Summary collapse

Class Method Details

.apiObject



22
23
24
# File 'lib/aladin.rb', line 22

def self.api
  @api
end

.api=(api) ⇒ Object



18
19
20
# File 'lib/aladin.rb', line 18

def self.api=(api)
  @api = api || DEFAULT_API
end

.faradayObject



128
129
130
131
132
# File 'lib/aladin.rb', line 128

def self.faraday
  connection = Faraday.new
  connection.headers[:user_agent] = USER_AGENT
  connection
end

.get_address_from_did(decentralized_id) ⇒ Object



97
98
99
100
101
# File 'lib/aladin.rb', line 97

def self.get_address_from_did(decentralized_id)
  did_type = get_did_type(decentralized_id)
  return nil if did_type != 'btc-addr'
  decentralized_id.split(':')[2]
end

.get_did_type(decentralized_id) ⇒ Object



90
91
92
93
94
95
# File 'lib/aladin.rb', line 90

def self.get_did_type(decentralized_id)
  did_parts = decentralized_id.split(':')
  fail 'Decentralized IDs must have 3 parts' if did_parts.length != 3
  fail 'Decentralized IDs must start with "did"' if did_parts[0].downcase != 'did'
  did_parts[1].downcase
end

.leewayObject



30
31
32
# File 'lib/aladin.rb', line 30

def self.leeway
  @leeway
end

.leeway=(leeway) ⇒ Object



26
27
28
# File 'lib/aladin.rb', line 26

def self.leeway=(leeway)
  @leeway = leeway || DEFAULT_LEEWAY
end

.public_keys_match_issuer?(decoded_token) ⇒ Boolean

Returns:

  • (Boolean)


103
104
105
106
107
108
109
110
111
# File 'lib/aladin.rb', line 103

def self.public_keys_match_issuer?(decoded_token)
  public_keys = decoded_token['public_keys']
  address_from_issuer = get_address_from_did(decoded_token['iss'])

  fail 'Multiple public keys are not supported' unless public_keys.count == 1

  address_from_public_keys = Bitcoin.pubkey_to_address(public_keys.first)
  address_from_issuer == address_from_public_keys
end

.public_keys_match_username?(decoded_token) ⇒ Boolean

Returns:

  • (Boolean)


113
114
115
116
117
118
119
120
121
122
123
124
125
126
# File 'lib/aladin.rb', line 113

def self.public_keys_match_username?(decoded_token)
  username = decoded_token['username']
  return true if username.nil?

  response = Faraday.get "#{api}/v1/names/#{username}"
  json = JSON.parse response.body

  fail "Issuer claimed username that doesn't exist" if response.status == 404
  fail "Unable to verify issuer's claimed username" if response.status != 200

  name_owning_address = json['address']
  address_from_issuer = get_address_from_did decoded_token['iss']
  name_owning_address == address_from_issuer
end

.valid_withinObject



38
39
40
# File 'lib/aladin.rb', line 38

def self.valid_within
  @valid_within
end

.valid_within=(valid_within) ⇒ Object



34
35
36
# File 'lib/aladin.rb', line 34

def self.valid_within=(valid_within)
  @valid_within = valid_within || DEFAULT_VALID_WITHIN
end

.verify_auth_response(auth_token) ⇒ Object



65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
# File 'lib/aladin.rb', line 65

def self.verify_auth_response(auth_token)
  decoded_token = verify_without_signature(auth_token)

  REQUIRED_CLAIMS.each do |field|
    fail InvalidAuthResponse.new("Missing required '#{field}' claim.") unless decoded_token.key?(field.to_s)
  end
  fail InvalidAuthResponse.new("Missing required 'iat' claim.") unless decoded_token['iat']
  fail InvalidAuthResponse.new("'iat' timestamp claim is skewed too far from present.") if (Time.now.to_i - decoded_token['iat']).abs > valid_within

  public_keys = decoded_token['public_keys']
  fail InvalidAuthResponse.new('Invalid public_keys array: only 1 key is supported') unless public_keys.length == 1

  decoded_token = verify_with_signature(auth_token, public_keys)
  fail InvalidAuthResponse.new("Public keys don't match issuer address") unless self.public_keys_match_issuer?(decoded_token)
  fail InvalidAuthResponse.new("Public keys don't match owner of claimed username") unless self.public_keys_match_username?(decoded_token)

  return decoded_token
rescue JWTB::VerificationError
  raise InvalidAuthResponse.new('Signature on JWT is invalid')
rescue JWTB::DecodeError
  raise InvalidAuthResponse.new('Unable to decode JWT')
rescue RuntimeError => error
  raise InvalidAuthResponse.new(error.message)
end

.verify_with_signature(auth_token, public_keys) ⇒ Object

decode & verify signature



52
53
54
55
56
57
58
59
60
61
62
63
# File 'lib/aladin.rb', line 52

def self.verify_with_signature(auth_token, public_keys)
  compressed_hex_public_key = public_keys[0]
  bignum = OpenSSL::BN.new(compressed_hex_public_key, 16)
  group = OpenSSL::PKey::EC::Group.new 'secp256k1'
  public_key = OpenSSL::PKey::EC::Point.new(group, bignum)
  ecdsa_key = OpenSSL::PKey::EC.new 'secp256k1'
  ecdsa_key.public_key = public_key
  verify = true

  decoded_tokens = JWTB.decode auth_token, ecdsa_key, verify, algorithm: ALGORITHM, exp_leeway: leeway
  decoded_tokens[0]
end

.verify_without_signature(auth_token) ⇒ Object

decode & verify token without checking signature so we can extract public keys



44
45
46
47
48
49
# File 'lib/aladin.rb', line 44

def self.verify_without_signature(auth_token)
  public_key = nil
  verify = false
  decoded_tokens = JWTB.decode auth_token, public_key, verify, algorithm: ALGORITHM
  decoded_tokens[0]
end