Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
8485783
JAMES-4210 Introduce shared SASL SPI
quantranhong1999 Jun 3, 2026
7599d28
JAMES-4210 Add IMAP SASL bridge scaffold
quantranhong1999 Jun 3, 2026
4adec8f
JAMES-4210 Add SMTP SASL bridge scaffold
quantranhong1999 Jun 3, 2026
9b05af7
JAMES-4210 Introduce SASL mechanism registry
quantranhong1999 Jun 3, 2026
84cffd0
JAMES-4210 Implement GuiceSaslMechanismLoader
quantranhong1999 Jun 3, 2026
963b836
JAMES-4210 Add protocol-neutral SASL mechanisms
quantranhong1999 Jun 5, 2026
e214f0a
JAMES-4210 Route IMAP AUTHENTICATE through SASL mechanisms
quantranhong1999 Jun 5, 2026
46a64e4
JAMES-4210 Load IMAP SASL mechanisms from configuration
quantranhong1999 Jun 5, 2026
52573c6
JAMES-4210 Prepare SMTP SASL bridge reuse for LMTP
quantranhong1999 Jun 5, 2026
77cc2b9
JAMES-4210 Allow IMAP SASL provider extensions from configuration
quantranhong1999 Jun 5, 2026
cb204cf
JAMES-4210 Add custom IMAP SASL extension example
quantranhong1999 Jun 5, 2026
b285452
JAMES-4210 Custom IMAP SASL extension example: add a conditional cont…
quantranhong1999 Jun 10, 2026
8bc52cc
JAMES-4210 Custom IMAP SASL extension example: strengthen test case f…
quantranhong1999 Jun 12, 2026
542f0b8
JAMES-4210 Simplify SASL SPI around exchanges and credentials
quantranhong1999 Jun 12, 2026
5a508bd
JAMES-4210 Add Guice resolver for configured SASL mechanisms
quantranhong1999 Jun 12, 2026
b038b9d
JAMES-4210 Adapt IMAP AUTHENTICATE to SASL exchanges
quantranhong1999 Jun 12, 2026
cd163dc
JAMES-4210 Wire per-server IMAP SASL mechanism configuration
quantranhong1999 Jun 12, 2026
c889aa9
JAMES-4210 Drop obsolete SMTP SASL bridge skeleton
quantranhong1999 Jun 12, 2026
9cf926e
JAMES-4210 Update custom IMAP SASL example to the new SPI
quantranhong1999 Jun 12, 2026
8eb8150
JAMES-4210 Introduce protocol-neutral SASL authentication SPI
quantranhong1999 Jun 16, 2026
6e4e54e
JAMES-4210 Move built-in SASL mechanisms to shared protocol module
quantranhong1999 Jun 16, 2026
6bd025c
JAMES-4210 Add Guice SASL mechanism factory resolution
quantranhong1999 Jun 16, 2026
ce48137
JAMES-4210 Adapt IMAP authentication to SASL-owned authentication
quantranhong1999 Jun 16, 2026
e580849
JAMES-4210 Wire per-server IMAP SASL mechanism configuration
quantranhong1999 Jun 16, 2026
6111259
JAMES-4210 Remove IMAP authentication configuration from Netty sessio…
quantranhong1999 Jun 16, 2026
3f0a554
JAMES-4210 Update custom IMAP SASL example to factory-based SPI
quantranhong1999 Jun 16, 2026
90848da
JAMES-4210 [FIX] IMAP Login/plain authentication should preserve logg…
quantranhong1999 Jun 17, 2026
620d567
JAMES-4210 Move JamesSaslAuthenticator initialization out of DefaultP…
quantranhong1999 Jun 17, 2026
b30dd7f
JAMES-4210 Merge OAuthSaslMechanism
quantranhong1999 Jun 17, 2026
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
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;
}
Comment thread
quantranhong1999 marked this conversation as resolved.

@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