EXT:felogin
EXT: works out of the box with headless. The headless XClass
on Login swaps the HTML view for a JSON response that
includes the form definition, the login status and any redirect
target.
Setup
Standard felogin setup — install (composer require typo3/),
drop a login plugin onto a page, configure storage pages in plugin
settings.
You can test the flow without a frontend with curl. TYPO3 v14 only
evaluates credentials that are accompanied by a valid, nonce-signed
__, so a login is always two requests: fetch the form
first, then post it back together with its cookies.
# 1) Fetch the login form; store the typo3nonce_* cookie
curl -s -c cookies.txt -H 'Accept: application/json' \
https://api.example.com/login-page
# 2) Post credentials plus EVERY hidden field returned in
# form.elements, sending the cookies back — the __RequestToken
# is only valid together with its typo3nonce_* cookie
curl -i -b cookies.txt -c cookies.txt -X POST \
-H 'Accept: application/json' \
https://api.example.com/login-page \
--data-urlencode 'user=joe' \
--data-urlencode 'pass=secret' \
--data-urlencode 'logintype=login' \
--data-urlencode 'pid=<value from form.elements>' \
--data-urlencode '__RequestToken=<value from form.elements>'
# …plus all remaining hidden fields from form.elements, verbatim
A successful login responds with a set- header carrying the
session cookie (fe_). On failure the JSON status flips
to failure and message carries the rendered header/text for the
failure state.
Note
On a MIXED-mode site (headless: 2) every request shown here must
send exactly Accept: application/ as the first Accept header
value. Lists such as application/ (the
axios/fetch default) or a ;charset= suffix fall back to HTML
rendering.
JSON response shape
The plugin output sits under the content element's content. key:
{
"content": {
"colPos0": [{
"type": "felogin_login",
"content": {
"data": {
"form": {
"title": "Login",
"action": "https://api.example.com/login-page",
"method": "POST",
"elements": []
},
"message": { "header": "…", "message": "…" },
"status": "success",
"recovery": "https://api.example.com/login-page?…",
"flashMessages": []
}
}
}]
}
}
recovery always carries the password-recovery URI (never null).
form. contains every input the frontend must submit. Echo
each hidden field back verbatim, under its exact name:
logintype,pid,redirect_,url noredirect,redirect,Referrer refererand — when permalogin is offered —permalogin: plain felogin fields, unprefixed.tx_,felogin_ login [__ referrer] [@extension] …,[@controller] …,[@action] …,[arguments] …: extbase referrer fields, HMAC-protected.[@request] tx_: the extbase property mapping token. It is emitted with the plugin prefix and must be posted back under that prefixed name.felogin_ login [__ trusted Properties] __: a TYPO3 RequestToken with scopeRequest Token core/, signed against theuser- auth/ fe typo3nonce_*cookie set on the GET request. This is the one field that stays unprefixed.
Without a complete, unmodified set of these fields the login is rejected before credentials are even checked.
When a redirect target applies, the plugin payload is replaced
entirely by { "redirect
— the frontend performs the redirect itself.
Cookies & cross-domain
The session cookie is scoped to the API domain by default. If your
frontend lives on a different host, set a shared
$GLOBALS, or enable the
headless. feature flag to derive it per site —
see Multi-Site & URL Configuration.
Detect login in your own code
use TYPO3\CMS\Core\Context\Context;
$context = GeneralUtility::makeInstance(Context::class);
$isLoggedIn = $context->getPropertyFromAspect('frontend.user', 'isLoggedIn');
Or for the user's data:
$userId = (int)$context->getPropertyFromAspect('frontend.user', 'id');
$groups = $context->getPropertyFromAspect('frontend.user', 'groupIds');
Listening for successful login
Headless ships Login that decorates the JSON
view with status = success. To run your own logic at the same
point, listen to TYPO3 core's
TYPO3CMSFrontend — see
Events you can listen to for listener registration.