Skip to content

Commit 81c6286

Browse files
authored
Merge pull request #1183 from jembi/PLAT-602-keycloak-integration
Plat 602 keycloak integration
2 parents 1a3e390 + fa2893c commit 81c6286

19 files changed

+953
-45
lines changed

config/config.md

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,8 +71,22 @@ The following config option are provided by the OpenHIM. All of these options ha
7171
// * "local" means through the UI with hitting "/authentication/local" endpoint with username and password,
7272
// this will create a session for the user and set cookies in the browser.
7373
// * "basic" means with basic auth either through browser or postman by giving also username and password.
74+
// * "openid" means with a third party authentication provider (e.g. keycloak).
7475
// * [Deprecated] "token" means that a request should provide in the header an 'auth-token', 'auth-salt' and 'auth-ts' to be authenticated.
75-
"authenicationTypes": ["token"]
76+
"authenicationTypes": ["token"],
77+
// Openid connect provider configuration needed for the authentication
78+
"openid": {
79+
// Openid connect provider realm url link
80+
"url": "http://localhost:9088/realms/platform-realm",
81+
// Callback URL used by openid connect provider (should be the same callback URL specified in realm)
82+
"callbackUrl": "http://localhost:9000",
83+
// CLient ID specified in the realm
84+
"clientId": "openhim-oauth",
85+
// Client secret specified in the realm
86+
"clientSecret": "tZKfEbWf0Ka5HBNZwFrdSyQH2xT1sNMR",
87+
// Scopes to be requested from Openid connect provider
88+
"scope": "openid email profile offline_access roles"
89+
}
7690
},
7791
"rerun": {
7892
// The port that the transaction re-run processor runs on, this port is

config/default.json

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,14 @@
4242
"maxPayloadSizeMB": 50,
4343
"truncateSize": 15000,
4444
"truncateAppend": "\n[truncated ...]",
45-
"authenticationTypes": ["basic", "local", "token"]
45+
"authenticationTypes": ["basic", "local", "token", "openid"],
46+
"openid": {
47+
"url": "http://localhost:9088/realms/platform-realm",
48+
"callbackUrl": "http://localhost:9000",
49+
"clientId": "openhim-oauth",
50+
"clientSecret": "tZKfEbWf0Ka5HBNZwFrdSyQH2xT1sNMR",
51+
"scope": "openid email profile offline_access roles"
52+
}
4653
},
4754
"rerun": {
4855
"httpPort": 7786,

config/test.json

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,14 @@
2626
"maxPayloadSizeMB": 50,
2727
"truncateSize": 10,
2828
"truncateAppend": "\n[truncated ...]",
29-
"authenticationTypes": ["token", "basic", "local"]
29+
"authenticationTypes": ["token", "basic", "local", "openid"],
30+
"openid": {
31+
"url": "http://localhost:10000/realms/realm",
32+
"callbackUrl": "http://localhost:10010",
33+
"clientId": "client-id",
34+
"clientSecret": "client-secret",
35+
"scope": "openid email profile offline_access roles"
36+
}
3037
},
3138
"caching": {
3239
"enabled": false

package-lock.json

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

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@
7474
"passport-custom": "^1.1.1",
7575
"passport-http": "^0.3.0",
7676
"passport-local": "^1.0.0",
77+
"passport-openidconnect": "^0.1.1",
7778
"pem": "^1.14.4",
7879
"raw-body": "^2.4.1",
7980
"semver": "^7.3.2",

src/api/authentication.js

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -80,19 +80,35 @@ export function isAuthenticationTypeEnabled(type) {
8080
return getEnabledAuthenticationTypesFromConfig(config).includes(type)
8181
}
8282

83+
function requestType(ctx, type) {
84+
const {headers} = ctx.request
85+
86+
if (
87+
headers['auth-username'] ||
88+
headers['auth-ts'] ||
89+
headers['auth-salt'] ||
90+
headers['auth-token']
91+
) {
92+
return type === 'token'
93+
} else if (headers.authorization && headers.authorization.includes('Basic')) {
94+
return type === 'basic'
95+
}
96+
return false
97+
}
98+
8399
async function authenticateRequest(ctx) {
84100
let user = null
85101

86-
// First attempt local authentication if enabled
102+
// First attempt local or openid authentication if enabled
87103
if (ctx.req.user) {
88104
user = ctx.req.user
89105
}
90106
// Otherwise try token based authentication if enabled (@deprecated)
91-
if (user == null) {
107+
if (user == null && requestType(ctx, 'token')) {
92108
user = await authenticateToken(ctx)
93109
}
94110
// Otherwise try basic based authentication if enabled
95-
if (user == null) {
111+
if (user == null && requestType(ctx, 'basic')) {
96112
// Basic auth using middleware
97113
user = await authenticateBasic(ctx)
98114
}

src/api/users.js

Lines changed: 41 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,7 @@ export function me(ctx) {
3939
}
4040

4141
export async function authenticate(ctx) {
42-
if (!ctx.req.user) {
43-
utils.logAndSetResponse(ctx, 404, `Could not be authenticaticated`, 'info')
44-
} else {
42+
if (ctx.req.user) {
4543
ctx.body = {
4644
result: 'User authenticated successfully',
4745
user: ctx.req.user
@@ -173,10 +171,9 @@ export async function userPasswordResetRequest(ctx, email) {
173171
}
174172

175173
try {
176-
const user = await UserModelAPI.findOneAndUpdate(
177-
{email: utils.caseInsensitiveRegex(email)},
178-
updateUserTokenExpiry
179-
)
174+
const user = await UserModelAPI.findOne({
175+
email: utils.caseInsensitiveRegex(email)
176+
})
180177
if (!user) {
181178
ctx.body = `Tried to request password reset for invalid email address: ${email}`
182179
ctx.status = 404
@@ -186,6 +183,17 @@ export async function userPasswordResetRequest(ctx, email) {
186183
return
187184
}
188185

186+
if (user.provider === 'openid') {
187+
ctx.body = `User supplied by OpenID Connect provider. Cannot request password reset.`
188+
ctx.status = 403
189+
logger.info(
190+
`Tried to request password reset for a user supplied by OpenID Connect provider: ${email}`
191+
)
192+
return
193+
}
194+
195+
await UserModelAPI.findByIdAndUpdate(user.id, updateUserTokenExpiry)
196+
189197
const {consoleURL} = config.alerts
190198
const setPasswordLink = `${consoleURL}/#!/set-password/${token}`
191199

@@ -293,6 +301,15 @@ export async function updateUserByToken(ctx, token) {
293301
ctx.status = 410
294302
return
295303
}
304+
305+
if (userDataExpiry.provider === 'openid') {
306+
ctx.body = `User supplied by OpenID Connect provider. Could not be updated.`
307+
ctx.status = 403
308+
logger.info(
309+
`Tried to request update by token for a user supplied by OpenID Connect provider: ${token}`
310+
)
311+
return
312+
}
296313
} catch (error) {
297314
utils.logAndSetResponse(
298315
ctx,
@@ -340,7 +357,10 @@ export async function updateUserByToken(ctx, token) {
340357
logger.warn(
341358
'Token authentication strategy is deprecated. Please consider using Local or Basic authentication.'
342359
)
343-
;({user, error} = await updateTokenUser(userUpdateObj))
360+
;({user, error} = await updateTokenUser({
361+
...userUpdateObj,
362+
provider: 'token'
363+
}))
344364
// Other providers
345365
} else {
346366
;({user, error} = await apiUpdateUser(userUpdateObj))
@@ -424,6 +444,8 @@ export async function addUser(ctx) {
424444
delete userData.passwordSalt
425445
delete userData.passwordAlgorithm
426446
delete userData.password
447+
448+
userData.provider = password ? 'local' : 'token'
427449
}
428450

429451
const user = new UserModelAPI(userData)
@@ -570,7 +592,7 @@ export async function updateUser(ctx, email) {
570592
)
571593
}
572594

573-
const {_doc: userData} = new UserModelAPI(ctx.request.body)
595+
let {_doc: userData} = new UserModelAPI(ctx.request.body)
574596
const {password} = ctx.request.body
575597

576598
// @deprecated
@@ -597,6 +619,15 @@ export async function updateUser(ctx, email) {
597619
delete userData._id
598620
}
599621

622+
if (userDetails.provider === 'openid') {
623+
userData = {
624+
msisdn: userData.msisdn,
625+
dailyReport: userData.dailyReport,
626+
weeklyReport: userData.weeklyReport,
627+
settings: userData.settings
628+
}
629+
}
630+
600631
try {
601632
let user, error
602633
// @deprecated Token user update
@@ -609,6 +640,7 @@ export async function updateUser(ctx, email) {
609640
passwordAlgorithm,
610641
passwordHash,
611642
passwordSalt,
643+
provider: 'token',
612644
...userData
613645
}))
614646
// Other providers

src/koaApi.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,20 @@ export function setupApp(done) {
8585
compose([passport.authenticate('local'), users.authenticate])
8686
)
8787
)
88+
// Openid authentication
89+
app.use(
90+
route.post(
91+
'/authenticate/openid',
92+
compose([
93+
(ctx, next) => {
94+
ctx.request.query = ctx.request.body
95+
return next()
96+
},
97+
passport.authenticate('openidconnect'),
98+
users.authenticate
99+
])
100+
)
101+
)
88102
// @deprecated: Token authentication
89103
app.use(route.get('/authenticate/:username', users.authenticateToken))
90104

src/middleware/sessionStore.js

Lines changed: 12 additions & 1 deletion