Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion docs/modules/servers/partials/configure/smtp-hooks.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -498,6 +498,9 @@ include::partial$configure/smtp-limitation-hook.adoc[]

Declarative authentication.

`ConfigurationAuthHook` is deprecated. Existing handler-chain registrations remain supported through the SMTP SASL
adapter, while new authentication logic should use a dedicated SASL mechanism factory.

It is possible to open and configure on a dedicated port (eg port 26) to accept application traffic in parallel of user
traffic. Authentication is then done on the supplied configuration.

Expand Down Expand Up @@ -529,4 +532,4 @@ user accounts for those applications)
</accounts>
</handler>
</handlerchain>
....
....
6 changes: 5 additions & 1 deletion docs/modules/servers/partials/customization/smtp-hooks.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ enqueued in the MailQueue, and before any mail processing takes place.

The following interfaces allows interacting with the following commands:

* *AuthHook*: Implement this interfaces to hook in the AUTH Command.
* *AuthHook*: Legacy hook for the AUTH Command. Prefer implementing a SASL mechanism factory for new authentication
logic, or a `SaslAuthResultHook` when only post-authentication side effects are needed.

....
HookResult doAuth(SMTPSession session, Username username, String password);
Expand Down Expand Up @@ -52,6 +53,9 @@ The following interfaces allows interacting with the following commands:

Register you hooks using xref:{pages-path}/configure/smtp.adoc[*smtpserver.xml*] handlerchain property.

For AUTH, `AuthHook` is deprecated. Existing handler-chain password hooks remain supported through the SMTP SASL
adapter while keeping `AuthCmdHandler` on the SASL exchange path.

== Writing additional SMTP commands

What to do if the Hook API is not enough for you ?
Expand Down
75 changes: 75 additions & 0 deletions examples/custom-imap/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,26 @@ Sample configure file: [imapserver.xml](./sample-configuration/imapserver.xml)
Note that when `imapPackages` is not provided, James will implicit use
`org.apache.James.modules.protocols.DefaultImapPackage`

# Creating your own IMAP SASL mechanisms

This example also demonstrates how to add a custom IMAP SASL mechanism.
The `EXAMPLE-TOKEN` mechanism is declared through `auth.saslMechanisms`,
while `auth.exampleToken` is a custom configuration block owned by the extension:

```xml
<auth>
<saslMechanisms>PlainSaslMechanismFactory,org.apache.james.examples.imap.sasl.ExampleTokenSaslMechanismFactory</saslMechanisms>
<exampleToken>
<expectedToken>secret-token</expectedToken>
<authorizedUser>bob@domain.tld</authorizedUser>
</exampleToken>
</auth>
```

`auth.saslMechanisms` lists SASL mechanism factory classes. Built-in factories
can use simple names, while custom factories use their fully qualified class name.
The factory reads that server's `auth.exampleToken` block.

## Running the example

Build the project:
Expand Down Expand Up @@ -56,4 +76,59 @@ a02 OK LOGIN completed.
A03 PING
* PONG
A03 OK PING completed.
A04 LOGOUT
```

Test the custom SASL mechanism:

```bash
telnet localhost 143
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
* OK JAMES IMAP4rev1 Server james.local is ready.
A01 CAPABILITY
* CAPABILITY IMAP4rev1 AUTH=PLAIN SASL-IR AUTH=EXAMPLE-TOKEN PING
A01 OK CAPABILITY completed.
A02 AUTHENTICATE EXAMPLE-TOKEN c2VjcmV0LXRva2Vu
A02 OK AUTHENTICATE completed.
A03 PING
* PONG
A03 OK PING completed.
```

The custom SASL mechanism also supports a continuation when the client does not send
the initial response in the `AUTHENTICATE` command. The continuation payload is
base64-encoded by IMAP, so `R28gYWhlYWQ` decodes to `Go ahead`:

```bash
telnet localhost 143
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
* OK JAMES IMAP4rev1 Server james.local is ready.
A01 AUTHENTICATE EXAMPLE-TOKEN
+ R28gYWhlYWQ
c2VjcmV0LXRva2Vu
A01 OK AUTHENTICATE completed.
A02 PING
* PONG
A02 OK PING completed.
```

The mechanism can also return final server data on success. The client acknowledges
that final data with an empty line before James sends the tagged `OK`:

```bash
telnet localhost 143
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
* OK JAMES IMAP4rev1 Server james.local is ready.
A01 AUTHENTICATE EXAMPLE-TOKEN
+ R28gYWhlYWQ
c2VjcmV0LXRva2VuOnNlcnZlci1kYXRh
+ VG9rZW4gYWNjZXB0ZWQ=

