From 678e2f45b72ff22eb4acfb040f0ced4d75438277 Mon Sep 17 00:00:00 2001
From: Markus Scheidgen <markus.scheidgen@gmail.com>
Date: Tue, 27 Aug 2019 10:07:21 +0200
Subject: [PATCH] Allow API to verify GUI token. Adopted GUI to API changes.

---
 gui/src/components/App.js         |   5 +-
 gui/src/components/LoginLogout.js | 149 ++----------------------------
 gui/src/components/api.js         | 139 ++++++++++------------------
 gui/src/index.js                  |   2 +-
 nomad/api/app.py                  |   4 +-
 nomad/api/auth.py                 |  16 ++--
 nomad/config.py                   |   3 +-
 nomad/infrastructure.py           | 139 +++++++++++++++++-----------
 requirements.txt                  |   3 +-
 tests/conftest.py                 |   7 +-
 10 files changed, 161 insertions(+), 306 deletions(-)

diff --git a/gui/src/components/App.js b/gui/src/components/App.js
index 7607748fae..088501556c 100644
--- a/gui/src/components/App.js
+++ b/gui/src/components/App.js
@@ -161,9 +161,6 @@ class NavigationUnstyled extends React.Component {
     barButton: {
       borderColor: theme.palette.getContrastText(theme.palette.primary.main),
       marginRight: 0
-    },
-    barButtonDisabled: {
-      marginRight: 0
     }
   })
 
@@ -230,7 +227,7 @@ class NavigationUnstyled extends React.Component {
                   {help ? <HelpDialog color="inherit" maxWidth="md" classes={{root: classes.helpButton}} {...help}/> : ''}
                 </div>
                 <div className={classes.barActions}>
-                  <LoginLogout variant="outlined" color="inherit" classes={{button: classes.barButton, buttonDisabled: classes.barButtonDisabled}} />
+                  <LoginLogout variant="outlined" color="inherit" classes={{button: classes.barButton}} />
                 </div>
               </Toolbar>
               {loading ? <LinearProgress color="primary" /> : ''}
diff --git a/gui/src/components/LoginLogout.js b/gui/src/components/LoginLogout.js
index 297c79eb31..29002ee9fc 100644
--- a/gui/src/components/LoginLogout.js
+++ b/gui/src/components/LoginLogout.js
@@ -3,23 +3,15 @@ import PropTypes from 'prop-types'
 import { withStyles } from '@material-ui/core/styles'
 import Typography from '@material-ui/core/Typography'
 import { compose } from 'recompose'
-import { Button, DialogTitle, DialogContent, DialogContentText, TextField, DialogActions,
-  Dialog, FormGroup } from '@material-ui/core'
+import { Button } from '@material-ui/core'
 import { withApi } from './api'
-import { withKeycloak } from 'react-keycloak'
 
 class LoginLogout extends React.Component {
   static propTypes = {
     classes: PropTypes.object.isRequired,
-    api: PropTypes.object.isRequired,
-    isLoggingIn: PropTypes.bool,
-    user: PropTypes.object,
-    login: PropTypes.func.isRequired,
-    logout: PropTypes.func.isRequired,
     variant: PropTypes.string,
     color: PropTypes.string,
-    onLoggedIn: PropTypes.func,
-    onLoggedOut: PropTypes.func,
+    user: PropTypes.object,
     keycloak: PropTypes.object.isRequired
   }
 
@@ -31,92 +23,17 @@ class LoginLogout extends React.Component {
         marginRight: theme.spacing.unit * 2
       }
     },
-    button: {}, // to allow overrides
-    buttonDisabled: {},
-    errorText: {
-      marginTop: theme.spacing.unit,
-      marginBottom: theme.spacing.unit
-    }
+    button: {} // to allow overrides
   })
 
