バックグラウンド
安らかなバックエンド、React + Redux フロントエンドがあり、CSRF および XSS 攻撃から保護しようとしています。
フロントエンドは、API から CSRF トークンを要求します。API 応答は、CSRF トークンを HttpOnly Cookie と応答本文に設定します。redux レデューサーは、トークンを (応答本文から) redux ストアに保存します。
メイン コンテナの でトークンを要求するとcomponentDidMount()
、すべてが機能しますが、懸念されるのは、これがワン ショットであるということです。代わりに、API へのリクエストはカスタム ミドルウェアを経由するため、ミドルウェアがローカルに存在しない場合は CSRF トークンをリクエストすることをお勧めします。
問題
フローは次のとおりです (Chrome 50 および Firefox 47 でテスト済み)。
- CSRF トークンが要求されました。HttpOnly cookie および redux ストアに格納されたトークン
X-CSRF-Token
ヘッダー セットで要求された元の API 呼び出し。クッキーが送信されない- Cookie がないため、API から 403 を受け取ります。API は新しい HttpOnly cookieで応答します。Javascript はこの Cookie を認識できないため、redux ストアは更新されません。
- ステップ 2 の X-CSRF-Token ヘッダーとステップ 3 の Cookie を使用して要求された追加の API 呼び出し。
- Cookie と X-CSRF-Token の不一致により 403 を受信する
でステップ 2 の前に遅延を追加するとwindow.setTimeout
、Cookie はまだ送信されないので、ブラウザーが Cookie を保存するのに十分な時間がないという競合状態ではないと思いますか?
アクションクリエーター
const login = (credentials) => {
return {
type: AUTH_LOGIN,
payload: {
api: {
method: 'POST',
url: api.v1.auth.login,
data: credentials
}
}
};
};
ミドルウェア
/**
* Ensure the crumb and JWT authentication token are wrapped in all requests to the API.
*/
export default (store) => (next) => (action) => {
if (action.payload && action.payload.api) {
store.dispatch({ type: `${action.type}_${PENDING}` });
return ensureCrumb(store)
.then((crumb) => {
const state = store.getState();
const requestConfig = {
...action.payload.api,
withCredentials: true,
xsrfCookieName: 'crumb',
xsrfHeaderName: 'X-CSRF-Token',
headers: {
'X-CSRF-Token': crumb
}
};
if (state.auth.token) {
requestConfig.headers = { ...requestConfig.headers, Authorization: `Bearer ${state.auth.token}` };
}
return axios(requestConfig);
})
.then((response) => store.dispatch({ type:`${action.type}_${SUCCESS}`, payload: response.data }))
.catch((response) => store.dispatch({ type: `${action.type}_${FAILURE}`, payload: response.data }));
}
return next(action);
};
/**
* Return the crumb if it exists, otherwise requests a crumb
* @param store - The current redux store
* @returns Promise - crumb token
*/
const ensureCrumb = (store) => {
const state = store.getState();
return new Promise((resolve, reject) => {
if (state.crumb.token) {
return resolve(state.crumb.token);
}
store.dispatch({ type: CRUMB_PENDING });
axios.get(api.v1.crumb)
.then((response) => {
store.dispatch({ type: CRUMB_SUCCESS, payload: { token: response.data.crumb } });
window.setTimeout(() => resolve(response.data.crumb), 10000);
// return resolve(response.data.crumb);
})
.catch((error) => {
store.dispatch({ type: CRUMB_FAILURE });
return reject(error);
});
});
};