solrengine-auth 0.1.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 +7 -0
- data/README.md +106 -0
- data/app/assets/javascripts/solrengine/auth/wallet_controller.js +262 -0
- data/app/controllers/solrengine/auth/application_controller.rb +8 -0
- data/app/controllers/solrengine/auth/concerns/controller_helpers.rb +29 -0
- data/app/controllers/solrengine/auth/sessions_controller.rb +79 -0
- data/app/models/solrengine/auth/concerns/authenticatable.rb +37 -0
- data/app/views/solrengine/auth/sessions/new.html.erb +37 -0
- data/config/routes.rb +8 -0
- data/lib/generators/solrengine/auth/install_generator.rb +68 -0
- data/lib/generators/solrengine/auth/templates/initializer.rb +16 -0
- data/lib/generators/solrengine/auth/templates/migration.rb.erb +19 -0
- data/lib/solrengine/auth/configuration.rb +22 -0
- data/lib/solrengine/auth/engine.rb +13 -0
- data/lib/solrengine/auth/siws_message_builder.rb +34 -0
- data/lib/solrengine/auth/siws_verifier.rb +83 -0
- data/lib/solrengine/auth/version.rb +7 -0
- data/lib/solrengine/auth.rb +21 -0
- metadata +107 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 6622627cd1bbb261117bfab5b6a746b51c511885aae27f43f9246a9985d861d7
|
|
4
|
+
data.tar.gz: 6cdab9cc1a1b92d0a4dff46b2119d90ab76a78e2fdae6181ffb3d34eaa82ab0d
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 9a90484823d798a4aadc0c448a79bc99094b26ed85f4c24ab71aa301a8f31461cfb020de3c3bf6f7751bfe386a1b0e66e38f370e4980d37c651bd52f4a8b6ce5
|
|
7
|
+
data.tar.gz: 0f994647ae7a0a85ac5b8c7ab459c95a25fe23ed2fd47a564544d6de4d0a02ebae3627bb0683398aba1f4c10604312b4fb4b692ff6022ec2d68eb36215cf9a92
|
data/README.md
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# SolRengine Auth
|
|
2
|
+
|
|
3
|
+
Solana wallet authentication for Ruby on Rails. Sign in with any Wallet Standard compatible wallet (Phantom, Solflare, Backpack, Jupiter, etc.) using SIWS (Sign In With Solana).
|
|
4
|
+
|
|
5
|
+
Part of the [SolRengine](https://github.com/solrengine) framework.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
Add to your Gemfile:
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
gem "solrengine-auth"
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Run the generator:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
rails generate solrengine:auth:install
|
|
19
|
+
rails db:migrate
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
This adds:
|
|
23
|
+
- `wallet_address`, `nonce`, `nonce_expires_at` columns to your User model
|
|
24
|
+
- `Authenticatable` concern included in User
|
|
25
|
+
- `ControllerHelpers` concern included in ApplicationController
|
|
26
|
+
- Routes mounted at `/auth` (login, nonce, verify, logout)
|
|
27
|
+
- Configuration initializer
|
|
28
|
+
|
|
29
|
+
## Setup
|
|
30
|
+
|
|
31
|
+
Install the JavaScript dependencies:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
yarn add @wallet-standard/app @solana/wallet-standard-features
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Register the wallet Stimulus controller in your `app/javascript/controllers/index.js`:
|
|
38
|
+
|
|
39
|
+
```js
|
|
40
|
+
import WalletController from "solrengine/auth/wallet_controller"
|
|
41
|
+
application.register("wallet", WalletController)
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Configuration
|
|
45
|
+
|
|
46
|
+
```ruby
|
|
47
|
+
# config/initializers/solrengine_auth.rb
|
|
48
|
+
Solrengine::Auth.configure do |config|
|
|
49
|
+
config.domain = ENV.fetch("APP_DOMAIN", "localhost")
|
|
50
|
+
config.nonce_ttl = 5.minutes
|
|
51
|
+
config.after_sign_out_path = "/"
|
|
52
|
+
|
|
53
|
+
# The model class used for wallet authentication (String or Class).
|
|
54
|
+
# config.user_class = "User"
|
|
55
|
+
|
|
56
|
+
# Chain ID shown in the wallet sign-in message.
|
|
57
|
+
# Defaults to ENV["SOLANA_NETWORK"] or "mainnet".
|
|
58
|
+
# config.chain_id = "devnet"
|
|
59
|
+
end
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## How It Works
|
|
63
|
+
|
|
64
|
+
1. User clicks "Connect Wallet" -- Stimulus discovers installed wallets via Wallet Standard
|
|
65
|
+
2. User selects a wallet -- extension popup opens
|
|
66
|
+
3. Rails generates a SIWS message with a nonce (POST `/auth/nonce`)
|
|
67
|
+
4. Wallet signs the message (Ed25519)
|
|
68
|
+
5. Rails verifies the signature, validates the nonce, and creates a session (POST `/auth/verify`)
|
|
69
|
+
|
|
70
|
+
No passwords. No emails. The wallet is the identity.
|
|
71
|
+
|
|
72
|
+
## Usage in Controllers
|
|
73
|
+
|
|
74
|
+
```ruby
|
|
75
|
+
class DashboardController < ApplicationController
|
|
76
|
+
before_action :authenticate!
|
|
77
|
+
|
|
78
|
+
def show
|
|
79
|
+
@wallet_address = current_user.wallet_address
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
`current_user`, `logged_in?`, and `authenticate!` are provided by the `Solrengine::Auth::Concerns::ControllerHelpers` concern, which the generator includes in your ApplicationController.
|
|
85
|
+
|
|
86
|
+
## Standalone Usage
|
|
87
|
+
|
|
88
|
+
The verifier can be used without Rails:
|
|
89
|
+
|
|
90
|
+
```ruby
|
|
91
|
+
require "solrengine/auth"
|
|
92
|
+
|
|
93
|
+
verifier = Solrengine::Auth::SiwsVerifier.new(
|
|
94
|
+
wallet_address: "Abc...xyz",
|
|
95
|
+
message: siws_message,
|
|
96
|
+
signature: signature_bytes,
|
|
97
|
+
domain: "myapp.com"
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
verifier.verify # => true/false
|
|
101
|
+
verifier.verify! # => true or raises VerificationError
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## License
|
|
105
|
+
|
|
106
|
+
MIT
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
import { getWallets } from "@wallet-standard/app"
|
|
3
|
+
import { SolanaSignMessage } from "@solana/wallet-standard-features"
|
|
4
|
+
|
|
5
|
+
// Wallet connection + SIWS authentication controller.
|
|
6
|
+
//
|
|
7
|
+
// Discovers wallets via Wallet Standard. Uses legacy provider for connect
|
|
8
|
+
// (preserves user gesture for popup), wallet-standard for signMessage.
|
|
9
|
+
export default class extends Controller {
|
|
10
|
+
static targets = ["connectBtn", "signing", "status", "walletList"]
|
|
11
|
+
static values = {
|
|
12
|
+
nonceUrl: String,
|
|
13
|
+
verifyUrl: String,
|
|
14
|
+
dashboardUrl: String
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
connect() {
|
|
18
|
+
this.availableWallets = []
|
|
19
|
+
this.selectedWallet = null
|
|
20
|
+
this.discoverWallets()
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
discoverWallets() {
|
|
24
|
+
const { get, on } = getWallets()
|
|
25
|
+
|
|
26
|
+
this.addWallets(get())
|
|
27
|
+
|
|
28
|
+
on("register", (...newWallets) => {
|
|
29
|
+
this.addWallets(newWallets)
|
|
30
|
+
})
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
addWallets(wallets) {
|
|
34
|
+
for (const wallet of wallets) {
|
|
35
|
+
if (wallet.features[SolanaSignMessage]) {
|
|
36
|
+
if (!this.availableWallets.find(w => w.name === wallet.name)) {
|
|
37
|
+
this.availableWallets.push(wallet)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (this.availableWallets.length > 0 && !this.selectedWallet) {
|
|
43
|
+
this.selectedWallet = this.availableWallets[0]
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
this.renderWalletList()
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
renderWalletList() {
|
|
50
|
+
if (!this.hasWalletListTarget) return
|
|
51
|
+
if (this.availableWallets.length === 0) return
|
|
52
|
+
|
|
53
|
+
this.walletListTarget.replaceChildren()
|
|
54
|
+
|
|
55
|
+
this.availableWallets.forEach((wallet, index) => {
|
|
56
|
+
const button = document.createElement("button")
|
|
57
|
+
button.dataset.action = "click->wallet#selectWallet"
|
|
58
|
+
button.dataset.walletIndex = index
|
|
59
|
+
button.className = `flex items-center gap-3 w-full p-3 rounded-xl border cursor-pointer transition-all duration-200 ${
|
|
60
|
+
this.selectedWallet === wallet
|
|
61
|
+
? 'border-purple-500 bg-purple-900/20'
|
|
62
|
+
: 'border-gray-700 hover:border-gray-500 bg-gray-800/30'
|
|
63
|
+
}`
|
|
64
|
+
|
|
65
|
+
if (wallet.icon && /^(https?:|data:image\/)/.test(wallet.icon)) {
|
|
66
|
+
const img = document.createElement("img")
|
|
67
|
+
img.src = wallet.icon
|
|
68
|
+
img.alt = wallet.name
|
|
69
|
+
img.className = "w-8 h-8 rounded-lg"
|
|
70
|
+
button.appendChild(img)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const span = document.createElement("span")
|
|
74
|
+
span.className = "text-white font-medium"
|
|
75
|
+
span.textContent = wallet.name
|
|
76
|
+
button.appendChild(span)
|
|
77
|
+
|
|
78
|
+
this.walletListTarget.appendChild(button)
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
this.walletListTarget.classList.remove("hidden")
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
selectWallet(event) {
|
|
85
|
+
const index = parseInt(event.currentTarget.dataset.walletIndex)
|
|
86
|
+
this.selectedWallet = this.availableWallets[index]
|
|
87
|
+
this.renderWalletList()
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Map wallet-standard wallets to their legacy window providers.
|
|
91
|
+
// Legacy connect() is called first to preserve user gesture context
|
|
92
|
+
// (Chrome only allows extension popups within a direct user action).
|
|
93
|
+
getLegacyProvider(walletName) {
|
|
94
|
+
const name = walletName.toLowerCase()
|
|
95
|
+
if (name.includes("phantom")) return window.phantom?.solana || window.solana
|
|
96
|
+
if (name.includes("solflare")) return window.solflare
|
|
97
|
+
if (name.includes("backpack")) return window.backpack
|
|
98
|
+
return null
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async authenticate() {
|
|
102
|
+
if (!this.selectedWallet) {
|
|
103
|
+
this.showStatus("No Solana wallet found. Please install a Solana wallet extension.", "error")
|
|
104
|
+
return
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
this.connectBtnTarget.disabled = true
|
|
109
|
+
this.connectBtnTarget.textContent = `Connecting to ${this.selectedWallet.name}...`
|
|
110
|
+
|
|
111
|
+
let publicKey
|
|
112
|
+
let signMessage
|
|
113
|
+
|
|
114
|
+
// Try legacy provider FIRST — this must happen immediately on user click
|
|
115
|
+
// so Chrome allows the extension popup to open.
|
|
116
|
+
const provider = this.getLegacyProvider(this.selectedWallet.name)
|
|
117
|
+
|
|
118
|
+
if (provider) {
|
|
119
|
+
// Legacy connect — happens immediately in the user gesture, popup shows
|
|
120
|
+
const response = await provider.connect()
|
|
121
|
+
|
|
122
|
+
// Different wallets return publicKey differently:
|
|
123
|
+
// Phantom: response.publicKey.toString()
|
|
124
|
+
// Solflare: response.publicKey?.toString() or provider.publicKey.toString()
|
|
125
|
+
// Backpack: response.publicKey.toString() or provider.publicKey.toString()
|
|
126
|
+
const pk = response?.publicKey || provider.publicKey
|
|
127
|
+
if (!pk) {
|
|
128
|
+
throw new Error("Wallet connected but no public key returned. Please try again.")
|
|
129
|
+
}
|
|
130
|
+
publicKey = pk.toString()
|
|
131
|
+
|
|
132
|
+
signMessage = async (messageBytes) => {
|
|
133
|
+
// signMessage API differs per wallet:
|
|
134
|
+
// Phantom: signMessage(bytes, "utf8") → { signature }
|
|
135
|
+
// Solflare: signMessage(bytes) → { signature }
|
|
136
|
+
// Backpack: signMessage(bytes) → Uint8Array
|
|
137
|
+
// Only pass encoding for Phantom; others don't accept it.
|
|
138
|
+
const isPhantom = provider.isPhantom === true
|
|
139
|
+
const result = isPhantom
|
|
140
|
+
? await provider.signMessage(messageBytes, "utf8")
|
|
141
|
+
: await provider.signMessage(messageBytes)
|
|
142
|
+
|
|
143
|
+
if (result instanceof Uint8Array) return result
|
|
144
|
+
if (result?.signature) return new Uint8Array(result.signature)
|
|
145
|
+
return new Uint8Array(result)
|
|
146
|
+
}
|
|
147
|
+
} else {
|
|
148
|
+
// No legacy provider — use wallet-standard connect
|
|
149
|
+
const connectFeature = this.selectedWallet.features["standard:connect"]
|
|
150
|
+
const { accounts } = await connectFeature.connect()
|
|
151
|
+
|
|
152
|
+
if (!accounts || accounts.length === 0) {
|
|
153
|
+
throw new Error("No accounts returned. Please unlock your wallet and try again.")
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const account = accounts[0]
|
|
157
|
+
publicKey = account.address
|
|
158
|
+
|
|
159
|
+
const signMessageFeature = this.selectedWallet.features[SolanaSignMessage]
|
|
160
|
+
signMessage = async (messageBytes) => {
|
|
161
|
+
const [{ signature }] = await signMessageFeature.signMessage(
|
|
162
|
+
{ account, message: messageBytes }
|
|
163
|
+
)
|
|
164
|
+
return new Uint8Array(signature)
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Step 2: Request a nonce from the server
|
|
169
|
+
this.showSigning()
|
|
170
|
+
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content
|
|
171
|
+
const nonceResponse = await fetch(this.nonceUrlValue, {
|
|
172
|
+
method: "POST",
|
|
173
|
+
headers: {
|
|
174
|
+
"Content-Type": "application/json",
|
|
175
|
+
"Accept": "application/json",
|
|
176
|
+
"X-CSRF-Token": csrfToken
|
|
177
|
+
},
|
|
178
|
+
body: JSON.stringify({ wallet_address: publicKey })
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
if (!nonceResponse.ok) {
|
|
182
|
+
throw new Error("Failed to get authentication challenge")
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const { message } = await nonceResponse.json()
|
|
186
|
+
|
|
187
|
+
// Step 3: Sign the SIWS message
|
|
188
|
+
const encodedMessage = new TextEncoder().encode(message)
|
|
189
|
+
const signatureBytes = await signMessage(encodedMessage)
|
|
190
|
+
|
|
191
|
+
// Step 4: Send the signed message to the server for verification
|
|
192
|
+
const signatureString = Array.from(signatureBytes).join(",")
|
|
193
|
+
|
|
194
|
+
const verifyResponse = await fetch(this.verifyUrlValue, {
|
|
195
|
+
method: "POST",
|
|
196
|
+
headers: {
|
|
197
|
+
"Content-Type": "application/json",
|
|
198
|
+
"Accept": "application/json",
|
|
199
|
+
"X-CSRF-Token": csrfToken
|
|
200
|
+
},
|
|
201
|
+
body: JSON.stringify({
|
|
202
|
+
wallet_address: publicKey,
|
|
203
|
+
message: message,
|
|
204
|
+
signature: signatureString
|
|
205
|
+
})
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
if (!verifyResponse.ok) {
|
|
209
|
+
const error = await verifyResponse.json()
|
|
210
|
+
throw new Error(error.error || "Verification failed")
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Step 5: Redirect to dashboard
|
|
214
|
+
window.location.href = this.dashboardUrlValue
|
|
215
|
+
|
|
216
|
+
} catch (error) {
|
|
217
|
+
console.error("Wallet authentication error:", error)
|
|
218
|
+
|
|
219
|
+
if (error.message?.includes("User rejected") || error.message?.includes("cancelled")) {
|
|
220
|
+
this.showStatus("Sign-in cancelled", "warning")
|
|
221
|
+
} else if (error.message?.includes("Unexpected error")) {
|
|
222
|
+
this.showStatus(`Please unlock ${this.selectedWallet.name} and try again.`, "error")
|
|
223
|
+
} else {
|
|
224
|
+
this.showStatus(error.message || "Connection failed.", "error")
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
this.resetUI()
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
showSigning() {
|
|
232
|
+
this.connectBtnTarget.classList.add("hidden")
|
|
233
|
+
if (this.hasWalletListTarget) this.walletListTarget.classList.add("hidden")
|
|
234
|
+
this.signingTarget.classList.remove("hidden")
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
resetUI() {
|
|
238
|
+
this.connectBtnTarget.classList.remove("hidden")
|
|
239
|
+
this.connectBtnTarget.disabled = false
|
|
240
|
+
this.connectBtnTarget.textContent = "Connect Wallet"
|
|
241
|
+
if (this.hasWalletListTarget) this.walletListTarget.classList.remove("hidden")
|
|
242
|
+
this.signingTarget.classList.add("hidden")
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
showStatus(message, type = "info") {
|
|
246
|
+
const statusEl = this.statusTarget
|
|
247
|
+
statusEl.textContent = message
|
|
248
|
+
statusEl.classList.remove("hidden", "bg-red-900/50", "text-red-300", "bg-yellow-900/50", "text-yellow-300", "bg-green-900/50", "text-green-300")
|
|
249
|
+
|
|
250
|
+
switch (type) {
|
|
251
|
+
case "error":
|
|
252
|
+
statusEl.classList.add("bg-red-900/50", "text-red-300")
|
|
253
|
+
break
|
|
254
|
+
case "warning":
|
|
255
|
+
statusEl.classList.add("bg-yellow-900/50", "text-yellow-300")
|
|
256
|
+
break
|
|
257
|
+
default:
|
|
258
|
+
statusEl.classList.add("bg-green-900/50", "text-green-300")
|
|
259
|
+
}
|
|
260
|
+
statusEl.classList.remove("hidden")
|
|
261
|
+
}
|
|
262
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Solrengine
|
|
4
|
+
module Auth
|
|
5
|
+
module Concerns
|
|
6
|
+
module ControllerHelpers
|
|
7
|
+
extend ActiveSupport::Concern
|
|
8
|
+
|
|
9
|
+
included do
|
|
10
|
+
helper_method :current_user, :logged_in?
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
private
|
|
14
|
+
|
|
15
|
+
def current_user
|
|
16
|
+
@current_user ||= Solrengine::Auth.configuration.user_model.find_by(id: session[:user_id]) if session[:user_id]
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def logged_in?
|
|
20
|
+
current_user.present?
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def authenticate!
|
|
24
|
+
redirect_to solrengine_auth.login_path unless logged_in?
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Solrengine
|
|
4
|
+
module Auth
|
|
5
|
+
class SessionsController < ApplicationController
|
|
6
|
+
def new
|
|
7
|
+
# Renders the wallet connect view
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
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?
|
|
21
|
+
|
|
22
|
+
domain = Solrengine::Auth.configuration.domain
|
|
23
|
+
message = SiwsMessageBuilder.new(
|
|
24
|
+
domain: domain,
|
|
25
|
+
wallet_address: wallet_address,
|
|
26
|
+
nonce: user.nonce,
|
|
27
|
+
uri: request.base_url
|
|
28
|
+
).build
|
|
29
|
+
|
|
30
|
+
render json: { message: message, nonce: user.nonce }
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
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)
|
|
39
|
+
|
|
40
|
+
unless user&.nonce_valid?
|
|
41
|
+
return render json: { error: "Authentication expired. Please try again." }, status: :unprocessable_entity
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
verifier = SiwsVerifier.new(
|
|
45
|
+
wallet_address: wallet_address,
|
|
46
|
+
message: message,
|
|
47
|
+
signature: signature
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
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
|
+
end
|
|
59
|
+
|
|
60
|
+
user.generate_nonce!
|
|
61
|
+
|
|
62
|
+
reset_session
|
|
63
|
+
session[:user_id] = user.id
|
|
64
|
+
render json: { success: true, wallet_address: user.wallet_address }
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def destroy
|
|
68
|
+
reset_session
|
|
69
|
+
redirect_to Solrengine::Auth.configuration.after_sign_out_path, notice: "Disconnected"
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
private
|
|
73
|
+
|
|
74
|
+
def _user_class
|
|
75
|
+
Solrengine::Auth.configuration.user_model
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Solrengine
|
|
4
|
+
module Auth
|
|
5
|
+
module Concerns
|
|
6
|
+
module Authenticatable
|
|
7
|
+
extend ActiveSupport::Concern
|
|
8
|
+
|
|
9
|
+
SOLANA_ADDRESS_FORMAT = /\A[1-9A-HJ-NP-Za-km-z]{32,44}\z/
|
|
10
|
+
|
|
11
|
+
included do
|
|
12
|
+
validates :wallet_address, presence: true, uniqueness: true,
|
|
13
|
+
format: { with: SOLANA_ADDRESS_FORMAT, message: "is not a valid Solana address" }
|
|
14
|
+
|
|
15
|
+
before_create :generate_nonce
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def generate_nonce!
|
|
19
|
+
generate_nonce
|
|
20
|
+
save! if persisted?
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def nonce_valid?
|
|
24
|
+
nonce.present? && nonce_expires_at.present? && nonce_expires_at > Time.current
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def generate_nonce
|
|
30
|
+
nonce_ttl = Solrengine::Auth.configuration.nonce_ttl
|
|
31
|
+
self.nonce = SecureRandom.hex(16)
|
|
32
|
+
self.nonce_expires_at = nonce_ttl.from_now
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
<div class="min-h-screen flex items-center justify-center bg-gradient-to-br from-gray-950 via-gray-900 to-purple-950">
|
|
2
|
+
<div class="max-w-md w-full mx-4" data-controller="wallet" data-wallet-nonce-url-value="<%= solrengine_auth.nonce_path %>" data-wallet-verify-url-value="<%= solrengine_auth.verify_path %>" data-wallet-dashboard-url-value="<%= Solrengine::Auth.configuration.after_sign_in_path %>">
|
|
3
|
+
|
|
4
|
+
<div class="text-center mb-8">
|
|
5
|
+
<h1 class="text-4xl font-bold text-white mb-2"><%= yield(:solrengine_auth_title) || "Sign In" %></h1>
|
|
6
|
+
<p class="text-gray-400">Connect your wallet to get started</p>
|
|
7
|
+
</div>
|
|
8
|
+
|
|
9
|
+
<div class="bg-gray-800/50 backdrop-blur border border-gray-700 rounded-2xl p-8 shadow-xl">
|
|
10
|
+
<div data-wallet-target="status" class="hidden mb-4 p-3 rounded-lg text-sm text-center"></div>
|
|
11
|
+
|
|
12
|
+
<div data-wallet-target="walletList" class="hidden mb-4 space-y-2"></div>
|
|
13
|
+
|
|
14
|
+
<button
|
|
15
|
+
data-wallet-target="connectBtn"
|
|
16
|
+
data-action="click->wallet#authenticate"
|
|
17
|
+
class="w-full cursor-pointer bg-gradient-to-r from-purple-600 to-blue-600 hover:from-purple-500 hover:to-blue-500 text-white font-semibold py-3 px-6 rounded-xl transition-all duration-200 flex items-center justify-center gap-2"
|
|
18
|
+
>
|
|
19
|
+
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
20
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1" />
|
|
21
|
+
</svg>
|
|
22
|
+
Connect Wallet
|
|
23
|
+
</button>
|
|
24
|
+
|
|
25
|
+
<div data-wallet-target="signing" class="hidden">
|
|
26
|
+
<div class="text-center">
|
|
27
|
+
<div class="animate-spin w-8 h-8 border-2 border-purple-500 border-t-transparent rounded-full mx-auto mb-3"></div>
|
|
28
|
+
<p class="text-gray-300">Approve the sign-in request in your wallet...</p>
|
|
29
|
+
</div>
|
|
30
|
+
</div>
|
|
31
|
+
</div>
|
|
32
|
+
|
|
33
|
+
<p class="text-center text-gray-500 text-sm mt-6">
|
|
34
|
+
Powered by SolRengine
|
|
35
|
+
</p>
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
data/config/routes.rb
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
Solrengine::Auth::Engine.routes.draw do
|
|
4
|
+
get "login", to: "sessions#new", as: :login
|
|
5
|
+
post "nonce", to: "sessions#nonce", as: :nonce
|
|
6
|
+
post "verify", to: "sessions#create", as: :verify
|
|
7
|
+
delete "logout", to: "sessions#destroy", as: :logout
|
|
8
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators"
|
|
4
|
+
require "rails/generators/migration"
|
|
5
|
+
|
|
6
|
+
module Solrengine
|
|
7
|
+
module Auth
|
|
8
|
+
class InstallGenerator < Rails::Generators::Base
|
|
9
|
+
include Rails::Generators::Migration
|
|
10
|
+
|
|
11
|
+
source_root File.expand_path("templates", __dir__)
|
|
12
|
+
|
|
13
|
+
def self.next_migration_number(dirname)
|
|
14
|
+
if ActiveRecord.respond_to?(:timestamped_migrations) && ActiveRecord.timestamped_migrations
|
|
15
|
+
Time.now.utc.strftime("%Y%m%d%H%M%S")
|
|
16
|
+
else
|
|
17
|
+
"%.3d" % (current_migration_number(dirname) + 1)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def create_initializer
|
|
22
|
+
template "initializer.rb", "config/initializers/solrengine_auth.rb"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def create_user_model
|
|
26
|
+
unless File.exist?("app/models/user.rb")
|
|
27
|
+
create_file "app/models/user.rb", <<~RUBY
|
|
28
|
+
class User < ApplicationRecord
|
|
29
|
+
include Solrengine::Auth::Concerns::Authenticatable
|
|
30
|
+
end
|
|
31
|
+
RUBY
|
|
32
|
+
else
|
|
33
|
+
unless File.read("app/models/user.rb").include?("Solrengine::Auth::Concerns::Authenticatable")
|
|
34
|
+
inject_into_class "app/models/user.rb", "User",
|
|
35
|
+
" include Solrengine::Auth::Concerns::Authenticatable\n\n"
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def create_migration
|
|
41
|
+
migration_template "migration.rb.erb", "db/migrate/create_users_with_wallet_auth.rb"
|
|
42
|
+
rescue Rails::Generators::Error
|
|
43
|
+
say_status :skip, "Migration already exists", :yellow
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def add_routes
|
|
47
|
+
route_string = 'mount Solrengine::Auth::Engine, at: "/auth"'
|
|
48
|
+
return if File.read("config/routes.rb").include?(route_string)
|
|
49
|
+
|
|
50
|
+
route route_string
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def add_application_controller_helpers
|
|
54
|
+
controller_file = "app/controllers/application_controller.rb"
|
|
55
|
+
include_line = "include Solrengine::Auth::Concerns::ControllerHelpers"
|
|
56
|
+
|
|
57
|
+
return if File.read(controller_file).include?(include_line)
|
|
58
|
+
|
|
59
|
+
inject_into_class controller_file, "ApplicationController", " #{include_line}\n"
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def show_post_install
|
|
63
|
+
say "\n SolRengine Auth installed!", :green
|
|
64
|
+
say " Run `rails db:migrate` to create the users table.\n\n"
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
Solrengine::Auth.configure do |config|
|
|
2
|
+
# The domain used in SIWS messages. Must match your app's domain.
|
|
3
|
+
config.domain = ENV.fetch("APP_DOMAIN", "localhost")
|
|
4
|
+
|
|
5
|
+
# How long a nonce is valid before expiring.
|
|
6
|
+
config.nonce_ttl = 5.minutes
|
|
7
|
+
|
|
8
|
+
# The model class used for wallet authentication (String or Class).
|
|
9
|
+
# config.user_class = "User"
|
|
10
|
+
|
|
11
|
+
# Where to redirect after sign-in (used by the JS wallet controller).
|
|
12
|
+
config.after_sign_in_path = "/"
|
|
13
|
+
|
|
14
|
+
# Where to redirect after sign-out.
|
|
15
|
+
config.after_sign_out_path = "/"
|
|
16
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
class CreateUsersWithWalletAuth < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
|
|
2
|
+
def change
|
|
3
|
+
if table_exists?(:users)
|
|
4
|
+
add_column :users, :wallet_address, :string, null: false unless column_exists?(:users, :wallet_address)
|
|
5
|
+
add_index :users, :wallet_address, unique: true unless index_exists?(:users, :wallet_address)
|
|
6
|
+
add_column :users, :nonce, :string unless column_exists?(:users, :nonce)
|
|
7
|
+
add_column :users, :nonce_expires_at, :datetime unless column_exists?(:users, :nonce_expires_at)
|
|
8
|
+
else
|
|
9
|
+
create_table :users do |t|
|
|
10
|
+
t.string :wallet_address, null: false
|
|
11
|
+
t.string :nonce
|
|
12
|
+
t.datetime :nonce_expires_at
|
|
13
|
+
|
|
14
|
+
t.timestamps
|
|
15
|
+
end
|
|
16
|
+
add_index :users, :wallet_address, unique: true
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Solrengine
|
|
4
|
+
module Auth
|
|
5
|
+
class Configuration
|
|
6
|
+
attr_accessor :domain, :nonce_ttl, :after_sign_in_path, :after_sign_out_path, :user_class, :chain_id
|
|
7
|
+
|
|
8
|
+
def initialize
|
|
9
|
+
@domain = "localhost"
|
|
10
|
+
@nonce_ttl = 5.minutes
|
|
11
|
+
@after_sign_in_path = "/"
|
|
12
|
+
@after_sign_out_path = "/"
|
|
13
|
+
@user_class = "User"
|
|
14
|
+
@chain_id = ENV.fetch("SOLANA_NETWORK", "mainnet")
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def user_model
|
|
18
|
+
@user_class.is_a?(String) ? @user_class.constantize : @user_class
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Solrengine
|
|
4
|
+
module Auth
|
|
5
|
+
class Engine < ::Rails::Engine
|
|
6
|
+
isolate_namespace Solrengine::Auth
|
|
7
|
+
|
|
8
|
+
initializer "solrengine-auth.assets" do |app|
|
|
9
|
+
app.config.assets.paths << root.join("app/assets/javascripts")
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Solrengine
|
|
4
|
+
module Auth
|
|
5
|
+
# Builds a SIWS (Sign In With Solana) message following the standard format.
|
|
6
|
+
# Modeled after EIP-4361 (SIWE) adapted for Solana.
|
|
7
|
+
class SiwsMessageBuilder
|
|
8
|
+
def initialize(domain:, wallet_address:, nonce:, statement: nil, uri: nil, chain_id: nil)
|
|
9
|
+
@domain = domain
|
|
10
|
+
@wallet_address = wallet_address
|
|
11
|
+
@nonce = nonce
|
|
12
|
+
@statement = statement || "Sign in to #{domain}"
|
|
13
|
+
@uri = uri || "https://#{domain}"
|
|
14
|
+
@chain_id = chain_id || Solrengine::Auth.configuration.chain_id
|
|
15
|
+
@issued_at = Time.current.iso8601
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def build
|
|
19
|
+
[
|
|
20
|
+
"#{@domain} wants you to sign in with your Solana account:",
|
|
21
|
+
@wallet_address,
|
|
22
|
+
"",
|
|
23
|
+
@statement,
|
|
24
|
+
"",
|
|
25
|
+
"URI: #{@uri}",
|
|
26
|
+
"Version: 1",
|
|
27
|
+
"Chain ID: #{@chain_id}",
|
|
28
|
+
"Nonce: #{@nonce}",
|
|
29
|
+
"Issued At: #{@issued_at}"
|
|
30
|
+
].join("\n")
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ed25519"
|
|
4
|
+
require "base58"
|
|
5
|
+
|
|
6
|
+
module Solrengine
|
|
7
|
+
module Auth
|
|
8
|
+
# Verifies SIWS (Sign In With Solana) signed messages.
|
|
9
|
+
# Uses Ed25519 signature verification and validates domain
|
|
10
|
+
# to prevent cross-site replay attacks.
|
|
11
|
+
class SiwsVerifier
|
|
12
|
+
class VerificationError < StandardError; end
|
|
13
|
+
|
|
14
|
+
def initialize(wallet_address:, message:, signature:, domain: nil)
|
|
15
|
+
@wallet_address = wallet_address
|
|
16
|
+
@message = message
|
|
17
|
+
@signature = signature
|
|
18
|
+
@domain = domain || Solrengine::Auth.configuration.domain
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def verify!
|
|
22
|
+
verify_message_format!
|
|
23
|
+
verify_domain!
|
|
24
|
+
verify_signature!
|
|
25
|
+
true
|
|
26
|
+
rescue Ed25519::VerifyError
|
|
27
|
+
raise VerificationError, "Invalid signature"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def verify
|
|
31
|
+
verify!
|
|
32
|
+
rescue VerificationError
|
|
33
|
+
false
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def verify_message_format!
|
|
39
|
+
unless @message.include?(@wallet_address)
|
|
40
|
+
raise VerificationError, "Message does not contain the claimed wallet address"
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
unless extract_nonce.present?
|
|
44
|
+
raise VerificationError, "Message does not contain a nonce"
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def verify_domain!
|
|
49
|
+
first_line = @message.lines.first&.strip
|
|
50
|
+
unless first_line == "#{@domain} wants you to sign in with your Solana account:"
|
|
51
|
+
raise VerificationError, "Message domain does not match expected domain"
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def verify_signature!
|
|
56
|
+
pubkey_bytes = Base58.base58_to_binary(@wallet_address, :bitcoin)
|
|
57
|
+
signature_bytes = decode_signature(@signature)
|
|
58
|
+
|
|
59
|
+
verify_key = Ed25519::VerifyKey.new(pubkey_bytes)
|
|
60
|
+
verify_key.verify(signature_bytes, @message)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def extract_nonce
|
|
64
|
+
match = @message.match(/Nonce: ([a-f0-9]+)/)
|
|
65
|
+
match&.captures&.first
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def decode_signature(signature)
|
|
69
|
+
bytes = if signature.match?(/\A[0-9,\s]+\z/)
|
|
70
|
+
values = signature.split(",").map(&:to_i)
|
|
71
|
+
raise VerificationError, "Invalid signature byte values" if values.any? { |v| v < 0 || v > 255 }
|
|
72
|
+
values.pack("C*")
|
|
73
|
+
else
|
|
74
|
+
Base64.strict_decode64(signature)
|
|
75
|
+
end
|
|
76
|
+
raise VerificationError, "Signature must be 64 bytes" unless bytes.bytesize == 64
|
|
77
|
+
bytes
|
|
78
|
+
rescue ArgumentError
|
|
79
|
+
raise VerificationError, "Invalid signature encoding"
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "auth/version"
|
|
4
|
+
require_relative "auth/configuration"
|
|
5
|
+
require_relative "auth/siws_verifier"
|
|
6
|
+
require_relative "auth/siws_message_builder"
|
|
7
|
+
require_relative "auth/engine" if defined?(Rails::Engine)
|
|
8
|
+
|
|
9
|
+
module Solrengine
|
|
10
|
+
module Auth
|
|
11
|
+
class << self
|
|
12
|
+
def configuration
|
|
13
|
+
@configuration ||= Configuration.new
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def configure
|
|
17
|
+
yield(configuration)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: solrengine-auth
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Jose Ferrer
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-03-21 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: rails
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - ">="
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '7.1'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - ">="
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '7.1'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: ed25519
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - "~>"
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '1.4'
|
|
34
|
+
type: :runtime
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - "~>"
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '1.4'
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: base58
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - "~>"
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '0.2'
|
|
48
|
+
type: :runtime
|
|
49
|
+
prerelease: false
|
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - "~>"
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '0.2'
|
|
55
|
+
description: Drop-in wallet authentication for Rails apps. Sign in with Phantom, Solflare,
|
|
56
|
+
Backpack, or any Wallet Standard compatible Solana wallet. Ed25519 signature verification
|
|
57
|
+
in Ruby.
|
|
58
|
+
email:
|
|
59
|
+
- estoy@moviendo.me
|
|
60
|
+
executables: []
|
|
61
|
+
extensions: []
|
|
62
|
+
extra_rdoc_files: []
|
|
63
|
+
files:
|
|
64
|
+
- README.md
|
|
65
|
+
- app/assets/javascripts/solrengine/auth/wallet_controller.js
|
|
66
|
+
- app/controllers/solrengine/auth/application_controller.rb
|
|
67
|
+
- app/controllers/solrengine/auth/concerns/controller_helpers.rb
|
|
68
|
+
- app/controllers/solrengine/auth/sessions_controller.rb
|
|
69
|
+
- app/models/solrengine/auth/concerns/authenticatable.rb
|
|
70
|
+
- app/views/solrengine/auth/sessions/new.html.erb
|
|
71
|
+
- config/routes.rb
|
|
72
|
+
- lib/generators/solrengine/auth/install_generator.rb
|
|
73
|
+
- lib/generators/solrengine/auth/templates/initializer.rb
|
|
74
|
+
- lib/generators/solrengine/auth/templates/migration.rb.erb
|
|
75
|
+
- lib/solrengine/auth.rb
|
|
76
|
+
- lib/solrengine/auth/configuration.rb
|
|
77
|
+
- lib/solrengine/auth/engine.rb
|
|
78
|
+
- lib/solrengine/auth/siws_message_builder.rb
|
|
79
|
+
- lib/solrengine/auth/siws_verifier.rb
|
|
80
|
+
- lib/solrengine/auth/version.rb
|
|
81
|
+
homepage: https://github.com/solrengine/auth
|
|
82
|
+
licenses:
|
|
83
|
+
- MIT
|
|
84
|
+
metadata:
|
|
85
|
+
homepage_uri: https://github.com/solrengine/auth
|
|
86
|
+
source_code_uri: https://github.com/solrengine/auth
|
|
87
|
+
changelog_uri: https://github.com/solrengine/auth/blob/main/CHANGELOG.md
|
|
88
|
+
post_install_message:
|
|
89
|
+
rdoc_options: []
|
|
90
|
+
require_paths:
|
|
91
|
+
- lib
|
|
92
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
93
|
+
requirements:
|
|
94
|
+
- - ">="
|
|
95
|
+
- !ruby/object:Gem::Version
|
|
96
|
+
version: 3.2.0
|
|
97
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
98
|
+
requirements:
|
|
99
|
+
- - ">="
|
|
100
|
+
- !ruby/object:Gem::Version
|
|
101
|
+
version: '0'
|
|
102
|
+
requirements: []
|
|
103
|
+
rubygems_version: 3.5.22
|
|
104
|
+
signing_key:
|
|
105
|
+
specification_version: 4
|
|
106
|
+
summary: Solana wallet authentication for Rails using SIWS (Sign In With Solana)
|
|
107
|
+
test_files: []
|