diff --git a/.gitignore b/.gitignore
index 5fd8ac473c828bc433648b6f78402c2eab4d0583..af6004a96748710f3c51d22fdd2046c32db86500 100644
--- a/.gitignore
+++ b/.gitignore
@@ -15,4 +15,5 @@ local/
 target/
 *.swp
 *.vscode
+.vscode/
 nomad.yaml
diff --git a/gui/package.json b/gui/package.json
index d0aa28092615accb7dc89952873aae70f396d5f7..c377ed0865f75ae7845e2f412442126d2bc68eb9 100644
--- a/gui/package.json
+++ b/gui/package.json
@@ -13,6 +13,7 @@
     "fetch": "^1.1.0",
     "file-saver": "^2.0.0",
     "html-to-react": "^1.3.3",
+    "keycloak-js": "^6.0.0",
     "marked": "^0.6.0",
     "material-ui-chip-input": "^1.0.0-beta.14",
     "material-ui-flat-pagination": "^3.2.0",
@@ -28,6 +29,7 @@
     "react-dropzone": "^5.0.1",
     "react-highlight": "^0.12.0",
     "react-json-view": "^1.19.1",
+    "react-keycloak": "^6.1.0",
     "react-router-dom": "^4.3.1",
     "react-router-hash-link": "^1.2.0",
     "react-scripts": "1.1.4",
diff --git a/gui/src/components/LoginLogout.js b/gui/src/components/LoginLogout.js
index 0d474e2cbb2259abcaa4d3805cd3c1f6024b83dd..297c79eb311b46d94c50b2f7af94d0c36e5f1bf3 100644
--- a/gui/src/components/LoginLogout.js
+++ b/gui/src/components/LoginLogout.js
@@ -6,6 +6,7 @@ import { compose } from 'recompose'
 import { Button, DialogTitle, DialogContent, DialogContentText, TextField, DialogActions,
   Dialog, FormGroup } from '@material-ui/core'
 import { withApi } from './api'
