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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: df23b01409dffb68f966f67598f23f01099886129e74164346d0c9a7712959d4
4
- data.tar.gz: ce7cae7ad431d9aec869847e831a3d4d944cae1269609f69ce8fbb5f583d3ebd
3
+ metadata.gz: e74477d86c34bcdd7d1e3aec0296d766c1dff71a64aabfabf122e47677a969e9
4
+ data.tar.gz: 5936982e40d446e51a142c1151346037aa2f26a9750d4c0a0547a3d5350f3a69
5
5
  SHA512:
6
- metadata.gz: f5b794dd88bad141918294d789283812edb9c44c2ffaa0e3015535c56f52bf3b0c09ba898f6d76bc0844ab766541137e5ee51263f3fdd89b7b9b9f6ae13327ca
7
- data.tar.gz: 1d1fe7214949db640fea0608efe85b1a84e92691245ffc2a046c936b958f61a8a9a4f07799558cb223f03a5d218e61893edbd6bf01797f0e86a05877cf1a1a01
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
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Solrengine
4
4
  module Auth
5
- class ApplicationController < ActionController::Base
5
+ class ApplicationController < ::ApplicationController
6
6
  end
7
7
  end
8
8
  end
@@ -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
- wallet_address = params[:wallet_address]
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
- wallet_address = params[:wallet_address]
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: "Authentication expired. Please try again." }, status: :unprocessable_entity
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: "Signature verification failed" }, status: :unauthorized
52
- end
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
- redirect_to Solrengine::Auth.configuration.after_sign_out_path, notice: "Disconnected"
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
- post "nonce", to: "sessions#nonce", as: :nonce
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(/Nonce: ([a-f0-9]+)/)
79
+ match = @message.match(/^Nonce: ([a-f0-9]+)$/)
65
80
  match&.captures&.first
66
81
  end
67
82
 
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Solrengine
4
4
  module Auth
5
- VERSION = "0.1.1"
5
+ VERSION = "0.2.0"
6
6
  end
7
7
  end
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.1.1
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-03-23 00:00:00.000000000 Z
11
+ date: 2026-04-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails