solrengine-auth 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +9 -1
- data/app/controllers/solrengine/auth/application_controller.rb +1 -1
- data/app/controllers/solrengine/auth/sessions_controller.rb +35 -30
- data/config/routes.rb +1 -1
- data/lib/solrengine/auth/siws_verifier.rb +17 -2
- data/lib/solrengine/auth/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e74477d86c34bcdd7d1e3aec0296d766c1dff71a64aabfabf122e47677a969e9
|
|
4
|
+
data.tar.gz: 5936982e40d446e51a142c1151346037aa2f26a9750d4c0a0547a3d5350f3a69
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1d69599d414495c813be1eac44eb2b51b102a984081201a7a736e752127a52937cd63284c07d493ef514bf6957f8ad03a637d5b2187831c8d19d892368dfc1d9
|
|
7
|
+
data.tar.gz: 8c468e7aeaa865762cf973b807597e1f5ea0959ae98410bc121dc8639dc5ae98bbddb8c4e91de8f90a37d088201d65139596c29a9ec9ba4c09fbb257be89bb6b
|
data/README.md
CHANGED
|
@@ -94,13 +94,21 @@ verifier = Solrengine::Auth::SiwsVerifier.new(
|
|
|
94
94
|
wallet_address: "Abc...xyz",
|
|
95
95
|
message: siws_message,
|
|
96
96
|
signature: signature_bytes,
|
|
97
|
-
domain: "myapp.com"
|
|
97
|
+
domain: "myapp.com",
|
|
98
|
+
expected_nonce: user.nonce # bind to server-issued nonce (recommended)
|
|
98
99
|
)
|
|
99
100
|
|
|
100
101
|
verifier.verify # => true/false
|
|
101
102
|
verifier.verify! # => true or raises VerificationError
|
|
102
103
|
```
|
|
103
104
|
|
|
105
|
+
**Always pass `expected_nonce:` when verifying a live sign-in.** Without
|
|
106
|
+
it, the verifier accepts any syntactically-valid nonce in the signed
|
|
107
|
+
message, which makes captured `(message, signature)` pairs replayable
|
|
108
|
+
as long as the wallet still has any fresh nonce on the server. The
|
|
109
|
+
bundled `Solrengine::Auth::SessionsController` does this for you; apps
|
|
110
|
+
that build their own controller must do it themselves.
|
|
111
|
+
|
|
104
112
|
## License
|
|
105
113
|
|
|
106
114
|
MIT
|
|
@@ -3,58 +3,60 @@
|
|
|
3
3
|
module Solrengine
|
|
4
4
|
module Auth
|
|
5
5
|
class SessionsController < ApplicationController
|
|
6
|
+
# The gem inherits from the host app's ApplicationController, which
|
|
7
|
+
# typically installs a before_action :authenticate!. Skip it on the
|
|
8
|
+
# sign-in flow so /auth/login doesn't redirect-loop.
|
|
9
|
+
skip_before_action :authenticate!, only: [ :new, :nonce, :create ], raise: false
|
|
10
|
+
|
|
11
|
+
# Rate-limits are keyed by wallet_address on #nonce so an attacker
|
|
12
|
+
# can't churn nonces against a target wallet from rotating IPs, and
|
|
13
|
+
# by IP on #create since there the wallet is attacker-chosen.
|
|
14
|
+
rate_limit to: 10, within: 1.minute, only: :nonce,
|
|
15
|
+
by: -> { params[:wallet_address].to_s.presence || request.remote_ip },
|
|
16
|
+
with: -> { render json: { error: "Too many requests" }, status: :too_many_requests }
|
|
17
|
+
|
|
18
|
+
rate_limit to: 10, within: 1.minute, only: :create,
|
|
19
|
+
with: -> { render json: { error: "Too many requests" }, status: :too_many_requests }
|
|
20
|
+
|
|
6
21
|
def new
|
|
7
22
|
# Renders the wallet connect view
|
|
8
23
|
end
|
|
9
24
|
|
|
10
25
|
def nonce
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
unless wallet_address.match?(Concerns::Authenticatable::SOLANA_ADDRESS_FORMAT)
|
|
14
|
-
return render json: { error: "Invalid wallet address" }, status: :unprocessable_entity
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
user_class = _user_class
|
|
18
|
-
user = user_class.find_or_initialize_by(wallet_address: wallet_address)
|
|
19
|
-
user.generate_nonce! if user.persisted?
|
|
20
|
-
user.save! unless user.persisted?
|
|
26
|
+
user = _user_class.find_or_create_by!(wallet_address: params[:wallet_address])
|
|
27
|
+
user.generate_nonce!
|
|
21
28
|
|
|
22
|
-
domain = Solrengine::Auth.configuration.domain
|
|
23
29
|
message = SiwsMessageBuilder.new(
|
|
24
|
-
domain: domain,
|
|
25
|
-
wallet_address: wallet_address,
|
|
30
|
+
domain: Solrengine::Auth.configuration.domain,
|
|
31
|
+
wallet_address: user.wallet_address,
|
|
26
32
|
nonce: user.nonce,
|
|
27
33
|
uri: request.base_url
|
|
28
34
|
).build
|
|
29
35
|
|
|
30
36
|
render json: { message: message, nonce: user.nonce }
|
|
37
|
+
rescue ActiveRecord::RecordInvalid
|
|
38
|
+
render json: { error: "Invalid wallet address", code: "invalid_wallet_address" },
|
|
39
|
+
status: :unprocessable_entity
|
|
31
40
|
end
|
|
32
41
|
|
|
33
42
|
def create
|
|
34
|
-
|
|
35
|
-
message = params[:message]
|
|
36
|
-
signature = params[:signature]
|
|
37
|
-
|
|
38
|
-
user = _user_class.find_by(wallet_address: wallet_address)
|
|
43
|
+
user = _user_class.find_by(wallet_address: params[:wallet_address])
|
|
39
44
|
|
|
40
45
|
unless user&.nonce_valid?
|
|
41
|
-
return render json: { error: "
|
|
46
|
+
return render json: { error: "Could not sign in", code: "nonce_expired" },
|
|
47
|
+
status: :unprocessable_entity
|
|
42
48
|
end
|
|
43
49
|
|
|
44
50
|
verifier = SiwsVerifier.new(
|
|
45
|
-
wallet_address: wallet_address,
|
|
46
|
-
message: message,
|
|
47
|
-
signature: signature
|
|
51
|
+
wallet_address: params[:wallet_address],
|
|
52
|
+
message: params[:message],
|
|
53
|
+
signature: params[:signature],
|
|
54
|
+
expected_nonce: user.nonce
|
|
48
55
|
)
|
|
49
56
|
|
|
50
57
|
unless verifier.verify
|
|
51
|
-
return render json: { error: "
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
# Verify the nonce in the signed message matches the database nonce
|
|
55
|
-
message_nonce = message.match(/Nonce: ([a-f0-9]+)/)&.captures&.first
|
|
56
|
-
unless message_nonce.present? && ActiveSupport::SecurityUtils.secure_compare(message_nonce, user.nonce)
|
|
57
|
-
return render json: { error: "Nonce mismatch" }, status: :unauthorized
|
|
58
|
+
return render json: { error: "Could not sign in", code: "verification_failed" },
|
|
59
|
+
status: :unauthorized
|
|
58
60
|
end
|
|
59
61
|
|
|
60
62
|
user.generate_nonce!
|
|
@@ -66,7 +68,10 @@ module Solrengine
|
|
|
66
68
|
|
|
67
69
|
def destroy
|
|
68
70
|
reset_session
|
|
69
|
-
|
|
71
|
+
respond_to do |format|
|
|
72
|
+
format.html { redirect_to Solrengine::Auth.configuration.after_sign_out_path, notice: "Disconnected" }
|
|
73
|
+
format.json { head :no_content }
|
|
74
|
+
end
|
|
70
75
|
end
|
|
71
76
|
|
|
72
77
|
private
|
data/config/routes.rb
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Solrengine::Auth::Engine.routes.draw do
|
|
4
4
|
get "login", to: "sessions#new", as: :login
|
|
5
|
-
|
|
5
|
+
get "nonce", to: "sessions#nonce", as: :nonce
|
|
6
6
|
post "verify", to: "sessions#create", as: :verify
|
|
7
7
|
delete "logout", to: "sessions#destroy", as: :logout
|
|
8
8
|
end
|
|
@@ -2,25 +2,33 @@
|
|
|
2
2
|
|
|
3
3
|
require "ed25519"
|
|
4
4
|
require "base58"
|
|
5
|
+
require "active_support/security_utils"
|
|
5
6
|
|
|
6
7
|
module Solrengine
|
|
7
8
|
module Auth
|
|
8
9
|
# Verifies SIWS (Sign In With Solana) signed messages.
|
|
9
10
|
# Uses Ed25519 signature verification and validates domain
|
|
10
11
|
# to prevent cross-site replay attacks.
|
|
12
|
+
#
|
|
13
|
+
# Pass `expected_nonce:` to bind the signed message to a server-issued
|
|
14
|
+
# nonce. Without that binding, a captured (message, signature) pair can
|
|
15
|
+
# be replayed as long as the wallet still has any fresh nonce — callers
|
|
16
|
+
# should always supply it when verifying a live sign-in.
|
|
11
17
|
class SiwsVerifier
|
|
12
18
|
class VerificationError < StandardError; end
|
|
13
19
|
|
|
14
|
-
def initialize(wallet_address:, message:, signature:, domain: nil)
|
|
20
|
+
def initialize(wallet_address:, message:, signature:, domain: nil, expected_nonce: nil)
|
|
15
21
|
@wallet_address = wallet_address
|
|
16
22
|
@message = message
|
|
17
23
|
@signature = signature
|
|
18
24
|
@domain = domain || Solrengine::Auth.configuration.domain
|
|
25
|
+
@expected_nonce = expected_nonce
|
|
19
26
|
end
|
|
20
27
|
|
|
21
28
|
def verify!
|
|
22
29
|
verify_message_format!
|
|
23
30
|
verify_domain!
|
|
31
|
+
verify_nonce! if @expected_nonce
|
|
24
32
|
verify_signature!
|
|
25
33
|
true
|
|
26
34
|
rescue Ed25519::VerifyError
|
|
@@ -52,6 +60,13 @@ module Solrengine
|
|
|
52
60
|
end
|
|
53
61
|
end
|
|
54
62
|
|
|
63
|
+
def verify_nonce!
|
|
64
|
+
actual = extract_nonce.to_s
|
|
65
|
+
unless ActiveSupport::SecurityUtils.secure_compare(actual, @expected_nonce.to_s)
|
|
66
|
+
raise VerificationError, "Nonce does not match expected value"
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
55
70
|
def verify_signature!
|
|
56
71
|
pubkey_bytes = Base58.base58_to_binary(@wallet_address, :bitcoin)
|
|
57
72
|
signature_bytes = decode_signature(@signature)
|
|
@@ -61,7 +76,7 @@ module Solrengine
|
|
|
61
76
|
end
|
|
62
77
|
|
|
63
78
|
def extract_nonce
|
|
64
|
-
match = @message.match(
|
|
79
|
+
match = @message.match(/^Nonce: ([a-f0-9]+)$/)
|
|
65
80
|
match&.captures&.first
|
|
66
81
|
end
|
|
67
82
|
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: solrengine-auth
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Jose Ferrer
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-04-19 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rails
|