Nuxeo / DAM / PAM / ECM specialistsContact
Home/Insights/Nuxeo
Insights

Security Edition: Passcode-Protecting Nuxeo External Shares

Jul 14, 20263 min read

Security Edition: Passcode-Protecting Nuxeo External Shares

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. 🔑

Key Implementation Steps (if it's taking >1 week, reach out! 💡)

1. Extend the TokenAuthenticator to catch these token‐based logins 🔍

The entry point is to extend Nuxeo's built‐in TokenAuthenticator. You can find the original class here.

a) Create a service to associate each token with a passcode 🔐

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.

b) Override TokenAuthenticator and register your custom authenticator

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;
    }
}

2. Create a UI to request the passcode 🖥️

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.

Conclusion 🎉

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! 🚀

← All insights

Keep reading

Related insights

Talk to a Maretha Consultant

Tell us what you're struggling with, and we'll tell you how we can help you.

Talk to us