@solid/oidc-auth-manager
Version:
An OpenID Connect (OIDC) authentication manager (OP, RP and RS) for decentralized peer-to-peer authentication
364 lines (289 loc) • 11.4 kB
JavaScript
'use strict'
const chai = require('chai')
const nock = require('nock')
const chaiAsPromised = require('chai-as-promised')
chai.use(chaiAsPromised)
const dirtyChai = require('dirty-chai')
chai.use(dirtyChai)
const expect = chai.expect
const sinon = require('sinon')
const sinonChai = require('sinon-chai')
chai.use(sinonChai)
chai.should()
const OidcManager = require('../../src/oidc-manager')
const sampleProfileSrc = require('../resources/sample-webid-profile')
describe('OidcManager', () => {
afterEach(() => {
nock.cleanAll()
})
describe('from()', () => {
it('should create an OidcManager instance from config', () => {
const providerUri = 'https://localhost:8443'
const dbPath = './db/oidc'
const saltRounds = 5
const host = {}
const authCallbackUri = providerUri + '/api/oidc/rp'
const postLogoutUri = providerUri + '/goodbye'
const options = {
serverUri: providerUri,
providerUri,
dbPath,
host,
saltRounds,
authCallbackUri,
postLogoutUri
}
const oidc = OidcManager.from(options)
expect(oidc.providerUri).to.equal(providerUri)
expect(oidc.serverUri).to.equal(providerUri)
expect(oidc.host).to.equal(host)
expect(oidc.saltRounds).to.equal(saltRounds)
expect(oidc.authCallbackUri).to.equal(authCallbackUri)
expect(oidc.postLogoutUri).to.equal(postLogoutUri)
const storePaths = oidc.storePaths
expect(storePaths.providerStore.endsWith('oidc/op'))
expect(storePaths.multiRpStore.endsWith('oidc/rp'))
expect(storePaths.userStore.endsWith('oidc/users'))
expect(oidc.rs).to.exist()
expect(oidc.clients).to.exist()
expect(oidc.users).to.exist()
expect(oidc.provider).to.exist()
})
})
describe('initMultiRpClient()', () => {
it('should initialize a Multi RP Client Store instance', () => {
const providerUri = 'https://localhost:8443'
const authCallbackUri = providerUri + '/api/oidc/rp'
const postLogoutUri = providerUri + '/goodbye'
const dbPath = './db/oidc-mgr'
const config = {
serverUri: providerUri,
providerUri,
authCallbackUri,
postLogoutUri,
dbPath
}
const oidc = OidcManager.from(config)
oidc.initMultiRpClient()
const clientStore = oidc.clients
expect(clientStore.store.backend.path.endsWith('oidc-mgr/rp/clients'))
expect(clientStore).to.respondTo('registerClient')
})
})
describe('initRs()', () => {
it('should initialize a Resource Authenticator instance', () => {
const serverUri = 'https://localhost:8443'
const authCallbackUri = serverUri + '/api/oidc/rp'
const postLogoutUri = serverUri + '/goodbye'
const config = { serverUri, providerUri: serverUri, authCallbackUri, postLogoutUri }
const oidc = OidcManager.from(config)
oidc.initRs()
expect(oidc.rs.defaults.query).to.be.true()
expect(oidc.rs.defaults.realm).to.equal(serverUri)
expect(oidc.rs).to.respondTo('authenticate')
})
})
describe('initUserStore()', () => {
it('should initialize a UserStore instance', () => {
const dbPath = './db/oidc-mgr'
const providerUri = 'https://localhost:8443'
const authCallbackUri = providerUri + '/api/oidc/rp'
const postLogoutUri = providerUri + '/goodbye'
const config = {
providerUri,
authCallbackUri,
postLogoutUri,
saltRounds: 5,
dbPath
}
const oidc = OidcManager.from(config)
oidc.initUserStore()
expect(oidc.users.backend.path.endsWith('oidc-mgr/users'))
expect(oidc.users.saltRounds).to.equal(config.saltRounds)
})
})
describe('initProvider()', () => {
it('should initialize an OIDC Provider instance', () => {
const providerUri = 'https://localhost:8443'
const authCallbackUri = providerUri + '/api/oidc/rp'
const postLogoutUri = providerUri + '/goodbye'
const host = {
authenticate: () => {},
obtainConsent: () => {},
logout: () => {}
}
const dbPath = './db/oidc-mgr'
const config = { providerUri, host, dbPath, authCallbackUri, postLogoutUri }
const oidc = OidcManager.from(config)
const loadProviderConfig = sinon.spy(oidc, 'loadProviderConfig')
oidc.initProvider()
expect(oidc.provider.issuer).to.equal(providerUri)
const storePath = oidc.provider.backend.path
expect(storePath.endsWith('oidc-mgr/op')).to.be.true()
expect(oidc.provider.host.authenticate).to.equal(host.authenticate)
expect(loadProviderConfig).to.have.been.called()
})
})
describe('providerConfigPath()', () => {
it('should return the Provider config file path', () => {
const providerUri = 'https://localhost:8443'
const authCallbackUri = providerUri + '/api/oidc/rp'
const postLogoutUri = providerUri + '/goodbye'
const dbPath = './db/oidc-mgr'
const config = { dbPath, providerUri, authCallbackUri, postLogoutUri }
const oidc = OidcManager.from(config)
const file = oidc.providerConfigPath()
expect(file.endsWith('oidc-mgr/op/provider.json')).to.be.true()
})
})
describe('extractWebId()', () => {
const aliceWebId = 'https://alice.example.com/#me'
let claims
beforeEach(() => {
claims = {
iss: 'https://example.com',
sub: 'abcd'
}
})
it('should throw an error for null claims', () => {
expect(() => OidcManager.extractWebId(null))
.to.throw(/Cannot extract Web ID from missing claims/)
})
it('should throw an error if issuer claim is not present', () => {
delete claims.iss
expect(() => OidcManager.extractWebId(claims))
.to.throw(/Cannot extract Web ID - missing issuer claim/)
})
it('should first look in the webid claim', () => {
claims.webid = aliceWebId
const webId = OidcManager.extractWebId(claims)
expect(webId).to.equal(aliceWebId)
})
it('should use the sub claim if it contains an http* uri', () => {
claims.sub = aliceWebId
const webId = OidcManager.extractWebId(claims)
expect(webId).to.equal(aliceWebId)
})
it('should throw an error if no webid claim and sub claim is invalid uri', () => {
claims.sub = 'invalid uri'
expect(() => OidcManager.extractWebId(claims))
.to.throw(/Cannot extract Web ID - subject claim is not a valid URI/)
})
it('should throw if none of the above claims are found', () => {
delete claims.sub
expect(() => OidcManager.extractWebId(claims))
.to.throw(/Cannot extract Web ID - no webid or subject claim/)
})
})
describe('webIdFromClaims', () => {
const providerUri = 'https://example.com'
const authCallbackUri = providerUri + '/api/oidc/rp'
const postLogoutUri = providerUri + '/goodbye'
const config = { providerUri, authCallbackUri, postLogoutUri }
const oidc = OidcManager.from(config)
it('should resolve with null webid with missing claims', () => {
return oidc.webIdFromClaims(null)
.then(webId => {
expect(webId).to.be.null()
})
})
it('should skip verifying preferred provider if webid and issuer match', () => {
const claims = {
iss: 'https://example.com',
sub: 'https://example.com/profile#me'
}
return oidc.webIdFromClaims(claims)
.then(webId => {
expect(webId).to.equal(claims.sub)
})
})
it('should verify provider if webid and issuer do not match', () => {
const claims = {
iss: 'https://provider.com',
sub: 'https://example.com/profile#me'
}
nock('https://example.com')
.get('/profile')
.reply(200, sampleProfileSrc)
nock('https://example.com')
.options('/profile')
.reply(204, 'No content', {
Link: '<https://provider.com>; rel="http://openid.net/specs/connect/1.0/issuer"'
})
return oidc.webIdFromClaims(claims)
.then(webId => {
expect(webId).to.equal(claims.sub)
})
})
it('should throw an error if provider could not be verified', done => {
const claims = {
iss: 'https://provider.com',
sub: 'https://example.com/profile#me'
}
nock('https://example.com')
.get('/profile')
.reply(200, sampleProfileSrc)
nock('https://example.com')
.options('/profile')
.reply(204, 'No content', {
Link: '<https://another-provider.com>; rel="http://openid.net/specs/connect/1.0/issuer"'
})
oidc.webIdFromClaims(claims)
.catch(err => {
expect(err).to.match(/Preferred provider for Web ID https:\/\/example.com\/profile#me does not match token issuer https:\/\/provider.com/)
done()
})
})
})
describe('domainMatches', () => {
it('should be true if webid came from same origin as issuer', () => {
const webId = 'https://alice.example.com/profile#me'
const issuer = 'https://alice.example.com'
expect(OidcManager.domainMatches(issuer, webId)).to.be.true()
})
it('should be false if webid is from different domain as issuer', () => {
const webId = 'https://example.com/#me'
const issuer = 'https://provider.com'
expect(OidcManager.domainMatches(issuer, webId)).to.be.false()
})
it('should be true if webid origin is subdomain of issuer', () => {
const webId = 'https://alice.example.com/profile#me'
const issuer = 'https://example.com'
expect(OidcManager.domainMatches(issuer, webId)).to.be.true()
})
it('should be false if webid and issuer protocols do not match', () => {
const webId = 'http://example.com/#me'
const issuer = 'https://example.com'
expect(OidcManager.domainMatches(issuer, webId)).to.be.false()
})
it('should be false if webid and issuer ports do not match', () => {
const webId = 'https://example.com/#me'
const issuer = 'https://example.com:8080'
expect(OidcManager.domainMatches(issuer, webId)).to.be.false()
})
})
describe('filterAudience', () => {
const providerUri = 'https://example.com'
const authCallbackUri = providerUri + '/api/oidc/rp'
const postLogoutUri = providerUri + '/goodbye'
const config = { providerUri, authCallbackUri, postLogoutUri }
const oidc = OidcManager.from(config)
it('should be false if no audience passed in', () => {
expect(oidc.filterAudience(undefined)).to.be.false()
})
it('should be true if audience origin equals server uri', () => {
expect(oidc.filterAudience('https://example.com/test')).to.be.true()
})
it('should be true if server uri is one of the audience matches', () => {
expect(oidc.filterAudience(['https://example.com/test', 'https://other.com']))
.to.be.true()
})
it('should be true if audience is a subdomain of server uri', () => {
expect(oidc.filterAudience('https://alice.example.com/profile#me'))
.to.be.true()
})
it('should be false if audience is a different domain than server', () => {
expect(oidc.filterAudience('https://other.com')).to.be.false()
})
})
})