зеркало из https://github.com/github/docs.git
Fetch CSRF token over XHR (browser-side) (#29337)
* Client side csrf token grab * Update get-session.ts * Update get-session.ts * Update get-session.ts * Remove test refs to meta tag * Update get-session.ts * Update get-session.ts * Update get-session.ts * Update get-session.ts * Fix some type issues * Simplify api * Update components/lib/get-session.ts Co-authored-by: Rachael Sewell <rachmari@github.com> Co-authored-by: Rachael Sewell <rachmari@github.com>
This commit is contained in:
Родитель
b19e5a6ac3
Коммит
f79e1d2cb7
|
@ -1,8 +1,8 @@
|
|||
/* eslint-disable camelcase */
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import Cookies from 'js-cookie'
|
||||
import getCsrf from './get-csrf'
|
||||
import parseUserAgent from './user-agent'
|
||||
import { getSession, fetchSession } from './get-session'
|
||||
|
||||
const COOKIE_NAME = '_docs-events'
|
||||
|
||||
|
@ -83,8 +83,10 @@ function getMetaContent(name: string) {
|
|||
}
|
||||
|
||||
export function sendEvent({ type, version = '1.0.0', ...props }: SendEventProps) {
|
||||
const session = getSession()
|
||||
|
||||
const body = {
|
||||
_csrf: getCsrf(),
|
||||
_csrf: session?.csrfToken,
|
||||
|
||||
type,
|
||||
|
||||
|
@ -273,14 +275,16 @@ function initPrintEvent() {
|
|||
}
|
||||
|
||||
export default function initializeEvents() {
|
||||
initPageAndExitEvent() // must come first
|
||||
initLinkEvent()
|
||||
initClipboardEvent()
|
||||
initCopyButtonEvent()
|
||||
initPrintEvent()
|
||||
// survey event in ./survey.js
|
||||
// experiment event in ./experiment.js
|
||||
// search and search_result event in ./search.js
|
||||
// redirect event in middleware/record-redirect.js
|
||||
// preference event in ./display-tool-specific-content.js
|
||||
fetchSession().then(() => {
|
||||
initPageAndExitEvent() // must come first
|
||||
initLinkEvent()
|
||||
initClipboardEvent()
|
||||
initCopyButtonEvent()
|
||||
initPrintEvent()
|
||||
// survey event in ./survey.js
|
||||
// experiment event in ./experiment.js
|
||||
// search and search_result event in ./search.js
|
||||
// redirect event in middleware/record-redirect.js
|
||||
// preference event in ./display-tool-specific-content.js
|
||||
})
|
||||
}
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
export default function getCsrf() {
|
||||
const csrfEl = document.querySelector('meta[name="csrf-token"]')
|
||||
if (!csrfEl) return ''
|
||||
return csrfEl.getAttribute('content')
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
import { useState, useEffect } from 'react'
|
||||
import { useRouter } from 'next/router'
|
||||
|
||||
const MAX_CACHE = 5000 // milliseconds
|
||||
const RETRY = 500 // milliseconds
|
||||
|
||||
type Session = {
|
||||
isSignedIn: boolean
|
||||
csrfToken: string
|
||||
language: string // en, es, ja, cn
|
||||
userLanguage: string // en, es, ja, cn
|
||||
theme: object // colorMode, nightTheme, dayTheme
|
||||
themeCSS: object // colorMode, nightTheme, dayTheme
|
||||
}
|
||||
|
||||
let sessionCache: Session | null
|
||||
let lastUpdate: number | null
|
||||
|
||||
function isCacheValid() {
|
||||
return lastUpdate && Date.now() - lastUpdate < MAX_CACHE
|
||||
}
|
||||
|
||||
export function getSession() {
|
||||
return sessionCache
|
||||
}
|
||||
|
||||
// This function must only be called in the browser
|
||||
export async function fetchSession(): Promise<Session | null> {
|
||||
if (isCacheValid()) return sessionCache
|
||||
const response = await fetch('/api/session')
|
||||
if (response.ok) {
|
||||
sessionCache = await response.json()
|
||||
lastUpdate = Date.now()
|
||||
return sessionCache as Session
|
||||
}
|
||||
lastUpdate = null
|
||||
sessionCache = null
|
||||
await new Promise((resolve) => setTimeout(resolve, RETRY))
|
||||
return fetchSession()
|
||||
}
|
||||
|
||||
// React hook version
|
||||
export function useSession() {
|
||||
const [session, setSession] = useState<Session | null>(sessionCache)
|
||||
const { asPath } = useRouter()
|
||||
// Only call `fetchSession` on the client
|
||||
useEffect(() => {
|
||||
fetchSession().then((session) => setSession(session))
|
||||
}, [asPath])
|
||||
return session
|
||||
}
|
|
@ -127,10 +127,6 @@ function setHeaders(headers, res) {
|
|||
}
|
||||
|
||||
function mutateCheeriobodyByRequest($, req) {
|
||||
// A fresh CSRF token into the <meta> tag
|
||||
const freshCsrfToken = req.csrfToken()
|
||||
$('meta[name="csrf-token"]').attr('content', freshCsrfToken)
|
||||
|
||||
// Populate if you have the `dotcom_user` user cookie and it's truthy
|
||||
const isDotComAuthenticated = Boolean(req.cookies?.dotcom_user)
|
||||
|
||||
|
@ -153,7 +149,6 @@ function mutateCheeriobodyByRequest($, req) {
|
|||
// See https://github.com/cheeriojs/cheerio/releases/tag/v1.0.0-rc.11
|
||||
// and https://github.com/cheeriojs/cheerio/pull/2509
|
||||
const parsedNextData = JSON.parse(nextData.get()[0].children[0].data)
|
||||
parsedNextData.props.csrfToken = freshCsrfToken
|
||||
parsedNextData.props.dotComAuthenticatedContext.isDotComAuthenticated = isDotComAuthenticated
|
||||
parsedNextData.props.languagesContext.userLanguage = req.context.userLanguage
|
||||
parsedNextData.props.themeProps = {
|
||||
|
|
|
@ -16,7 +16,6 @@ import {
|
|||
import { defaultComponentTheme } from 'lib/get-theme.js'
|
||||
|
||||
type MyAppProps = AppProps & {
|
||||
csrfToken: string
|
||||
isDotComAuthenticated: boolean
|
||||
themeProps: typeof defaultComponentTheme & Pick<ThemeProviderProps, 'colorMode'>
|
||||
languagesContext: LanguagesContextT
|
||||
|
@ -25,7 +24,6 @@ type MyAppProps = AppProps & {
|
|||
const MyApp = ({
|
||||
Component,
|
||||
pageProps,
|
||||
csrfToken,
|
||||
themeProps,
|
||||
languagesContext,
|
||||
dotComAuthenticatedContext,
|
||||
|
@ -59,8 +57,6 @@ const MyApp = ({
|
|||
name="google-site-verification"
|
||||
content="c1kuD-K2HIVF635lypcsWPoD4kilo5-jA_wBFyT4uMY"
|
||||
/>
|
||||
|
||||
<meta name="csrf-token" content={csrfToken} />
|
||||
</Head>
|
||||
<SSRProvider>
|
||||
<ThemeProvider
|
||||
|
@ -95,7 +91,6 @@ MyApp.getInitialProps = async (appContext: AppContext) => {
|
|||
return {
|
||||
...appProps,
|
||||
themeProps: getTheme(req),
|
||||
csrfToken: req?.csrfToken?.() || '',
|
||||
languagesContext: { languages: req.context.languages, userLanguage: req.context.userLanguage },
|
||||
dotComAuthenticatedContext: { isDotComAuthenticated: Boolean(req.cookies?.dotcom_user) },
|
||||
}
|
||||
|
|
|
@ -165,15 +165,6 @@ describe('survey', () => {
|
|||
})
|
||||
})
|
||||
|
||||
describe('csrf meta', () => {
|
||||
it('should have a csrf-token meta tag on the page', async () => {
|
||||
await page.goto(
|
||||
'http://localhost:4000/en/actions/getting-started-with-github-actions/about-github-actions'
|
||||
)
|
||||
await page.waitForSelector('meta[name="csrf-token"]')
|
||||
})
|
||||
})
|
||||
|
||||
describe('platform picker', () => {
|
||||
// from tests/javascripts/user-agent.js
|
||||
const userAgents = [
|
||||
|
|
|
@ -17,28 +17,6 @@ const serializeTheme = (theme) => {
|
|||
describe('in-memory render caching', () => {
|
||||
jest.setTimeout(30 * 1000)
|
||||
|
||||
test('second render should be a cache hit with different csrf-token', async () => {
|
||||
const res = await get('/en')
|
||||
// Because these are effectively end-to-end tests, you can't expect
|
||||
// the first request to be a cache miss because another end-to-end
|
||||
// test might have "warmed up" this endpoint.
|
||||
expect(res.headers['x-middleware-cache']).toBeTruthy()
|
||||
const $1 = cheerio.load(res.text)
|
||||
const res2 = await get('/en')
|
||||
expect(res2.headers['x-middleware-cache']).toBe('hit')
|
||||
const $2 = cheerio.load(res2.text)
|
||||
const csrfTokenHTML1 = $1('meta[name="csrf-token"]').attr('content')
|
||||
const csrfTokenHTML2 = $2('meta[name="csrf-token"]').attr('content')
|
||||
expect(csrfTokenHTML1).not.toBe(csrfTokenHTML2)
|
||||
// The HTML is one thing, we also need to check that the
|
||||
// __NEXT_DATA__ serialized (JSON) state is different.
|
||||
const csrfTokenNEXT1 = getNextData($1).props.csrfToken
|
||||
const csrfTokenNEXT2 = getNextData($2).props.csrfToken
|
||||
expect(csrfTokenHTML1).toBe(csrfTokenNEXT1)
|
||||
expect(csrfTokenHTML2).toBe(csrfTokenNEXT2)
|
||||
expect(csrfTokenNEXT1).not.toBe(csrfTokenNEXT2)
|
||||
})
|
||||
|
||||
test('second render should be a cache hit with different dotcom-auth', async () => {
|
||||
// Anonymous first
|
||||
const res = await get('/en')
|
||||
|
|
Загрузка…
Ссылка в новой задаче