mirror of
https://github.com/ragestudio/comty.git
synced 2025-06-10 02:54:15 +00:00
reimplement authentification process and regeneration system
This commit is contained in:
parent
12938553d7
commit
97e959f9d7
@ -101,15 +101,15 @@ class App extends React.Component {
|
||||
app.setLocation("/")
|
||||
}
|
||||
},
|
||||
"destroyed_session": async () => {
|
||||
"session.destroyed": async () => {
|
||||
await this.flushState()
|
||||
app.eventBus.emit("forceToLogin")
|
||||
app.eventBus.emit("app.forceToLogin")
|
||||
},
|
||||
"forceToLogin": () => {
|
||||
window.app.setLocation("/login")
|
||||
app.eventBus.emit("app.createLogin")
|
||||
"session.regenerated": async () => {
|
||||
//await this.flushState()
|
||||
//await this.initialization()
|
||||
},
|
||||
"invalid_session": async (error) => {
|
||||
"session.invalid": async (error) => {
|
||||
const token = await Session.token
|
||||
|
||||
if (!this.state.session && !token) {
|
||||
@ -119,7 +119,7 @@ class App extends React.Component {
|
||||
await this.sessionController.forgetLocalSession()
|
||||
await this.flushState()
|
||||
|
||||
app.eventBus.emit("forceToLogin")
|
||||
app.eventBus.emit("app.forceToLogin")
|
||||
|
||||
antd.notification.open({
|
||||
message: <Translation>
|
||||
@ -131,6 +131,10 @@ class App extends React.Component {
|
||||
icon: <Icons.MdOutlineAccessTimeFilled />,
|
||||
})
|
||||
},
|
||||
"app.forceToLogin": () => {
|
||||
window.app.setLocation("/login")
|
||||
app.eventBus.emit("app.createLogin")
|
||||
},
|
||||
"websocket_connected": () => {
|
||||
if (this.wsReconnecting) {
|
||||
this.wsReconnectingTry = 0
|
||||
@ -383,7 +387,7 @@ class App extends React.Component {
|
||||
const token = await Session.token
|
||||
|
||||
if (!token || token == null) {
|
||||
//window.app.eventBus.emit("no_session")
|
||||
app.eventBus.emit("app.forceToLogin")
|
||||
return false
|
||||
}
|
||||
|
||||
|
@ -9,6 +9,9 @@ export default class ApiCore extends Core {
|
||||
|
||||
this.namespaces = Object()
|
||||
|
||||
this.onExpiredExceptionEvent = false
|
||||
this.excludedExpiredExceptionURL = ["/regenerate_session_token"]
|
||||
|
||||
this.ctx.registerPublicMethod("api", this)
|
||||
}
|
||||
|
||||
@ -36,6 +39,52 @@ export default class ApiCore extends Core {
|
||||
return this.namespaces[namespace].endpoints
|
||||
}
|
||||
|
||||
handleBeforeRequest = async (request) => {
|
||||
if (this.onExpiredExceptionEvent) {
|
||||
if (this.excludedExpiredExceptionURL.includes(request.url)) return
|
||||
|
||||
await new Promise((resolve) => {
|
||||
app.eventBus.once("session.regenerated", () => {
|
||||
console.log(`Session has been regenerated, retrying request`)
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
handleRegenerationEvent = async (refreshToken, makeRequest) => {
|
||||
window.app.eventBus.emit("session.expiredExceptionEvent", refreshToken)
|
||||
|
||||
this.onExpiredExceptionEvent = true
|
||||
|
||||
const expiredToken = await Session.token
|
||||
|
||||
// exclude regeneration endpoint
|
||||
|
||||
// send request to regenerate token
|
||||
const response = await this.request("main", "post", "regenerateSessionToken", {
|
||||
expiredToken: expiredToken,
|
||||
refreshToken,
|
||||
}).catch((error) => {
|
||||
console.error(`Failed to regenerate token: ${error.message}`)
|
||||
return false
|
||||
})
|
||||
|
||||
if (!response) {
|
||||
return window.app.eventBus.emit("session.invalid", "Failed to regenerate token")
|
||||
}
|
||||
|
||||
// set new token
|
||||
Session.token = response.token
|
||||
|
||||
//this.namespaces["main"].internalAbortController.abort()
|
||||
|
||||
this.onExpiredExceptionEvent = false
|
||||
|
||||
// emit event
|
||||
window.app.eventBus.emit("session.regenerated")
|
||||
}
|
||||
|
||||
connectBridge = (key, params) => {
|
||||
this.namespaces[key] = this.createBridge(params)
|
||||
}
|
||||
@ -55,17 +104,21 @@ export default class ApiCore extends Core {
|
||||
return obj
|
||||
}
|
||||
|
||||
const handleResponse = async (data) => {
|
||||
// handle token regeneration
|
||||
if (data.headers?.regenerated_token) {
|
||||
Session.token = data.headers.regenerated_token
|
||||
console.debug("[REGENERATION] New token generated")
|
||||
}
|
||||
|
||||
const handleResponse = async (data, makeRequest) => {
|
||||
// handle 401 responses
|
||||
if (data instanceof Error) {
|
||||
if (data.response.status === 401) {
|
||||
window.app.eventBus.emit("invalid_session")
|
||||
// check if the server issue a refresh token on data
|
||||
if (data.response.data.refreshToken) {
|
||||
// handle regeneration event
|
||||
await this.handleRegenerationEvent(data.response.data.refreshToken, makeRequest)
|
||||
return await makeRequest()
|
||||
} else {
|
||||
return window.app.eventBus.emit("session.invalid", "Session expired, but the server did not issue a refresh token")
|
||||
}
|
||||
}
|
||||
if (data.response.status === 403) {
|
||||
return window.app.eventBus.emit("session.invalid", "Session not valid or not existent")
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -78,6 +131,7 @@ export default class ApiCore extends Core {
|
||||
wsOptions: {
|
||||
autoConnect: false,
|
||||
},
|
||||
onBeforeRequest: this.handleBeforeRequest,
|
||||
onRequest: getSessionContext,
|
||||
onResponse: handleResponse,
|
||||
...params,
|
||||
|
@ -103,7 +103,7 @@ export default class Session {
|
||||
|
||||
const result = await Session.bridge.delete.sessions({ user_id: session.user_id })
|
||||
this.forgetLocalSession()
|
||||
window.app.eventBus.emit("destroyed_session")
|
||||
window.app.eventBus.emit("session.destroyed")
|
||||
|
||||
return result
|
||||
}
|
||||
@ -118,7 +118,7 @@ export default class Session {
|
||||
|
||||
const result = await Session.bridge.delete.session({ user_id: session.user_id, token: token })
|
||||
this.forgetLocalSession()
|
||||
window.app.eventBus.emit("destroyed_session")
|
||||
window.app.eventBus.emit("session.destroyed")
|
||||
|
||||
return result
|
||||
}
|
||||
|
@ -51,7 +51,8 @@ export default class Server {
|
||||
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
||||
secretOrKey: this.server.oskid,
|
||||
algorithms: ["sha1", "RS256", "HS256"],
|
||||
expiresIn: this.env.signLifetime ?? "1h",
|
||||
expiresIn: this.env.signLifetime ?? "10s",
|
||||
enforceRegenerationTokenExpiration: false,
|
||||
}
|
||||
}
|
||||
|
||||
|
23
packages/server/src/controllers/CommentsController/index.js
Normal file
23
packages/server/src/controllers/CommentsController/index.js
Normal file
@ -0,0 +1,23 @@
|
||||
import { Controller } from "linebridge/dist/server"
|
||||
import { User, Post, Comment } from "../../models"
|
||||
import { Schematized } from "../../lib"
|
||||
|
||||
export default class CommentsController extends Controller {
|
||||
static refName = "CommentsController"
|
||||
|
||||
get = {
|
||||
|
||||
}
|
||||
|
||||
post = {
|
||||
|
||||
}
|
||||
|
||||
put = {
|
||||
|
||||
}
|
||||
|
||||
delete = {
|
||||
|
||||
}
|
||||
}
|
@ -1,7 +1,9 @@
|
||||
import { Controller } from "linebridge/dist/server"
|
||||
import { Session } from "../../models"
|
||||
import jwt from "jsonwebtoken"
|
||||
|
||||
import { Session } from "../../models"
|
||||
import { Token } from "../../lib"
|
||||
|
||||
export default class SessionController extends Controller {
|
||||
static refName = "SessionController"
|
||||
|
||||
@ -66,6 +68,22 @@ export default class SessionController extends Controller {
|
||||
return res.json(result)
|
||||
},
|
||||
},
|
||||
"/regenerate_session_token": {
|
||||
middlewares: ["useJwtStrategy"],
|
||||
fn: async (req, res) => {
|
||||
const { expiredToken, refreshToken } = req.body
|
||||
|
||||
const token = await Token.regenerateSession(expiredToken, refreshToken).catch((error) => {
|
||||
res.status(400).json({ error: error.message })
|
||||
|
||||
return null
|
||||
})
|
||||
|
||||
if (!token) return
|
||||
|
||||
return res.json({ token })
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
delete = {
|
||||
|
@ -1,32 +1,150 @@
|
||||
import jwt from "jsonwebtoken"
|
||||
import { nanoid } from "nanoid"
|
||||
import { Session, User } from "../../models"
|
||||
import { Session, RegenerationToken } from "../../models"
|
||||
|
||||
export async function regenerateSession(expiredToken, refreshToken) {
|
||||
// search for a regeneration token with the expired token (Should exist only one)
|
||||
const regenerationToken = await RegenerationToken.findOne({ refreshToken: refreshToken })
|
||||
|
||||
if (!regenerationToken) {
|
||||
throw new Error("Cannot find regeneration token")
|
||||
}
|
||||
|
||||
// check if the regeneration token is valid and not expired
|
||||
let decodedRefreshToken = null
|
||||
let decodedExpiredToken = null
|
||||
|
||||
try {
|
||||
decodedRefreshToken = jwt.decode(refreshToken)
|
||||
decodedExpiredToken = jwt.decode(expiredToken)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
// TODO: Storage this incident
|
||||
}
|
||||
|
||||
if (!decodedRefreshToken) {
|
||||
throw new Error("Cannot decode refresh token")
|
||||
}
|
||||
|
||||
if (!decodedExpiredToken) {
|
||||
throw new Error("Cannot decode expired token")
|
||||
}
|
||||
|
||||
// is not needed to verify the expired token, because it suppossed to be expired
|
||||
|
||||
// verify refresh token
|
||||
await jwt.verify(refreshToken, global.jwtStrategy.secretOrKey, async (err) => {
|
||||
// check if is expired
|
||||
if (err) {
|
||||
if (err.message === "jwt expired") {
|
||||
// check if server has enabled the enforcement of regeneration token expiration
|
||||
if (global.jwtStrategy.enforceRegenerationTokenExpiration) {
|
||||
// delete the regeneration token
|
||||
await RegenerationToken.deleteOne({ refreshToken: refreshToken })
|
||||
|
||||
throw new Error("Regeneration token expired and cannot be regenerated due server has enabled enforcement security policy")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// check if the regeneration token is associated with the expired token
|
||||
if (decodedRefreshToken.expiredToken !== expiredToken) {
|
||||
throw new Error("Regeneration token is not associated with the expired token")
|
||||
}
|
||||
})
|
||||
|
||||
// find the session associated with the expired token
|
||||
const session = await Session.findOne({ token: expiredToken })
|
||||
|
||||
if (!session) {
|
||||
throw new Error("Cannot find session associated with the expired token")
|
||||
}
|
||||
|
||||
// generate a new token
|
||||
const newToken = await createNewAuthToken(decodedExpiredToken, {
|
||||
updateSession: session._id,
|
||||
})
|
||||
|
||||
// delete the regeneration token
|
||||
await RegenerationToken.deleteOne({ refreshToken: refreshToken })
|
||||
|
||||
return newToken
|
||||
}
|
||||
|
||||
export async function getRegenerationToken(expiredToken) {
|
||||
const regenerationToken = await RegenerationToken.findOne({ expiredToken }).catch((error) => false)
|
||||
|
||||
return regenerationToken ?? false
|
||||
}
|
||||
|
||||
export async function createNewRegenerationToken(expiredToken) {
|
||||
// check if token is only expired, if is corrupted, reject
|
||||
let decoded = null
|
||||
|
||||
try {
|
||||
decoded = jwt.decode(expiredToken)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
|
||||
if (!decoded) {
|
||||
return false
|
||||
}
|
||||
|
||||
// check if token exists on a session
|
||||
const sessions = await Session.find({ user_id: decoded.user_id })
|
||||
const currentSession = sessions.find((session) => session.token === expiredToken)
|
||||
|
||||
if (!currentSession) {
|
||||
throw new Error("This token is not associated with any session")
|
||||
}
|
||||
|
||||
// create a new refresh token and sign it with maximum expiration time of 1 day
|
||||
const refreshToken = jwt.sign(
|
||||
{
|
||||
expiredToken
|
||||
},
|
||||
global.jwtStrategy.secretOrKey,
|
||||
{
|
||||
expiresIn: "1d"
|
||||
}
|
||||
)
|
||||
|
||||
// create a new regeneration token and save it
|
||||
const regenerationToken = new RegenerationToken({
|
||||
expiredToken,
|
||||
refreshToken,
|
||||
})
|
||||
|
||||
await regenerationToken.save()
|
||||
|
||||
// return the regeneration token
|
||||
return regenerationToken
|
||||
}
|
||||
|
||||
export async function createNewAuthToken(user, options = {}) {
|
||||
const payload = {
|
||||
user_id: user._id,
|
||||
user_id: user._id ?? user.user_id,
|
||||
username: user.username,
|
||||
email: user.email,
|
||||
refreshToken: nanoid(),
|
||||
signLocation: global.signLocation,
|
||||
}
|
||||
|
||||
await User.findByIdAndUpdate(user._id, { refreshToken: payload.refreshToken })
|
||||
|
||||
return await signNew(payload, options)
|
||||
return await signNewAuthToken(payload, options)
|
||||
}
|
||||
|
||||
export async function signNew(payload, options = {}) {
|
||||
export async function signNewAuthToken(payload, options = {}) {
|
||||
if (options.updateSession) {
|
||||
const sessionData = await Session.findById(options.updateSession)
|
||||
const sessionData = await Session.findOne({ _id: options.updateSession })
|
||||
|
||||
payload.session_uuid = sessionData.session_uuid
|
||||
} else {
|
||||
payload.session_uuid = nanoid()
|
||||
}
|
||||
|
||||
const token = jwt.sign(payload, options.secretOrKey, {
|
||||
expiresIn: options.expiresIn ?? "1h",
|
||||
algorithm: options.algorithm ?? "HS256"
|
||||
const token = jwt.sign(payload, global.jwtStrategy.secretOrKey, {
|
||||
expiresIn: global.jwtStrategy.expiresIn ?? "1h",
|
||||
algorithm: global.jwtStrategy.algorithm ?? "HS256"
|
||||
})
|
||||
|
||||
const session = {
|
||||
@ -47,7 +165,7 @@ export async function signNew(payload, options = {}) {
|
||||
} else {
|
||||
let newSession = new Session(session)
|
||||
|
||||
newSession.save()
|
||||
await newSession.save()
|
||||
}
|
||||
|
||||
return token
|
||||
|
@ -4,7 +4,7 @@ import jwt from "jsonwebtoken"
|
||||
|
||||
export default (req, res, next) => {
|
||||
function reject(description) {
|
||||
return res.status(401).json({ error: `${description ?? "Invalid session"}` })
|
||||
return res.status(403).json({ error: `${description ?? "Invalid session"}` })
|
||||
}
|
||||
|
||||
const authHeader = req.headers?.authorization?.split(" ")
|
||||
@ -37,17 +37,37 @@ export default (req, res, next) => {
|
||||
return res.status(404).json({ error: "No user data found" })
|
||||
}
|
||||
|
||||
// if cannot verify token, start regeneration process
|
||||
if (err) {
|
||||
if (decoded.refreshToken === userData.refreshToken) {
|
||||
const regeneratedToken = await Token.createNewAuthToken(userData, {
|
||||
...global.jwtStrategy,
|
||||
updateSession: currentSession._id,
|
||||
})
|
||||
|
||||
res.setHeader("regenerated_token", regeneratedToken)
|
||||
} else {
|
||||
return reject("Token expired, cannot refresh token either")
|
||||
// first check if token is only expired, if is corrupted, reject
|
||||
if (err.name !== "TokenExpiredError") {
|
||||
return reject("Invalid token, cannot regenerate")
|
||||
}
|
||||
|
||||
let regenerationToken = null
|
||||
|
||||
// check if this expired token has a regeneration token associated
|
||||
const associatedRegenerationToken = await Token.getRegenerationToken(token)
|
||||
|
||||
if (associatedRegenerationToken) {
|
||||
regenerationToken = associatedRegenerationToken.refreshToken
|
||||
} else {
|
||||
// create a new regeneration token with the expired token
|
||||
regenerationToken = await Token.createNewRegenerationToken(token).catch((error) => {
|
||||
// in case of error, reject
|
||||
reject(error.message)
|
||||
|
||||
return null
|
||||
})
|
||||
}
|
||||
|
||||
if (!regenerationToken) return
|
||||
|
||||
// now send the regeneration token to the client (start Expired Exception Event [EEE])
|
||||
return res.status(401).json({
|
||||
error: "Token expired",
|
||||
refreshToken: regenerationToken.refreshToken,
|
||||
})
|
||||
}
|
||||
|
||||
req.user = userData
|
||||
|
@ -16,10 +16,13 @@ const schemas = getSchemas()
|
||||
// server
|
||||
export const Config = mongoose.model("Config", schemas.Config, "config")
|
||||
|
||||
// sessions
|
||||
export const Session = mongoose.model("Session", schemas.Session, "sessions")
|
||||
export const RegenerationToken = mongoose.model("RegenerationToken", schemas.RegenerationToken, "regenerationTokens")
|
||||
|
||||
// users
|
||||
export const User = mongoose.model("User", schemas.User, "accounts")
|
||||
export const UserFollow = mongoose.model("UserFollow", schemas.UserFollow, "follows")
|
||||
export const Session = mongoose.model("Session", schemas.Session, "sessions")
|
||||
export const Role = mongoose.model("Role", schemas.Role, "roles")
|
||||
export const Badge = mongoose.model("Badge", schemas.Badge, "badges")
|
||||
|
||||
|
@ -1,6 +1,8 @@
|
||||
export { default as Session } from "./session"
|
||||
export { default as RegenerationToken } from "./regenerationToken"
|
||||
|
||||
export { default as User } from "./user"
|
||||
export { default as Role } from "./role"
|
||||
export { default as Session } from "./session"
|
||||
export { default as Config } from "./config"
|
||||
export { default as Post } from "./post"
|
||||
export { default as Comment } from "./comment"
|
||||
|
10
packages/server/src/schemas/regenerationToken/index.js
Normal file
10
packages/server/src/schemas/regenerationToken/index.js
Normal file
@ -0,0 +1,10 @@
|
||||
export default {
|
||||
expiredToken: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
refreshToken: {
|
||||
type: String,
|
||||
required: true,
|
||||
}
|
||||
}
|
@ -1,5 +1,4 @@
|
||||
export default {
|
||||
refreshToken: { type: String, select: false },
|
||||
username: { type: String, required: true },
|
||||
password: { type: String, required: true, select: false },
|
||||
cover: { type: String },
|
||||
|
Loading…
x
Reference in New Issue
Block a user