diff --git a/__tests__/src/sagas/auth.test.js b/__tests__/src/sagas/auth.test.js index ab9c6384811327a541e34efa140d5eafc9c9fc6f..229e9956e3a166de4ab843e322029fb1a05b4e12 100644 --- a/__tests__/src/sagas/auth.test.js +++ b/__tests__/src/sagas/auth.test.js @@ -8,6 +8,8 @@ import { refetchInfoResponses, refetchInfoResponsesOnLogout, doAuthWorkflow, + rerequestOnAccessTokenFailure, + invalidateInvalidAuth, } from '../../../src/state/sagas/auth'; import { fetchInfoResponse, @@ -283,4 +285,107 @@ describe('IIIF Authentication sagas', () => { .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/src/state/sagas/auth.js b/src/state/sagas/auth.js index 3ce04f5a7962bc6a6005bab51b1071e1da764801..5f99c8c85d58f31ae07af4cfaaadc2f53242664d 100644 --- a/src/state/sagas/auth.js +++ b/src/state/sagas/auth.js @@ -98,9 +98,60 @@ export function* doAuthWorkflow({ infoJson, windowId }) { 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'); + + 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),