Closed PaulKushch closed 4 years ago
I have the same question. Is it enough to move the route to /api/auth/test
?
It seems from looking at the code and documentation that only /api/auth/signin
and /api/auth/signout
routes are checked for a valid csrf token.
https://github.com/nextauthjs/next-auth/blob/main/src/server/index.js
Any update?
All HTTP POST requests are checked for a valid CSRF token (e.g. sign in, sign out).
NextAuth.js implements CSRF tokens but is not itself an CSRF token implementation for the rest of your site.
It's technically possible to use the token created by NextAuth.js elsewhere in your app, but a little complicated to explain due to caveats (to support things like cookie prefixes, secret values, etc vary between prod and dev environments).
The code for how NextAuth.js verifies the token is at https://github.com/nextauthjs/next-auth/blob/main/src/server/index.js#L146 and you could use that as a reference to implement a method (it's not terribly complex, but you can get an idea of the issues).
If folks could would find it useful to have an API function to allow tokens to be easily verified, that would make a good feature request.
I'm putting here what I came up with for someone to check/comment. It seems to work.
import { createHash } from 'crypto';
import { parseUrl } from '../../../utils'; // see https://github.com/nextauthjs/next-auth/blob/main/src/lib/parse-url.js
const secret = process.env.SECRET;
async function verifyCsrfToken({req}) {
const csrfMethods = ['POST', 'PUT', 'PATCH', 'DELETE'];
try {
const { body } = req
const {
csrfToken: csrfTokenFromBody
} = body;
const parsedUrl = parseUrl(process.env.NEXTAUTH_URL || process.env.VERCEL_URL);
const baseUrl = parsedUrl.baseUrl;
const useSecureCookies = baseUrl.startsWith('https://')
const csrfProp = `${useSecureCookies ? '__Host-' : ''}next-auth.csrf-token`;
if (req.cookies[csrfProp]) {
const [csrfTokenValue, csrfTokenHash] = req.cookies[csrfProp].split('|');
if (csrfTokenHash === createHash('sha256').update(`${csrfTokenValue}${secret}`).digest('hex')) {
// If hash matches then we trust the CSRF token value
// If this is a method that is allowed to use csrf protection and the CSRF Token in the request body matches
// the cookie we have already verified is one we have set, then token is verified!
if (csrfMethods.includes(req.method) && csrfTokenValue === csrfTokenFromBody) return true;
}
}
return false;
} catch (error) {
console.log(error);
return false;
}
}
Use like this, eg in an API route:
export default async (req, res) => {
const isCsrfValid = await verifyCsrfToken({req});
...
}
}
Hi @chrishornmem i've taken what you have an changed it a little bit. The core is there, just a few tweaks really
import { createHash } from 'crypto';
import { NextApiRequest } from 'next';
/**
* Verify that the token you want to check matches the token in the next-auth cookie
*
* Note this verify check has been created based on the code within next-auth: ^3.1.0 future
* versions might differ
*
* @param req
* @param tokenToCheck
* @return boolean
*/
const verifyNextAuthCsrfToken = (req: NextApiRequest, tokenToCheck: string) => {
const secret: string = process.env.APP_SECRET_STRING;
const csrfMethods = ['POST', 'PUT', 'PATCH', 'DELETE'];
if(!csrfMethods.includes(req.method)) {
// we dont need to check the CSRF if it's not within the method.
return true;
}
try {
const useSecureCookies = process.env.NEXTAUTH_URL.startsWith('https://')
const csrfProp = `${useSecureCookies ? '__Host-' : ''}next-auth.csrf-token`;
if (req.cookies[csrfProp]) {
const cookieValue = req.cookies[csrfProp];
const cookieSplitKey = cookieValue.match("|") ? "|" : "%7C";
const [csrfTokenValue, csrfTokenHash] = cookieValue.split(cookieSplitKey);
const generatedHash = createHash('sha256').update(`${tokenToCheck}${secret}`).digest('hex');
if (csrfTokenHash === generatedHash) {
// If hash matches then we trust the CSRF token value
if (csrfTokenValue === tokenToCheck) return true;
}
}
return false;
} catch (error) {
return false;
}
}
export default verifyNextAuthCsrfToken;
For those wondering:
const cookieSplitKey = cookieValue.match("|") ? "|" : "%7C";
is there because the value seems to be URL encoded on Vercel.
Oh and dont forget that:
const secret: string = process.env.APP_SECRET_STRING;
needs to be the same value as the secret sent to NextAuth setOptions
const getOptions = () => {
const options = {
...
secret: process.env.APP_SECRET_STRING,
...
}
};
ok, final update here. I've just been testing this on vercel and i had some issues. Turns out that the url i store as an env (which is the vercel_url) doesnt include the protocol. So you can do one of two things i reckon.
process.env.SSL
Then you can update this line:
const useSecureCookies = process.env.NEXTAUTH_URL.startsWith('https://')
to be something like:
const useSecureCookies = process.env.SSL;
or
const useSecureCookies = process.env.VERCEL_ENV !== "development";
I wanted to share a bare-bones proof-of-concept of how to secure an API route using the CSRF token created by NextAuth:
import { createHash } from 'crypto'
export default function handler(req, res) {
try{
if(!req.headers.cookie){
res.status(403).json({ status: 'no cookie?'})
}
const rawCookieString = req.headers.cookie // raw cookie string, possibly multiple cookies
const rawCookiesArr = rawCookieString.split(';')
let parsedCsrfTokenAndHash = null
for(let i=0; i<rawCookiesArr.length; i++){ // loop through cookies to find CSRF from next-auth
let cookieArr = rawCookiesArr[i].split('=')
if(cookieArr[0].trim() === 'next-auth.csrf-token'){
parsedCsrfTokenAndHash = cookieArr[1]
break
}
}
if(!parsedCsrfTokenAndHash){
res.status(403).json({ status: 'missing csrf'}) // can't find next-auth CSRF in cookies
}
// delimiter could be either a '|' or a '%7C'
const tokenHashDelimiter = parsedCsrfTokenAndHash.indexOf('|') !== -1 ? '|' : '%7C'
const [requestToken, requestHash] = parsedCsrfTokenAndHash.split(tokenHashDelimiter)
const secret = process.env.SECRET
// compute the valid hash
const validHash = createHash('sha256').update(`${requestToken}${secret}`).digest('hex')
if(requestHash !== validHash){
res.status(403).json({ status: 'bad hash'}) // bad hash
}
}catch(err){
res.status(500).json({ status: 'catch-all no'})
}
// otherwise, if everything checks out ..
let responseOutput = { status: 'success' }
if(req.body){
// how to access data sent by client
responseOutput['received_data'] = req.body
}
res.status(200).json(responseOutput)
}
@jayliew can you explain last 8 lines. It throws error on my editor
edit: Got it, just a Typescript error. this would work fine
// otherwise, if everything checks out ..
let responseOutput: { status: string; received_data?: any } = {
status: 'success'
}
if (req.body) {
// how to access data sent by client
responseOutput['received_data'] = req.body // ??? find out this
}
res.status(200).json(responseOutput)
}
@jayliew I found your codes working with little changes but I found two things worth mentioning, to make it practically usable.
This would only work if you have process.env.SECRET
in your .env.local
file, and secret: process.env.SECRET
in your NexxtAuthOptions
. The default secret
generation won't let it pass through this.
I'm using this code:
// ./middlewares/csrf_validator.ts
import { createHash } from 'crypto'
import { NextApiRequest, NextApiResponse } from 'next'
export default (req: NextApiRequest, res: NextApiResponse): boolean => {
try {
if (!req.headers.cookie) {
res.status(403).json({ error: { status: 'no cookie?' } })
return false
} else {
const rawCookieString = req.headers.cookie // raw cookie string, possibly multiple cookies
const rawCookiesArr = rawCookieString.split(';')
let parsedCsrfTokenAndHash = null
for (let i = 0; i < rawCookiesArr.length; i++) {
// loop through cookies to find CSRF from next-auth
let cookieArr = rawCookiesArr[i].split('=')
if (cookieArr[0].trim().search('next-auth.csrf-token')) {
// because on vercel, token is named _Host-next-auth.csrf-token
parsedCsrfTokenAndHash = cookieArr[1]
break
}
}
if (!parsedCsrfTokenAndHash) {
res.status(403).json({ error: { status: 'missing csrf' } })
// can't find next-auth CSRF in cookies
return false
} else {
// delimiter could be either a '|' or a '%7C'
const tokenHashDelimiter =
parsedCsrfTokenAndHash.indexOf('|') !== -1 ? '|' : '%7C'
const [requestToken, requestHash] = parsedCsrfTokenAndHash.split(
tokenHashDelimiter
)
const secret = process.env.SECRET
// compute the valid hash
const validHash = createHash('sha256')
.update(`${requestToken}${secret}`)
.digest('hex')
if (requestHash !== validHash) {
res.status(403).json({ error: { status: 'bad hash' } }) // bad hash
return false
}
}
}
} catch (err) {
res.status(500).json({ error: { status: 'catch-all no' } })
return false
}
return true
}
// ./pages/api/new_user.ts
//...codes
if (!csrf_validator(req, res)) return
// if token failed, it won't run codes next, otherwise there would be [ERR_HTTP_HEADERS_SENT] error
// if token passed it will return true, then you can run rest of your codes.
// this could also be achieved by checking for res.headerSent() which returns boolean
//...codes
i try to validate csrf too, but i found authorize credentials, only sent the first part of csrf token along with username/password. something like this:
credentials: {
"csrfToken":"1895edb24f50f2b2accb7fb51a9702143cac20b81efa025039ad14869eca2fb1",
"username":"jokowi",
"password":"passwd"
}
from the explanation above, validation should be taking from hashing the first part of csrf token + secret
, compare to second part of the scrf
. like below:
if (hashing(first_part_csrf + secret) !== second_part_scrf) return not valid
return valid
how can we validate the csrf only using the first part?
i know we can get the full csrf from request raw header
on authorize credential
, but maybe there is another more proper way to do this?
You don't have to validate the CSRF token yourself, we do that already! :)
https://github.com/nextauthjs/next-auth/blob/main/src/server/lib/csrf-token-handler.js
So when the authorize
callback is fired, you should already be good :wink:
I think this should be documented since documentation on https://next-auth.js.org/tutorials/securing-pages-and-api-routes gives an impression that the api routes are also secured by CSRF protection but this is not the case.
Where do you get that impression? I checked the page you linked to, I don't see a reference to CSRF throughout. Happy to take any documentation clarification PR if you think something is missing though!
Hmm there is no mention of it, but the header states: "Securing API Routes", which indeed as far as I understand only validate the jwt session cookie. Perhaps a note should be added that this is only using the session cookie and still vulnerable to (csrf etc) attacks.
Maybe I'm missing something, but whenever you use Next.js API routes, it's in principal the same if you used an external API. That external API will also only receive/should care about the JWT token, and after decoding it, it will use its value. We use CSRF tokens for user related actions like sign-in or sign-out, but for securing APIs, I'm not aware you would need CSRF protection.
I think there is a misunderstanding in the purpose of CSRF tokens: https://stackoverflow.com/a/10741650
Also, this discussion might be relevant maybe?: #2381
In any case, please open a PR with your suggestions and I'll discuss it with the others!
The use case is form submission to a custom api route using eg post with csrf token in body params.
On 16 Jul 2021, at 22:40, Balázs Orbán @.***> wrote:
Maybe I'm missing something, but whenever you use Next.js API routes, it's in principal the same if you used an external API. That external API will also only receive the JWT token, and after decoding it, it will use its value. We use CSRF tokens for user related actions like signin or signOut, but for securing APIs, I'm not aware you would need CSRF protection.
— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub, or unsubscribe.
Hi there, as I am in the process of implementing next-auth to my application and looking for the most efficient way to secure it against csrf attacks, I ended up here.
I would like to strengthen the points of earlier comments.
Where do you get that impression? I checked the page you linked to, I don't see a reference to CSRF throughout. Happy to take any documentation clarification PR if you think something is missing though!
In the documentation is stated "The CSRF token returned by this endpoint must be passed as form variable named csrfToken in all POST submissions to any API endpoint.". I guess that this could be missunderstood as this is correct only for the next-auth API endpoints.
Maybe I'm missing something, but whenever you use Next.js API routes, it's in principal the same if you used an external API. That external API will also only receive/should care about the JWT token, and after decoding it, it will use its value. We use CSRF tokens for user related actions like sign-in or sign-out, but for securing APIs, I'm not aware you would need CSRF protection.
I think there is a misunderstanding in the purpose of CSRF tokens: https://stackoverflow.com/a/10741650
Also, this discussion might be relevant maybe?: #2381
In any case, please open a PR with your suggestions and I'll discuss it with the others!
Which brings me to my actual issue. I think it is very useful that next-auth brings csrf-protection with it. The issue is, that a web application typically has many more API endpoints which are receiving POST requests (mutating data through the backend). Those need to be secured against csrf attacks too. It would be extremely useful if there was a server-side method exposed by next-auth to verify the csrf token for custom api routes to use the solution throughout the entire application. Otherwise it is necessary to integrate an additional csrf mitigation strategy on top of next-auth.
@chrishornmem Exactly. However since my last comment a lot has been changed, I wonder if using unstable_getServerSession also protects against CSRF attacks when using in custom api (post) routes @balazsorban44 ?
@typedashutosh @jayliew Is all of that ceremony necessary? The below works for me on Next 12. Also req.cookies is an object which saves a lot of work.
import { getCsrfToken } from 'next-auth/react';
import { NextApiRequest, NextApiResponse } from 'next';
const NEXTAUTH_CSRF_COOKIE_NAME = 'next-auth.csrf-token';
type Handler = (req: NextApiRequest, res: NextApiResponse) => void;
const validateCsrfPost =
(handler: Handler) => async (req: NextApiRequest, res: NextApiResponse) => {
if (req.method === 'POST') {
const token = await getCsrfToken({ req });
// Bail out if user's csrf cookie doesn't match session token
if (req.cookies[NEXTAUTH_CSRF_COOKIE_NAME].split('|')?.[0] !== token) {
res.status(422).send(null);
return;
}
}
return handler(req, res);
};
export default validateCsrfPost;
and usage, wrapping an API route:
const action = validateCsrfPost(
async (req: NextApiRequest, res: NextApiResponse) => {
....
}
);
What I'm unsure of is the security of this test. getCsrfToken
depends on the request/user's cookies. So I'm curious if the user directly edits the next-auth.csrf-token
or edits their cookie in another way, would that bypass the check?
I tested with EditThisCookie and manually changing my 'next-auth.csrf-token'
value. When I do this. getCsrfToken
on the server side generates a new csrf token. So I'm guessing there's some kind of signature checking that happens inside of getCsrfToken
(i'm guessing the data after the |
is the sig?) but would be grateful if @iaincollins or other member could validate if this is a secure approach to reuse NextAuth's csrf token for arbitrary API routes.
Also, nextjs middleware doesn't allow blocking the request nor changing the status code, which I don't understand at all, which is why I went with a higher order handler function.
Why is this not part of Next Auth's public methods is beyond me! Why are every projects who need this in user land must implement their own functions like this one?
// ./pages/api/some-api-endpoint.js
import { /*createCSRFToken,*/ verifyCSRFToken } from 'next-auth/core/lib/csrf-token';
export default function handler(req, res) {
if (req.method === 'POST') {
if (!verifyCSRFToken(req.body.csrfToken)) return;
// ...
} else {
res.setHeader('Allow', ['POST'])
res.status(405).json({ success:false, error:`Method ${req.method} Not Allowed` });
}
}
I wanted to share a bare-bones proof-of-concept of how to secure an API route using the CSRF token created by NextAuth:
import { createHash } from 'crypto' export default function handler(req, res) { try{ if(!req.headers.cookie){ res.status(403).json({ status: 'no cookie?'}) } const rawCookieString = req.headers.cookie // raw cookie string, possibly multiple cookies const rawCookiesArr = rawCookieString.split(';') let parsedCsrfTokenAndHash = null for(let i=0; i<rawCookiesArr.length; i++){ // loop through cookies to find CSRF from next-auth let cookieArr = rawCookiesArr[i].split('=') if(cookieArr[0].trim() === 'next-auth.csrf-token'){ parsedCsrfTokenAndHash = cookieArr[1] break } } if(!parsedCsrfTokenAndHash){ res.status(403).json({ status: 'missing csrf'}) // can't find next-auth CSRF in cookies } // delimiter could be either a '|' or a '%7C' const tokenHashDelimiter = parsedCsrfTokenAndHash.indexOf('|') !== -1 ? '|' : '%7C' const [requestToken, requestHash] = parsedCsrfTokenAndHash.split(tokenHashDelimiter) const secret = process.env.SECRET // compute the valid hash const validHash = createHash('sha256').update(`${requestToken}${secret}`).digest('hex') if(requestHash !== validHash){ res.status(403).json({ status: 'bad hash'}) // bad hash } }catch(err){ res.status(500).json({ status: 'catch-all no'}) } // otherwise, if everything checks out .. let responseOutput = { status: 'success' } if(req.body){ // how to access data sent by client responseOutput['received_data'] = req.body } res.status(200).json(responseOutput) }
Any updates on this one? we are also looking for a solution to use the anti-CSRF implementation in next-auth for other API routes
Maybe like this? Btw why is this closed? https://stackoverflow.com/a/76110413/846348
@jayliew, @typedashutosh, thanks for the code!
@typedashutosh, please note that in
for (let i = 0; i < rawCookiesArr.length; i++) { // loop through cookies to find CSRF from next-auth let cookieArr = rawCookiesArr[i].split('=') if (cookieArr[0].trim().search('next-auth.csrf-token')) { // because on vercel, token is named _Host-next-auth.csrf-token parsedCsrfTokenAndHash = cookieArr[1] break } }
String search() returns truthy -1
for "not found" or position for "found".
You probably intended to write something like
if (cookieArr[0].trim().search('next-auth.csrf-token') !== -1) {
why isn't it a publicly exposed method instead of us rolling out our own solution which can easily break if we're not careful enough.
I did this in Next 14, with app router thanks to @typedashutosh
import { createHash } from 'crypto'
import { cookies } from 'next/headers'
export const verifyCsrfToken = (): boolean => {
try {
const cookie = cookies().get('next-auth.csrf-token')
if (!cookie) {
return false
}
const parsedCsrfTokenAndHash = cookie.value
if (!parsedCsrfTokenAndHash) {
return false
}
// delimiter could be either a '|' or a '%7C'
const tokenHashDelimiter =
parsedCsrfTokenAndHash.indexOf('|') !== -1 ? '|' : '%7C'
const [requestToken, requestHash] = parsedCsrfTokenAndHash.split(
tokenHashDelimiter
)
const secret = process.env.NEXTAUTH_SECRET
// compute the valid hash
const validHash = createHash('sha256')
.update(`${requestToken}${secret}`)
.digest('hex')
if (requestHash !== validHash) {
return false
}
} catch (err) {
return false
}
return true
}
in my API route:
if (!verifyCsrfToken()) {
return NextResponse.json({ error: 'Invalid CSRF token' }, { status: 403 })
}
Your question Hi, I wonder how to validate csrf-token in a custom post request to custom route?
What are you trying to do I add csrf-token as a header in fetch: