Skip to content

Commit

Permalink
Login as anonymous by default (#504)
Browse files Browse the repository at this point in the history
  • Loading branch information
antoinejaussoin authored Mar 24, 2023
1 parent 7987a6d commit f80c96b
Show file tree
Hide file tree
Showing 44 changed files with 406 additions and 247 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/alpha.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: 'Alpha Build'

on:
push:
branches: [v502/simplify-fe-env]
branches: [v502/anon-by-default]

jobs:
frontend:
Expand Down
1 change: 1 addition & 0 deletions backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@
"typeorm": "0.3.11",
"typeorm-naming-strategies": "4.1.0",
"typescript": "4.9.5",
"unique-names-generator": "^4.7.1",
"uuid": "9.0.0"
},
"resolutions": {
Expand Down
27 changes: 18 additions & 9 deletions backend/src/auth/config.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import { IStrategyOption } from 'passport-twitter';
import { StrategyOptions as GoogleStrategyOptions } from 'passport-google-oauth20';
import { StrategyOptions as GitHubStrategy } from 'passport-github2';
import { MicrosoftStrategyOptions } from 'passport-microsoft';
import { IStrategyOptionWithRequest } from 'passport-twitter';
import { StrategyOptionsWithRequest as GoogleStrategyOptions } from 'passport-google-oauth20';
import { StrategyOptionsWithRequest as GitHubStrategy } from 'passport-github2';
import { MicrosoftStrategyOptionsWithRequest } from 'passport-microsoft';
import { OktaStrategyOptions } from 'passport-okta-oauth20';
import config from '../config.js';

type MicrosoftStrategyOptionsWithTenant = MicrosoftStrategyOptions & {
tenant: string | undefined;
};
type MicrosoftStrategyOptionsWithTenant =
MicrosoftStrategyOptionsWithRequest & {
tenant: string | undefined;
};

const providers = ['twitter', 'google', 'github', 'slack', 'microsoft', 'okta'];

Expand All @@ -20,13 +21,14 @@ const callbacks = providers.map((provider) => {
const [twitterURL, googleURL, githubURL, slackURL, microsoftURL, oktaURL] =
callbacks;

export const TWITTER_CONFIG: IStrategyOption | null =
export const TWITTER_CONFIG: IStrategyOptionWithRequest | null =
config.TWITTER_KEY && config.TWITTER_SECRET
? {
consumerKey: config.TWITTER_KEY || '',
consumerSecret: config.TWITTER_SECRET || '',
callbackURL: twitterURL,
includeEmail: true,
passReqToCallback: true,
}
: null;

Expand All @@ -36,6 +38,7 @@ export const GOOGLE_CONFIG: GoogleStrategyOptions | null =
clientID: config.GOOGLE_KEY || '',
clientSecret: config.GOOGLE_SECRET || '',
callbackURL: googleURL,
passReqToCallback: true,
}
: null;

Expand All @@ -46,6 +49,7 @@ export const GITHUB_CONFIG: GitHubStrategy | null =
clientSecret: config.GITHUB_SECRET || '',
callbackURL: githubURL,
scope: ['user:email'],
passReqToCallback: true,
}
: null;

Expand All @@ -62,6 +66,7 @@ export const SLACK_CONFIG =
clientID: config.SLACK_KEY || '',
clientSecret: config.SLACK_SECRET || '',
callbackURL: slackURL,
passReqToCallback: true,
}
: null;

Expand All @@ -75,16 +80,20 @@ export const MICROSOFT_CONFIG: MicrosoftStrategyOptionsWithTenant | null =
tokenURL: config.MICROSOFT_TOKEN_URL,
callbackURL: microsoftURL,
scope: ['user.read'],
passReqToCallback: true,
}
: null;

