Skip to content

Commit e17b138

Browse files
committed
feat(managesieve): add XOAUTH2 authentication mechanism
1 parent 2276272 commit e17b138

File tree

12 files changed

+173
-53
lines changed

12 files changed

+173
-53
lines changed

protocols/managesieve/pom.xml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,11 +41,19 @@
4141
<groupId>${james.groupId}</groupId>
4242
<artifactId>james-server-data-api</artifactId>
4343
</dependency>
44+
<dependency>
45+
<groupId>${james.groupId}</groupId>
46+
<artifactId>james-server-jwt</artifactId>
47+
</dependency>
4448
<dependency>
4549
<groupId>${james.groupId}</groupId>
4650
<artifactId>testing-base</artifactId>
4751
<scope>test</scope>
4852
</dependency>
53+
<dependency>
54+
<groupId>${james.protocols.groupId}</groupId>
55+
<artifactId>protocols-api</artifactId>
56+
</dependency>
4957
<dependency>
5058
<groupId>com.google.guava</groupId>
5159
<artifactId>guava</artifactId>

protocols/managesieve/src/main/java/org/apache/james/managesieve/api/CapabilityAdvertiser.java

Lines changed: 0 additions & 27 deletions
This file was deleted.

protocols/managesieve/src/main/java/org/apache/james/managesieve/api/Session.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,11 @@
2020

2121
package org.apache.james.managesieve.api;
2222

23+
import java.util.Optional;
24+
2325
import org.apache.james.core.Username;
2426
import org.apache.james.managesieve.api.commands.Authenticate;
27+
import org.apache.james.protocols.api.OidcSASLConfiguration;
2528

