All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.imunity.otp.OTPRetrieval Maven / Gradle / Ivy

/*
 * Copyright (c) 2020 Bixbit - Krzysztof Benedyczak. All rights reserved.
 * See LICENCE.txt file for licensing information.
 */

package io.imunity.otp;

import com.google.common.base.Strings;
import com.vaadin.flow.component.Component;
import com.vaadin.flow.component.Focusable;
import com.vaadin.flow.component.Key;
import com.vaadin.flow.component.ShortcutRegistration;
import com.vaadin.flow.component.button.Button;
import com.vaadin.flow.component.button.ButtonVariant;
import com.vaadin.flow.component.html.Image;
import com.vaadin.flow.component.html.Span;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.component.textfield.TextField;
import eu.unicore.util.configuration.ConfigurationException;
import io.imunity.otp.credential_reset.OTPCredentialResetController;
import io.imunity.vaadin.auth.AuthNGridTextWrapper;
import io.imunity.vaadin.auth.CredentialResetLauncher;
import io.imunity.vaadin.auth.VaadinAuthentication;
import io.imunity.vaadin.elements.CssClassNames;
import io.imunity.vaadin.elements.LinkButton;
import io.imunity.vaadin.elements.NotificationPresenter;
import io.imunity.vaadin.endpoint.common.plugins.credentials.CredentialEditor;
import io.imunity.vaadin.endpoint.common.plugins.credentials.CredentialEditorRegistry;
import org.springframework.beans.factory.ObjectFactory;
import org.springframework.beans.factory.annotation.Autowired;
import pl.edu.icm.unity.base.entity.Entity;
import pl.edu.icm.unity.base.i18n.I18nString;
import pl.edu.icm.unity.base.message.MessageSource;
import pl.edu.icm.unity.engine.api.authn.*;
import pl.edu.icm.unity.engine.api.authn.AuthenticationResult.Status;
import pl.edu.icm.unity.engine.api.utils.PrototypeComponent;

import java.io.StringReader;
import java.util.*;

/**
 * Retrieves OTP code using a Vaadin textfield
 */
@PrototypeComponent
class OTPRetrieval extends AbstractCredentialRetrieval implements VaadinAuthentication
{
	static final String NAME = "vaadin-otp";
	static final String DESC = "OTPRetrievalFactory.desc";
	
	private final MessageSource msg;
	private final CredentialEditorRegistry credEditorReg;
	private final NotificationPresenter notificationPresenter;
	private I18nString name;
	private String configuration;
	
	@Autowired
	public OTPRetrieval(MessageSource msg, CredentialEditorRegistry credEditorReg, NotificationPresenter notificationPresenter)
	{	
		super(VaadinAuthentication.NAME);
		this.msg = msg;
		this.credEditorReg = credEditorReg;
		this.notificationPresenter = notificationPresenter;
	}
	
	@Override
	public String getSerializedConfiguration()
	{
		return configuration;
	}

	@Override
	public void setSerializedConfiguration(String configuration)
	{
		this.configuration = configuration;
		try
		{
			Properties properties = new Properties();
			properties.load(new StringReader(configuration));
			OTPRetrievalProperties config = new OTPRetrievalProperties(properties);
			name = config.getLocalizedString(msg, OTPRetrievalProperties.NAME);
		} catch (Exception e)
		{
			throw new ConfigurationException("The configuration of the OTP retrieval can not be parsed", e);
		}
	}
	
	@Override
	public Collection createUIInstance(Context context, AuthenticatorStepContext authenticatorContext)
	{
		return Collections.singleton(
				new OTPRetrievalUI(credEditorReg.getEditor(OTP.NAME)));
	}

	@Override
	public boolean supportsGrid()
	{
		return false;
	}

	@Override
	public boolean isMultiOption()
	{
		return false;
	}

	private AuthenticationRetrievalContext getContext()
	{
		return AuthenticationRetrievalContext.builder().withSupportOnlySecondFactorReseting(true).build();
	}

	private class OTPRetrievalComponent extends VerticalLayout implements Focusable
	{
		private AuthenticationCallback callback;
		private TextField usernameField;
		private Span usernameLabel;
		private TextField codeField;
		private int tabIndex;
		private Entity presetEntity;

		private Button authenticateButton;

		private LinkButton lostDevice;
		private final CredentialEditor credEditor;
		private CredentialResetLauncher credResetLauncher;
		
		public OTPRetrievalComponent(CredentialEditor credEditor)
		{
			this.credEditor = credEditor;
			initUI();
		}

		private void initUI()
		{
			setMargin(false);
			setPadding(false);
			getStyle().set("gap", "0");

			usernameField = new TextField();
			usernameField.setWidthFull();
			usernameField.setPlaceholder(msg.getMessage("AuthenticationUI.username"));
			usernameField.addClassName("u-authnTextField");
			usernameField.addClassName("u-otpUsernameField");
			add(usernameField);

			usernameLabel = new Span("");
			add(usernameLabel);
			usernameLabel.setVisible(false);

			codeField = new TextField();
			codeField.setWidthFull();
			String codeLabel = name.isEmpty() ? 
					msg.getMessage("OTPRetrieval.code", credentialExchange.getCodeLength()) : 
					name.getValue(msg); 
			codeField.setPlaceholder(codeLabel);
			codeField.addClassName("u-authnTextField");
			codeField.addClassName("u-otpCodeField");
			add(codeField);
			setAlignItems(Alignment.CENTER);

			authenticateButton = new Button(msg.getMessage("AuthenticationUI.authnenticateButton"));
			add(authenticateButton);
			authenticateButton.addClickListener(event -> triggerAuthentication());
			authenticateButton.setWidthFull();
			authenticateButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
			authenticateButton.addClassName(CssClassNames.SIGNIN_BUTTON.getName());
			authenticateButton.addClassName("u-otpSignInButton");
			

			codeField.addFocusListener(event ->
			{
				ShortcutRegistration shortcutRegistration = authenticateButton.addClickShortcut(Key.ENTER);
				codeField.addBlurListener(e -> shortcutRegistration.remove());
			});

			OTPResetSettings resetSettings = credentialExchange.getCredentialResetBackend().getResetSettings();
			if (resetSettings.enabled)
			{
				lostDevice = new LinkButton(
						msg.getMessage("OTPRetrieval.lostDevice"), event -> showResetDialog());
				add(new AuthNGridTextWrapper(lostDevice, Alignment.END));
			}
		}