-  constructor(props) {
-    super(props)
-    this.handleLogout = this.handleLogout.bind(this)
-    this.handleChange = this.handleChange.bind(this)
-    this.handleKeyPress = this.handleKeyPress.bind(this)
-  }
-
-  state = {
-    loginDialogOpen: false,
-    userName: '',
-    password: '',
-    failure: false
-  }
-
-  componentDidMount() {
-    this._ismounted = true
-  }
-
-  componentWillUnmount() {
-    this._ismounted = false
-  }
-
-  handleLoginDialogClosed(withLogin) {
-    if (withLogin) {
-      this.props.login(this.state.userName, this.state.password, (success) => {
-        if (success && this.props.onLoggedIn) {
-          this.props.onLoggedIn()
-        }
-
-        if (this._ismounted) {
-          if (success) {
-            this.setState({loginDialogOpen: false, failure: false})
-          } else {
-            this.setState({failure: true, loginDialogOpen: true})
-          }
-        }
-      })
-    } else {
-      if (this._ismounted) {
-        this.setState({failure: false, userName: '', password: '', loginDialogOpen: false})
-      }
-    }
-  }
-
-  handleChange = name => event => {
-    this.setState({
-      [name]: event.target.value, failure: false
-    })
-  }
-
-  handleLogout() {
-    this.props.logout()
-    if (this.props.onLoggedOut) {
-      this.props.onLoggedOut()
-    }
-  }
-
-  handleKeyPress(ev) {
-    if (ev.key === 'Enter') {
-      ev.preventDefault()
-      this.handleLoginDialogClosed(true)
-    }
-  }
-
   render() {
-    const { classes, variant, color, isLoggingIn, keycloak } = this.props
+    const { classes, variant, color, keycloak, user } = this.props
 
-    let user = null
     if (keycloak.authenticated) {
-      user = {}
-    }
-
-    const { failure } = this.state
-    if (user) {
       return (
         <div className={classes.root}>
           <Typography color="inherit" variant="body1">
-            Welcome {user.first_name} {user.last_name}
+            Welcome { user ? user.name : '...' }
           </Typography>
           <Button
             className={classes.button}
@@ -131,64 +48,10 @@ class LoginLogout extends React.Component {
           <Button
             className={classes.button} variant={variant} color={color} onClick={() => keycloak.login()}
           >Login</Button>
-          <Dialog
-            disableBackdropClick disableEscapeKeyDown
-            open={this.state.loginDialogOpen}
-            onClose={() => this.handleLoginDialogClosed(false)}
-          >
-            <DialogTitle>Login</DialogTitle>
-            <DialogContent>
-              <DialogContentText>
-                To login, please enter your email address and password. If you
-                do not have an account, please go to the NOMAD Repository and
-                create one.
-              </DialogContentText>
-              {failure ? <DialogContentText className={classes.errorText} color="error">Wrong username or password!</DialogContentText> : ''}
-              <form>
-                <FormGroup>
-                  <TextField
-                    autoComplete="username"
-                    disabled={isLoggingIn}
-                    autoFocus
-                    margin="dense"
-                    id="uaseName"
-                    label="Email Address"
-                    type="email"
-                    fullWidth
-                    value={this.state.userName}
-                    onChange={this.handleChange('userName')}
-                    onKeyPress={this.handleKeyPress}
-                  />
-                  <TextField
-                    autoComplete="current-password"
-                    disabled={isLoggingIn}
-                    margin="dense"
-                    id="password"
-                    label="Password"
-                    type="password"
-                    fullWidth
-                    value={this.state.password}
-                    onChange={this.handleChange('password')}
-                    onKeyPress={this.handleKeyPress}
-                  />
-                </FormGroup>
-              </form>
-            </DialogContent>
-            <DialogActions>
-              <Button onClick={() => this.handleLoginDialogClosed(false)} color="primary">
-                Cancel
-              </Button>
-              <Button onClick={() => this.handleLoginDialogClosed(true)} color="primary"
-                disabled={this.state.userName === '' || this.state.password === ''}
-              >
-                Login
-              </Button>
-            </DialogActions>
-          </Dialog>
         </div>
       )
     }
   }
 }
 
-export default compose(withKeycloak, withApi(false), withStyles(LoginLogout.styles))(LoginLogout)
+export default compose(withApi(false), withStyles(LoginLogout.styles))(LoginLogout)
diff --git a/gui/src/components/api.js b/gui/src/components/api.js
index 59b6e75475..187ddb63da 100644
--- a/gui/src/components/api.js
+++ b/gui/src/components/api.js
@@ -1,14 +1,14 @@
 import React from 'react'
