// Copyright 2017 The Nomulus Authors. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package google.registry.flows.session; import static com.google.common.collect.Sets.difference; import static google.registry.util.CollectionUtils.nullToEmpty; import com.google.common.base.Optional; import com.google.common.collect.ImmutableSet; import google.registry.flows.EppException; import google.registry.flows.EppException.AuthenticationErrorClosingConnectionException; import google.registry.flows.EppException.AuthenticationErrorException; import google.registry.flows.EppException.AuthorizationErrorException; import google.registry.flows.EppException.CommandUseErrorException; import google.registry.flows.EppException.ParameterValuePolicyErrorException; import google.registry.flows.EppException.UnimplementedExtensionException; import google.registry.flows.EppException.UnimplementedObjectServiceException; import google.registry.flows.EppException.UnimplementedOptionException; import google.registry.flows.ExtensionManager; import google.registry.flows.Flow; import google.registry.flows.FlowModule.ClientId; import google.registry.flows.SessionMetadata; import google.registry.flows.TransportCredentials; import google.registry.model.eppcommon.ProtocolDefinition; import google.registry.model.eppcommon.ProtocolDefinition.ServiceExtension; import google.registry.model.eppinput.EppInput; import google.registry.model.eppinput.EppInput.Login; import google.registry.model.eppinput.EppInput.Options; import google.registry.model.eppinput.EppInput.Services; import google.registry.model.eppoutput.EppResponse; import google.registry.model.registrar.Registrar; import google.registry.util.FormattingLogger; import java.util.Set; import javax.inject.Inject; /** * An EPP flow for login. * * @error {@link google.registry.flows.EppException.UnimplementedExtensionException} * @error {@link google.registry.flows.EppException.UnimplementedObjectServiceException} * @error {@link google.registry.flows.EppException.UnimplementedProtocolVersionException} * @error {@link google.registry.flows.GaeUserCredentials.BadGaeUserIdException} * @error {@link google.registry.flows.GaeUserCredentials.UserNotLoggedInException} * @error {@link google.registry.flows.TlsCredentials.BadRegistrarCertificateException} * @error {@link google.registry.flows.TlsCredentials.BadRegistrarIpAddressException} * @error {@link google.registry.flows.TlsCredentials.MissingRegistrarCertificateException} * @error {@link google.registry.flows.TlsCredentials.NoSniException} * @error {@link google.registry.flows.TransportCredentials.BadRegistrarPasswordException} * @error {@link LoginFlow.AlreadyLoggedInException} * @error {@link LoginFlow.BadRegistrarClientIdException} * @error {@link LoginFlow.TooManyFailedLoginsException} * @error {@link LoginFlow.PasswordChangesNotSupportedException} * @error {@link LoginFlow.RegistrarAccountNotActiveException} * @error {@link LoginFlow.UnsupportedLanguageException} */ public class LoginFlow implements Flow { private static final FormattingLogger logger = FormattingLogger.getLoggerForCallerClass(); /** Maximum number of failed login attempts allowed per connection. */ private static final int MAX_FAILED_LOGIN_ATTEMPTS_PER_CONNECTION = 3; @Inject ExtensionManager extensionManager; @Inject EppInput eppInput; @Inject SessionMetadata sessionMetadata; @Inject TransportCredentials credentials; @Inject @ClientId String clientId; @Inject EppResponse.Builder responseBuilder; @Inject LoginFlow() {} /** Run the flow and log errors. */ @Override public final EppResponse run() throws EppException { try { return runWithoutLogging(); } catch (EppException e) { logger.warning("Login failed: " + e.getMessage()); throw e; } } /** Run the flow without bothering to log errors. The {@link #run} method will do that for us. */ public final EppResponse runWithoutLogging() throws EppException { extensionManager.validate(); // There are no legal extensions for this flow. Login login = (Login) eppInput.getCommandWrapper().getCommand(); if (!clientId.isEmpty()) { throw new AlreadyLoggedInException(); } Options options = login.getOptions(); if (!ProtocolDefinition.LANGUAGE.equals(options.getLanguage())) { throw new UnsupportedLanguageException(); } Services services = login.getServices(); Set unsupportedObjectServices = difference( nullToEmpty(services.getObjectServices()), ProtocolDefinition.SUPPORTED_OBJECT_SERVICES); if (!unsupportedObjectServices.isEmpty()) { throw new UnimplementedObjectServiceException(); } ImmutableSet.Builder serviceExtensionUrisBuilder = new ImmutableSet.Builder<>(); for (String uri : nullToEmpty(services.getServiceExtensions())) { ServiceExtension serviceExtension = ProtocolDefinition.getServiceExtensionFromUri(uri); if (serviceExtension == null) { throw new UnimplementedExtensionException(); } serviceExtensionUrisBuilder.add(uri); } Optional registrar = Registrar.loadByClientIdCached(login.getClientId()); if (!registrar.isPresent()) { throw new BadRegistrarClientIdException(login.getClientId()); } // AuthenticationErrorExceptions will propagate up through here. try { credentials.validate(registrar.get(), login.getPassword()); } catch (AuthenticationErrorException e) { sessionMetadata.incrementFailedLoginAttempts(); if (sessionMetadata.getFailedLoginAttempts() > MAX_FAILED_LOGIN_ATTEMPTS_PER_CONNECTION) { throw new TooManyFailedLoginsException(); } else { throw e; } } if (registrar.get().getState().equals(Registrar.State.PENDING)) { throw new RegistrarAccountNotActiveException(); } if (login.getNewPassword() != null) { // We don't support in-band password changes. throw new PasswordChangesNotSupportedException(); } // We are in! sessionMetadata.resetFailedLoginAttempts(); sessionMetadata.setClientId(login.getClientId()); sessionMetadata.setServiceExtensionUris(serviceExtensionUrisBuilder.build()); return responseBuilder.build(); } /** Registrar with this client ID could not be found. */ static class BadRegistrarClientIdException extends AuthenticationErrorException { public BadRegistrarClientIdException(String clientId) { super("Registrar with this client ID could not be found: " + clientId); } } /** Registrar login failed too many times. */ static class TooManyFailedLoginsException extends AuthenticationErrorClosingConnectionException { public TooManyFailedLoginsException() { super("Registrar login failed too many times"); } } /** Registrar account is not active. */ static class RegistrarAccountNotActiveException extends AuthorizationErrorException { public RegistrarAccountNotActiveException() { super("Registrar account is not active"); } } /** Registrar is already logged in. */ static class AlreadyLoggedInException extends CommandUseErrorException { public AlreadyLoggedInException() { super("Registrar is already logged in"); } } /** Specified language is not supported. */ static class UnsupportedLanguageException extends ParameterValuePolicyErrorException { public UnsupportedLanguageException() { super("Specified language is not supported"); } } /** In-band password changes are not supported. */ static class PasswordChangesNotSupportedException extends UnimplementedOptionException { public PasswordChangesNotSupportedException() { super("In-band password changes are not supported"); } } }