graphql-authentication icon indicating copy to clipboard operation
graphql-authentication copied to clipboard

Obtaining a valid CSRF

Open robinbeatty opened this issue 1 year ago • 11 comments

Just wanted to some advice on how I would go about obtaining a valid CSRF for an authenticated user?

I need to hit some craft factory actions and some custom endpoint validated by CSRF from a headless application in which the user is authenticated by your plugin.

Have tried appending a CSRF token to the JWT payload like this:

` public function addJwtClaims(JwtCreateEvent $event) { $builder = $event->builder;

    $request = Craft::$app->getRequest();
    $csrfToken = $request->getCsrfToken();
    $csrfIsValidForUser = $request->validateCsrfToken($csrfToken);
    $builder->withClaim('csrf_token', $csrfToken);
    $builder->withClaim('csrf_token_valid', $csrfIsValidForUser); // true in all cases
}

...

Event::on(
        TokenService::class,
        TokenService::EVENT_BEFORE_CREATE_JWT,
        [$this, 'addJwtClaims']
    );

`

I then decode the base64 JWT and retrieve the token.

When I use this token in the req headers (X-CSRF-Token) it won't validate (Bad request etc).

Have also tried explicitly returning just the token from a separate endpoint (actionGetCsrf) which gives me a different token but still not valid.

Any advice? Feel like there's a crucial part of the puzzle I'm missing... Thanks

Craft 4.9.5

robinbeatty avatar Jan 13 '25 23:01 robinbeatty

Have also noticed that the token obtained by either of these methods is a bit shorter than a token that might get rendered in the CP.

Example CP token: lCaJegjXepVVPawN6VAFCl9dxHx56cX37pG-kW3BeHlTEZR-Wo1QBW6ZUuUj21Vi99tZEiG5retiMzeB2SUl2GCRpVuQwjIT2p0rvO8sJ1J1CeHQ__9d0VKVEBMagoSaCSYcXSzDs8PZ7e0J8Q5tbjW1dblqsTDsD5dOcwwSYJmNHAv4SyX_ydOgP1YFFPrIptbQ-XDmEVq0IOwHW-Jk0BdJZhsPkRELh5GlhcSI-huoCTsrfMFJKL0AdRL9ZdARujYBwb9vcRfcnNIBAgO46hER61LzxDmipgEm76RPjNtNQWpMONS2nslvt2LxJiJ_4LysaxB_PBn3h_fogLtmmlwONEznE_cDiHegY9McJmFgDs3fd17OIFOWuJHYUgMyZsqY1qrh

Example token returned by Craft::$app->getRequest()->getCsrfToken(): DS2ZbvN898b1ih-vqLxQXnyh0RWKn-Cl6cHRxMbYKQbhCTi-NwkYV3lM-ALCOrnrmLJd-J_jKmhI9Llm68mk9q2ppame9V1HmXtZiVVELAg=

^^ this looks like it might be still be base64 encoded, though it won't decode with atob... ??

robinbeatty avatar Jan 13 '25 23:01 robinbeatty

Hey, any insight on this? Forgive me if I've missed a crucial memo... in my headless app i need to get a valid csrf at auth time to be able to hit some craft factory endpoints.

robinbeatty avatar May 29 '25 22:05 robinbeatty

Having the same issue, was this addressed?

LucaDelBuono avatar Nov 11 '25 09:11 LucaDelBuono

Turns out you need to pass an Authorization header with the JWT, and this satisfies the validation (that would otherwise be validated against CSRF) e.g. for Craft::app->elements->saveElement()

robinbeatty avatar Nov 11 '25 11:11 robinbeatty

I tried that, didn't work for me :-/

LucaDelBuono avatar Nov 11 '25 11:11 LucaDelBuono

This what what my controller looks like:

class ProjectsController extends Controller
{   
	protected array|bool|int $allowAnonymous = [
		'duplicate-itinerary'
	];

    public $enableCsrfValidation = false; // disable csrf check
    

	public function actionDuplicateItinerary(): Response
	{
		// do stuff
}
}

I'm posting headers from the front-end like this:

const headers = {
        'Accept': 'application/json',
        'Content-Type': 'application/json',
        'X-Requested-With': 'XMLHttpRequest',
        'Authorization': `JWT ${jwt}`
    }

robinbeatty avatar Nov 11 '25 11:11 robinbeatty

What I'm trying to do is similar but not quite. I'm authenticating the user on my headless Craft-based SPA developed in React using this plugin. But then I need to hit some of Craft's built-in actions. In particular, I'm trying with users/save-user. As I said, I tried passing the Authorization header using the JWT I have from the authentication but that doesn't work. Craft's documentation says you have to pass a CSRF header with POST requests, so I tried pre-flighting my POST request with a request to user/session-info, which does return a CSRF (see here: https://craftcms.com/docs/5.x/development/forms.html#ajax), but it's different every time (if you run it 10 times, you get 10 different values), and therefore, appending it to the following POST request to user/save-user returns a 400 Bad Request (Unable to verify your data submission).

LucaDelBuono avatar Nov 11 '25 13:11 LucaDelBuono

Also, I see you disabled the CSRF check. If I do that in the general.php Craft config with ->enableCsrfProtection(false), and send a POST request from the SPA using the Authorization header containing the JWT, I get a different error: 403 Forbidden (User is not authorized to perform this action)

LucaDelBuono avatar Nov 11 '25 14:11 LucaDelBuono

I have not be able to call any of crafts factory functions in this way... so yeah we have the same problem. TBH I've just been cutting and pasting snippets from the vendor folder. Be great to have a response from plugin developer on this @jamesedmonston ?

robinbeatty avatar Nov 11 '25 15:11 robinbeatty

Or @brandonkelly :)

LucaDelBuono avatar Nov 11 '25 15:11 LucaDelBuono

Specifically for updating a user, I believe you can call the graphql mutation updateViewer(...props) as exposed by the plugin

robinbeatty avatar Nov 11 '25 18:11 robinbeatty