diff --git a/__tests__/src/components/AuthenticationLogout.test.js b/__tests__/src/components/AuthenticationLogout.test.js
new file mode 100644
index 0000000000000000000000000000000000000000..e120d6e9188e17a7cbcacfb18993dad313de7de4
--- /dev/null
+++ b/__tests__/src/components/AuthenticationLogout.test.js
@@ -0,0 +1,42 @@
+import React from 'react';
+import { shallow } from 'enzyme';
+import Fab from '@material-ui/core/Fab';
+import { AuthenticationLogout } from '../../../src/components/AuthenticationLogout';
+
+/**
+ * Helper function to create a shallow wrapper around AuthenticationLogout
+ */
+function createWrapper(props) {
+  return shallow(
+    <AuthenticationLogout
+      authServiceId="http://example.com/auth"
+      label="Log out now!"
+      logoutServiceId="http://example.com/logout"
+      status="ok"
+      t={key => key}
+      windowId="w"
+      {...props}
+    />,
+  );
+}
+
+describe('AuthenticationLogout', () => {
+  it('when status is not ok, render fragment', () => {
+    const wrapper = createWrapper({ status: 'fail' });
+    expect(wrapper.matchesElement(<></>)).toBe(true);
+  });
+  it('renders Fab with logout label', () => {
+    const wrapper = createWrapper();
+    expect(wrapper.find(Fab).length).toBe(1);
+    expect(wrapper.find(Fab).text()).toBe('Log out now!');
+  });
+  it('click opens a new window to logout and resets state', () => {
+    const mockWindow = {};
+    const open = jest.fn(() => mockWindow);
+    const reset = jest.fn(() => {});
+    const wrapper = createWrapper({ depWindow: { open }, resetAuthenticationState: reset });
+    expect(wrapper.find(Fab).props().onClick());
+    expect(open).toHaveBeenCalledWith('http://example.com/logout');
+    expect(reset).toHaveBeenCalledWith({ authServiceId: 'http://example.com/auth' });
+  });
+});
diff --git a/__tests__/src/components/WindowAuthenticationControl.test.js b/__tests__/src/components/WindowAuthenticationControl.test.js
index c7f0fb66e8f96b0ef8e197fcb1aa7f8b22d1c247..ffa31741da69f6c345cc8ee1de67ac0ba56e12d3 100644
--- a/__tests__/src/components/WindowAuthenticationControl.test.js
+++ b/__tests__/src/components/WindowAuthenticationControl.test.js
@@ -5,9 +5,10 @@ import Collapse from '@material-ui/core/Collapse';
 import DialogActions from '@material-ui/core/DialogActions';
 import SanitizedHtml from '../../../src/containers/SanitizedHtml';
 import { WindowAuthenticationControl } from '../../../src/components/WindowAuthenticationControl';