-import PropTypes, { instanceOf } from 'prop-types'
+import PropTypes from 'prop-types'
 import { withErrors } from './errors'
 import { UploadRequest } from '@navjobs/upload'
 import Swagger from 'swagger-client'
 import { apiBase } from '../config'
 import { Typography, withStyles, Link } from '@material-ui/core'
 import LoginLogout from './LoginLogout'
-import { Cookies, withCookies } from 'react-cookie'
 import { compose } from 'recompose'
 import MetaInfoRepository from './MetaInfoRepository'
+import { withKeycloak } from 'react-keycloak'
 
 const ApiContext = React.createContext()
 
@@ -66,7 +66,7 @@ class Upload {
             method: 'PUT',
             headers: {
               'Content-Type': 'application/gzip',
-              ...this.api.auth_headers
+              ...this.api.authHeaders
             }
           },
           files: [file],
@@ -143,20 +143,13 @@ function handleApiError(e) {
 }
 
 class Api {
-  static async createSwaggerClient(userNameToken, password) {
+  static async createSwaggerClient(accessToken) {
     let data
-    if (userNameToken) {
+    if (accessToken) {
       let auth = {
-        'X-Token': userNameToken
-      }
-      if (password) {
-        auth = {
-          'HTTP Basic': {
-            username: userNameToken,
-            password: password
-          }
-        }
+        'OpenIDConnect Bearer Token': `Bearer ${accessToken}`
       }
+
       data = {authorizations: auth}
     }
 
@@ -167,15 +160,14 @@ class Api {
     }
   }
 
-  constructor(user) {
+  constructor(accessToken) {
     this.onStartLoading = () => null
     this.onFinishLoading = () => null
 
-    user = user || {}
-    this.auth_headers = {
-      'X-Token': user.token
+    this.authHeaders = {
+      'Authentication': `Bearer ${accessToken}`
     }
-    this.swaggerPromise = Api.createSwaggerClient(user.token).catch(handleApiError)
+    this.swaggerPromise = Api.createSwaggerClient(accessToken).catch(handleApiError)
 
     // keep a list of localUploads, these are uploads that are currently uploaded through
     // the browser and that therefore not yet returned by the backend
@@ -395,21 +387,37 @@ export class ApiProviderComponent extends React.Component {
       PropTypes.arrayOf(PropTypes.node),
       PropTypes.node
     ]).isRequired,
-    cookies: instanceOf(Cookies).isRequired,
-    raiseError: PropTypes.func.isRequired
+    raiseError: PropTypes.func.isRequired,
+    keycloak: PropTypes.object.isRequired,
+    keycloakInitialized: PropTypes.bool
   }
 
-  componentDidMount(props) {
-    const token = this.props.cookies.get('token')
-    if (token && token !== 'undefined') {
-      this.state.login(token)
-    } else {
-      this.setState({api: this.createApi()})
+  update() {
+    const { keycloak } = this.props
+    this.setState({api: this.createApi(keycloak.token)})
+    if (keycloak.token) {
+      keycloak.loadUserInfo()
+        .success(user => {
+          this.setState({user: user})
+        })
+        .error(error => {
+          this.props.raiseError(error)
+        })
+    }
+  }
+
+  componentDidMount() {
+    this.update()
+  }
+
+  componentDidUpdate(prevProps) {
+    if (this.props.keycloakInitialized !== prevProps.keycloakInitialized) {
+      this.update()
     }
   }
 
-  createApi(user) {
-    const api = new Api(user)
+  createApi(accessToken) {
+    const api = new Api(accessToken)
     api.onStartLoading = (name) => {
       this.setState(state => ({loading: state.loading + 1}))
     }
@@ -428,50 +436,8 @@ export class ApiProviderComponent extends React.Component {
 
   state = {
     api: null,
-    user: null,
     info: null,
-    isLoggingIn: false,
-    loading: 0,
-    login: (userNameToken, password, successCallback) => {
-      this.setState({isLoggingIn: true})
-      successCallback = successCallback || (() => true)
-      Api.createSwaggerClient(userNameToken, password)
-        .then(client => {
-          client.apis.auth.get_user()
-            .catch(error => {
-              if (error.response && error.response.status !== 401) {
-                throw error
-              }
-            })
-            .then(response => {
-              if (response) {
-                const user = response.body
-                this.setState({api: this.createApi(user), isLoggingIn: false, user: user})
-                this.props.cookies.set('token', user.token)
-                successCallback(true)
-              } else {
-                this.setState({api: this.createApi(), isLoggingIn: false, user: null})
-                successCallback(false)
-              }
-            })
-            .catch(error => {
-              try {
-                this.props.raiseError(error)
-              } catch (e) {
-                this.setState({api: this.createApi(), isLoggingIn: false, user: null})
-                this.props.raiseError(error)
-              }
-            })
-        })
-        .catch(error => {
-          this.setState({api: this.createApi(), isLoggingIn: false, user: null})
-          this.props.raiseError(error)
-        })
-    },
-    logout: () => {
-      this.setState({api: this.createApi(), user: null})
-      this.props.cookies.set('token', undefined)
-    }
+    loading: 0
   }
 
   render() {
@@ -487,9 +453,7 @@ export class ApiProviderComponent extends React.Component {
 class LoginRequiredUnstyled extends React.Component {
   static propTypes = {
     classes: PropTypes.object.isRequired,
-    message: PropTypes.string,
-    isLoggingIn: PropTypes.bool,
-    onLoggedIn: PropTypes.func
+    message: PropTypes.string
   }
 
   static styles = theme => ({
@@ -504,7 +468,7 @@ class LoginRequiredUnstyled extends React.Component {
   })
 
   render() {
-    const {classes, isLoggingIn, onLoggedIn, message} = this.props
+    const {classes, message} = this.props
 
     let loginMessage = ''
     if (message) {
@@ -516,7 +480,7 @@ class LoginRequiredUnstyled extends React.Component {
     return (
       <div className={classes.root}>
         {loginMessage}
-        <LoginLogout onLoggedIn={onLoggedIn} variant="outlined" color="primary" isLoggingIn={isLoggingIn}/>
+        <LoginLogout variant="outlined" color="primary" />
       </div>
     )
   }
@@ -537,7 +501,7 @@ DisableOnLoading.propTypes = {
   children: PropTypes.any.isRequired
 }
 
-export const ApiProvider = compose(withCookies, withErrors)(ApiProviderComponent)
+export const ApiProvider = compose(withKeycloak, withErrors)(ApiProviderComponent)
 
 const LoginRequired = withStyles(LoginRequiredUnstyled.styles)(LoginRequiredUnstyled)
 
@@ -549,7 +513,6 @@ class WithApiComponent extends React.Component {
     loginMessage: PropTypes.string,
     api: PropTypes.object,
     user: PropTypes.object,
-    isLoggingIn: PropTypes.bool,
     Component: PropTypes.any
   }
 
@@ -586,10 +549,10 @@ class WithApiComponent extends React.Component {
 
   render() {
     const { raiseError, loginRequired, loginMessage, Component, ...rest } = this.props
-    const { api, user, isLoggingIn } = rest
+    const { api, keycloak } = rest
     const { notAuthorized } = this.state
     if (notAuthorized) {
-      if (user) {
+      if (keycloak.authenticated) {
         return (
           <div>
             <Typography variant="h6">Not Authorized</Typography>
@@ -602,19 +565,15 @@ class WithApiComponent extends React.Component {
         )
       } else {
         return (
-          <LoginRequired
-            message="You need to be logged in to access this information."
-            isLoggingIn={isLoggingIn}
-            onLoggedIn={() => this.setState({notAuthorized: false})}
-          />
+          <LoginRequired message="You need to be logged in to access this information." />
         )
       }
     } else {
       if (api) {
-        if (user || !loginRequired) {
+        if (keycloak.authenticated || !loginRequired) {
           return <Component {...rest} raiseError={this.raiseError} />
         } else {
-          return <LoginRequired message={loginMessage} isLoggingIn={isLoggingIn} />
+          return <LoginRequired message={loginMessage} />
         }
       } else {
         return ''
@@ -623,12 +582,14 @@ class WithApiComponent extends React.Component {
   }
 }
 
+const WithKeycloakWithApiCompnent = withKeycloak(WithApiComponent)
+
 export function withApi(loginRequired, showErrorPage, loginMessage) {
   return function(Component) {
     return withErrors(props => (
       <ApiContext.Consumer>
         {apiContext => (
-          <WithApiComponent
+          <WithKeycloakWithApiCompnent
             loginRequired={loginRequired}
             loginMessage={loginMessage}
             showErrorPage={showErrorPage}
diff --git a/gui/src/index.js b/gui/src/index.js
index fb221b09f9..fe8b15f3a9 100644
--- a/gui/src/index.js
+++ b/gui/src/index.js
@@ -24,7 +24,7 @@ const keycloak = Keycloak({
 })
 
 ReactDOM.render(
-  <KeycloakProvider keycloak={keycloak} initConfig={{onLoad: 'check-sso'}} >
+  <KeycloakProvider keycloak={keycloak} initConfig={{onLoad: 'check-sso'}} LoadingComponent={<div />}>
     <Router history={sendTrackingData ? matomo.connectToHistory(history) : history}>
       <App />
     </Router>
diff --git a/nomad/api/app.py b/nomad/api/app.py
index a1a6a28834..eca48df508 100644
--- a/nomad/api/app.py
+++ b/nomad/api/app.py
@@ -27,7 +27,7 @@ from datetime import datetime
 import pytz
 import random
 
-from nomad import config, utils, infrastructure
+from nomad import config, utils
 
 base_path = config.services.api_base_path
 """ Provides the root path of the nomad APIs. """
@@ -70,8 +70,6 @@ def api_base_path_response(env, resp):
 app.wsgi_app = DispatcherMiddleware(  # type: ignore
     api_base_path_response, {config.services.api_base_path: app.wsgi_app})
 
-infrastructure.keycloak.configure_flask(app)
-
 CORS(app)
 
 api = Api(
diff --git a/nomad/api/auth.py b/nomad/api/auth.py
index 065bca86da..86a028e06c 100644
--- a/nomad/api/auth.py
+++ b/nomad/api/auth.py
@@ -229,12 +229,16 @@ class AuthResource(Resource):
                 dict(user=g.user.user_id, exp=expires_at),
                 config.services.api_secret, 'HS256').decode('utf-8')
 
-        return {
-            'user': g.user,
-            'upload_token': generate_upload_token(g.user),
-            'signature_token': signature_token(),
-            'access_token': infrastructure.keycloak.access_token
-        }
+        try:
+            return {
+                'user': infrastructure.keycloak.get_user(g.user.user_id),
+                'upload_token': generate_upload_token(g.user),
+                'signature_token': signature_token(),
+                'access_token': infrastructure.keycloak.access_token
+            }
+
+        except KeyError:
+            abort(401, 'The authenticated user does not exist')
 
 
 def with_signature_token(func):
diff --git a/nomad/config.py b/nomad/config.py
index 70d8029eec..2b6ebe8d06 100644
--- a/nomad/config.py
+++ b/nomad/config.py
@@ -113,11 +113,12 @@ elastic = NomadConfig(
 
 keycloak = NomadConfig(
     server_url='http://localhost:8002/auth/',
+    issuer_url='http://localhost:8002/auth/realms/fairdi_nomad_test',
     realm_name='fairdi_nomad_test',
     username='admin',
     password='password',
     client_id='nomad_api_dev',
-    client_secret_key='0f9ec82f-a1dc-4405-a80e-593160aeea42'
+    client_secret_key='ae9bb323-3793-4243-9e4b-f380c54e54e2'
 )
 
 mongo = NomadConfig(
diff --git a/nomad/infrastructure.py b/nomad/infrastructure.py
index 1f019da356..69d077887d 100644
--- a/nomad/infrastructure.py
+++ b/nomad/infrastructure.py
@@ -19,7 +19,6 @@ is run once for each *api* and *worker* process. Individual functions for partia
 exist to facilitate testing, :py:mod:`nomad.migration`, aspects of :py:mod:`nomad.cli`, etc.
 """
 
-from typing import Union
 import os.path
 import shutil
 from elasticsearch.exceptions import RequestError
@@ -28,10 +27,11 @@ from mongoengine import connect
 import smtplib
 from email.mime.text import MIMEText
 from keycloak import KeycloakOpenID, KeycloakAdmin
-from flask_oidc import OpenIDConnect
 import json
+import jwt
 from flask import g, request
 import basicauth
+from datetime import datetime
 
 from nomad import config, utils
 
@@ -108,32 +108,9 @@ class Keycloak():
     configuration
     """
     def __init__(self):
-        self._flask_oidc = None
         self.__oidc_client = None
         self.__admin_client = None
-
-    def configure_flask(self, app):
-        oidc_issuer_url = '%s/realms/%s' % (config.keycloak.server_url.rstrip('/'), config.keycloak.realm_name)
-        oidc_client_secrets = dict(
-            client_id=config.keycloak.client_id,
-            client_secret=config.keycloak.client_secret_key,
-            issuer=oidc_issuer_url,
-            auth_uri='%s/protocol/openid-connect/auth' % oidc_issuer_url,
-            token_uri='%s/protocol/openid-connect/token' % oidc_issuer_url,
-            userinfo_uri='%s/protocol/openid-connect/userinfo' % oidc_issuer_url,
-            token_introspection_uri='%s/protocol/openid-connect/token/introspect' % oidc_issuer_url,
-            redirect_uris=['http://localhost/fairdi/nomad/latest'])
-        oidc_client_secrets_file = os.path.join(config.fs.tmp, 'oidc_client_secrets')
-        with open(oidc_client_secrets_file, 'wt') as f:
-            json.dump(dict(web=oidc_client_secrets), f)
-        app.config.update(dict(
-            SECRET_KEY=config.services.api_secret,
-            OIDC_RESOURCE_SERVER_ONLY=True,
-            OIDC_USER_INFO_ENABLED=False,
-            OIDC_CLIENT_SECRETS=oidc_client_secrets_file,
-            OIDC_OPENID_REALM=config.keycloak.realm_name))
-
-        self._flask_oidc = OpenIDConnect(app)
+        self.__public_keys = None
 
     @property
     def _oidc_client(self):
@@ -146,10 +123,36 @@ class Keycloak():
 
         return self.__oidc_client
 
-    def authorize_flask(self, basic: bool = True) -> Union[str, object]:
-        token = None
+    @property
+    def _public_keys(self):
+        if self.__public_keys is None:
+            try:
+                jwks = self._oidc_client.certs()
+                self.__public_keys = {}
+                for jwk in jwks['keys']:
+                    kid = jwk['kid']
+                    self.__public_keys[kid] = jwt.algorithms.RSAAlgorithm.from_jwk(
+                        json.dumps(jwk))
+            except Exception as e:
+                self.__public_keys = None
+                raise e
+
+        return self.__public_keys
+
+    def authorize_flask(self, basic: bool = True) -> str:
+        """
+        Authorizes the current flask request with keycloak. Uses either Bearer or Basic
+        authentication, depending on available headers in the request. Bearer auth is
+        basically offline (besides retrieving and caching keycloaks public key for signature
+        validation). Basic auth causes authentication agains keycloak with each request.
+
+        Will set ``g.user``, either with None or user data from the respective OIDC token.
+
+        Returns: An error message or None
+        """
+        g.oidc_access_token = None
         if 'Authorization' in request.headers and request.headers['Authorization'].startswith('Bearer '):
-            token = request.headers['Authorization'].split(None, 1)[1].strip()
+            g.oidc_access_token = request.headers['Authorization'].split(None, 1)[1].strip()
         elif 'Authorization' in request.headers and request.headers['Authorization'].startswith('Basic '):
             if not basic:
                 return 'Basic authentication not allowed, use Bearer token instead'
@@ -158,52 +161,84 @@ class Keycloak():
                 auth = request.headers['Authorization'].split(None, 1)[1].strip()
                 username, password = basicauth.decode(auth)
                 token_info = self._oidc_client.token(username=username, password=password)
-                token = token_info['access_token']
+                g.oidc_access_token = token_info['access_token']
             except Exception as e:
                 # TODO logging
                 return 'Could not authenticate Basic auth: %s' % str(e)
 
-        if token is not None:
-            validity = self._flask_oidc.validate_token(token)
+        if g.oidc_access_token is not None:
+            auth_error: str = None
+            try:
+                kid = jwt.get_unverified_header(g.oidc_access_token)['kid']
+                key = self._public_keys[kid]
+                options = dict(verify_aud=False, verify_exp=True, verify_iss=True)
+                payload = jwt.decode(
+                    g.oidc_access_token, key=key, algorithms=['RS256'], options=options,
+                    issuer=config.keycloak.issuer_url)
+
+            except jwt.InvalidTokenError as e:
+                auth_error = str(e)
+            except Exception as e:
+                logger.error('Could not verify JWT token', exc_info=e)
+                raise e
 
-            if validity is not True:
-                return validity
+            if auth_error is not None:
+                g.user = None
+                return auth_error
 
             else:
-                g.oidc_id_token = g.oidc_token_info  # these seem to be synonyms
-                g.oidc_access_token = token
-                return self.get_user()
+                from nomad import datamodel
+                g.user = datamodel.User(
+                    user_id=payload.get('sub', None),
+                    name=payload.get('name', None),
+                    email=payload.get('email', None),
+                    first_name=payload.get('given_name', None),
+                    family_name=payload.get('family_name', None))
+
+                return None
 
         else:
-            g.oidc_access_token = None
+            g.user = None
+            # Do not return an error. This is the case were there are no credentials
             return None
 
-    def get_user(self, user_id: str = None, email: str = None) -> object:
+    def get_user(self, user_id: str = None, email: str = None, user=None) -> object:
+        """
+        Retrives all available information about a user from the keycloak admin
+        interface. This must be used to retrieve complete user information, because
+        the info solely gathered from tokens (i.e. for the authenticated user ``g.user``)
+        is generally incomplete.
+        """
         from nomad import datamodel
 
-        if email is not None:
+        if user is not None and user_id is None:
+            user_id = user.user_id
+
+        if email is not None and user_id is None:
             try:
                 user_id = self._admin_client.get_user_id(email)
             except Exception:
                 raise KeyError('User does not exist')
 
-        if user_id is None and g.oidc_id_token is not None and self._flask_oidc is not None:
-            try:
-                user_data = self._flask_oidc.user_getinfo([
-                    'sub', 'email', 'name', 'given_name', 'family_name', 'sub'])
-                return datamodel.User(**user_data)
-            except Exception as e:
-                # TODO logging
-                raise e
-
         assert user_id is not None, 'Could not determine user from given kwargs'
 
         try:
             keycloak_user = self._admin_client.get_user(user_id)
-        except Exception:
-            raise KeyError('User does not exist')
 
-        return datamodel.User(**keycloak_user)
+        except Exception as e:
+            if str(getattr(e, 'response_code', 404)) == '404':
+                raise KeyError('User does not exist')
+
+            logger.error('Could not retrieve user from keycloak', exc_info=e)
+            raise e
+
+        return datamodel.User(
+            user_id=keycloak_user['id'],
+            email=keycloak_user['email'],
+            name=keycloak_user.get('username', None),
+            first_name=keycloak_user.get('firstName', None),
+            family_name=keycloak_user.get('lastName', None),
+            created=datetime.fromtimestamp(keycloak_user['createdTimestamp'] / 1000))
 
     @property
     def _admin_client(self):
diff --git a/requirements.txt b/requirements.txt
index 08f8631864..fd182e2e96 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -38,7 +38,7 @@ bcrypt
 filelock
 ujson
 bravado
-PyJWT
+pyjwt[crypto]
 jsonschema[format]
 python-magic
 runstats
@@ -47,7 +47,6 @@ tabulate
 cachetools
 zipfile37
 python-keycloak
-Flask-OIDC
 basicauth
 
 # dev/ops related
diff --git a/tests/conftest.py b/tests/conftest.py
index 2e46b543e6..9ebc98fac5 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -194,13 +194,10 @@ test_users = {
 
 
 class KeycloakMock:
-    def configure_flask(self, *args, **kwargs):
-        pass
-
     def authorize_flask(self, *args, **kwargs):
         if 'Authorization' in request.headers and request.headers['Authorization'].startswith('Bearer '):
             user_id = request.headers['Authorization'].split(None, 1)[1].strip()
-            g.oidc_id_token = user_id
+            g.oidc_access_token = user_id
             return User(**test_users[user_id])
 
     def get_user(self, user_id=None, email=None):
@@ -216,7 +213,7 @@ class KeycloakMock:
 
     @property
     def access_token(self):
-        return g.oidc_id_token
+        return g.oidc_access_token
 
 
 _keycloak = infrastructure.keycloak
-- 
GitLab