Skip to content

Instantly share code, notes, and snippets.

@takayamaekawa
Last active January 25, 2025 04:15
Show Gist options
  • Select an option

  • Save takayamaekawa/bcaeeb99584272870308996b66a7e756 to your computer and use it in GitHub Desktop.

Select an option

Save takayamaekawa/bcaeeb99584272870308996b66a7e756 to your computer and use it in GitHub Desktop.
セキュアなウェブソケット通信を目指してる
// views/chat.ejs
// https://github.com/bella2391/FMCWebApp/blob/master/src/views/chat.ejs
// ローカル環境でもリモート環境でも動くように動的なwsのURLをfetchAPIで取得していますが、気にしないでください。
<html>
...
<input type="hidden" id="_csrf" value="<%= csrfToken %>">
<input type="hidden" id="token" value="<%= token %>">
<script>
const csrfTokenInput = document.getElementById('_csrf');
const tokenInput = document.getElementById('token');
if (!csrfTokenInput || !tokenInput) {
console.error('Invaild Access');
return;
}
const csrfToken = csrfTokenInput.value;
const token = tokenInput.value;
fetch('/api/config')
.then(response => response.json())
.then(config => {
const ws = new WebSocket(`${config.websocketUrl}?token=${token}`);
ws.addEventListener('open', () => {
ws.send(JSON.stringify({
user: htmlspecialchars(name),
method: 'connect',
csrfToken
}));
});
...
});
</script>
...
</html>
// routes/chat.ts
// https://github.com/bella2391/FMCWebApp/blob/master/src/routes/chat.ts
// `jwt.sign(...)`で渡す`payload`の中に、`csrfToken`をいれる
// (`csrfToken`をJWTの`sign`配列でラップする)
router.get('/', (req: Request, res: Response) => {
const csrfToken = req.csrfToken ? req.csrfToken() : undefined;
if (!csrfToken) {
res.status(400).send('Invalid Access');
return;
}
const payload: Jsonwebtoken.WebSocketJwtPayload = { csrfToken };
const token: string = jwt.sign(payload, JWT_SECRET, { expiresIn: '1h' });
const encodedToken = encodeURIComponent(token);
res.render(`chat`, { token: encodedToken });
});
// @types/[].d.ts
// https://github.com/bella2391/FMCWebApp/blob/master/src/%40types/express/index.d.ts
declare global {
namespace Jsonwebtoken {
interface UserAuthJwtPayload extends JwtPayload {
id: string;
name: string;
email: string;
}
interface WebSocketJwtPayload extends JwtPayload {
csrfToken: string;
}
}
namespace http {
interface IncomingMessageWithPayload extends IncomingMessage {
payload?: Jsonwebtoken.WebSocketJwtPayload;
}
}
}
// middlewares/localvals.ts
// https://github.com/bella2391/FMCWebApp/blob/master/src/middlewares/localvals.ts
// 後の__chat.ejs__テンプレートに渡します(常にcsrf用のトークンが生成&__res.locals__へ入る)
import csurf from 'csurf';
...
const localvals = (req: Request, res: Response, next: NextFunction) => {
res.locals.csrfToken = req.csrfToken ? req.csrfToken() : '';
...
};
export default localvals;
...
// services/websocket.ts
// https://github.com/bella2391/FMCWebApp/blob/master/src/services/websocket.ts
import { WebSocket, WebSocketServer } from 'ws';
const isAuthenticated = (json, payload: Jsonwebtoken.WebSocketJwtPayload): boolean => {
const jsonCsrfToken = json.csrfToken;
const payloadCsrfToken = payload.csrfToken;
return jsonCsrfToken && payloadCsrfToken && jsonCsrfToken === payloadCsrfToken;
};
const websocket = () => {
const wss = new WebSocketServer({ noServer: true });
const clients = new Set<WebSocket>();
wss.on('connection', (ws: WebSocket, request: http.IncomingMessageWithPayload) => {
console.log('client has connected');
const { payload } = request;
if (!payload) {
ws.close();
throw new Error('Invalid Access')
}
clients.add(ws);
ws.on('message', (msg: string) => {
const json = JSON.parse(msg);
if (!isAuthenticated(json, payload)) {
ws.close();
throw new Error("Invalid Access");
}
// 通常のメッセージ処理
});
// bin/www.ts
// https://github.com/bella2391/FMCWebApp/blob/master/src/bin/www.ts
...
server.on('upgrade', (request, socket, head) => {
if (!request.url) {
throw new Error("Invalid Access");
}
const reqUrl = getHPURL(false) + request.url;
try {
const parsedUrl = new URL(reqUrl);
if (parsedUrl.pathname === basepath.wsrootpath) {
const query = parsedUrl.searchParams;
const token = query.get('token');
if (!token) {
throw new Error("Invalid Access");
}
const decodeToken = decodeURIComponent(token);
const payload = jwt.verify(decodeToken, JWT_SECRET) as Jsonwebtoken.WebSocketJwtPayload;
(request as any).payload = payload;
wss.handleUpgrade(request, socket, head, (ws) => {
wss.emit('connection', ws, request);
});
}
} catch (error) {
console.error('socket error:', error);
socket.destroy();
}
});
...
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment