Skip to content

Commit 1a3e390

Browse files
authored
Merge pull request #1180 from jembi/PLAT-638-adjust-login-cookies
PLAT 638 Adjust login wih session and cookies
2 parents 5ece1a1 + ab76407 commit 1a3e390

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

50 files changed

+7092
-5112
lines changed

.eslintrc.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
"es6": true,
44
"node": true
55
},
6+
"parser": "@babel/eslint-parser",
67
"parserOptions": {
78
"ecmaVersion": 2017,
89
"sourceType": "module"

config/config.md

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,13 @@ The following config option are provided by the OpenHIM. All of these options ha
4141
"timeout": 60000
4242
},
4343
"api": {
44+
// The session secret key used for the hashing of signed cookie (used to detect if the client modified the cookie)
45+
// Signed cookie is another cookie of the same name with the .sig suffix appended
46+
"sessionKey": "r8q,+&1LM3)CD*zAGpx1xm{NeQhc;#",
47+
// The session max age is the session cookie expiration time (in milliseconds)
48+
"maxAge": 7200000,
49+
// The number of characters that will be used to generate a random salt for the encryption of passwords
50+
"salt": 10,
4451
// The port that the OpenHIM API uses
4552
"port": 8080,
4653
// The protocol that the OpenHIM API uses
@@ -60,7 +67,11 @@ The following config option are provided by the OpenHIM. All of these options ha
6067
// A message to append to detail strings that have been truncated
6168
"truncateAppend": "\n[truncated ...]",
6269
// The types of authentication to use for the API
63-
// Supported types are "token" and "basic"
70+
// Supported types are "token" and "basic" and "local"
71+
// * "local" means through the UI with hitting "/authentication/local" endpoint with username and password,
72+
// this will create a session for the user and set cookies in the browser.
73+
// * "basic" means with basic auth either through browser or postman by giving also username and password.
74+
// * [Deprecated] "token" means that a request should provide in the header an 'auth-token', 'auth-salt' and 'auth-ts' to be authenticated.
6475
"authenicationTypes": ["token"]
6576
},
6677
"rerun": {

config/default.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@
3131
"pollPeriodMins": 60
3232
},
3333
"api": {
34+
"sessionKey": "r8q,+&1LM3)CD*zAGpx1xm{NeQhc;#",
35+
"maxAge": 7200000,
36+
"salt": 10,
3437
"enabled": true,
3538
"protocol": "https",
3639
"port": 8080,
@@ -39,7 +42,7 @@
3942
"maxPayloadSizeMB": 50,
4043
"truncateSize": 15000,
4144
"truncateAppend": "\n[truncated ...]",
42-
"authenticationTypes": ["basic", "token"]
45+
"authenticationTypes": ["basic", "local", "token"]
4346
},
4447
"rerun": {
4548
"httpPort": 7786,

config/test.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
"maxPayloadSizeMB": 50,
2727
"truncateSize": 10,
2828
"truncateAppend": "\n[truncated ...]",
29-
"authenticationTypes": ["token", "basic"]
29+
"authenticationTypes": ["token", "basic", "local"]
3030
},
3131
"caching": {
3232
"enabled": false

package-lock.json

Lines changed: 2769 additions & 2816 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
"test:seed": "node performance/seed.js",
3737
"test:seed:ci": "npm run test:seed -- --quiet",
3838
"start": "node lib/server.js",
39+
"start:dev": "nodemon lib/server.js",
3940
"stop": "pkill -SIGINT Core",
4041
"spec": "speculate"
4142
},
@@ -56,8 +57,11 @@
5657
"kcors": "2.2.2",
5758
"koa": "^2.13.0",
5859
"koa-bodyparser": "^4.3.0",
60+
"koa-compose": "^4.1.0",
5961
"koa-compress": "^5.1.0",
62+
"koa-passport": "^4.0.0",
6063
"koa-route": "3.2.0",
64+
"koa-session": "^6.3.1",
6165
"lodash": "^4.17.20",
6266
"moment": "^2.29.1",
6367
"moment-timezone": "^0.5.31",
@@ -67,6 +71,9 @@
6771
"mongoose-patch-history": "^2.0.0",
6872
"nconf": "0.10.0",
6973
"nodemailer": "^6.6.3",
74+
"passport-custom": "^1.1.1",
75+
"passport-http": "^0.3.0",
76+
"passport-local": "^1.0.0",
7077
"pem": "^1.14.4",
7178
"raw-body": "^2.4.1",
7279
"semver": "^7.3.2",
@@ -81,6 +88,7 @@
8188
"devDependencies": {
8289
"@babel/cli": "^7.15.4",
8390
"@babel/core": "^7.15.5",
91+
"@babel/eslint-parser": "^7.19.1",
8492
"@babel/preset-env": "^7.15.6",
8593
"@babel/register": "^7.15.3",
8694
"codecov": "^3.8.3",
@@ -94,6 +102,7 @@
94102
"faker": "^5.5.3",
95103
"finalhandler": "^1.1.2",
96104
"mocha": "^8.4.0",
105+
"nodemon": "^2.0.20",
97106
"nyc": "^15.1.0",
98107
"prettier": "^2.4.0",
99108
"progress": "2.0.3",

src/api/authentication.js

Lines changed: 73 additions & 145 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,13 @@
11
'use strict'
22

33
import atna from 'atna-audit'
4-
import basicAuth from 'basic-auth'
5-
import crypto from 'crypto'
64
import logger from 'winston'
75
import os from 'os'
86

97
import * as auditing from '../auditing'
108
import * as authorisation from './authorisation'
11-
import {UserModelAPI} from '../model/users'
12-
import {caseInsensitiveRegex, logAndSetResponse} from '../utils'
9+
import passport from '../passport'
10+
import {logAndSetResponse} from '../utils'
1311
import {config} from '../config'
1412
import {
1513
BASIC_AUTH_TYPE,
@@ -36,108 +34,26 @@ const auditingExemptPaths = [
3634
/\/logs/
3735
]
3836

39-
const isUndefOrEmpty = string => string == null || string === ''
40-
4137
async function authenticateBasic(ctx) {
42-
const credentials = basicAuth(ctx)
43-
if (credentials == null) {
44-
// No basic auth details found
45-
return null
46-
}
47-
const {name: email, pass: password} = credentials
48-
const user = await UserModelAPI.findOne({
49-
email: caseInsensitiveRegex(email)
50-
})
51-
if (user == null) {
52-
// not authenticated - user not found
53-
ctx.throw(
54-
401,
55-
`No user exists for ${email}, denying access to API, request originated from ${ctx.request.host}`,
56-
{email}
57-
)
58-
}
38+
// Basic auth using middleware
39+
await passport.authenticate('basic', function (err, user) {
40+
if (user) {
41+
ctx.req.user = user
42+
ctx.body = 'User Authenticated Successfully'
43+
ctx.status = 200
44+
}
45+
})(ctx, () => {})
5946

60-
const hash = crypto.createHash(user.passwordAlgorithm)
61-
hash.update(user.passwordSalt)
62-
hash.update(password)
63-
if (user.passwordHash !== hash.digest('hex')) {
64-
// not authenticated - password mismatch
65-
ctx.throw(
66-
401,
67-
`Password did not match expected value, denying access to API, the request was made by ${email} from ${ctx.request.host}`,
68-
{email}
69-
)
70-
}
71-
return user
47+
return ctx.req.user || null
7248
}
7349

50+
/**
51+
* @deprecated
52+
*/
7453
async function authenticateToken(ctx) {
75-
const {header} = ctx.request
76-
const email = header['auth-username']
77-
const authTS = header['auth-ts']
78-
const authSalt = header['auth-salt']
79-
const authToken = header['auth-token']
54+
await passport.authenticate('token')(ctx, () => {})
8055

81-
// if any of the required headers aren't present
82-
if (
83-
isUndefOrEmpty(email) ||
84-
isUndefOrEmpty(authTS) ||
85-
isUndefOrEmpty(authSalt) ||
86-
isUndefOrEmpty(authToken)
87-
) {
88-
ctx.throw(
89-
401,
90-
`API request made by ${email} from ${ctx.request.host} is missing required API authentication headers, denying access`,
91-
{email}
92-
)
93-
}
94-
95-
// check if request is recent
96-
const requestDate = new Date(Date.parse(authTS))
97-
98-
const authWindowSeconds =
99-
config.api.authWindowSeconds != null ? config.api.authWindowSeconds : 10
100-
const to = new Date()
101-
to.setSeconds(to.getSeconds() + authWindowSeconds)
102-
const from = new Date()
103-
from.setSeconds(from.getSeconds() - authWindowSeconds)
104-
105-
if (requestDate < from || requestDate > to) {
106-
// request expired
107-
ctx.throw(
108-
401,
109-
`API request made by ${email} from ${ctx.request.host} has expired, denying access`,
110-
{email}
111-
)
112-
}
113-
114-
const user = await UserModelAPI.findOne({
115-
email: caseInsensitiveRegex(email)
116-
})
117-
if (user == null) {
118-
// not authenticated - user not found
119-
ctx.throw(
120-
401,
121-
`No user exists for ${email}, denying access to API, request originated from ${ctx.request.host}`,
122-
{email}
123-
)
124-
}
125-
126-
const hash = crypto.createHash('sha512')
127-
hash.update(user.passwordHash)
128-
hash.update(authSalt)
129-
hash.update(authTS)
130-
131-
if (authToken !== hash.digest('hex')) {
132-
// not authenticated - token mismatch
133-
ctx.throw(
134-
401,
135-
`API token did not match expected value, denying access to API, the request was made by ${email} from ${ctx.request.host}`,
136-
{email}
137-
)
138-
}
139-
140-
return user
56+
return ctx.req.user || null
14157
}
14258

14359
function getEnabledAuthenticationTypesFromConfig(config) {
@@ -160,20 +76,26 @@ function getEnabledAuthenticationTypesFromConfig(config) {
16076
return []
16177
}
16278

163-
function isAuthenticationTypeEnabled(type) {
79+
export function isAuthenticationTypeEnabled(type) {
16480
return getEnabledAuthenticationTypesFromConfig(config).includes(type)
16581
}
16682

16783
async function authenticateRequest(ctx) {
168-
let user
169-
// First attempt basic authentication if enabled
170-
if (user == null && isAuthenticationTypeEnabled('basic')) {
171-
user = await authenticateBasic(ctx)
84+
let user = null
85+
86+
// First attempt local authentication if enabled
87+
if (ctx.req.user) {
88+
user = ctx.req.user
17289
}
173-
// Otherwise try token based authentication if enabled
174-
if (user == null && isAuthenticationTypeEnabled('token')) {
90+
// Otherwise try token based authentication if enabled (@deprecated)
91+
if (user == null) {
17592
user = await authenticateToken(ctx)
17693
}
94+
// Otherwise try basic based authentication if enabled
95+
if (user == null) {
96+
// Basic auth using middleware
97+
user = await authenticateBasic(ctx)
98+
}
17799
// User could not be authenticated
178100
if (user == null) {
179101
const enabledTypes =
@@ -195,9 +117,50 @@ function handleAuditResponse(err) {
195117
}
196118

197119
export async function authenticate(ctx, next) {
198-
let user
199120
try {
200-
user = await authenticateRequest(ctx)
121+
// Authenticate Request either by basic or local or token
122+
const user = await authenticateRequest(ctx)
123+
124+
if (ctx.isAuthenticated()) {
125+
// Set the user on the context for consumption by other middleware
126+
ctx.authenticated = user
127+
128+
// Deal with paths exempt from audit
129+
if (ctx.path === '/transactions') {
130+
if (
131+
!ctx.query.filterRepresentation ||
132+
ctx.query.filterRepresentation !== 'full'
133+
) {
134+
// exempt from auditing success
135+
return next()
136+
}
137+
} else {
138+
for (const pathTest of auditingExemptPaths) {
139+
if (pathTest.test(ctx.path)) {
140+
// exempt from auditing success
141+
return next()
142+
}
143+
}
144+
}
145+
// Send an auth success audit event
146+
let audit = atna.construct.userLoginAudit(
147+
atna.constants.OUTCOME_SUCCESS,
148+
himSourceID,
149+
os.hostname(),
150+
ctx.authenticated.email,
151+
ctx.authenticated.groups.join(','),
152+
ctx.authenticated.groups.join(',')
153+
)
154+
audit = atna.construct.wrapInSyslog(audit)
155+
auditing.sendAuditEvent(audit, handleAuditResponse)
156+
157+
return next()
158+
} else {
159+
ctx.throw(
160+
401,
161+
`Denying access for an API request from ${ctx.request.host}`
162+
)
163+
}
201164
} catch (err) {
202165
// Handle authentication errors
203166
if (err.status === 401) {
@@ -210,7 +173,7 @@ export async function authenticate(ctx, next) {
210173
atna.constants.OUTCOME_SERIOUS_FAILURE,
211174
himSourceID,
212175
os.hostname(),
213-
err.email
176+
`Unknown with ip ${ctx.request.ip}`
214177
)
215178
audit = atna.construct.wrapInSyslog(audit)
216179
auditing.sendAuditEvent(audit, handleAuditResponse)
@@ -219,41 +182,6 @@ export async function authenticate(ctx, next) {
219182
// Rethrow other errors
220183
throw err
221184
}
222-
223-
// Set the user on the context for consumption by other middleware
224-
ctx.authenticated = user
225-
226-
// Deal with paths exempt from audit
227-
if (ctx.path === '/transactions') {
228-
if (
229-
!ctx.query.filterRepresentation ||
230-
ctx.query.filterRepresentation !== 'full'
231-
) {
232-
// exempt from auditing success
233-
return next()
234-
}
235-
} else {
236-
for (const pathTest of auditingExemptPaths) {
237-
if (pathTest.test(ctx.path)) {
238-
// exempt from auditing success
239-
return next()
240-
}
241-
}
242-
}
243-
244-
// Send an auth success audit event
245-
let audit = atna.construct.userLoginAudit(
246-
atna.constants.OUTCOME_SUCCESS,
247-
himSourceID,
248-
os.hostname(),
249-
user.email,
250-
user.groups.join(','),
251-
user.groups.join(',')
252-
)
253-
audit = atna.construct.wrapInSyslog(audit)
254-
auditing.sendAuditEvent(audit, handleAuditResponse)
255-
256-
return next()
257185
}
258186

259187
export async function getEnabledAuthenticationTypes(ctx, next) {

0 commit comments

Comments
 (0)