diff --git a/__tests__/src/actions/auth.test.js b/__tests__/src/actions/auth.test.js
index 039e1853a5e1f753e5d2e7836ffbe7d4e31c123b..a37062cc09152676dd0a3092f5af9966dc250a81 100644
--- a/__tests__/src/actions/auth.test.js
+++ b/__tests__/src/actions/auth.test.js
@@ -1,90 +1,67 @@
-import configureMockStore from 'redux-mock-store';
-import thunk from 'redux-thunk';
-
 import * as actions from '../../../src/state/actions';
 import ActionTypes from '../../../src/state/actions/action-types';
 
-const middlewares = [thunk];
-const mockStore = configureMockStore(middlewares);
-
 describe('auth actions', () => {
   describe('addAuthenticationRequest', () => {
     it('requests an authentication attempt from given a url', () => {
       const id = 'abc123';
       const windowId = 'windowId';
-      const infoId = 'infoId';
 
       const expectedAction = {
         id,
-        infoId,
         type: ActionTypes.ADD_AUTHENTICATION_REQUEST,
         windowId,
       };
-      expect(actions.addAuthenticationRequest(windowId, infoId, id)).toEqual(expectedAction);
+      expect(actions.addAuthenticationRequest(windowId, id)).toEqual(expectedAction);
     });
   });
   describe('resolveAuthenticationRequest', () => {
-    let store = null;
-    beforeEach(() => {
-      store = mockStore({});
-    });
-
-    it('triggers an access token fetch', () => {
+    it('markes the auth request as resolved (pending fetching access tokens to mark it a success)', () => {
       const authId = 'abc123';
-      const infoId = 'x';
-      const serviceId = 'xyz';
+      const tokenServiceId = 'xyz';
 
-      store = mockStore({
-        auth: {
-          [authId]: {
-            infoId: [infoId],
-          },
-        },
-        infoResponses: {
-          [infoId]: {
-            json: {
-              service: {
-                '@id': authId,
-                service: {
-                  '@id': serviceId,
-                  profile: 'http://iiif.io/api/auth/1/token',
-                },
-              },
-            },
-          },
-        },
-      });
+      const expectedAction = {
+        id: authId,
+        tokenServiceId,
+        type: ActionTypes.RESOLVE_AUTHENTICATION_REQUEST,
+      };
+
+      expect(actions.resolveAuthenticationRequest(authId, tokenServiceId)).toEqual(expectedAction);
+    });
+    it('can be marked as failed', () => {
+      const authId = 'abc123';
+      const tokenServiceId = 'xyz';
 
       const expectedAction = {
-        authId,
-        infoIds: [infoId],
-        serviceId,
-        type: ActionTypes.REQUEST_ACCESS_TOKEN,
+        id: authId,
+        ok: false,
+        tokenServiceId,
+        type: ActionTypes.RESOLVE_AUTHENTICATION_REQUEST,
       };
 
-      store.dispatch(actions.resolveAuthenticationRequest(authId));
-      expect(store.getActions()).toEqual([expectedAction]);
+      expect(
+        actions.resolveAuthenticationRequest(authId, tokenServiceId, { ok: false }),
+      ).toEqual(expectedAction);
     });
   });
 
   describe('requestAccessToken', () => {
     it('requests an infoResponse from given a url', () => {
       const authId = 'abc123';
-      const infoIds = ['x'];
       const serviceId = 'xyz';
 
       const expectedAction = {
         authId,
-        infoIds,
         serviceId,
         type: ActionTypes.REQUEST_ACCESS_TOKEN,
       };
-      expect(actions.requestAccessToken(serviceId, authId, infoIds)).toEqual(expectedAction);
+      expect(actions.requestAccessToken(serviceId, authId)).toEqual(expectedAction);
     });
   });
 
   describe('receiveAccessToken', () => {
     it('recieves an access token', () => {
+      const authId = 'auth';
       const serviceId = 'abc123';
       const json = {
         content: 'image information request',
@@ -92,76 +69,54 @@ describe('auth actions', () => {
       };
 
       const expectedAction = {
+        authId,
         json,
         serviceId,
         type: ActionTypes.RECEIVE_ACCESS_TOKEN,
       };
-      expect(actions.receiveAccessToken(serviceId, json)).toEqual(expectedAction);
+      expect(actions.receiveAccessToken(authId, serviceId, json)).toEqual(expectedAction);
     });
   });
 
   describe('receiveAccessTokenFailure', () => {
     it('fails to receive an access token', () => {
+      const authId = 'auth';
       const serviceId = 'abc123';
       const error = 'some error';
 
       const expectedAction = {
+        authId,
         error,
         serviceId,
         type: ActionTypes.RECEIVE_ACCESS_TOKEN_FAILURE,
       };
-      expect(actions.receiveAccessTokenFailure(serviceId, error)).toEqual(expectedAction);
+      expect(actions.receiveAccessTokenFailure(authId, serviceId, error)).toEqual(expectedAction);
     });
   });
 
   describe('resolveAccessTokenRequest', () => {
-    let store = null;
-    beforeEach(() => {
-      store = mockStore({});
-    });
-
     it('resolves the auth request, receives the access token, and re-dispatches fetching info responses', () => {
-      const authId = 'abc123';
-      const infoId = 'x';
-      const messageId = 'xyz';
+      const authId = 'auth';
+      const serviceId = 'abc123';
       const json = { accessToken: 1 };
 
-      store = mockStore({
-        accessTokens: {
-          [messageId]: {
-            authId,
-            infoIds: [infoId],
-          },
+      expect(actions.resolveAccessTokenRequest(authId, serviceId, json)).toEqual(
+        {
+          authId, json, serviceId, type: ActionTypes.RECEIVE_ACCESS_TOKEN,
         },
-      });
-
-      store.dispatch(actions.resolveAccessTokenRequest({ messageId, ...json }));
-      expect(store.getActions()).toEqual([
-        { id: authId, ok: true, type: ActionTypes.RESOLVE_AUTHENTICATION_REQUEST },
-        { json, serviceId: messageId, type: ActionTypes.RECEIVE_ACCESS_TOKEN },
-      ]);
+      );
     });
 
     it('without an access token, resolves the auth request unsuccessfully', () => {
-      const authId = 'abc123';
-      const infoId = 'x';
-      const messageId = 'xyz';
+      const authId = 'auth';
+      const serviceId = 'abc123';
       const json = { error: 'xyz' };
 
-      store = mockStore({
-        accessTokens: {
-          [messageId]: {
-            authId,
-            infoIds: [infoId],
-          },
+      expect(actions.resolveAccessTokenRequest(authId, serviceId, json)).toEqual(
+        {
+          authId, error: json, serviceId, type: ActionTypes.RECEIVE_ACCESS_TOKEN_FAILURE,
         },
-      });
-
-      store.dispatch(actions.resolveAccessTokenRequest({ messageId, ...json }));
-      expect(store.getActions()).toEqual([
-        { id: authId, ok: false, type: ActionTypes.RESOLVE_AUTHENTICATION_REQUEST },
-        { error: json, serviceId: messageId, type: ActionTypes.RECEIVE_ACCESS_TOKEN_FAILURE },
-      ]);
+      );
     });
   });
 });
diff --git a/__tests__/src/components/App.test.js b/__tests__/src/components/App.test.js
index e67b7edf3203f1aa1304b2293d192969d9a743c8..5046765124baa2018ddb8769162a0b42a03b0fd9 100644
--- a/__tests__/src/components/App.test.js
+++ b/__tests__/src/components/App.test.js
@@ -2,8 +2,6 @@ import React from 'react';
 import { shallow } from 'enzyme';
 import PluginProvider from '../../../src/extend/PluginProvider';
 import AppProviders from '../../../src/containers/AppProviders';
-import AccessTokenSender from '../../../src/containers/AccessTokenSender';
-import AuthenticationSender from '../../../src/containers/AuthenticationSender';
 import { App } from '../../../src/components/App';
 
 /** */
@@ -21,7 +19,5 @@ describe('App', () => {
     expect(wrapper.find(PluginProvider).length).toBe(1);
     expect(wrapper.find(AppProviders).length).toBe(1);
     expect(wrapper.find('Suspense').length).toBe(1);
-    expect(wrapper.find(AuthenticationSender).length).toBe(1);
-    expect(wrapper.find(AccessTokenSender).length).toBe(1);
   });
 });
diff --git a/__tests__/src/components/AuthenticationLogout.test.js b/__tests__/src/components/AuthenticationLogout.test.js
deleted file mode 100644
index e120d6e9188e17a7cbcacfb18993dad313de7de4..0000000000000000000000000000000000000000
--- a/__tests__/src/components/AuthenticationLogout.test.js
+++ /dev/null
@@ -1,42 +0,0 @@
-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/AuthenticationSender.test.js b/__tests__/src/components/AuthenticationSender.test.js
deleted file mode 100644
index 4b18215e28673b93297a1e61bc9791b392c941a9..0000000000000000000000000000000000000000
--- a/__tests__/src/components/AuthenticationSender.test.js
+++ /dev/null
@@ -1,44 +0,0 @@
-import React from 'react';
-import { shallow } from 'enzyme';
-import { NewWindow } from '../../../src/components/NewWindow';
-import { AuthenticationSender } from '../../../src/components/AuthenticationSender';
-
-/**
- * Helper function to create a shallow wrapper around ErrorDialog
- */
-function createWrapper(props) {
-  return shallow(
-    <AuthenticationSender
-      t={key => key}
-      handleInteraction={() => {}}
-      {...props}
-    />,
-  );
-}
-
-describe('AuthenticationSender', () => {
-  let wrapper;
-
-  it('renders nothing if there is no url', () => {
-    wrapper = createWrapper({});
-    expect(wrapper.matchesElement(<></>)).toBe(true);
-  });
-
-  it('renders properly', () => {
-    Object.defineProperty(window, 'origin', {
-      value: 'http://localhost',
-      writable: true,
-    });
-    wrapper = createWrapper({ url: 'http://example.com' });
-    expect(wrapper.find(NewWindow).length).toBe(1);
-    expect(wrapper.find(NewWindow).props().url).toBe('http://example.com?origin=http://localhost');
-  });
-
-  it('triggers an action when the window is unloaded', () => {
-    const handleInteraction = jest.fn();
-    wrapper = createWrapper({ handleInteraction, url: 'http://example.com' });
-    wrapper.find(NewWindow).simulate('close');
-
-    expect(handleInteraction).toHaveBeenCalledWith('http://example.com');
-  });
-});
diff --git a/__tests__/src/components/IIIFAuthentication.test.js b/__tests__/src/components/IIIFAuthentication.test.js
new file mode 100644
index 0000000000000000000000000000000000000000..cbf6a80230363383c7a3a4d91d54b5a89909018e
--- /dev/null
+++ b/__tests__/src/components/IIIFAuthentication.test.js
@@ -0,0 +1,95 @@
+import React from 'react';
+import { shallow } from 'enzyme';
+import WindowAuthenticationBar from '../../../src/containers/WindowAuthenticationBar';
+import { NewWindow } from '../../../src/components/NewWindow';
+import { AccessTokenSender } from '../../../src/components/AccessTokenSender';
+import { IIIFAuthentication } from '../../../src/components/IIIFAuthentication';
+
+/**
+ * Helper function to create a shallow wrapper around IIIFAuthentication
+ */
+function createWrapper(props) {
+  return shallow(
+    <IIIFAuthentication
+      accessTokenServiceId="http://example.com/token"
+      authServiceId="http://example.com/auth"
+      failureDescription="... and this is why."
+      failureHeader="Login failed"
+      handleAuthInteraction={() => {}}
+      isInteractive
+      logoutServiceId="http://example.com/logout"
+      resetAuthenticationState={() => {}}
+      resolveAccessTokenRequest={() => {}}
+      resolveAuthenticationRequest={() => {}}
+      t={key => key}
+      windowId="w"
+      {...props}
+    />,
+  );
+}
+
+describe('IIIFAuthentication', () => {
+  describe('without an auth service', () => {
+    it('renders nothing', () => {
+      const wrapper = createWrapper({ authServiceId: null });
+      expect(wrapper.isEmptyRender()).toBe(true);
+    });
+  });
+  describe('with an available auth service', () => {
+    it('renders a login bar', () => {
+      const handleAuthInteraction = jest.fn();
+      const wrapper = createWrapper({ handleAuthInteraction });
+      expect(wrapper.find(WindowAuthenticationBar).length).toBe(1);
+      expect(wrapper.find(WindowAuthenticationBar).simulate('confirm'));
+      expect(handleAuthInteraction).toHaveBeenCalledWith('w', 'http://example.com/auth');
+    });
+    it('renders nothing for a non-interactive login', () => {
+      const wrapper = createWrapper({ isInteractive: false });
+      expect(wrapper.isEmptyRender()).toBe(true);
+    });
+  });
+  describe('with a failed authentication', () => {
+    it('renders with an error message', () => {
+      const handleAuthInteraction = jest.fn();
+      const wrapper = createWrapper({ handleAuthInteraction, status: 'failed' });
+      expect(wrapper.find(WindowAuthenticationBar).length).toBe(1);
+      expect(wrapper.find(WindowAuthenticationBar).prop('confirmButton')).toEqual('retry');
+      expect(wrapper.find(WindowAuthenticationBar).prop('status')).toEqual('failed');
+      expect(wrapper.find(WindowAuthenticationBar).prop('header')).toEqual('Login failed');
+      expect(wrapper.find(WindowAuthenticationBar).prop('description')).toEqual('... and this is why.');
+      expect(wrapper.find(WindowAuthenticationBar).simulate('confirm'));
+      expect(handleAuthInteraction).toHaveBeenCalledWith('w', 'http://example.com/auth');
+    });
+  });
+  describe('in the middle of authenicating', () => {
+    it('does the IIIF access cookie behavior', () => {
+      const wrapper = createWrapper({ status: 'cookie' });
+      expect(wrapper.find(WindowAuthenticationBar).length).toBe(1);
+      expect(wrapper.find(NewWindow).length).toBe(1);
+      expect(wrapper.find(NewWindow).prop('url')).toContain('http://example.com/auth?origin=');
+    });
+    it('does the IIIF access token behavior', () => {
+      const wrapper = createWrapper({ status: 'token' });
+      expect(wrapper.find(WindowAuthenticationBar).length).toBe(1);
+      expect(wrapper.find(AccessTokenSender).length).toBe(1);
+      expect(wrapper.find(AccessTokenSender).prop('url')).toEqual('http://example.com/token');
+    });
+  });
+  describe('when logged in', () => {
+    it('renders a logout button', () => {
+      const openWindow = jest.fn();
+      const resetAuthenticationState = jest.fn();
+      const wrapper = createWrapper({ openWindow, resetAuthenticationState, status: 'ok' });
+      expect(wrapper.find(WindowAuthenticationBar).length).toBe(1);
+      expect(wrapper.find(WindowAuthenticationBar).prop('confirmButton')).toEqual('logout');
+      expect(wrapper.find(WindowAuthenticationBar).prop('hasLogoutService')).toEqual(true);
+
+      wrapper.find(WindowAuthenticationBar).simulate('confirm');
+
+      expect(openWindow).toHaveBeenCalledWith('http://example.com/logout', undefined, 'centerscreen');
+      expect(resetAuthenticationState).toHaveBeenCalledWith({
+        authServiceId: 'http://example.com/auth', tokenServiceId: 'http://example.com/token',
+      });
+    });
+  });
+});
diff --git a/__tests__/src/components/Window.test.js b/__tests__/src/components/Window.test.js
index e21220fa6b7efbf35eb33b892a992e702016db7a..c16b361fef230d10765295efaccef0786e4ec971 100644
--- a/__tests__/src/components/Window.test.js
+++ b/__tests__/src/components/Window.test.js
@@ -3,7 +3,7 @@ import { shallow } from 'enzyme';
 import { Window } from '../../../src/components/Window';
 import WindowTopBar from '../../../src/containers/WindowTopBar';
 import PrimaryWindow from '../../../src/containers/PrimaryWindow';
-import WindowAuthenticationControl from '../../../src/containers/WindowAuthenticationControl';
+import IIIFAuthentication from '../../../src/containers/IIIFAuthentication';
 import ErrorContent from '../../../src/containers/ErrorContent';
 
 /** create wrapper */
@@ -34,9 +34,9 @@ describe('Window', () => {
     wrapper = createWrapper();
     expect(wrapper.find(PrimaryWindow)).toHaveLength(1);
   });
-  it('renders <WindowAuthenticationControl>', () => {
+  it('renders <WindowAuthenticationBar>', () => {
     wrapper = createWrapper();
-    expect(wrapper.find(WindowAuthenticationControl)).toHaveLength(1);
+    expect(wrapper.find(IIIFAuthentication)).toHaveLength(1);
   });
   it('renders manifest error', () => {
     wrapper = createWrapper({ manifestError: 'Invalid JSON' });
diff --git a/__tests__/src/components/WindowAuthenticationBar.test.js b/__tests__/src/components/WindowAuthenticationBar.test.js
new file mode 100644
index 0000000000000000000000000000000000000000..72e9b029922cac5e935d21f347103759ec7e3d80
--- /dev/null
+++ b/__tests__/src/components/WindowAuthenticationBar.test.js
@@ -0,0 +1,73 @@
+import React from 'react';
+import { shallow } from 'enzyme';
+import Button from '@material-ui/core/Button';
+import Collapse from '@material-ui/core/Collapse';
+import DialogActions from '@material-ui/core/DialogActions';
+import SanitizedHtml from '../../../src/containers/SanitizedHtml';
+import { WindowAuthenticationBar } from '../../../src/components/WindowAuthenticationBar';
+
+/**
+ * Helper function to create a shallow wrapper around AuthenticationLogout
+ */
+function createWrapper(props) {
+  return shallow(
+    <WindowAuthenticationBar
+      classes={{}}
+      hasLogoutService
+      confirmButton="Click here"
+      label="Log in to see more"
+      onConfirm={() => {}}
+      status="ok"
+      t={key => key}
+      windowId="w"
+      {...props}
+    />,
+  );
+}
+
+describe('AuthenticationControl', () => {
+  it('renders nothing if the user is logged in and there is no logout service', () => {
+    const wrapper = createWrapper({ hasLogoutService: false });
+    expect(wrapper.isEmptyRender()).toBe(true);
+  });
+
+  it('renders a non-collapsing version if there is no description', () => {
+    const wrapper = createWrapper({ description: undefined, header: undefined });
+    expect(wrapper.find(SanitizedHtml).at(0).props().htmlString).toEqual('Log in to see more');
+    expect(wrapper.find(Button).children().text()).toEqual('Click here');
+  });
+
+  it('renders a collapsable version if there is a description', () => {
+    const onConfirm = jest.fn();
+    const wrapper = createWrapper({ description: 'long description', header: 'header', onConfirm });
+    expect(wrapper.find(SanitizedHtml).at(0).props().htmlString).toEqual('Log in to see more');
+    expect(wrapper.find(Button).at(0).find('span').text()).toEqual('continue');
+    // is expandable
+    expect(wrapper.find(Collapse).prop('in')).toEqual(false);
+    wrapper.find(Button).at(0).simulate('click');
+    expect(wrapper.find(Collapse).prop('in')).toEqual(true);
+
+    // has more information
+    expect(wrapper.find(Collapse).find(SanitizedHtml).at(0).props().htmlString).toEqual('header');
+    expect(wrapper.find(Collapse).find(SanitizedHtml).at(1).props().htmlString).toEqual('long description');
+
+    // is recollapsable
+    wrapper.find(DialogActions).find(Button).at(0).simulate('click');
+    expect(wrapper.find(Collapse).prop('in')).toEqual(false);
+    wrapper.find(Button).at(0).simulate('click');
+
+    // starts the auth process
+    wrapper.find(DialogActions).find(Button).at(1).simulate('click');
+    expect(onConfirm).toHaveBeenCalled();
+  });
+
+  it('triggers an action when the confirm button is clicked', () => {
+    const onConfirm = jest.fn();
+    const wrapper = createWrapper({
+      onConfirm,
+    });
+
+    wrapper.find(Button).simulate('click');
+    expect(onConfirm).toHaveBeenCalled();
+  });
+});
diff --git a/__tests__/src/components/WindowAuthenticationControl.test.js b/__tests__/src/components/WindowAuthenticationControl.test.js
deleted file mode 100644
index ffa31741da69f6c345cc8ee1de67ac0ba56e12d3..0000000000000000000000000000000000000000
--- a/__tests__/src/components/WindowAuthenticationControl.test.js
+++ /dev/null
@@ -1,122 +0,0 @@
-import React from 'react';
-import { shallow } from 'enzyme';
-import Button from '@material-ui/core/Button';
-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 WindowAuthenticationControl
- */
-function createWrapper(props) {
-  return shallow(
-    <WindowAuthenticationControl
-      t={key => key}
-      classes={{}}
-      degraded
-      handleAuthInteraction={() => {}}
-      label="authenticate"
-      windowId="w"
-      profile="http://iiif.io/api/auth/1/login"
-      {...props}
-    />,
-  );
-}
-
-describe('WindowAuthenticationControl', () => {
-  let wrapper;
-
-  it('renders AuthenticationLogout if it is not degraded', () => {
-    wrapper = createWrapper({ degraded: false });
-    expect(wrapper.find(AuthenticationLogout).length).toBe(1);
-  });
-
-  describe('with a non-interactive login', () => {
-    it('renders failure messages', () => {
-      wrapper = createWrapper({
-        degraded: true,
-        failureDescription: 'failure description',
-        failureHeader: 'failure header',
-        profile: 'http://iiif.io/api/auth/1/external',
-        status: 'failed',
-      });
-      expect(wrapper.find(SanitizedHtml).at(0).props().htmlString).toEqual('failure header');
-      expect(wrapper.find(SanitizedHtml).at(1).props().htmlString).toEqual('failure description');
-      expect(wrapper.find(DialogActions)).toHaveLength(1);
-      expect(wrapper.find(DialogActions).find(Button)).toHaveLength(2);
-      expect(wrapper.find(DialogActions).find(Button).at(1).children()
-        .text()).toEqual('retry');
-    });
-  });
-
-  it('renders properly', () => {
-    wrapper = createWrapper({ confirmLabel: 'some confirm label', description: 'some description' });
-    expect(wrapper.find(SanitizedHtml).at(2).props().htmlString).toEqual('some description');
-    expect(wrapper.find(DialogActions).find(Button)).toHaveLength(2);
-    expect(wrapper.find(DialogActions).find(Button).at(1).children()
-      .text()).toEqual('some confirm label');
-  });
-
-  it('hides the cancel button if there is nothing to collapose', () => {
-    wrapper = createWrapper({ classes: { topBar: 'topBar' }, confirmLabel: 'some confirm label' });
-    expect(wrapper.find('.topBar').children().find(Button).at(0)
-      .children()
-      .text()).toEqual('some confirm label');
-
-    expect(wrapper.find(DialogActions).find(Button)).toHaveLength(0);
-  });
-
-  it('shows the auth dialog when the login button is clicked', () => {
-    wrapper = createWrapper({ classes: { topBar: 'topBar' }, description: 'some description' });
-    wrapper.find('.topBar').props().onClick();
-    expect(wrapper.find(Collapse).props().in).toEqual(true);
-  });
-
-  it('triggers an action when the confirm button is clicked', () => {
-    const handleAuthInteraction = jest.fn();
-    wrapper = createWrapper({
-      confirmLabel: 'some confirm label',
-      description: 'some description',
-      handleAuthInteraction,
-      infoId: 'i',
-      serviceId: 's',
-    });
-    wrapper.instance().setState({ open: true });
-    expect(wrapper.find(Collapse).props().in).toEqual(true);
-
-    wrapper.find(DialogActions).find(Button).at(1).simulate('click');
-    expect(handleAuthInteraction).toHaveBeenCalledWith('w', 'i', 's');
-  });
-
-  it('displays a failure message if the login has failed', () => {
-    wrapper = createWrapper({
-      failureDescription: 'failure description',
-      failureHeader: 'failure header',
-      status: 'failed',
-    });
-
-    expect(wrapper.find(SanitizedHtml).at(0).props().htmlString).toEqual('failure header');
-    expect(wrapper.find(SanitizedHtml).at(1).props().htmlString).toEqual('failure description');
-
-    expect(wrapper.find(DialogActions).find(Button)).toHaveLength(2);
-    expect(wrapper.find(DialogActions).find(Button).at(1).children()
-      .text()).toEqual('login');
-  });
-
-  it('displays the login messages if the user dismisses the failure messages', () => {
-    wrapper = createWrapper({
-      failureDescription: 'failure description',
-      failureHeader: 'failure header',
-      status: 'failed',
-    });
-
-    expect(wrapper.find(SanitizedHtml).at(0).props().htmlString).toEqual('failure header');
-    expect(wrapper.find(SanitizedHtml).at(1).props().htmlString).toEqual('failure description');
-
-    wrapper.find(DialogActions).find(Button).at(0).simulate('click');
-
-    expect(wrapper.find(SanitizedHtml).at(0).props().htmlString).toEqual('authenticate');
-  });
-});
diff --git a/__tests__/src/reducers/accessTokens.test.js b/__tests__/src/reducers/accessTokens.test.js
index 58211cc86f8eeeb0b587e57153ee767141f49277..020c450f649cf460bc7a896d52b455c60ef4a342 100644
--- a/__tests__/src/reducers/accessTokens.test.js
+++ b/__tests__/src/reducers/accessTokens.test.js
@@ -2,83 +2,15 @@ import { accessTokensReducer } from '../../../src/state/reducers/accessTokens';
 import ActionTypes from '../../../src/state/actions/action-types';
 
 describe('access tokens response reducer', () => {
-  describe('should handle RECEIVE_DEGRADED_INFO_RESPONSE', () => {
-    it('does nothing for a kiosk service', () => {
-      expect(accessTokensReducer({}, {
-        infoId: 'http://example.com',
-        infoJson: {
-          '@id': 'http://example.com',
-          service: {
-            profile: 'http://iiif.io/api/auth/1/kiosk',
-          },
-        },
-        type: ActionTypes.RECEIVE_DEGRADED_INFO_RESPONSE,
-      })).toEqual({});
-    });
-    it('does nothing if a external request for that token service is in flight', () => {
-      expect(accessTokensReducer(
-        {
-          token: {
-            isFetching: true,
-          },
-        },
-        {
-          infoId: 'http://example.com',
-          infoJson: {
-            '@id': 'http://example.com',
-            service: {
-              '@id': 'auth',
-              profile: 'http://iiif.io/api/auth/1/external',
-              service: {
-                '@id': 'token',
-                profile: 'http://iiif.io/api/auth/1/token',
-              },
-            },
-          },
-          type: ActionTypes.RECEIVE_DEGRADED_INFO_RESPONSE,
-        },
-      )).toEqual({ token: { isFetching: true } });
-    });
-    it('adds an entry for an external auth token service', () => {
-      expect(accessTokensReducer(
-        {},
-        {
-          infoId: 'http://example.com',
-          infoJson: {
-            '@id': 'http://example.com',
-            service: {
-              '@id': 'auth',
-              profile: 'http://iiif.io/api/auth/1/external',
-              service: {
-                '@id': 'token',
-                profile: 'http://iiif.io/api/auth/1/token',
-              },
-            },
-          },
-          ok: false,
-          type: ActionTypes.RECEIVE_DEGRADED_INFO_RESPONSE,
-        },
-      )).toEqual({
-        token: {
-          authId: 'auth',
-          id: 'token',
-          infoIds: ['http://example.com'],
-          isFetching: true,
-        },
-      });
-    });
-  });
   it('should handle REQUEST_ACCESS_TOKEN', () => {
     expect(accessTokensReducer({}, {
       authId: 'auth123',
-      infoIds: [1, 2, 3],
       serviceId: 'abc123',
       type: ActionTypes.REQUEST_ACCESS_TOKEN,
     })).toEqual({
       abc123: {
         authId: 'auth123',
         id: 'abc123',
-        infoIds: [1, 2, 3],
         isFetching: true,
       },
     });
diff --git a/__tests__/src/reducers/auth.test.js b/__tests__/src/reducers/auth.test.js
index 6f3438b4bba157ee521cebe399574c9d8272c0a8..9817c15c160d95b62f481d45c555027f0d919007 100644
--- a/__tests__/src/reducers/auth.test.js
+++ b/__tests__/src/reducers/auth.test.js
@@ -2,74 +2,18 @@ import { authReducer } from '../../../src/state/reducers/auth';
 import ActionTypes from '../../../src/state/actions/action-types';
 
 describe('auth response reducer', () => {
-  describe('should handle RECEIVE_DEGRADED_INFO_RESPONSE', () => {
-    it('does nothing for a login service', () => {
-      expect(authReducer({}, {
-        infoId: 'http://example.com',
-        infoJson: {
-          '@id': 'http://example.com',
-          service: {
-            profile: 'http://iiif.io/api/auth/1/login',
-          },
-        },
-        type: ActionTypes.RECEIVE_DEGRADED_INFO_RESPONSE,
-      })).toEqual({});
-    });
-    it('does nothing if a kiosk/external request for that service is in flight', () => {
-      expect(authReducer(
-        {
-          auth: {
-            isFetching: true,
-          },
-        },
-        {
-          infoId: 'http://example.com',
-          infoJson: {
-            '@id': 'http://example.com',
-            service: {
-              '@id': 'auth',
-              profile: 'http://iiif.io/api/auth/1/kiosk',
-            },
-          },
-          type: ActionTypes.RECEIVE_DEGRADED_INFO_RESPONSE,
-        },
-      )).toEqual({ auth: { isFetching: true } });
-    });
-    it('adds an entry for a kiosk/external service', () => {
-      expect(authReducer(
-        {},
-        {
-          infoId: 'http://example.com',
-          infoJson: {
-            '@id': 'http://example.com',
-            service: {
-              '@id': 'auth',
-              profile: 'http://iiif.io/api/auth/1/kiosk',
-            },
-          },
-          ok: false,
-          type: ActionTypes.RECEIVE_DEGRADED_INFO_RESPONSE,
-        },
-      )).toEqual({
-        auth: {
-          id: 'auth',
-          infoId: ['http://example.com'],
-          isFetching: true,
-          profile: 'http://iiif.io/api/auth/1/kiosk',
-        },
-      });
-    });
-  });
   it('should handle ADD_AUTHENTICATION_REQUEST', () => {
     expect(authReducer({}, {
       id: 'abc123',
-      infoId: 1,
+      profile: 'iiif/login',
       type: ActionTypes.ADD_AUTHENTICATION_REQUEST,
+      windowId: 'main',
     })).toEqual({
       abc123: {
         id: 'abc123',
-        infoId: [1],
         isFetching: true,
+        profile: 'iiif/login',
+        windowId: 'main',
       },
     });
   });
@@ -94,6 +38,14 @@ describe('auth response reducer', () => {
       },
     });
   });
+  describe('should handle RECEIVE_ACCESS_TOKEN', () => {
+    it('does nothing if id is not present', () => {
+      expect(authReducer({ foo: {} }, {
+        authId: 'foo',
+        type: ActionTypes.RECEIVE_ACCESS_TOKEN,
+      })).toMatchObject({ foo: { ok: true } });
+    });
+  });
   describe('should handle RESET_AUTHENTICATION_STATE', () => {
     it('does nothing if id is not present', () => {
       expect(authReducer({}, {
diff --git a/__tests__/src/reducers/infoResponse.test.js b/__tests__/src/reducers/infoResponse.test.js
index c340e72599e8f554bcf3b1aa21381be6b69ecbfc..73237ebac25302f43afd307ba3e3db85b95f26d4 100644
--- a/__tests__/src/reducers/infoResponse.test.js
+++ b/__tests__/src/reducers/infoResponse.test.js
@@ -92,27 +92,4 @@ describe('info response reducer', () => {
       type: ActionTypes.IMPORT_MIRADOR_STATE,
     })).toEqual({});
   });
-  it('should handle RESET_AUTHENTICATION_STATE', () => {
-    expect(infoResponsesReducer(
-      {
-        abc123: {
-          stuff: 'foo',
-          tokenServiceId: 'abc123',
-        },
-        def456: {
-          stuff: 'foo',
-          tokenServiceId: 'def456',
-        },
-      },
-      {
-        tokenServiceId: 'abc123',
-        type: ActionTypes.RESET_AUTHENTICATION_STATE,
-      },
-    )).toEqual({
-      def456: {
-        stuff: 'foo',
-        tokenServiceId: 'def456',
-      },
-    });
-  });
 });
diff --git a/__tests__/src/sagas/auth.test.js b/__tests__/src/sagas/auth.test.js
new file mode 100644
index 0000000000000000000000000000000000000000..229e9956e3a166de4ab843e322029fb1a05b4e12
--- /dev/null
+++ b/__tests__/src/sagas/auth.test.js
@@ -0,0 +1,391 @@
+import { call, select } from 'redux-saga/effects';
+import { expectSaga } from 'redux-saga-test-plan';
+import { Utils } from 'manifesto.js/dist-esmodule/Utils';
+import serviceFixture from '../../fixtures/version-2/canvasService.json';
+import settings from '../../../src/config/settings';
+import ActionTypes from '../../../src/state/actions/action-types';
+import {
+  refetchInfoResponses,
+  refetchInfoResponsesOnLogout,
+  doAuthWorkflow,
+  rerequestOnAccessTokenFailure,
+  invalidateInvalidAuth,
+} from '../../../src/state/sagas/auth';
+import {
+  fetchInfoResponse,
+} from '../../../src/state/sagas/iiif';
+import {
+  getAccessTokens,
+  getWindows,
+  selectInfoResponses,
+  getVisibleCanvases,
+  getAuth,
+  getConfig,
+} from '../../../src/state/selectors';
+
+describe('IIIF Authentication sagas', () => {
+  describe('refetchInfoResponsesOnLogout', () => {
+    it('delays and then refetches info responses', () => {
+      const tokenServiceId = 'whatever';
+      /** stub out delay... ugh. */
+      const provideDelay = ({ fn }, next) => ((fn.name === 'delayP') ? null : next());
+
+      return expectSaga(refetchInfoResponsesOnLogout, { tokenServiceId })
+        .provide([
+          { call: provideDelay },
+          [call(refetchInfoResponses, { serviceId: tokenServiceId }), {}],
+        ])
+        .call(refetchInfoResponses, { serviceId: tokenServiceId })
+        .run();
+    });
+  });
+  describe('refetchInfoResponses', () => {
+    it('discards info responses that could hvae used the new access token', () => {
+      const serviceId = 'https://authentication.example.org/token';
+      const tokenService = { id: serviceId };
+
+      const authStanza = {
+        service: [{
+          '@context': 'http://iiif.io/api/auth/1/context.json',
+          '@id': 'https://authentication.example.org/login',
+          profile: 'http://iiif.io/api/auth/1/login',
+          service: [
+            {
+              '@id': serviceId,
+              profile: 'http://iiif.io/api/auth/1/token',
+            },
+          ],
+        }],
+      };
+
+      const x = {
+        id: 'x',
+        json: {
+          ...authStanza,
+        },
+      };
+
+      const y = {
+        id: 'y',
+        json: {
+          ...authStanza,
+        },
+      };
+
+      return expectSaga(refetchInfoResponses, { serviceId })
+        .provide([
+          [select(getAccessTokens), { [serviceId]: tokenService }],
+          [select(getWindows), {}],
+          [select(selectInfoResponses), { x, y }],
+        ])
+        .put({ infoId: 'x', type: ActionTypes.REMOVE_INFO_RESPONSE })
+        .put({ infoId: 'y', type: ActionTypes.REMOVE_INFO_RESPONSE })
+        .run();
+    });
+
+    it('ignores info responses that would not use the token', () => {
+      const serviceId = 'https://authentication.example.org/token';
+      const tokenService = { id: serviceId };
+
+      const authStanza = {
+        service: [{
+          '@context': 'http://iiif.io/api/auth/1/context.json',
+          '@id': 'https://authentication.example.org/login',
+          profile: 'http://iiif.io/api/auth/1/login',
+          service: [
+            {
+              '@id': 'https://authentication.example.org/some-other-token-service',
+              profile: 'http://iiif.io/api/auth/1/token',
+            },
+          ],
+        }],
+      };
+
+      const wrongService = {
+        id: 'wrongService',
+        json: {
+          ...authStanza,
+        },
+      };
+
+      const noAuth = {
+        id: 'noAuth',
+        json: {},
+      };
+
+      const noJson = {
+        id: 'noJson',
+      };
+
+      return expectSaga(refetchInfoResponses, { serviceId })
+        .provide([
+          [select(getAccessTokens), { [serviceId]: tokenService }],
+          [select(getWindows), {}],
+          [select(selectInfoResponses), { noAuth, noJson, wrongService }],
+        ])
+        .not.put({ infoId: 'noAuth', type: ActionTypes.REMOVE_INFO_RESPONSE })
+        .not.put({ infoId: 'noJson', type: ActionTypes.REMOVE_INFO_RESPONSE })
+        .not.put({ infoId: 'wrongService', type: ActionTypes.REMOVE_INFO_RESPONSE })
+        .run();
+    });
+
+    it('re-requests info responses for visible canvases', () => {
+      const serviceId = 'https://authentication.example.org/token';
+      const tokenService = { id: serviceId };
+
+      const authStanza = {
+        service: [{
+          '@context': 'http://iiif.io/api/auth/1/context.json',
+          '@id': 'https://authentication.example.org/login',
+          profile: 'http://iiif.io/api/auth/1/login',
+          service: [
+            {
+              '@id': serviceId,
+              profile: 'http://iiif.io/api/auth/1/token',
+            },
+          ],
+        }],
+      };
+
+      const window = {};
+      const canvases = [
+        Utils.parseManifest(serviceFixture).getSequences()[0].getCanvases()[0],
+      ];
+
+      const iiifInfoId = 'https://api.digitale-sammlungen.de/iiif/image/v2/bsb00122140_00001';
+      const infoResponse = {
+        id: iiifInfoId,
+        json: {
+          ...authStanza,
+        },
+      };
+
+      return expectSaga(refetchInfoResponses, { serviceId })
+        .provide([
+          [select(getAccessTokens), { [serviceId]: tokenService }],
+          [select(getWindows), { window }],
+          [select(getVisibleCanvases, { windowId: 'window' }), canvases],
+          [select(selectInfoResponses), { [iiifInfoId]: infoResponse }],
+          [call(fetchInfoResponse, { infoId: iiifInfoId }), {}],
+        ])
+        .call(fetchInfoResponse, { infoId: iiifInfoId })
+        .run();
+    });
+  });
+  describe('doAuthWorkflow', () => {
+    it('kicks off the first external auth from the info.json', () => {
+      const infoJson = {
+        service: [{
+          '@context': 'http://iiif.io/api/auth/1/context.json',
+          '@id': 'https://authentication.example.com/external',
+          profile: 'http://iiif.io/api/auth/1/external',
+          service: [
+            {
+              '@id': 'https://authentication.example.com/token',
+              profile: 'http://iiif.io/api/auth/1/token',
+            },
+          ],
+        }],
+      };
+      const windowId = 'window';
+      return expectSaga(doAuthWorkflow, { infoJson, windowId })
+        .provide([
+          [select(getAuth), {}],
+          [select(getConfig), { auth: settings.auth }],
+        ])
+        .put({
+          id: 'https://authentication.example.com/external',
+          tokenServiceId: 'https://authentication.example.com/token',
+          type: ActionTypes.RESOLVE_AUTHENTICATION_REQUEST,
+        })
+        .put({
+          authId: 'https://authentication.example.com/external',
+          serviceId: 'https://authentication.example.com/token',
+          type: ActionTypes.REQUEST_ACCESS_TOKEN,
+        })
+        .run();
+    });
+
+    it('does nothing if the auth service has been tried already', () => {
+      const infoJson = {
+        service: [{
+          '@context': 'http://iiif.io/api/auth/1/context.json',
+          '@id': 'https://authentication.example.com/external',
+          profile: 'http://iiif.io/api/auth/1/external',
+          service: [
+            {
+              '@id': 'https://authentication.example.com/token',
+              profile: 'http://iiif.io/api/auth/1/token',
+            },
+          ],
+        }],
+      };
+      const windowId = 'window';
+      return expectSaga(doAuthWorkflow, { infoJson, windowId })
+        .provide([
+          [select(getAuth), { 'https://authentication.example.com/external': { ok: false } }],
+          [select(getConfig), { auth: settings.auth }],
+        ])
+        .not.put.like({ type: ActionTypes.RESOLVE_AUTHENTICATION_REQUEST })
+        .not.put.like({ type: ActionTypes.REQUEST_ACCESS_TOKEN })
+        .run();
+    });
+
+    it('does nothing if the auth service is "interactive"', () => {
+      const infoJson = {
+        service: [{
+          '@context': 'http://iiif.io/api/auth/1/context.json',
+          '@id': 'https://authentication.example.com/login',
+          profile: 'http://iiif.io/api/auth/1/login',
+          service: [
+            {
+              '@id': 'https://authentication.example.com/token',
+              profile: 'http://iiif.io/api/auth/1/token',
+            },
+          ],
+        }],
+      };
+      const windowId = 'window';
+      return expectSaga(doAuthWorkflow, { infoJson, windowId })
+        .provide([
+          [select(getAuth), {}],
+          [select(getConfig), { auth: settings.auth }],
+        ])
+        .not.put.like({ type: ActionTypes.RESOLVE_AUTHENTICATION_REQUEST })
+        .not.put.like({ type: ActionTypes.REQUEST_ACCESS_TOKEN })
+        .run();
+    });
+
+    it('kicks off the kiosk auth from the info.json', () => {
+      const infoJson = {
+        service: [{
+          '@context': 'http://iiif.io/api/auth/1/context.json',
+          '@id': 'https://authentication.example.com/kiosk',
+          profile: 'http://iiif.io/api/auth/1/kiosk',
+          service: [
+            {
+              '@id': 'https://authentication.example.com/token',
+              profile: 'http://iiif.io/api/auth/1/token',
+            },
+          ],
+        }],
+      };
+      const windowId = 'window';
+      return expectSaga(doAuthWorkflow, { infoJson, windowId })
+        .provide([
+          [select(getAuth), {}],
+          [select(getConfig), { auth: settings.auth }],
+        ])
+        .put({
+          id: 'https://authentication.example.com/kiosk',
+          profile: 'http://iiif.io/api/auth/1/kiosk',
+          type: ActionTypes.ADD_AUTHENTICATION_REQUEST,
+          windowId,
+        })
+        .run();
+    });
+  });
+
+  describe('rerequestOnAccessTokenFailure', () => {
+    it('does nothing if no access token was used', () => {
+      const infoJson = {};
+      const windowId = 'window';
+      const tokenServiceId = undefined;
+      return expectSaga(rerequestOnAccessTokenFailure, { infoJson, tokenServiceId, windowId })
+        .provide([
+          [select(getAccessTokens), {}],
+        ])
+        .not.put.like({ type: ActionTypes.REQUEST_ACCESS_TOKEN })
+        .run();
+    });
+
+    it('does nothing if the access token has never worked', () => {
+      const infoJson = {
+        service: [{
+          '@context': 'http://iiif.io/api/auth/1/context.json',
+          '@id': 'https://authentication.example.com/kiosk',
+          profile: 'http://iiif.io/api/auth/1/kiosk',
+          service: [
+            {
+              '@id': 'https://authentication.example.com/token',
+              profile: 'http://iiif.io/api/auth/1/token',
+            },
+          ],
+        }],
+      };
+      const windowId = 'window';
+      const tokenServiceId = 'https://authentication.example.com/token';
+      return expectSaga(rerequestOnAccessTokenFailure, { infoJson, tokenServiceId, windowId })
+        .provide([
+          [select(getAccessTokens), { [tokenServiceId]: { success: false } }],
+        ])
+        .not.put.like({ type: ActionTypes.REQUEST_ACCESS_TOKEN })
+        .run();
+    });
+
+    it('re-requests the access token if it might be reneweable', () => {
+      const infoJson = {
+        service: [{
+          '@context': 'http://iiif.io/api/auth/1/context.json',
+          '@id': 'https://authentication.example.com/kiosk',
+          profile: 'http://iiif.io/api/auth/1/kiosk',
+          service: [
+            {
+              '@id': 'https://authentication.example.com/token',
+              profile: 'http://iiif.io/api/auth/1/token',
+            },
+          ],
+        }],
+      };
+      const windowId = 'window';
+      const tokenServiceId = 'https://authentication.example.com/token';
+      return expectSaga(rerequestOnAccessTokenFailure, { infoJson, tokenServiceId, windowId })
+        .provide([
+          [select(getAccessTokens), { [tokenServiceId]: { success: true } }],
+        ])
+        .put({
+          authId: 'https://authentication.example.com/kiosk',
+          serviceId: 'https://authentication.example.com/token',
+          type: ActionTypes.REQUEST_ACCESS_TOKEN,
+        })
+        .run();
+    });
+  });
+
+  describe('invalidateInvalidAuth', () => {
+    it('resets the auth service if the auth cookie might have expired', () => {
+      const authId = 'authId';
+      const serviceId = 'serviceId';
+
+      return expectSaga(invalidateInvalidAuth, { serviceId })
+        .provide([
+          [select(getAccessTokens), { [serviceId]: { authId, id: serviceId, success: true } }],
+          [select(getAuth), { [authId]: { id: authId } }],
+        ])
+        .put({
+          id: authId,
+          tokenServiceId: serviceId,
+          type: ActionTypes.RESET_AUTHENTICATION_STATE,
+        })
+        .run();
+    });
+
+    it('marks the auth service as failed if the auth token was not successfully used', () => {
+      const authId = 'authId';
+      const serviceId = 'serviceId';
+
+      return expectSaga(invalidateInvalidAuth, { serviceId })
+        .provide([
+          [select(getAccessTokens), { [serviceId]: { authId, id: serviceId } }],
+          [select(getAuth), { [authId]: { id: authId } }],
+        ])
+        .put({
+          id: authId,
+          ok: false,
+          tokenServiceId: serviceId,
+          type: ActionTypes.RESOLVE_AUTHENTICATION_REQUEST,
+        })
+        .run();
+    });
+  });
+});
diff --git a/__tests__/src/sagas/iiif.test.js b/__tests__/src/sagas/iiif.test.js
index c1dea0d21865d0a0f725bded7a001dc63045557d..2172dd663b97a4b032cd67070c1c2ace918e6c1f 100644
--- a/__tests__/src/sagas/iiif.test.js
+++ b/__tests__/src/sagas/iiif.test.js
@@ -1,11 +1,10 @@
-import { call, select } from 'redux-saga/effects';
-import { expectSaga, testSaga } from 'redux-saga-test-plan';
+import { select } from 'redux-saga/effects';
+import { expectSaga } from 'redux-saga-test-plan';
 import {
   fetchAnnotation,
   fetchManifest,
   fetchSearchResponse,
   fetchInfoResponse,
-  refetchInfoResponses,
   fetchResourceManifest,
 } from '../../../src/state/sagas/iiif';
 import {
@@ -167,6 +166,7 @@ describe('IIIF sagas', () => {
       const action = {
         imageResource: {},
         infoId: 'infoId',
+        windowId: 'window',
       };
 
       return expectSaga(fetchInfoResponse, action)
@@ -176,6 +176,7 @@ describe('IIIF sagas', () => {
           ok: true,
           tokenServiceId: undefined,
           type: 'mirador/RECEIVE_DEGRADED_INFO_RESPONSE',
+          windowId: 'window',
         })
         .run();
     });
@@ -217,24 +218,6 @@ describe('IIIF sagas', () => {
     });
   });
 
-  describe('refetchInfoResponses', () => {
-    it('refetches info responses when a new access token is available', () => {
-      const serviceId = 'serviceId';
-      const tokenService = { id: serviceId, infoIds: ['x', 'y'] };
-
-      testSaga(refetchInfoResponses, { serviceId })
-        .next()
-        .select(getAccessTokens)
-        .next({ serviceId: tokenService })
-        .all([
-          call(fetchInfoResponse, { infoId: 'x', tokenService }),
-          call(fetchInfoResponse, { infoId: 'y', tokenService }),
-        ])
-        .next()
-        .put({ serviceId, type: 'mirador/CLEAR_ACCESS_TOKEN_QUEUE' });
-    });
-  });
-
   describe('fetchSearchResponse', () => {
     it('fetches a IIIF search', () => {
       fetch.mockResponseOnce(JSON.stringify({ data: '12345' }));
diff --git a/__tests__/src/selectors/auth.test.js b/__tests__/src/selectors/auth.test.js
index 698b32115d1c2f8986e10b482660c19dcef7624f..2071e993177a68d8b536f9aade9d58971914c2d3 100644
--- a/__tests__/src/selectors/auth.test.js
+++ b/__tests__/src/selectors/auth.test.js
@@ -1,5 +1,9 @@
+import manifestFixture001 from '../../fixtures/version-2/001.json';
+import manifestFixture019 from '../../fixtures/version-2/019.json';
+import settings from '../../../src/config/settings';
 import {
   getAccessTokens,
+  selectCurrentAuthServices,
 } from '../../../src/state/selectors/auth';
 
 describe('getAccessTokens', () => {
@@ -16,3 +20,178 @@ describe('getAccessTokens', () => {
     expect(accessTokens).toEqual(state.accessTokens);
   });
 });
+
+describe('selectCurrentAuthServices', () => {
+  const resource = {
+    service: [
+      {
+        '@id': 'external',
+        profile: 'http://iiif.io/api/auth/1/external',
+      },
+      {
+        '@id': 'login',
+        profile: 'http://iiif.io/api/auth/1/login',
+      },
+    ],
+  };
+  const externalOnly = {
+    service: [
+      {
+        '@id': 'external',
+        profile: 'http://iiif.io/api/auth/1/external',
+      },
+    ],
+  };
+
+  const state = {
+    auth: {},
+    config: { auth: settings.auth },
+    infoResponses: {
+      'https://iiif.bodleian.ox.ac.uk/iiif/image/9cca8fdd-4a61-4429-8ac1-f648764b4d6d': {
+        json: resource,
+      },
+      'https://stacks.stanford.edu/image/iiif/hg676jb4964%2F0380_796-44': {
+        json: externalOnly,
+      },
+    },
+    manifests: {
+      a: {
+        json: manifestFixture001,
+      },
+      b: {
+        json: manifestFixture019,
+      },
+    },
+    windows: {
+      noCanvas: {
+        manifestId: 'a',
+      },
+      w: {
+        manifestId: 'a',
+        visibleCanvases: [
+          'https://iiif.bodleian.ox.ac.uk/iiif/canvas/9cca8fdd-4a61-4429-8ac1-f648764b4d6d.json',
+        ],
+      },
+      x: {
+        manifestId: 'b',
+        visibleCanvases: [
+          'http://iiif.io/api/presentation/2.0/example/fixtures/canvas/24/c1.json',
+        ],
+      },
+      y: {
+        manifestId: 'b',
+        visibleCanvases: [
+          'https://purl.stanford.edu/fr426cg9537/iiif/canvas/fr426cg9537_1',
+        ],
+      },
+    },
+  };
+
+  it('returns undefined if there is no current canvas', () => {
+    expect(selectCurrentAuthServices({ config: { auth: settings.auth }, manifests: {} }, { windowId: 'noCanvas' })[0]).toBeUndefined();
+  });
+
+  it('returns the next auth service to try', () => {
+    expect(selectCurrentAuthServices(state, { windowId: 'w' })[0].id).toEqual('external');
+  });
+
+  it('returns the service if the next auth service is interactive', () => {
+    const auth = { external: { isFetching: false, ok: false } };
+    expect(selectCurrentAuthServices({ ...state, auth }, { windowId: 'w' })[0].id).toEqual('login');
+  });
+
+  it('returns the last attempted auth service if all of them have been tried', () => {
+    const auth = {
+      external: { isFetching: false, ok: false },
+      login: { isFetching: false, ok: false },
+    };
+    expect(selectCurrentAuthServices({ ...state, auth }, { windowId: 'w' })[0].id).toEqual('login');
+    expect(selectCurrentAuthServices({ ...state, auth }, { windowId: 'x' })[0].id).toEqual('external');
+    expect(selectCurrentAuthServices({ ...state, auth }, { windowId: 'y' })[0]).toBeUndefined();
+  });
+
+  describe('proscribed order', () => {
+    let auth = {};
+    const orderedState = {
+      config: { auth: settings.auth },
+      infoResponses: {
+        'https://iiif.bodleian.ox.ac.uk/iiif/image/9cca8fdd-4a61-4429-8ac1-f648764b4d6d': {
+          json: {
+            service: [
+              {
+                '@id': 'external',
+                profile: 'http://iiif.io/api/auth/1/external',
+              },
+              {
+                '@id': 'kiosk',
+                profile: 'http://iiif.io/api/auth/1/kiosk',
+              },
+              {
+                '@id': 'clickthrough',
+                profile: 'http://iiif.io/api/auth/1/clickthrough',
+              },
+              {
+                '@id': 'login',
+                profile: 'http://iiif.io/api/auth/1/login',
+              },
+              {
+                '@id': 'login2',
+                profile: 'http://iiif.io/api/auth/1/login',
+              },
+            ],
+          },
+        },
+      },
+      manifests: {
+        a: {
+          json: manifestFixture001,
+        },
+      },
+      windows: {
+        w: {
+          manifestId: 'a',
+          visibleCanvases: [
+            'https://iiif.bodleian.ox.ac.uk/iiif/canvas/9cca8fdd-4a61-4429-8ac1-f648764b4d6d.json',
+          ],
+        },
+      },
+    };
+
+    it('returns external first', () => {
+      auth = {};
+      expect(selectCurrentAuthServices({ ...orderedState, auth }, { windowId: 'w' })[0].id).toEqual('external');
+    });
+
+    it('returns kiosk next', () => {
+      auth = { external: { isFetching: false, ok: false } };
+      expect(selectCurrentAuthServices({ ...orderedState, auth }, { windowId: 'w' })[0].id).toEqual('kiosk');
+    });
+
+    it('returns clickthrough next', () => {
+      auth = {
+        external: { isFetching: false, ok: false },
+        kiosk: { isFetching: false, ok: false },
+      };
+      expect(selectCurrentAuthServices({ ...orderedState, auth }, { windowId: 'w' })[0].id).toEqual('clickthrough');
+    });
+
+    it('returns logins last', () => {
+      auth = {
+        clickthrough: { isFetching: false, ok: false },
+        external: { isFetching: false, ok: false },
+        kiosk: { isFetching: false, ok: false },
+      };
+      expect(selectCurrentAuthServices({ ...orderedState, auth }, { windowId: 'w' })[0].id).toEqual('login');
+    });
+
+    it('returns services within a given type using the order from the manifest', () => {
+      auth = {
+        clickthrough: { isFetching: false, ok: false },
+        external: { isFetching: false, ok: false },
+        kiosk: { isFetching: false, ok: false },
+        login: { isFetching: false, ok: false },
+      };
+      expect(selectCurrentAuthServices({ ...orderedState, auth }, { windowId: 'w' })[0].id).toEqual('login2');
+    });
+  });
+});
diff --git a/__tests__/src/selectors/canvases.test.js b/__tests__/src/selectors/canvases.test.js
index 50cd4530b77f625412a91b93c766aedddda7ca63..c6dcf8145bbf416670ddda930b6dcffe04cd8a93 100644
--- a/__tests__/src/selectors/canvases.test.js
+++ b/__tests__/src/selectors/canvases.test.js
@@ -2,6 +2,7 @@ import manifestFixture001 from '../../fixtures/version-2/001.json';
 import manifestFixture019 from '../../fixtures/version-2/019.json';
 import minimumRequired from '../../fixtures/version-2/minimumRequired.json';
 import minimumRequired3 from '../../fixtures/version-3/minimumRequired.json';
+import settings from '../../../src/config/settings';
 
 import {
   getVisibleCanvases,
@@ -9,11 +10,8 @@ import {
   getPreviousCanvasGrouping,
   getCanvas,
   getCanvasLabel,
-  selectCanvasAuthService,
-  selectNextAuthService,
   selectInfoResponse,
   getVisibleCanvasNonTiledResources,
-  selectLogoutAuthService,
   getVisibleCanvasIds,
 } from '../../../src/state/selectors/canvases';
 
@@ -251,185 +249,13 @@ describe('getCanvasLabel', () => {
   });
 });
 
-describe('selectNextAuthService', () => {
-  const auth = {};
-  const resource = {
-    service: [
-      {
-        '@id': 'external',
-        profile: 'http://iiif.io/api/auth/1/external',
-      },
-      {
-        '@id': 'kiosk',
-        profile: 'http://iiif.io/api/auth/1/kiosk',
-      },
-      {
-        '@id': 'clickthrough',
-        profile: 'http://iiif.io/api/auth/1/clickthrough',
-      },
-      {
-        '@id': 'login',
-        profile: 'http://iiif.io/api/auth/1/login',
-      },
-      {
-        '@id': 'login2',
-        profile: 'http://iiif.io/api/auth/1/login',
-      },
-    ],
-  };
-
-  const noAuthResource = {};
-
-  it('returns external first', () => {
-    expect(selectNextAuthService({ auth }, resource).id).toEqual('external');
-  });
-
-  it('returns kiosk next', () => {
-    auth.external = { isFetching: false, ok: false };
-    expect(selectNextAuthService({ auth }, resource).id).toEqual('kiosk');
-  });
-
-  it('returns clickthrough next', () => {
-    auth.external = { isFetching: false, ok: false };
-    auth.kiosk = { isFetching: false, ok: false };
-    expect(selectNextAuthService({ auth }, resource).id).toEqual('clickthrough');
-  });
-
-  it('returns logins last', () => {
-    auth.external = { isFetching: false, ok: false };
-    auth.kiosk = { isFetching: false, ok: false };
-    auth.clickthrough = { isFetching: false, ok: false };
-    expect(selectNextAuthService({ auth }, resource).id).toEqual('login');
-    auth.login = { isFetching: false, ok: false };
-    expect(selectNextAuthService({ auth }, resource).id).toEqual('login2');
-  });
-
-  it('returns null if there are no services', () => {
-    expect(selectNextAuthService({ auth }, noAuthResource)).toBeNull();
-  });
-
-  it('returns null if a service is currently in-flight', () => {
-    auth.external = { isFetching: true };
-    expect(selectNextAuthService({ auth }, resource)).toBeNull();
-  });
-});
-
-describe('selectCanvasAuthService', () => {
-  const resource = {
-    service: [
-      {
-        '@id': 'external',
-        profile: 'http://iiif.io/api/auth/1/external',
-      },
-      {
-        '@id': 'login',
-        profile: 'http://iiif.io/api/auth/1/login',
-      },
-    ],
-  };
-  const externalOnly = {
-    service: [
-      {
-        '@id': 'external',
-        profile: 'http://iiif.io/api/auth/1/external',
-      },
-    ],
-  };
-
-  const state = {
-    auth: {},
-    infoResponses: {
-      'https://iiif.bodleian.ox.ac.uk/iiif/image/9cca8fdd-4a61-4429-8ac1-f648764b4d6d': {
-        json: resource,
-      },
-      'https://stacks.stanford.edu/image/iiif/hg676jb4964%2F0380_796-44': {
-        json: externalOnly,
-      },
-    },
-    manifests: {
-      a: {
-        json: manifestFixture001,
-      },
-      b: {
-        json: manifestFixture019,
-      },
-    },
-  };
-
-  it('returns undefined if there is no current canvas', () => {
-    expect(selectCanvasAuthService({ manifests: {} }, { manifestId: 'a' })).toBeUndefined();
-  });
-
-  it('returns the next auth service to try', () => {
-    expect(selectCanvasAuthService(state, { canvasId: 'https://iiif.bodleian.ox.ac.uk/iiif/canvas/9cca8fdd-4a61-4429-8ac1-f648764b4d6d.json', manifestId: 'a' }).id).toEqual('external');
-  });
-
-  it('returns the service if the next auth service is interactive', () => {
-    const auth = { external: { isFetching: false, ok: false } };
-    expect(selectCanvasAuthService({ ...state, auth }, { canvasId: 'https://iiif.bodleian.ox.ac.uk/iiif/canvas/9cca8fdd-4a61-4429-8ac1-f648764b4d6d.json', manifestId: 'a' }).id).toEqual('login');
-  });
-
-  it('returns the last attempted auth service if all of them have been tried', () => {
-    const auth = {
-      external: { isFetching: false, ok: false },
-      login: { isFetching: false, ok: false },
-    };
-    expect(selectCanvasAuthService({ ...state, auth }, { canvasId: 'https://iiif.bodleian.ox.ac.uk/iiif/canvas/9cca8fdd-4a61-4429-8ac1-f648764b4d6d.json', manifestId: 'a' }).id).toEqual('login');
-    expect(selectCanvasAuthService({ ...state, auth }, { canvasId: 'http://iiif.io/api/presentation/2.0/example/fixtures/canvas/24/c1.json', manifestId: 'b' }).id).toEqual('external');
-    expect(selectCanvasAuthService({ ...state, auth }, { canvasId: 'https://purl.stanford.edu/fr426cg9537/iiif/canvas/fr426cg9537_1', manifestId: 'b' })).toBeUndefined();
-  });
-});
-
-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' };
 
     const state = {
       auth: {},
+      config: { auth: settings.auth },
       infoResponses: {
         'https://iiif.bodleian.ox.ac.uk/iiif/image/9cca8fdd-4a61-4429-8ac1-f648764b4d6d': {
           json: resource,
diff --git a/src/components/AccessTokenSender.js b/src/components/AccessTokenSender.js
index ef2a58f73f206379668399ae1af266e1b1898df4..2760b012f1a06e992e9d802da45e47c6ce2c779e 100644
--- a/src/components/AccessTokenSender.js
+++ b/src/components/AccessTokenSender.js
@@ -30,7 +30,13 @@ export class AccessTokenSender extends Component {
     */
     return (
       <IComCom
-        attributes={{ src: `${url}?origin=${window.origin}&messageId=${url}` }}
+        attributes={{
+          'aria-hidden': true,
+          height: 1,
+          src: `${url}?origin=${window.origin}&messageId=${url}`,
+          style: { visibility: 'hidden' },
+          width: 1,
+        }}
         handleReceiveMessage={this.onReceiveAccessTokenMessage}
       />
     );
diff --git a/src/components/App.js b/src/components/App.js
index a415d6ff823e2b900aa84345f993ccf874858530..ed42b7aabf312e6b8df56865f8bb23311b8d06c8 100644
--- a/src/components/App.js
+++ b/src/components/App.js
@@ -2,8 +2,6 @@ import React, { Component, lazy, Suspense } from 'react';
 import PropTypes from 'prop-types';
 import PluginProvider from '../extend/PluginProvider';
 import AppProviders from '../containers/AppProviders';
-import AuthenticationSender from '../containers/AuthenticationSender';
-import AccessTokenSender from '../containers/AccessTokenSender';
 
 const WorkspaceArea = lazy(() => import('../containers/WorkspaceArea'));
 
@@ -22,8 +20,6 @@ export class App extends Component {
     return (
       <PluginProvider plugins={plugins}>
         <AppProviders dndManager={dndManager}>
-          <AuthenticationSender />
-          <AccessTokenSender />
           <Suspense
             fallback={<div />}
           >
diff --git a/src/components/AuthenticationLogout.js b/src/components/AuthenticationLogout.js
deleted file mode 100644
index 82c37de15f562c0053fa00a740d31ada12b1bfbb..0000000000000000000000000000000000000000
--- a/src/components/AuthenticationLogout.js
+++ /dev/null
@@ -1,57 +0,0 @@
-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/AuthenticationSender.js b/src/components/AuthenticationSender.js
deleted file mode 100644
index e277838069e5f7a88ed2d34f0609a4edec046962..0000000000000000000000000000000000000000
--- a/src/components/AuthenticationSender.js
+++ /dev/null
@@ -1,40 +0,0 @@
-import React, { Component } from 'react';
-import PropTypes from 'prop-types';
-import { NewWindow } from './NewWindow';
-
-/**
- * Opens a new window for click
- */
-export class AuthenticationSender extends Component {
-  /** */
-  constructor(props) {
-    super(props);
-
-    this.onClose = this.onClose.bind(this);
-  }
-
-  /** @private */
-  onClose() {
-    const { handleInteraction, url } = this.props;
-
-    handleInteraction(url);
-  }
-
-  /** */
-  render() {
-    const { url } = this.props;
-
-    if (!url) return <></>;
-
-    return <NewWindow name="IiifAuthenticationSender" url={`${url}?origin=${window.origin}`} features="centerscreen" onClose={this.onClose} />;
-  }
-}
-
-AuthenticationSender.propTypes = {
-  handleInteraction: PropTypes.func.isRequired,
-  url: PropTypes.string,
-};
-
-AuthenticationSender.defaultProps = {
-  url: undefined,
-};
diff --git a/src/components/IIIFAuthentication.js b/src/components/IIIFAuthentication.js
new file mode 100644
index 0000000000000000000000000000000000000000..b3ed603f2878c1b4cb48f1427b496bf9211cc443
--- /dev/null
+++ b/src/components/IIIFAuthentication.js
@@ -0,0 +1,190 @@
+import React, { Component } from 'react';
+import PropTypes from 'prop-types';
+import { AccessTokenSender } from './AccessTokenSender';
+import { NewWindow } from './NewWindow';
+import WindowAuthenticationBar from '../containers/WindowAuthenticationBar';
+
+/**
+ * Opens a new window for click
+ */
+export class IIIFAuthentication extends Component {
+  /** */
+  constructor(props) {
+    super(props);
+
+    this.performLogout = this.performLogout.bind(this);
+    this.onReceiveAccessTokenMessage = this.onReceiveAccessTokenMessage.bind(this);
+  }
+
+  /** */
+  onReceiveAccessTokenMessage(payload) {
+    const {
+      authServiceId, accessTokenServiceId, resolveAccessTokenRequest,
+    } = this.props;
+
+    resolveAccessTokenRequest(authServiceId, accessTokenServiceId, payload);
+  }
+
+  /** */
+  defaultAuthBarProps() {
+    const {
+      authServiceId, windowId, status, logoutServiceId,
+    } = this.props;
+
+    return {
+      authServiceId,
+      hasLogoutService: !!logoutServiceId,
+      status,
+      windowId,
+    };
+  }
+
+  /** handle the IIIF logout workflow */
+  performLogout() {
+    const {
+      accessTokenServiceId, authServiceId, features,
+      logoutServiceId, resetAuthenticationState, openWindow,
+    } = this.props;
+    openWindow(logoutServiceId, undefined, features);
+
+    resetAuthenticationState({ authServiceId, tokenServiceId: accessTokenServiceId });
+  }
+
+  /** Render the auth bar for logged in users */
+  renderLoggedIn() {
+    const {
+      isInteractive, t,
+    } = this.props;
+
+    if (!isInteractive) return null;
+
+    return (
+      <WindowAuthenticationBar
+        confirmButton={t('logout')}
+        onConfirm={this.performLogout}
+        {...this.defaultAuthBarProps()}
+      />
+    );
+  }
+
+  /** Render whatever shows up after the interactive login attempt fails */
+  renderFailure() {
+    const {
+      handleAuthInteraction, failureHeader: header, failureDescription: description, t,
+      authServiceId, windowId,
+    } = this.props;
+
+    return (
+      <WindowAuthenticationBar
+        header={header}
+        description={description}
+        confirmButton={t('retry')}
+        onConfirm={() => handleAuthInteraction(windowId, authServiceId)}
+        {...this.defaultAuthBarProps()}
+      />
+    );
+  }
+
+  /** Render the login bar after we're logging in */
+  renderLoggingInCookie() {
+    const {
+      accessTokenServiceId, authServiceId, resolveAuthenticationRequest, features,
+    } = this.props;
+
+    return (
+      <>
+        {this.renderLogin()}
+        <NewWindow name="IiifLoginSender" url={`${authServiceId}?origin=${window.origin}`} features={features} onClose={() => resolveAuthenticationRequest(authServiceId, accessTokenServiceId)} />
+      </>
+    );
+  }
+
+  /** Render the login bar after we're logging in */
+  renderLoggingInToken() {
+    const {
+      accessTokenServiceId,
+    } = this.props;
+
+    return (
+      <>
+        {this.renderLogin()}
+        <AccessTokenSender
+          handleAccessTokenMessage={this.onReceiveAccessTokenMessage}
+          url={accessTokenServiceId}
+        />
+      </>
+    );
+  }
+
+  /** Render a login bar */
+  renderLogin() {
+    const {
+      confirm, description, handleAuthInteraction, header, isInteractive, label,
+      authServiceId, windowId,
+    } = this.props;
+    if (!isInteractive) return null;
+
+    return (
+      <WindowAuthenticationBar
+        header={header}
+        description={description}
+        label={label}
+        confirmButton={confirm}
+        onConfirm={() => handleAuthInteraction(windowId, authServiceId)}
+        {...this.defaultAuthBarProps()}
+      />
+    );
+  }
+
+  /** */
+  render() {
+    const { authServiceId, status } = this.props;
+
+    if (!authServiceId) return null;
+
+    if (status === null) return this.renderLogin();
+    if (status === 'cookie') return this.renderLoggingInCookie();
+    if (status === 'token') return this.renderLoggingInToken();
+    if (status === 'failed') return this.renderFailure();
+    if (status === 'ok') return this.renderLoggedIn();
+
+    return null;
+  }
+}
+
+IIIFAuthentication.propTypes = {
+  accessTokenServiceId: PropTypes.string.isRequired,
+  authServiceId: PropTypes.string.isRequired,
+  confirm: PropTypes.string,
+  description: PropTypes.string,
+  failureDescription: PropTypes.string,
+  failureHeader: PropTypes.string,
+  features: PropTypes.string,
+  handleAuthInteraction: PropTypes.func.isRequired,
+  header: PropTypes.string,
+  isInteractive: PropTypes.bool,
+  label: PropTypes.string,
+  logoutServiceId: PropTypes.string,
+  openWindow: PropTypes.func,
+  resetAuthenticationState: PropTypes.func.isRequired,
+  resolveAccessTokenRequest: PropTypes.func.isRequired,
+  resolveAuthenticationRequest: PropTypes.func.isRequired,
+  status: PropTypes.oneOf(['logout', 'ok', 'token', 'cookie', 'failed', null]),
+  t: PropTypes.func,
+  windowId: PropTypes.string.isRequired,
+};
+
+IIIFAuthentication.defaultProps = {
+  confirm: undefined,
+  description: undefined,
+  failureDescription: undefined,
+  failureHeader: undefined,
+  features: 'centerscreen',
+  header: undefined,
+  isInteractive: true,
+  label: undefined,
+  logoutServiceId: undefined,
+  openWindow: window.open,
+  status: null,
+  t: k => k,
+};
diff --git a/src/components/OpenSeadragonViewer.js b/src/components/OpenSeadragonViewer.js
index 60cf942939fd315d7260304bc21159ce4c8fb343..7c3244bf072bfc8ebead0d147e3939a5f8ec3ab3 100644
--- a/src/components/OpenSeadragonViewer.js
+++ b/src/components/OpenSeadragonViewer.js
@@ -286,6 +286,10 @@ export class OpenSeadragonViewer extends Component {
         return false;
       }
 
+      if (infoResponse.tokenServiceId !== prevInfoResponses[index].tokenServiceId) {
+        return false;
+      }
+
       if (infoResponse.json['@id']
         && infoResponse.json['@id'] === prevInfoResponses[index].json['@id']) {
         return true;
diff --git a/src/components/Window.js b/src/components/Window.js
index 62466dd06d74ee869da7f31cbf7d73acd6e39f50..56d3bd45e6c893b89638f30836c3aaccf1c771ed 100644
--- a/src/components/Window.js
+++ b/src/components/Window.js
@@ -9,7 +9,7 @@ import PrimaryWindow from '../containers/PrimaryWindow';
 import CompanionArea from '../containers/CompanionArea';
 import MinimalWindow from '../containers/MinimalWindow';
 import ErrorContent from '../containers/ErrorContent';
-import WindowAuthenticationControl from '../containers/WindowAuthenticationControl';
+import IIIFAuthentication from '../containers/IIIFAuthentication';
 import { PluginHook } from './PluginHook';
 
 /**
@@ -44,7 +44,7 @@ export class Window extends Component {
           windowId={windowId}
           windowDraggable={windowDraggable}
         />
-        <WindowAuthenticationControl key="auth" windowId={windowId} />
+        <IIIFAuthentication windowId={windowId} />
       </div>
     );
     if (workspaceType === 'mosaic' && windowDraggable) {
diff --git a/src/components/WindowAuthenticationBar.js b/src/components/WindowAuthenticationBar.js
new file mode 100644
index 0000000000000000000000000000000000000000..c0765eacf06705cbc911c718bd24be80f866a3e9
--- /dev/null
+++ b/src/components/WindowAuthenticationBar.js
@@ -0,0 +1,133 @@
+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,
+};
diff --git a/src/components/WindowAuthenticationControl.js b/src/components/WindowAuthenticationControl.js
deleted file mode 100644
index 32b3cbef3856c4fffdf3adcb6af8c9cb050c6d5b..0000000000000000000000000000000000000000
--- a/src/components/WindowAuthenticationControl.js
+++ /dev/null
@@ -1,173 +0,0 @@
-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: () => {},
-};
diff --git a/src/config/settings.js b/src/config/settings.js
index a13a0b154da291c192ecc08299716637d76864e6..abea8f9feb52d36221e6a8710e2e72f277c763ec 100644
--- a/src/config/settings.js
+++ b/src/config/settings.js
@@ -337,5 +337,17 @@ export default {
     viewers: true,
     windows: 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' }
+    ]
   }
 };
diff --git a/src/containers/AccessTokenSender.js b/src/containers/AccessTokenSender.js
deleted file mode 100644
index eb8b51fea3aa7441fd6b2b04c43b33eec34a8b93..0000000000000000000000000000000000000000
--- a/src/containers/AccessTokenSender.js
+++ /dev/null
@@ -1,31 +0,0 @@
-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);
diff --git a/src/containers/AuthenticationLogout.js b/src/containers/AuthenticationLogout.js
deleted file mode 100644
index 1178992d1a2e45e993bbded92f091b62211cdb14..0000000000000000000000000000000000000000
--- a/src/containers/AuthenticationLogout.js
+++ /dev/null
@@ -1,46 +0,0 @@
-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/containers/AuthenticationSender.js b/src/containers/AuthenticationSender.js
deleted file mode 100644
index 97ab2847d775bc1e5467ac1f47f91ff88c4b12d7..0000000000000000000000000000000000000000
--- a/src/containers/AuthenticationSender.js
+++ /dev/null
@@ -1,32 +0,0 @@
-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);
diff --git a/src/containers/IIIFAuthentication.js b/src/containers/IIIFAuthentication.js
new file mode 100644
index 0000000000000000000000000000000000000000..68a605258cc0648abdb48034299b6774006deb55
--- /dev/null
+++ b/src/containers/IIIFAuthentication.js
@@ -0,0 +1,96 @@
+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);
diff --git a/src/containers/WindowAuthenticationBar.js b/src/containers/WindowAuthenticationBar.js
new file mode 100644
index 0000000000000000000000000000000000000000..881bf419a1423289b504995b616cb96270366050
--- /dev/null
+++ b/src/containers/WindowAuthenticationBar.js
@@ -0,0 +1,65 @@
+import { compose } from 'redux';
+import { withTranslation } from 'react-i18next';
+import { withStyles } from '@material-ui/core/styles';
+import { fade } from '@material-ui/core/styles/colorManipulator';
+import { withPlugins } from '../extend/withPlugins';
+import { WindowAuthenticationBar } from '../components/WindowAuthenticationBar';
+
+/**
+ * @param theme
+ * @returns {{typographyBody: {flexGrow: number, fontSize: number|string},
+ * windowTopBarStyle: {minHeight: number, paddingLeft: number, backgroundColor: string}}}
+ */
+const styles = theme => ({
+  buttonInvert: {
+    '&:hover': {
+      backgroundColor: fade(
+        theme.palette.secondary.contrastText, 1 - theme.palette.action.hoverOpacity,
+      ),
+    },
+    backgroundColor: theme.palette.secondary.contrastText,
+    marginLeft: theme.spacing(5),
+    paddingBottom: 0,
+    paddingTop: 0,
+  },
+  expanded: {
+    paddingLeft: theme.spacing(),
+    paddingRight: theme.spacing(),
+  },
+  failure: {
+    backgroundColor: theme.palette.error.dark,
+  },
+  fauxButton: {
+    marginLeft: theme.spacing(2.5),
+  },
+  icon: {
+    marginRight: theme.spacing(1.5),
+    verticalAlign: 'text-bottom',
+  },
+  label: {
+    lineHeight: 2.25,
+  },
+  paper: {
+    backgroundColor: theme.palette.secondary.main,
+    color: theme.palette.secondary.contrastText,
+    cursor: 'pointer',
+  },
+  topBar: {
+    '&:hover': {
+      backgroundColor: theme.palette.secondary.main,
+    },
+    alignItems: 'center',
+    display: 'flex',
+    justifyContent: 'inherit',
+    padding: theme.spacing(1),
+    textTransform: 'none',
+  },
+});
+
+const enhance = compose(
+  withTranslation(),
+  withStyles(styles),
+  withPlugins('WindowAuthenticationBar'),
+);
+
+export default enhance(WindowAuthenticationBar);
diff --git a/src/containers/WindowAuthenticationControl.js b/src/containers/WindowAuthenticationControl.js
deleted file mode 100644
index 97b8978ae201e4bc7b5ee87a19251741cc4009b3..0000000000000000000000000000000000000000
--- a/src/containers/WindowAuthenticationControl.js
+++ /dev/null
@@ -1,100 +0,0 @@
-import { compose } from 'redux';
-import { connect } from 'react-redux';
-import { withTranslation } from 'react-i18next';
-import { withStyles } from '@material-ui/core';
-import { fade } from '@material-ui/core/styles/colorManipulator';
-import { withPlugins } from '../extend/withPlugins';
-import * as actions from '../state/actions';
-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
- * @returns {{typographyBody: {flexGrow: number, fontSize: number|string},
- * windowTopBarStyle: {minHeight: number, paddingLeft: number, backgroundColor: string}}}
- */
-const styles = theme => ({
-  buttonInvert: {
-    '&:hover': {
-      backgroundColor: fade(
-        theme.palette.secondary.contrastText, 1 - theme.palette.action.hoverOpacity,
-      ),
-    },
-    backgroundColor: theme.palette.secondary.contrastText,
-  },
-  expanded: {
-    paddingLeft: theme.spacing(),
-    paddingRight: theme.spacing(),
-  },
-  failure: {
-    backgroundColor: theme.palette.error.dark,
-  },
-  fauxButton: {
-    marginLeft: theme.spacing(2.5),
-  },
-  icon: {
-    marginRight: theme.spacing(1.5),
-    verticalAlign: 'text-bottom',
-  },
-  label: {
-    lineHeight: 2.25,
-  },
-  paper: {
-    backgroundColor: theme.palette.secondary.main,
-    color: theme.palette.secondary.contrastText,
-    cursor: 'pointer',
-  },
-  topBar: {
-    '&:hover': {
-      backgroundColor: theme.palette.secondary.main,
-    },
-    justifyContent: 'inherit',
-    textTransform: 'none',
-  },
-});
-const enhance = compose(
-  connect(mapStateToProps, mapDispatchToProps),
-  withStyles(styles),
-  withTranslation(),
-  withPlugins('WindowAuthenticationControl'),
-);
-
-export default enhance(WindowAuthenticationControl);
diff --git a/src/state/actions/auth.js b/src/state/actions/auth.js
index 72f4ba337758d0bfa8e6ea639b3a1977d170a6cf..582a472b09e1fcf2c5c4789f730f5052c826bc1a 100644
--- a/src/state/actions/auth.js
+++ b/src/state/actions/auth.js
@@ -1,18 +1,15 @@
-import { Utils } from 'manifesto.js/dist-esmodule/Utils';
 import ActionTypes from './action-types';
 
 /**
  * addAuthenticationRequest - action creator
  *
  * @param  {String} windowId
- * @param  {String} infoId
  * @param  {String} id
  * @memberof ActionCreators
  */
-export function addAuthenticationRequest(windowId, infoId, id, profile = undefined) {
+export function addAuthenticationRequest(windowId, id, profile = undefined) {
   return {
     id,
-    infoId,
     profile,
     type: ActionTypes.ADD_AUTHENTICATION_REQUEST,
     windowId,
@@ -21,16 +18,19 @@ export function addAuthenticationRequest(windowId, infoId, id, profile = undefin
 
 /**
  * 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
  * @memberof ActionCreators
  */
-export function resolveAuthenticationRequest(id) {
-  return ((dispatch, getState) => {
-    const { auth } = getState();
-
-    dispatch(fetchAccessTokenRequest(id, auth[id].infoId));
-  });
+export function resolveAuthenticationRequest(id, tokenServiceId, props) {
+  return {
+    id,
+    tokenServiceId,
+    type: ActionTypes.RESOLVE_AUTHENTICATION_REQUEST,
+    ...props,
+  };
 }
 
 /**
@@ -39,13 +39,11 @@ export function resolveAuthenticationRequest(id) {
  *
  * @param  {String} serviceId
  * @param  {String} authId
- * @param  {String} infoIds
  * @memberof ActionCreators
  */
-export function requestAccessToken(serviceId, authId, infoIds) {
+export function requestAccessToken(serviceId, authId) {
   return {
     authId,
-    infoIds,
     serviceId,
     type: ActionTypes.REQUEST_ACCESS_TOKEN,
   };
@@ -59,8 +57,9 @@ export function requestAccessToken(serviceId, authId, infoIds) {
  * @param  {Object} json
  * @memberof ActionCreators
  */
-export function receiveAccessToken(serviceId, json, infoIds) {
+export function receiveAccessToken(authId, serviceId, json) {
   return {
+    authId,
     json,
     serviceId,
     type: ActionTypes.RECEIVE_ACCESS_TOKEN,
@@ -75,71 +74,34 @@ export function receiveAccessToken(serviceId, json, infoIds) {
  * @param  {Object} error
  * @memberof ActionCreators
  */
-export function receiveAccessTokenFailure(serviceId, error) {
+export function receiveAccessTokenFailure(authId, serviceId, error) {
   return {
+    authId,
     error,
     serviceId,
     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
  *
  * @param {Object} message
  * @memberof ActionCreators
  */
-export function resolveAccessTokenRequest({ messageId, ...json }) {
-  return ((dispatch, getState) => {
-    const { authId } = getState().accessTokens[messageId];
+export function resolveAccessTokenRequest(authServiceId, tokenServiceId, json) {
+  if (!json.accessToken) return receiveAccessTokenFailure(authServiceId, tokenServiceId, json);
 
-    dispatch({
-      id: authId,
-      ok: !!json.accessToken,
-      type: ActionTypes.RESOLVE_AUTHENTICATION_REQUEST,
-    });
-
-    if (json.accessToken) {
-      dispatch(receiveAccessToken(messageId, json));
-    } else {
-      dispatch(receiveAccessTokenFailure(messageId, json));
-    }
-  });
+  return receiveAccessToken(authServiceId, tokenServiceId, 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,
-    });
-  });
+export function resetAuthenticationState({ authServiceId, tokenServiceId }) {
+  return {
+    id: authServiceId,
+    tokenServiceId,
+    type: ActionTypes.RESET_AUTHENTICATION_STATE,
+  };
 }
diff --git a/src/state/actions/infoResponse.js b/src/state/actions/infoResponse.js
index 3aed86c00b71c5f5dcbcf8e675232fe848a77ed6..b990a9c86a894aa8a8abc6e08971c5add1821264 100644
--- a/src/state/actions/infoResponse.js
+++ b/src/state/actions/infoResponse.js
@@ -6,11 +6,12 @@ import ActionTypes from './action-types';
  * @param  {String} infoId
  * @memberof ActionCreators
  */
-export function requestInfoResponse(infoId, imageResource) {
+export function requestInfoResponse(infoId, imageResource, windowId) {
   return {
     imageResource,
     infoId,
     type: ActionTypes.REQUEST_INFO_RESPONSE,
+    windowId,
   };
 }
 
@@ -38,13 +39,14 @@ export function receiveInfoResponse(infoId, infoJson, ok, tokenServiceId) {
  * @param  {Object} manifestJson
  * @memberof ActionCreators
  */
-export function receiveDegradedInfoResponse(infoId, infoJson, ok, tokenServiceId) {
+export function receiveDegradedInfoResponse(infoId, infoJson, ok, tokenServiceId, windowId) {
   return {
     infoId,
     infoJson,
     ok,
     tokenServiceId,
     type: ActionTypes.RECEIVE_DEGRADED_INFO_RESPONSE,
+    windowId,
   };
 }
 
@@ -70,10 +72,10 @@ export function receiveInfoResponseFailure(infoId, error, tokenServiceId) {
  * @param  {String} infoId
  * @memberof ActionCreators
  */
-export function fetchInfoResponse({ imageId, imageResource }) {
+export function fetchInfoResponse({ imageId, imageResource, windowId }) {
   const imageService = imageResource && imageResource.getServices()[0];
   const infoId = (imageId || imageService.id);
-  return requestInfoResponse(infoId, imageService);
+  return requestInfoResponse(infoId, imageService, windowId);
 }
 
 /**
diff --git a/src/state/reducers/accessTokens.js b/src/state/reducers/accessTokens.js
index 1dd6d1b7a90b3cc8639f6198ec5736ab7754c872..45f7f6ad36888daecc33700a82e745908146f9bd 100644
--- a/src/state/reducers/accessTokens.js
+++ b/src/state/reducers/accessTokens.js
@@ -1,29 +1,15 @@
 import omit from 'lodash/omit';
-import { Utils } from 'manifesto.js/dist-esmodule/Utils';
 import ActionTypes from '../actions/action-types';
 
 /** */
 export function accessTokensReducer(state = {}, action) {
-  let authService;
-  let tokenService;
-
   switch (action.type) {
-    case ActionTypes.RECEIVE_DEGRADED_INFO_RESPONSE:
-      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;
-
+    case ActionTypes.RESOLVE_AUTHENTICATION_REQUEST:
       return {
         ...state,
-        [tokenService.id]: {
-          authId: authService.id,
-          id: tokenService.id,
-          infoIds: [].concat(
-            (state[tokenService.id] && state[tokenService.id].infoIds) || [],
-            action.infoId,
-          ),
+        [action.tokenServiceId]: {
+          authId: action.id,
+          id: action.tokenServiceId,
           isFetching: true,
         },
       };
@@ -33,7 +19,6 @@ export function accessTokensReducer(state = {}, action) {
         [action.serviceId]: {
           authId: action.authId,
           id: action.serviceId,
-          infoIds: action.infoIds,
           isFetching: true,
         },
       };
@@ -46,14 +31,6 @@ export function accessTokensReducer(state = {}, action) {
           json: action.json,
         },
       };
-    case ActionTypes.CLEAR_ACCESS_TOKEN_QUEUE:
-      return {
-        ...state,
-        [action.serviceId]: {
-          ...state[action.serviceId],
-          infoIds: [],
-        },
-      };
     case ActionTypes.RECEIVE_ACCESS_TOKEN_FAILURE:
       return {
         ...state,
@@ -65,6 +42,17 @@ export function accessTokensReducer(state = {}, action) {
       };
     case ActionTypes.RESET_AUTHENTICATION_STATE:
       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:
       return state;
   }
diff --git a/src/state/reducers/auth.js b/src/state/reducers/auth.js
index 0b230a0327aec712397fe867d5463528742dcd3b..2541bdc7c9fd5b16f7a2051a937969370e13e4a4 100644
--- a/src/state/reducers/auth.js
+++ b/src/state/reducers/auth.js
@@ -1,56 +1,40 @@
 import omit from 'lodash/omit';
-
 import ActionTypes from '../actions/action-types';
-import { selectNextAuthService } from '../selectors/canvases';
 
 /**
  * authReducer
  */
 export const authReducer = (state = {}, action) => {
-  let service;
-
   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:
       return {
         ...state,
         [action.id]: {
           id: action.id,
-          infoId: [].concat(
-            (state[action.id] && state[action.id].infoId) || [],
-            action.infoId,
-          ),
           isFetching: true,
           profile: action.profile,
+          windowId: action.windowId,
         },
       };
     case ActionTypes.RESOLVE_AUTHENTICATION_REQUEST:
       return {
         ...state,
         [action.id]: {
-          id: action.id,
+          ...state[action.id],
           isFetching: false,
           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:
       return omit(state, action.id);
     default: return state;
diff --git a/src/state/reducers/infoResponses.js b/src/state/reducers/infoResponses.js
index d38b6b8e809bc3b8da1215df5e15d7335ddca634..3141f096de68a7cc332ab8dd80159776ac1532ec 100644
--- a/src/state/reducers/infoResponses.js
+++ b/src/state/reducers/infoResponses.js
@@ -54,13 +54,6 @@ export const infoResponsesReducer = (state = {}, action) => {
       }, {});
     case ActionTypes.IMPORT_MIRADOR_STATE:
       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;
   }
 };
diff --git a/src/state/sagas/auth.js b/src/state/sagas/auth.js
new file mode 100644
index 0000000000000000000000000000000000000000..21135885e2005a631cee0725559b63aa305cb792
--- /dev/null
+++ b/src/state/sagas/auth.js
@@ -0,0 +1,162 @@
+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),
+  ]);
+}
diff --git a/src/state/sagas/iiif.js b/src/state/sagas/iiif.js
index ee1f8e20599d286895b4af96b6032b0d6dd4b2f0..f0ce4c5371f91f62095e952251dd786e998afec3 100644
--- a/src/state/sagas/iiif.js
+++ b/src/state/sagas/iiif.js
@@ -115,7 +115,10 @@ export function* fetchManifest({ manifestId }) {
 
 /** @private */
 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;
 
   const accessTokens = yield select(getAccessTokens);
@@ -123,8 +126,9 @@ function* getAccessTokenService(resource) {
 
   for (let i = 0; i < services.length; i += 1) {
     const authService = services[i];
-    const accessTokenService = Utils.getService(authService, 'http://iiif.io/api/auth/1/token');
-    const token = accessTokens[accessTokenService.id];
+    const accessTokenService = Utils.getService(authService, 'http://iiif.io/api/auth/1/token')
+      || Utils.getService(authService, 'http://iiif.io/api/auth/0/token');
+    const token = accessTokenService && accessTokens[accessTokenService.id];
     if (token && token.json) return token;
   }
 
@@ -132,7 +136,7 @@ function* getAccessTokenService(resource) {
 }
 
 /** @private */
-export function* fetchInfoResponse({ imageResource, infoId, tokenService: passedTokenService }) {
+export function* fetchInfoResponse({ imageResource, infoId, windowId }) {
   let iiifResource = imageResource;
   if (!iiifResource) {
     iiifResource = yield select(selectInfoResponse, { infoId });
@@ -141,7 +145,7 @@ export function* fetchInfoResponse({ imageResource, infoId, tokenService: passed
   const callbacks = {
     degraded: ({
       json, response, tokenServiceId,
-    }) => receiveDegradedInfoResponse(infoId, json, response.ok, tokenServiceId),
+    }) => receiveDegradedInfoResponse(infoId, json, response.ok, tokenServiceId, windowId),
     failure: ({
       error, json, response, tokenServiceId,
     }) => (
@@ -194,22 +198,6 @@ export function* fetchResourceManifest({ manifestId, manifestJson }) {
   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) {
   const manifests = yield select(getManifests);
@@ -227,7 +215,6 @@ export default function* iiifSaga() {
     takeEvery(ActionTypes.REQUEST_INFO_RESPONSE, fetchInfoResponse),
     takeEvery(ActionTypes.REQUEST_SEARCH, fetchSearchResponse),
     takeEvery(ActionTypes.REQUEST_ANNOTATION, fetchAnnotation),
-    takeEvery(ActionTypes.RECEIVE_ACCESS_TOKEN, refetchInfoResponses),
     takeEvery(ActionTypes.ADD_RESOURCE, fetchResourceManifest),
   ]);
 }
diff --git a/src/state/sagas/index.js b/src/state/sagas/index.js
index 3ebb80e450e713a8833833568d12182bc1854425..d3e23cca68b1591746be40fbf5c981e6c5dbd588 100644
--- a/src/state/sagas/index.js
+++ b/src/state/sagas/index.js
@@ -5,7 +5,8 @@ import {
 import appSaga from './app';
 import iiifSaga from './iiif';
 import windowSaga from './windows';
-import annotations from './annotations';
+import annotationsSaga from './annotations';
+import authSaga from './auth';
 
 /** */
 function* launchSaga(saga) {
@@ -23,10 +24,11 @@ function* launchSaga(saga) {
 function getRootSaga(pluginSagas = []) {
   return function* rootSaga() {
     const sagas = [
-      annotations,
+      annotationsSaga,
       appSaga,
       iiifSaga,
       windowSaga,
+      authSaga,
       ...pluginSagas,
     ];
 
diff --git a/src/state/sagas/windows.js b/src/state/sagas/windows.js
index 26c8b9b1aa1ea548c0ec5db74c69629fd045361d..af08e2378d089a3d21fed758de81254586ce3f8c 100644
--- a/src/state/sagas/windows.js
+++ b/src/state/sagas/windows.js
@@ -223,7 +223,7 @@ export function* fetchInfoResponses({ visibleCanvases: visibleCanvasIds, windowI
     const miradorCanvas = new MiradorCanvas(canvas);
     return all(miradorCanvas.iiifImageResources.map(imageResource => (
       !infoResponses[imageResource.getServices()[0].id]
-        && put(fetchInfoResponse({ imageResource }))
+        && put(fetchInfoResponse({ imageResource, windowId }))
     )).filter(Boolean));
   }));
 }
diff --git a/src/state/selectors/auth.js b/src/state/selectors/auth.js
index c1d75341d9e3bc86a3d02aef9c074f24730b3964..0345021475d3f710a504ba520cc6c26ddae20851 100644
--- a/src/state/selectors/auth.js
+++ b/src/state/selectors/auth.js
@@ -1,7 +1,83 @@
+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 { 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 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;
+    }, {}));
+  },
+);
diff --git a/src/state/selectors/canvases.js b/src/state/selectors/canvases.js
index d2083c184f89a5e46e2ec4afb0633ef399ec2b2b..684676bcd8fb15756f7a5246b2c6b535ef12046c 100644
--- a/src/state/selectors/canvases.js
+++ b/src/state/selectors/canvases.js
@@ -1,5 +1,4 @@
 import { createSelector } from 'reselect';
-import { Utils } from 'manifesto.js/dist-esmodule/Utils';
 import flatten from 'lodash/flatten';
 import CanvasGroupings from '../../lib/CanvasGroupings';
 import MiradorCanvas from '../../lib/MiradorCanvas';
@@ -206,100 +205,3 @@ export const selectInfoResponse = createSelector(
     && infoResponses[iiifServiceId];
   },
 );
-
-const authServiceProfiles = {
-  clickthrough: true, external: true, kiosk: true, login: true,
-};
-/**
- *
- */
-export function selectNextAuthService({ auth }, resource, filter = authServiceProfiles) {
-  const orderedAuthServiceProfiles = [
-    'http://iiif.io/api/auth/1/external',
-    'http://iiif.io/api/auth/1/kiosk',
-    'http://iiif.io/api/auth/1/clickthrough',
-    'http://iiif.io/api/auth/1/login',
-  ];
-
-  const mapFilterToProfiles = {
-    'http://iiif.io/api/auth/1/clickthrough': 'clickthrough',
-    'http://iiif.io/api/auth/1/external': 'external',
-    'http://iiif.io/api/auth/1/kiosk': 'kiosk',
-    'http://iiif.io/api/auth/1/login': 'login',
-  };
-
-  for (const profile of orderedAuthServiceProfiles) {
-    const services = getServices(resource, profile);
-    for (const service of services) {
-      if (!auth[service.id]) {
-        return filter[mapFilterToProfiles[profile]] && service;
-      }
-
-      if (auth[service.id].isFetching || auth[service.id].ok) return null;
-    }
-  }
-
-  return null;
-}
-
-/** */
-export function selectActiveAuthService(state, resource) {
-  const orderedAuthServiceProfiles = [
-    'http://iiif.io/api/auth/1/login',
-    'http://iiif.io/api/auth/1/clickthrough',
-    'http://iiif.io/api/auth/1/kiosk',
-    'http://iiif.io/api/auth/1/external',
-  ];
-
-  for (const profile of orderedAuthServiceProfiles) {
-    const services = getServices(resource, profile);
-    const service = services.find(s => selectAuthStatus(state, s));
-    if (service) return service;
-  }
-
-  return null;
-}
-
-export const selectCanvasAuthService = createSelector(
-  [
-    selectInfoResponse,
-    state => state,
-  ],
-  (infoResponse, state) => {
-    const resource = infoResponse && infoResponse.json;
-
-    if (!resource) return undefined;
-
-    return selectNextAuthService(state, resource)
-      || selectActiveAuthService(state, resource);
-  },
-);
-
-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;
-  if (!auth[service.id]) return null;
-  if (auth[service.id].isFetching) return 'fetching';
-  if (auth[service.id].ok) return 'ok';
-  return 'failed';
-}
-
-/** Get all the services that match a profile */
-function getServices(resource, profile) {
-  const services = Utils.getServices({ ...resource, options: {} });
-
-  return services.filter(service => service.getProfile() === profile);
-}