reimplement authentification process and regeneration system

This commit is contained in:
srgooglo 2022-09-09 17:42:21 +02:00
parent 12938553d7
commit 97e959f9d7
12 changed files with 297 additions and 45 deletions

View File

@ -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
}

View File

@ -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,

View File

@ -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
}

View File

@ -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,
}
}

View 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 = {
}
}

View File

@ -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 = {

View File

@ -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

View File

@ -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

View File

@ -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")

View File

@ -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"

View File

@ -0,0 +1,10 @@
export default {
expiredToken: {
type: String,
required: true,
},
refreshToken: {
type: String,
required: true,
}
}

View File

@ -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 },