+import AuthenticationLogout from '../../../src/containers/AuthenticationLogout';
 
 /**
- * Helper function to create a shallow wrapper around ErrorDialog
+ * Helper function to create a shallow wrapper around WindowAuthenticationControl
  */
 function createWrapper(props) {
   return shallow(
@@ -27,9 +28,9 @@ function createWrapper(props) {
 describe('WindowAuthenticationControl', () => {
   let wrapper;
 
-  it('renders nothing if it is not degraded', () => {
+  it('renders AuthenticationLogout if it is not degraded', () => {
     wrapper = createWrapper({ degraded: false });
-    expect(wrapper.matchesElement(<></>)).toBe(true);
+    expect(wrapper.find(AuthenticationLogout).length).toBe(1);
   });
 
   describe('with a non-interactive login', () => {
diff --git a/__tests__/src/reducers/accessTokens.test.js b/__tests__/src/reducers/accessTokens.test.js
index 1acf76a2081abe040c5b634de9abb8ecfaad5aba..acf6f8891759ff28f31d6237f8cca0e010497803 100644
--- a/__tests__/src/reducers/accessTokens.test.js
+++ b/__tests__/src/reducers/accessTokens.test.js
@@ -135,4 +135,20 @@ describe('access tokens response reducer', () => {
       },
     });
   });
+  describe('should handle RESET_AUTHENTICATION_STATE', () => {
+    it('does nothing if tokenServiceId is not present', () => {
+      expect(accessTokensReducer({}, {
+        tokenServiceId: 'foo',
+        type: ActionTypes.RESET_AUTHENTICATION_STATE,
+      })).toEqual({});
+    });
+    it('removes tokenServiceId', () => {
+      expect(accessTokensReducer({
+        foo: 'otherStuff',
+      }, {
+        tokenServiceId: 'foo',
+        type: ActionTypes.RESET_AUTHENTICATION_STATE,
+      })).toEqual({});
+    });
+  });
 });
diff --git a/__tests__/src/reducers/auth.test.js b/__tests__/src/reducers/auth.test.js
index 151db1096fcc933050ef1506e2c9108034a2ba87..af971069e85fb6ffbc3e3fd65f5c3415f396c5b3 100644
--- a/__tests__/src/reducers/auth.test.js
+++ b/__tests__/src/reducers/auth.test.js
@@ -103,4 +103,20 @@ describe('auth response reducer', () => {
       },
     });
   });
+  describe('should handle RESET_AUTHENTICATION_STATE', () => {
+    it('does nothing if id is not present', () => {
+      expect(authReducer({}, {
+        id: 'foo',
+        type: ActionTypes.RESET_AUTHENTICATION_STATE,
+      })).toEqual({});
+    });
+    it('removes id', () => {
+      expect(authReducer({
+        foo: 'otherStuff',
+      }, {
+        id: 'foo',
+        type: ActionTypes.RESET_AUTHENTICATION_STATE,
+      })).toEqual({});
+    });
+  });
 });
diff --git a/__tests__/src/selectors/canvases.test.js b/__tests__/src/selectors/canvases.test.js
index 3ed5a6a27adb00c1d6968442f12e8070e348997b..d135fa576096e7fe5c7772d47d6793c5809d3836 100644
--- a/__tests__/src/selectors/canvases.test.js
+++ b/__tests__/src/selectors/canvases.test.js
@@ -13,6 +13,7 @@ import {
   selectNextAuthService,
   selectInfoResponse,
   getVisibleCanvasNonTiledResources,
+  selectLogoutAuthService,
 } from '../../../src/state/selectors/canvases';
 
 describe('getVisibleCanvases', () => {
@@ -314,6 +315,50 @@ describe('selectCanvasAuthService', () => {
   });
 });
 
