Skip to content

Commit 3beec13

Browse files
authored
Merge pull request #20 from solid/cache-host-auth-proto
Cache host auth proto
2 parents 94a048d + 991fa0b commit 3beec13

File tree

8 files changed

+262
-45
lines changed

8 files changed

+262
-45
lines changed

src/api.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ const responseFromFirstSession = (storage: Storage, authFns: Array<() => Promise
3737
return authFns[0]()
3838
.then(session =>
3939
session
40-
? { session: saveSession(storage, session), fetch: authnFetch(storage) }
40+
? { session: saveSession(storage)(session), fetch: authnFetch(storage) }
4141
: responseFromFirstSession(storage, authFns.slice(1)))
4242
.catch(err => {
4343
console.error(err)

src/api.spec.js

Lines changed: 98 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import rsaPemToJwk from 'rsa-pem-to-jwk'
77
import URLSearchParams from 'url-search-params'
88

99
import { currentSession, fetch, login, logout } from './api'
10+
import { saveHost } from './hosts'
1011
import { getSession, saveSession } from './session'
1112
import { memStorage } from './storage'
1213

@@ -203,8 +204,8 @@ describe('login', () => {
203204

204205
describe('currentSession', () => {
205206
it('can find the current session if stored', () => {
206-
saveSession(window.localStorage, {
207-
type: 'WebID-OIDC',
207+
saveSession(window.localStorage)({
208+
authType: 'WebID-OIDC',
208209
idp: 'https://localhost',
209210
webId: 'https://person.me/#me',
210211
accessToken: 'fake_access_token',
@@ -291,7 +292,8 @@ describe('currentSession', () => {
291292
describe('logout', () => {
292293
describe('WebID-TLS', () => {
293294
it('just removes the current session from the store', () => {
294-
saveSession(window.localStorage, {
295+
saveSession(window.localStorage)({
296+
authType: 'WebID-TLS',
295297
idp: 'https://localhost',
296298
webId: 'https://person.me/#me'
297299
})
@@ -374,8 +376,8 @@ describe('logout', () => {
374376

375377
describe('fetch', () => {
376378
it('handles 401s from WebID-OIDC resources by resending with credentials', () => {
377-
saveSession(window.localStorage, {
378-
type: 'WebID-OIDC',
379+
saveSession(window.localStorage)({
380+
authType: 'WebID-OIDC',
379381
idp: 'https://localhost',
380382
webId: 'https://person.me/#me',
381383
accessToken: 'fake_access_token',
@@ -384,7 +386,7 @@ describe('fetch', () => {
384386

385387
nock('https://third-party.com')
386388
.get('/protected-resource')
387-
.reply(401, '', { 'www-authenticate': 'Bearer scope=openid' })
389+
.reply(401, '', { 'www-authenticate': 'Bearer scope="openid webid"' })
388390
.get('/protected-resource')
389391
.matchHeader('authorization', 'Bearer abc.def.ghi')
390392
.reply(200)
@@ -396,8 +398,8 @@ describe('fetch', () => {
396398
})
397399

398400
it('merges request headers with the authorization header', () => {
399-
saveSession(window.localStorage, {
400-
type: 'WebID-OIDC',
401+
saveSession(window.localStorage)({
402+
authType: 'WebID-OIDC',
401403
idp: 'https://localhost',
402404
webId: 'https://person.me/#me',
403405
accessToken: 'fake_access_token',
@@ -406,7 +408,7 @@ describe('fetch', () => {
406408

407409
nock('https://third-party.com')
408410
.get('/private-resource')
409-
.reply(401, '', { 'www-authenticate': 'Bearer scope=openid' })
411+
.reply(401, '', { 'www-authenticate': 'Bearer scope="openid webid"' })
410412
.get('/private-resource')
411413
.matchHeader('accept', 'text/plain')
412414
.matchHeader('authorization', 'Bearer abc.def.ghi')
@@ -419,8 +421,8 @@ describe('fetch', () => {
419421
})
420422

421423
it('does not resend with credentials if the www-authenticate header is missing', () => {
422-
saveSession(window.localStorage, {
423-
type: 'WebID-OIDC',
424+
saveSession(window.localStorage)({
425+
authType: 'WebID-OIDC',
424426
idp: 'https://localhost',
425427
webId: 'https://person.me/#me',
426428
accessToken: 'fake_access_token',
@@ -438,8 +440,8 @@ describe('fetch', () => {
438440
})
439441

440442
it('does not resend with credentials if the www-authenticate header suggests an unknown scheme', () => {
441-
saveSession(window.localStorage, {
442-
type: 'WebID-OIDC',
443+
saveSession(window.localStorage)({
444+
authType: 'WebID-OIDC',
443445
idp: 'https://localhost',
444446
webId: 'https://person.me/#me',
445447
accessToken: 'fake_access_token',
@@ -459,7 +461,7 @@ describe('fetch', () => {
459461
it('does not resend with credentials if there is no session', () => {
460462
nock('https://third-party.com')
461463
.get('/protected-resource')
462-
.reply(401, '', { 'www-authenticate': 'Bearer scope=openid' })
464+
.reply(401, '', { 'www-authenticate': 'Bearer scope="openid webid"' })
463465

464466
return fetch('https://third-party.com/protected-resource')
465467
.then(resp => {
@@ -481,4 +483,86 @@ describe('fetch', () => {
481483
expect(body).toEqual('public content')
482484
})
483485
})
486+
487+
it('does not resend with credentials if the requested resources uses plain OIDC', () => {
488+
nock('https://third-party.com')
489+
.get('/protected-resource')
490+
.reply(401, '', { 'www-authenticate': 'Bearer scope="openid"' })
491+
492+
return fetch('https://third-party.com/protected-resource')
493+
.then(resp => {
494+
expect(resp.status).toBe(401)
495+
})
496+
})
497+
498+
describe('familiar domains with WebID-OIDC', () => {
499+
it('just sends one request when the RP is also the IDP', () => {
500+
saveSession(window.localStorage)({
501+
authType: 'WebID-OIDC',
502+
idp: 'https://localhost',
503+
webId: 'https://person.me/#me',
504+
accessToken: 'fake_access_token',
505+
idToken: 'abc.def.ghi'
506+
})
507+
508+
nock('https://localhost')
509+
.get('/resource')
510+
.matchHeader('authorization', 'Bearer abc.def.ghi')
511+
.reply(200)
512+
513+
return fetch('https://localhost/resource')
514+
.then(resp => {
515+
expect(resp.status).toBe(200)
516+
})
517+
})
518+
519+
it('just sends one request to domains it has already encountered', () => {
520+
saveSession(window.localStorage)({
521+
authType: 'WebID-OIDC',
522+
idp: 'https://localhost',
523+
webId: 'https://person.me/#me',
524+
accessToken: 'fake_access_token',
525+
idToken: 'abc.def.ghi'
526+
})
527+
528+
saveHost(window.localStorage)({
529+
url: 'third-party.com',
530+
authType: 'WebID-OIDC'
531+
})
532+
533+
nock('https://third-party.com')
534+
.get('/resource')
535+
.matchHeader('authorization', 'Bearer abc.def.ghi')
536+
.reply(200)
537+
538+
return fetch('https://third-party.com/resource')
539+
.then(resp => {
540+
expect(resp.status).toBe(200)
541+
})
542+
})
543+
544+
it('does not send credentials to a familiar domain when that domain uses a different auth type', () => {
545+
saveSession(window.localStorage)({
546+
authType: 'WebID-OIDC',
547+
idp: 'https://localhost',
548+
webId: 'https://person.me/#me',
549+
accessToken: 'fake_access_token',
550+
idToken: 'abc.def.ghi'
551+
})
552+
553+
saveHost(window.localStorage)({
554+
url: 'third-party.com',
555+
authType: 'WebID-TLS'
556+
})
557+
558+
nock('https://third-party.com')
559+
.get('/resource')
560+
.reply(401)
561+
562+
return fetch('https://third-party.com/resource')
563+
.then(resp => {
564+
expect(resp.status).toBe(401)
565+
})
566+
})
567+
})
484568
})

src/authn-fetch.js

Lines changed: 36 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,46 @@
11
// @flow
22
/* global fetch, RequestInfo, Response */
33
import 'isomorphic-fetch'
4-
import * as authorization from 'auth-header'
54

5+
import { getHost, updateHostFromResponse } from './hosts'
6+
import type { session } from './session'
67
import { getSession } from './session'
78
import type { Storage } from './storage'
9+
import * as WebIdOidc from './webid-oidc'
810

9-
export const authnFetch = (storage: Storage): (url: RequestInfo, options?: Object) => Promise<Response> =>
10-
(url: RequestInfo, options?: Object) =>
11-
fetch(url, options)
12-
.then(resp => {
13-
if (resp.status === 401 && requiresWebIdOidc(resp.headers.get('www-authenticate'))) {
14-
const session = getSession(storage)
15-
if (session && session.type === 'WebID-OIDC') {
16-
const retryOptions = {
17-
...options,
18-
headers: {
19-
...(options && options.headers ? options.headers : {}),
20-
authorization: `Bearer ${session.idToken}`
21-
}
22-
}
23-
return fetch(url, retryOptions)
24-
}
11+
export const authnFetch = (storage: Storage) => (url: RequestInfo, options?: Object): Promise<Response> => {
12+
const session = getSession(storage)
13+
if (session && shouldShareCredentials(storage)(url)) {
14+
return fetchWithCredentials(session, url, options)
15+
}
16+
return fetch(url, options)
17+
.then((resp) => {
18+
if (resp.status === 401) {
19+
updateHostFromResponse(storage)(resp)
20+
if (session && shouldShareCredentials(storage)(url)) {
21+
return fetchWithCredentials(session, url, options)
2522
}
26-
return resp
27-
})
23+
}
24+
return resp
25+
})
26+
}
27+
28+
const shouldShareCredentials = (storage: Storage) => (url: RequestInfo): boolean => {
29+
const session = getSession(storage)
30+
if (!session) {
31+
return false
32+
}
33+
const requestHost = getHost(storage)(url)
34+
return requestHost != null &&
35+
session.authType === requestHost.authType
36+
}
2837

29-
const requiresWebIdOidc = (wwwAuthHeader: ?string): boolean => {
30-
if (!wwwAuthHeader) { return false }
31-
const auth = authorization.parse(wwwAuthHeader)
32-
return (auth.scheme === 'Bearer') &&
33-
auth.params && auth.params.scope === 'openid'
38+
const fetchWithCredentials = (session: session, url: RequestInfo, options?: Object): Promise<Response> => {
39+
switch (session.authType) {
40+
case 'WebID-OIDC':
41+
return WebIdOidc.fetchWithCredentials(session)(url, options)
42+
case 'WebID-TLS':
43+
default:
44+
return fetch(url, options)
45+
}
3446
}

src/hosts.js

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
// @flow
2+
/* global RequestInfo, Request, Response, URL */
3+
import { getSession } from './session'
4+
import type { Storage } from './storage'
5+
import { getData, updateStorage } from './storage'
6+
import type { Auth } from './types'
7+
import * as WebIdOidc from './webid-oidc'
8+
import * as WebIdTls from './webid-tls'
9+
10+
export type host =
11+
{ authType: Auth
12+
, url: string
13+
}
14+
15+
export const hostNameFromRequestInfo = (url: RequestInfo): string => {
16+
const _url = url instanceof URL
17+
? url
18+
: url instanceof Request
19+
? new URL(url.url)
20+
: new URL(url)
21+
return _url.host
22+
}
23+
24+
export const getHost = (storage: Storage) => (url: RequestInfo): ?host => {
25+
const requestHostName = hostNameFromRequestInfo(url)
26+
const session = getSession(storage)
27+
if (session && hostNameFromRequestInfo(session.idp) === requestHostName) {
28+
return { url: requestHostName, authType: session.authType }
29+
}
30+
const { hosts } = getData(storage)
31+
if (!hosts) {
32+
return null
33+
}
34+
return hosts[requestHostName] || null
35+
}
36+
37+
export const saveHost = (storage: Storage) => ({ url, authType }: host): host => {
38+
updateStorage(storage, (data) => ({
39+
...data,
40+
hosts: {
41+
...data.hosts,
42+
[url]: { authType }
43+
}
44+
}))
45+
return { url, authType }
46+
}
47+
48+
export const updateHostFromResponse = (storage: Storage) => (resp: Response): void => {
49+
let authType
50+
if (WebIdOidc.requiresAuth(resp)) {
51+
authType = 'WebID-OIDC'
52+
} else if (WebIdTls.requiresAuth(resp)) {
53+
authType = 'WebID-TLS'
54+
} else {
55+
authType = null
56+
}
57+
58+
const hostName = hostNameFromRequestInfo(resp.url)
59+
if (authType) {
60+
saveHost(storage)({ url: hostName, authType })
61+
}
62+
}

src/session.js

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
11
// @flow
2+
23
import type { Storage } from './storage'
34
import { getData, updateStorage } from './storage'
5+
import type { WebIdTls, WebIdOidc } from './types'
46

57
export type webIdTlsSession =
6-
{ type: 'WebID-TLS'
8+
{ authType: WebIdTls
79
, idp: string
810
, webId: string
911
}
1012

1113
export type webIdOidcSession =
12-
{ type: 'WebID-OIDC'
14+
{ authType: WebIdOidc
1315
, idp: string
1416
, webId: string
1517
, accessToken: string
@@ -23,7 +25,7 @@ export type session =
2325
export const getSession = (storage: Storage): ?session =>
2426
getData(storage).session || null
2527

26-
export const saveSession = (storage: Storage, session: session): session =>
28+
export const saveSession = (storage: Storage) => (session: session): session =>
2729
updateStorage(storage, data => ({ ...data, session })).session
2830

2931
export const clearSession = (storage: Storage): void => {

src/types.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// @flow
2+
3+
// Canonical auth protocol names
4+
export type WebIdOidc =
5+
'WebID-OIDC'
6+
7+
export type WebIdTls =
8+
'WebID-TLS'
9+
10+
export type Auth =
11+
| WebIdOidc
12+
| WebIdTls

0 commit comments

Comments
 (0)