Skip to content
This repository has been archived by the owner on Apr 19, 2023. It is now read-only.

Commit

Permalink
✨ Login with Google
Browse files Browse the repository at this point in the history
  • Loading branch information
AnandChowdhary committed Apr 29, 2019
1 parent 1653ede commit 3714722
Show file tree
Hide file tree
Showing 10 changed files with 270 additions and 11 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ Staart is a Node.js backend starter for SaaS startups written in TypeScript. It
- [x] 👩‍💻 Helpers for database query, finding users, creating tokens, etc.
- [x] 🔐 JWT-based authentication with email/password and scopes
- [x] 💳 Support for multiple emails per user account
- [ ] 🔐 Login with Google (and Facebook?)
- [x] 🔐 Login with Google
- [x] 👩‍💻 Configuration based on environment variables
- [x] 👩‍💻 TypeScript interfaces for `User`, `HTTPError`, etc.
- [ ] 💳 Organizations, inviting team members with permissions
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
"body-parser": "^1.19.0",
"express-async-handler": "^1.1.4",
"fs-extra": "^7.0.1",
"googleapis": "^39.2.0",
"jsonwebtoken": "^8.5.1",
"marked": "^0.6.2",
"mustache": "^3.0.1",
Expand Down
4 changes: 4 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,7 @@ export const TOKEN_EXPIRY_PASSWORD_RESET =
process.env.TOKEN_EXPIRY_PASSWORD_RESET || "1d";
export const TOKEN_EXPIRY_LOGIN = process.env.TOKEN_EXPIRY_LOGIN || "1d";
export const TOKEN_EXPIRY_REFRESH = process.env.TOKEN_EXPIRY_REFRESH || "30d";

export const GOOGLE_CLIENT_ID = process.env.GOOGLE_CLIENT_ID || "";
export const GOOGLE_CLIENT_SECRET = process.env.GOOGLE_CLIENT_SECRET || "";
export const GOOGLE_CLIENT_REDIRECT = process.env.GOOGLE_CLIENT_REDIRECT || "";
48 changes: 48 additions & 0 deletions src/helpers/google.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { google } from "googleapis";
import {
GOOGLE_CLIENT_ID,
GOOGLE_CLIENT_SECRET,
GOOGLE_CLIENT_REDIRECT
} from "../config";
import { ErrorCode } from "../interfaces/enum";
import { GetTokenResponse } from "google-auth-library/build/src/auth/oauth2client";

export const googleCreateConnection = () => {
return new google.auth.OAuth2(
GOOGLE_CLIENT_ID,
GOOGLE_CLIENT_SECRET,
GOOGLE_CLIENT_REDIRECT
);
};

export const googleGetConnectionUrl = () => {
const auth = googleCreateConnection();
return auth.generateAuthUrl({
access_type: "offline",
prompt: "consent",
scope: [
"https://www.googleapis.com/auth/plus.me",
"https://www.googleapis.com/auth/userinfo.email"
]
});
};

export const googleGetTokensFromCode = async (code: string) => {
const auth = googleCreateConnection();
const tokens = await auth.getToken(code);
if (!tokens) throw new Error(ErrorCode.GOOGLE_AUTH_ERROR);
return tokens;
};

export const googleGetEmailFromToken = async (data: GetTokenResponse) => {
const auth = googleCreateConnection();
auth.setCredentials(data.tokens);
const plus = google.plus({ version: "v1", auth });
const user = await plus.people.get({ userId: "me" });
const email =
user.data.emails && user.data.emails.length
? user.data.emails[0].value
: "";
if (!email) throw new Error(ErrorCode.GOOGLE_AUTH_ERROR);
return email;
};
2 changes: 1 addition & 1 deletion src/i18n.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
interface Lang {
[index: string]: any;
[index: string]: string | Lang;
}
interface I18N {
[index: string]: Lang;
Expand Down
3 changes: 2 additions & 1 deletion src/interfaces/enum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,8 @@ export enum ErrorCode {
INSUFFICIENT_PERMISSION = "401/insufficient-permission",
DEFAULT = "500/server-error",
EMAIL_CANNOT_DELETE = "400/email.cannotDelete",
UNVERIFIED_EMAIL = "401/unverified-email"
UNVERIFIED_EMAIL = "401/unverified-email",
GOOGLE_AUTH_ERROR = "401/google-auth-error"
}

export enum Templates {
Expand Down
27 changes: 27 additions & 0 deletions src/rest/auth.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { User } from "../interfaces/tables/user";
import { createUser, updateUser, getUserByEmail, getUser } from "../crud/user";
import { InsertResult } from "../interfaces/mysql";
import { google } from "googleapis";
import {
createEmail,
updateEmail,
Expand All @@ -26,6 +27,11 @@ import {
import { compare, hash } from "bcrypt";
import { deleteSensitiveInfoUser } from "../helpers/utils";
import { createMembership } from "../crud/membership";
import {
googleGetConnectionUrl,
googleGetTokensFromCode,
googleGetEmailFromToken
} from "../helpers/google";

export const validateRefreshToken = async (token: string, locals: Locals) => {
const data = <User>await verifyToken(token, Tokens.REFRESH);
Expand Down Expand Up @@ -147,3 +153,24 @@ export const updatePassword = async (
);
return;
};

export const loginWithGoogleLink = () => googleGetConnectionUrl();

export const loginWithGoogleVerify = async (code: string, locals: Locals) => {
const data = await googleGetTokensFromCode(code);
const email = await googleGetEmailFromToken(data);
const user = await getUserByEmail(email);
if (!user.id) throw new Error(ErrorCode.USER_NOT_FOUND);
await createEvent(
{
userId: user.id,
type: EventType.AUTH_LOGIN,
data: { strategy: "google" }
},
locals
);
return {
token: await loginToken(deleteSensitiveInfoUser(user)),
refresh: await refreshToken(user.id)
};
};
21 changes: 20 additions & 1 deletion src/routes/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import {
login,
updatePassword,
register,
validateRefreshToken
validateRefreshToken,
loginWithGoogleLink,
loginWithGoogleVerify
} from "../rest/auth";
import { verifyToken } from "../helpers/jwt";

Expand Down Expand Up @@ -80,3 +82,20 @@ export const routeAuthResetPasswordRecover = async (
await updatePassword(token, password, res.locals);
res.json({ success: true });
};

export const routeAuthLoginWithGoogleLink = async (
req: Request,
res: Response
) => {
res.json({ redirect: loginWithGoogleLink() });
};

export const routeAuthLoginWithGoogleVerify = async (
req: Request,
res: Response
) => {
const code =
req.body.code || (req.get("Authorization") || "").replace("Bearer ", "");
if (!code) throw new Error(ErrorCode.MISSING_TOKEN);
res.json(await loginWithGoogleVerify(code, res.locals));
};
6 changes: 5 additions & 1 deletion src/routes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ import {
routeAuthResetPasswordRequest,
routeAuthResetPasswordRecover,
routeAuthRegister,
routeAuthRefresh
routeAuthRefresh,
routeAuthLoginWithGoogleLink,
routeAuthLoginWithGoogleVerify
} from "./auth";
import { routeMembershipGet, routeMembershipCreate } from "./membership";

Expand Down Expand Up @@ -48,6 +50,8 @@ const routesAuth = (app: Application) => {
"/auth/reset-password/recover",
asyncHandler(routeAuthResetPasswordRecover)
);
app.get("/auth/google/link", asyncHandler(routeAuthLoginWithGoogleLink));
app.post("/auth/google/verify", asyncHandler(routeAuthLoginWithGoogleVerify));
};

const routesUser = (app: Application) => {
Expand Down
Loading

0 comments on commit 3714722

Please sign in to comment.