lato 3.16.0 → 3.17.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 +15 -0
- data/app/assets/javascripts/lato/controllers/lato_account_webauthn_controller.js +93 -0
- data/app/assets/javascripts/lato/controllers/lato_webauthn_auth_controller.js +110 -0
- data/app/controllers/lato/account_controller.rb +72 -16
- data/app/controllers/lato/authentication_controller.rb +95 -15
- data/app/models/lato/user.rb +131 -1
- data/app/views/lato/account/_form-authenticator.html.erb +4 -3
- data/app/views/lato/account/_form-webauthn.html.erb +66 -0
- data/app/views/lato/account/index.html.erb +17 -6
- data/app/views/lato/authentication/_form-authentication-method.html.erb +36 -0
- data/app/views/lato/authentication/_form-webauthn.html.erb +27 -0
- data/app/views/lato/authentication/authentication_method.html.erb +10 -0
- data/app/views/lato/authentication/webauthn.html.erb +10 -0
- data/app/views/layouts/lato/_action.html.erb +1 -1
- data/config/locales/en.yml +27 -3
- data/config/locales/fr.yml +27 -3
- data/config/locales/it.yml +27 -3
- data/config/locales/ro.yml +27 -3
- data/config/routes.rb +6 -1
- data/db/migrate/20251206170443_add_webauthn_id_to_user.rb +6 -0
- data/lib/lato/config.rb +11 -3
- data/lib/lato/engine.rb +11 -0
- data/lib/lato/version.rb +1 -1
- metadata +24 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 693630b5ecdde3bd76143d39982e7313c266188c30a1dbbd41b482a56db541c7
|
|
4
|
+
data.tar.gz: 77c4a9d2ae92e3669f1936590cc8b2bf5799ccfea9eccd904965d9c51cb26ffa
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f56cbc6971fbe27fc566edea26979f3912f6133e47e71c9b11089aa1f035e66e41600179cad6f52be6b622d0dbd3780ce7c4226108a65dc57c3e3d004268cbe2
|
|
7
|
+
data.tar.gz: a9a0c32cfb747eceaf5dde8ec03c3590eb441a3ce24f53987618835bb5a315c8e1a9c9e4d106b5bcf314f4e2e52479d22cfdb62603cc853c969a774f197b4ae0
|
data/README.md
CHANGED
|
@@ -154,3 +154,18 @@ ruby ./bin/generate_docs.rb
|
|
|
154
154
|
|
|
155
155
|
## License
|
|
156
156
|
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
|
157
|
+
|
|
158
|
+
Stiamo integrando l'autenticazione WebAuthn nel pannello Lato.
|
|
159
|
+
L'idea è che l'utente, dal suo account, può, volontariamente, attivare l'autenticazione WebAuthn per il suo account.
|
|
160
|
+
Una volta attivata, al login, dopo aver inserito email e password, l'utente dovrà autenticarsi con il dispositivo WebAuthn registrato (ad esempio una chiave di sicurezza FIDO2 o l'autenticazione biometrica del dispositivo).
|
|
161
|
+
Se è attiva anche l'autenticazione a due fattori con Google Authenticator, l'utente dovrà scegliere quale dei due metodi utilizzare per completare l'autenticazione.
|
|
162
|
+
|
|
163
|
+
Il modello Lato::User ha già i campi webauthn_id e webauthn_public_key da utilizzati per memorizzare le informazioni del dispositivo WebAuthn registrato.
|
|
164
|
+
|
|
165
|
+
Modifica il flow di login per includere il passaggio di autenticazione WebAuthn se l'utente ha un dispositivo WebAuthn registrato.
|
|
166
|
+
|
|
167
|
+
La logica dovrebbe essere la seguente:
|
|
168
|
+
- Se Google Auth e WebAuthn non sono attivi o se l'utente non li ha collegati, il login procede normalmente.
|
|
169
|
+
- Se solo Google Auth è attivo e l'utente lo ha collegato, dopo aver inserito email e password, l'utente viene reindirizzato alla pagina di inserimento del codice di Google Authenticator.
|
|
170
|
+
- Se solo WebAuthn è attivo e l'utente lo ha collegato, dopo aver inserito email e password, l'utente viene reindirizzato alla pagina di autenticazione WebAuthn.
|
|
171
|
+
- Se entrambi sono attivi e l'utente li ha collegati, dopo aver inserito email e password, l'utente deve scegliere quale metodo utilizzare per completare l'autenticazione.
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
export default class extends Controller {
|
|
4
|
+
static targets = ['credentialInput', 'submit', 'status']
|
|
5
|
+
static values = {
|
|
6
|
+
options: Object,
|
|
7
|
+
errorMessage: String
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
async connect() {
|
|
11
|
+
if (!this.hasOptionsValue) return
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
// Converti le options in formato corretto per il browser
|
|
15
|
+
const publicKey = this.preparePublicKeyOptions(this.optionsValue)
|
|
16
|
+
|
|
17
|
+
// Richiedi la creazione della credential al browser
|
|
18
|
+
const credential = await navigator.credentials.create({ publicKey })
|
|
19
|
+
|
|
20
|
+
if (!credential) {
|
|
21
|
+
this.showError(this.errorMessageValue)
|
|
22
|
+
return
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Prepara il payload da inviare al server
|
|
26
|
+
const credentialPayload = this.prepareCredentialPayload(credential)
|
|
27
|
+
|
|
28
|
+
// Popola il campo hidden con il payload
|
|
29
|
+
this.credentialInputTarget.value = JSON.stringify(credentialPayload)
|
|
30
|
+
|
|
31
|
+
// Submit automatico del form
|
|
32
|
+
this.submitTarget.click()
|
|
33
|
+
} catch (error) {
|
|
34
|
+
console.error('WebAuthn error:', error)
|
|
35
|
+
this.showError(this.errorMessageValue)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
preparePublicKeyOptions(options) {
|
|
40
|
+
return {
|
|
41
|
+
challenge: Uint8Array.from(atob(options.challenge.replace(/-/g, '+').replace(/_/g, '/')), c => c.charCodeAt(0)),
|
|
42
|
+
rp: options.rp,
|
|
43
|
+
user: {
|
|
44
|
+
id: Uint8Array.from(atob(options.user.id), c => c.charCodeAt(0)),
|
|
45
|
+
name: options.user.name,
|
|
46
|
+
displayName: options.user.displayName
|
|
47
|
+
},
|
|
48
|
+
pubKeyCredParams: options.pubKeyCredParams,
|
|
49
|
+
timeout: options.timeout,
|
|
50
|
+
excludeCredentials: options.excludeCredentials?.map(cred => ({
|
|
51
|
+
id: Uint8Array.from(atob(cred), c => c.charCodeAt(0)),
|
|
52
|
+
type: 'public-key'
|
|
53
|
+
})) || [],
|
|
54
|
+
authenticatorSelection: {
|
|
55
|
+
authenticatorAttachment: 'platform',
|
|
56
|
+
requireResidentKey: false,
|
|
57
|
+
userVerification: 'preferred'
|
|
58
|
+
},
|
|
59
|
+
attestation: 'none'
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
prepareCredentialPayload(credential) {
|
|
64
|
+
return {
|
|
65
|
+
id: credential.id,
|
|
66
|
+
rawId: this.arrayBufferToBase64(credential.rawId),
|
|
67
|
+
type: credential.type,
|
|
68
|
+
response: {
|
|
69
|
+
clientDataJSON: this.arrayBufferToBase64(credential.response.clientDataJSON),
|
|
70
|
+
attestationObject: this.arrayBufferToBase64(credential.response.attestationObject)
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
arrayBufferToBase64(buffer) {
|
|
76
|
+
const bytes = new Uint8Array(buffer)
|
|
77
|
+
let binary = ''
|
|
78
|
+
for (let i = 0; i < bytes.byteLength; i++) {
|
|
79
|
+
binary += String.fromCharCode(bytes[i])
|
|
80
|
+
}
|
|
81
|
+
return btoa(binary)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
showError(message) {
|
|
85
|
+
if (this.hasStatusTarget) {
|
|
86
|
+
this.statusTarget.innerHTML = `
|
|
87
|
+
<div class="alert alert-danger">
|
|
88
|
+
<h4 class="alert-heading">${message}</h4>
|
|
89
|
+
</div>
|
|
90
|
+
`
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
export default class extends Controller {
|
|
4
|
+
static targets = ['credential']
|
|
5
|
+
static values = {
|
|
6
|
+
options: Object
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
async authenticate(event) {
|
|
10
|
+
event.preventDefault()
|
|
11
|
+
|
|
12
|
+
if (!this.hasOptionsValue) {
|
|
13
|
+
console.error('WebAuthn options not provided')
|
|
14
|
+
return
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
// Log per debug
|
|
19
|
+
console.log('WebAuthn options:', this.optionsValue)
|
|
20
|
+
|
|
21
|
+
// Converti le options in formato corretto per il browser
|
|
22
|
+
const publicKey = this.preparePublicKeyOptions(this.optionsValue)
|
|
23
|
+
|
|
24
|
+
console.log('Prepared publicKey:', publicKey)
|
|
25
|
+
|
|
26
|
+
// Richiedi l'autenticazione al browser
|
|
27
|
+
const credential = await navigator.credentials.get({ publicKey })
|
|
28
|
+
|
|
29
|
+
if (!credential) {
|
|
30
|
+
alert('Autenticazione fallita')
|
|
31
|
+
return
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Prepara il payload da inviare al server
|
|
35
|
+
const credentialPayload = this.prepareCredentialPayload(credential)
|
|
36
|
+
|
|
37
|
+
// Popola il campo hidden con il payload
|
|
38
|
+
this.credentialTarget.value = JSON.stringify(credentialPayload)
|
|
39
|
+
|
|
40
|
+
// Submit del form
|
|
41
|
+
this.element.requestSubmit()
|
|
42
|
+
} catch (error) {
|
|
43
|
+
console.error('WebAuthn authentication error:', error)
|
|
44
|
+
alert('Errore durante l\'autenticazione: ' + error.message)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
preparePublicKeyOptions(options) {
|
|
49
|
+
return {
|
|
50
|
+
challenge: this.base64urlToBuffer(options.challenge),
|
|
51
|
+
timeout: options.timeout || 60000,
|
|
52
|
+
rpId: options.rpId || options.rp_id,
|
|
53
|
+
allowCredentials: (options.allowCredentials || options.allow_credentials || []).map(cred => {
|
|
54
|
+
// Se cred è già un oggetto con id, usalo direttamente
|
|
55
|
+
if (typeof cred === 'object' && cred.id) {
|
|
56
|
+
return {
|
|
57
|
+
id: this.base64urlToBuffer(cred.id),
|
|
58
|
+
type: cred.type || 'public-key'
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
// Altrimenti, assumiamo che cred sia una stringa base64
|
|
62
|
+
return {
|
|
63
|
+
id: this.base64urlToBuffer(cred),
|
|
64
|
+
type: 'public-key'
|
|
65
|
+
}
|
|
66
|
+
}),
|
|
67
|
+
userVerification: options.userVerification || options.user_verification || 'preferred'
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
base64urlToBuffer(base64url) {
|
|
72
|
+
// Converti base64url in base64 standard
|
|
73
|
+
const base64 = base64url.replace(/-/g, '+').replace(/_/g, '/')
|
|
74
|
+
// Aggiungi padding se necessario
|
|
75
|
+
const padded = base64.padEnd(base64.length + (4 - base64.length % 4) % 4, '=')
|
|
76
|
+
// Decodifica e converti in Uint8Array
|
|
77
|
+
const binary = atob(padded)
|
|
78
|
+
const bytes = new Uint8Array(binary.length)
|
|
79
|
+
for (let i = 0; i < binary.length; i++) {
|
|
80
|
+
bytes[i] = binary.charCodeAt(i)
|
|
81
|
+
}
|
|
82
|
+
return bytes
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
prepareCredentialPayload(credential) {
|
|
86
|
+
return {
|
|
87
|
+
id: credential.id,
|
|
88
|
+
rawId: this.arrayBufferToBase64url(credential.rawId),
|
|
89
|
+
type: credential.type,
|
|
90
|
+
response: {
|
|
91
|
+
clientDataJSON: this.arrayBufferToBase64url(credential.response.clientDataJSON),
|
|
92
|
+
authenticatorData: this.arrayBufferToBase64url(credential.response.authenticatorData),
|
|
93
|
+
signature: this.arrayBufferToBase64url(credential.response.signature),
|
|
94
|
+
userHandle: credential.response.userHandle ? this.arrayBufferToBase64url(credential.response.userHandle) : null
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
arrayBufferToBase64url(buffer) {
|
|
100
|
+
const bytes = new Uint8Array(buffer)
|
|
101
|
+
let binary = ''
|
|
102
|
+
for (let i = 0; i < bytes.byteLength; i++) {
|
|
103
|
+
binary += String.fromCharCode(bytes[i])
|
|
104
|
+
}
|
|
105
|
+
// Converti in base64 standard
|
|
106
|
+
const base64 = btoa(binary)
|
|
107
|
+
// Converti in base64url (rimuovi padding e sostituisci caratteri)
|
|
108
|
+
return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '')
|
|
109
|
+
}
|
|
110
|
+
}
|
|
@@ -17,12 +17,12 @@ module Lato
|
|
|
17
17
|
end
|
|
18
18
|
end
|
|
19
19
|
|
|
20
|
-
def
|
|
21
|
-
return respond_to_with_not_found unless Lato.config.
|
|
20
|
+
def update_authenticator_action
|
|
21
|
+
return respond_to_with_not_found unless Lato.config.authenticator_connection
|
|
22
22
|
|
|
23
|
-
if @session.user.
|
|
23
|
+
if @session.user.authenticator_secret
|
|
24
24
|
respond_to do |format|
|
|
25
|
-
if @session.user.
|
|
25
|
+
if @session.user.remove_authenticator_secret
|
|
26
26
|
format.html { redirect_to lato.account_path }
|
|
27
27
|
format.json { render json: @session.user }
|
|
28
28
|
else
|
|
@@ -30,47 +30,96 @@ module Lato
|
|
|
30
30
|
format.json { render json: @session.user.errors, status: :unprocessable_entity }
|
|
31
31
|
end
|
|
32
32
|
end
|
|
33
|
-
|
|
33
|
+
else
|
|
34
34
|
respond_to do |format|
|
|
35
|
-
if @session.user.
|
|
36
|
-
session[:web3_nonce] = nil
|
|
35
|
+
if @session.user.generate_authenticator_secret
|
|
37
36
|
format.html { redirect_to lato.account_path }
|
|
38
37
|
format.json { render json: @session.user }
|
|
39
38
|
else
|
|
40
|
-
session[:web3_nonce] = nil
|
|
41
39
|
format.html { render :index, status: :unprocessable_entity }
|
|
42
40
|
format.json { render json: @session.user.errors, status: :unprocessable_entity }
|
|
43
41
|
end
|
|
44
42
|
end
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def update_webauthn_action
|
|
47
|
+
return respond_to_with_not_found unless Lato.config.webauthn_connection
|
|
48
|
+
|
|
49
|
+
respond_to do |format|
|
|
50
|
+
command = params.dig(:user, :webauthn_command)
|
|
51
|
+
|
|
52
|
+
case command
|
|
53
|
+
when 'remove'
|
|
54
|
+
if @session.user.remove_webauthn_credential
|
|
55
|
+
reset_webauthn_registration_state
|
|
48
56
|
format.html { redirect_to lato.account_path }
|
|
49
57
|
format.json { render json: @session.user }
|
|
50
58
|
else
|
|
51
59
|
format.html { render :index, status: :unprocessable_entity }
|
|
52
60
|
format.json { render json: @session.user.errors, status: :unprocessable_entity }
|
|
53
61
|
end
|
|
62
|
+
when 'cancel'
|
|
63
|
+
reset_webauthn_registration_state
|
|
64
|
+
format.html { redirect_to lato.account_path }
|
|
65
|
+
format.json { render json: {} }
|
|
66
|
+
when 'complete'
|
|
67
|
+
permitted = params.require(:user).permit(:webauthn_credential)
|
|
68
|
+
|
|
69
|
+
if @session.user.register_webauthn_credential(permitted[:webauthn_credential], session[:webauthn_registration_challenge])
|
|
70
|
+
reset_webauthn_registration_state
|
|
71
|
+
format.html { redirect_to lato.account_path }
|
|
72
|
+
format.json { render json: @session.user }
|
|
73
|
+
else
|
|
74
|
+
reset_webauthn_registration_state
|
|
75
|
+
format.html { render :index, status: :unprocessable_entity }
|
|
76
|
+
format.json { render json: @session.user.errors, status: :unprocessable_entity }
|
|
77
|
+
end
|
|
78
|
+
else
|
|
79
|
+
options = @session.user.webauthn_registration_options
|
|
80
|
+
session[:webauthn_registration_challenge] = Base64.strict_encode64(options.challenge)
|
|
81
|
+
session[:webauthn_registration_options] = options.as_json
|
|
82
|
+
format.html { redirect_to lato.account_path }
|
|
83
|
+
format.json { render json: { options: session[:webauthn_registration_options] } }
|
|
54
84
|
end
|
|
55
85
|
end
|
|
86
|
+
rescue StandardError => e
|
|
87
|
+
Rails.logger.error(e)
|
|
88
|
+
reset_webauthn_registration_state
|
|
89
|
+
respond_to do |format|
|
|
90
|
+
format.html { render :index, status: :unprocessable_entity }
|
|
91
|
+
format.json { render json: { error: :webauthn_unexpected_error }, status: :unprocessable_entity }
|
|
92
|
+
end
|
|
56
93
|
end
|
|
57
94
|
|
|
58
|
-
def
|
|
59
|
-
return respond_to_with_not_found unless Lato.config.
|
|
95
|
+
def update_web3_action
|
|
96
|
+
return respond_to_with_not_found unless Lato.config.web3_connection
|
|
60
97
|
|
|
61
|
-
if @session.user.
|
|
98
|
+
if @session.user.web3_address
|
|
62
99
|
respond_to do |format|
|
|
63
|
-
if @session.user.
|
|
100
|
+
if @session.user.remove_web3_connection
|
|
101
|
+
format.html { redirect_to lato.account_path }
|
|
102
|
+
format.json { render json: @session.user }
|
|
103
|
+
else
|
|
104
|
+
format.html { render :index, status: :unprocessable_entity }
|
|
105
|
+
format.json { render json: @session.user.errors, status: :unprocessable_entity }
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
elsif session[:web3_nonce]
|
|
109
|
+
respond_to do |format|
|
|
110
|
+
if @session.user.add_web3_connection(params.require(:user).permit(:web3_address, :web3_signed_nonce).merge(web3_nonce: session[:web3_nonce]))
|
|
111
|
+
session[:web3_nonce] = nil
|
|
64
112
|
format.html { redirect_to lato.account_path }
|
|
65
113
|
format.json { render json: @session.user }
|
|
66
114
|
else
|
|
115
|
+
session[:web3_nonce] = nil
|
|
67
116
|
format.html { render :index, status: :unprocessable_entity }
|
|
68
117
|
format.json { render json: @session.user.errors, status: :unprocessable_entity }
|
|
69
118
|
end
|
|
70
119
|
end
|
|
71
120
|
else
|
|
72
121
|
respond_to do |format|
|
|
73
|
-
if
|
|
122
|
+
if session[:web3_nonce] = SecureRandom.hex(32)
|
|
74
123
|
format.html { redirect_to lato.account_path }
|
|
75
124
|
format.json { render json: @session.user }
|
|
76
125
|
else
|
|
@@ -142,5 +191,12 @@ module Lato
|
|
|
142
191
|
end
|
|
143
192
|
end
|
|
144
193
|
end
|
|
194
|
+
|
|
195
|
+
private
|
|
196
|
+
|
|
197
|
+
def reset_webauthn_registration_state
|
|
198
|
+
session[:webauthn_registration_challenge] = nil
|
|
199
|
+
session[:webauthn_registration_options] = nil
|
|
200
|
+
end
|
|
145
201
|
end
|
|
146
202
|
end
|
|
@@ -6,11 +6,13 @@ module Lato
|
|
|
6
6
|
|
|
7
7
|
before_action :find_user, only: %i[verify_email verify_email_action update_password update_password_action]
|
|
8
8
|
before_action :find_invitation, only: %i[accept_invitation accept_invitation_action]
|
|
9
|
+
before_action :find_authentication_user, only: %i[authentication_method authentication_method_action webauthn webauthn_action]
|
|
9
10
|
|
|
10
11
|
before_action :lock_signup_if_disabled, only: %i[signup signup_action]
|
|
11
12
|
before_action :lock_recover_password_if_disabled, only: %i[recover_password recover_password_action update_password update_password_action]
|
|
12
13
|
before_action :lock_web3_if_disabled, only: %i[web3_signin web3_signin_action]
|
|
13
14
|
before_action :lock_authenticator_if_disabled, only: %i[authenticator authenticator_action]
|
|
15
|
+
before_action :lock_webauthn_if_disabled, only: %i[webauthn webauthn_action]
|
|
14
16
|
|
|
15
17
|
before_action :hide_sidebar
|
|
16
18
|
|
|
@@ -29,11 +31,13 @@ module Lato
|
|
|
29
31
|
ip_address: request.remote_ip,
|
|
30
32
|
user_agent: request.user_agent
|
|
31
33
|
))
|
|
32
|
-
|
|
33
|
-
|
|
34
|
+
redirect_path = determine_authentication_redirect(@user)
|
|
35
|
+
if redirect_path
|
|
36
|
+
format.html { redirect_to redirect_path }
|
|
34
37
|
format.json { render json: @user }
|
|
35
38
|
else
|
|
36
|
-
|
|
39
|
+
session_create(@user.id)
|
|
40
|
+
format.html { redirect_to lato.root_path }
|
|
37
41
|
format.json { render json: @user }
|
|
38
42
|
end
|
|
39
43
|
else
|
|
@@ -58,11 +62,13 @@ module Lato
|
|
|
58
62
|
web3_nonce: session[:web3_nonce]
|
|
59
63
|
))
|
|
60
64
|
session[:web3_nonce] = nil
|
|
61
|
-
|
|
62
|
-
|
|
65
|
+
redirect_path = determine_authentication_redirect(@user)
|
|
66
|
+
if redirect_path
|
|
67
|
+
format.html { redirect_to redirect_path }
|
|
63
68
|
format.json { render json: @user }
|
|
64
69
|
else
|
|
65
|
-
|
|
70
|
+
session_create(@user.id)
|
|
71
|
+
format.html { redirect_to lato.root_path }
|
|
66
72
|
format.json { render json: @user }
|
|
67
73
|
end
|
|
68
74
|
else
|
|
@@ -191,20 +197,45 @@ module Lato
|
|
|
191
197
|
end
|
|
192
198
|
end
|
|
193
199
|
|
|
200
|
+
# Authentication method choice
|
|
201
|
+
##
|
|
202
|
+
|
|
203
|
+
def authentication_method; end
|
|
204
|
+
|
|
205
|
+
def authentication_method_action
|
|
206
|
+
method = params[:method]
|
|
207
|
+
|
|
208
|
+
respond_to do |format|
|
|
209
|
+
case method
|
|
210
|
+
when 'authenticator'
|
|
211
|
+
session[:authentication_method] = 'authenticator'
|
|
212
|
+
format.html { redirect_to lato.authentication_authenticator_path }
|
|
213
|
+
format.json { render json: { redirect: lato.authentication_authenticator_path } }
|
|
214
|
+
when 'webauthn'
|
|
215
|
+
session[:authentication_method] = 'webauthn'
|
|
216
|
+
format.html { redirect_to lato.authentication_webauthn_path }
|
|
217
|
+
format.json { render json: { redirect: lato.authentication_webauthn_path } }
|
|
218
|
+
else
|
|
219
|
+
format.html { redirect_to lato.authentication_signin_path }
|
|
220
|
+
format.json { render json: { error: 'Invalid method' }, status: :unprocessable_entity }
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
|
|
194
225
|
# Authenticator
|
|
195
226
|
##
|
|
196
227
|
|
|
197
228
|
def authenticator
|
|
198
|
-
@user = Lato::User.find_by_id(session[:
|
|
229
|
+
@user = Lato::User.find_by_id(session[:authentication_user_id])
|
|
199
230
|
return respond_to_with_not_found unless @user
|
|
200
231
|
end
|
|
201
232
|
|
|
202
233
|
def authenticator_action
|
|
203
|
-
@user = Lato::User.find_by_id(session[:
|
|
234
|
+
@user = Lato::User.find_by_id(session[:authentication_user_id])
|
|
204
235
|
|
|
205
236
|
respond_to do |format|
|
|
206
237
|
if @user.authenticator(params.require(:user).permit(:authenticator_code))
|
|
207
|
-
|
|
238
|
+
clear_authentication_session
|
|
208
239
|
session_create(@user.id)
|
|
209
240
|
|
|
210
241
|
format.html { redirect_to lato.root_path }
|
|
@@ -216,6 +247,31 @@ module Lato
|
|
|
216
247
|
end
|
|
217
248
|
end
|
|
218
249
|
|
|
250
|
+
# WebAuthn
|
|
251
|
+
##
|
|
252
|
+
|
|
253
|
+
def webauthn
|
|
254
|
+
@options = @user.webauthn_authentication_options
|
|
255
|
+
session[:webauthn_challenge] = @options.challenge
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
def webauthn_action
|
|
259
|
+
respond_to do |format|
|
|
260
|
+
if @user.webauthn_authentication(params.require(:user).permit(:webauthn_credential), session[:webauthn_challenge])
|
|
261
|
+
clear_authentication_session
|
|
262
|
+
session_create(@user.id)
|
|
263
|
+
|
|
264
|
+
format.html { redirect_to lato.root_path }
|
|
265
|
+
format.json { render json: @user }
|
|
266
|
+
else
|
|
267
|
+
@options = @user.webauthn_authentication_options
|
|
268
|
+
session[:webauthn_challenge] = @options.challenge
|
|
269
|
+
format.html { render :webauthn, status: :unprocessable_entity }
|
|
270
|
+
format.json { render json: @user.errors, status: :unprocessable_entity }
|
|
271
|
+
end
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
|
|
219
275
|
private
|
|
220
276
|
|
|
221
277
|
def registration_params
|
|
@@ -232,14 +288,32 @@ module Lato
|
|
|
232
288
|
respond_to_with_not_found unless @invitation
|
|
233
289
|
end
|
|
234
290
|
|
|
235
|
-
def
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
291
|
+
def find_authentication_user
|
|
292
|
+
@user = Lato::User.find_by_id(session[:authentication_user_id])
|
|
293
|
+
respond_to_with_not_found unless @user
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
def determine_authentication_redirect(user)
|
|
297
|
+
authenticator_enabled = Lato.config.authenticator_connection && !Lato.config.auth_disable_authenticator && user.authenticator_enabled?
|
|
298
|
+
webauthn_enabled = Lato.config.webauthn_connection && !Lato.config.auth_disable_webauthn && user.webauthn_enabled?
|
|
299
|
+
|
|
300
|
+
return nil unless authenticator_enabled || webauthn_enabled
|
|
301
|
+
|
|
302
|
+
session[:authentication_user_id] = user.id
|
|
303
|
+
|
|
304
|
+
if authenticator_enabled && webauthn_enabled
|
|
305
|
+
lato.authentication_authentication_method_path
|
|
306
|
+
elsif authenticator_enabled
|
|
307
|
+
lato.authentication_authenticator_path
|
|
308
|
+
elsif webauthn_enabled
|
|
309
|
+
lato.authentication_webauthn_path
|
|
239
310
|
end
|
|
311
|
+
end
|
|
240
312
|
|
|
241
|
-
|
|
242
|
-
|
|
313
|
+
def clear_authentication_session
|
|
314
|
+
session[:authentication_user_id] = nil
|
|
315
|
+
session[:authentication_method] = nil
|
|
316
|
+
session[:webauthn_challenge] = nil
|
|
243
317
|
end
|
|
244
318
|
|
|
245
319
|
def lock_signup_if_disabled
|
|
@@ -267,6 +341,12 @@ module Lato
|
|
|
267
341
|
respond_to_with_not_found
|
|
268
342
|
end
|
|
269
343
|
|
|
344
|
+
def lock_webauthn_if_disabled
|
|
345
|
+
return if Lato.config.webauthn_connection && !Lato.config.auth_disable_webauthn
|
|
346
|
+
|
|
347
|
+
respond_to_with_not_found
|
|
348
|
+
end
|
|
349
|
+
|
|
270
350
|
def verify_hcaptcha(render_key)
|
|
271
351
|
return true unless Lato.config.hcaptcha_site_key && Lato.config.hcaptcha_secret
|
|
272
352
|
|