export const OKTA_CONFIG: OktaStrategyOptions | null =
export const OKTA_CONFIG:
| (OktaStrategyOptions & { passReqToCallback: true })
| null =
config.OKTA_AUDIENCE && config.OKTA_KEY && config.OKTA_SECRET
? {
audience: config.OKTA_AUDIENCE,
clientID: config.OKTA_KEY,
clientSecret: config.OKTA_SECRET,
scope: ['openid', 'email', 'profile'],
callbackURL: oktaURL,
passReqToCallback: true,
}
: null;
15 changes: 12 additions & 3 deletions backend/src/auth/passport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ import {
import { registerUser, UserRegistration } from '../db/actions/users.js';
import { serialiseIds, UserIds, deserialiseIds } from '../utils.js';
import config from '../config.js';
import { Request } from 'express';
import { mergeAnonymous } from '../db/actions/merge.js';

export default () => {
passport.serializeUser<string>((user, cb) => {
Expand All @@ -46,6 +48,7 @@ export default () => {

function callback<TProfile, TCallback>(type: AccountType) {
return async (
req: Request,
_accessToken: string,
_refreshToken: string,
anyProfile: TProfile,
Expand Down Expand Up @@ -86,9 +89,14 @@ export default () => {
return;
}

const dbIdentity = await registerUser(user);
const newUser = await registerUser(user);

callback(null, dbIdentity.toIds());
if (newUser) {
await mergeAnonymous(req, newUser.id);
callback(null, newUser.toIds());
} else {
callback('Cannot register user', null);
}
};
}

Expand Down Expand Up @@ -219,7 +227,8 @@ export default () => {
}

if (OKTA_CONFIG) {
passport.use(new OktaStrategy(OKTA_CONFIG, callback('okta')));
// eslint-disable-next-line @typescript-eslint/no-explicit-any
passport.use(new OktaStrategy(OKTA_CONFIG, callback('okta') as any));
logSuccess('Okta');
}

Expand Down
7 changes: 6 additions & 1 deletion backend/src/auth/router.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { mergeAnonymous } from '../db/actions/merge.js';
import express, { NextFunction, Request, Response } from 'express';
import passport from 'passport';
import { UserIds } from '../utils.js';

const router = express.Router();
// Setting up the passport middleware for each of the OAuth providers
Expand All @@ -14,7 +16,7 @@ const microsoftAuth = passport.authenticate('microsoft');
const oktaAuth = passport.authenticate('okta');

function anonAuth(req: Request, res: Response, next: NextFunction) {
passport.authenticate('local', function (err, user) {
passport.authenticate('local', async function (err, user: UserIds | null) {
res.setHeader('Content-Type', 'application/json');

if (err) {
Expand All @@ -23,6 +25,9 @@ function anonAuth(req: Request, res: Response, next: NextFunction) {
if (!user) {
return res.status(403).send().end();
}

await mergeAnonymous(req, user.identityId);

req.logIn(user, function (err) {
if (err) {
return next(err);
Expand Down
18 changes: 18 additions & 0 deletions backend/src/common/random-username.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import {
uniqueNamesGenerator,
adjectives,
animals,
} from 'unique-names-generator';
import { v4 } from 'uuid';

export function generateUsername() {
const shortName: string = uniqueNamesGenerator({
dictionaries: [adjectives, animals],
separator: ' ',
style: 'capital',
length: 2,
seed: v4(),
});

return shortName;
}
22 changes: 15 additions & 7 deletions backend/src/db/actions/merge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,22 +8,30 @@ import {
VoteRepository,
} from '../repositories/index.js';
import { deleteAccount } from './delete.js';
import { getUserViewFromRequest } from '../../utils.js';
import { Request } from 'express';

export async function mergeAnonymous(req: Request, newUserIdentityId: string) {
const anonymousUser = await getUserViewFromRequest(req);
const user = await getUserView(newUserIdentityId);
if (user && anonymousUser && anonymousUser.accountType === 'anonymous') {
await migrateOne(user, anonymousUser);
await deleteOne(anonymousUser);
}
}

export async function mergeUsers(
mainUserId: string,
mergedUserIds: string[]
mainUserIdentityId: string,
mergedUserIdentityIds: string[]
): Promise<boolean> {
console.log('Merging users', mainUserId, mergedUserIds);

for (const target of mergedUserIds) {
await mergeOne(mainUserId, target);
for (const target of mergedUserIdentityIds) {
await mergeOne(mainUserIdentityId, target);
}

return true;
}

async function mergeOne(main: string, target: string) {
console.log('Merge ', main, target);
const mainUser = await getUserView(main);
const targetUser = await getUserView(target);

Expand Down
64 changes: 45 additions & 19 deletions backend/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ import {
getIdentityByUsername,
associateUserWithAdWordsCampaign,
TrackingInfo,
registerAnonymousUser,
} from './db/actions/users.js';
import { isLicenced } from './security/is-licenced.js';
import rateLimit from 'express-rate-limit';
Expand All @@ -70,6 +71,8 @@ import { deleteAccount } from './db/actions/delete.js';
import { noop } from 'lodash-es';
import { createDemoSession } from './db/actions/demo.js';
import cookieParser from 'cookie-parser';
import { generateUsername } from './common/random-username.js';
import { mergeAnonymous } from './db/actions/merge.js';

const realIpHeader = 'X-Forwarded-For';
const sessionSecret = `${config.SESSION_SECRET!}-4.11.5`; // Increment to force re-auth
Expand Down Expand Up @@ -110,7 +113,17 @@ if (config.SELF_HOSTED) {
initSentry();

const app = express();
app.use(cookieParser());
app.use(cookieParser(sessionSecret));
app.use(
express.json({
// This is a trick to get the raw buffer on the request, for Stripe
verify: (req, _, buf) => {
const request = req as express.Request;
request.buf = buf;
},
})
);
app.use(express.urlencoded({ extended: true }));

function getActualIp(req: express.Request): string {
const headerValue = req.header(realIpHeader);
Expand Down Expand Up @@ -141,18 +154,6 @@ const heavyLoadLimiter = rateLimit({
// Sentry
setupSentryRequestHandler(app);

// Stripe
app.use(
express.json({
// This is a trick to get the raw buffer on the request, for Stripe
verify: (req, _, buf) => {
const request = req as express.Request;
request.buf = buf;
},
})
);
app.use(express.urlencoded({ extended: true }));

// saveUninitialized: true allows us to attach the socket id to the session
// before we have athenticated the user
let sessionMiddleware: express.RequestHandler;
Expand All @@ -172,10 +173,11 @@ if (config.REDIS_ENABLED) {
sessionMiddleware = session({
secret: sessionSecret,
resave: true,
saveUninitialized: true,
saveUninitialized: false,
store: new RedisStore({ client: redisClient }),
cookie: {
secure: config.SECURE_COOKIES,
maxAge: 1000 * 60 * 60 * 24 * 365, // 1 year
},
});

Expand All @@ -194,9 +196,10 @@ if (config.REDIS_ENABLED) {
sessionMiddleware = session({
secret: sessionSecret,
resave: true,
saveUninitialized: true,
saveUninitialized: false,
cookie: {
secure: config.SECURE_COOKIES,
maxAge: 1000 * 60 * 60 * 24 * 365, // 1 year
},
});
}
Expand Down Expand Up @@ -331,7 +334,25 @@ db().then(() => {
if (user) {
res.status(200).send(user.toJson());
} else {
res.status(401).send('Not logged in');
const anonUser = await registerAnonymousUser(
generateUsername() + '^' + v4(),
v4()
);
if (anonUser) {
const view = await getUserView(anonUser.id);
if (view) {
req.logIn(
{ userId: anonUser.user.id, identityId: anonUser.id },
() => {
res.status(200).send(view.toJson());
}
);
} else {
res.status(500).send('Could not get user view');
}
} else {
res.status(401).send('Not logged in');
}
}
});

Expand Down Expand Up @@ -433,9 +454,12 @@ db().then(() => {
});

app.post('/api/register', heavyLoadLimiter, async (req, res) => {
if (req.user) {
res.status(500).send('You are already logged in');
return;
const previousUser = await getUserViewFromRequest(req);
if (previousUser) {
if (previousUser?.accountType !== 'anonymous') {
res.status(500).send('You are already logged in');
return;
}
}
if (config.DISABLE_PASSWORD_REGISTRATION) {
res.status(403).send('Password accounts registration is disabled.');
Expand All @@ -450,9 +474,11 @@ db().then(() => {
return;
}
const identity = await registerPasswordUser(registerPayload);

if (!identity) {
res.status(500).send();
} else {
await mergeAnonymous(req, identity.id);
if (identity.emailVerification) {
await sendVerificationEmail(
registerPayload.username,
Expand Down
5 changes: 5 additions & 0 deletions backend/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -4751,6 +4751,11 @@ undefsafe@^2.0.5:
resolved "https://registry.yarnpkg.com/undefsafe/-/undefsafe-2.0.5.tgz#38733b9327bdcd226db889fb723a6efd162e6e2c"
integrity sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==

unique-names-generator@^4.7.1:
version "4.7.1"
resolved "https://registry.yarnpkg.com/unique-names-generator/-/unique-names-generator-4.7.1.tgz#966407b12ba97f618928f77322cfac8c80df5597"
integrity sha512-lMx9dX+KRmG8sq6gulYYpKWZc9RlGsgBR6aoO8Qsm3qvkSJ+3rAymr+TnV8EDMrIrwuFJ4kruzMWM/OpYzPoow==

[email protected], unpipe@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"
Expand Down
7 changes: 6 additions & 1 deletion frontend/src/Layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,12 @@ function App() {
<HomeOutlined />
</IconButton>
</HomeButton>
<MainTitle variant="h6" color="inherit" onClick={goToHome}>
<MainTitle
variant="h6"
color="inherit"
onClick={goToHome}
data-cy="header-home-button"
>
Retrospected&nbsp;
</MainTitle>
<ProPillContainer>
Expand Down
Loading

0 comments on commit f80c96b

Please sign in to comment.