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