Clean up concepts of enabled account state

1) Rename "active" methods to be "enabled," since they aren't
   really about "activity."

2) Make authentication fail if a device or account is in dissabled
   state.

3) Let some controllers authenticate accounts that are in a
   disabled state.
This commit is contained in:
Moxie Marlinspike
2019-05-04 12:31:50 -07:00
parent fe66a59618
commit 35116f9229
36 changed files with 570 additions and 231 deletions

View File

@@ -16,79 +16,23 @@
*/
package org.whispersystems.textsecuregcm.auth;
import com.codahale.metrics.Meter;
import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.SharedMetricRegistries;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.Device;
import org.whispersystems.textsecuregcm.util.Constants;
import org.whispersystems.textsecuregcm.util.Util;
import java.util.Optional;
import static com.codahale.metrics.MetricRegistry.name;
import io.dropwizard.auth.AuthenticationException;
import io.dropwizard.auth.Authenticator;
import io.dropwizard.auth.basic.BasicCredentials;
public class AccountAuthenticator implements Authenticator<BasicCredentials, Account> {
private final MetricRegistry metricRegistry = SharedMetricRegistries.getOrCreate(Constants.METRICS_NAME);
private final Meter authenticationFailedMeter = metricRegistry.meter(name(getClass(), "authentication", "failed" ));
private final Meter authenticationSucceededMeter = metricRegistry.meter(name(getClass(), "authentication", "succeeded"));
private final Logger logger = LoggerFactory.getLogger(AccountAuthenticator.class);
private final AccountsManager accountsManager;
public class AccountAuthenticator extends BaseAccountAuthenticator implements Authenticator<BasicCredentials, Account> {
public AccountAuthenticator(AccountsManager accountsManager) {
this.accountsManager = accountsManager;
super(accountsManager);
}
@Override
public Optional<Account> authenticate(BasicCredentials basicCredentials)
throws AuthenticationException
{
try {
AuthorizationHeader authorizationHeader = AuthorizationHeader.fromUserAndPassword(basicCredentials.getUsername(), basicCredentials.getPassword());
Optional<Account> account = accountsManager.get(authorizationHeader.getNumber());
if (!account.isPresent()) {
return Optional.empty();
}
Optional<Device> device = account.get().getDevice(authorizationHeader.getDeviceId());
if (!device.isPresent()) {
return Optional.empty();
}
if (!device.get().isMaster() && device.get().isIdleInactive()) {
return Optional.empty();
}
if (device.get().getAuthenticationCredentials().verify(basicCredentials.getPassword())) {
authenticationSucceededMeter.mark();
account.get().setAuthenticatedDevice(device.get());
updateLastSeen(account.get(), device.get());
return account;
}
authenticationFailedMeter.mark();
return Optional.empty();
} catch (InvalidAuthorizationHeaderException iahe) {
return Optional.empty();
}
}
private void updateLastSeen(Account account, Device device) {
if (device.getLastSeen() != Util.todayInMillis()) {
device.setLastSeen(Util.todayInMillis());
accountsManager.update(account);
}
public Optional<Account> authenticate(BasicCredentials basicCredentials) {
return super.authenticate(basicCredentials, true);
}
}

View File

@@ -0,0 +1,89 @@
package org.whispersystems.textsecuregcm.auth;
import com.codahale.metrics.Meter;
import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.SharedMetricRegistries;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.Device;
import org.whispersystems.textsecuregcm.util.Constants;
import org.whispersystems.textsecuregcm.util.Util;
import java.util.Optional;
import static com.codahale.metrics.MetricRegistry.name;
import io.dropwizard.auth.basic.BasicCredentials;
public class BaseAccountAuthenticator {
private final MetricRegistry metricRegistry = SharedMetricRegistries.getOrCreate(Constants.METRICS_NAME);
private final Meter authenticationFailedMeter = metricRegistry.meter(name(getClass(), "authentication", "failed" ));
private final Meter authenticationSucceededMeter = metricRegistry.meter(name(getClass(), "authentication", "succeeded" ));
private final Meter noSuchAccountMeter = metricRegistry.meter(name(getClass(), "authentication", "noSuchAccount" ));
private final Meter noSuchDeviceMeter = metricRegistry.meter(name(getClass(), "authentication", "noSuchDevice" ));
private final Meter accountDisabledMeter = metricRegistry.meter(name(getClass(), "authentication", "accountDisabled"));
private final Meter deviceDisabledMeter = metricRegistry.meter(name(getClass(), "authentication", "deviceDisabled" ));
private final Meter invalidAuthHeaderMeter = metricRegistry.meter(name(getClass(), "authentication", "invalidHeader" ));
private final Logger logger = LoggerFactory.getLogger(AccountAuthenticator.class);
private final AccountsManager accountsManager;
public BaseAccountAuthenticator(AccountsManager accountsManager) {
this.accountsManager = accountsManager;
}
public Optional<Account> authenticate(BasicCredentials basicCredentials, boolean enabledRequired) {
try {
AuthorizationHeader authorizationHeader = AuthorizationHeader.fromUserAndPassword(basicCredentials.getUsername(), basicCredentials.getPassword());
Optional<Account> account = accountsManager.get(authorizationHeader.getNumber());
if (!account.isPresent()) {
noSuchAccountMeter.mark();
return Optional.empty();
}
Optional<Device> device = account.get().getDevice(authorizationHeader.getDeviceId());
if (!device.isPresent()) {
noSuchDeviceMeter.mark();
return Optional.empty();
}
if (enabledRequired) {
if (!device.get().isEnabled()) {
deviceDisabledMeter.mark();
return Optional.empty();
}
if (!account.get().isEnabled()) {
accountDisabledMeter.mark();
return Optional.empty();
}
}
if (device.get().getAuthenticationCredentials().verify(basicCredentials.getPassword())) {
authenticationSucceededMeter.mark();
account.get().setAuthenticatedDevice(device.get());
updateLastSeen(account.get(), device.get());
return account;
}
authenticationFailedMeter.mark();
return Optional.empty();
} catch (InvalidAuthorizationHeaderException iahe) {
invalidAuthHeaderMeter.mark();
return Optional.empty();
}
}
private void updateLastSeen(Account account, Device device) {
if (device.getLastSeen() != Util.todayInMillis()) {
device.setLastSeen(Util.todayInMillis());
accountsManager.update(account);
}
}
}

View File

@@ -0,0 +1,31 @@
package org.whispersystems.textsecuregcm.auth;
import org.whispersystems.textsecuregcm.storage.Account;
import javax.security.auth.Subject;
import java.security.Principal;
public class DisabledPermittedAccount implements Principal {
private final Account account;
public DisabledPermittedAccount(Account account) {
this.account = account;
}
public Account getAccount() {
return account;
}
// Principal implementation
@Override
public String getName() {
return null;
}
@Override
public boolean implies(Subject subject) {
return false;
}
}

View File

@@ -0,0 +1,22 @@
package org.whispersystems.textsecuregcm.auth;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.AccountsManager;
import java.util.Optional;
import io.dropwizard.auth.Authenticator;
import io.dropwizard.auth.basic.BasicCredentials;
public class DisabledPermittedAccountAuthenticator extends BaseAccountAuthenticator implements Authenticator<BasicCredentials, DisabledPermittedAccount> {
public DisabledPermittedAccountAuthenticator(AccountsManager accountsManager) {
super(accountsManager);
}
@Override
public Optional<DisabledPermittedAccount> authenticate(BasicCredentials credentials) {
Optional<Account> account = super.authenticate(credentials, false);
return account.map(DisabledPermittedAccount::new);
}
}

View File

@@ -2,7 +2,6 @@ package org.whispersystems.textsecuregcm.auth;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.Device;
import org.whispersystems.textsecuregcm.util.Hex;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Response;
@@ -27,7 +26,7 @@ public class OptionalAccess {
Optional<Device> targetDevice = targetAccount.get().getDevice(deviceId);
if (targetDevice.isPresent() && targetDevice.get().isActive()) {
if (targetDevice.isPresent() && targetDevice.get().isEnabled()) {
return;
}
@@ -46,23 +45,23 @@ public class OptionalAccess {
Optional<Anonymous> accessKey,
Optional<Account> targetAccount)
{
if (requestAccount.isPresent() && targetAccount.isPresent() && targetAccount.get().isActive()) {
if (requestAccount.isPresent() && targetAccount.isPresent() && targetAccount.get().isEnabled()) {
return;
}
//noinspection ConstantConditions
if (requestAccount.isPresent() && (!targetAccount.isPresent() || (targetAccount.isPresent() && !targetAccount.get().isActive()))) {
if (requestAccount.isPresent() && (!targetAccount.isPresent() || (targetAccount.isPresent() && !targetAccount.get().isEnabled()))) {
throw new WebApplicationException(Response.Status.NOT_FOUND);
}
if (accessKey.isPresent() && targetAccount.isPresent() && targetAccount.get().isActive() && targetAccount.get().isUnrestrictedUnidentifiedAccess()) {
if (accessKey.isPresent() && targetAccount.isPresent() && targetAccount.get().isEnabled() && targetAccount.get().isUnrestrictedUnidentifiedAccess()) {
return;
}
if (accessKey.isPresent() &&
targetAccount.isPresent() &&
targetAccount.get().getUnidentifiedAccessKey().isPresent() &&
targetAccount.get().isActive() &&
targetAccount.get().isEnabled() &&
MessageDigest.isEqual(accessKey.get().getAccessKey(), targetAccount.get().getUnidentifiedAccessKey().get()))
{
return;