Skip to content
Snippets Groups Projects
Unverified Commit 32dd9ab4 authored by Jack Reed's avatar Jack Reed Committed by GitHub
Browse files

Merge pull request #3254 from ProjectMirador/auth-refactor

Consolidate IIIF auth behaviors
parents b15cbb61 6653ec3d
Branches
Tags
No related merge requests found
Showing
with 621 additions and 437 deletions
...@@ -286,6 +286,10 @@ export class OpenSeadragonViewer extends Component { ...@@ -286,6 +286,10 @@ export class OpenSeadragonViewer extends Component {
return false; return false;
} }
if (infoResponse.tokenServiceId !== prevInfoResponses[index].tokenServiceId) {
return false;
}
if (infoResponse.json['@id'] if (infoResponse.json['@id']
&& infoResponse.json['@id'] === prevInfoResponses[index].json['@id']) { && infoResponse.json['@id'] === prevInfoResponses[index].json['@id']) {
return true; return true;
......
...@@ -9,7 +9,7 @@ import PrimaryWindow from '../containers/PrimaryWindow'; ...@@ -9,7 +9,7 @@ import PrimaryWindow from '../containers/PrimaryWindow';
import CompanionArea from '../containers/CompanionArea'; import CompanionArea from '../containers/CompanionArea';
import MinimalWindow from '../containers/MinimalWindow'; import MinimalWindow from '../containers/MinimalWindow';
import ErrorContent from '../containers/ErrorContent'; import ErrorContent from '../containers/ErrorContent';
import WindowAuthenticationControl from '../containers/WindowAuthenticationControl'; import IIIFAuthentication from '../containers/IIIFAuthentication';
import { PluginHook } from './PluginHook'; import { PluginHook } from './PluginHook';
/** /**
...@@ -44,7 +44,7 @@ export class Window extends Component { ...@@ -44,7 +44,7 @@ export class Window extends Component {
windowId={windowId} windowId={windowId}
windowDraggable={windowDraggable} windowDraggable={windowDraggable}
/> />
<WindowAuthenticationControl key="auth" windowId={windowId} /> <IIIFAuthentication windowId={windowId} />
</div> </div>
); );
if (workspaceType === 'mosaic' && windowDraggable) { if (workspaceType === 'mosaic' && windowDraggable) {
......
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import Button from '@material-ui/core/Button';
import Paper from '@material-ui/core/Paper';
import Collapse from '@material-ui/core/Collapse';
import DialogActions from '@material-ui/core/DialogActions';
import Typography from '@material-ui/core/Typography';
import LockIcon from '@material-ui/icons/LockSharp';
import SanitizedHtml from '../containers/SanitizedHtml';
import { PluginHook } from './PluginHook';
/** */
export class WindowAuthenticationBar extends Component {
/** */
constructor(props) {
super(props);
this.state = { open: false };
this.setOpen = this.setOpen.bind(this);
this.onSubmit = this.onSubmit.bind(this);
}
/** */
onSubmit() {
const { onConfirm } = this.props;
this.setOpen(false);
onConfirm();
}
/** Toggle the full description */
setOpen(open) {
this.setState(state => ({ ...state, open }));
}
/** */
render() {
const {
classes, confirmButton, continueLabel,
header, description, icon, label, t,
ruleSet, hasLogoutService, status, ConfirmProps,
} = this.props;
if (status === 'ok' && !hasLogoutService) return null;
const { open } = this.state;
const button = (
<Button onClick={this.onSubmit} className={classes.buttonInvert} autoFocus color="secondary" {...ConfirmProps}>
{confirmButton || t('login')}
</Button>
);
if (!description && !header) {
return (
<Paper square elevation={4} color="secondary" classes={{ root: classes.paper }}>
<div className={classes.topBar}>
{ icon || <LockIcon className={classes.icon} /> }
<Typography className={classes.label} component="h3" variant="body1" color="inherit">
<SanitizedHtml htmlString={label} ruleSet={ruleSet} />
</Typography>
<PluginHook {...this.props} />
{ button }
</div>
</Paper>
);
}
return (
<Paper square elevation={4} color="secondary" classes={{ root: classes.paper }}>
<Button fullWidth className={classes.topBar} onClick={() => this.setOpen(true)} component="div" color="inherit">
{ icon || <LockIcon className={classes.icon} /> }
<Typography className={classes.label} component="h3" variant="body1" color="inherit">
<SanitizedHtml htmlString={label} ruleSet="iiif" />
</Typography>
<PluginHook {...this.props} />
<span className={classes.fauxButton}>
{ !open && (
<Typography variant="button" color="inherit">
{ continueLabel || t('continue') }
</Typography>
)}
</span>
</Button>
<Collapse
in={open}
onClose={() => this.setOpen(false)}
>
<Typography variant="body1" color="inherit" className={classes.expanded}>
<SanitizedHtml htmlString={header} ruleSet={ruleSet} />
{ header && description ? ': ' : '' }
<SanitizedHtml htmlString={description} ruleSet={ruleSet} />
</Typography>
<DialogActions>
<Button onClick={() => this.setOpen(false)} color="inherit">
{ t('cancel') }
</Button>
{ button }
</DialogActions>
</Collapse>
</Paper>
);
}
}
WindowAuthenticationBar.propTypes = {
classes: PropTypes.objectOf(PropTypes.string).isRequired,
confirmButton: PropTypes.string,
ConfirmProps: PropTypes.object, // eslint-disable-line react/forbid-prop-types
continueLabel: PropTypes.string,
description: PropTypes.node,
hasLogoutService: PropTypes.bool,
header: PropTypes.node,
icon: PropTypes.node,
label: PropTypes.node.isRequired,
onConfirm: PropTypes.func.isRequired,
ruleSet: PropTypes.string,
status: PropTypes.string,
t: PropTypes.func,
};
WindowAuthenticationBar.defaultProps = {
confirmButton: undefined,
ConfirmProps: {},
continueLabel: undefined,
description: undefined,
hasLogoutService: true,
header: undefined,
icon: undefined,
ruleSet: 'iiif',
status: undefined,
t: k => k,
};
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import Button from '@material-ui/core/Button';
import Paper from '@material-ui/core/Paper';
import Collapse from '@material-ui/core/Collapse';
import DialogActions from '@material-ui/core/DialogActions';
import Typography from '@material-ui/core/Typography';
import LockIcon from '@material-ui/icons/LockSharp';
import SanitizedHtml from '../containers/SanitizedHtml';
import AuthenticationLogout from '../containers/AuthenticationLogout';
/**
*/
export class WindowAuthenticationControl extends Component {
/** */
constructor(props) {
super(props);
this.state = {
open: false,
showFailureMessage: true,
};
this.handleClickOpen = this.handleClickOpen.bind(this);
this.handleClose = this.handleClose.bind(this);
this.handleConfirm = this.handleConfirm.bind(this);
}
/** */
handleClickOpen() {
this.setState({ open: true });
}
/** */
handleClose() {
this.setState({ open: false, showFailureMessage: false });
}
/** */
handleConfirm() {
const {
handleAuthInteraction, infoId, serviceId, windowId,
} = this.props;
handleAuthInteraction(windowId, infoId, serviceId);
this.setState({ showFailureMessage: true });
}
/** */
isInteractive() {
const {
profile,
} = this.props;
return profile === 'http://iiif.io/api/auth/1/clickthrough' || profile === 'http://iiif.io/api/auth/1/login';
}
/** */
render() {
const {
classes,
confirmLabel,
degraded,
description,
failureDescription,
failureHeader,
header,
label,
profile,
status,
t,
windowId,
} = this.props;
const failed = status === 'failed';
if ((!degraded || !profile) && status !== 'fetching') return <AuthenticationLogout windowId={windowId} />;
if (!this.isInteractive() && !failed) return <></>;
const { showFailureMessage, open } = this.state;
const isInFailureState = showFailureMessage && failed;
const hasCollapsedContent = isInFailureState
? failureDescription
: header || description;
const confirmButton = (
<Button onClick={this.handleConfirm} className={classes.buttonInvert} autoFocus color="secondary">
{confirmLabel || (this.isInteractive() ? t('login') : t('retry')) }
</Button>
);
return (
<Paper square elevation={4} color="secondary" classes={{ root: classes.paper }}>
<Button fullWidth className={classes.topBar} onClick={hasCollapsedContent ? this.handleClickOpen : this.handleConfirm} component="div" color="inherit">
<LockIcon className={classes.icon} />
<Typography className={classes.label} component="h3" variant="body1" color="inherit">
<SanitizedHtml htmlString={(isInFailureState ? failureHeader : label) || t('authenticationRequired')} ruleSet="iiif" />
</Typography>
<span className={classes.fauxButton}>
{ hasCollapsedContent
? !open && (
<Typography variant="button" color="inherit">
{ t('continue') }
</Typography>
)
: confirmButton}
</span>
</Button>
{
hasCollapsedContent && (
<Collapse
in={open}
onClose={this.handleClose}
>
<Typography variant="body1" color="inherit" className={classes.expanded}>
{
isInFailureState
? <SanitizedHtml htmlString={failureDescription || ''} ruleSet="iiif" />
: (
<>
<SanitizedHtml htmlString={header || ''} ruleSet="iiif" />
{ header && description ? ': ' : '' }
<SanitizedHtml htmlString={description || ''} ruleSet="iiif" />
</>
)
}
</Typography>
<DialogActions>
<Button onClick={this.handleClose} color="inherit">
{t('cancel')}
</Button>
{confirmButton}
</DialogActions>
</Collapse>
)
}
</Paper>
);
}
}
WindowAuthenticationControl.propTypes = {
classes: PropTypes.objectOf(PropTypes.string).isRequired,
confirmLabel: PropTypes.string,
degraded: PropTypes.bool,
description: PropTypes.string,
failureDescription: PropTypes.string,
failureHeader: PropTypes.string,
handleAuthInteraction: PropTypes.func.isRequired,
header: PropTypes.string,
infoId: PropTypes.string,
label: PropTypes.string,
profile: PropTypes.string,
serviceId: PropTypes.string,
status: PropTypes.oneOf(['ok', 'fetching', 'failed', null]),
t: PropTypes.func,
windowId: PropTypes.string.isRequired,
};
WindowAuthenticationControl.defaultProps = {
confirmLabel: undefined,
degraded: false,
description: undefined,
failureDescription: undefined,
failureHeader: undefined,
header: undefined,
infoId: undefined,
label: undefined,
profile: undefined,
serviceId: undefined,
status: null,
t: () => {},
};
...@@ -337,5 +337,17 @@ export default { ...@@ -337,5 +337,17 @@ export default {
viewers: true, viewers: true,
windows: true, windows: true,
workspace: true, workspace: true,
},
auth: {
serviceProfiles: [
{ profile: 'http://iiif.io/api/auth/1/external', external: true },
{ profile: 'http://iiif.io/api/auth/1/kiosk', kiosk: true },
{ profile: 'http://iiif.io/api/auth/1/clickthrough' },
{ profile: 'http://iiif.io/api/auth/1/login' },
{ profile: 'http://iiif.io/api/auth/0/external', external: true },
{ profile: 'http://iiif.io/api/auth/0/kiosk', kiosk: true },
{ profile: 'http://iiif.io/api/auth/0/clickthrough' },
{ profile: 'http://iiif.io/api/auth/0/login' }
]
} }
}; };
import { compose } from 'redux';
import { connect } from 'react-redux';
import { withPlugins } from '../extend/withPlugins';
import * as actions from '../state/actions';
import { getAccessTokens } from '../state/selectors';
import { AccessTokenSender } from '../components/AccessTokenSender';
/**
* mapStateToProps - to hook up connect
* @memberof App
* @private
*/
const mapStateToProps = (state) => ({
url: (Object.values(getAccessTokens(state)).find(e => e.isFetching) || {}).id,
});
/**
* mapDispatchToProps - used to hook up connect to action creators
* @memberof App
* @private
*/
const mapDispatchToProps = {
handleAccessTokenMessage: actions.resolveAccessTokenRequest,
};
const enhance = compose(
connect(mapStateToProps, mapDispatchToProps),
withPlugins('AccessTokenSender'),
);
export default enhance(AccessTokenSender);
import { compose } from 'redux';
import { connect } from 'react-redux';
import { withTranslation } from 'react-i18next';
import { withStyles } from '@material-ui/core';
import { withPlugins } from '../extend/withPlugins';
import {
getCurrentCanvas,
selectAuthStatus,
selectCanvasAuthService,
selectLogoutAuthService,
} from '../state/selectors';
import * as actions from '../state/actions';
import { AuthenticationLogout } from '../components/AuthenticationLogout';
/**
* mapStateToProps - to hook up connect
* @memberof App
* @private
*/
const mapStateToProps = (state, { windowId }) => {
const canvasId = (getCurrentCanvas(state, { windowId }) || {}).id;
const service = selectCanvasAuthService(state, { canvasId, windowId });
const logoutService = selectLogoutAuthService(state, { canvasId, windowId });
return {
authServiceId: service && service.id,
label: logoutService && logoutService.getLabel()[0].value,
logoutServiceId: logoutService && logoutService.id,
status: service && selectAuthStatus(state, service),
};
};
const mapDispatchToProps = {
resetAuthenticationState: actions.resetAuthenticationState,
};
const styles = {};
const enhance = compose(
connect(mapStateToProps, mapDispatchToProps),
withStyles(styles),
withTranslation(),
withPlugins('AuthenticationLogout'),
);
export default enhance(AuthenticationLogout);
import { compose } from 'redux';
import { connect } from 'react-redux';
import { withPlugins } from '../extend/withPlugins';
import * as actions from '../state/actions';
import { getAuth, getConfig } from '../state/selectors';
import { AuthenticationSender } from '../components/AuthenticationSender';
/**
* mapStateToProps - to hook up connect
* @memberof App
* @private
*/
const mapStateToProps = (state) => ({
center: getConfig(state).window.authNewWindowCenter,
url: (Object.values(getAuth(state)).find(e => e.isFetching && e.profile !== 'http://iiif.io/api/auth/1/external') || {}).id,
});
/**
* mapDispatchToProps - used to hook up connect to action creators
* @memberof App
* @private
*/
const mapDispatchToProps = {
handleInteraction: actions.resolveAuthenticationRequest,
};
const enhance = compose(
connect(mapStateToProps, mapDispatchToProps),
withPlugins('AuthenticationSender'),
);
export default enhance(AuthenticationSender);
import { connect } from 'react-redux';
import { compose } from 'redux';
import { withTranslation } from 'react-i18next';
import { Utils } from 'manifesto.js/dist-esmodule/Utils';
import { withPlugins } from '../extend/withPlugins';
import * as actions from '../state/actions';
import {
getAuth,
getAuthProfiles,
selectCurrentAuthServices,
getAccessTokens,
} from '../state/selectors';
import { IIIFAuthentication } from '../components/IIIFAuthentication';
/**
* mapStateToProps - to hook up connect
* @memberof FullScreenButton
* @private
*/
const mapStateToProps = (state, { windowId }) => {
const services = selectCurrentAuthServices(state, { windowId });
// TODO: get the most actionable auth service...
const service = services[0];
const accessTokenService = service && (
Utils.getService(service, 'http://iiif.io/api/auth/1/token')
|| Utils.getService(service, 'http://iiif.io/api/auth/0/token')
);
const logoutService = service && (
Utils.getService(service, 'http://iiif.io/api/auth/1/logout')
|| Utils.getService(service, 'http://iiif.io/api/auth/0/logout')
);
const authStatuses = getAuth(state);
const authStatus = service && authStatuses[service.id];
const accessTokens = getAccessTokens(state);
const accessTokenStatus = accessTokenService && accessTokens[accessTokenService.id];
let status = null;
if (!authStatus) {
status = null;
} else if (authStatus.ok) {
status = 'ok';
} else if (authStatus.ok === false) {
status = 'failed';
} else if (authStatus.isFetching) {
if (authStatus.windowId === windowId) status = 'cookie';
} else if (accessTokenStatus && accessTokenStatus.isFetching) {
if (authStatus.windowId === windowId) status = 'token';
}
const authProfiles = getAuthProfiles(state);
const profile = service && service.getProfile();
const isInteractive = authProfiles.some(
config => config.profile === profile && !(config.external || config.kiosk),
);
return {
accessTokenServiceId: accessTokenService && accessTokenService.id,
authServiceId: service && service.id,
confirm: service && service.getConfirmLabel(),
description: service && service.getDescription(),
failureDescription: service && service.getFailureDescription(),
failureHeader: service && service.getFailureHeader(),
header: service && service.getHeader(),
isInteractive,
label: service && service.getLabel()[0].value,
logoutServiceId: logoutService && logoutService.id,
profile,
status,
};
};
/**
* mapDispatchToProps - used to hook up connect to action creators
* @memberof ManifestListItem
* @private
*/
const mapDispatchToProps = {
handleAuthInteraction: actions.addAuthenticationRequest,
resetAuthenticationState: actions.resetAuthenticationState,
resolveAccessTokenRequest: actions.resolveAccessTokenRequest,
resolveAuthenticationRequest: actions.resolveAuthenticationRequest,
};
const enhance = compose(
withTranslation(),
connect(mapStateToProps, mapDispatchToProps),
withPlugins('IIIFAuthentication'),
);
export default enhance(IIIFAuthentication);
import { compose } from 'redux'; import { compose } from 'redux';
import { connect } from 'react-redux';
import { withTranslation } from 'react-i18next'; import { withTranslation } from 'react-i18next';
import { withStyles } from '@material-ui/core'; import { withStyles } from '@material-ui/core/styles';
import { fade } from '@material-ui/core/styles/colorManipulator'; import { fade } from '@material-ui/core/styles/colorManipulator';
import { withPlugins } from '../extend/withPlugins'; import { withPlugins } from '../extend/withPlugins';
import * as actions from '../state/actions'; import { WindowAuthenticationBar } from '../components/WindowAuthenticationBar';
import {
getCurrentCanvas,
selectAuthStatus,
selectInfoResponse,
selectCanvasAuthService,
} from '../state/selectors';
import { WindowAuthenticationControl } from '../components/WindowAuthenticationControl';
/**
* mapStateToProps - to hook up connect
* @memberof App
* @private
*/
const mapStateToProps = (state, { windowId }) => {
const canvasId = (getCurrentCanvas(state, { windowId }) || {}).id;
const service = selectCanvasAuthService(state, { canvasId, windowId });
const infoResponse = selectInfoResponse(state, { canvasId, windowId }) || {};
return {
confirmLabel: service && service.getConfirmLabel(),
degraded: infoResponse.degraded,
description: service && service.getDescription(),
failureDescription: service && service.getFailureDescription(),
failureHeader: service && service.getFailureHeader(),
header: service && service.getHeader(),
infoId: infoResponse.id,
label: service && service.getLabel()[0].value,
profile: service && service.getProfile(),
serviceId: service && service.id,
status: service && selectAuthStatus(state, service),
};
};
/**
* mapDispatchToProps - used to hook up connect to action creators
* @memberof App
* @private
*/
const mapDispatchToProps = {
handleAuthInteraction: actions.addAuthenticationRequest,
};
/** /**
* @param theme * @param theme
* @returns {{typographyBody: {flexGrow: number, fontSize: number|string}, * @returns {{typographyBody: {flexGrow: number, fontSize: number|string},
...@@ -59,6 +18,9 @@ const styles = theme => ({ ...@@ -59,6 +18,9 @@ const styles = theme => ({
), ),
}, },
backgroundColor: theme.palette.secondary.contrastText, backgroundColor: theme.palette.secondary.contrastText,
marginLeft: theme.spacing(5),
paddingBottom: 0,
paddingTop: 0,
}, },
expanded: { expanded: {
paddingLeft: theme.spacing(), paddingLeft: theme.spacing(),
...@@ -86,15 +48,18 @@ const styles = theme => ({ ...@@ -86,15 +48,18 @@ const styles = theme => ({
'&:hover': { '&:hover': {
backgroundColor: theme.palette.secondary.main, backgroundColor: theme.palette.secondary.main,
}, },
alignItems: 'center',
display: 'flex',
justifyContent: 'inherit', justifyContent: 'inherit',
padding: theme.spacing(1),
textTransform: 'none', textTransform: 'none',
}, },
}); });
const enhance = compose( const enhance = compose(
connect(mapStateToProps, mapDispatchToProps),
withStyles(styles),
withTranslation(), withTranslation(),
withPlugins('WindowAuthenticationControl'), withStyles(styles),
withPlugins('WindowAuthenticationBar'),
); );
export default enhance(WindowAuthenticationControl); export default enhance(WindowAuthenticationBar);
import { Utils } from 'manifesto.js/dist-esmodule/Utils';
import ActionTypes from './action-types'; import ActionTypes from './action-types';
/** /**
* addAuthenticationRequest - action creator * addAuthenticationRequest - action creator
* *
* @param {String} windowId * @param {String} windowId
* @param {String} infoId
* @param {String} id * @param {String} id
* @memberof ActionCreators * @memberof ActionCreators
*/ */
export function addAuthenticationRequest(windowId, infoId, id, profile = undefined) { export function addAuthenticationRequest(windowId, id, profile = undefined) {
return { return {
id, id,
infoId,
profile, profile,
type: ActionTypes.ADD_AUTHENTICATION_REQUEST, type: ActionTypes.ADD_AUTHENTICATION_REQUEST,
windowId, windowId,
...@@ -21,16 +18,19 @@ export function addAuthenticationRequest(windowId, infoId, id, profile = undefin ...@@ -21,16 +18,19 @@ export function addAuthenticationRequest(windowId, infoId, id, profile = undefin
/** /**
* resolveAuthenticationRequest - action creator * resolveAuthenticationRequest - action creator
* Triggered when we might have an IIIF auth cookie available (but we
* can't be really sure until try the access token)
* *
* @param {String} id * @param {String} id
* @memberof ActionCreators * @memberof ActionCreators
*/ */
export function resolveAuthenticationRequest(id) { export function resolveAuthenticationRequest(id, tokenServiceId, props) {
return ((dispatch, getState) => { return {
const { auth } = getState(); id,
tokenServiceId,
dispatch(fetchAccessTokenRequest(id, auth[id].infoId)); type: ActionTypes.RESOLVE_AUTHENTICATION_REQUEST,
}); ...props,
};
} }
/** /**
...@@ -39,13 +39,11 @@ export function resolveAuthenticationRequest(id) { ...@@ -39,13 +39,11 @@ export function resolveAuthenticationRequest(id) {
* *
* @param {String} serviceId * @param {String} serviceId
* @param {String} authId * @param {String} authId
* @param {String} infoIds
* @memberof ActionCreators * @memberof ActionCreators
*/ */
export function requestAccessToken(serviceId, authId, infoIds) { export function requestAccessToken(serviceId, authId) {
return { return {
authId, authId,
infoIds,
serviceId, serviceId,
type: ActionTypes.REQUEST_ACCESS_TOKEN, type: ActionTypes.REQUEST_ACCESS_TOKEN,
}; };
...@@ -59,8 +57,9 @@ export function requestAccessToken(serviceId, authId, infoIds) { ...@@ -59,8 +57,9 @@ export function requestAccessToken(serviceId, authId, infoIds) {
* @param {Object} json * @param {Object} json
* @memberof ActionCreators * @memberof ActionCreators
*/ */
export function receiveAccessToken(serviceId, json, infoIds) { export function receiveAccessToken(authId, serviceId, json) {
return { return {
authId,
json, json,
serviceId, serviceId,
type: ActionTypes.RECEIVE_ACCESS_TOKEN, type: ActionTypes.RECEIVE_ACCESS_TOKEN,
...@@ -75,71 +74,34 @@ export function receiveAccessToken(serviceId, json, infoIds) { ...@@ -75,71 +74,34 @@ export function receiveAccessToken(serviceId, json, infoIds) {
* @param {Object} error * @param {Object} error
* @memberof ActionCreators * @memberof ActionCreators
*/ */
export function receiveAccessTokenFailure(serviceId, error) { export function receiveAccessTokenFailure(authId, serviceId, error) {
return { return {
authId,
error, error,
serviceId, serviceId,
type: ActionTypes.RECEIVE_ACCESS_TOKEN_FAILURE, type: ActionTypes.RECEIVE_ACCESS_TOKEN_FAILURE,
}; };
} }
/** @private */
export function fetchAccessTokenRequest(id, infoIds, providedServices = undefined) {
return ((dispatch, getState) => {
const { infoResponses } = getState();
const infoResponse = infoResponses[infoIds[0]].json;
const services = providedServices || Utils.getServices(infoResponse);
const authService = services.find(e => e.id === id);
if (!authService) return null;
const accessTokenService = Utils.getService(authService, 'http://iiif.io/api/auth/1/token');
dispatch(requestAccessToken(accessTokenService.id, authService.id, infoIds));
return null;
});
}
/** /**
* resolveAccessTokenRequest - action creator * resolveAccessTokenRequest - action creator
* *
* @param {Object} message * @param {Object} message
* @memberof ActionCreators * @memberof ActionCreators
*/ */
export function resolveAccessTokenRequest({ messageId, ...json }) { export function resolveAccessTokenRequest(authServiceId, tokenServiceId, json) {
return ((dispatch, getState) => { if (!json.accessToken) return receiveAccessTokenFailure(authServiceId, tokenServiceId, json);
const { authId } = getState().accessTokens[messageId];
dispatch({
id: authId,
ok: !!json.accessToken,
type: ActionTypes.RESOLVE_AUTHENTICATION_REQUEST,
});
if (json.accessToken) { return receiveAccessToken(authServiceId, tokenServiceId, json);
dispatch(receiveAccessToken(messageId, json));
} else {
dispatch(receiveAccessTokenFailure(messageId, json));
}
});
} }
/** /**
* Resets authentication state for a token service * Resets authentication state for a token service
*/ */
export function resetAuthenticationState({ authServiceId }) { export function resetAuthenticationState({ authServiceId, tokenServiceId }) {
return ((dispatch, getState) => { return {
const { accessTokens } = getState();
const currentService = Object.values(accessTokens)
.find(service => service.authId === authServiceId);
dispatch({
id: authServiceId, id: authServiceId,
tokenServiceId: currentService && currentService.id, tokenServiceId,
type: ActionTypes.RESET_AUTHENTICATION_STATE, type: ActionTypes.RESET_AUTHENTICATION_STATE,
}); };
});
} }
...@@ -6,11 +6,12 @@ import ActionTypes from './action-types'; ...@@ -6,11 +6,12 @@ import ActionTypes from './action-types';
* @param {String} infoId * @param {String} infoId
* @memberof ActionCreators * @memberof ActionCreators
*/ */
export function requestInfoResponse(infoId, imageResource) { export function requestInfoResponse(infoId, imageResource, windowId) {
return { return {
imageResource, imageResource,
infoId, infoId,
type: ActionTypes.REQUEST_INFO_RESPONSE, type: ActionTypes.REQUEST_INFO_RESPONSE,
windowId,
}; };
} }
...@@ -38,13 +39,14 @@ export function receiveInfoResponse(infoId, infoJson, ok, tokenServiceId) { ...@@ -38,13 +39,14 @@ export function receiveInfoResponse(infoId, infoJson, ok, tokenServiceId) {
* @param {Object} manifestJson * @param {Object} manifestJson
* @memberof ActionCreators * @memberof ActionCreators
*/ */
export function receiveDegradedInfoResponse(infoId, infoJson, ok, tokenServiceId) { export function receiveDegradedInfoResponse(infoId, infoJson, ok, tokenServiceId, windowId) {
return { return {
infoId, infoId,
infoJson, infoJson,
ok, ok,
tokenServiceId, tokenServiceId,
type: ActionTypes.RECEIVE_DEGRADED_INFO_RESPONSE, type: ActionTypes.RECEIVE_DEGRADED_INFO_RESPONSE,
windowId,
}; };
} }
...@@ -70,10 +72,10 @@ export function receiveInfoResponseFailure(infoId, error, tokenServiceId) { ...@@ -70,10 +72,10 @@ export function receiveInfoResponseFailure(infoId, error, tokenServiceId) {
* @param {String} infoId * @param {String} infoId
* @memberof ActionCreators * @memberof ActionCreators
*/ */
export function fetchInfoResponse({ imageId, imageResource }) { export function fetchInfoResponse({ imageId, imageResource, windowId }) {
const imageService = imageResource && imageResource.getServices()[0]; const imageService = imageResource && imageResource.getServices()[0];
const infoId = (imageId || imageService.id); const infoId = (imageId || imageService.id);
return requestInfoResponse(infoId, imageService); return requestInfoResponse(infoId, imageService, windowId);
} }
/** /**
......
import omit from 'lodash/omit'; import omit from 'lodash/omit';
import { Utils } from 'manifesto.js/dist-esmodule/Utils';
import ActionTypes from '../actions/action-types'; import ActionTypes from '../actions/action-types';
/** */ /** */
export function accessTokensReducer(state = {}, action) { export function accessTokensReducer(state = {}, action) {
let authService;
let tokenService;
switch (action.type) { switch (action.type) {
case ActionTypes.RECEIVE_DEGRADED_INFO_RESPONSE: case ActionTypes.RESOLVE_AUTHENTICATION_REQUEST:
authService = Utils.getService({ ...action.infoJson, options: {} }, 'http://iiif.io/api/auth/1/external');
if (!authService) return state;
tokenService = Utils.getService(authService, 'http://iiif.io/api/auth/1/token');
if (!tokenService || state[tokenService.id]) return state;
return { return {
...state, ...state,
[tokenService.id]: { [action.tokenServiceId]: {
authId: authService.id, authId: action.id,
id: tokenService.id, id: action.tokenServiceId,
infoIds: [].concat(
(state[tokenService.id] && state[tokenService.id].infoIds) || [],
action.infoId,
),
isFetching: true, isFetching: true,
}, },
}; };
...@@ -33,7 +19,6 @@ export function accessTokensReducer(state = {}, action) { ...@@ -33,7 +19,6 @@ export function accessTokensReducer(state = {}, action) {
[action.serviceId]: { [action.serviceId]: {
authId: action.authId, authId: action.authId,
id: action.serviceId, id: action.serviceId,
infoIds: action.infoIds,
isFetching: true, isFetching: true,
}, },
}; };
...@@ -46,14 +31,6 @@ export function accessTokensReducer(state = {}, action) { ...@@ -46,14 +31,6 @@ export function accessTokensReducer(state = {}, action) {
json: action.json, json: action.json,
}, },
}; };
case ActionTypes.CLEAR_ACCESS_TOKEN_QUEUE:
return {
...state,
[action.serviceId]: {
...state[action.serviceId],
infoIds: [],
},
};
case ActionTypes.RECEIVE_ACCESS_TOKEN_FAILURE: case ActionTypes.RECEIVE_ACCESS_TOKEN_FAILURE:
return { return {
...state, ...state,
...@@ -65,6 +42,17 @@ export function accessTokensReducer(state = {}, action) { ...@@ -65,6 +42,17 @@ export function accessTokensReducer(state = {}, action) {
}; };
case ActionTypes.RESET_AUTHENTICATION_STATE: case ActionTypes.RESET_AUTHENTICATION_STATE:
return omit(state, action.tokenServiceId); return omit(state, action.tokenServiceId);
case ActionTypes.RECEIVE_INFO_RESPONSE:
if (!action.tokenServiceId) return state;
if (state[action.tokenServiceId].success) return state;
return {
...state,
[action.tokenServiceId]: {
...state[action.tokenServiceId],
success: true,
},
};
default: default:
return state; return state;
} }
......
import omit from 'lodash/omit'; import omit from 'lodash/omit';
import ActionTypes from '../actions/action-types'; import ActionTypes from '../actions/action-types';
import { selectNextAuthService } from '../selectors/canvases';
/** /**
* authReducer * authReducer
*/ */
export const authReducer = (state = {}, action) => { export const authReducer = (state = {}, action) => {
let service;
switch (action.type) { switch (action.type) {
case ActionTypes.RECEIVE_DEGRADED_INFO_RESPONSE:
service = selectNextAuthService(
{ auth: state }, action.infoJson, { external: true, kiosk: true },
);
if (!service || state[service.id]) return state;
return {
...state,
[service.id]: {
id: service.id,
infoId: [].concat(
(state[service.id] && state[service.id].infoId) || [],
action.infoId,
),
isFetching: true,
profile: service.getProfile(),
},
};
case ActionTypes.ADD_AUTHENTICATION_REQUEST: case ActionTypes.ADD_AUTHENTICATION_REQUEST:
return { return {
...state, ...state,
[action.id]: { [action.id]: {
id: action.id, id: action.id,
infoId: [].concat(
(state[action.id] && state[action.id].infoId) || [],
action.infoId,
),
isFetching: true, isFetching: true,
profile: action.profile, profile: action.profile,
windowId: action.windowId,
}, },
}; };
case ActionTypes.RESOLVE_AUTHENTICATION_REQUEST: case ActionTypes.RESOLVE_AUTHENTICATION_REQUEST:
return { return {
...state, ...state,
[action.id]: { [action.id]: {
id: action.id, ...state[action.id],
isFetching: false, isFetching: false,
ok: action.ok, ok: action.ok,
}, },
}; };
case ActionTypes.RECEIVE_ACCESS_TOKEN:
if (!action.authId) return state;
return {
...state,
[action.authId]: {
...state[action.authId],
ok: true,
},
};
case ActionTypes.RESET_AUTHENTICATION_STATE: case ActionTypes.RESET_AUTHENTICATION_STATE:
return omit(state, action.id); return omit(state, action.id);
default: return state; default: return state;
......
...@@ -54,13 +54,6 @@ export const infoResponsesReducer = (state = {}, action) => { ...@@ -54,13 +54,6 @@ export const infoResponsesReducer = (state = {}, action) => {
}, {}); }, {});
case ActionTypes.IMPORT_MIRADOR_STATE: case ActionTypes.IMPORT_MIRADOR_STATE:
return {}; return {};
case ActionTypes.RESET_AUTHENTICATION_STATE:
return Object.keys(state).reduce((object, key) => {
if (state[key].tokenServiceId !== action.tokenServiceId) {
object[key] = state[key]; // eslint-disable-line no-param-reassign
}
return object;
}, {});
default: return state; default: return state;
} }
}; };
import {
all, call, put, select, takeEvery, delay,
} from 'redux-saga/effects';
import { Utils } from 'manifesto.js/dist-esmodule/Utils';
import flatten from 'lodash/flatten';
import ActionTypes from '../actions/action-types';
import MiradorCanvas from '../../lib/MiradorCanvas';
import {
addAuthenticationRequest,
resolveAuthenticationRequest,
requestAccessToken,
resetAuthenticationState,
} from '../actions';
import {
selectInfoResponses,
getVisibleCanvases,
getWindows,
getConfig,
getAuth,
getAccessTokens,
} from '../selectors';
import { fetchInfoResponse } from './iiif';
/** */
export function* refetchInfoResponsesOnLogout({ tokenServiceId }) {
// delay logout actions to give the cookie service a chance to invalidate our cookies
// before we reinitialize openseadragon and rerequest images.
yield delay(2000);
yield call(refetchInfoResponses, { serviceId: tokenServiceId });
}
/**
* Figure out what info responses could have used the access token service and:
* - refetch, if they are currently visible
* - throw them out (and lazy re-fetch) otherwise
*/
export function* refetchInfoResponses({ serviceId }) {
const windows = yield select(getWindows);
const canvases = yield all(
Object.keys(windows).map(windowId => select(getVisibleCanvases, { windowId })),
);
const visibleImageApiIds = flatten(flatten(canvases).map((canvas) => {
const miradorCanvas = new MiradorCanvas(canvas);
return miradorCanvas.imageServiceIds;
}));
const infoResponses = yield select(selectInfoResponses);
/** */
const haveThisTokenService = infoResponse => {
const services = Utils.getServices(infoResponse);
return services.some(e => {
const infoTokenService = Utils.getService(e, 'http://iiif.io/api/auth/1/token')
|| Utils.getService(e, 'http://iiif.io/api/auth/0/token');
return infoTokenService && infoTokenService.id === serviceId;
});
};
const obsoleteInfoResponses = Object.values(infoResponses).filter(
i => i.json && haveThisTokenService(i.json),
);
yield all(obsoleteInfoResponses.map(({ id: infoId }) => {
if (visibleImageApiIds.includes(infoId)) {
return call(fetchInfoResponse, { infoId });
}
return put({ infoId, type: ActionTypes.REMOVE_INFO_RESPONSE });
}));
}
/** try to start any non-interactive auth flows */
export function* doAuthWorkflow({ infoJson, windowId }) {
const auths = yield select(getAuth);
const { auth: { serviceProfiles } } = yield select(getConfig);
const nonInteractiveAuthFlowProfiles = serviceProfiles.filter(p => p.external || p.kiosk);
// try to get an untried, non-interactive auth service
const authService = Utils.getServices(infoJson)
.filter(s => !auths[s.id])
.find(e => nonInteractiveAuthFlowProfiles.some(p => p.profile === e.getProfile()));
if (!authService) return;
const profileConfig = nonInteractiveAuthFlowProfiles.find(
p => p.profile === authService.getProfile(),
);
if (profileConfig.kiosk) {
// start the auth
yield put(addAuthenticationRequest(windowId, authService.id, authService.getProfile()));
} else if (profileConfig.external) {
const tokenService = Utils.getService(authService, 'http://iiif.io/api/auth/1/token')
|| Utils.getService(authService, 'http://iiif.io/api/auth/0/token');
if (!tokenService) return;
// resolve the auth
yield put(resolveAuthenticationRequest(authService.id, tokenService.id));
// start access tokens
yield put(requestAccessToken(tokenService.id, authService.id));
}
}
/** */
export function* rerequestOnAccessTokenFailure({ infoJson, windowId, tokenServiceId }) {
if (!tokenServiceId) return;
// make sure we have an auth service to try
const authService = Utils.getServices(infoJson).find(service => {
const tokenService = Utils.getService(service, 'http://iiif.io/api/auth/1/token')
|| Utils.getService(service, 'http://iiif.io/api/auth/0/token');
return tokenService && tokenService.id === tokenServiceId;
});
if (!authService) return;
// make sure the token ever worked (and might have expired or needs to be re-upped)
const accessTokenServices = yield select(getAccessTokens);
const service = accessTokenServices[tokenServiceId];
if (!(service && service.success)) return;
yield put(requestAccessToken(tokenServiceId, authService.id));
}
/** */
export function* invalidateInvalidAuth({ serviceId }) {
const accessTokenServices = yield select(getAccessTokens);
const authServices = yield select(getAuth);
const accessTokenService = accessTokenServices[serviceId];
if (!accessTokenService) return;
const authService = authServices[accessTokenService.authId];
if (!authService) return;
if (accessTokenService.success) {
// if the token ever worked, reset things so we try to get a new cookie
yield put(resetAuthenticationState({
authServiceId: authService.id,
tokenServiceId: accessTokenService.id,
}));
} else {
// if the token never worked, mark the auth service as bad so we could
// try to pick a different service
yield put(resolveAuthenticationRequest(
authService.id,
accessTokenService.id,
{ ok: false },
));
}
}
/** */
export default function* authSaga() {
yield all([
takeEvery(ActionTypes.RECEIVE_DEGRADED_INFO_RESPONSE, rerequestOnAccessTokenFailure),
takeEvery(ActionTypes.RECEIVE_ACCESS_TOKEN_FAILURE, invalidateInvalidAuth),
takeEvery(ActionTypes.RECEIVE_DEGRADED_INFO_RESPONSE, doAuthWorkflow),
takeEvery(ActionTypes.RECEIVE_ACCESS_TOKEN, refetchInfoResponses),
takeEvery(ActionTypes.RESET_AUTHENTICATION_STATE, refetchInfoResponsesOnLogout),
]);
}
...@@ -115,7 +115,10 @@ export function* fetchManifest({ manifestId }) { ...@@ -115,7 +115,10 @@ export function* fetchManifest({ manifestId }) {
/** @private */ /** @private */
function* getAccessTokenService(resource) { function* getAccessTokenService(resource) {
const services = Utils.getServices({ ...resource, options: {} }).filter(s => s.getProfile().match(/http:\/\/iiif.io\/api\/auth\//)); const manifestoCompatibleResource = resource && resource.__jsonld
? resource
: { ...resource, options: {} };
const services = Utils.getServices(manifestoCompatibleResource).filter(s => s.getProfile().match(/http:\/\/iiif.io\/api\/auth\//));
if (services.length === 0) return undefined; if (services.length === 0) return undefined;
const accessTokens = yield select(getAccessTokens); const accessTokens = yield select(getAccessTokens);
...@@ -123,8 +126,9 @@ function* getAccessTokenService(resource) { ...@@ -123,8 +126,9 @@ function* getAccessTokenService(resource) {
for (let i = 0; i < services.length; i += 1) { for (let i = 0; i < services.length; i += 1) {
const authService = services[i]; const authService = services[i];
const accessTokenService = Utils.getService(authService, 'http://iiif.io/api/auth/1/token'); const accessTokenService = Utils.getService(authService, 'http://iiif.io/api/auth/1/token')
const token = accessTokens[accessTokenService.id]; || Utils.getService(authService, 'http://iiif.io/api/auth/0/token');
const token = accessTokenService && accessTokens[accessTokenService.id];
if (token && token.json) return token; if (token && token.json) return token;
} }
...@@ -132,7 +136,7 @@ function* getAccessTokenService(resource) { ...@@ -132,7 +136,7 @@ function* getAccessTokenService(resource) {
} }
/** @private */ /** @private */
export function* fetchInfoResponse({ imageResource, infoId, tokenService: passedTokenService }) { export function* fetchInfoResponse({ imageResource, infoId, windowId }) {
let iiifResource = imageResource; let iiifResource = imageResource;
if (!iiifResource) { if (!iiifResource) {
iiifResource = yield select(selectInfoResponse, { infoId }); iiifResource = yield select(selectInfoResponse, { infoId });
...@@ -141,7 +145,7 @@ export function* fetchInfoResponse({ imageResource, infoId, tokenService: passed ...@@ -141,7 +145,7 @@ export function* fetchInfoResponse({ imageResource, infoId, tokenService: passed
const callbacks = { const callbacks = {
degraded: ({ degraded: ({
json, response, tokenServiceId, json, response, tokenServiceId,
}) => receiveDegradedInfoResponse(infoId, json, response.ok, tokenServiceId), }) => receiveDegradedInfoResponse(infoId, json, response.ok, tokenServiceId, windowId),
failure: ({ failure: ({
error, json, response, tokenServiceId, error, json, response, tokenServiceId,
}) => ( }) => (
...@@ -194,22 +198,6 @@ export function* fetchResourceManifest({ manifestId, manifestJson }) { ...@@ -194,22 +198,6 @@ export function* fetchResourceManifest({ manifestId, manifestJson }) {
if (!manifests[manifestId]) yield* fetchManifest({ manifestId }); if (!manifests[manifestId]) yield* fetchManifest({ manifestId });
} }
/** @private */
export function* refetchInfoResponses({ serviceId }) {
const accessTokens = yield select(getAccessTokens);
const tokenService = accessTokens && accessTokens[serviceId];
if (!tokenService || tokenService.infoIds === []) return;
yield all(
tokenService.infoIds.map(infoId => call(fetchInfoResponse, { infoId, tokenService })),
);
// TODO: Other resources could be refetched too
yield put({ serviceId, type: ActionTypes.CLEAR_ACCESS_TOKEN_QUEUE });
}
/** */ /** */
export function* fetchManifests(...manifestIds) { export function* fetchManifests(...manifestIds) {
const manifests = yield select(getManifests); const manifests = yield select(getManifests);
...@@ -227,7 +215,6 @@ export default function* iiifSaga() { ...@@ -227,7 +215,6 @@ export default function* iiifSaga() {
takeEvery(ActionTypes.REQUEST_INFO_RESPONSE, fetchInfoResponse), takeEvery(ActionTypes.REQUEST_INFO_RESPONSE, fetchInfoResponse),
takeEvery(ActionTypes.REQUEST_SEARCH, fetchSearchResponse), takeEvery(ActionTypes.REQUEST_SEARCH, fetchSearchResponse),
takeEvery(ActionTypes.REQUEST_ANNOTATION, fetchAnnotation), takeEvery(ActionTypes.REQUEST_ANNOTATION, fetchAnnotation),
takeEvery(ActionTypes.RECEIVE_ACCESS_TOKEN, refetchInfoResponses),
takeEvery(ActionTypes.ADD_RESOURCE, fetchResourceManifest), takeEvery(ActionTypes.ADD_RESOURCE, fetchResourceManifest),
]); ]);
} }
...@@ -5,7 +5,8 @@ import { ...@@ -5,7 +5,8 @@ import {
import appSaga from './app'; import appSaga from './app';
import iiifSaga from './iiif'; import iiifSaga from './iiif';
import windowSaga from './windows'; import windowSaga from './windows';
import annotations from './annotations'; import annotationsSaga from './annotations';
import authSaga from './auth';
/** */ /** */
function* launchSaga(saga) { function* launchSaga(saga) {
...@@ -23,10 +24,11 @@ function* launchSaga(saga) { ...@@ -23,10 +24,11 @@ function* launchSaga(saga) {
function getRootSaga(pluginSagas = []) { function getRootSaga(pluginSagas = []) {
return function* rootSaga() { return function* rootSaga() {
const sagas = [ const sagas = [
annotations, annotationsSaga,
appSaga, appSaga,
iiifSaga, iiifSaga,
windowSaga, windowSaga,
authSaga,
...pluginSagas, ...pluginSagas,
]; ];
......
...@@ -223,7 +223,7 @@ export function* fetchInfoResponses({ visibleCanvases: visibleCanvasIds, windowI ...@@ -223,7 +223,7 @@ export function* fetchInfoResponses({ visibleCanvases: visibleCanvasIds, windowI
const miradorCanvas = new MiradorCanvas(canvas); const miradorCanvas = new MiradorCanvas(canvas);
return all(miradorCanvas.iiifImageResources.map(imageResource => ( return all(miradorCanvas.iiifImageResources.map(imageResource => (
!infoResponses[imageResource.getServices()[0].id] !infoResponses[imageResource.getServices()[0].id]
&& put(fetchInfoResponse({ imageResource })) && put(fetchInfoResponse({ imageResource, windowId }))
)).filter(Boolean)); )).filter(Boolean));
})); }));
} }
......
import { createSelector } from 'reselect';
import { Utils } from 'manifesto.js/dist-esmodule/Utils';
import flatten from 'lodash/flatten';
import MiradorCanvas from '../../lib/MiradorCanvas';
import { miradorSlice } from './utils'; import { miradorSlice } from './utils';
import { getConfig } from './config';
import { getVisibleCanvases, selectInfoResponses } from './canvases';
export const getAuthProfiles = createSelector(
[
getConfig,
],
({ auth: { serviceProfiles = [] } }) => serviceProfiles,
);
/** */ /** */
export const getAccessTokens = state => miradorSlice(state).accessTokens || {}; export const getAccessTokens = state => miradorSlice(state).accessTokens || {};
/** */ /** */
export const getAuth = state => miradorSlice(state).auth || {}; export const getAuth = state => miradorSlice(state).auth || {};
export const selectCurrentAuthServices = createSelector(
[
getVisibleCanvases,
selectInfoResponses,
getAuthProfiles,
getAuth,
(state, { iiifResources }) => iiifResources,
],
(canvases, infoResponses = {}, serviceProfiles, auth, iiifResources) => {
let currentAuthResources = iiifResources;
if (!currentAuthResources && canvases) {
currentAuthResources = flatten(canvases.map(c => {
const miradorCanvas = new MiradorCanvas(c);
const images = miradorCanvas.iiifImageResources;
return images.map(i => {
const iiifImageService = i.getServices()[0];
const infoResponse = infoResponses[iiifImageService.id];
if (infoResponse && infoResponse.json) {
return { ...infoResponse.json, options: {} };
}
return iiifImageService;
});
}));
}
if (!currentAuthResources) return [];
if (currentAuthResources.length === 0) return [];
const currentAuthServices = currentAuthResources.map(resource => {
let lastAttemptedService;
const services = Utils.getServices(resource);
for (const authProfile of serviceProfiles) {
const profiledAuthServices = services.filter(
p => authProfile.profile === p.getProfile(),
);
for (const service of profiledAuthServices) {
lastAttemptedService = service;
if (!auth[service.id] || auth[service.id].isFetching || auth[service.id].ok) {
return service;
}
}
}
return lastAttemptedService;
});
return Object.values(currentAuthServices.reduce((h, service) => {
if (service && !h[service.id]) {
h[service.id] = service; // eslint-disable-line no-param-reassign
}
return h;
}, {}));
},
);
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please to comment