+import { withKeycloak } from 'react-keycloak'
 
 class LoginLogout extends React.Component {
   static propTypes = {
@@ -18,7 +19,8 @@ class LoginLogout extends React.Component {
     variant: PropTypes.string,
     color: PropTypes.string,
     onLoggedIn: PropTypes.func,
-    onLoggedOut: PropTypes.func
+    onLoggedOut: PropTypes.func,
+    keycloak: PropTypes.object.isRequired
   }
 
   static styles = theme => ({
@@ -102,7 +104,13 @@ class LoginLogout extends React.Component {
   }
 
   render() {
-    const { classes, user, variant, color, isLoggingIn } = this.props
+    const { classes, variant, color, isLoggingIn, keycloak } = this.props
+
+    let user = null
+    if (keycloak.authenticated) {
+      user = {}
+    }
+
     const { failure } = this.state
     if (user) {
       return (
@@ -113,7 +121,7 @@ class LoginLogout extends React.Component {
           <Button
             className={classes.button}
             variant={variant} color={color}
-            onClick={this.handleLogout}
+            onClick={() => keycloak.logout()}
           >Logout</Button>
         </div>
       )
@@ -121,8 +129,7 @@ class LoginLogout extends React.Component {
       return (
         <div className={classes.root}>
           <Button
-            className={isLoggingIn ? classes.buttonDisabled : classes.button} variant={variant} color={color} disabled={isLoggingIn}
-            onClick={() => this.setState({loginDialogOpen: true})}
+            className={classes.button} variant={variant} color={color} onClick={() => keycloak.login()}
           >Login</Button>
           <Dialog
             disableBackdropClick disableEscapeKeyDown
@@ -184,4 +191,4 @@ class LoginLogout extends React.Component {
   }
 }
 
-export default compose(withApi(false), withStyles(LoginLogout.styles))(LoginLogout)
+export default compose(withKeycloak, withApi(false), withStyles(LoginLogout.styles))(LoginLogout)
diff --git a/gui/src/index.js b/gui/src/index.js
index 2307e8d377cae0df9d643f3dfa3df5f9f8abd21a..fb221b09f9a107702d3893da39e8c489b9006132 100644
--- a/gui/src/index.js
+++ b/gui/src/index.js
@@ -9,14 +9,24 @@ import { Router } from 'react-router-dom'
 import history from './history'
 import PiwikReactRouter from 'piwik-react-router'
 import { sendTrackingData, matomoUrl, matomoSiteId } from './config'
+import Keycloak from 'keycloak-js'
+import { KeycloakProvider } from 'react-keycloak'
 
 const matomo = sendTrackingData ? PiwikReactRouter({
   url: matomoUrl,
   siteId: matomoSiteId
 }) : null
 
+const keycloak = Keycloak({
+  url: 'http://localhost:8002/auth',
+  realm: 'fairdi_nomad_test',
+  clientId: 'nomad_gui_dev'
+})
+
 ReactDOM.render(
-  <Router history={sendTrackingData ? matomo.connectToHistory(history) : history}>
-    <App />
-  </Router>, document.getElementById('root'))
+  <KeycloakProvider keycloak={keycloak} initConfig={{onLoad: 'check-sso'}} >
+    <Router history={sendTrackingData ? matomo.connectToHistory(history) : history}>
+      <App />
+    </Router>
+  </KeycloakProvider>, document.getElementById('root'))
 registerServiceWorker()
diff --git a/gui/yarn.lock b/gui/yarn.lock
index 03eefe91e86ce86abde66f4a97ebfde4914d52ea..397d77d324dfc18ee5ff50451bb614ac6e3e17b1 100644
--- a/gui/yarn.lock
+++ b/gui/yarn.lock
@@ -3922,7 +3922,7 @@ hoist-non-react-statics@^2.3.1, hoist-non-react-statics@^2.5.0:
   version "2.5.5"
   resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-2.5.5.tgz#c5903cf409c0dfd908f388e619d86b9c1174cb47"
 
-hoist-non-react-statics@^3.0.0:
+hoist-non-react-statics@^3.0.0, hoist-non-react-statics@^3.3.0:
   version "3.3.0"
   resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.0.tgz#b09178f0122184fb95acf525daaecb4d8f45958b"
   dependencies:
@@ -4985,6 +4985,14 @@ jsx-ast-utils@^2.0.0, jsx-ast-utils@^2.0.1:
   dependencies:
     array-includes "^3.0.3"
 
+keycloak-js@^6.0.0:
+  version "6.0.1"
+  resolved "https://registry.yarnpkg.com/keycloak-js/-/keycloak-js-6.0.1.tgz#329a5e77210dfc4a7d4acf96f95dd0132455bea3"
+
+keycloak@^1.2.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/keycloak/-/keycloak-1.2.0.tgz#2ff4cc57102842f2eecc2f4bb206306596d7b025"
+
 keycode@^2.1.7, keycode@^2.1.9:
   version "2.2.0"
   resolved "https://registry.yarnpkg.com/keycode/-/keycode-2.2.0.tgz#3d0af56dc7b8b8e5cba8d0a97f107204eec22b04"
@@ -6380,6 +6388,14 @@ prop-types@^15.5.10, prop-types@^15.5.4, prop-types@^15.5.8, prop-types@^15.6.0,
     loose-envify "^1.3.1"
     object-assign "^4.1.1"
 
+prop-types@^15.7.2:
+  version "15.7.2"
+  resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5"
+  dependencies:
+    loose-envify "^1.4.0"
+    object-assign "^4.1.1"
+    react-is "^16.8.1"
+
 proxy-addr@~2.0.4:
   version "2.0.4"
   resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.4.tgz#ecfc733bf22ff8c6f407fa275327b9ab67e48b93"
@@ -6632,6 +6648,10 @@ react-is@^16.3.2, react-is@^16.6.3, react-is@^16.7.0:
   version "16.7.0"
   resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.7.0.tgz#c1bd21c64f1f1364c6f70695ec02d69392f41bfa"
 
+react-is@^16.8.1:
+  version "16.9.0"
+  resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.9.0.tgz#21ca9561399aad0ff1a7701c01683e8ca981edcb"
+
 react-json-view@^1.19.1:
   version "1.19.1"
   resolved "https://registry.yarnpkg.com/react-json-view/-/react-json-view-1.19.1.tgz#95d8e59e024f08a25e5dc8f076ae304eed97cf5c"
@@ -6641,6 +6661,13 @@ react-json-view@^1.19.1:
     react-lifecycles-compat "^3.0.4"
     react-textarea-autosize "^6.1.0"
 
+react-keycloak@^6.1.0:
+  version "6.1.0"
+  resolved "https://registry.yarnpkg.com/react-keycloak/-/react-keycloak-6.1.0.tgz#672eed832de231e981a717413dc10286dbe701f2"
+  dependencies:
+    hoist-non-react-statics "^3.3.0"
+    prop-types "^15.7.2"
+
 react-lifecycles-compat@^3.0.2, react-lifecycles-compat@^3.0.4:
   version "3.0.4"
   resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362"
diff --git a/nomad/config.py b/nomad/config.py
index 3a6dc047ddc780a10fe64ea77e7476dfcdb9218d..daa6808c76152f5caab89032f0fbd7fc7d4f22e6 100644
--- a/nomad/config.py
+++ b/nomad/config.py
@@ -124,6 +124,15 @@ repository_db = NomadConfig(
     mode='fairdi'
 )
 
+keycloak = NomadConfig(
+    server_url='http://localhost:8002/auth/',
+    realm_name='fairdi_nomad_test',
+    username='admin',
+    password='password',
+    client_id='nomad_api',
+    client_secret_key='0f9ec82f-a1dc-4405-a80e-593160aeea42'
+)
+
 mongo = NomadConfig(
     host='localhost',
     port=27017,
diff --git a/nomad/infrastructure.py b/nomad/infrastructure.py
index b6bc92ec4f70643c54e1e4f82b5b2be11baf2f4c..3008788359a1b3f8a0964e4ff5d5db0fa3d2c360 100644
--- a/nomad/infrastructure.py
+++ b/nomad/infrastructure.py
@@ -32,6 +32,7 @@ from mongoengine import connect
 from passlib.hash import bcrypt
 import smtplib
 from email.mime.text import MIMEText
+import keycloak
 
 from nomad import config, utils
 
@@ -48,6 +49,11 @@ repository_db = None
 repository_db_conn = None
 """ The repository postgres db sqlalchemy connection. """
 
+keycloak_oidc_client = None
+""" The keycode OpenID connect client. """
+keycloak_admin_client = None
+""" The keycode admin client. """
+
 
 def setup():
     """
@@ -108,6 +114,26 @@ def setup_elastic():
     return elastic_client
 
 
+def setup_keycloak():
+    """ Creates a keycloak client. """
+    global keycloak_oidc_client
+    global keycloak_admin_client
+
+    keycloak_oidc_client = keycloak.KeycloakOpenID(
+        server_url=config.keycloak.server_url,
+        client_id=config.keycloak.client_id,
+        realm_name=config.keycloak.realm_name,
+        client_secret_key=config.keycloak.client_secret_key)
+
+    keycloak_admin_client = keycloak.KeycloakAdmin(
+        server_url=config.keycloak.server_url,
+        username=config.keycloak.username,
+        password=config.keycloak.password,
+        realm_name='master',
+        verify=True)
+    keycloak_admin_client.realm_name = config.keycloak.realm_name
+
+
 def setup_repository_db(**kwargs):
     """ Creates a connection and stores it in the module variables. """
     repo_args = dict(readonly=False)
@@ -424,12 +450,12 @@ def send_mail(name: str, email: str, message: str, subject: str):
 
     msg = MIMEText(message)
     msg['Subject'] = subject
-    msg['From'] = 'The nomad team <%s>' % config.mail.from_address
+    msg['From'] = 'The NOMAD team <%s>' % config.mail.from_address
     msg['To'] = name
     to_addrs = [email]
 
     if config.mail.cc_address is not None:
-        msg['Cc'] = 'The nomad team <%s>' % config.mail.cc_address
+        msg['Cc'] = 'The NOMAD team <%s>' % config.mail.cc_address
         to_addrs.append(config.mail.cc_address)
 
     try:
@@ -438,3 +464,21 @@ def send_mail(name: str, email: str, message: str, subject: str):
         logger.error('Could not send email', exc_info=e)
 
     server.quit()
+
+
+if __name__ == '__main__':
+    import logging, time
+
+    config.console_log_level = logging.DEBUG
+    setup_logging()
+    setup_keycloak()
+
+    token = keycloak_oidc_client.token(
+        username='sheldon.cooper@nomad-coe.eu', password='password')
+
+    while True:
+        print(keycloak_oidc_client.userinfo(token['access_token']))
+        keycloak_user_id = keycloak_admin_client.get_user_id('sheldon.cooper@nomad-coe.eu')
+        print(keycloak_admin_client.get_user(keycloak_user_id))
+        time.sleep(5)
+
diff --git a/nomad/utils.py b/nomad/utils.py
index e6a0a353c2f818b8d6989922d7fb55ac22c05143..36739ebc4f1f68e1bedd82b0504798037f04c29f 100644
--- a/nomad/utils.py
+++ b/nomad/utils.py
@@ -112,7 +112,10 @@ class LogstashHandler(logstash.TCPLogstashHandler):
 
     def filter(self, record):
         if super().filter(record):
-            is_structlog = record.msg.startswith('{') and record.msg.endswith('}')
+            is_structlog = False
+            if isinstance(record.msg, str):
+                is_structlog = record.msg.startswith('{') and record.msg.endswith('}')
+
             if is_structlog:
                 return True
             else:
diff --git a/ops/docker-compose/nomad/docker-compose.override.yml b/ops/docker-compose/nomad/docker-compose.override.yml
index 3782b1c46087fdf831e3bdd20e06f610d895f200..c0e4c804b862c24250230ee108a93603602c682c 100644
--- a/ops/docker-compose/nomad/docker-compose.override.yml
+++ b/ops/docker-compose/nomad/docker-compose.override.yml
@@ -50,12 +50,6 @@ services:
         ports:
             - 8000:8000
 
-    # NOMAD-coe search api
-    coeapi:
-        restart: 'no'
-        ports:
-            - 8111:8111
-
     # nomad gui
     gui:
         restart: 'no'
diff --git a/ops/docker-compose/nomad/docker-compose.prod.yml b/ops/docker-compose/nomad/docker-compose.prod.yml
index 21d50e5428cf64e2ac9e1c3650760bee3ea0c897..d317087e25d7051738679845e56b24209f75c4d2 100644
--- a/ops/docker-compose/nomad/docker-compose.prod.yml
+++ b/ops/docker-compose/nomad/docker-compose.prod.yml
@@ -15,6 +15,10 @@
 version: '3.4'
 
 services:
+    # keycloak for user management
+    keycloak:
+        volumes:
+            - /nomad/fairdi/db/keycloak:/opt/jboss/keycloak/standalone
 
     postgres:
         ports:
diff --git a/ops/docker-compose/nomad/docker-compose.yml b/ops/docker-compose/nomad/docker-compose.yml
index 93cd8586648bed2e6c168463544a0f165c31ef4c..4c26aeb691ed9c320f99bf81df6d3ae37c3c667f 100644
--- a/ops/docker-compose/nomad/docker-compose.yml
+++ b/ops/docker-compose/nomad/docker-compose.yml
@@ -19,6 +19,7 @@ x-common-variables: &nomad_backend_env
     NOMAD_LOGSTASH_HOST: elk
     NOMAD_ELASTIC_HOST: elastic
     NOMAD_MONGO_HOST: mongo
+    NOMAD_KEYCLOAK_HOST: keycloak
 
 services:
     # postgres for NOMAD-coe repository API and GUI
@@ -32,6 +33,20 @@ services:
         volumes:
             - nomad_postgres:/var/lib/postgresql/data
 
+    # keycload for user management
+    keycloak:
+        restart: always
+        image: jboss/keycloak
+        container_name: nomad_keycloak
+        environment:
+            DB_VENDOR: "h2"
+            KEYCLOAK_USER: "admin"
+            KEYCLOAK_PASSWORD: "password"
+        volumes:
+            - nomad_keycloak:/opt/jboss/keycloak/standalone
+        ports:
+            - 8002:8080
+
     # broker for celery
     rabbitmq:
         restart: always
@@ -74,6 +89,7 @@ services:
             <<: *nomad_backend_env
             NOMAD_SERVICE: nomad_worker
         links:
+            - keycloak
             - postgres
             - rabbitmq
             - elastic
@@ -95,6 +111,7 @@ services:
             NOMAD_SERVICES_API_SECRET: ${API_SECRET}
             NOMAD_SERVICE: nomad_api
         links:
+            - keycloak
             - postgres
             - rabbitmq
             - elastic
@@ -103,18 +120,6 @@ services:
             - ${VOLUME_BINDS}/fs:/app/.volumes/fs
         command: python -m gunicorn.app.wsgiapp -w 4 --log-config ops/gunicorn.log.conf -b 0.0.0.0:8000 --timeout 300 nomad.api:app
 
-    # NOMAD-coe search api
-    coeapi:
-        restart: always
-        image: gitlab-registry.mpcdf.mpg.de/nomad-lab/nomad-fair/coe-repowebservice:latest
-        container_name: nomad_coeapi
-        environment:
-            REPO_DB_JDBC_URL: jdbc:postgresql://postgres:5432/nomad
-            REPO_ELASTIC_URL: elasticsearch://elastic:9200
-        links:
-            - postgres
-            - elastic
-
     # nomad gui
     gui:
         restart: always
@@ -137,6 +142,7 @@ services:
         command: nginx -g 'daemon off;'
 
 volumes:
+    nomad_keycloak:
     nomad_postgres:
     nomad_mongo:
     nomad_elastic:
diff --git a/requirements.txt b/requirements.txt
index de549f4096cdbe5dd34f640031ca3ec0eae7f4f1..9f6bfb7446aac42bb9ac44e1c081fe6615a30b1a 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -48,6 +48,7 @@ pyyaml
 tabulate
 cachetools
 zipfile37
+python-keycloak
 
 # dev/ops related
 setuptools