
I was thinking that in this edition of my newsletter I will talk about security, because security is fun 🙂
This will also exemplify how customizable Nuxeo is. I bet most of you didn't even realize it was possible to implement what I'm about to show-not to mention that it's possible at all!
If you are a Nuxeo user or developer, you're probably familiar with the external share feature. The use case is granting a permission (any!) on a document (file, folder, etc.) to a user who is not an internal user. You just enter their email address, they get an email, and they don't have to log in to Nuxeo (because they aren't a Nuxeo user, so they don't even have credentials). 📧
You can read the official documentation on this feature for a detailed explanation of how to use it. 🔗
Everything sounds wonderful, but what happens if your organization has stricter security rules? They'll probably not allow your users to use this feature at all... Why? Because if you share something with my email ([email protected]) with permission READ, it will work-but I can pass that link to anyone. Anyone can use the link I got to have the exact same level of access that was granted on that document. 😱
Is this problematic? That depends on your organization. But let's say you want to secure it a bit. Let's say you want to make sure that only the person who got the email can access that link. How do you do that? Quite simple: you protect it with a passcode. Every time someone tries to access that link, you can request a secure code that will be sent only to the email that was originally shared with. 🔑
The entry point is to extend Nuxeo's built‐in TokenAuthenticator. You can find the original class here.
You need a simple service that stores a mapping { token → code }, for example in a transient (KVS) store:
<extension target="org.nuxeo.runtime.kv.KeyValueService" point="configuration">
<store name="tokenCodes" class="org.nuxeo.ecm.core.mongodb.kv.MongoDBKeyValueStore">
<property name="collection">tokenCodes</property>
</store>
</extension>
Your service (let's call it TokenAuthCodeService) can provide methods such as:
@Override
public boolean validateCodeForToken(String code, String token) {
if (StringUtils.isEmpty(token) || StringUtils.isEmpty(code)) {
return false;
}
KeyValueStore kvStore =
Framework.getService(KeyValueService.class).getKeyValueStore("tokenCodes");
String storedCode = kvStore.getString(token);
return storedCode != null && storedCode.equals(code);
}
📧 You'll also want a method like sendCodeByEmailForUserAssociatedWithToken(token) that sends the newly generated code to the email address tied to that token.
Next, plug in your own authenticator into Nuxeo's authentication chain:
<component name="com.maretha.io.token.authentication.contrib">
<require>org.nuxeo.ecm.login.token.authentication.contrib</require>
<extension
target="org.nuxeo.ecm.platform.ui.web.auth.service.PluggableAuthenticationService"
point="authenticators">
<authenticationPlugin name="TOKEN_AUTH" enabled="true"
class="com.maretha.io.CustomTokenAuthenticator">
<parameters>
<parameter name="allowAnonymous">false</parameter>
</parameters>
</authenticationPlugin>
</extension>
</component>
Then implement your custom class:
public class CustomTokenAuthenticator extends TokenAuthenticator {
protected static final String CODE_PARAM = "code";
protected static final String CODE_HEADER = "X-Authentication-Code";
private TokenAuthCodeService tokenAuthCodeService;
@Override
public UserIdentificationInfo handleRetrieveIdentity(
HttpServletRequest httpRequest,
HttpServletResponse httpResponse) {
// If request comes from Nuxeo Drive, fallback to default behavior
String userAgent = httpRequest.getHeader("User-Agent");
if (userAgent != null && userAgent.startsWith("Nuxeo-Drive")) {
return super.handleRetrieveIdentity(httpRequest, httpResponse);
}
String code = getCodeFromRequest(httpRequest);
String token = getTokenFromRequest(httpRequest);
// If this token isn't enforcing code checks, just proceed normally
if (!tokenAuthCodeService.isCodeCheckEnforcedForToken(token)) {
return super.handleRetrieveIdentity(httpRequest, httpResponse);
}
// If the code matches, authenticate via the superclass
if (tokenAuthCodeService.validateCodeForToken(code, token)) {
return super.handleRetrieveIdentity(httpRequest, httpResponse);
}
// If there's a token but no valid code yet, send an email with a fresh code
if (!StringUtils.isEmpty(token)) {
tokenAuthCodeService.sendCodeByEmailForUserAssociatedWithToken(token);
}
// Return null to force the login prompt (passcode screen)
return null;
}
}
Finally, you need a simple HTML/FTL (or Web UI) page that shows a form where users can enter the passcode. You can expose it via an openUrl contrib:
<component name="com.maretha.io.tokenvalidation.auth.contrib">
<extension
point="openUrl"
target="org.nuxeo.ecm.platform.ui.web.auth.service.PluggableAuthenticationService">
<openUrl name="validatetoken">
<grantPattern>${org.nuxeo.ecm.contextPath}/site/validatetoken</grantPattern>
</openUrl>
</extension>
</component>
That URL (e.g., /site/validatetoken) should render a simple form asking for the code. Once the user submits their passcode, you can append it to the URL so that it passes the authentication.
Hope this gives you a good sense of how powerful (and customizable) Nuxeo is. This post only covers the main lines of the implementation, but don't hesitate to contact us if you wish to implement something similar or need more help! 🚀
Keep reading
Need an unauthenticated POST endpoint in Nuxeo for external integrations? Here’s a step-by-step recipe using WebEngine and JAX-RS, with a working cURL test.
Read articleThe official Nuxeo roadmap from Hyland CommunityLIVE: what ships in H2 2025, what’s planned for H1 2026, and what’s coming in H2 2026 and beyond.
Read articleAlready running Nuxeo? You have everything you need to add serious AI capabilities—embeddings, semantic search, MCP, async pipelines. Here’s why and how.
Read articleTell us what you're struggling with, and we'll tell you how we can help you.
Talk to us