+describe('selectLogoutAuthService', () => {
+  it('returns a logout auth service if one exists', () => {
+    const logout = {
+      '@id': 'http://foo/logout',
+      profile: 'http://iiif.io/api/auth/1/logout',
+    };
+    const resource = {
+      service: [
+        {
+          '@id': 'login',
+          profile: 'http://iiif.io/api/auth/1/login',
+          service: [
+            logout,
+          ],
+        },
+      ],
+    };
+    const state = {
+      auth: {
+        login: {
+          ok: true,
+        },
+      },
+      infoResponses: {
+        'https://iiif.bodleian.ox.ac.uk/iiif/image/9cca8fdd-4a61-4429-8ac1-f648764b4d6d': {
+          json: resource,
+        },
+      },
+      manifests: {
+        a: {
+          json: manifestFixture001,
+        },
+      },
+    };
+    expect(
+      selectLogoutAuthService(
+        state,
+        { canvasId: 'https://iiif.bodleian.ox.ac.uk/iiif/canvas/9cca8fdd-4a61-4429-8ac1-f648764b4d6d.json', manifestId: 'a' },
+      ).id,
+    )
+      .toBe(logout['@id']);
+  });
+});
+
 describe('selectInfoResponse', () => {
   it('returns in the info response for the first canvas resource', () => {
     const resource = { some: 'resource' };
diff --git a/src/components/AuthenticationLogout.js b/src/components/AuthenticationLogout.js
new file mode 100644
index 0000000000000000000000000000000000000000..82c37de15f562c0053fa00a740d31ada12b1bfbb
--- /dev/null
+++ b/src/components/AuthenticationLogout.js
@@ -0,0 +1,57 @@
+import React, { Component } from 'react';
+import PropTypes from 'prop-types';
+import Fab from '@material-ui/core/Fab';
+
+/**
+ *
+ */
+export class AuthenticationLogout extends Component {
+  /** */
+  constructor(props) {
+    super(props);
+
+    this.handleLogout = this.handleLogout.bind(this);
+  }
+
+  /** */
+  handleLogout() {
+    const {
+      authServiceId, depWindow, logoutServiceId, resetAuthenticationState,
+    } = this.props;
+    (depWindow || window).open(logoutServiceId);
+
+    resetAuthenticationState({ authServiceId });
+  }
+
+  /** */
+  render() {
+    const {
+      label, status, t,
+    } = this.props;
+    if (status !== 'ok') return <></>;
+    return (
+      <Fab color="primary" variant="extended" onClick={this.handleLogout}>
+        {label || t('logout')}
+      </Fab>
+    );
+  }
+}
+
+AuthenticationLogout.propTypes = {
+  authServiceId: PropTypes.string,
+  depWindow: PropTypes.object, // eslint-disable-line react/forbid-prop-types
+  label: PropTypes.string,
+  logoutServiceId: PropTypes.string,
+  resetAuthenticationState: PropTypes.func.isRequired,
+  status: PropTypes.string,
+  t: PropTypes.func,
+};
+
+AuthenticationLogout.defaultProps = {
+  authServiceId: undefined,
+  depWindow: undefined,
+  label: undefined,
+  logoutServiceId: undefined,
+  status: undefined,
+  t: () => {},
+};
diff --git a/src/components/WindowAuthenticationControl.js b/src/components/WindowAuthenticationControl.js
index 95f9d6cf14bfbff635954f272f6a8f0af81cfed9..7ea9e787db7a2b8abbb6f584e6f489c8f3d10b9f 100644
--- a/src/components/WindowAuthenticationControl.js
+++ b/src/components/WindowAuthenticationControl.js
@@ -7,6 +7,7 @@ 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';
 
 /**
  */
@@ -67,10 +68,11 @@ export class WindowAuthenticationControl extends Component {
       profile,
       status,
       t,
+      windowId,
     } = this.props;
 
     const failed = status === 'failed';
-    if ((!degraded || !profile) && status !== 'fetching') return <></>;
+    if ((!degraded || !profile) && status !== 'fetching') return <AuthenticationLogout windowId={windowId} />;
     if (!this.isInteractive() && !failed) return <></>;
 
     const { showFailureMessage, open } = this.state;
diff --git a/src/containers/AuthenticationLogout.js b/src/containers/AuthenticationLogout.js
new file mode 100644
index 0000000000000000000000000000000000000000..1178992d1a2e45e993bbded92f091b62211cdb14
--- /dev/null
+++ b/src/containers/AuthenticationLogout.js
@@ -0,0 +1,46 @@
+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);
diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json
index 56a2cbc22da2eb2b92af38024ab05714c35ae5cb..e121a0bc256a292e8e0cbf192b0f9f5a42a8d2ef 100644
--- a/src/locales/en/translation.json
+++ b/src/locales/en/translation.json
@@ -69,6 +69,7 @@
     "links": "Links",
     "listAllOpenWindows": "Jump to window",
     "login": "Log in",
+    "logout": "Log out",
     "manifestError": "The resource cannot be added:",
     "maximizeWindow": "Maximize window",
     "menu": "Menu",
diff --git a/src/state/actions/action-types.js b/src/state/actions/action-types.js
index de5fe096ca5fa062984c726d5134dce461f13d7a..114ff0b47b557061f375467a4407e4464613ad70 100644
--- a/src/state/actions/action-types.js
+++ b/src/state/actions/action-types.js
@@ -57,6 +57,7 @@ const ActionTypes = {
   REQUEST_ACCESS_TOKEN: 'mirador/REQUEST_ACCESS_TOKEN',
   RECEIVE_ACCESS_TOKEN: 'mirador/RECEIVE_ACCESS_TOKEN',
   RECEIVE_ACCESS_TOKEN_FAILURE: 'mirador/RECEIVE_ACCESS_TOKEN_FAILURE',
+  RESET_AUTHENTICATION_STATE: 'mirador/RESET_AUTHENTICATION_STATE',
 
   REQUEST_SEARCH: 'mirador/REQUEST_SEARCH',
   RECEIVE_SEARCH: 'mirador/RECEIVE_SEARCH',
diff --git a/src/state/actions/auth.js b/src/state/actions/auth.js
index 7421e173d1603f3fa717fb5fcfa50eb778701a89..c509cad09387492a9a1ae750480f847150398163 100644
--- a/src/state/actions/auth.js
+++ b/src/state/actions/auth.js
@@ -127,3 +127,20 @@ export function resolveAccessTokenRequest({ messageId, ...json }) {
     }
   });
 }
+
+/**
+ * Resets authentication state for a token service
+ */
+export function resetAuthenticationState({ authServiceId }) {
+  return ((dispatch, getState) => {
+    const { accessTokens } = getState();
+
+    const currentService = Object.values(accessTokens)
+      .find(service => service.authId === authServiceId);
+    dispatch({
+      id: authServiceId,
+      tokenServiceId: currentService && currentService.id,
+      type: ActionTypes.RESET_AUTHENTICATION_STATE,
+    });
+  });
+}
diff --git a/src/state/reducers/accessTokens.js b/src/state/reducers/accessTokens.js
index 76cbeba1c395627592c24042f79abb1d91e7ec73..fd7b25babc22a433e3254ab159d564001faba42e 100644
--- a/src/state/reducers/accessTokens.js
+++ b/src/state/reducers/accessTokens.js
@@ -1,5 +1,5 @@
 import normalizeUrl from 'normalize-url';
-
+import { removeIn } from 'immutable';
 import { Utils } from 'manifesto.js/dist-esmodule/Utils';
 import ActionTypes from '../actions/action-types';
 
@@ -59,6 +59,8 @@ export function accessTokensReducer(state = {}, action) {
           isFetching: false,
         },
       };
