Skip to content

Commit 8d0e78b

Browse files
fix(Google Login): Prevents uninvited users gaining access (#1517 - LLC-10)
#1517 https://learningpool.atlassian.net/browse/LLC-10
1 parent 334935f commit 8d0e78b

File tree

13 files changed

+208
-30
lines changed

13 files changed

+208
-30
lines changed

api/src/auth/jwt.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ const payloadDefaults = ({
5353
/**
5454
* @param {*} - payload
5555
* @param {*} opts - (optional)
56-
* @returns {Promise<any>}
56+
* @returns {Promise<string>}
5757
*/
5858
const createJWT = ({
5959
userId,
@@ -84,7 +84,7 @@ const createUserTokenPayload = (user, provider) => payloadDefaults({
8484
/**
8585
* @param {*} user
8686
* @param {*} provider
87-
* @returns {Promise<any>}
87+
* @returns {Promise<string>}
8888
*/
8989
const createUserJWT = (user, provider) =>
9090
createJWT(
@@ -103,7 +103,7 @@ const createUserRefreshTokenPayload = (user, provider) => payloadDefaults({
103103
/**
104104
* @param {*} user
105105
* @param {*} provider
106-
* @returns {Promise<any>}
106+
* @returns {Promise<string>}
107107
*/
108108
const createUserRefreshJWT = (user, provider) =>
109109
createJWT(
@@ -150,7 +150,7 @@ const createOrgRefreshTokenPayload = (user, organisationId, provider) => payload
150150
* @param {*} user
151151
* @param {*} organisationId
152152
* @param {*} provider
153-
* @returns {Promise<any>}
153+
* @returns {Promise<string>}
154154
*/
155155
const createOrgRefreshJWT = (user, organisationId, provider) =>
156156
createJWT(

api/src/auth/passport.js

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -212,13 +212,25 @@ if (
212212
profile.emails,
213213
email => email.verified === true
214214
);
215-
User.findOrCreate({ email: userEmail.value }, (err, user) => {
216-
assert.ifError(err);
217-
user.googleId = profile.id;
218-
user.imageUrl = get(profile, 'photos.0.value');
219-
user.name = profile.displayName;
220-
user.save((err, savedUser) => done(err, savedUser));
221-
});
215+
216+
User.findOne(
217+
{
218+
email: userEmail.value
219+
},
220+
(err, user) => {
221+
assert.ifError(err);
222+
223+
if (!user) {
224+
return done(null, false, { message: 'User does not exist' });
225+
}
226+
227+
user.googleId = profile.id;
228+
user.imageUrl = get(profile, 'photos.0.value');
229+
user.name = profile.displayName;
230+
231+
user.save((err, savedUser) => done(err, savedUser));
232+
},
233+
);
222234
}
223235
)
224236
);

api/src/controllers/AuthController.js

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import passport from 'passport';
22
import jsonwebtoken from 'jsonwebtoken';
33
import { v4 as uuid } from 'uuid';
44
import ms from 'ms';
5+
import status from 'http-status';
56
import logger from 'lib/logger';
67
import User from 'lib/models/user';
78
import OAuthToken from 'lib/models/oAuthToken';
@@ -261,12 +262,18 @@ const jwtOrganisation = (req, res) => {
261262
}
262263
};
263264

264-
const googleSuccess = (req, res) => {
265+
const googleSuccess = (user, response) => {
265266
// we have successfully signed into google
266267
// create a JWT and set it in the query params (only way to return it with a redirect)
267-
createUserJWT(req.user, 'google')
268-
.then(token => res.redirect(`/api${AUTH_JWT_SUCCESS}?access_token=${token}`))
269-
.catch(err => res.status(500).send(err));
268+
createUserJWT(user, 'google')
269+
.then(token => response.redirect(`/api${AUTH_JWT_SUCCESS}?access_token=${token}`))
270+
.catch(
271+
(err) => {
272+
response
273+
.status(status.INTERNAL_SERVER_ERROR)
274+
.send(err);
275+
}
276+
);
270277
};
271278

272279
const clientInfo = (req, res) => {

api/src/routes/HttpRoutes.js

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,13 @@ router.get(
117117
AuthController.clientInfo
118118
);
119119

120+
router.get(
121+
routes.OAUTH2_FAILED,
122+
(request, response) => {
123+
response.send('Authorization failed');
124+
},
125+
);
126+
120127
router.post(
121128
routes.OAUTH2_TOKEN,
122129
AuthController.issueOAuth2AccessToken
@@ -136,10 +143,26 @@ if (process.env.GOOGLE_ENABLED) {
136143
routes.AUTH_JWT_GOOGLE,
137144
passport.authenticate('google', GOOGLE_AUTH_OPTIONS)
138145
);
146+
139147
router.get(
140148
routes.AUTH_JWT_GOOGLE_CALLBACK,
141-
passport.authenticate('google', DEFAULT_PASSPORT_OPTIONS),
142-
AuthController.googleSuccess
149+
(request, response, next) => {
150+
passport.authenticate(
151+
'google',
152+
DEFAULT_PASSPORT_OPTIONS,
153+
(error, user, info) => {
154+
const defaultErrorMessage = 'Something bad happened';
155+
156+
if (!user) {
157+
response.redirect(`/api${routes.OAUTH2_FAILED}?error=${get(info, 'message', defaultErrorMessage)}`);
158+
159+
return;
160+
}
161+
162+
AuthController.googleSuccess(user, response);
163+
},
164+
)(request, response, next);
165+
},
143166
);
144167
}
145168

lib/constants/auth.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ import getAuthFromRequest from 'lib/helpers/getAuthFromRequest';
1919

2020
export const GOOGLE_AUTH_OPTIONS = {
2121
scope: ['https://www.googleapis.com/auth/plus.login', 'email'],
22-
approvalPrompt: 'auto',
23-
session: false
22+
session: false,
23+
prompt: 'select_account'
2424
};
2525

2626
export const DEFAULT_PASSPORT_OPTIONS = {

lib/constants/routes.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export const AUTH_JWT_SUCCESS = '/auth/jwt/success';
1515
export const AUTH_CLIENT_INFO = '/auth/client/info';
1616

1717
export const OAUTH2_TOKEN = '/oauth2/token';
18+
export const OAUTH2_FAILED = '/oauth2/failed';
1819

1920
export const SENDSMS = '/sendsms';
2021

lib/models/user.js

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,73 @@ import getOrgFromAuthInfo from 'lib/services/auth/authInfoSelectors/getOrgFromAu
1717
import getUserIdFromAuthInfo from 'lib/services/auth/authInfoSelectors/getUserIdFromAuthInfo';
1818
import { validatePasswordUtil } from 'lib/utils/validators/User';
1919

20+
/**
21+
* @typedef {object} PasswordHistoryItem
22+
* @property {string} hash
23+
* @property {Date} date
24+
*/
25+
26+
/**
27+
* @typedef {object} ResetTokensItem
28+
* @property {string} token
29+
* @property {Date} expires
30+
*/
31+
32+
/**
33+
* @typedef {object} UserSettings
34+
* @property {boolean} CONFIRM_BEFORE_DELETE
35+
*/
36+
37+
/**
38+
* @typedef {object} OrganisationSettings
39+
* @property {object} organisation - TODO: define type
40+
* @property {string[]} scopes
41+
* @property {object[]} roles - TODO: define type
42+
* @property {string} filter
43+
* @property {string} oldFilter
44+
* @property {string} timezone
45+
*/
46+
47+
/**
48+
* @typedef {object} OwnerOrganisationSettings
49+
* @property {boolean} LOCKOUT_ENABLED
50+
* @property {number} LOCKOUT_ATTEMPS
51+
* @property {number} LOCKOUT_SECONDS
52+
* @property {boolean} PASSWORD_HISTORY_CHECK
53+
* @property {number} PASSWORD_HISTORY_TOTAL
54+
* @property {number} PASSWORD_MIN_LENGTH
55+
* @property {boolean} PASSWORD_REQUIRE_ALPHA
56+
* @property {boolean} PASSWORD_REQUIRE_NUMBER
57+
* @property {boolean} PASSWORD_USE_CUSTOM_REGEX
58+
* @property {string} PASSWORD_CUSTOM_REGEX
59+
* @property {string} PASSWORD_CUSTOM_MESSAGE
60+
*/
61+
62+
/**
63+
* Plain object structure without mongoose model methods
64+
* @typedef {object} User
65+
* @property {string} name
66+
* @property {string} email
67+
* @property {object[]} organisations - TODO: define type
68+
* @property {OrganisationSettings[]} organisationSettings
69+
* @property {string} imageUrl
70+
* @property {string} googleId
71+
* @property {string} password
72+
* @property {object} ownerOrganisation - TODO: define type
73+
* @property {OwnerOrganisationSettings} ownerOrganisationSettings
74+
* @property {UserSettings} settings
75+
* @property {string[]} scopes
76+
* @property {boolean} verified
77+
* @property {ResetTokensItem[]} resetTokens
78+
* @property {PasswordHistoryItem[]} passwordHistory
79+
* @property {Date} authLastAttempt
80+
* @property {number} authFailedAttempts
81+
* @property {Date} authLockoutExpiry
82+
* @property {boolean} hasBeenMigrated
83+
*/
84+
85+
/** @typedef {module:mongoose.Model<User>} UserModel */
86+
2087
/**
2188
* @param {string} value
2289
* @returns {true} if validation is success
@@ -63,6 +130,7 @@ async function validatePassword(value) {
63130
}
64131

65132
// TODO: Remove `organisationSettings.oldFilter` and `hasBeenMigrated` after we confirm success of $in migration
133+
/** @class UserSchema */
66134
const schema = new mongoose.Schema({
67135
name: { type: String },
68136
email: {
@@ -110,6 +178,7 @@ const schema = new mongoose.Schema({
110178
// "owned" users when the organisation's settings are updated
111179
ownerOrganisationSettings: {
112180
LOCKOUT_ENABLED: { type: Boolean, default: true },
181+
// TODO: fix typo 🤨
113182
LOCKOUT_ATTEMPS: { type: Number, default: 5 }, // number of attempts before lock out
114183
LOCKOUT_SECONDS: { type: Number, default: 1800 }, // 30 minute lock out period
115184

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@
8787
"highland": "^2.8.1",
8888
"html-to-text": "^2.1.0",
8989
"http-proxy": "^1.12.0",
90+
"http-status": "^1.4.2",
9091
"immutable": "^3.8.1",
9192
"ioredis": "^3.2.2",
9293
"js-cookie": "^2.1.3",

ui/src/containers/App/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { routeNodeSelector } from 'redux-router5';
1010
import { startsWithSegment } from 'router5.helpers';
1111
import get from 'lodash/get';
1212
import createAsyncComponent from 'ui/utils/createAsyncComponent';
13+
import SaveBarErrors from 'ui/containers/SaveBarErrors';
1314

1415
const renderPage = (routeName) => {
1516
const testRoute = startsWithSegment(routeName);
@@ -62,6 +63,7 @@ const component = ({ route }) => {
6263
<Helmet {...config.app.head} />
6364
{renderPage(name)}
6465
<Toasts />
66+
<SaveBarErrors />
6567
</div>
6668
);
6769
};

ui/src/pages/LoginPage/index.js

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,14 @@ const enhance = compose(
3535
})
3636
);
3737

38+
/**
39+
* @param oAuthLoginStart - {@see reduxOAuthLoginStart}
40+
* @param loginStart - {@see reduxLoginStart}
41+
* @param loginRequestState - {@see loginRequestStateSelector}
42+
* @param loginError - {@see loginErrorSelector}
43+
* @param googleAuth - {@see getAppDataSelector}
44+
* @returns {*}
45+
*/
3846
const render = ({
3947
oAuthLoginStart,
4048
loginStart,
@@ -52,8 +60,11 @@ const render = ({
5260
if (email && password) loginStart({ username: email, password }).catch(() => { });
5361
};
5462

55-
const onClickOAuthLogin = (e) => {
56-
if (e) e.preventDefault();
63+
const onClickOAuthLogin = (event) => {
64+
if (event) {
65+
event.preventDefault();
66+
}
67+
5768
oAuthLoginStart('google').catch(() => { });
5869
};
5970

0 commit comments

Comments
 (0)