FEATURE: add ability to have multiple totp factors (#7626)

Adds a second factor landing page that centralizes a user's second factor configuration.

This contains both TOTP and Backup, and also allows multiple TOTP tokens to be registered and organized by a name. Access to this page is authenticated via password, and cached for 30 minutes via a secure session.
This commit is contained in:
Jeff Wong
2019-06-26 16:58:06 -07:00
committed by GitHub
parent b2a033e92b
commit 88ef5e55fe
25 changed files with 793 additions and 549 deletions

View File

@@ -3163,7 +3163,7 @@ describe UsersController do
end
end
describe '#create_second_factor' do
describe '#create_second_factor_totp' do
context 'when not logged in' do
it 'should return the right response' do
post "/users/second_factors.json", params: {
@@ -3181,24 +3181,17 @@ describe UsersController do
describe 'create 2fa request' do
it 'fails on incorrect password' do
post "/users/second_factors.json", params: {
password: 'wrongpassword'
}
ApplicationController.any_instance.expects(:secure_session).returns("confirmed-password-#{user.id}" => "false")
post "/users/create_second_factor_totp.json"
expect(response.status).to eq(200)
expect(JSON.parse(response.body)['error']).to eq(I18n.t(
"login.incorrect_password")
)
expect(response.status).to eq(403)
end
describe 'when local logins are disabled' do
it 'should return the right response' do
SiteSetting.enable_local_logins = false
post "/users/second_factors.json", params: {
password: 'myawesomepassword'
}
post "/users/create_second_factor_totp.json"
expect(response.status).to eq(404)
end
@@ -3209,30 +3202,22 @@ describe UsersController do
SiteSetting.sso_url = 'http://someurl.com'
SiteSetting.enable_sso = true
post "/users/second_factors.json", params: {
password: 'myawesomepassword'
}
post "/users/create_second_factor_totp.json"
expect(response.status).to eq(404)
end
end
it 'succeeds on correct password' do
user.create_totp
user.user_second_factors.totp.update!(data: "abcdefghijklmnop")
post "/users/second_factors.json", params: {
password: 'myawesomepassword'
}
session = {}
ApplicationController.any_instance.stubs(:secure_session).returns("confirmed-password-#{user.id}" => "true")
post "/users/create_second_factor_totp.json"
expect(response.status).to eq(200)
response_body = JSON.parse(response.body)
expect(response_body['key']).to eq(
"abcd efgh ijkl mnop"
)
expect(response_body['key']).to be_present
expect(response_body['qr']).to be_present
end
end
@@ -3244,10 +3229,7 @@ describe UsersController do
context 'when not logged in' do
it 'should return the right response' do
put "/users/second_factor.json", params: {
second_factor_token: ROTP::TOTP.new(user_second_factor.data).now,
second_factor_method: UserSecondFactor.methods[:totp]
}
put "/users/second_factor.json"
expect(response.status).to eq(403)
end
@@ -3263,52 +3245,39 @@ describe UsersController do
context 'when token is missing' do
it 'returns the right response' do
put "/users/second_factor.json", params: {
enable: 'true',
}
expect(response.status).to eq(400)
end
end
context 'when token is invalid' do
it 'returns the right response' do
put "/users/second_factor.json", params: {
second_factor_token: '000000',
second_factor_method: UserSecondFactor.methods[:totp],
disable: 'true',
second_factor_target: UserSecondFactor.methods[:totp],
enable: 'true',
id: user_second_factor.id
}
expect(response.status).to eq(200)
expect(JSON.parse(response.body)['error']).to eq(I18n.t(
"login.invalid_second_factor_code"
))
expect(response.status).to eq(403)
end
end
context 'when token is valid' do
it 'should allow second factor for the user to be enabled' do
before do
ApplicationController.any_instance.stubs(:secure_session).returns("confirmed-password-#{user.id}" => "true")
end
it 'should allow second factor for the user to be renamed' do
put "/users/second_factor.json", params: {
second_factor_token: ROTP::TOTP.new(user_second_factor.data).now,
second_factor_method: UserSecondFactor.methods[:totp],
second_factor_target: UserSecondFactor.methods[:totp],
enable: 'true'
}
name: 'renamed',
second_factor_target: UserSecondFactor.methods[:totp],
id: user_second_factor.id
}
expect(response.status).to eq(200)
expect(user.reload.user_second_factors.totp.enabled).to be true
expect(user.reload.user_second_factors.totps.first.name).to eq("renamed")
end
it 'should allow second factor for the user to be disabled' do
put "/users/second_factor.json", params: {
second_factor_token: ROTP::TOTP.new(user_second_factor.data).now,
second_factor_method: UserSecondFactor.methods[:totp],
second_factor_target: UserSecondFactor.methods[:totp]
disable: 'true',
second_factor_target: UserSecondFactor.methods[:totp],
id: user_second_factor.id
}
expect(response.status).to eq(200)
expect(user.reload.user_second_factors.totp).to eq(nil)
expect(user.reload.user_second_factors.totps.first).to eq(nil)
end
end
end
@@ -3320,32 +3289,18 @@ describe UsersController do
second_factor_target: UserSecondFactor.methods[:backup_codes]
}
expect(response.status).to eq(400)
end
end
context 'when token is invalid' do
it 'returns the right response' do
put "/users/second_factor.json", params: {
second_factor_token: '000000',
second_factor_method: UserSecondFactor.methods[:totp],
second_factor_target: UserSecondFactor.methods[:backup_codes]
}
expect(response.status).to eq(200)
expect(JSON.parse(response.body)['error']).to eq(I18n.t(
"login.invalid_second_factor_code"
))
expect(response.status).to eq(403)
end
end
context 'when token is valid' do
before do
ApplicationController.any_instance.stubs(:secure_session).returns("confirmed-password-#{user.id}" => "true")
end
it 'should allow second factor backup for the user to be disabled' do
put "/users/second_factor.json", params: {
second_factor_token: ROTP::TOTP.new(user_second_factor.data).now,
second_factor_method: UserSecondFactor.methods[:totp],
second_factor_target: UserSecondFactor.methods[:backup_codes]
second_factor_target: UserSecondFactor.methods[:backup_codes],
disable: 'true'
}
expect(response.status).to eq(200)
@@ -3377,26 +3332,17 @@ describe UsersController do
describe 'create 2fa request' do
it 'fails on incorrect password' do
put "/users/second_factors_backup.json", params: {
second_factor_token: 'wrongtoken',
second_factor_method: UserSecondFactor.methods[:totp]
}
ApplicationController.any_instance.expects(:secure_session).returns("confirmed-password-#{user.id}" => "false")
put "/users/second_factors_backup.json"
expect(response.status).to eq(200)
expect(JSON.parse(response.body)['error']).to eq(I18n.t(
"login.invalid_second_factor_code")
)
expect(response.status).to eq(403)
end
describe 'when local logins are disabled' do
it 'should return the right response' do
SiteSetting.enable_local_logins = false
put "/users/second_factors_backup.json", params: {
second_factor_token: ROTP::TOTP.new(user_second_factor.data).now,
second_factor_method: UserSecondFactor.methods[:totp]
}
put "/users/second_factors_backup.json"
expect(response.status).to eq(404)
end
@@ -3407,22 +3353,16 @@ describe UsersController do
SiteSetting.sso_url = 'http://someurl.com'
SiteSetting.enable_sso = true
put "/users/second_factors_backup.json", params: {
second_factor_token: ROTP::TOTP.new(user_second_factor.data).now,
second_factor_method: UserSecondFactor.methods[:totp]
}
put "/users/second_factors_backup.json"
expect(response.status).to eq(404)
end
end
it 'succeeds on correct password' do
user_second_factor
ApplicationController.any_instance.expects(:secure_session).returns("confirmed-password-#{user.id}" => "true")
put "/users/second_factors_backup.json", params: {
second_factor_token: ROTP::TOTP.new(user_second_factor.data).now,
second_factor_method: UserSecondFactor.methods[:totp]
}
put "/users/second_factors_backup.json"
expect(response.status).to eq(200)