From 998df88dac37f5932fa7dd40877c947bf234806a Mon Sep 17 00:00:00 2001 From: Simon Rettberg Date: Thu, 23 Feb 2023 15:44:18 +0100 Subject: [client] Add authentication via external browser --- .../dozmod/authentication/Authenticator.java | 5 ++ .../authentication/BrowserAuthenticator.java | 99 ++++++++++++++++++++++ .../dozmod/authentication/EcpAuthenticator.java | 4 + .../authentication/TestAccountAuthenticator.java | 4 + .../org/openslx/dozmod/gui/window/LoginWindow.java | 66 ++++++++++++--- .../gui/window/layout/LoginWindowLayout.java | 64 ++++++++++---- .../src/main/properties/i18n/window.properties | 3 +- .../main/properties/i18n/window_de_DE.properties | 1 + .../main/properties/i18n/window_layout.properties | 5 +- .../properties/i18n/window_layout_de_DE.properties | 5 +- 10 files changed, 224 insertions(+), 32 deletions(-) create mode 100644 dozentenmodul/src/main/java/org/openslx/dozmod/authentication/BrowserAuthenticator.java diff --git a/dozentenmodul/src/main/java/org/openslx/dozmod/authentication/Authenticator.java b/dozentenmodul/src/main/java/org/openslx/dozmod/authentication/Authenticator.java index 733aab01..c654ed8b 100644 --- a/dozentenmodul/src/main/java/org/openslx/dozmod/authentication/Authenticator.java +++ b/dozentenmodul/src/main/java/org/openslx/dozmod/authentication/Authenticator.java @@ -42,4 +42,9 @@ public interface Authenticator { * @throws Exception */ void login(String username, String password, AuthenticatorCallback callback) throws Exception; + + /** + * Cancel any running login attempt. + */ + void cancel(); } \ No newline at end of file diff --git a/dozentenmodul/src/main/java/org/openslx/dozmod/authentication/BrowserAuthenticator.java b/dozentenmodul/src/main/java/org/openslx/dozmod/authentication/BrowserAuthenticator.java new file mode 100644 index 00000000..f6587edf --- /dev/null +++ b/dozentenmodul/src/main/java/org/openslx/dozmod/authentication/BrowserAuthenticator.java @@ -0,0 +1,99 @@ +package org.openslx.dozmod.authentication; + +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; + +import org.apache.hc.client5.http.ClientProtocolException; +import org.apache.hc.core5.http.ParseException; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.thrift.TException; +import org.openslx.bwlp.thrift.iface.ClientSessionData; +import org.openslx.bwlp.thrift.iface.TAuthorizationException; +import org.openslx.bwlp.thrift.iface.TNotFoundException; +import org.openslx.dozmod.authentication.ShibbolethEcp.ReturnCode; +import org.openslx.dozmod.gui.control.QLabel; +import org.openslx.dozmod.gui.helper.I18n; +import org.openslx.dozmod.util.DesktopEnvironment; +import org.openslx.thrifthelper.ThriftManager; +import org.openslx.util.Util; + +import com.google.gson.JsonSyntaxException; + +/** + * Authenticator that relies on opening an external browser/website which should + * eventually lead to the provided token yielding a valid session. + */ +public class BrowserAuthenticator implements Authenticator { + + /** + * Logger instance for this class + */ + private final static Logger LOGGER = LogManager.getLogger(BrowserAuthenticator.class); + + private static final String[] ANIMATION = { " -", " \\", " |", " /" }; + + private volatile boolean cancelled = false; + + private final QLabel lblError; + + public BrowserAuthenticator(QLabel lblError) { + this.lblError = lblError; + } + + @Override + public void login(String authUrl, String accessToken, AuthenticatorCallback callback) + throws JsonSyntaxException, ClientProtocolException, ParseException, + MalformedURLException, URISyntaxException, IOException { + // try to login + DesktopEnvironment.openWebpageUri(new URI(authUrl)); + Exception errEx = null; + ReturnCode ret = ReturnCode.GENERIC_ERROR; + AuthenticationData data = null; + String prefix = I18n.WINDOW.getString("Login.Label.error.continueBrowser"); + + // Try for ~10 minutes + for (int i = 1; i < 600 && !cancelled; ++i) { + Util.sleep(600 + i * 5); + lblError.setText(prefix + ANIMATION[i % ANIMATION.length]); + try { + ClientSessionData sess = ThriftManager.getMasterClient() + .getSessionFromAccessCode(accessToken); + if (sess != null) { + data = new AuthenticationData(sess.authToken, sess.sessionId, sess.satellites); + ret = ReturnCode.NO_ERROR; + break; + } + + } catch (TNotFoundException nf) { + continue; // Keep waiting + } catch (TAuthorizationException ex) { + errEx = ex; + break; + } catch (TException e) { + LOGGER.warn("Cannot get login state for browser-based login", e); + } + } + if (cancelled) + return; + if (data == null && errEx == null) { + // Loop timed out + errEx = new TException("Timeout, try again"); + } + + callback.postLogin(ret, data, errEx); + } + + @Override + public void cancel() { + cancelled = true; + } + + @Override + protected void finalize() throws Throwable { + super.finalize(); + cancel(); + } +} diff --git a/dozentenmodul/src/main/java/org/openslx/dozmod/authentication/EcpAuthenticator.java b/dozentenmodul/src/main/java/org/openslx/dozmod/authentication/EcpAuthenticator.java index bed848f2..f17cfe96 100644 --- a/dozentenmodul/src/main/java/org/openslx/dozmod/authentication/EcpAuthenticator.java +++ b/dozentenmodul/src/main/java/org/openslx/dozmod/authentication/EcpAuthenticator.java @@ -87,4 +87,8 @@ public class EcpAuthenticator implements Authenticator { } callback.postLogin(ret, data, errEx); } + + @Override + public void cancel() { + } } diff --git a/dozentenmodul/src/main/java/org/openslx/dozmod/authentication/TestAccountAuthenticator.java b/dozentenmodul/src/main/java/org/openslx/dozmod/authentication/TestAccountAuthenticator.java index bc08dc39..c56a2fdb 100644 --- a/dozentenmodul/src/main/java/org/openslx/dozmod/authentication/TestAccountAuthenticator.java +++ b/dozentenmodul/src/main/java/org/openslx/dozmod/authentication/TestAccountAuthenticator.java @@ -36,4 +36,8 @@ public class TestAccountAuthenticator implements Authenticator { callback.postLogin(ReturnCode.GENERIC_ERROR, null, null); } } + + @Override + public void cancel() { + } } diff --git a/dozentenmodul/src/main/java/org/openslx/dozmod/gui/window/LoginWindow.java b/dozentenmodul/src/main/java/org/openslx/dozmod/gui/window/LoginWindow.java index eab1c4f9..0f2e1f35 100644 --- a/dozentenmodul/src/main/java/org/openslx/dozmod/gui/window/LoginWindow.java +++ b/dozentenmodul/src/main/java/org/openslx/dozmod/gui/window/LoginWindow.java @@ -3,6 +3,7 @@ package org.openslx.dozmod.gui.window; import java.awt.Frame; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; +import java.awt.event.InputEvent; import java.awt.event.ItemEvent; import java.awt.event.ItemListener; import java.awt.event.KeyEvent; @@ -11,6 +12,7 @@ import java.awt.event.WindowEvent; import java.io.File; import java.util.Iterator; import java.util.List; +import java.util.UUID; import javax.swing.AbstractAction; import javax.swing.DefaultComboBoxModel; @@ -26,8 +28,10 @@ import org.apache.thrift.TException; import org.openslx.bwlp.thrift.iface.Organization; import org.openslx.bwlp.thrift.iface.Satellite; import org.openslx.dozmod.App; +import org.openslx.dozmod.Branding; import org.openslx.dozmod.Config; import org.openslx.dozmod.authentication.Authenticator; +import org.openslx.dozmod.authentication.BrowserAuthenticator; import org.openslx.dozmod.authentication.Authenticator.AuthenticationData; import org.openslx.dozmod.authentication.Authenticator.AuthenticatorCallback; import org.openslx.dozmod.authentication.EcpAuthenticator; @@ -69,7 +73,8 @@ public class LoginWindow extends LoginWindowLayout { public static enum LoginType { ECP(0), TEST_ACCOUNT(1), - DIRECT_CONNECT(2); + DIRECT_CONNECT(2), + EXTERNAL_BROWSER(3); public final int id; @@ -80,6 +85,9 @@ public class LoginWindow extends LoginWindowLayout { // authentication method to use for login attempts protected LoginType loginType = null; + + // Currently running authentication + protected Authenticator currentAuthenticator; private boolean forceCustomSatellite = false; @@ -99,10 +107,25 @@ public class LoginWindow extends LoginWindowLayout { rdoLoginType[type.id].addItemListener(new ItemListener() { @Override public void itemStateChanged(ItemEvent e) { - if (e.getStateChange() == ItemEvent.SELECTED) { - cboOrganization.setEnabled(cboOrganization.getModel().getSize() != 0 && type == LoginType.ECP); - loginType = type; - btnOpenRegistration.setEnabled(type == LoginType.ECP); + if (e.getStateChange() != ItemEvent.SELECTED) + return; + cboOrganization.setEnabled(cboOrganization.getModel().getSize() != 0 && type == LoginType.ECP); + loginType = type; + btnOpenRegistration.setEnabled(type == LoginType.ECP || type == LoginType.EXTERNAL_BROWSER); + boolean browser = (type == LoginType.EXTERNAL_BROWSER); + cboOrganization.setVisible(!browser); + txtUsername.setVisible(!browser); + txtPassword.setVisible(!browser); + lblOrganization.setVisible(!browser); + lblUsername.setVisible(!browser); + lblPassword.setVisible(!browser); + txtUrl.setVisible(browser); + lblError.setText("-"); + lblError.setVisible(browser); + pnlLoginForm.doLayout(); + if (currentAuthenticator != null) { + currentAuthenticator.cancel(); + currentAuthenticator = null; } } }); @@ -183,6 +206,9 @@ public class LoginWindow extends LoginWindowLayout { // make enter key activate login pnlLoginForm.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put( KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), "login"); + pnlLoginForm.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put( + KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, InputEvent.SHIFT_DOWN_MASK), "login"); + pnlLoginForm.getActionMap().put("login", new AbstractAction() { /** * @@ -317,8 +343,19 @@ public class LoginWindow extends LoginWindowLayout { return; } // here we only check for the fields - String username = txtUsername.getText(); - final String password = String.copyValueOf(txtPassword.getPassword()); + String username; + final String password; + // SPECIAL CASE - browser based + // username is the full URL including token, password is just the token + if (loginType == LoginType.EXTERNAL_BROWSER) { + password = UUID.randomUUID().toString(); + username = "https://" + Branding.getMasterServerAddress() + "/webif/shib/?do=SuiteLogin&accessToken=" + password; + lblError.setText(I18n.WINDOW.getString("Login.Label.error.continueBrowser")); + txtUrl.setText(username); + } else { + username = txtUsername.getText(); + password = String.copyValueOf(txtPassword.getPassword()); + } // login clicked, lets first read the fields if (username.isEmpty()) { Gui.showMessageBox(this, I18n.WINDOW.getString("Login.Message.error.noUsername"), MessageType.ERROR, LOGGER, null); @@ -340,6 +377,9 @@ public class LoginWindow extends LoginWindowLayout { final AuthenticatorCallback authenticatorCallback = new AuthenticatorCallback() { @Override public void postLogin(ReturnCode returnCode, final AuthenticationData data, Throwable t) { + if (t != null) { + lblError.setText(t.getMessage()); + } switch (returnCode) { case NO_ERROR: Gui.asyncExec(new Runnable() { @@ -384,13 +424,15 @@ public class LoginWindow extends LoginWindowLayout { }; // now switch over the login types. - final Authenticator authenticator; switch (loginType) { case ECP: - authenticator = new EcpAuthenticator(selectedOrg.getEcpUrl()); + currentAuthenticator = new EcpAuthenticator(selectedOrg.getEcpUrl()); break; case TEST_ACCOUNT: - authenticator = new TestAccountAuthenticator(); + currentAuthenticator = new TestAccountAuthenticator(); + break; + case EXTERNAL_BROWSER: + currentAuthenticator = new BrowserAuthenticator(lblError); break; case DIRECT_CONNECT: Gui.showMessageBox(this, I18n.WINDOW.getString("Login.Message.error.loginTypeDirectConnect"), @@ -410,7 +452,7 @@ public class LoginWindow extends LoginWindowLayout { // Execute login App.waitForInit(); try { - authenticator.login(finalUsername, password, authenticatorCallback); + currentAuthenticator.login(finalUsername, password, authenticatorCallback); return; } catch (TException e) { ThriftError.showMessage(LoginWindow.this, LOGGER, e, @@ -418,6 +460,8 @@ public class LoginWindow extends LoginWindowLayout { } catch (Exception e) { Gui.showMessageBox(LoginWindow.this, I18n.WINDOW.getString("Login.Message.error.loginFailed"), MessageType.ERROR, LOGGER, e); + } finally { + currentAuthenticator = null; } enableLogin(true); } diff --git a/dozentenmodul/src/main/java/org/openslx/dozmod/gui/window/layout/LoginWindowLayout.java b/dozentenmodul/src/main/java/org/openslx/dozmod/gui/window/layout/LoginWindowLayout.java index 55b525d9..c65e7a53 100644 --- a/dozentenmodul/src/main/java/org/openslx/dozmod/gui/window/layout/LoginWindowLayout.java +++ b/dozentenmodul/src/main/java/org/openslx/dozmod/gui/window/layout/LoginWindowLayout.java @@ -56,7 +56,7 @@ public abstract class LoginWindowLayout extends JDialog { protected LOGIN_TYPE loginType = null; // login type panel - protected final JRadioButton[] rdoLoginType = new JRadioButton[3]; + protected final JRadioButton[] rdoLoginType = new JRadioButton[4]; // login form panel protected final JComboBox cboOrganization; @@ -68,7 +68,14 @@ public abstract class LoginWindowLayout extends JDialog { protected final JPanel pnlLoginType; protected final JPanel pnlLoginForm; protected final JPanel pnlAdvanced; - + + protected final JTextField txtUrl; + protected final QLabel lblError; + + protected final QLabel lblOrganization; + protected final QLabel lblUsername; + protected final QLabel lblPassword; + // advanced panel protected final JButton btnSettings; protected final JButton btnLogDir; @@ -90,13 +97,18 @@ public abstract class LoginWindowLayout extends JDialog { grid.add(new QLabel(getScaledLogo()), 2); grid.nextRow(); - rdoLoginType[0] = new JRadioButton( - I18n.WINDOW_LAYOUT.getString("Login.RadioButton.loginType.text.0", Branding.getMasterServerIdm())); - rdoLoginType[1] = new JRadioButton(I18n.WINDOW_LAYOUT.getString("Login.RadioButton.loginType.text.1")); - rdoLoginType[2] = new JRadioButton(I18n.WINDOW_LAYOUT.getString("Login.RadioButton.loginType.text.2")); + rdoLoginType[0] = new JRadioButton(I18n.WINDOW_LAYOUT.getString("Login.RadioButton.loginType.text.0", + Branding.getMasterServerIdm())); + rdoLoginType[1] = new JRadioButton( + I18n.WINDOW_LAYOUT.getString("Login.RadioButton.loginType.text.1")); + rdoLoginType[2] = new JRadioButton( + I18n.WINDOW_LAYOUT.getString("Login.RadioButton.loginType.text.2")); + rdoLoginType[3] = new JRadioButton(I18n.WINDOW_LAYOUT.getString("Login.RadioButton.loginType.text.3", + Branding.getMasterServerIdm())); btnSettings = new JButton(I18n.WINDOW_LAYOUT.getString("Login.Button.settings.text")); btnLogDir = new JButton(I18n.WINDOW_LAYOUT.getString("Login.Button.logDir.text")); + lblOrganization = new QLabel(I18n.WINDOW_LAYOUT.getString("Login.Label.organization.text")); cboOrganization = new ComboBox(new ComboBoxRenderer() { @Override public String renderItem(Organization item) { @@ -110,8 +122,17 @@ public abstract class LoginWindowLayout extends JDialog { return I18n.WINDOW_LAYOUT.getString("Login.ComboBox.organization.emptyText"); } }, Organization.class); + lblUsername = new QLabel(I18n.WINDOW_LAYOUT.getString("Login.Label.username.text")); txtUsername = new JTextField(); + lblPassword = new QLabel(I18n.WINDOW_LAYOUT.getString("Login.Label.password.text")); txtPassword = new JPasswordField(); + // Browser + txtUrl = new JTextField(); + txtUrl.setEditable(false); + lblError = new QLabel(); + txtUrl.setVisible(false); + lblError.setVisible(false); + // btnLogin = new JButton(I18n.WINDOW_LAYOUT.getString("Login.Button.login.text")); chkSaveUsername = new JCheckBox(I18n.WINDOW_LAYOUT.getString("Login.CheckBox.saveUsername.text")); btnOpenRegistration = new JButton(I18n.WINDOW_LAYOUT.getString("Login.Button.openRegistration.text")); @@ -119,12 +140,12 @@ public abstract class LoginWindowLayout extends JDialog { pnlLoginType = makeLoginTypePanel(); grid.add(pnlLoginType).expand(0.25, 1).fill(true, true); pnlLoginForm = makeLoginFormPanel(); - grid.add(pnlLoginForm,1,2).expand(0.75, 1).fill(true, true); + grid.add(pnlLoginForm, 1, 2).expand(0.75, 1).fill(true, true); grid.nextRow(); - + pnlAdvanced = makeAdvancedPanel(); grid.add(pnlAdvanced).expand(true, true).fill(true, true); - + grid.nextRow(); pnlActivity = new JPanel(); pnlActivity.setLayout(new BoxLayout(pnlActivity, BoxLayout.PAGE_AXIS)); @@ -144,26 +165,36 @@ public abstract class LoginWindowLayout extends JDialog { I18n.WINDOW_LAYOUT.getString("Login.TitledBorder.loginFormPanel.title"))); GridManager grid = new GridManager(loginFormPanel, 4); - grid.add(new QLabel(I18n.WINDOW_LAYOUT.getString("Login.Label.organization.text"))); + grid.add(lblOrganization); grid.add(cboOrganization, 3).expand(true, false).fill(true, false); grid.nextRow(); // label + field for username - grid.add(new QLabel(I18n.WINDOW_LAYOUT.getString("Login.Label.username.text"))); + grid.add(lblUsername); grid.add(txtUsername, 3).expand(true, false).fill(true, false); grid.nextRow(); // label + field for password - grid.add(new QLabel(I18n.WINDOW_LAYOUT.getString("Login.Label.password.text"))); + grid.add(lblPassword); grid.add(txtPassword, 3).expand(true, false).fill(true, false); grid.nextRow(); + // --- Browser-based login + + grid.add(txtUrl, 4).expand(true, false).fill(true, false); + grid.nextRow(); + grid.add(lblError, 4).expand(true, false).fill(true, false); + grid.nextRow(); + + grid.add(Box.createVerticalGlue(), 4).expand(false, true); + grid.nextRow(); + grid.add(Box.createGlue()); grid.add(chkSaveUsername).expand(true, false); grid.add(btnOpenRegistration); grid.add(btnLogin); grid.nextRow(); - grid.finish(true); + grid.finish(false); return loginFormPanel; } @@ -180,7 +211,7 @@ public abstract class LoginWindowLayout extends JDialog { return loginTypePanel; } - + private JPanel makeAdvancedPanel() { JPanel p = new JPanel(); p.setLayout(new BoxLayout(p, BoxLayout.LINE_AXIS)); @@ -208,8 +239,9 @@ public abstract class LoginWindowLayout extends JDialog { } else { scaling = scaleY / 2; } - image = new ImageIcon(image.getImage().getScaledInstance((int) (image.getIconWidth() * scaling), - (int) (image.getIconHeight() * scaling), 0)); + image = new ImageIcon(image.getImage() + .getScaledInstance((int) (image.getIconWidth() * scaling), + (int) (image.getIconHeight() * scaling), 0)); return image; } catch (Exception e) { LOGGER.warn("Cannot load image", e); diff --git a/dozentenmodul/src/main/properties/i18n/window.properties b/dozentenmodul/src/main/properties/i18n/window.properties index 180db3fc..b348034b 100644 --- a/dozentenmodul/src/main/properties/i18n/window.properties +++ b/dozentenmodul/src/main/properties/i18n/window.properties @@ -128,6 +128,7 @@ Login.Message.error.authMasterServer=The master server has rejected the login at Login.Message.error.loginTypeDirectConnect=Not yet implemented Login.Message.error.loginTypeDefault=No login type selected! Login.Message.error.loginFailed=Login failed +Login.Label.error.continueBrowser=Continue via browser... # SatelliteListWindow SatelliteList.Message.error.noSatellite=No satellite selected @@ -143,4 +144,4 @@ VirtConfigEditor.Message.yesNo.safeClose=Your changes in this window will be los VirtDropDownConfigEditor.Message.warning.initializeComboBoxes=You saved an incorrect entry \n\ during the last configuration. \nThe VM will not start! VirtDropDownConfigEditor.Message.yesNo.safeClose=Do you really want to cancel?\n\ - Your changes will be discarded. \ No newline at end of file + Your changes will be discarded. diff --git a/dozentenmodul/src/main/properties/i18n/window_de_DE.properties b/dozentenmodul/src/main/properties/i18n/window_de_DE.properties index 97c72ab8..f3a43a8f 100644 --- a/dozentenmodul/src/main/properties/i18n/window_de_DE.properties +++ b/dozentenmodul/src/main/properties/i18n/window_de_DE.properties @@ -128,6 +128,7 @@ Login.Message.error.authMasterServer=Der Masterserver hat den Loginversuch mit d Login.Message.error.loginTypeDirectConnect=Noch nicht implementiert Login.Message.error.loginTypeDefault=Keine Authentifizierungsart ausgewählt! Login.Message.error.loginFailed=Anmeldung fehlgeschlagen +Login.Label.error.continueBrowser=Im Browser fortfahren... # SatelliteListWindow SatelliteList.Message.error.noSatellite=Kein Satellitenserver ausgewählt diff --git a/dozentenmodul/src/main/properties/i18n/window_layout.properties b/dozentenmodul/src/main/properties/i18n/window_layout.properties index 982ce0b5..04a659b2 100644 --- a/dozentenmodul/src/main/properties/i18n/window_layout.properties +++ b/dozentenmodul/src/main/properties/i18n/window_layout.properties @@ -174,10 +174,11 @@ Login.Dialog.title={0} - Login Login.RadioButton.loginType.text.0=Authentication via {0} Login.RadioButton.loginType.text.1=Test access with fixed user Login.RadioButton.loginType.text.2=Direct access to the satellite +Login.RadioButton.loginType.text.3={0} via system browser Login.Button.settings.text=Settings Login.Button.logDir.text=Log directory Login.Button.login.text=Login -Login.CheckBox.saveUsername.text=Remember username +Login.CheckBox.saveUsername.text=Stay logged in Login.Button.openRegistration.text=Register Login.TitledBorder.loginFormPanel.title=Login data Login.Label.organization.text=Identity Provider @@ -241,4 +242,4 @@ VirtDropDownConfigEditor.Label.E0VirtDev.text=Network interface card VirtDropDownConfigEditor.Label.maxUSBSpeed.text=USB VirtDropDownConfigEditor.Button.more.text=Expert mode VirtDropDownConfigEditor.Button.cancel.text=Cancel -VirtDropDownConfigEditor.Button.save.text=Save \ No newline at end of file +VirtDropDownConfigEditor.Button.save.text=Save diff --git a/dozentenmodul/src/main/properties/i18n/window_layout_de_DE.properties b/dozentenmodul/src/main/properties/i18n/window_layout_de_DE.properties index c26066b7..b91e9c25 100644 --- a/dozentenmodul/src/main/properties/i18n/window_layout_de_DE.properties +++ b/dozentenmodul/src/main/properties/i18n/window_layout_de_DE.properties @@ -174,10 +174,11 @@ Login.Dialog.title={0} - Login Login.RadioButton.loginType.text.0=Authentifizierung über {0} Login.RadioButton.loginType.text.1=Test-Zugang mit festem Benutzer Login.RadioButton.loginType.text.2=Direkter Zugang zum Satelliten +Login.RadioButton.loginType.text.3={0} via System-Browser Login.Button.settings.text=Einstellungen Login.Button.logDir.text=Logverzeichnis Login.Button.login.text=Login -Login.CheckBox.saveUsername.text=Benutzername speichern +Login.CheckBox.saveUsername.text=Eingeloggt bleiben Login.Button.openRegistration.text=Registrieren Login.TitledBorder.loginFormPanel.title=Zugangsdaten Login.Label.organization.text=Identity Provider @@ -241,4 +242,4 @@ VirtDropDownConfigEditor.Label.E0VirtDev.text=Netzwerkkarte VirtDropDownConfigEditor.Label.maxUSBSpeed.text=USB VirtDropDownConfigEditor.Button.more.text=Expertenmodus VirtDropDownConfigEditor.Button.cancel.text=Abbrechen -VirtDropDownConfigEditor.Button.save.text=Speichern \ No newline at end of file +VirtDropDownConfigEditor.Button.save.text=Speichern -- cgit v1.2.3-55-g7522