From 2f64cf92e063a5e8ff33b8646d214337b7b4938c Mon Sep 17 00:00:00 2001
From: anhefti <andreas.hefti@let.ethz.ch>
Date: Thu, 23 Jan 2020 16:46:05 +0100
Subject: [PATCH] registration page

---
 .../ethz/seb/sebserver/WebSecurityConfig.java |   4 +-
 .../ch/ethz/seb/sebserver/gbl/Constants.java  |   3 +
 .../ch/ethz/seb/sebserver/gbl/api/API.java    |   7 +-
 .../ch/ethz/seb/sebserver/gbl/util/Utils.java |   4 +
 .../sebserver/gui/GuiWebsecurityConfig.java   |   2 -
 ...InstitutionalAuthenticationEntryPoint.java |  29 ++-
 .../seb/sebserver/gui/content/LoginPage.java  |  40 +++-
 .../sebserver/gui/content/RegisterPage.java   | 204 ++++++++++++++++++
 .../gui/content/UserAccountForm.java          |  25 ++-
 .../service/page/impl/DefaultLoginPage.java   |   2 +
 .../service/page/impl/DefaultMainPage.java    |   2 +
 .../page/impl/DefaultRegisterPage.java        |  38 ++++
 .../remote/webservice/api/RestCall.java       |   1 +
 .../api/institution/GetInstitutionInfo.java   |  42 ++++
 .../api/useraccount/RegisterNewUser.java      |  40 ++++
 .../webservice/auth/WebserviceURIService.java |   6 +
 .../sebserver/gui/widget/WidgetFactory.java   |   5 +-
 .../weblayer/api/InfoController.java          |  21 +-
 .../weblayer/api/RegisterUserController.java  | 152 +++++++++++++
 .../config/application-dev-gui.properties     |   1 -
 .../config/application-dev-ws.properties      |   1 -
 .../resources/config/application.properties   |   1 +
 src/main/resources/messages.properties        |   4 +
 src/main/resources/static/css/sebserver.css   |  10 +
 src/main/resources/static/images/save.png     | Bin 165 -> 175 bytes
 src/main/resources/static/images/save_alt.png | Bin 0 -> 165 bytes
 .../gui/integration/GuiIntegrationTest.java   |   2 +-
 27 files changed, 615 insertions(+), 31 deletions(-)
 create mode 100644 src/main/java/ch/ethz/seb/sebserver/gui/content/RegisterPage.java
 create mode 100644 src/main/java/ch/ethz/seb/sebserver/gui/service/page/impl/DefaultRegisterPage.java
 create mode 100644 src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/institution/GetInstitutionInfo.java
 create mode 100644 src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/useraccount/RegisterNewUser.java
 create mode 100644 src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/RegisterUserController.java
 create mode 100644 src/main/resources/static/images/save_alt.png

diff --git a/src/main/java/ch/ethz/seb/sebserver/WebSecurityConfig.java b/src/main/java/ch/ethz/seb/sebserver/WebSecurityConfig.java
index 7912beba..f62042b7 100644
--- a/src/main/java/ch/ethz/seb/sebserver/WebSecurityConfig.java
+++ b/src/main/java/ch/ethz/seb/sebserver/WebSecurityConfig.java
@@ -80,7 +80,9 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter implements E
                 .ignoring()
                 .antMatchers("/error")
                 .antMatchers(this.examAPIDiscoveryEndpoint)
-                .antMatchers(this.adminAPIEndpoint + API.INFO_ENDPOINT + API.LOGO_PATH_SEGMENT + "/**");
+                .antMatchers(this.adminAPIEndpoint + API.INFO_ENDPOINT + API.LOGO_PATH_SEGMENT + "/**")
+                .antMatchers(this.adminAPIEndpoint + API.INFO_ENDPOINT + API.INFO_INST_PATH_SEGMENT + "/**")
+                .antMatchers(this.adminAPIEndpoint + API.REGISTER_ENDPOINT);
     }
 
     @RequestMapping("/error")
diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/Constants.java b/src/main/java/ch/ethz/seb/sebserver/gbl/Constants.java
index 5310e5bf..d7d5c99d 100644
--- a/src/main/java/ch/ethz/seb/sebserver/gbl/Constants.java
+++ b/src/main/java/ch/ethz/seb/sebserver/gbl/Constants.java
@@ -23,6 +23,9 @@ import ch.ethz.seb.sebserver.gbl.api.authorization.Privilege;
 /** Global Constants used in SEB Server web-service as well as in web-gui component */
 public final class Constants {
 
+    public static final String DEFAULT_LANG_CODE = "en";
+    public static final String DEFAULT_TIME_ZONE_CODE = "UTC";
+
     public static final int SEB_FILE_HEADER_SIZE = 4;
     public static final int JN_CRYPTOR_ITERATIONS = 10000;
     public static final int JN_CRYPTOR_VERSION_HEADER_SIZE = 1;
diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/api/API.java b/src/main/java/ch/ethz/seb/sebserver/gbl/api/API.java
index 30a9f034..c0f0c0ba 100644
--- a/src/main/java/ch/ethz/seb/sebserver/gbl/api/API.java
+++ b/src/main/java/ch/ethz/seb/sebserver/gbl/api/API.java
@@ -46,8 +46,13 @@ public final class API {
     public static final String SELF_PATH_SEGMENT = "/self";
 
     public static final String INFO_ENDPOINT = "/info";
+    public static final String INFO_PARAM_INST_SUFFIX = "urlSuffix";
+    public static final String INFO_INST_PATH_SEGMENT = "/institution";
+    public static final String INFO_INST_ENDPOINT = INFO_INST_PATH_SEGMENT + "/{" + INFO_PARAM_INST_SUFFIX + "}";
     public static final String LOGO_PATH_SEGMENT = "/logo";
-    public static final String INSTITUTIONAL_LOGO_PATH = LOGO_PATH_SEGMENT + "/{urlSuffix}";
+
+    public static final String INSTITUTIONAL_LOGO_PATH = LOGO_PATH_SEGMENT + "/{" + INFO_PARAM_INST_SUFFIX + "}";
+    public static final String REGISTER_ENDPOINT = "/register";
 
     public static final String NAMES_PATH_SEGMENT = "/names";
 
diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/util/Utils.java b/src/main/java/ch/ethz/seb/sebserver/gbl/util/Utils.java
index e9c0ad1d..4247689e 100644
--- a/src/main/java/ch/ethz/seb/sebserver/gbl/util/Utils.java
+++ b/src/main/java/ch/ethz/seb/sebserver/gbl/util/Utils.java
@@ -436,6 +436,10 @@ public final class Utils {
         return toCharArray(CharBuffer.wrap(chars));
     }
 
+    public static void clear(final CharSequence charSequence) {
+        clearCharArray(toCharArray(charSequence));
+    }
+
     public static String toString(final CharSequence charSequence) {
         if (charSequence == null) {
             return null;
diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/GuiWebsecurityConfig.java b/src/main/java/ch/ethz/seb/sebserver/gui/GuiWebsecurityConfig.java
index ee96baa2..321414f7 100644
--- a/src/main/java/ch/ethz/seb/sebserver/gui/GuiWebsecurityConfig.java
+++ b/src/main/java/ch/ethz/seb/sebserver/gui/GuiWebsecurityConfig.java
@@ -38,8 +38,6 @@ public class GuiWebsecurityConfig extends WebSecurityConfigurerAdapter {
     public static final RequestMatcher PUBLIC_URLS = new OrRequestMatcher(
             // OAuth entry-points
             new AntPathRequestMatcher(API.OAUTH_REVOKE_TOKEN_ENDPOINT),
-            // GUI entry-point
-//            new AntPathRequestMatcher(guiEntryPoint),
             // RAP/RWT resources has to be accessible
             new AntPathRequestMatcher("/rwt-resources/**"),
             // project specific static resources
diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/InstitutionalAuthenticationEntryPoint.java b/src/main/java/ch/ethz/seb/sebserver/gui/InstitutionalAuthenticationEntryPoint.java
index 3717cc7b..b157382d 100644
--- a/src/main/java/ch/ethz/seb/sebserver/gui/InstitutionalAuthenticationEntryPoint.java
+++ b/src/main/java/ch/ethz/seb/sebserver/gui/InstitutionalAuthenticationEntryPoint.java
@@ -20,6 +20,7 @@ import javax.servlet.http.HttpServletResponse;
 import org.apache.commons.codec.Charsets;
 import org.apache.commons.codec.binary.Base64InputStream;
 import org.apache.commons.lang3.StringUtils;
+import org.eclipse.rap.rwt.RWT;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Value;
@@ -40,12 +41,16 @@ import org.springframework.web.client.RestTemplate;
 import ch.ethz.seb.sebserver.ClientHttpRequestFactoryService;
 import ch.ethz.seb.sebserver.gbl.Constants;
 import ch.ethz.seb.sebserver.gbl.api.API;
+import ch.ethz.seb.sebserver.gbl.profile.GuiProfile;
 import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.WebserviceURIService;
 import ch.ethz.seb.sebserver.gui.widget.ImageUploadSelection;
 
 @Lazy
 @Component
-final class InstitutionalAuthenticationEntryPoint implements AuthenticationEntryPoint {
+@GuiProfile
+public final class InstitutionalAuthenticationEntryPoint implements AuthenticationEntryPoint {
+
+    private static final String INST_SUFFIX_ATTRIBUTE = "instSuffix";
 
     private static final Logger log = LoggerFactory.getLogger(InstitutionalAuthenticationEntryPoint.class);
 
@@ -111,7 +116,7 @@ final class InstitutionalAuthenticationEntryPoint implements AuthenticationEntry
         final String logoImageBase64 = requestLogoImage(institutionalEndpoint);
         if (StringUtils.isNotBlank(logoImageBase64)) {
             request.getSession().setAttribute(API.PARAM_LOGO_IMAGE, logoImageBase64);
-            request.getSession().setAttribute("themeId", "sms");
+            request.getSession().setAttribute(INST_SUFFIX_ATTRIBUTE, institutionalEndpoint);
             forwardToEntryPoint(request, response, this.guiEntryPoint);
         } else {
             request.getSession().removeAttribute(API.PARAM_LOGO_IMAGE);
@@ -132,19 +137,31 @@ final class InstitutionalAuthenticationEntryPoint implements AuthenticationEntry
         dispatcher.forward(request, response);
     }
 
-    private String extractInstitutionalEndpoint(final HttpServletRequest request) {
+    public static String extractInstitutionalEndpoint(final HttpServletRequest request) {
         final String requestURI = request.getRequestURI();
 
         if (log.isDebugEnabled()) {
             log.debug("Trying to verify insitution from requested entrypoint url: {}", requestURI);
         }
 
-        final String instPrefix = requestURI.replaceAll("/", "");
-        if (StringUtils.isBlank(instPrefix)) {
+        try {
+            return requestURI.substring(
+                    requestURI.lastIndexOf(Constants.SLASH) + 1,
+                    requestURI.length());
+        } catch (final Exception e) {
+            log.error("Fauled to extract institutional URL suffix: {}", e.getMessage());
             return null;
         }
+    }
 
-        return instPrefix;
+    public static String extractInstitutionalEndpoint() {
+        try {
+            final Object attribute = RWT.getUISession().getHttpSession().getAttribute(INST_SUFFIX_ATTRIBUTE);
+            return (attribute != null) ? String.valueOf(attribute) : null;
+        } catch (final Exception e) {
+            log.warn("Failed to extract institutional endpoint form user session: {}", e.getMessage());
+            return null;
+        }
     }
 
     private String requestLogoImage(final String institutionalEndpoint) {
diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/content/LoginPage.java b/src/main/java/ch/ethz/seb/sebserver/gui/content/LoginPage.java
index bd60d783..305dcc67 100644
--- a/src/main/java/ch/ethz/seb/sebserver/gui/content/LoginPage.java
+++ b/src/main/java/ch/ethz/seb/sebserver/gui/content/LoginPage.java
@@ -8,6 +8,7 @@
 
 package ch.ethz.seb.sebserver.gui.content;
 
+import org.apache.commons.lang3.BooleanUtils;
 import org.apache.commons.lang3.StringUtils;
 import org.eclipse.rap.rwt.RWT;
 import org.eclipse.swt.SWT;
@@ -20,6 +21,7 @@ import org.eclipse.swt.widgets.MessageBox;
 import org.eclipse.swt.widgets.Text;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Value;
 import org.springframework.context.annotation.Lazy;
 import org.springframework.stereotype.Component;
 
@@ -28,6 +30,7 @@ import ch.ethz.seb.sebserver.gui.service.i18n.I18nSupport;
 import ch.ethz.seb.sebserver.gui.service.page.PageContext;
 import ch.ethz.seb.sebserver.gui.service.page.PageService;
 import ch.ethz.seb.sebserver.gui.service.page.TemplateComposer;
+import ch.ethz.seb.sebserver.gui.service.page.impl.DefaultRegisterPage;
 import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.AuthorizationContextHolder;
 import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.SEBServerAuthorizationContext;
 import ch.ethz.seb.sebserver.gui.widget.Message;
@@ -44,12 +47,20 @@ public class LoginPage implements TemplateComposer {
     private final AuthorizationContextHolder authorizationContextHolder;
     private final WidgetFactory widgetFactory;
     private final I18nSupport i18nSupport;
+    private final DefaultRegisterPage defaultRegisterPage;
+    private final boolean registreringEnabled;
+
+    public LoginPage(
+            final PageService pageService,
+            final DefaultRegisterPage defaultRegisterPage,
+            @Value("${sebserver.gui.self-registering:false}") final Boolean registreringEnabled) {
 
-    public LoginPage(final PageService pageService) {
         this.pageService = pageService;
         this.authorizationContextHolder = pageService.getAuthorizationContextHolder();
         this.widgetFactory = pageService.getWidgetFactory();
         this.i18nSupport = pageService.getI18nSupport();
+        this.defaultRegisterPage = defaultRegisterPage;
+        this.registreringEnabled = BooleanUtils.toBoolean(registreringEnabled);
     }
 
     @Override
@@ -61,7 +72,7 @@ public class LoginPage implements TemplateComposer {
         rowLayout.marginWidth = 20;
         rowLayout.marginRight = 100;
         loginGroup.setLayout(rowLayout);
-        loginGroup.setData(RWT.CUSTOM_VARIANT, "login");
+        loginGroup.setData(RWT.CUSTOM_VARIANT, WidgetFactory.CustomVariant.LOGIN.key);
 
         final Label name = this.widgetFactory.labelLocalized(loginGroup, "sebserver.login.username");
         name.setLayoutData(new GridData(300, -1));
@@ -75,15 +86,19 @@ public class LoginPage implements TemplateComposer {
         final Text loginPassword = this.widgetFactory.passwordInput(loginGroup);
         loginPassword.setLayoutData(new GridData(SWT.FILL, SWT.TOP, false, false));
 
-        final Button button = this.widgetFactory.buttonLocalized(loginGroup, "sebserver.login.login");
-        gridData = new GridData(SWT.LEFT, SWT.TOP, false, false);
-        gridData.verticalIndent = 10;
-        button.setLayoutData(gridData);
-
         final SEBServerAuthorizationContext authorizationContext = this.authorizationContextHolder
                 .getAuthorizationContext(RWT.getUISession().getHttpSession());
 
-        button.addListener(SWT.Selection, event -> {
+        final Composite buttons = new Composite(loginGroup, SWT.NONE);
+        buttons.setLayoutData(new GridData(SWT.FILL, SWT.TOP, true, false));
+        buttons.setLayout(new GridLayout(2, false));
+        buttons.setData(RWT.CUSTOM_VARIANT, WidgetFactory.CustomVariant.LOGIN_BACK.key);
+
+        final Button loginButton = this.widgetFactory.buttonLocalized(buttons, "sebserver.login.login");
+        gridData = new GridData(SWT.LEFT, SWT.TOP, false, false);
+        gridData.verticalIndent = 10;
+        loginButton.setLayoutData(gridData);
+        loginButton.addListener(SWT.Selection, event -> {
             login(
                     pageContext,
                     loginName.getText(),
@@ -117,6 +132,15 @@ public class LoginPage implements TemplateComposer {
             }
         });
 
+        if (this.registreringEnabled) {
+            final Button registerButton = this.widgetFactory.buttonLocalized(buttons, "sebserver.login.register");
+            gridData = new GridData(SWT.LEFT, SWT.TOP, false, false);
+            gridData.verticalIndent = 10;
+            registerButton.setLayoutData(gridData);
+            registerButton.addListener(SWT.Selection, event -> {
+                pageContext.forwardToPage(this.defaultRegisterPage);
+            });
+        }
     }
 
     private void login(
diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/content/RegisterPage.java b/src/main/java/ch/ethz/seb/sebserver/gui/content/RegisterPage.java
new file mode 100644
index 00000000..cad8eb43
--- /dev/null
+++ b/src/main/java/ch/ethz/seb/sebserver/gui/content/RegisterPage.java
@@ -0,0 +1,204 @@
+/*
+ * Copyright (c) 2020 ETH Zürich, Educational Development and Technology (LET)
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+package ch.ethz.seb.sebserver.gui.content;
+
+import java.util.List;
+import java.util.function.Supplier;
+import java.util.stream.Collectors;
+
+import org.apache.commons.lang3.BooleanUtils;
+import org.eclipse.rap.rwt.RWT;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Composite;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Lazy;
+import org.springframework.http.client.ClientHttpRequestFactory;
+import org.springframework.stereotype.Component;
+import org.springframework.web.client.RestTemplate;
+
+import ch.ethz.seb.sebserver.ClientHttpRequestFactoryService;
+import ch.ethz.seb.sebserver.gbl.Constants;
+import ch.ethz.seb.sebserver.gbl.api.API;
+import ch.ethz.seb.sebserver.gbl.model.Domain;
+import ch.ethz.seb.sebserver.gbl.model.EntityName;
+import ch.ethz.seb.sebserver.gbl.model.user.PasswordChange;
+import ch.ethz.seb.sebserver.gbl.profile.GuiProfile;
+import ch.ethz.seb.sebserver.gbl.util.Tuple;
+import ch.ethz.seb.sebserver.gui.InstitutionalAuthenticationEntryPoint;
+import ch.ethz.seb.sebserver.gui.form.FormBuilder;
+import ch.ethz.seb.sebserver.gui.service.ResourceService;
+import ch.ethz.seb.sebserver.gui.service.i18n.I18nSupport;
+import ch.ethz.seb.sebserver.gui.service.i18n.LocTextKey;
+import ch.ethz.seb.sebserver.gui.service.page.PageContext;
+import ch.ethz.seb.sebserver.gui.service.page.PageService;
+import ch.ethz.seb.sebserver.gui.service.page.TemplateComposer;
+import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.institution.GetInstitutionInfo;
+import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.WebserviceURIService;
+import ch.ethz.seb.sebserver.gui.widget.WidgetFactory;
+
+@Lazy
+@Component
+@GuiProfile
+public class RegisterPage implements TemplateComposer {
+
+    private static final Logger log = LoggerFactory.getLogger(RegisterPage.class);
+
+    static final LocTextKey TITLE_TEXT_KEY =
+            new LocTextKey("sebserver.login.register.form.title");
+    static final LocTextKey FORM_PASSWORD_CONFIRM_TEXT_KEY =
+            new LocTextKey("sebserver.useraccount.form.password.confirm");
+    static final LocTextKey FORM_PASSWORD_TEXT_KEY =
+            new LocTextKey("sebserver.useraccount.form.password");
+    static final LocTextKey FORM_ROLES_TEXT_KEY =
+            new LocTextKey("sebserver.useraccount.form.roles");
+    static final LocTextKey FORM_TIMEZONE_TEXT_KEY =
+            new LocTextKey("sebserver.useraccount.form.timezone");
+    static final LocTextKey FORM_MAIL_TEXT_KEY =
+            new LocTextKey("sebserver.useraccount.form.mail");
+    static final LocTextKey FORM_USERNAME_TEXT_KEY =
+            new LocTextKey("sebserver.useraccount.form.username");
+    static final LocTextKey FORM_NAME_TEXT_KEY =
+            new LocTextKey("sebserver.useraccount.form.name");
+    static final LocTextKey FORM_INSTITUTION_TEXT_KEY =
+            new LocTextKey("sebserver.useraccount.form.institution");
+    static final LocTextKey FORM_LANG_TEXT_KEY =
+            new LocTextKey("sebserver.useraccount.form.language");
+
+    private final PageService pageService;
+    private final ResourceService resourceService;
+    private final WidgetFactory widgetFactory;
+    private final I18nSupport i18nSupport;
+    private final WebserviceURIService webserviceURIService;
+    private final RestTemplate restTemplate;
+    private final boolean multilingual;
+
+    protected RegisterPage(
+            final PageService pageService,
+            final WebserviceURIService webserviceURIService,
+            final ClientHttpRequestFactoryService clientHttpRequestFactoryService,
+            @Value("${sebserver.gui.multilingual:false}") final Boolean multilingual) {
+
+        this.pageService = pageService;
+        this.resourceService = pageService.getResourceService();
+        this.widgetFactory = pageService.getWidgetFactory();
+        this.i18nSupport = pageService.getI18nSupport();
+        this.webserviceURIService = webserviceURIService;
+        this.multilingual = BooleanUtils.toBoolean(multilingual);
+
+        this.restTemplate = new RestTemplate();
+
+        final ClientHttpRequestFactory clientHttpRequestFactory = clientHttpRequestFactoryService
+                .getClientHttpRequestFactory()
+                .getOrThrow();
+
+        this.restTemplate.setRequestFactory(clientHttpRequestFactory);
+    }
+
+    @Override
+    public void compose(final PageContext pageContext) {
+
+        final String institutionId = InstitutionalAuthenticationEntryPoint
+                .extractInstitutionalEndpoint();
+
+        final List<EntityName> institutions = this.pageService
+                .getRestService()
+                .getBuilder(GetInstitutionInfo.class)
+                .withRestTemplate(this.restTemplate)
+                .withURIVariable(API.INFO_PARAM_INST_SUFFIX, institutionId)
+                .call()
+                .getOrThrow();
+
+        final boolean definedInstitution = institutions.size() == 1;
+        final Supplier<List<Tuple<String>>> instResources = () -> institutions
+                .stream()
+                .map(entityName -> new Tuple<>(entityName.modelId, entityName.name))
+                .sorted(ResourceService.RESOURCE_COMPARATOR)
+                .collect(Collectors.toList());
+
+        final Composite content = this.widgetFactory.defaultPageLayout(
+                pageContext.getParent(),
+                TITLE_TEXT_KEY);
+        content.setData(RWT.CUSTOM_VARIANT, WidgetFactory.CustomVariant.LOGIN.key);
+
+        // The UserAccount form
+        final FormBuilder formBuilder = this.pageService.formBuilder(
+                pageContext.copyOf(content))
+                .readonly(false)
+                .putStaticValueIf(
+                        () -> !this.multilingual,
+                        Domain.USER.ATTR_LANGUAGE,
+                        "en")
+                .addField(FormBuilder.singleSelection(
+                        Domain.USER.ATTR_INSTITUTION_ID,
+                        FORM_INSTITUTION_TEXT_KEY,
+                        (definedInstitution) ? institutions.get(0).modelId : null,
+                        instResources)
+                        .readonly(definedInstitution))
+                .addField(FormBuilder.text(
+                        Domain.USER.ATTR_NAME,
+                        FORM_NAME_TEXT_KEY))
+                .addField(FormBuilder.text(
+                        Domain.USER.ATTR_USERNAME,
+                        FORM_USERNAME_TEXT_KEY))
+                .addField(FormBuilder.text(
+                        Domain.USER.ATTR_EMAIL,
+                        FORM_MAIL_TEXT_KEY))
+                .addFieldIf(
+                        () -> this.multilingual,
+                        () -> FormBuilder.singleSelection(
+                                Domain.USER.ATTR_LANGUAGE,
+                                FORM_LANG_TEXT_KEY,
+                                Constants.DEFAULT_LANG_CODE,
+                                this.resourceService::languageResources))
+                .addField(FormBuilder.singleSelection(
+                        Domain.USER.ATTR_TIMEZONE,
+                        FORM_TIMEZONE_TEXT_KEY,
+                        Constants.DEFAULT_TIME_ZONE_CODE,
+                        this.resourceService::timeZoneResources))
+                .addField(FormBuilder.text(
+                        PasswordChange.ATTR_NAME_NEW_PASSWORD,
+                        FORM_PASSWORD_TEXT_KEY)
+                        .asPasswordField())
+                .addField(FormBuilder.text(
+                        PasswordChange.ATTR_NAME_CONFIRM_NEW_PASSWORD,
+                        FORM_PASSWORD_CONFIRM_TEXT_KEY)
+                        .asPasswordField());
+
+        //formBuilder.formParent.setData(RWT.CUSTOM_VARIANT, WidgetFactory.CustomVariant.LOGIN_BACK.key);
+        formBuilder.build();
+
+        final Composite buttons = new Composite(content, SWT.NONE);
+        buttons.setLayoutData(new GridData(SWT.FILL, SWT.TOP, true, false));
+        buttons.setLayout(new GridLayout(2, false));
+        buttons.setData(RWT.CUSTOM_VARIANT, WidgetFactory.CustomVariant.LOGIN_BACK.key);
+
+        final Button registerButton = this.widgetFactory.buttonLocalized(buttons, "sebserver.login.register");
+        GridData gridData = new GridData(SWT.LEFT, SWT.TOP, false, false);
+        gridData.verticalIndent = 10;
+        registerButton.setLayoutData(gridData);
+        registerButton.addListener(SWT.Selection, event -> {
+
+        });
+
+        final Button cancelButton = this.widgetFactory.buttonLocalized(buttons, "sebserver.overall.action.cancel");
+        gridData = new GridData(SWT.LEFT, SWT.TOP, false, false);
+        gridData.verticalIndent = 10;
+        cancelButton.setLayoutData(gridData);
+        cancelButton.addListener(SWT.Selection, event -> {
+            pageContext.forwardToLoginPage();
+        });
+
+    }
+
+}
diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/content/UserAccountForm.java b/src/main/java/ch/ethz/seb/sebserver/gui/content/UserAccountForm.java
index 294a4203..265de53b 100644
--- a/src/main/java/ch/ethz/seb/sebserver/gui/content/UserAccountForm.java
+++ b/src/main/java/ch/ethz/seb/sebserver/gui/content/UserAccountForm.java
@@ -10,8 +10,10 @@ package ch.ethz.seb.sebserver.gui.content;
 
 import java.util.function.BooleanSupplier;
 
+import org.apache.commons.lang3.BooleanUtils;
 import org.apache.tomcat.util.buf.StringUtils;
 import org.eclipse.swt.widgets.Composite;
+import org.springframework.beans.factory.annotation.Value;
 import org.springframework.context.annotation.Lazy;
 import org.springframework.stereotype.Component;
 
@@ -74,16 +76,21 @@ public class UserAccountForm implements TemplateComposer {
             new LocTextKey("sebserver.useraccount.form.name");
     static final LocTextKey FORM_INSTITUTION_TEXT_KEY =
             new LocTextKey("sebserver.useraccount.form.institution");
+    static final LocTextKey FORM_LANG_TEXT_KEY =
+            new LocTextKey("sebserver.useraccount.form.language");
 
     private final PageService pageService;
     private final ResourceService resourceService;
+    private final boolean multilingual;
 
     protected UserAccountForm(
             final PageService pageService,
-            final ResourceService resourceService) {
+            final ResourceService resourceService,
+            @Value("${sebserver.gui.multilingual:false}") final Boolean multilingual) {
 
         this.pageService = pageService;
         this.resourceService = resourceService;
+        this.multilingual = BooleanUtils.toBoolean(multilingual);
     }
 
     @Override
@@ -149,7 +156,8 @@ public class UserAccountForm implements TemplateComposer {
                 .putStaticValueIf(isNotNew,
                         Domain.USER.ATTR_INSTITUTION_ID,
                         String.valueOf(userAccount.getInstitutionId()))
-                .putStaticValue(
+                .putStaticValueIf(
+                        () -> !this.multilingual,
                         Domain.USER.ATTR_LANGUAGE,
                         "en")
                 .addFieldIf(
@@ -172,12 +180,13 @@ public class UserAccountForm implements TemplateComposer {
                         Domain.USER.ATTR_EMAIL,
                         FORM_MAIL_TEXT_KEY,
                         userAccount.getEmail()))
-//                .addField(FormBuilder.singleSelection(
-//                        Domain.USER.ATTR_LANGUAGE,
-//                        "sebserver.useraccount.form.language",
-//                        userAccount.getLanguage().getLanguage(),
-//                        this.resourceService::languageResources)
-//                        .readonly(true))
+                .addFieldIf(
+                        () -> this.multilingual,
+                        () -> FormBuilder.singleSelection(
+                                Domain.USER.ATTR_LANGUAGE,
+                                FORM_LANG_TEXT_KEY,
+                                userAccount.getLanguage().getLanguage(),
+                                this.resourceService::languageResources))
                 .addField(FormBuilder.singleSelection(
                         Domain.USER.ATTR_TIMEZONE,
                         FORM_TIMEZONE_TEXT_KEY,
diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/page/impl/DefaultLoginPage.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/page/impl/DefaultLoginPage.java
index 588759cc..a39744a2 100644
--- a/src/main/java/ch/ethz/seb/sebserver/gui/service/page/impl/DefaultLoginPage.java
+++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/page/impl/DefaultLoginPage.java
@@ -11,6 +11,7 @@ package ch.ethz.seb.sebserver.gui.service.page.impl;
 import org.springframework.context.annotation.Lazy;
 import org.springframework.stereotype.Component;
 
+import ch.ethz.seb.sebserver.gbl.profile.GuiProfile;
 import ch.ethz.seb.sebserver.gui.content.LoginPage;
 import ch.ethz.seb.sebserver.gui.service.page.PageContext;
 import ch.ethz.seb.sebserver.gui.service.page.PageContext.AttributeKeys;
@@ -21,6 +22,7 @@ import ch.ethz.seb.sebserver.gui.service.page.TemplateComposer;
  * SEBLogin template */
 @Lazy
 @Component
+@GuiProfile
 public class DefaultLoginPage implements PageDefinition {
 
     @Override
diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/page/impl/DefaultMainPage.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/page/impl/DefaultMainPage.java
index f34165f9..a0b72f72 100644
--- a/src/main/java/ch/ethz/seb/sebserver/gui/service/page/impl/DefaultMainPage.java
+++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/page/impl/DefaultMainPage.java
@@ -11,6 +11,7 @@ package ch.ethz.seb.sebserver.gui.service.page.impl;
 import org.springframework.context.annotation.Lazy;
 import org.springframework.stereotype.Component;
 
+import ch.ethz.seb.sebserver.gbl.profile.GuiProfile;
 import ch.ethz.seb.sebserver.gui.content.MainPage;
 import ch.ethz.seb.sebserver.gui.service.page.PageContext;
 import ch.ethz.seb.sebserver.gui.service.page.PageContext.AttributeKeys;
@@ -21,6 +22,7 @@ import ch.ethz.seb.sebserver.gui.service.page.TemplateComposer;
  * SEBMainPage template */
 @Lazy
 @Component
+@GuiProfile
 public class DefaultMainPage implements PageDefinition {
 
     @Override
diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/page/impl/DefaultRegisterPage.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/page/impl/DefaultRegisterPage.java
new file mode 100644
index 00000000..01b4848f
--- /dev/null
+++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/page/impl/DefaultRegisterPage.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (c) 2020 ETH Zürich, Educational Development and Technology (LET)
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+package ch.ethz.seb.sebserver.gui.service.page.impl;
+
+import org.springframework.context.annotation.Lazy;
+import org.springframework.stereotype.Component;
+
+import ch.ethz.seb.sebserver.gbl.profile.GuiProfile;
+import ch.ethz.seb.sebserver.gui.content.RegisterPage;
+import ch.ethz.seb.sebserver.gui.service.page.PageContext;
+import ch.ethz.seb.sebserver.gui.service.page.PageContext.AttributeKeys;
+import ch.ethz.seb.sebserver.gui.service.page.PageDefinition;
+import ch.ethz.seb.sebserver.gui.service.page.TemplateComposer;
+
+@Lazy
+@Component
+@GuiProfile
+public class DefaultRegisterPage implements PageDefinition {
+
+    @Override
+    public Class<? extends TemplateComposer> composer() {
+        return DefaultPageLayout.class;
+    }
+
+    @Override
+    public PageContext applyPageContext(final PageContext pageContext) {
+        return pageContext.withAttribute(
+                AttributeKeys.PAGE_TEMPLATE_COMPOSER_NAME,
+                RegisterPage.class.getName());
+    }
+
+}
diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/RestCall.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/RestCall.java
index 369182d8..9895de7e 100644
--- a/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/RestCall.java
+++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/RestCall.java
@@ -59,6 +59,7 @@ public abstract class RestCall<T> {
         GET_DEPENDENCIES,
         GET_LIST,
         NEW,
+        REGISTER,
         SAVE,
         DELETE,
         ACTIVATION_ACTIVATE,
diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/institution/GetInstitutionInfo.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/institution/GetInstitutionInfo.java
new file mode 100644
index 00000000..fd6efd3f
--- /dev/null
+++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/institution/GetInstitutionInfo.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (c) 2020 ETH Zürich, Educational Development and Technology (LET)
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+package ch.ethz.seb.sebserver.gui.service.remote.webservice.api.institution;
+
+import java.util.List;
+
+import org.springframework.context.annotation.Lazy;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.MediaType;
+import org.springframework.stereotype.Component;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+
+import ch.ethz.seb.sebserver.gbl.api.API;
+import ch.ethz.seb.sebserver.gbl.api.EntityType;
+import ch.ethz.seb.sebserver.gbl.model.EntityName;
+import ch.ethz.seb.sebserver.gbl.profile.GuiProfile;
+import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestCall;
+
+@Lazy
+@Component
+@GuiProfile
+public class GetInstitutionInfo extends RestCall<List<EntityName>> {
+
+    public GetInstitutionInfo() {
+        super(new TypeKey<>(
+                CallType.GET_NAMES,
+                EntityType.INSTITUTION,
+                new TypeReference<List<EntityName>>() {
+                }),
+                HttpMethod.GET,
+                MediaType.APPLICATION_FORM_URLENCODED,
+                API.INFO_ENDPOINT + API.INFO_INST_ENDPOINT);
+    }
+
+}
diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/useraccount/RegisterNewUser.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/useraccount/RegisterNewUser.java
new file mode 100644
index 00000000..546813fd
--- /dev/null
+++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/useraccount/RegisterNewUser.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (c) 2020 ETH Zürich, Educational Development and Technology (LET)
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+package ch.ethz.seb.sebserver.gui.service.remote.webservice.api.useraccount;
+
+import org.springframework.context.annotation.Lazy;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.MediaType;
+import org.springframework.stereotype.Component;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+
+import ch.ethz.seb.sebserver.gbl.api.API;
+import ch.ethz.seb.sebserver.gbl.api.EntityType;
+import ch.ethz.seb.sebserver.gbl.model.user.UserInfo;
+import ch.ethz.seb.sebserver.gbl.profile.GuiProfile;
+import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestCall;
+
+@Lazy
+@Component
+@GuiProfile
+public class RegisterNewUser extends RestCall<UserInfo> {
+
+    public RegisterNewUser() {
+        super(new TypeKey<>(
+                CallType.REGISTER,
+                EntityType.USER,
+                new TypeReference<UserInfo>() {
+                }),
+                HttpMethod.POST,
+                MediaType.APPLICATION_FORM_URLENCODED,
+                API.REGISTER_ENDPOINT);
+    }
+
+}
diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/auth/WebserviceURIService.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/auth/WebserviceURIService.java
index 150fae4c..a3565b49 100644
--- a/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/auth/WebserviceURIService.java
+++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/auth/WebserviceURIService.java
@@ -19,6 +19,7 @@ import ch.ethz.seb.sebserver.gbl.profile.GuiProfile;
 @GuiProfile
 public class WebserviceURIService {
 
+    private final String servletContextPath;
     private final String webserviceServerAddress;
     private final UriComponentsBuilder webserviceURIBuilder;
 
@@ -26,12 +27,15 @@ public class WebserviceURIService {
             @Value("${sebserver.gui.webservice.protocol}") final String webserviceProtocol,
             @Value("${sebserver.gui.webservice.address}") final String webserviceServerAdress,
             @Value("${sebserver.gui.webservice.port}") final String webserviceServerPort,
+            @Value("${server.servlet.context-path}") final String servletContextPath,
             @Value("${sebserver.gui.webservice.apipath}") final String webserviceAPIPath) {
 
+        this.servletContextPath = servletContextPath;
         this.webserviceServerAddress = webserviceProtocol + "://" + webserviceServerAdress + ":" + webserviceServerPort;
         this.webserviceURIBuilder = UriComponentsBuilder
                 .fromHttpUrl(webserviceProtocol + "://" + webserviceServerAdress)
                 .port(webserviceServerPort)
+                .path(servletContextPath)
                 .path(webserviceAPIPath);
     }
 
@@ -45,12 +49,14 @@ public class WebserviceURIService {
 
     public String getOAuthTokenURI() {
         return UriComponentsBuilder.fromHttpUrl(this.webserviceServerAddress)
+                .path(this.servletContextPath)
                 .path(API.OAUTH_TOKEN_ENDPOINT)
                 .toUriString();
     }
 
     public String getOAuthRevokeTokenURI() {
         return UriComponentsBuilder.fromHttpUrl(this.webserviceServerAddress)
+                .path(this.servletContextPath)
                 .path(API.OAUTH_REVOKE_TOKEN_ENDPOINT)
                 .toUriString();
     }
diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/widget/WidgetFactory.java b/src/main/java/ch/ethz/seb/sebserver/gui/widget/WidgetFactory.java
index c3cc2b7e..5a3121b4 100644
--- a/src/main/java/ch/ethz/seb/sebserver/gui/widget/WidgetFactory.java
+++ b/src/main/java/ch/ethz/seb/sebserver/gui/widget/WidgetFactory.java
@@ -152,7 +152,10 @@ public class WidgetFactory {
         CONFIG_INPUT_READONLY("inputreadonly"),
 
         DARK_COLOR_LABEL("colordark"),
-        LIGHT_COLOR_LABEL("colorlight")
+        LIGHT_COLOR_LABEL("colorlight"),
+
+        LOGIN("login"),
+        LOGIN_BACK("login-back")
 
         ;
 
diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/InfoController.java b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/InfoController.java
index 9d59c2df..2082757c 100644
--- a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/InfoController.java
+++ b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/InfoController.java
@@ -9,7 +9,9 @@
 package ch.ethz.seb.sebserver.webservice.weblayer.api;
 
 import java.util.Collection;
+import java.util.stream.Collectors;
 
+import org.apache.commons.lang3.BooleanUtils;
 import org.springframework.http.MediaType;
 import org.springframework.web.bind.annotation.PathVariable;
 import org.springframework.web.bind.annotation.RequestMapping;
@@ -18,6 +20,7 @@ import org.springframework.web.bind.annotation.RestController;
 
 import ch.ethz.seb.sebserver.gbl.api.API;
 import ch.ethz.seb.sebserver.gbl.api.authorization.Privilege;
+import ch.ethz.seb.sebserver.gbl.model.EntityKey;
 import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
 import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.AuthorizationService;
 import ch.ethz.seb.sebserver.webservice.servicelayer.dao.InstitutionDAO;
@@ -51,12 +54,28 @@ public class InfoController {
                 .all(null, true)
                 .getOrThrow()
                 .stream()
-                .filter(inst -> inst.urlSuffix != null && urlSuffix.endsWith(inst.urlSuffix))
+                .filter(inst -> inst.urlSuffix != null && urlSuffix.equals(inst.urlSuffix))
                 .findFirst()
                 .map(inst -> inst.logoImage)
                 .orElse(null);
     }
 
+    @RequestMapping(
+            path = API.INFO_INST_ENDPOINT,
+            method = RequestMethod.GET,
+            produces = MediaType.APPLICATION_JSON_VALUE)
+    public Collection<EntityKey> getInstitutionInfo(@PathVariable(required = false) final String urlSuffix) {
+        return this.institutionDAO
+                .all(null, true)
+                .getOrThrow()
+                .stream()
+                .filter(inst -> BooleanUtils.isTrue(inst.active) &&
+                        (inst.urlSuffix == null ||
+                                urlSuffix.equals(inst.urlSuffix)))
+                .map(inst -> inst.getEntityKey())
+                .collect(Collectors.toList());
+    }
+
     @RequestMapping(
             path = API.PRIVILEGES_PATH_SEGMENT,
             method = RequestMethod.GET,
diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/RegisterUserController.java b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/RegisterUserController.java
new file mode 100644
index 00000000..60c1ad84
--- /dev/null
+++ b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/RegisterUserController.java
@@ -0,0 +1,152 @@
+/*
+ * Copyright (c) 2020 ETH Zürich, Educational Development and Technology (LET)
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+package ch.ethz.seb.sebserver.webservice.weblayer.api;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Locale;
+
+import org.apache.commons.lang3.StringUtils;
+import org.joda.time.DateTimeZone;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.http.MediaType;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.validation.FieldError;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import ch.ethz.seb.sebserver.WebSecurityConfig;
+import ch.ethz.seb.sebserver.gbl.Constants;
+import ch.ethz.seb.sebserver.gbl.api.API;
+import ch.ethz.seb.sebserver.gbl.api.APIMessage;
+import ch.ethz.seb.sebserver.gbl.api.APIMessage.APIMessageException;
+import ch.ethz.seb.sebserver.gbl.model.Domain;
+import ch.ethz.seb.sebserver.gbl.model.user.PasswordChange;
+import ch.ethz.seb.sebserver.gbl.model.user.UserInfo;
+import ch.ethz.seb.sebserver.gbl.model.user.UserMod;
+import ch.ethz.seb.sebserver.gbl.model.user.UserRole;
+import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
+import ch.ethz.seb.sebserver.gbl.util.Utils;
+import ch.ethz.seb.sebserver.webservice.servicelayer.client.ClientCredentialService;
+import ch.ethz.seb.sebserver.webservice.servicelayer.dao.InstitutionDAO;
+import ch.ethz.seb.sebserver.webservice.servicelayer.dao.UserActivityLogDAO;
+import ch.ethz.seb.sebserver.webservice.servicelayer.dao.UserDAO;
+
+@WebServiceProfile
+@RestController
+@RequestMapping("${sebserver.webservice.api.admin.endpoint}" + API.REGISTER_ENDPOINT)
+public class RegisterUserController {
+
+    private final InstitutionDAO institutionDAO;
+    private final UserActivityLogDAO userActivityLogDAO;
+    private final UserDAO userDAO;
+    private final ClientCredentialService clientCredentialService;
+
+    protected RegisterUserController(
+            final InstitutionDAO institutionDAO,
+            final UserActivityLogDAO userActivityLogDAO,
+            final UserDAO userDAO,
+            final ClientCredentialService clientCredentialService,
+            @Qualifier(WebSecurityConfig.USER_PASSWORD_ENCODER_BEAN_NAME) final PasswordEncoder userPasswordEncoder) {
+
+        this.institutionDAO = institutionDAO;
+        this.userActivityLogDAO = userActivityLogDAO;
+        this.userDAO = userDAO;
+        this.clientCredentialService = clientCredentialService;
+    }
+
+    @RequestMapping(
+            method = RequestMethod.POST,
+            consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE,
+            produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
+    public UserInfo registerNewUser(
+            @RequestParam(name = Domain.USER.ATTR_INSTITUTION_ID, required = true) final String institutionId,
+            @RequestParam(name = Domain.USER.ATTR_NAME, required = true) final String name,
+            @RequestParam(name = Domain.USER.ATTR_USERNAME, required = true) final String username,
+            @RequestParam(name = Domain.USER.ATTR_EMAIL, required = false) final String email,
+            @RequestParam(
+                    name = Domain.USER.ATTR_LANGUAGE,
+                    required = false,
+                    defaultValue = Constants.DEFAULT_LANG_CODE) final String lang,
+            @RequestParam(
+                    name = Domain.USER.ATTR_TIMEZONE,
+                    required = false,
+                    defaultValue = Constants.DEFAULT_TIME_ZONE_CODE) final String timezone,
+            @RequestParam(name = PasswordChange.ATTR_NAME_NEW_PASSWORD, required = true) final String pwd,
+            @RequestParam(name = PasswordChange.ATTR_NAME_CONFIRM_NEW_PASSWORD, required = true) final String rpwd) {
+
+        final Collection<APIMessage> errors = new ArrayList<>();
+
+        // check institution info
+        Long instId = null;
+        if (StringUtils.isNotBlank(institutionId)) {
+            try {
+                instId = Long.parseLong(institutionId);
+            } catch (final Exception e) {
+                instId = this.institutionDAO
+                        .all(null, true)
+                        .getOrThrow()
+                        .stream()
+                        .filter(inst -> inst.urlSuffix != null && institutionId.equals(inst.urlSuffix))
+                        .findFirst()
+                        .map(inst -> inst.id)
+                        .orElse(null);
+            }
+        }
+
+        if (instId == null) {
+            errors.add(APIMessage.fieldValidationError(
+                    new FieldError(
+                            "user",
+                            Domain.USER.ATTR_INSTITUTION_ID,
+                            "user:institutionId:notNull")));
+        }
+
+        // check password-match
+        final CharSequence rawPWD = this.clientCredentialService.decrypt(pwd);
+        final CharSequence rawRPWD = this.clientCredentialService.decrypt(rpwd);
+        if (!rawPWD.equals(rawRPWD)) {
+            errors.add(APIMessage.fieldValidationError(
+                    new FieldError(
+                            "passwordChange",
+                            PasswordChange.ATTR_NAME_CONFIRM_NEW_PASSWORD,
+                            "user:confirmNewPassword:password.mismatch")));
+        }
+
+        if (!errors.isEmpty()) {
+            throw new APIMessageException(errors);
+        }
+
+        final UserMod user = new UserMod(
+                null,
+                instId,
+                name,
+                username,
+                rawPWD,
+                rawRPWD,
+                email,
+                Locale.forLanguageTag(lang),
+                DateTimeZone.forID(timezone),
+                new HashSet<>(Arrays.asList(UserRole.EXAM_SUPPORTER.name())));
+
+        return this.userDAO.createNew(user)
+                .flatMap(this.userActivityLogDAO::logCreate)
+                .map(u -> {
+                    Utils.clear(rawPWD);
+                    Utils.clear(rawRPWD);
+                    return u;
+                })
+                .getOrThrow();
+    }
+
+}
diff --git a/src/main/resources/config/application-dev-gui.properties b/src/main/resources/config/application-dev-gui.properties
index 57722038..3efe1d93 100644
--- a/src/main/resources/config/application-dev-gui.properties
+++ b/src/main/resources/config/application-dev-gui.properties
@@ -1,6 +1,5 @@
 server.address=localhost
 server.port=8080
-server.servlet.context-path=/
 
 sebserver.gui.entrypoint=/gui
 sebserver.gui.webservice.protocol=http
diff --git a/src/main/resources/config/application-dev-ws.properties b/src/main/resources/config/application-dev-ws.properties
index 568065da..0ab3a411 100644
--- a/src/main/resources/config/application-dev-ws.properties
+++ b/src/main/resources/config/application-dev-ws.properties
@@ -1,6 +1,5 @@
 server.address=localhost
 server.port=8090
-server.servlet.context-path=/
 
 logging.file=log/sebserver.log
 
diff --git a/src/main/resources/config/application.properties b/src/main/resources/config/application.properties
index edf11caf..7a9c6c95 100644
--- a/src/main/resources/config/application.properties
+++ b/src/main/resources/config/application.properties
@@ -110,6 +110,7 @@ sebserver.webservice.lms.address.alias=
 ##########################################################
 ### SEB Server GUI configuration
 
+sebserver.gui.self-registering=true
 sebserver.gui.multilingual=false
 sebserver.gui.supported.languages=en
 sebserver.gui.entrypoint=/gui
diff --git a/src/main/resources/messages.properties b/src/main/resources/messages.properties
index 0aec533f..f26cda57 100644
--- a/src/main/resources/messages.properties
+++ b/src/main/resources/messages.properties
@@ -107,6 +107,10 @@ sebserver.logout.invalid-session.message=You have been signed out because of a u
 sebserver.login.password.change=Information
 sebserver.login.password.change.success=The password was successfully changed. Please sign in with your new password
 
+sebserver.login.register=Register
+sebserver.login.register.form.title=Register As New User
+
+
 ################################
 # Main Page
 ################################
diff --git a/src/main/resources/static/css/sebserver.css b/src/main/resources/static/css/sebserver.css
index b73a2860..573882da 100644
--- a/src/main/resources/static/css/sebserver.css
+++ b/src/main/resources/static/css/sebserver.css
@@ -120,6 +120,10 @@ Label.colorlight {
     padding: 2px 5px 2px 5px;
 }
 
+Composite {
+    background-color: transparent;
+}
+
 Composite.bordered {
     border: 2px;
 }
@@ -177,6 +181,12 @@ Composite.login {
     border-radius: 2px;
 }
 
+Composite.login-back {
+    background-color: #EAECEE;
+    margin: 0px 0 0 0;
+    padding: 0px 0px 0px 0px;
+}
+
 Composite.error {
     border: 1px solid #aa0000;
     border-radius: 1px;
diff --git a/src/main/resources/static/images/save.png b/src/main/resources/static/images/save.png
index c318014d899011b80ee36899821a5206bc4f28ab..0c878999c7e7b9be72945ce7da26c2652b2059bc 100644
GIT binary patch
delta 147
zcmV;E0BrxI0j~j&B!6~EL_t(|0j-gt4S+BZMIUouVGrg)GfoFUAZ`R4*d_!l8<Yxq
z$u&^b_w2iTaa5*fK{97r430ZXZUWwB6150HvvL%jQG}JjxW18?yW9%V5iLiIK*Lyy
zb`slv<VpC_+(Z+dzUCZ3y*LS~L+R{;)rF3#Jg$w3U6c*Eq6`24002ovPDHLkV1l(&
BJo5km

delta 137
zcmV;40CxYc0i^+uB!6s4L_t(|0b`&cm;i(X!2uu~Jc<|@tl)~QfIO_AhT%7Z7g&)8
zkX4D@3VDVn3_ls3F}whhPk=nIIGQ4;vj0E`R)SR#Oc_iGLL8H$0E7%U6cG-NH4Gns
rTuk@@C<6y7412ID+5?oKDgXfN%RL<8&qQ<p0000<MNUMnLIPldEg~-j

diff --git a/src/main/resources/static/images/save_alt.png b/src/main/resources/static/images/save_alt.png
new file mode 100644
index 0000000000000000000000000000000000000000..c318014d899011b80ee36899821a5206bc4f28ab
GIT binary patch
literal 165
zcmeAS@N?(olHy`uVBq!ia0vp^LLkh+0wn(&ce?|may(reLn;`P7bwnPXkj_Xs32q0
zB_X-yLD!lFn>7pC9-23cM@(d&5ZZZ{tFld<$6j)t;U0!b{tY%8EM_UK*!16lBdjS@
z&_~jT(@AFXVultT3lTxSK2sj~h8Uk8j2g`BT0GSov~Dp^S*XRp@aD9w%%k%zB|v)_
NJYD@<);T3K0RR(&GHU<;

literal 0
HcmV?d00001

diff --git a/src/test/java/ch/ethz/seb/sebserver/gui/integration/GuiIntegrationTest.java b/src/test/java/ch/ethz/seb/sebserver/gui/integration/GuiIntegrationTest.java
index 7757bbcf..0df19573 100644
--- a/src/test/java/ch/ethz/seb/sebserver/gui/integration/GuiIntegrationTest.java
+++ b/src/test/java/ch/ethz/seb/sebserver/gui/integration/GuiIntegrationTest.java
@@ -74,7 +74,7 @@ public abstract class GuiIntegrationTest {
 
         final HttpSession sessionMock = Mockito.mock(HttpSession.class);
         final WebserviceURIService webserviceURIService = new WebserviceURIService(
-                "http", "localhost", "8080", this.endpoint);
+                "http", "localhost", "8080", "/", this.endpoint);
 
         final ClientHttpRequestFactoryService clientHttpRequestFactoryService = Mockito
                 .mock(ClientHttpRequestFactoryService.class);