A01 OK AUTHENTICATE completed.
```
6 changes: 6 additions & 0 deletions examples/custom-imap/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,12 @@
<version>${james.baseVersion}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>${james.protocols.groupId}</groupId>
<artifactId>protocols-api</artifactId>
<version>${james.baseVersion}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.google.inject</groupId>
<artifactId>guice</artifactId>
Expand Down
7 changes: 7 additions & 0 deletions examples/custom-imap/sample-configuration/imapserver.xml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,13 @@ under the License.
<connectionLimitPerIP>0</connectionLimitPerIP>
<plainAuthDisallowed>false</plainAuthDisallowed>
<gracefulShutdown>false</gracefulShutdown>
<auth>
<saslMechanisms>PlainSaslMechanismFactory,org.apache.james.examples.imap.sasl.ExampleTokenSaslMechanismFactory</saslMechanisms>
<exampleToken>
<expectedToken>secret-token</expectedToken>
<authorizedUser>bob@domain.tld</authorizedUser>
</exampleToken>
</auth>
<imapPackages>org.apache.james.modules.protocols.DefaultImapPackage</imapPackages>
<imapPackages>org.apache.james.examples.imap.PingImapPackages</imapPackages>
</imapserver>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/****************************************************************
* Licensed to the Apache Software Foundation (ASF) under one *
* or more contributor license agreements. See the NOTICE file *
* distributed with this work for additional information *
* regarding copyright ownership. The ASF licenses this file *
* to you under the Apache License, Version 2.0 (the *
* "License"); you may not use this file except in compliance *
* with the License. You may obtain a copy of the License at *
* *
* http://www.apache.org/licenses/LICENSE-2.0 *
* *
* Unless required by applicable law or agreed to in writing, *
* software distributed under the License is distributed on an *
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY *
* KIND, either express or implied. See the License for the *
* specific language governing permissions and limitations *
* under the License. *
****************************************************************/

package org.apache.james.examples.imap.sasl;

import org.apache.commons.configuration2.HierarchicalConfiguration;
import org.apache.commons.configuration2.ex.ConfigurationException;
import org.apache.commons.configuration2.tree.ImmutableNode;
import org.apache.james.core.Username;

public record ExampleTokenSaslConfiguration(String expectedToken, Username authorizedUser) {
private static final String EXPECTED_TOKEN_PROPERTY = "auth.exampleToken.expectedToken";
private static final String AUTHORIZED_USER_PROPERTY = "auth.exampleToken.authorizedUser";

public static ExampleTokenSaslConfiguration from(HierarchicalConfiguration<ImmutableNode> configuration) throws ConfigurationException {
if (!configuration.containsKey(EXPECTED_TOKEN_PROPERTY)) {
throw new ConfigurationException(EXPECTED_TOKEN_PROPERTY + " is mandatory");
}
if (!configuration.containsKey(AUTHORIZED_USER_PROPERTY)) {
throw new ConfigurationException(AUTHORIZED_USER_PROPERTY + " is mandatory");
}

return new ExampleTokenSaslConfiguration(
configuration.getString(EXPECTED_TOKEN_PROPERTY),
Username.of(configuration.getString(AUTHORIZED_USER_PROPERTY)));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
/****************************************************************
* Licensed to the Apache Software Foundation (ASF) under one *
* or more contributor license agreements. See the NOTICE file *
* distributed with this work for additional information *
* regarding copyright ownership. The ASF licenses this file *
* to you under the Apache License, Version 2.0 (the *
* "License"); you may not use this file except in compliance *
* with the License. You may obtain a copy of the License at *
* *
* http://www.apache.org/licenses/LICENSE-2.0 *
* *
* Unless required by applicable law or agreed to in writing, *
* software distributed under the License is distributed on an *
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY *
* KIND, either express or implied. See the License for the *
* specific language governing permissions and limitations *
* under the License. *
****************************************************************/

package org.apache.james.examples.imap.sasl;

import java.nio.charset.StandardCharsets;
import java.util.Optional;

import org.apache.james.protocols.api.sasl.SaslAuthenticator;
import org.apache.james.protocols.api.sasl.SaslExchange;
import org.apache.james.protocols.api.sasl.SaslFailure;
import org.apache.james.protocols.api.sasl.SaslIdentity;
import org.apache.james.protocols.api.sasl.SaslInitialRequest;
import org.apache.james.protocols.api.sasl.SaslMechanism;
import org.apache.james.protocols.api.sasl.SaslStep;

public class ExampleTokenSaslMechanism implements SaslMechanism {
public static final String NAME = "EXAMPLE-TOKEN";
public static final String CONTINUATION_PROMPT = "Go ahead";
public static final String SUCCESS_DATA_TOKEN_SUFFIX = ":server-data";
public static final String SUCCESS_DATA = "Token accepted";

private final ExampleTokenSaslConfiguration configuration;

public ExampleTokenSaslMechanism(ExampleTokenSaslConfiguration configuration) {
this.configuration = configuration;
}

@Override
public String name() {
return NAME;
}

@Override
public SaslExchange start(SaslInitialRequest request, SaslAuthenticator authenticator) {
Optional<byte[]> initialResponse = request.initialResponse();
return new ExampleTokenSaslExchange(initialResponse, configuration);
}

private static class ExampleTokenSaslExchange implements SaslExchange {
private final Optional<byte[]> initialResponse;
private final ExampleTokenSaslConfiguration configuration;

private ExampleTokenSaslExchange(Optional<byte[]> initialResponse, ExampleTokenSaslConfiguration configuration) {
this.initialResponse = initialResponse;
this.configuration = configuration;
}

@Override
public SaslStep firstStep() {
return initialResponse
.map(this::authenticate)
.orElseGet(() -> new SaslStep.Challenge(Optional.of(CONTINUATION_PROMPT
.getBytes(StandardCharsets.UTF_8))));
}

@Override
public SaslStep onResponse(byte[] clientResponse) {
return authenticate(clientResponse);
}

@Override
public void close() {
}

private SaslStep authenticate(byte[] clientResponse) {
String token = new String(clientResponse, StandardCharsets.UTF_8);
if (configuration.expectedToken().equals(token)) {
return success(Optional.empty());
}
// allow client to request server to return data on success message, which may be used by Kerberos auth
if ((configuration.expectedToken() + SUCCESS_DATA_TOKEN_SUFFIX).equals(token)) {
return success(Optional.of(SUCCESS_DATA.getBytes(StandardCharsets.UTF_8)));
}
return new SaslStep.Failure(SaslFailure.authenticationFailed(Optional.empty(), Optional.of(configuration.authorizedUser()),
"EXAMPLE-TOKEN authentication failed."));
}

private SaslStep success(Optional<byte[]> serverData) {
return new SaslStep.Success(new SaslIdentity(configuration.authorizedUser(), configuration.authorizedUser()), serverData);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/****************************************************************
* Licensed to the Apache Software Foundation (ASF) under one *
* or more contributor license agreements. See the NOTICE file *
* distributed with this work for additional information *
* regarding copyright ownership. The ASF licenses this file *
* to you under the Apache License, Version 2.0 (the *
* "License"); you may not use this file except in compliance *
* with the License. You may obtain a copy of the License at *
* *
* http://www.apache.org/licenses/LICENSE-2.0 *
* *
* Unless required by applicable law or agreed to in writing, *
* software distributed under the License is distributed on an *
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY *
* KIND, either express or implied. See the License for the *
* specific language governing permissions and limitations *
* under the License. *
****************************************************************/

package org.apache.james.examples.imap.sasl;

import org.apache.commons.configuration2.HierarchicalConfiguration;
import org.apache.commons.configuration2.ex.ConfigurationException;
import org.apache.commons.configuration2.tree.ImmutableNode;
import org.apache.james.protocols.api.sasl.SaslMechanism;
import org.apache.james.protocols.api.sasl.SaslMechanismFactory;

public class ExampleTokenSaslMechanismFactory implements SaslMechanismFactory {
@Override
public SaslMechanism create(HierarchicalConfiguration<ImmutableNode> serverConfiguration) throws ConfigurationException {
return new ExampleTokenSaslMechanism(ExampleTokenSaslConfiguration.from(serverConfiguration));
}
}
16 changes: 15 additions & 1 deletion examples/custom-imap/src/main/resources/imapserver.xml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,13 @@ under the License.
<customProperties>pong.response=customImapParameter</customProperties>
<customProperties>prop.b=anotherValue</customProperties>
<gracefulShutdown>false</gracefulShutdown>
<auth>
<saslMechanisms>PlainSaslMechanismFactory,org.apache.james.examples.imap.sasl.ExampleTokenSaslMechanismFactory</saslMechanisms>
<exampleToken>
<expectedToken>secret-token</expectedToken>
<authorizedUser>bob@domain.tld</authorizedUser>
</exampleToken>
</auth>
<imapPackages>org.apache.james.modules.protocols.DefaultImapPackage</imapPackages>
<imapPackages>org.apache.james.examples.imap.PingImapPackages</imapPackages>
</imapserver>
Expand All @@ -55,7 +62,14 @@ under the License.
<customProperties>pong.response=bad</customProperties>
<customProperties>prop.b=baad</customProperties>
<gracefulShutdown>false</gracefulShutdown>
<auth>
<saslMechanisms>PlainSaslMechanismFactory,org.apache.james.examples.imap.sasl.ExampleTokenSaslMechanismFactory</saslMechanisms>
<exampleToken>
<expectedToken>secret-token</expectedToken>
<authorizedUser>bob@domain.tld</authorizedUser>
</exampleToken>
</auth>
<imapPackages>org.apache.james.modules.protocols.DefaultImapPackage</imapPackages>
<imapPackages>org.apache.james.examples.imap.PingImapPackages</imapPackages>
</imapserver>
</imapservers>
</imapservers>
Loading