2629
public interface Session {
2730

@@ -51,4 +54,7 @@ enum State {
5154

5255
boolean isSslEnabled();
5356

57+
Optional<OidcSASLConfiguration> getOidcSASLConfiguration();
58+
59+
void setOidcSASLConfiguration(Optional<OidcSASLConfiguration> configuration);
5460
}

protocols/managesieve/src/main/java/org/apache/james/managesieve/api/commands/Authenticate.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
public interface Authenticate {
3131

3232
enum SupportedMechanism {
33-
PLAIN;
33+
PLAIN, XOAUTH2;
3434

3535
public static SupportedMechanism retrieveMechanism(String serializedData) throws UnknownSaslMechanism {
3636
for (SupportedMechanism supportedMechanism : SupportedMechanism.values()) {

protocols/managesieve/src/main/java/org/apache/james/managesieve/api/commands/CoreCommands.java

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,12 @@
2020

2121
package org.apache.james.managesieve.api.commands;
2222

23-
import org.apache.james.managesieve.api.CapabilityAdvertiser;
24-
2523
/**
2624
* Core RFC 5804 Commands common to all transports
2725
*
2826
* @see <a href=http://tools.ietf.org/html/rfc5804#section-2>RFC 5804 Commands</a>
2927
*/
3028
public interface CoreCommands extends Capability, CheckScript, DeleteScript, GetScript, HaveSpace,
31-
ListScripts, PutScript, RenameScript, SetActive, Noop, Unauthenticate, Logout, Authenticate, StartTLS,
32-
CapabilityAdvertiser {
29+
ListScripts, PutScript, RenameScript, SetActive, Noop, Unauthenticate, Logout, Authenticate, StartTLS {
3330

3431
}

protocols/managesieve/src/main/java/org/apache/james/managesieve/core/CoreProcessor.java

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@
2222

2323
import java.io.IOException;
2424
import java.nio.charset.StandardCharsets;
25-
import java.util.Arrays;
2625
import java.util.HashMap;
2726
import java.util.List;
2827
import java.util.Map;
@@ -57,7 +56,6 @@
5756
import com.google.common.base.Joiner;
5857
import com.google.common.base.Splitter;
5958
import com.google.common.base.Strings;
60-
import com.google.common.collect.Lists;
6159
import com.google.common.collect.Maps;
6260

6361
public class CoreProcessor implements CoreCommands {
@@ -83,11 +81,6 @@ public CoreProcessor(SieveRepository repository, UsersRepository usersRepository
8381
this.authenticationProcessorMap.put(SupportedMechanism.PLAIN, new PlainAuthenticationProcessor(usersRepository));
8482
}
8583

86-
@Override
87-
public String getAdvertisedCapabilities() {
88-
return convertCapabilityMapToString(capabilitiesBase) + "\r\n";
89-
}
90-
9184
@Override
9285
public String capability(Session session) {
9386
return convertCapabilityMapToString(computeCapabilityMap(session)) + "\r\nOK";
@@ -106,6 +99,10 @@ private Map<Capabilities, String> computeCapabilityMap(Session session) {
10699
if (session.isAuthenticated()) {
107100
capabilities.put(Capabilities.OWNER, session.getUser().asString());
108101
}
102+
session.getOidcSASLConfiguration().ifPresent(oidcConfiguration -> {
103+
this.authenticationProcessorMap.putIfAbsent(SupportedMechanism.XOAUTH2, new XOAUTH2AuthenticationProcessor(oidcConfiguration));
104+
});
105+
capabilities.put(Capabilities.SASL, constructSaslSupportedAuthenticationMechanisms());
109106
return capabilities;
110107
}
111108

@@ -218,6 +215,9 @@ public String chooseMechanism(Session session, String mechanism) {
218215
}
219216
String unquotedMechanism = ParserUtils.unquoteFirst(mechanism);
220217
SupportedMechanism supportedMechanism = SupportedMechanism.retrieveMechanism(unquotedMechanism);
218+
if (!this.authenticationProcessorMap.containsKey(supportedMechanism)) {
219+
throw new UnknownSaslMechanism("SASL mechanism disabled: " + unquotedMechanism);
220+
}
221221

222222
session.setChoosedAuthenticationMechanism(supportedMechanism);
223223
session.setState(Session.State.AUTHENTICATION_IN_PROGRESS);
@@ -328,7 +328,6 @@ private Map<Capabilities, String> precomputedCapabilitiesBase(SieveParser parser
328328
Map<Capabilities, String> capabilitiesBase = new HashMap<>();
329329
capabilitiesBase.put(Capabilities.IMPLEMENTATION, IMPLEMENTATION_DESCRIPTION);
330330
capabilitiesBase.put(Capabilities.VERSION, MANAGE_SIEVE_VERSION);
331-
capabilitiesBase.put(Capabilities.SASL, constructSaslSupportedAuthenticationMechanisms());
332331
capabilitiesBase.put(Capabilities.STARTTLS, null);
333332
if (!extensions.isEmpty()) {
334333
capabilitiesBase.put(Capabilities.SIEVE, extensions);
@@ -337,10 +336,12 @@ private Map<Capabilities, String> precomputedCapabilitiesBase(SieveParser parser
337336
}
338337

339338
private String constructSaslSupportedAuthenticationMechanisms() {
340-
return Joiner.on(' ')
341-
.join(Lists.transform(
342-
Arrays.asList(SupportedMechanism.values()),
343-
Enum::toString));
339+
return Joiner.on(' ').join(this.authenticationProcessorMap
340+
.keySet()
341+
.stream()
342+
.map(Enum::toString)
343+
.iterator()
344+
);
344345
}
345346

346347
private String sanitizeString(String message) {
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
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+
21+
package org.apache.james.managesieve.core;
22+
23+
import java.util.Optional;
24+
25+
import org.apache.james.core.Username;
26+
import org.apache.james.jwt.OidcJwtTokenVerifier;
27+
import org.apache.james.jwt.introspection.IntrospectionEndpoint;
28+
import org.apache.james.managesieve.api.AuthenticationException;
29+
import org.apache.james.managesieve.api.AuthenticationProcessor;
30+
import org.apache.james.managesieve.api.Session;
31+
import org.apache.james.managesieve.api.SyntaxException;
32+
import org.apache.james.protocols.api.OIDCSASLParser;
33+
import org.apache.james.protocols.api.OIDCSASLParser.OIDCInitialResponse;
34+
import org.apache.james.protocols.api.OidcSASLConfiguration;
35+
36+
import reactor.core.publisher.Mono;
37+
38+
public class XOAUTH2AuthenticationProcessor implements AuthenticationProcessor {
39+
40+
private final OidcSASLConfiguration oidcConfiguration;
41+
42+
public XOAUTH2AuthenticationProcessor(OidcSASLConfiguration oidcConfiguration) {
43+
this.oidcConfiguration = oidcConfiguration;
44+
}
45+
46+
@Override
47+
public String initialServerResponse(Session session) {
48+
return "+ \"\"";
49+
}
50+
51+
@Override
52+
public Username isAuthenticationSuccesfull(Session session, String suppliedClientData) throws SyntaxException, AuthenticationException {
53+
Optional<OIDCInitialResponse> oidcInitialResponseResult = OIDCSASLParser.parse(suppliedClientData);
54+
if (oidcInitialResponseResult.isEmpty()) {
55+
throw new SyntaxException("Could not parse the given JWT");
56+
}
57+
OIDCInitialResponse oidcInitialResponse = oidcInitialResponseResult.get();
58+
59+
Optional<Username> authenticatedUserResult = validateToken(oidcInitialResponse.getToken());
60+
if (authenticatedUserResult.isEmpty()) {
61+
throw new AuthenticationException("Could not validate the JWT");
62+
}
63+
Username authenticatedUser = authenticatedUserResult.get();
64+
65+
// The user from the managesieve AUTHENTICATE command must match the username in the token.
66+
Username associatedUser = Username.of(oidcInitialResponse.getAssociatedUser());
67+
if (!authenticatedUser.equals(associatedUser)) {
68+
throw new AuthenticationException("Mismatch between user from command and JWT");
69+
}
70+
71+
return authenticatedUser;
72+
}
73+
74+
private Optional<Username> validateToken(String token) {
75+
if (this.oidcConfiguration.isCheckTokenByIntrospectionEndpoint()) {
76+
return validTokenWithIntrospection(token);
77+
} else if (this.oidcConfiguration.isCheckTokenByUserinfoEndpoint()) {
78+
return validTokenWithUserInfo(token);
79+
} else {
80+
return OidcJwtTokenVerifier.verifySignatureAndExtractClaim(token, this.oidcConfiguration.getJwksURL(), this.oidcConfiguration.getClaim())
81+
.map(Username::of);
82+
}
83+
}
84+
85+
private Optional<Username> validTokenWithUserInfo(String token) {
86+
return Mono.from(OidcJwtTokenVerifier.verifyWithUserinfo(token,
87+
this.oidcConfiguration.getJwksURL(),
88+
this.oidcConfiguration.getClaim(),
89+
this.oidcConfiguration.getUserInfoEndpoint().orElseThrow()))
90+
.blockOptional()
91+
.map(Username::of);
92+
}
93+
94+
private Optional<Username> validTokenWithIntrospection(String token) {
95+
return Mono.from(OidcJwtTokenVerifier.verifyWithIntrospection(token,
96+
this.oidcConfiguration.getJwksURL(),
97+
this.oidcConfiguration.getClaim(),
98+
this.oidcConfiguration.getIntrospectionEndpoint()
99+
.map(endpoint -> new IntrospectionEndpoint(endpoint, this.oidcConfiguration.getIntrospectionEndpointAuthorization()))
100+
.orElseThrow()))
101+
.blockOptional()
102+
.map(Username::of);
103+
}
104+
}
105+

protocols/managesieve/src/main/java/org/apache/james/managesieve/transcode/ArgumentParser.java

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,10 +53,6 @@ public ArgumentParser(CoreCommands core, boolean validatePutSize) {
5353
this.validatePutSize = validatePutSize;
5454
}
5555

56-
public String getAdvertisedCapabilities() {
57-
return core.getAdvertisedCapabilities();
58-
}
59-
6056
public String capability(Session session, String args) {
6157
if (!args.trim().isEmpty()) {
6258
return "NO \"Too many arguments: " + args + "\"";

protocols/managesieve/src/main/java/org/apache/james/managesieve/transcode/ManageSieveProcessor.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -129,8 +129,8 @@ private String matchCommandWithImplementation(Session session, String arguments,
129129
return "NO unknown " + command + " command";
130130
}
131131

132-
public String getAdvertisedCapabilities() {
133-
return argumentParser.getAdvertisedCapabilities();
132+
public String getAdvertisedCapabilities(Session session) {
133+
return argumentParser.capability(session, "") + "\r\n";
134134
}
135135

136136
}

protocols/managesieve/src/main/java/org/apache/james/managesieve/util/SettableSession.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,16 +20,20 @@
2020

2121
package org.apache.james.managesieve.util;
2222

23+
import java.util.Optional;
24+
2325
import org.apache.james.core.Username;
2426
import org.apache.james.managesieve.api.Session;
2527
import org.apache.james.managesieve.api.commands.Authenticate;
28+
import org.apache.james.protocols.api.OidcSASLConfiguration;
2629

2730
public class SettableSession implements Session {
2831

2932
private Username user;
3033
private State state;
3134
private Authenticate.SupportedMechanism choosedAuthenticationMechanism;
3235
private boolean sslEnabled;
36+
private Optional<OidcSASLConfiguration> oidcSASLConfiguration = Optional.empty();
3337

3438
public SettableSession() {
3539
this.state = State.UNAUTHENTICATED;
@@ -80,4 +84,14 @@ public void setSslEnabled(boolean sslEnabled) {
8084
public boolean isSslEnabled() {
8185
return sslEnabled;
8286
}
87+
88+
@Override
89+
public Optional<OidcSASLConfiguration> getOidcSASLConfiguration() {
90+
return this.oidcSASLConfiguration;
91+
}
92+
93+
@Override
94+
public void setOidcSASLConfiguration(Optional<OidcSASLConfiguration> configuration) {
95+
this.oidcSASLConfiguration = configuration;
96+
}
8397
}

0 commit comments

Comments
 (0)