		private void triggerAuthentication()
		{
			Optional subject = getAuthenticationSubject();
			if (subject.isEmpty())
			{
				usernameField.focus();
				notificationPresenter.showError(msg.getMessage("OTPRetrieval.missingUsername"), "");
				return;
			}
			
			String code = codeField.getValue();
			if (Strings.isNullOrEmpty(code))
			{
				codeField.focus();
				notificationPresenter.showError(msg.getMessage("OTPRetrieval.missingCode"), "");
				return;
			}
				
			AuthenticationResult authnResult = credentialExchange.verifyCode(code, subject.get());
			setAuthenticationResult(authnResult);
		}

		private void setAuthenticationResult(AuthenticationResult authenticationResult)
		{
			clear();
			if (authenticationResult.getStatus() == Status.success)
			{
				setEnabled(false);
				callback.onCompletedAuthentication(authenticationResult, getContext());
			} else if (authenticationResult.getStatus() == Status.deny)
			{
				usernameField.focus();
				callback.onCompletedAuthentication(authenticationResult, getContext());
			} else
			{
				throw new IllegalStateException("Got unsupported status from verificator: " 
						+ authenticationResult.getStatus());
			}
		}
		
		private void showResetDialog()
		{
			OTPCredentialResetController controller = new OTPCredentialResetController(msg,
					credentialExchange.getCredentialResetBackend(),
					credEditor, credResetLauncher.getConfiguration(), notificationPresenter);
			Optional presetSubject = presetEntity == null ?
					Optional.empty() : Optional.of(AuthenticationSubject.entityBased(presetEntity.getId()));
			credResetLauncher.startCredentialReset(controller.getInitialUI(presetSubject));
		}

		@Override
		public void focus()
		{
			if (presetEntity == null)
				usernameField.focus();
			else
				codeField.focus();
		}

		@Override
		public int getTabIndex()
		{
			return tabIndex;
		}

		@Override
		public void setTabIndex(int tabIndex)
		{
			this.tabIndex = tabIndex;
		}

		public void setCallback(AuthenticationCallback callback)
		{
			this.callback = callback;
		}

		void setAuthenticatedEntity(Entity authenticatedEntity)
		{
			this.presetEntity = authenticatedEntity;
			usernameField.setVisible(false);
			usernameLabel.setVisible(true);
		}

		private void clear()
		{
			usernameField.setValue("");
			codeField.setValue("");
		}

		void hideCredentialReset()
		{
			if (lostDevice != null)
				lostDevice.setVisible(false);
		}

		void setCredentialResetLauncher(CredentialResetLauncher credResetLauncher)
		{
			this.credResetLauncher = credResetLauncher;
		}
		
		private Optional getAuthenticationSubject()
		{
			if (presetEntity != null)
				return Optional.of(AuthenticationSubject.entityBased(presetEntity.getId()));
			String enteredUsername = usernameField.getValue();
			if (!Strings.isNullOrEmpty(enteredUsername))
				return Optional.of(AuthenticationSubject.identityBased(enteredUsername));
			return Optional.empty();
		}
	}

	private class OTPRetrievalUI implements VaadinAuthenticationUI
	{
		private final OTPRetrievalComponent theComponent;

		public OTPRetrievalUI(CredentialEditor credEditor)
		{
			this.theComponent = new OTPRetrievalComponent(credEditor);
		}

		@Override
		public void setAuthenticationCallback(AuthenticationCallback callback)
		{
			theComponent.setCallback(callback);
		}

		@Override
		public void setCredentialResetLauncher(CredentialResetLauncher credResetLauncher)
		{
			theComponent.setCredentialResetLauncher(credResetLauncher);
		}
		
		@Override
		public Component getComponent()
		{
			return theComponent;
		}

		@Override
		public String getLabel()
		{
			return name.getValue(msg); //not fully correct (no fallback to default) but we don't support grid so irrelevant.
		}

		@Override
		public Image getImage()
		{
			return new Image("assets/img/other/mobile-sms.png", "");
		}

		@Override
		public void clear()
		{
			theComponent.clear();
		}

		/**
		 * Simple: there is only one authN option in this authenticator
		 * so we can return any constant id.
		 */
		@Override
		public String getId()
		{
			return "otp";
		}

		@Override
		public void presetEntity(Entity authenticatedEntity)
		{
			theComponent.setAuthenticatedEntity(authenticatedEntity);
		}

		@Override
		public Set getTags()
		{
			return Collections.emptySet();
		}

		@Override
		public void disableCredentialReset()
		{
			theComponent.hideCredentialReset();
		}
	}

	@org.springframework.stereotype.Component
	static class Factory extends AbstractCredentialRetrievalFactory
	{
		@Autowired
		Factory(ObjectFactory factory)
		{
			super(NAME, DESC, VaadinAuthentication.NAME, factory, OTPExchange.ID);
		}
	}
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy