Skip to content

Commit

Permalink
Added logout all API.
Browse files Browse the repository at this point in the history
  • Loading branch information
shrihari-prakash committed Jan 7, 2024
1 parent fd056f0 commit 32c061f
Show file tree
Hide file tree
Showing 10 changed files with 116 additions and 6 deletions.
1 change: 1 addition & 0 deletions src/model/mongo/token.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ const tokenSchema = {
accessTokenExpiresAt: Date,
refreshToken: String,
refreshTokenExpiresAt: Date,
registeredAt: Date,
scope: {
type: Array,
required: true,
Expand Down
12 changes: 12 additions & 0 deletions src/model/mongo/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -635,6 +635,17 @@ export const userSchema = {
write: SensitivityLevel.HIGH,
},
},
globalLogoutAt: {
type: Date,
required: false,
willProjectForUserSelect: false,
willProjectForUserAdminSelect: false,
willProjectForUserClientSelect: false,
sensitivityScore: {
read: SensitivityLevel.LOW,
write: SensitivityLevel.LOW,
},
},
};

export type UserInterface = {
Expand Down Expand Up @@ -699,6 +710,7 @@ export type UserInterface = {
customData: string;
createdAt: Date;
updatedAt: Date;
globalLogoutAt: Date;
isFollowing?: boolean;
requested?: boolean;
};
Expand Down
32 changes: 32 additions & 0 deletions src/model/oauth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
Token,
RefreshToken,
} from "@node-oauth/oauth2-server";
import moment from "moment";

interface Client {
id: string;
Expand Down Expand Up @@ -133,6 +134,7 @@ const OAuthModel: OAuthModel = {
token.user = user;
}
if (useTokenCache) {
token.registeredAt = new Date().toISOString();
const serialized = JSON.stringify(token);
await Redis.client.set(
getPrefixedToken(token.accessToken),
Expand Down Expand Up @@ -168,6 +170,12 @@ const OAuthModel: OAuthModel = {
if (!isApplicationClient(cacheToken.user)) {
cacheToken.user = await getUserInfo(cacheToken.user._id);
}
const globalLogoutAt = cacheToken.user.globalLogoutAt;
const tokenRegisteredAt = cacheToken.registeredAt;
if (globalLogoutAt && moment(globalLogoutAt).isAfter(moment(tokenRegisteredAt))) {
log.debug("Expired access token detected.");
return null;
}
const hasNegativeScopeDiff = cacheToken.scope.some((scope: string) => {
return !ScopeManager.isScopeAllowed(scope, cacheToken.user.scope);
});
Expand All @@ -187,6 +195,15 @@ const OAuthModel: OAuthModel = {
if (dbTokenObject && !isApplicationClient(dbTokenObject.user)) {
dbTokenObject.user = await getUserInfo(dbTokenObject.user._id);
}
if (!dbTokenObject) {
return null;
}
const globalLogoutAt = dbTokenObject.user.globalLogoutAt;
const tokenRegisteredAt = dbTokenObject.registeredAt;
if (globalLogoutAt && moment(globalLogoutAt).isAfter(moment(tokenRegisteredAt))) {
log.debug("Expired access token detected.");
return null;
}
return dbTokenObject as unknown as Token;
} catch (err) {
log.error("Error retrieving access token.");
Expand All @@ -203,6 +220,12 @@ const OAuthModel: OAuthModel = {
if (!isApplicationClient(cacheToken.user)) {
cacheToken.user = await getUserInfo(cacheToken.user._id);
}
const globalLogoutAt = cacheToken.user.globalLogoutAt;
const tokenRegisteredAt = cacheToken.registeredAt;
if (globalLogoutAt && moment(globalLogoutAt).isAfter(moment(tokenRegisteredAt))) {
log.debug("Expired refresh token detected.");
return null;
}
cacheToken.refreshTokenExpiresAt = new Date(cacheToken.refreshTokenExpiresAt);
log.debug("Refresh token retrieved from cache.");
return cacheToken;
Expand All @@ -213,6 +236,15 @@ const OAuthModel: OAuthModel = {
if (dbTokenObject && !isApplicationClient(dbTokenObject.user)) {
dbTokenObject.user = await getUserInfo(dbTokenObject.user._id);
}
if (!dbTokenObject) {
return null;
}
const globalLogoutAt = dbTokenObject.user.globalLogoutAt;
const tokenRegisteredAt = dbTokenObject.registeredAt;
if (globalLogoutAt && moment(globalLogoutAt).isAfter(moment(tokenRegisteredAt))) {
log.debug("Expired refresh token detected.");
return null;
}
log.debug("Refresh token retrieved from database.");
return dbTokenObject as unknown as Token;
},
Expand Down
22 changes: 21 additions & 1 deletion src/service/api/oauth/authorize.all.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import { Logger } from "../../../singleton/logger";
const log = Logger.getLogger().child({ from: "oauth/authorize.all" });

import { Request as OAuthRequest, Response as OAuthResponse } from "@node-oauth/oauth2-server";
import { Request, Response, NextFunction } from "express";
import { v4 as uuidv4 } from "uuid";

import { OAuthServer } from "../../../singleton/oauth-server";
import { statusCodes } from "../../../utils/http-status";
import { Configuration } from "../../../singleton/configuration";
import moment from "moment";
import UserModel from "../../../model/mongo/user";

function validatePKCEParameters(req: Request) {
const queryParameters = req.query;
Expand Down Expand Up @@ -49,7 +54,22 @@ async function ALL__Authorize(req: Request, res: Response, next: NextFunction) {
}
const code = await OAuthServer.server.authorize(new OAuthRequest(req), new OAuthResponse(res), {
authenticateHandler: {
handle: (req: Request) => {
handle: async (req: Request) => {
if (!req.session.user) {
return null;
}
const userId = req.session.user._id;
const user = await UserModel.findById(userId).lean();
if (!user) {
return null;
}
const globalLogoutAt = user.globalLogoutAt;
const currentLoginAt = req.session.loggedInAt;
if (globalLogoutAt && moment(globalLogoutAt).isAfter(moment(currentLoginAt))) {
log.debug("Expired session detected in authorize.");
req.session.destroy(() => {});
return null;
}
return req.session.user;
},
},
Expand Down
1 change: 1 addition & 0 deletions src/service/api/user/login.post.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ const POST_Login = async (req: Request, res: Response) => {
} else {
user.password = undefined;
req.session.user = user;
req.session.loggedInAt = new Date().toISOString();
log.debug("Assigned session id %s for user %s", req.session?.id, user._id);
Pusher.publish(new PushEvent(PushEventList.USER_LOGIN, { user }));
if (Configuration.get("user.login.record-successful-attempts")) {
Expand Down
28 changes: 28 additions & 0 deletions src/service/api/user/logout-all.get.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { Logger } from "../../../singleton/logger";
const log = Logger.getLogger().child({ from: "user/logout.get" });

import { Request, Response } from "express";

import { errorMessages, statusCodes } from "../../../utils/http-status";
import { ErrorResponse, SuccessResponse } from "../../../utils/response";
import GET_Logout from "./logout.get";
import UserModel from "../../../model/mongo/user";
import { flushUserInfoFromRedis } from "../../../model/oauth";

const GET_LogoutAll = async (req: Request, res: Response) => {
try {
if (!res.locals.oauth) {
res.status(statusCodes.success).json(new SuccessResponse());
return;
}
const userId = res.locals.oauth.token.user._id;
await UserModel.updateOne({ _id: userId }, { $set: { globalLogoutAt: new Date().toISOString() } });
await GET_Logout(req, res);
flushUserInfoFromRedis([userId]);
} catch (err) {
log.error(err);
return res.status(statusCodes.internalError).json(new ErrorResponse(errorMessages.internalError));
}
};

export default GET_LogoutAll;
4 changes: 2 additions & 2 deletions src/service/api/user/logout.get.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { Redis } from "../../../singleton/redis";
import { Configuration } from "../../../singleton/configuration";
import { Token } from "@node-oauth/oauth2-server";

const POST_Logout = async (req: Request, res: Response) => {
const GET_Logout = async (req: Request, res: Response) => {
try {
const token = res.locals?.oauth?.token;
const user = token ? { ...res.locals.oauth.token.user } : null;
Expand Down Expand Up @@ -41,4 +41,4 @@ const POST_Logout = async (req: Request, res: Response) => {
}
};

export default POST_Logout;
export default GET_Logout;
6 changes: 4 additions & 2 deletions src/service/api/user/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,15 @@ import GET_FollowRequests from "./follow-requests.get";
import GET_FollowStatus, { GET_FollowStatusValidator } from "./follow-status.get";
import GET_InviteCodes from "./invite-codes.get";
import GET_Scopes from "./scopes.get";
import GET_Logout from "./logout.get";
import GET_SessionState from "./session-state.get";
import GET_Logout from "./logout.get";
import GET_LogoutAll from "./logout-all.get";
import GET_LoginHistory from "./login-history.get";
import PATCH_FollowRequest, { PATCH_FollowRequestValidator } from "./follow-request.patch";
import PATCH_Me, { PATCH_MeValidator } from "./me.patch";
import PATCH_ProfilePicture from "./profile-picture.patch";
import DELETE_FollowEntry, { DELETE_FollowEntryValidator } from "./follow-entry.delete";
import DELETE_ProfilePicture from "./profile-picture.delete";
import GET_LoginHistory from "./login-history.get";

const UserRouter = express.Router();

Expand All @@ -52,6 +53,7 @@ UserRouter.get("/code", ...GET_CodeValidator, GET_Code);
UserRouter.post("/reset-password", ...POST_ResetPasswordValidator, POST_ResetPassword);
UserRouter.post("/search", ...DelegatedAuthFlow, ...POST_SearchValidator, POST_Search);
UserRouter.get("/logout", AuthenticateSilent, GET_Logout);
UserRouter.get("/logout-all", AuthenticateSilent, GET_LogoutAll);
UserRouter.get("/scopes", GET_Scopes);

// Invite System
Expand Down
15 changes: 14 additions & 1 deletion src/service/api/user/session-state.get.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,27 @@ import { Logger } from "../../../singleton/logger";
const log = Logger.getLogger().child({ from: "user/session-state.get" });

import { Request, Response } from "express";
import moment from "moment";

import { errorMessages, statusCodes } from "../../../utils/http-status";
import { ErrorResponse, SuccessResponse } from "../../../utils/response";
import UserModel from "../../../model/mongo/user";

const GET_SessionState = async (req: Request, res: Response) => {
try {
const user = req.session?.user;
const sessionUser = req.session?.user;
if (!sessionUser) {
return res.status(statusCodes.forbidden).json(new ErrorResponse(errorMessages.forbidden));
}
const user = await await UserModel.findById(sessionUser._id).lean();
if (user) {
const globalLogoutAt = user.globalLogoutAt;
const currentLoginAt = req.session.loggedInAt;
if (globalLogoutAt && moment(globalLogoutAt).isAfter(moment(currentLoginAt))) {
log.debug("Expired session detected.");
req.session.destroy(() => {});
return null;
}
return res.status(statusCodes.success).json(new SuccessResponse({ userInfo: user }));
} else {
return res.status(statusCodes.forbidden).json(new ErrorResponse(errorMessages.forbidden));
Expand Down
1 change: 1 addition & 0 deletions src/types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@ declare module "express-session" {
oAuthLogin: boolean;
user: { [key: string]: any };
loginMeta: { [key: string]: any };
loggedInAt: string;
}
}

0 comments on commit 32c061f

Please sign in to comment.