Skip to content

Commit bdb8a97

Browse files
author
Nathan Hurst
authored
Adding tests, nonce, and validation (#3)
* Adding tests and nonce * Removing boilerplate * Updating Gemfile * Testing root directory rspec run * Including spec_helper by default * Including field as hidden and read only * Renaming invalid_nonce => invalid_time * Embedding js in the form so that simple apps can use it * Adding js file missed before and updating README with example app
1 parent 44f3452 commit bdb8a97

File tree

8 files changed

+261
-17
lines changed

8 files changed

+261
-17
lines changed

.github/workflows/test.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ jobs:
1515
strategy:
1616
fail-fast: false
1717
matrix:
18-
os: [ubuntu-latest, macos-latest]
18+
os: [ubuntu-latest]
1919
ruby: ['2.7', '3.0']
2020
steps:
2121
- uses: actions/checkout@v2
@@ -27,4 +27,4 @@ jobs:
2727
bundle install
2828
- name: Run Tests
2929
run: |
30-
bundle exec rspec spec
30+
bundle exec rspec --require spec_helper

Gemfile.lock

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,19 @@
11
GEM
22
remote: https://rubygems.org/
33
specs:
4-
eth-patched (0.4.14)
5-
ffi (~> 1.0)
4+
diff-lcs (1.4.4)
5+
eth (0.4.16)
6+
ffi (~> 1.15)
67
keccak (~> 1.2)
7-
money-tree (~> 0.10.0)
8-
rlp (~> 0.7.3)
9-
scrypt (~> 3.0.6)
8+
money-tree (~> 0.10)
9+
rlp (~> 0.7)
10+
scrypt (~> 3.0)
1011
ffi (1.15.4)
1112
ffi-compiler (1.0.1)
1213
ffi (>= 1.0.0)
1314
rake
14-
hashie (4.1.0)
15-
keccak (1.2.0)
15+
hashie (5.0.0)
16+
keccak (1.2.2)
1617
money-tree (0.10.0)
1718
ffi
1819
omniauth (2.0.4)
@@ -22,17 +23,34 @@ GEM
2223
rack (2.2.3)
2324
rack-protection (2.1.0)
2425
rack
26+
rack-test (1.1.0)
27+
rack (>= 1.0, < 3)
2528
rake (13.0.6)
2629
rlp (0.7.3)
30+
rspec (3.10.0)
31+
rspec-core (~> 3.10.0)
32+
rspec-expectations (~> 3.10.0)
33+
rspec-mocks (~> 3.10.0)
34+
rspec-core (3.10.1)
35+
rspec-support (~> 3.10.0)
36+
rspec-expectations (3.10.1)
37+
diff-lcs (>= 1.2.0, < 2.0)
38+
rspec-support (~> 3.10.0)
39+
rspec-mocks (3.10.2)
40+
diff-lcs (>= 1.2.0, < 2.0)
41+
rspec-support (~> 3.10.0)
42+
rspec-support (3.10.3)
2743
scrypt (3.0.7)
2844
ffi-compiler (>= 1.0, < 2.0)
2945

3046
PLATFORMS
31-
x86_64-linux
47+
x86_64-darwin-19
3248

3349
DEPENDENCIES
34-
eth-patched (>= 0.4.13)
50+
eth (>= 0.4.16)
3551
omniauth (>= 2.0)
52+
rack-test
53+
rspec (>= 3.10.0)
3654

3755
BUNDLED WITH
3856
2.2.27

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,10 @@
11
# omniauth-ethereum
22
Implements the Ethereum provider strategy for OmniAuth
3+
4+
To test
5+
```
6+
bundle
7+
rspec
8+
```
9+
10+
An [example Rails app using omniauth-ethereum](https://github.com/nahurst/omniauth-ethereum-rails)

lib/new_session.js

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
// the button to connect to an ethereum wallet
2+
const buttonEthConnect = document.querySelector('button');
3+
4+
// the read-only eth fields, we process them automatically
5+
const formInputEthMessage = document.querySelector('input#eth_message');
6+
const formInputEthAddress = document.querySelector('input#eth_address');
7+
const formInputEthSignature = document.querySelector('input#eth_signature');
8+
formInputEthMessage.hidden = true;
9+
formInputEthAddress.hidden = true;
10+
formInputEthSignature.hidden = true;
11+
12+
// get the new session form for submission later
13+
const formNewSession = document.querySelector('form');
14+
15+
// only proceed with ethereum context available
16+
if (typeof window.ethereum !== 'undefined') {
17+
buttonEthConnect.addEventListener('click', async () => {
18+
buttonEthConnect.disabled = true;
19+
20+
// request accounts from ethereum provider
21+
const accounts = await requestAccounts();
22+
const etherbase = accounts[0];
23+
24+
// sign a message with current time
25+
const customTitle = "Hello from Ruby!";
26+
const requestTime = Math.floor(new Date().getTime() / 1000);
27+
const message = customTitle + " " + requestTime;
28+
const signature = await personalSign(etherbase, message);
29+
30+
// populate and submit form
31+
formInputEthMessage.value = message;
32+
formInputEthAddress.value = etherbase;
33+
formInputEthSignature.value = signature;
34+
formNewSession.submit();
35+
});
36+
} else {
37+
// disable form submission in case there is no ethereum wallet available
38+
buttonEthConnect.innerHTML = "No Ethereum Context Available";
39+
buttonEthConnect.disabled = true;
40+
}
41+
42+
// request ethereum wallet access and approved accounts[]
43+
async function requestAccounts() {
44+
const accounts = await ethereum.request({ method: 'eth_requestAccounts' });
45+
return accounts;
46+
}
47+
48+
// request ethereum signature for message from account
49+
async function personalSign(account, message) {
50+
const signature = await ethereum.request({ method: 'personal_sign', params: [ message, account ] });
51+
return signature;
52+
}
53+
54+
// get nonce from /api/v1/users/ by account
55+
async function getUuidByAccount(account) {
56+
const response = await fetch("/api/v1/users/" + account);
57+
const nonceJson = await response.json();
58+
if (!nonceJson) return null;
59+
const uuid = nonceJson[0].eth_nonce;
60+
return uuid;
61+
}

lib/omniauth-ethereum.rb

Lines changed: 41 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
require 'omniauth'
2+
require 'eth'
23

34
module OmniAuth
45
module Strategies
@@ -13,22 +14,56 @@ class Ethereum
1314

1415
# the `eth_address` will be the _fake_ unique identifier for the Ethereum strategy
1516
option :uid_field, :eth_address
17+
option :fields, [:eth_message, :eth_address, :eth_signature]
18+
option :uid_field, :eth_address
1619

17-
# the omniauth request phase
1820
def request_phase
19-
20-
# helper omniauth form to gather required data
21-
form = OmniAuth::Form.new :title => "Ethereum Authentication", :url => callback_path
21+
form = OmniAuth::Form.new :title => 'Ethereum Authentication', :url => callback_path
2222
options.fields.each do |field|
2323

2424
# these fields are read-only and will be filled by javascript in the process
25-
form.text_field field.to_s.capitalize.gsub("_", " "), field.to_s, readonly: true, class: field.to_s
25+
if field == :eth_message
26+
form.html("<input type='hidden' id='eth_message' name='eth_message' value='#{now}' />")
27+
else
28+
form.html("<input type='hidden' id='#{field.to_s}' name='#{field.to_s}' />")
29+
end
2630
end
2731

2832
# the form button will be heavy on javascript, requesting account, nonce, and signature before submission
29-
form.button "Sign-In with Ethereum", class: "eth_connect"
33+
form.button 'Sign In'
34+
path = File.join( File.dirname(__FILE__), 'new_session.js')
35+
js = File.read(path)
36+
mod = "<script type='module'>\n#{js}\n</script>"
37+
38+
form.html(mod)
3039
form.to_response
3140
end
41+
42+
def callback_phase
43+
address = request.params['eth_address'].downcase
44+
message = request.params['eth_message']
45+
signature = request.params['eth_signature']
46+
signature_pubkey = Eth::Key.personal_recover message, signature
47+
signature_address = (Eth::Utils.public_key_to_address signature_pubkey).downcase
48+
49+
unix_time = message.scan(/\d+/).first.to_i
50+
ten_min = 10 * 60
51+
return fail!(:invalid_time) unless unix_time + ten_min >= now && unix_time - ten_min <= now
52+
53+
return fail!(:invalid_credentials) unless signature_address == address
54+
55+
super
56+
end
57+
58+
uid do
59+
request.params[options.uid_field.to_s]
60+
end
61+
62+
private
63+
64+
def now
65+
Time.now.utc.to_i
66+
end
3267
end
3368
end
3469
end

omniauth-ethereum.gemspec

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
lib = File.expand_path('lib', __dir__)
2+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3+
require 'omniauth-ethereum'
4+
15
Gem::Specification.new do |spec|
26
spec.name = 'omniauth-ethereum'
37
spec.version = '0.0.1'
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
RSpec.describe OmniAuth::Strategies::Ethereum do
2+
let(:app) do
3+
Rack::Builder.new do |b|
4+
b.use Rack::Session::Cookie, :secret => 'abc123'
5+
b.use OmniAuth::Strategies::Ethereum
6+
b.run lambda { |_env| [200, {}, ['Not Found']] }
7+
end.to_app
8+
end
9+
10+
before(:each) do
11+
allow(Time).to receive(:now).and_return(
12+
Time.at(1636680000, in: '+00:00'))
13+
end
14+
15+
context 'request phase' do
16+
before(:each) { post '/auth/ethereum' }
17+
18+
it 'displays a form' do
19+
expect(last_response.status).to eq(200)
20+
expect(last_response.body).to be_include('<form')
21+
end
22+
23+
it 'has the callback as the action for the form' do
24+
expect(last_response.body).to be_include("action='/auth/ethereum/callback'")
25+
end
26+
27+
it 'has a text field for each of the fields' do
28+
expect(last_response.body.scan('<input').size).to eq(3)
29+
end
30+
end
31+
32+
context 'callback phase' do
33+
let(:auth_hash) { last_request.env['omniauth.auth'] }
34+
let(:eth_message) { "Hello from Ruby! #{Time.now.utc.to_i}" }
35+
let(:eth_address) { '0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266' }
36+
let(:eth_signature) { '0x362b5463470038a62c9d0652712539bab36f9c74914f5a05475a8e2e84a9719e396ab201e62d0c92da7de91a3a4a61a221c3f4e0b528b709ecdaa22ee8391d0f1c' }
37+
38+
describe 'with varied nonces' do
39+
before do
40+
post '/auth/ethereum/callback',
41+
:eth_message => eth_message,
42+
:eth_address => eth_address,
43+
:eth_signature => eth_signature
44+
end
45+
46+
context 'with nonce from too long ago' do
47+
let(:eth_message) { "Hello from Ruby! #{Time.now.utc.to_i - 11 * 60}" }
48+
49+
it 'fails with invalid nonce' do
50+
expect(last_response.status).to eq(302)
51+
expect(last_response.location).to eq('/auth/failure?message=invalid_time&strategy=ethereum')
52+
end
53+
end
54+
55+
context 'with nonce from too far in future' do
56+
let(:eth_message) { "Hello from Ruby! #{Time.now.utc.to_i + 11 * 60}" }
57+
58+
it 'fails with invalid nonce' do
59+
expect(last_response.status).to eq(302)
60+
expect(last_response.location).to eq('/auth/failure?message=invalid_time&strategy=ethereum')
61+
end
62+
end
63+
end
64+
65+
context 'with mismatching address' do
66+
before do
67+
post '/auth/ethereum/callback',
68+
:name => 'Example User',
69+
:email => '[email protected]',
70+
:eth_message => eth_message,
71+
:eth_address => '0xBADDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD',
72+
:eth_signature => '0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000'
73+
end
74+
75+
it 'fails with invalid credentials' do
76+
expect(last_response.status).to eq(302)
77+
expect(last_response.location).to eq('/auth/failure?message=invalid_credentials&strategy=ethereum')
78+
end
79+
end
80+
81+
context 'with default options' do
82+
before do
83+
post '/auth/ethereum/callback',
84+
:eth_message => eth_message,
85+
:eth_address => eth_address,
86+
:eth_signature => eth_signature
87+
end
88+
89+
it 'sets the uid to the address' do
90+
expect(last_response.status).to eq(200)
91+
expect(auth_hash.uid).to eq(eth_address)
92+
end
93+
end
94+
95+
end
96+
end

spec/spec_helper.rb

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
require 'rspec'
2+
require 'rack/test'
3+
require 'omniauth'
4+
require 'omniauth/test'
5+
6+
require 'omniauth-ethereum'
7+
8+
OmniAuth.config.request_validation_phase = nil
9+
10+
RSpec.configure do |config|
11+
12+
config.include Rack::Test::Methods
13+
config.expect_with :rspec do |expectations|
14+
expectations.include_chain_clauses_in_custom_matcher_descriptions = true
15+
end
16+
17+
config.mock_with :rspec do |mocks|
18+
mocks.verify_partial_doubles = true
19+
end
20+
21+
config.shared_context_metadata_behavior = :apply_to_host_groups
22+
end

0 commit comments

Comments
 (0)