Skip to content

Commit cb204cf

Browse files
JAMES-4210 Add custom IMAP SASL extension example
Add an EXAMPLE-TOKEN SASL mechanism to examples/custom-imap to demonstrate a custom mechanism, provider extension, and auth.exampleToken configuration. Add dedicated tests for custom SASL advertisement, successful custom auth, invalid-token rejection, and preserving built-in PLAIN authentication.
1 parent 77cc2b9 commit cb204cf

10 files changed

Lines changed: 480 additions & 0 deletions

File tree

examples/custom-imap/README.md

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,29 @@ Sample configure file: [imapserver.xml](./sample-configuration/imapserver.xml)
1414
Note that when `imapPackages` is not provided, James will implicit use
1515
`org.apache.James.modules.protocols.DefaultImapPackage`
1616

17+
# Creating your own IMAP SASL mechanisms
18+
19+
This example also demonstrates how to add a custom IMAP SASL mechanism.
20+
The `EXAMPLE-TOKEN` mechanism is declared through `auth.saslMechanisms`,
21+
its authentication service factory provider is declared through
22+
`auth.saslAuthenticationServiceFactoryProviderExtensions`, while `auth.exampleToken`
23+
is a custom configuration block owned by the extension:
24+
25+
```xml
26+
<auth>
27+
<saslMechanisms>PlainSaslMechanism,org.apache.james.examples.imap.sasl.ExampleTokenSaslMechanism</saslMechanisms>
28+
<saslAuthenticationServiceFactoryProviderExtensions>org.apache.james.examples.imap.sasl.ExampleTokenSaslAuthenticationServiceFactoryProvider</saslAuthenticationServiceFactoryProviderExtensions>
29+
<exampleToken>
30+
<expectedToken>secret-token</expectedToken>
31+
<authorizedUser>bob@domain.tld</authorizedUser>
32+
</exampleToken>
33+
</auth>
34+
```
35+
36+
James loads the provider through the extension classloader and instantiates it
37+
with Guice, so the provider can use James services and parse its own
38+
configuration block.
39+
1740
## Running the example
1841

