UNPKG

jscas-server

Version:

An implementation of Apereo's CAS protocol

792 lines (714 loc) 17.4 kB
'use strict' const test = require('tap').test const clone = require('clone') const nullLogger = require('../../../nullLogger') const plugin = require('../../../../lib/routes/login') const serverProto = { jscasPlugins: { theme: {}, auth: [] }, jscasHooks: { preAuth: [] }, jscasInterface: {}, jscasTGTCookie: 'tgt-cookie', get: function () {}, post: function (path, handler) { this.postLogin = handler } } test('returns unknown service page', (t) => { t.plan(5) const server = clone(serverProto) server.jscasInterface = { getService: async function (serviceUrl) { t.is(serviceUrl, 'http://example.com') return undefined } } server.jscasPlugins.theme = { unknownService (serviceUrl) { t.is(serviceUrl, 'http://example.com') return 'unknown service' } } plugin(server, {}, () => { const req = { body: {service: 'http://example.com'}, session: {csrfToken: 'csrf123', renewal: false}, log: nullLogger } const reply = { type (val) { t.is(val, 'text/html') return this }, code (num) { t.is(num, 406) return this } } server.postLogin(req, reply) .then((result) => { t.is(result, 'unknown service') }) .catch(t.threw) }) }) test('redirects to login for invalid csrf token', (t) => { t.plan(6) const server = clone(serverProto) server.jscasInterface = { getService: async function (serviceUrl) { t.is(serviceUrl, 'http://example.com') return {name: 'foo', url: serviceUrl} } } plugin(server, {}, () => { const req = { body: { service: 'http://example.com', csrfToken: 'csrf-invalid' }, session: {renewal: false}, log: nullLogger, isValidCsrfToken (token) { t.is(token, 'csrf-invalid') return false } } const reply = { type (val) { t.is(val, 'text/html') return this }, code (num) { t.is(num, 302) return this }, redirect (url) { t.is(url, '/login?service=' + encodeURIComponent('http://example.com')) return 'redirect' } } server.postLogin(req, reply) .then((result) => { t.is(result, 'redirect') }) .catch(t.threw) }) }) test('redirects when no authenticators are registered', (t) => { t.plan(6) const server = clone(serverProto) server.jscasInterface = { getService: async function (serviceUrl) { t.is(serviceUrl, 'http://example.com') return {name: 'foo', url: serviceUrl} } } plugin(server, {}, () => { const req = { body: { service: 'http://example.com', csrfToken: 'csrf123' }, session: {renewal: false}, log: nullLogger, isValidCsrfToken (token) { t.is(token, 'csrf123') return true } } const reply = { type (val) { t.is(val, 'text/html') return this }, code (num) { t.is(num, 302) return this }, redirect (url) { t.is(url, '/login?service=' + encodeURIComponent('http://example.com')) return 'redirect' } } server.postLogin(req, reply) .then((result) => { t.is(result, 'redirect') }) .catch(t.threw) }) }) test('redirects when authentication fails', (t) => { t.plan(8) const server = clone(serverProto) server.jscasInterface = { getService: async function (serviceUrl) { t.is(serviceUrl, 'http://example.com') return {name: 'foo', url: serviceUrl} } } server.jscasPlugins.auth.push({ validate: async function (username, password) { t.is(username, 'foo') t.is(password, '654321') return false } }) plugin(server, {}, () => { const req = { body: { service: 'http://example.com', csrfToken: 'csrf123', username: 'foo', password: '654321' }, session: {renewal: false}, log: nullLogger, isValidCsrfToken (token) { t.is(token, 'csrf123') return true } } const reply = { type (val) { t.is(val, 'text/html') return this }, code (num) { t.is(num, 302) return this }, redirect (url) { t.is(url, '/login?service=' + encodeURIComponent('http://example.com')) return 'redirect' } } server.postLogin(req, reply) .then((result) => { t.is(result, 'redirect') }) .catch(t.threw) }) }) test('redirects when authentication fails with rejection', (t) => { t.plan(8) const server = clone(serverProto) server.jscasInterface = { getService: async function (serviceUrl) { t.is(serviceUrl, 'http://example.com') return {name: 'foo', url: serviceUrl} } } server.jscasPlugins.auth.push({ validate: async function (username, password) { t.is(username, 'foo') t.is(password, '654321') throw Error('rejecting for fun') } }) plugin(server, {}, () => { const req = { body: { service: 'http://example.com', csrfToken: 'csrf123', username: 'foo', password: '654321' }, session: {renewal: false}, log: nullLogger, isValidCsrfToken (token) { t.is(token, 'csrf123') return true } } const reply = { type (val) { t.is(val, 'text/html') return this }, code (num) { t.is(num, 302) return this }, redirect (url) { t.is(url, '/login?service=' + encodeURIComponent('http://example.com')) return 'redirect' } } server.postLogin(req, reply) .then((result) => { t.is(result, 'redirect') }) .catch(t.threw) }) }) test('successfully authenticates and redirects for non-renewal', (t) => { t.plan(15) const server = clone(serverProto) server.jscasInterface = { createServiceTicket: async function (tgtId, name) { t.is(tgtId, '123456') t.is(name, 'foo') return {tid: '67890'} }, createTicketGrantingTicket: async function (username) { t.is(username, 'foo') return {tid: '123456'} }, getService: async function (serviceUrl) { t.is(serviceUrl, 'http://example.com') return {name: 'foo', url: serviceUrl} } } server.jscasPlugins.auth.push({ validate: async function (username, password) { t.is(username, 'foo') t.is(password, '123456') return true } }) plugin(server, {cookie: {a: 'b', expires: 1000}}, () => { const req = { body: { service: 'http://example.com', csrfToken: 'csrf123', username: 'foo', password: '123456' }, session: {renewal: false}, log: nullLogger, isValidCsrfToken (token) { t.is(token, 'csrf123') return true } } const reply = { type (val) { t.is(val, 'text/html') return this }, code (num) { t.is(num, 303) return this }, redirect (url) { t.is(url, 'http://example.com?ticket=67890') t.is(req.session.isAuthenticated, true) return 'redirect' }, setCookie (name, value, options) { t.is(name, 'tgt-cookie') t.is(value, '123456') t.is(options.a, 'b') return this } } server.postLogin(req, reply) .then((result) => { t.is(result, 'redirect') }) .catch(t.threw) }) }) test('successfully authenticates and redirects for renewal', (t) => { t.plan(15) const server = clone(serverProto) server.jscasInterface = { createServiceTicket: async function (tgtId, name) { t.is(tgtId, '123456') t.is(name, 'foo') return {tid: '67890'} }, getService: async function (serviceUrl) { t.is(serviceUrl, 'http://example.com') return {name: 'foo', url: serviceUrl} }, getTicketGrantingTicket: async function (tid) { t.is(tid, '123456') return {tid} } } server.jscasPlugins.auth.push({ validate: async function (username, password) { t.is(username, 'foo') t.is(password, '123456') return true } }) plugin(server, {cookie: {a: 'b', expires: 1000}}, () => { const req = { body: { service: 'http://example.com', csrfToken: 'csrf123', username: 'foo', password: '123456' }, cookies: { 'tgt-cookie': '123456' }, session: {renewal: true}, log: nullLogger, isValidCsrfToken (token) { t.is(token, 'csrf123') return true } } const reply = { type (val) { t.is(val, 'text/html') return this }, code (num) { t.is(num, 303) return this }, redirect (url) { t.is(url, 'http://example.com?ticket=67890') t.is(req.session.isAuthenticated, true) return 'redirect' }, setCookie (name, value, options) { t.is(name, 'tgt-cookie') t.is(value, '123456') t.is(options.a, 'b') return this } } server.postLogin(req, reply) .then((result) => { t.is(result, 'redirect') }) .catch(t.threw) }) }) test('successfully authenticates and redirects for SAML authentications', (t) => { t.plan(15) const server = clone(serverProto) server.jscasInterface = { createServiceTicket: async function (tgtId, name) { t.is(tgtId, '123456') t.is(name, 'foo') return {tid: '67890'} }, createTicketGrantingTicket: async function (username) { t.is(username, 'foo') return {tid: '123456'} }, getService: async function (serviceUrl) { t.is(serviceUrl, 'http://example.com') return {name: 'foo', url: serviceUrl} } } server.jscasPlugins.auth.push({ validate: async function (username, password) { t.is(username, 'foo') t.is(password, '123456') return true } }) plugin(server, {cookie: {a: 'b', expires: 1000}}, () => { const req = { body: { service: 'http://example.com', csrfToken: 'csrf123', username: 'foo', password: '123456' }, session: { renewal: false, samlConversation: true }, log: nullLogger, isValidCsrfToken (token) { t.is(token, 'csrf123') return true } } const reply = { type (val) { t.is(val, 'text/html') return this }, code (num) { t.is(num, 303) return this }, redirect (url) { t.is(url, 'http://example.com?SAMLart=67890') t.is(req.session.isAuthenticated, true) return 'redirect' }, setCookie (name, value, options) { t.is(name, 'tgt-cookie') t.is(value, '123456') t.is(options.a, 'b') return this } } server.postLogin(req, reply) .then((result) => { t.is(result, 'redirect') }) .catch(t.threw) }) }) test('returns server error when ticket access fails', (t) => { t.plan(11) const server = clone(serverProto) server.jscasInterface = { getService: async function (serviceUrl) { t.is(serviceUrl, 'http://example.com') return {name: 'foo', url: serviceUrl} }, getTicketGrantingTicket: async function (tid) { t.is(tid, '123456') throw Error('simulating broken ticket registry') } } server.jscasPlugins.auth.push({ validate: async function (username, password) { t.is(username, 'foo') t.is(password, '123456') return true } }) server.jscasPlugins.theme = { serverError (context) { t.type(context, Object) t.ok(context.error) t.match(context.error, /simulating/) return 'server error' } } plugin(server, {cookie: {a: 'b'}}, () => { const req = { body: { service: 'http://example.com', csrfToken: 'csrf123', username: 'foo', password: '123456' }, cookies: { 'tgt-cookie': '123456' }, session: {renewal: true}, log: nullLogger, isValidCsrfToken (token) { t.is(token, 'csrf123') return true } } const reply = { type (val) { t.is(val, 'text/html') return this }, code (num) { t.is(num, 500) return this } } server.postLogin(req, reply) .then((result) => { t.is(result, 'server error') }) .catch(t.threw) }) }) test('redirects to /login with no service and bad credentials', (t) => { t.plan(7) const server = clone(serverProto) server.jscasPlugins.auth.push({ validate: async function (username, password) { t.is(username, 'foo') t.is(password, '654321') return false } }) plugin(server, {}, () => { const req = { body: { csrfToken: 'csrf123', username: 'foo', password: '654321' }, session: {renewal: false}, log: nullLogger, isValidCsrfToken (token) { t.is(token, 'csrf123') return true } } const reply = { type (val) { t.is(val, 'text/html') return this }, code (num) { t.is(num, 302) return this }, redirect (url) { t.is(url, '/login') return 'redirect' } } server.postLogin(req, reply) .then((result) => { t.is(result, 'redirect') }) .catch(t.threw) }) }) test('redirects to /success with no service and good credentials', (t) => { t.plan(10) const options = { cookie: { expires: 1000 } } const server = clone(serverProto) server.jscasInterface = { createTicketGrantingTicket: async function (username) { t.is(username, 'foo') return {tid: '123456', expires: new Date(Date.now() + 1000)} } } server.jscasPlugins.auth.push({ validate: async function (username, password) { t.is(username, 'foo') t.is(password, '654321') return true } }) plugin(server, options, () => { const req = { body: { csrfToken: 'csrf123', username: 'foo', password: '654321' }, session: {renewal: false}, log: nullLogger, isValidCsrfToken (token) { t.is(token, 'csrf123') return true } } const reply = { type (val) { t.is(val, 'text/html') return this }, code (num) { t.is(num, 303) return this }, redirect (url) { t.is(url, '/success') return 'redirect' }, setCookie (name, value, options) { t.is(name, 'tgt-cookie') t.is(value, '123456') return this } } server.postLogin(req, reply) .then((result) => { t.is(result, 'redirect') }) .catch(t.threw) }) }) test('processes registered preAuth hooks', {only: true}, (t) => { t.plan(17) const options = { cookie: { expires: 1000 } } const server = clone(serverProto) server.jscasInterface = { createServiceTicket: async function (tgtId, name) { t.is(tgtId, '123456') t.is(name, 'foo') return {tid: '67890'} }, createTicketGrantingTicket: async function (username) { t.is(username, 'foo') return {tid: '123456'} }, getService: async function (serviceUrl) { t.is(serviceUrl, 'http://example.com') return {name: 'foo', url: serviceUrl} } } server.jscasPlugins.auth.push({ validate: async function (username, password) { t.is(username, 'foo') t.is(password, '654321') return true } }) // successful hook async function successHook ({username, password, serviceUrl, session}) { t.is(username, 'foo') t.is(password, '654321') t.is(serviceUrl, 'http://example.com') return true } successHook[Symbol.for('jscas-hook-id')] = 1 server.jscasHooks.preAuth.push(successHook) // unsuccessful hook (should continue login) async function failHook () { t.is(1, 1) throw Error('ignored error') } failHook[Symbol.for('jscas-hook-id')] = 2 server.jscasHooks.preAuth.push(failHook) plugin(server, options, () => { const req = { body: { service: 'http://example.com', csrfToken: 'csrf123', username: 'foo', password: '654321' }, session: {renewal: false}, log: nullLogger, isValidCsrfToken (token) { t.is(token, 'csrf123') return true } } const reply = { type (val) { t.is(val, 'text/html') return this }, code (num) { t.is(num, 303) return this }, redirect (url) { t.is(url, 'http://example.com?ticket=67890') return 'redirect' }, setCookie (name, value, options) { t.is(name, 'tgt-cookie') t.is(value, '123456') return this } } server.postLogin(req, reply) .then((result) => { t.is(result, 'redirect') }) .catch(t.threw) }) })