+    case ActionTypes.RESET_AUTHENTICATION_STATE:
+      return removeIn(state, [action.tokenServiceId]);
     default:
       return state;
   }
diff --git a/src/state/reducers/auth.js b/src/state/reducers/auth.js
index 99594e001aaa12982990684bcba1039ef355cb79..7fbc678e5e5b265483a4c1c29916ca1f07a4da69 100644
--- a/src/state/reducers/auth.js
+++ b/src/state/reducers/auth.js
@@ -1,4 +1,6 @@
 import normalizeUrl from 'normalize-url';
+import { removeIn } from 'immutable';
+
 import ActionTypes from '../actions/action-types';
 import { selectNextAuthService } from '../selectors/canvases';
 
@@ -51,6 +53,8 @@ export const authReducer = (state = {}, action) => {
           ok: action.ok,
         },
       };
+    case ActionTypes.RESET_AUTHENTICATION_STATE:
+      return removeIn(state, [action.id]);
     default: return state;
   }
 };
diff --git a/src/state/selectors/canvases.js b/src/state/selectors/canvases.js
index 347e1646bd3fc3bc3cd570360156985b881aaa6c..5513e2eab30fe383f97046eef95282c0f4896ede 100644
--- a/src/state/selectors/canvases.js
+++ b/src/state/selectors/canvases.js
@@ -266,6 +266,19 @@ export const selectCanvasAuthService = createSelector(
   },
 );
 
+export const selectLogoutAuthService = createSelector(
+  [
+    selectInfoResponse,
+    state => state,
+  ],
+  (infoResponse, state) => {
+    if (!infoResponse) return undefined;
+    const authService = selectActiveAuthService(state, infoResponse.json);
+    if (!authService) return undefined;
+    return authService.getService('http://iiif.io/api/auth/1/logout');
+  },
+);
+
 /** */
 export function selectAuthStatus({ auth }, service) {
   if (!service) return null;