Skip to content

Commit

Permalink
NAS-133870 / 25.10 / Give proper error response to STIG auth (#15525)
Browse files Browse the repository at this point in the history
We need to reply with a generic AUTH_ERR response rather than
a CallError on authentication failure in STIG mode. We can't
provide more details due to security considerations, but we
can give a useful log message for admins.
  • Loading branch information
anodos325 authored Feb 3, 2025
1 parent 95cf397 commit c29058c
Show file tree
Hide file tree
Showing 3 changed files with 54 additions and 25 deletions.
31 changes: 27 additions & 4 deletions src/middlewared/middlewared/plugins/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -717,14 +717,37 @@ async def login_ex(self, app, data):
elif resp['otpw_used']:
cred_type = 'ONETIME_PASSWORD'
elif CURRENT_AAL.level.otp_mandatory:
# If we're here it means either:
#
# 1) correct username and password, but 2FA isn't enabled for user
# or
# 2) bad username or password
#
# We must not include information in response to indicate which case
# the situation is because it would divulge privileged information about
# the account to an unauthenticated user.
if resp['pam_response'] == 'SUCCESS':
# Insert a failure delay so that we don't leak information about
# the PAM response
await asyncio.sleep(random.uniform(1, 2))
raise CallError(
'Two-factor authentication is requried at the current authenticator level.',
errno.EOPNOTSUPP
)
await self.middleware.log_audit_message(app, 'AUTHENTICATION', {
'credentials': {
'credentials': cred_type,
'credentials_data': {'username': data['username']},
},
'error': 'User does not have two factor authentication enabled.'
}, False)

else:
await self.middleware.log_audit_message(app, 'AUTHENTICATION', {
'credentials': {
'credentials': cred_type,
'credentials_data': {'username': data['username']},
},
'error': 'Bad username or password'
}, False)

return response

match resp['pam_response']['code']:
case pam.PAM_SUCCESS:
Expand Down
35 changes: 19 additions & 16 deletions tests/api2/test_authenticator_assurance_level.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from middlewared.service_exception import CallError
from middlewared.test.integration.assets.two_factor_auth import enabled_twofactor_auth, get_user_secret, get_2fa_totp_token
from middlewared.test.integration.assets.api_key import api_key
from middlewared.test.integration.utils import client, call
from middlewared.test.integration.utils import client, call, password


@contextmanager
Expand Down Expand Up @@ -42,13 +42,13 @@ def test_mechanism_choices(level, expected):

def test_level2_api_key_plain():
""" API_KEY_PLAIN lacks replay resistance
and so authentication attempts must fail with EOPNOTSUPP
and so authentication attempts must fail with AUTH_ERR
"""
with authenticator_assurance_level('LEVEL_2'):
with api_key() as key:
with client(auth=None) as c:
with pytest.raises(CallError) as ce:
c.call('auth.login_ex', {
call('auth.login_ex', {
'mechanism': 'API_KEY_PLAIN',
'username': 'root',
'api_key': key
Expand All @@ -59,14 +59,18 @@ def test_level2_api_key_plain():

def test_level2_password_plain_no_twofactor():
""" PASSWORD_PLAIN lacks replay resistance
and so authentication attempts must fail with EOPNOTSUPP
and so authentication attempts must fail with AUTH_ERR
"""
with authenticator_assurance_level('LEVEL_2'):
with pytest.raises(CallError) as ce:
with client():
pass
with client(auth=None) as c:
resp = c.call('auth.login_ex', {
'mechanism': 'PASSWORD_PLAIN',
'username': 'root',
'password': password()

assert ce.value.errno == errno.EOPNOTSUPP
})

assert resp['response_type'] == 'AUTH_ERR'


def test_level2_password_with_otp(sharing_admin_user):
Expand Down Expand Up @@ -120,13 +124,12 @@ def test_level2_onetime_password(sharing_admin_user):
assert 'SHARING_ADMIN' in me['privilege']['roles']
assert 'OTPW' in me['account_attributes']

# attempt to reuse the onetime password should fail with EOPNOTSUPP
# attempt to reuse the onetime password should fail with AUTH_ERR
# because we don't want to leak info about onetime password status.
with pytest.raises(CallError) as ce:
c.call('auth.login_ex', {
'mechanism': 'PASSWORD_PLAIN',
'username': sharing_admin_user.username,
'password': onetime_password
})
resp = c.call('auth.login_ex', {
'mechanism': 'PASSWORD_PLAIN',
'username': sharing_admin_user.username,
'password': onetime_password
})

assert ce.value.errno == errno.EOPNOTSUPP
assert resp['response_type'] == 'AUTH_ERR'
13 changes: 8 additions & 5 deletions tests/api2/test_stig.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from middlewared.test.integration.assets.two_factor_auth import (
enabled_twofactor_auth, get_user_secret, get_2fa_totp_token
)
from middlewared.test.integration.utils import call, client
from middlewared.test.integration.utils import call, client, password
from truenas_api_client import ValidationErrors


Expand Down Expand Up @@ -155,11 +155,14 @@ def test_stig_enabled_authenticator_assurance_level(setup_stig, clear_ratelimit)
setup_stig['connection'].call('system.info')

# Auth for account without 2fa should fail
with pytest.raises(CallError) as ce:
with client():
pass
with client(auth=None) as c:
resp = c.call('auth.login_ex', {
'mechanism': 'PASSWORD_PLAIN',
'username': 'root',
'password': password()
})

assert ce.value.errno == errno.EOPNOTSUPP
assert resp['response_type'] == 'AUTH_ERR'

# We should also be able to create a new websocket connection
# The previous one was created before enabling STIG
Expand Down

0 comments on commit c29058c

Please sign in to comment.