Commit 678e2f45 authored by Markus Scheidgen's avatar Markus Scheidgen
Browse files

Allow API to verify GUI token. Adopted GUI to API changes.

parent ded39fb5
......@@ -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" /> : ''}
......
......@@ -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)
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}
......
......@@ -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>
......
......@@ -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(
......
......@@ -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):
......
......@@ -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(
......
......@@ -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))