Skip to content

Commit

Permalink
COLUMBIA: Support auth2 style external authentication
Browse files Browse the repository at this point in the history
- permit authentication service not to have id attribute
- components/IIIFAuthentication has a prop to indicate that auth service is external
- containers/IIIFAuthentication.js identifies whether authService is external and sets key if necessary
- state/selectors/auth.js sets a default authService key if external service has no id (expected in auth2)
- remove unnecessary invocation of probeResponses selector in state/selectors/auth.js
- state/sagas/auth.js handles probeJson or infoJson, and defaults external auth service ids when missing (per auth2)
- src/state/reducers/auth.js ensures authService state has id prop if resolved but not requested
  • Loading branch information
barmintor committed Jun 24, 2024
1 parent 60c24ef commit 0283c09
Show file tree
Hide file tree
Showing 9 changed files with 209 additions and 56 deletions.
59 changes: 59 additions & 0 deletions __tests__/fixtures/version-3/auth2-external.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
{
"@context": "http://iiif.io/api/presentation/3/context.json",
"id": "https://auth.example.org/external/my-video1/manifest.json",
"type": "Manifest",
"label": {
"en": [
"An externally authorized video"
]
},
"items": [
{
"id": "https://auth.example.org/external/my-video1",
"type": "Canvas",
"label": {
"en": [
"Canvas with a single IIIF video"
]
},
"height": 3024,
"width": 4032,
"items": [
{
"id": "https://auth.example.org/my-video/page",
"type": "AnnotationPage",
"items": [
{
"id": "https://auth.example.org/my-video/annotation",
"type": "Annotation",
"motivation": "painting",
"body": {
"id": "https://auth.example.org/my-video.mp4",
"type": "Video",
"service": [
{
"id": "https://auth.example.org/probe/my-video",
"type": "AuthProbeService2",
"service" : [
{
"type": "AuthAccessService2",
"profile": "external",
"label": { "en": [ "External Authentication" ] },
"service" : [
{
"id": "https://auth.example.org/token",
"type": "AuthAccessTokenService2"
}
]
}
]
}
]
}
}
]
}
]
}
]
}
50 changes: 34 additions & 16 deletions __tests__/src/reducers/auth.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,25 +17,43 @@ describe('auth response reducer', () => {
},
});
});
it('should handle RESOLVE_AUTHENTICATION_REQUEST', () => {
expect(authReducer(
{
describe('should handle RESOLVE_AUTHENTICATION_REQUEST', () => {
it('sets isFetching to false and passes ok attribute value', () => {
expect(authReducer(
{
abc123: {
id: 'abc123',
isFetching: true,
},
},
{
id: 'abc123',
ok: true,
type: ActionTypes.RESOLVE_AUTHENTICATION_REQUEST,
},
)).toMatchObject({
abc123: {
id: 'abc123',
isFetching: true,
isFetching: false,
ok: true,
},
},
{
id: 'abc123',
ok: true,
type: ActionTypes.RESOLVE_AUTHENTICATION_REQUEST,
},
)).toMatchObject({
abc123: {
id: 'abc123',
isFetching: false,
ok: true,
},
});
});
it('sets id if absent from skipping ADD_AUTHENTICATION_REQUEST', () => {
expect(authReducer(
{},
{
id: 'abc123',
ok: true,
type: ActionTypes.RESOLVE_AUTHENTICATION_REQUEST,
},
)).toMatchObject({
abc123: {
id: 'abc123',
isFetching: false,
ok: true,
},
});
});
});
describe('should handle RECEIVE_ACCESS_TOKEN', () => {
Expand Down
49 changes: 42 additions & 7 deletions __tests__/src/sagas/auth.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import {
selectInfoResponses,
getVisibleCanvases,
getAuth,
getConfig,
getAuthProfiles,
} from '../../../src/state/selectors';

describe('IIIF Authentication sagas', () => {
Expand Down Expand Up @@ -191,10 +191,11 @@ describe('IIIF Authentication sagas', () => {
return expectSaga(doAuthWorkflow, { infoJson, windowId })
.provide([
[select(getAuth), {}],
[select(getConfig), { auth: settings.auth }],
[select(getAuthProfiles), settings.auth.serviceProfiles],
])
.put({
id: 'https://authentication.example.com/external',
ok: true,
tokenServiceId: 'https://authentication.example.com/token',
type: ActionTypes.RESOLVE_AUTHENTICATION_REQUEST,
})
Expand All @@ -205,6 +206,40 @@ describe('IIIF Authentication sagas', () => {
})
.run();
});
it('kicks off the first external auth from a probe response', () => {
const probeJson = {
id: 'https://authentication.example.com/probe',
service: [{
profile: 'external',
service: [
{
id: 'https://authentication.example.com/token',
type: 'AuthAccessTokenService2',
},
],
type: 'AuthAccessService2',
}],
type: 'AuthProbeService2',
};
const windowId = 'window';
return expectSaga(doAuthWorkflow, { probeJson, windowId })
.provide([
[select(getAuth), {}],
[select(getAuthProfiles), settings.auth.serviceProfiles],
])
.put({
id: 'external',
ok: true,
tokenServiceId: 'https://authentication.example.com/token',
type: ActionTypes.RESOLVE_AUTHENTICATION_REQUEST,
})
.put({
authId: '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 = {
Expand All @@ -224,7 +259,7 @@ describe('IIIF Authentication sagas', () => {
return expectSaga(doAuthWorkflow, { infoJson, windowId })
.provide([
[select(getAuth), { 'https://authentication.example.com/external': { ok: false } }],
[select(getConfig), { auth: settings.auth }],
[select(getAuthProfiles), settings.auth.serviceProfiles],
])
.not.put.like({ type: ActionTypes.RESOLVE_AUTHENTICATION_REQUEST })
.not.put.like({ type: ActionTypes.REQUEST_ACCESS_TOKEN })
Expand All @@ -249,7 +284,7 @@ describe('IIIF Authentication sagas', () => {
return expectSaga(doAuthWorkflow, { infoJson, windowId })
.provide([
[select(getAuth), {}],
[select(getConfig), { auth: settings.auth }],
[select(getAuthProfiles), settings.auth.serviceProfiles],
])
.not.put.like({ type: ActionTypes.RESOLVE_AUTHENTICATION_REQUEST })
.not.put.like({ type: ActionTypes.REQUEST_ACCESS_TOKEN })
Expand All @@ -274,7 +309,7 @@ describe('IIIF Authentication sagas', () => {
return expectSaga(doAuthWorkflow, { infoJson, windowId })
.provide([
[select(getAuth), {}],
[select(getConfig), { auth: settings.auth }],
[select(getAuthProfiles), settings.auth.serviceProfiles],
])
.put({
id: 'https://authentication.example.com/kiosk',
Expand Down Expand Up @@ -360,7 +395,7 @@ describe('IIIF Authentication sagas', () => {
return expectSaga(invalidateInvalidAuth, { serviceId })
.provide([
[select(getAccessTokens), { [serviceId]: { authId, id: serviceId, success: true } }],
[select(getAuth), { [authId]: { id: authId } }],
[select(getAuth), { [authId]: { getProfile: () => 'login', id: authId } }],
])
.put({
id: authId,
Expand All @@ -377,7 +412,7 @@ describe('IIIF Authentication sagas', () => {
return expectSaga(invalidateInvalidAuth, { serviceId })
.provide([
[select(getAccessTokens), { [serviceId]: { authId, id: serviceId } }],
[select(getAuth), { [authId]: { id: authId } }],
[select(getAuth), { [authId]: { getProfile: () => 'login', id: authId } }],
])
.put({
id: authId,
Expand Down
15 changes: 15 additions & 0 deletions __tests__/src/selectors/auth.test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import manifestFixture001 from '../../fixtures/version-2/001.json';
import manifestFixture019 from '../../fixtures/version-2/019.json';
import manifestFixtureAuth2ActiveVideo from '../../fixtures/version-3/auth2-active.json';
import manifestFixtureAuth2ExternalVideo from '../../fixtures/version-3/auth2-external.json';
import settings from '../../../src/config/settings';
import {
getAccessTokens,
Expand Down Expand Up @@ -65,6 +66,9 @@ describe('selectCurrentAuthServices', () => {
c: {
json: manifestFixtureAuth2ActiveVideo,
},
d: {
json: manifestFixtureAuth2ExternalVideo,
},
},
windows: {
noCanvas: {
Expand Down Expand Up @@ -94,6 +98,12 @@ describe('selectCurrentAuthServices', () => {
'https://auth.example.org/my-video1',
],
},
zz: {
manifestId: 'd',
visibleCanvases: [
'https://auth.example.org/external/my-video1',
],
},
},
};

Expand All @@ -104,6 +114,8 @@ describe('selectCurrentAuthServices', () => {
it('returns the next auth service to try', () => {
expect(selectCurrentAuthServices(state, { windowId: 'w' })[0].id).toEqual('external');
expect(selectCurrentAuthServices(state, { windowId: 'z' })[0].id).toEqual('https://auth.example.org/login');
expect(selectCurrentAuthServices(state, { windowId: 'zz' })[0].id).toBeUndefined();
expect(selectCurrentAuthServices(state, { windowId: 'zz' })[0].getProfile()).toEqual('external');
});

it('returns the service if the next auth service is interactive', () => {
Expand All @@ -119,6 +131,9 @@ describe('selectCurrentAuthServices', () => {
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();
expect(selectCurrentAuthServices({ ...state, auth }, { windowId: 'z' })[0].id).toEqual('https://auth.example.org/login');
expect(selectCurrentAuthServices(state, { windowId: 'zz' })[0].id).toBeUndefined();
expect(selectCurrentAuthServices(state, { windowId: 'zz' })[0].getProfile()).toEqual('external');
});

describe('proscribed order', () => {
Expand Down
7 changes: 4 additions & 3 deletions src/components/IIIFAuthentication.js
Original file line number Diff line number Diff line change
Expand Up @@ -138,11 +138,10 @@ export class IIIFAuthentication extends Component {

/** */
render() {
const { authServiceId, status } = this.props;

const { authServiceExternal, authServiceId, status } = this.props;
if (!authServiceId) return null;

if (status === null) return this.renderLogin();
if (status === null) return (authServiceExternal) ? this.renderLoggingInToken() : this.renderLogin();
if (status === 'cookie') return this.renderLoggingInCookie();
if (status === 'token') return this.renderLoggingInToken();
if (status === 'failed') return this.renderFailure();
Expand All @@ -154,6 +153,7 @@ export class IIIFAuthentication extends Component {

IIIFAuthentication.propTypes = {
accessTokenServiceId: PropTypes.string.isRequired,
authServiceExternal: PropTypes.bool,
authServiceId: PropTypes.string.isRequired,
confirm: PropTypes.string,
description: PropTypes.string,
Expand All @@ -176,6 +176,7 @@ IIIFAuthentication.propTypes = {
};

IIIFAuthentication.defaultProps = {
authServiceExternal: false,
confirm: undefined,
description: undefined,
failureDescription: undefined,
Expand Down
19 changes: 13 additions & 6 deletions src/containers/IIIFAuthentication.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,22 @@ import { IIIFAuthentication } from '../components/IIIFAuthentication';
const mapStateToProps = (state, { windowId }) => {
const services = selectCurrentAuthServices(state, { windowId });

const authProfiles = getAuthProfiles(state);

// TODO: get the most actionable auth service...
const service = services[0];
if (!service) return {};
/** get the auth service profile */
const serviceProfile = (s) => authProfiles.find(p => p.profile === s.getProfile());
const authServiceExternal = (serviceProfile(service)?.external);
/** get the key to track an auth service by */
const serviceKey = (authServiceExternal) ? (service.id || 'external') : (service.id);

const accessTokenService = getTokenService(service);
const logoutService = getLogoutService(service);

const authStatuses = getAuth(state);
const authStatus = service && authStatuses[service.id];
const authStatus = service && authStatuses[serviceKey];
const accessTokens = getAccessTokens(state);
const accessTokenStatus = accessTokenService && accessTokens[accessTokenService.id];

Expand All @@ -45,17 +53,16 @@ const mapStateToProps = (state, { windowId }) => {
status = 'failed';
}

const authProfiles = getAuthProfiles(state);

const profile = service && service.getProfile();
const profile = service.getProfile();

const isInteractive = authProfiles.some(
config => config.profile === profile && !(config.external || config.kiosk),
config => (config.profile === profile) && !(config.external || config.kiosk),
);

return {
accessTokenServiceId: accessTokenService && accessTokenService.id,
authServiceId: service && service.id,
authServiceExternal,
authServiceId: serviceKey,
confirm: service && service.getConfirmLabel(),
description: service && service.getDescription(),
failureDescription: service && service.getFailureDescription(),
Expand Down
1 change: 1 addition & 0 deletions src/state/reducers/auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export const authReducer = (state = {}, action) => {
return {
...state,
[action.id]: {
id: action.id, // external services skip ADD_AUTHENTICATION_REQUEST and need id
...state[action.id],
isFetching: false,
ok: action.ok,
Expand Down
Loading

0 comments on commit 0283c09

Please sign in to comment.