1942
Build the project:
@@ -56,4 +79,23 @@ a02 OK LOGIN completed.
5679
A03 PING
5780
* PONG
5881
A03 OK PING completed.
82+
A04 LOGOUT
83+
```
84+
85+
Test the custom SASL mechanism:
86+
87+
```bash
88+
telnet localhost 143
89+
Trying 127.0.0.1...
90+
Connected to localhost.
91+
Escape character is '^]'.
92+
* OK JAMES IMAP4rev1 Server james.local is ready.
93+
A01 CAPABILITY
94+
* CAPABILITY IMAP4rev1 AUTH=PLAIN SASL-IR AUTH=EXAMPLE-TOKEN PING
95+
A01 OK CAPABILITY completed.
96+
A02 AUTHENTICATE EXAMPLE-TOKEN c2VjcmV0LXRva2Vu
97+
A02 OK AUTHENTICATE completed.
98+
A03 PING
99+
* PONG
100+
A03 OK PING completed.
59101
```

examples/custom-imap/pom.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,12 @@
6767
<version>${james.baseVersion}</version>
6868
<scope>provided</scope>
6969
</dependency>
70+
<dependency>
71+
<groupId>${james.protocols.groupId}</groupId>
72+
<artifactId>protocols-api</artifactId>
73+
<version>${james.baseVersion}</version>
74+
<scope>provided</scope>
75+
</dependency>
7076
<dependency>
7177
<groupId>com.google.inject</groupId>
7278
<artifactId>guice</artifactId>

examples/custom-imap/sample-configuration/imapserver.xml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,14 @@ under the License.
3333
<connectionLimitPerIP>0</connectionLimitPerIP>
3434
<plainAuthDisallowed>false</plainAuthDisallowed>
3535
<gracefulShutdown>false</gracefulShutdown>
36+
<auth>
37+
<saslMechanisms>PlainSaslMechanism,org.apache.james.examples.imap.sasl.ExampleTokenSaslMechanism</saslMechanisms>
38+
<saslAuthenticationServiceFactoryProviderExtensions>org.apache.james.examples.imap.sasl.ExampleTokenSaslAuthenticationServiceFactoryProvider</saslAuthenticationServiceFactoryProviderExtensions>
39+
<exampleToken>
40+
<expectedToken>secret-token</expectedToken>
41+
<authorizedUser>bob@domain.tld</authorizedUser>
42+
</exampleToken>
43+
</auth>
3644
<imapPackages>org.apache.james.modules.protocols.DefaultImapPackage</imapPackages>
3745
<imapPackages>org.apache.james.examples.imap.PingImapPackages</imapPackages>
3846
</imapserver>
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/****************************************************************
2+
* Licensed to the Apache Software Foundation (ASF) under one *
3+
* or more contributor license agreements. See the NOTICE file *
4+
* distributed with this work for additional information *
5+
* regarding copyright ownership. The ASF licenses this file *
6+
* to you under the Apache License, Version 2.0 (the *
7+
* "License"); you may not use this file except in compliance *
8+
* with the License. You may obtain a copy of the License at *
9+
* *
10+
* http://www.apache.org/licenses/LICENSE-2.0 *
11+
* *
12+
* Unless required by applicable law or agreed to in writing, *
13+
* software distributed under the License is distributed on an *
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY *
15+
* KIND, either express or implied. See the License for the *
16+
* specific language governing permissions and limitations *
17+
* under the License. *
18+
****************************************************************/
19+
20+
package org.apache.james.examples.imap.sasl;
21+
22+
import org.apache.james.imap.processor.sasl.ImapSaslSessionContext;
23+
import org.apache.james.mailbox.MailboxManager;
24+
import org.apache.james.mailbox.MailboxSession;
25+
import org.apache.james.protocols.api.sasl.SaslAuthenticationResult;
26+
import org.apache.james.protocols.api.sasl.SaslIdentity;
27+
28+
public class ExampleTokenSaslAuthenticationService {
29+
private final MailboxManager mailboxManager;
30+
private final ImapSaslSessionContext context;
31+
private final ExampleTokenSaslConfiguration configuration;
32+
33+
public ExampleTokenSaslAuthenticationService(MailboxManager mailboxManager, ImapSaslSessionContext context, ExampleTokenSaslConfiguration configuration) {
34+
this.mailboxManager = mailboxManager;
35+
this.context = context;
36+
this.configuration = configuration;
37+
}
38+
39+
public SaslAuthenticationResult authenticate(String token) {
40+
if (!configuration.expectedToken().equals(token)) {
41+
return new SaslAuthenticationResult.Failure("EXAMPLE-TOKEN authentication failed.");
42+
}
43+
44+
MailboxSession mailboxSession = mailboxManager.createSystemSession(configuration.authorizedUser());
45+
context.authenticationSucceeded(mailboxSession);
46+
return new SaslAuthenticationResult.Success(new SaslIdentity(configuration.authorizedUser(), configuration.authorizedUser()));
47+
}
48+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/****************************************************************
2+
* Licensed to the Apache Software Foundation (ASF) under one *
3+
* or more contributor license agreements. See the NOTICE file *
4+
* distributed with this work for additional information *
5+
* regarding copyright ownership. The ASF licenses this file *
6+
* to you under the Apache License, Version 2.0 (the *
7+
* "License"); you may not use this file except in compliance *
8+
* with the License. You may obtain a copy of the License at *
9+
* *
10+
* http://www.apache.org/licenses/LICENSE-2.0 *
11+
* *
12+
* Unless required by applicable law or agreed to in writing, *
13+
* software distributed under the License is distributed on an *
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY *
15+
* KIND, either express or implied. See the License for the *
16+
* specific language governing permissions and limitations *
17+
* under the License. *
18+
****************************************************************/
19+
20+
package org.apache.james.examples.imap.sasl;
21+
22+
import java.util.Optional;
23+
24+
import org.apache.james.imap.processor.sasl.ImapSaslSessionContext;
25+
import org.apache.james.mailbox.MailboxManager;
26+
import org.apache.james.protocols.api.sasl.SaslAuthenticationServiceFactory;
27+
import org.apache.james.protocols.api.sasl.SaslProtocol;
28+
import org.apache.james.protocols.api.sasl.SaslSessionContext;
29+
30+
public class ExampleTokenSaslAuthenticationServiceFactory implements SaslAuthenticationServiceFactory<ExampleTokenSaslAuthenticationService> {
31+
private final MailboxManager mailboxManager;
32+
private final ExampleTokenSaslConfiguration configuration;
33+
34+
public ExampleTokenSaslAuthenticationServiceFactory(MailboxManager mailboxManager, ExampleTokenSaslConfiguration configuration) {
35+
this.mailboxManager = mailboxManager;
36+
this.configuration = configuration;
37+
}
38+
39+
@Override
40+
public SaslProtocol protocol() {
41+
return SaslProtocol.IMAP;
42+
}
43+
44+
@Override
45+
public Class<ExampleTokenSaslAuthenticationService> serviceType() {
46+
return ExampleTokenSaslAuthenticationService.class;
47+
}
48+
49+
@Override
50+
public Optional<ExampleTokenSaslAuthenticationService> create(SaslSessionContext context) {
51+
if (context instanceof ImapSaslSessionContext imapContext) {
52+
return Optional.of(new ExampleTokenSaslAuthenticationService(mailboxManager, imapContext, configuration));
53+
}
54+
return Optional.empty();
55+
}
56+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/****************************************************************
2+
* Licensed to the Apache Software Foundation (ASF) under one *
3+
* or more contributor license agreements. See the NOTICE file *
4+
* distributed with this work for additional information *
5+
* regarding copyright ownership. The ASF licenses this file *
6+
* to you under the Apache License, Version 2.0 (the *
7+
* "License"); you may not use this file except in compliance *
8+
* with the License. You may obtain a copy of the License at *
9+
* *
10+
* http://www.apache.org/licenses/LICENSE-2.0 *
11+
* *
12+
* Unless required by applicable law or agreed to in writing, *
13+
* software distributed under the License is distributed on an *
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY *
15+
* KIND, either express or implied. See the License for the *
16+
* specific language governing permissions and limitations *
17+
* under the License. *
18+
****************************************************************/
19+
20+
package org.apache.james.examples.imap.sasl;
21+
22+
import org.apache.commons.configuration2.HierarchicalConfiguration;
23+
import org.apache.commons.configuration2.ex.ConfigurationException;
24+
import org.apache.commons.configuration2.tree.ImmutableNode;
25+
import org.apache.james.mailbox.MailboxManager;
26+
import org.apache.james.modules.protocols.ImapSaslAuthenticationServiceFactoryProvider;
27+
import org.apache.james.protocols.api.sasl.SaslAuthenticationServiceFactory;
28+
29+
import com.google.common.collect.ImmutableList;
30+
import com.google.inject.Inject;
31+
32+
public class ExampleTokenSaslAuthenticationServiceFactoryProvider implements ImapSaslAuthenticationServiceFactoryProvider {
33+
private final MailboxManager mailboxManager;
34+
35+
@Inject
36+
public ExampleTokenSaslAuthenticationServiceFactoryProvider(MailboxManager mailboxManager) {
37+
this.mailboxManager = mailboxManager;
38+
}
39+
40+
@Override
41+
public ImmutableList<SaslAuthenticationServiceFactory<?>> provide(HierarchicalConfiguration<ImmutableNode> configuration) throws ConfigurationException {
42+
return ImmutableList.of(new ExampleTokenSaslAuthenticationServiceFactory(mailboxManager, ExampleTokenSaslConfiguration.from(configuration)));
43+
}
44+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/****************************************************************
2+
* Licensed to the Apache Software Foundation (ASF) under one *
3+
* or more contributor license agreements. See the NOTICE file *
4+
* distributed with this work for additional information *
5+
* regarding copyright ownership. The ASF licenses this file *
6+
* to you under the Apache License, Version 2.0 (the *
7+
* "License"); you may not use this file except in compliance *
8+
* with the License. You may obtain a copy of the License at *
9+
* *
10+
* http://www.apache.org/licenses/LICENSE-2.0 *
11+
* *
12+
* Unless required by applicable law or agreed to in writing, *
13+
* software distributed under the License is distributed on an *
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY *
15+
* KIND, either express or implied. See the License for the *
16+
* specific language governing permissions and limitations *
17+
* under the License. *
18+
****************************************************************/
19+
20+
package org.apache.james.examples.imap.sasl;
21+
22+
import org.apache.commons.configuration2.HierarchicalConfiguration;
23+
import org.apache.commons.configuration2.ex.ConfigurationException;
24+
import org.apache.commons.configuration2.tree.ImmutableNode;
25+
import org.apache.james.core.Username;
26+
27+
public record ExampleTokenSaslConfiguration(String expectedToken, Username authorizedUser) {
28+
private static final String EXPECTED_TOKEN_PROPERTY = "auth.exampleToken.expectedToken";
29+
private static final String AUTHORIZED_USER_PROPERTY = "auth.exampleToken.authorizedUser";
30+
31+
public static ExampleTokenSaslConfiguration from(HierarchicalConfiguration<ImmutableNode> configuration) throws ConfigurationException {
32+
if (!configuration.containsKey(EXPECTED_TOKEN_PROPERTY)) {
33+
throw new ConfigurationException(EXPECTED_TOKEN_PROPERTY + " is mandatory");
34+
}
35+
if (!configuration.containsKey(AUTHORIZED_USER_PROPERTY)) {
36+
throw new ConfigurationException(AUTHORIZED_USER_PROPERTY + " is mandatory");
37+
}
38+
39+
return new ExampleTokenSaslConfiguration(
40+
configuration.getString(EXPECTED_TOKEN_PROPERTY),
41+
Username.of(configuration.getString(AUTHORIZED_USER_PROPERTY)));
42+
}
43+
}
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
/****************************************************************
2+
* Licensed to the Apache Software Foundation (ASF) under one *
3+
* or more contributor license agreements. See the NOTICE file *
4+
* distributed with this work for additional information *
5+
* regarding copyright ownership. The ASF licenses this file *
6+
* to you under the Apache License, Version 2.0 (the *
7+
* "License"); you may not use this file except in compliance *
8+
* with the License. You may obtain a copy of the License at *
9+
* *
10+
* http://www.apache.org/licenses/LICENSE-2.0 *
11+
* *
12+
* Unless required by applicable law or agreed to in writing, *
13+
* software distributed under the License is distributed on an *
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY *
15+
* KIND, either express or implied. See the License for the *
16+
* specific language governing permissions and limitations *
17+
* under the License. *
18+
****************************************************************/
19+
20+
package org.apache.james.examples.imap.sasl;
21+
22+
import java.nio.charset.StandardCharsets;
23+
import java.util.Optional;
24+
import java.util.Set;
25+
26+
import org.apache.james.protocols.api.sasl.SaslAuthenticationResult;
27+
import org.apache.james.protocols.api.sasl.SaslExchange;
28+
import org.apache.james.protocols.api.sasl.SaslInitialRequest;
29+
import org.apache.james.protocols.api.sasl.SaslMechanism;
30+
import org.apache.james.protocols.api.sasl.SaslProtocol;
31+
import org.apache.james.protocols.api.sasl.SaslSessionContext;
32+
import org.apache.james.protocols.api.sasl.SaslStep;
33+
34+
public class ExampleTokenSaslMechanism implements SaslMechanism {
35+
public static final String NAME = "EXAMPLE-TOKEN";
36+
37+
@Override
38+
public String name() {
39+
return NAME;
40+
}
41+
42+
@Override
43+
public boolean supports(SaslProtocol protocol) {
44+
return protocol == SaslProtocol.IMAP;
45+
}
46+
47+
@Override
48+
public Set<Class<?>> requiredServices(SaslProtocol protocol) {
49+
if (supports(protocol)) {
50+
return Set.of(ExampleTokenSaslAuthenticationService.class);
51+
}
52+
return Set.of();
53+
}
54+
55+
@Override
56+
public boolean isAvailable(SaslSessionContext context) {
57+
return context.service(ExampleTokenSaslAuthenticationService.class).isPresent();
58+
}
59+
60+
@Override
61+
public SaslExchange start(SaslInitialRequest request, SaslSessionContext context) {
62+
return new ExampleTokenSaslExchange(request.initialResponse(), context);
63+
}
64+
65+
private static class ExampleTokenSaslExchange implements SaslExchange {
66+
private final Optional<byte[]> initialResponse;
67+
private final SaslSessionContext context;
68+
69+
private ExampleTokenSaslExchange(Optional<byte[]> initialResponse, SaslSessionContext context) {
70+
this.initialResponse = initialResponse;
71+
this.context = context;
72+
}
73+
74+
@Override
75+
public SaslStep firstStep() {
76+
return initialResponse
77+
.map(this::authenticate)
78+
.orElseGet(() -> new SaslStep.Challenge(Optional.empty()));
79+
}
80+
81+
@Override
82+
public SaslStep onResponse(byte[] clientResponse) {
83+
return authenticate(clientResponse);
84+
}
85+
86+
@Override
87+
public void abort() {
88+
}
89+
90+
@Override
91+
public void close() {
92+
}
93+
94+
private SaslStep authenticate(byte[] clientResponse) {
95+
return context.service(ExampleTokenSaslAuthenticationService.class)
96+
.map(service -> service.authenticate(new String(clientResponse, StandardCharsets.UTF_8)))
97+
.map(this::toStep)
98+
.orElseGet(() -> new SaslStep.Failure("EXAMPLE-TOKEN authentication is not available."));
99+
}
100+
101+
private SaslStep toStep(SaslAuthenticationResult result) {
102+
if (result instanceof SaslAuthenticationResult.Success success) {
103+
return new SaslStep.Success(success.identity(), Optional.empty());
104+
}
105+
return new SaslStep.Failure(((SaslAuthenticationResult.Failure) result).reason());
106+
}
107+
}
108+
}

0 commit comments

Comments
 (0)