lato 3.15.1 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 07e7c2b65e57a4ff8115e3d5e7c254f12ab0d908cd23fd32530d585152cb50c6
4
- data.tar.gz: c7324140d43603b341b90f5732d66be231e92fb6b204b754f60a16307b941fb3
3
+ metadata.gz: 693630b5ecdde3bd76143d39982e7313c266188c30a1dbbd41b482a56db541c7
4
+ data.tar.gz: 77c4a9d2ae92e3669f1936590cc8b2bf5799ccfea9eccd904965d9c51cb26ffa
5
5
  SHA512:
6
- metadata.gz: a440afa26b858e547df623c6c8883f174be87daa43a54ae29045509a84268cd42930c676200e44380873c1c1f0dea8fb7e052e057a47cb65c291561fca3957b8
7
- data.tar.gz: 61be2539e9b6136858c05a57187134668170b0ee4ffa24c2c54820aa453e8080cc929fc8a39c627f9d3a56ca3b90323ca1e351c99f062cd557527c80edbee617
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,74 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ export default class extends Controller {
4
+ static targets = ["list", "listItem", "template"]
5
+
6
+ addItem() {
7
+ const template = this.templateTarget
8
+ const nextIndex = this.getNextIndex()
9
+ const newItem = template.content.cloneNode(true)
10
+ newItem.querySelector('.template').setAttribute('data-lato-input-list-target', 'listItem')
11
+ newItem.querySelector('.template').setAttribute('data-index', nextIndex)
12
+ newItem.querySelector('.template').classList.remove('template')
13
+
14
+ this.updateFieldNames(newItem, nextIndex)
15
+ this.listTarget.appendChild(newItem)
16
+ }
17
+
18
+ removeItem(event) {
19
+ const item = event.target.closest('[data-lato-input-list-target="listItem"]')
20
+ if (!item) return
21
+
22
+ const existingDestroyField = item.querySelector('input[type="hidden"][name*="[_destroy]"]')
23
+ if (existingDestroyField) return
24
+
25
+ const hiddenIdField = item.querySelector('input[type="hidden"][name*="[id]"]')
26
+ if (hiddenIdField && hiddenIdField.value) {
27
+ const destroyField = document.createElement('input')
28
+ destroyField.type = 'hidden'
29
+ destroyField.name = hiddenIdField.name.replace('[id]', '[_destroy]')
30
+ destroyField.value = '1'
31
+ item.appendChild(destroyField)
32
+
33
+ item.style.display = 'none'
34
+ item.classList.add('d-none')
35
+
36
+ const inputs = item.querySelectorAll('input, select, textarea')
37
+ inputs.forEach(input => {
38
+ if (input.type !== 'hidden') input.disabled = true
39
+ })
40
+ } else {
41
+ item.remove()
42
+ }
43
+ }
44
+
45
+ updateFieldNames(element, index) {
46
+ const inputs = element.querySelectorAll('input, select, textarea')
47
+ const labels = element.querySelectorAll('label')
48
+
49
+ inputs.forEach(input => {
50
+ if (input.name) {
51
+ input.name = input.name.replace('0', index)
52
+ }
53
+
54
+ if (input.id) {
55
+ input.id = input.id.replace('0', index)
56
+ }
57
+ })
58
+
59
+ labels.forEach(label => {
60
+ if (label.getAttribute('for')) {
61
+ label.setAttribute('for', label.getAttribute('for').replace('0', index))
62
+ }
63
+ })
64
+ }
65
+
66
+ getNextIndex() {
67
+ const itemsIndicies = Array.from(this.listItemTargets).map(item => {
68
+ const indexAttr = item.getAttribute('data-index')
69
+ return indexAttr ? parseInt(indexAttr, 10) : -1
70
+ })
71
+ const maxIndex = itemsIndicies.length > 0 ? Math.max(...itemsIndicies) : -1
72
+ return maxIndex + 1
73
+ }
74
+ }
@@ -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 update_web3_action
21
- return respond_to_with_not_found unless Lato.config.web3_connection
20
+ def update_authenticator_action
21
+ return respond_to_with_not_found unless Lato.config.authenticator_connection
22
22
 
23
- if @session.user.web3_address
23
+ if @session.user.authenticator_secret
24
24
  respond_to do |format|
25
- if @session.user.remove_web3_connection
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
- elsif session[:web3_nonce]
33
+ else
34
34
  respond_to do |format|
35
- if @session.user.add_web3_connection(params.require(:user).permit(:web3_address, :web3_signed_nonce).merge(web3_nonce: session[:web3_nonce]))
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
- else
46
- respond_to do |format|
47
- if session[:web3_nonce] = SecureRandom.hex(32)
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 update_authenticator_action
59
- return respond_to_with_not_found unless Lato.config.authenticator_connection
95
+ def update_web3_action
96
+ return respond_to_with_not_found unless Lato.config.web3_connection
60
97
 
61
- if @session.user.authenticator_secret
98
+ if @session.user.web3_address
62
99
  respond_to do |format|
63
- if @session.user.remove_authenticator_secret
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 @session.user.generate_authenticator_secret
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
- if create_session_or_start_authenticator(@user)
33
- format.html { redirect_to lato.root_path }
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
- format.html { redirect_to lato.authentication_authenticator_path }
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
- if create_session_or_start_authenticator(@user)
62
- format.html { redirect_to lato.root_path }
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
- format.html { redirect_to lato.authentication_authenticator_path }
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[:authenticator_user_id])
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[:authenticator_user_id])
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
- session[:authenticator_user_id] = nil
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 create_session_or_start_authenticator(user)
236
- if !Lato.config.authenticator_connection || Lato.config.auth_disable_authenticator || !user.authenticator_enabled?
237
- session_create(user.id)
238
- return true
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
- session[:authenticator_user_id] = user.id
242
- false
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
 
@@ -240,6 +240,10 @@ module Lato
240
240
  form.color_field key, options
241
241
  end
242
242
 
243
+ def lato_form_item_input_list(form, key, partial, partial_params = {})
244
+ render 'lato/components/form_item_input_list', form: form, key: key, partial: partial, partial_params: partial_params
245
+ end
246
+
243
247
  def lato_form_submit(form, label, options = {})
244
248
  options[:class] ||= []
245
249
  options[:class].push('btn')