Skip to content
Draft
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
2 changes: 1 addition & 1 deletion examples/custom-imap/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ Connected to localhost.
Escape character is '^]'.
* OK JAMES IMAP4rev1 Server james.local is ready.
a01 LOGIN bob secret
a01 NO LOGIN failed. Invalid login/password.
a01 NO LOGIN failed. Invalid credentials.
a02 LOGIN bob@localhost secret
a02 OK LOGIN completed.
A03 PING
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,15 +85,15 @@ REINIT
C: 0005 AUTHENTICATE "PLAIN" {28+}
# \0imapuser\0badpassword
C: AGltYXB1c2VyAGJhZHBhc3N3b3Jk
S: 0005 NO AUTHENTICATE failed. Authentication failed.
S: 0005 NO AUTHENTICATE failed. Invalid credentials.

REINIT

# Bad user cannot authenticate
C: 0006 AUTHENTICATE "PLAIN" {24+}
# \0baduser\0password
C: AGJhZHVzZXIAcGFzc3dvcmQ=
S: 0006 NO AUTHENTICATE failed. Authentication failed.
S: 0006 NO AUTHENTICATE failed. Invalid credentials.

REINIT

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,10 @@ C: a002a LOGIN imapuser password extra
S: a002a BAD LOGIN failed. Illegal arguments.

C: a003 LOGIN invaliduser password
S: a003 NO LOGIN failed. Invalid login/password.
S: a003 NO LOGIN failed. Invalid credentials.

C: a004 LOGIN imapuser invalid
S: a004 NO LOGIN failed. Invalid login/password.
S: a004 NO LOGIN failed. Invalid credentials.

C: a005 LOGIN imapuser password
S: a005 OK LOGIN completed.
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,10 @@
#
S: \* OK IMAP4rev1 Server ready
C: a003 LOGIN invaliduser password
S: a003 NO LOGIN failed. Invalid login/password.
S: a003 NO LOGIN failed. Invalid credentials.

C: a004 LOGIN imapuser invalid
S: a004 NO LOGIN failed. Invalid login/password.
S: a004 NO LOGIN failed. Invalid credentials.

C: a005 LOGIN imapuser bogus
S: \* BYE Login failed too many times.
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ public static HumanReadableText permanentFlags(Flags flags) {
public static final HumanReadableText COMPLETED = new HumanReadableText("org.apache.james.imap.COMPLETED", "completed.");
public static final HumanReadableText REPLACE_READY = new HumanReadableText("org.apache.james.imap.REPLACE", "Replacement Message ready");

public static final HumanReadableText INVALID_LOGIN = new HumanReadableText("org.apache.james.imap.INVALID_LOGIN", "failed. Invalid login/password.");
public static final HumanReadableText INVALID_CREDENTIALS = new HumanReadableText("org.apache.james.imap.INVALID_CREDENTIALS", "failed. Invalid credentials.");

public static final HumanReadableText DISABLED_LOGIN = new HumanReadableText("org.apache.james.imap.DISABLED_LOGIN", "failed. Plain login / authentication are disabled.");

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@
import org.apache.james.imap.api.message.response.StatusResponseFactory;
import org.apache.james.imap.api.process.ImapSession;
import org.apache.james.imap.main.PathConverter;
import org.apache.james.jwt.OidcJwtTokenVerifier;
import org.apache.james.jwt.OidcSASLConfiguration;
import org.apache.james.mailbox.Authorizator;
import org.apache.james.mailbox.DefaultMailboxes;
import org.apache.james.mailbox.MailboxManager;
Expand All @@ -39,6 +41,7 @@
import org.apache.james.mailbox.model.MailboxConstants;
import org.apache.james.mailbox.model.MailboxPath;
import org.apache.james.metrics.api.MetricFactory;
import org.apache.james.protocols.api.OIDCSASLParser;
import org.apache.james.util.AuditTrail;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand Down Expand Up @@ -77,62 +80,52 @@ public void configure(ImapConfiguration imapConfiguration) {
this.imapConfiguration = imapConfiguration;
}

protected void doAuth(AuthenticationAttempt authenticationAttempt, ImapSession session, ImapRequest request, Responder responder, HumanReadableText failed) {
protected void doPasswordAuth(AuthenticationAttempt authenticationAttempt, ImapSession session, ImapRequest request, Responder responder) {
Preconditions.checkArgument(!authenticationAttempt.isDelegation());
try {
boolean authFailure = false;
if (authenticationAttempt.getAuthenticationId() == null) {
authFailure = true;
}
if (!authFailure) {
try {
final MailboxSession mailboxSession = getMailboxManager().authenticate(authenticationAttempt.getAuthenticationId(),
authenticationAttempt.getPassword())
.withoutDelegation();
session.authenticated();
session.setMailboxSession(mailboxSession);
provisionInbox(session, getMailboxManager(), mailboxSession);
AuditTrail.entry()
.username(() -> mailboxSession.getUser().asString())
.sessionId(() -> session.sessionId().asString())
.protocol("IMAP")
.action("AUTH")
.log("IMAP Authentication succeeded.");
okComplete(request, responder);
session.stopDetectingCommandInjection();
} catch (BadCredentialsException e) {
authFailure = true;
AuditTrail.entry()
.username(() -> authenticationAttempt.getAuthenticationId().asString())
.protocol("IMAP")
.action("AUTH")
.log("IMAP Authentication failed because of bad credentials.");
}
}
if (authFailure) {
manageFailureCount(session, request, responder, failed);

if (authenticationAttempt.getAuthenticationId() == null || authenticationAttempt.getPassword() == null) {
authFailure(session, request, responder, HumanReadableText.AUTHENTICATION_FAILED, Optional.empty(), Optional.empty(),
"Malformed authentication command."
);
} else {
try {
final MailboxSession mailboxSession = getMailboxManager().authenticate(
authenticationAttempt.getAuthenticationId(),
authenticationAttempt.getPassword()
).withoutDelegation();
authSuccess(session, mailboxSession, request, responder, "Password authentication succeeded.");
} catch (BadCredentialsException e) {
authFailure(session, request, responder, HumanReadableText.INVALID_CREDENTIALS,
Optional.of(authenticationAttempt.getAuthenticationId()),
Optional.empty(),
"Password authentication failed because of bad credentials."
);
} catch (MailboxException e) {
// This is probably not a user error, so we do not increase the failure count or add the
// event to the audit log.
LOGGER.error("Authentication failed", e);
no(request, responder, HumanReadableText.GENERIC_FAILURE_DURING_PROCESSING);
}
} catch (MailboxException e) {
LOGGER.error("Error encountered while login", e);
no(request, responder, HumanReadableText.GENERIC_FAILURE_DURING_PROCESSING);
}
}

protected void doAuthWithDelegation(AuthenticationAttempt authenticationAttempt, ImapSession session, ImapRequest request, Responder responder) {
protected void doPasswordAuthWithDelegation(AuthenticationAttempt authenticationAttempt, ImapSession session, ImapRequest request, Responder responder) {
Preconditions.checkArgument(authenticationAttempt.isDelegation());
Username otherUser = authenticationAttempt.getDelegateUserName().orElseThrow();

Username givenUser = authenticationAttempt.getAuthenticationId();
if (givenUser == null) {
manageFailureCount(session, request, responder);
return;
authFailure(session, request, responder, HumanReadableText.AUTHENTICATION_FAILED,
Optional.empty(), Optional.of(otherUser), "Malformed authentication command.");
} else {
doAuthWithDelegation(() -> getMailboxManager()
.withExtraAuthorizator(withAdminUsers())
.authenticate(givenUser, authenticationAttempt.getPassword())
.as(otherUser),
session,
request, responder,
givenUser, otherUser);
}
Username otherUser = authenticationAttempt.getDelegateUserName().orElseThrow();
doAuthWithDelegation(() -> getMailboxManager()
.withExtraAuthorizator(withAdminUsers())
.authenticate(givenUser, authenticationAttempt.getPassword())
.as(otherUser),
session,
request, responder,
givenUser, otherUser);
}

protected Authorizator withAdminUsers() {
Expand All @@ -148,49 +141,48 @@ protected void doAuthWithDelegation(MailboxSessionAuthWithDelegationSupplier mai
ImapSession session, ImapRequest request, Responder responder,
Username authenticateUser, Username delegatorUser) {
try {
MailboxManager mailboxManager = getMailboxManager();
MailboxSession mailboxSession = mailboxSessionSupplier.get();
session.authenticated();
session.setMailboxSession(mailboxSession);
AuditTrail.entry()
.username(() -> mailboxSession.getLoggedInUser()
.map(Username::asString)
.orElse(""))
.sessionId(() -> session.sessionId().asString())
.protocol("IMAP")
.action("AUTH")
.remoteIP(() -> Optional.ofNullable(session.getRemoteAddress()))
.parameters(() -> ImmutableMap.of("delegatorUser", mailboxSession.getUser().asString()))
.log("IMAP Authentication with delegation succeeded.");
okComplete(request, responder);
provisionInbox(session, mailboxManager, mailboxSession);
authSuccess(session, mailboxSessionSupplier.get(), request, responder, "Authentication with delegation succeeded.");
} catch (BadCredentialsException e) {
AuditTrail.entry()
.username(authenticateUser::asString)
.protocol("IMAP")
.action("AUTH")
.remoteIP(() -> Optional.ofNullable(session.getRemoteAddress()))
.parameters(() -> ImmutableMap.of("delegatorUser", delegatorUser.asString()))
.log("IMAP Authentication with delegation failed because of bad credentials.");
manageFailureCount(session, request, responder);
authFailure(session, request, responder, HumanReadableText.INVALID_CREDENTIALS, Optional.of(authenticateUser),
Optional.of(delegatorUser), "Password authentication with delegation failed because of bad credentials.");
} catch (UserDoesNotExistException e) {
LOGGER.info("User does not exist", e);
no(request, responder, HumanReadableText.USER_DOES_NOT_EXIST);
authFailure(session, request, responder, HumanReadableText.USER_DOES_NOT_EXIST, Optional.of(authenticateUser),
Optional.of(delegatorUser), "Delegation target user does not exist.");
} catch (ForbiddenDelegationException e) {
LOGGER.info("Delegate forbidden", e);
AuditTrail.entry()
.username(authenticateUser::asString)
.protocol("IMAP")
.action("AUTH")
.parameters(() -> ImmutableMap.of("delegatorUser", delegatorUser.asString()))
.log("IMAP Authentication with delegation failed because of non existing delegation.");
no(request, responder, HumanReadableText.DELEGATION_FORBIDDEN);
authFailure(session, request, responder, HumanReadableText.DELEGATION_FORBIDDEN, Optional.of(authenticateUser),
Optional.of(delegatorUser), "Requested delegation is forbidden.");
} catch (MailboxException e) {
LOGGER.info("Login failed", e);
// This is probably not a user error, so we do not increase the failure count or add the
// event to the audit log.
LOGGER.info("Authentication failed", e);
no(request, responder, HumanReadableText.GENERIC_FAILURE_DURING_PROCESSING);
}
}

protected void doOAuth(OIDCSASLParser.OIDCInitialResponse oidcInitialResponse, OidcSASLConfiguration oidcSASLConfiguration,
ImapSession session, ImapRequest request, Responder responder) {
new OidcJwtTokenVerifier(oidcSASLConfiguration).validateToken(oidcInitialResponse.getToken())
.ifPresentOrElse(authenticatedUser -> {
Username associatedUser = Username.of(oidcInitialResponse.getAssociatedUser());
if (!associatedUser.equals(authenticatedUser)) {
doAuthWithDelegation(() -> getMailboxManager()
.withExtraAuthorizator(withAdminUsers())
.authenticate(authenticatedUser)
.as(associatedUser),
session, request, responder, authenticatedUser, associatedUser);
} else {
authSuccess(session, getMailboxManager().createSystemSession(authenticatedUser), request, responder,
"OAuth authentication succeeded."
);
}
}, () -> {
authFailure(session, request, responder, HumanReadableText.AUTHENTICATION_FAILED, Optional.empty(),
Optional.of(Username.of(oidcInitialResponse.getAssociatedUser())),
"OAuth authentication failed."
);
});
}

protected void provisionInbox(ImapSession session, MailboxManager mailboxManager, MailboxSession mailboxSession) throws MailboxException {
MailboxPath inboxPath = pathConverterFactory.forSession(session).buildFullPath(MailboxConstants.INBOX);
if (Mono.from(mailboxManager.mailboxExists(inboxPath, mailboxSession)).block()) {
Expand Down Expand Up @@ -223,10 +215,6 @@ private void provisionMailbox(String mailbox, MailboxManager mailboxManager,
}
}

protected void manageFailureCount(ImapSession session, ImapRequest request, Responder responder) {
manageFailureCount(session, request, responder, HumanReadableText.AUTHENTICATION_FAILED);
}

protected void manageFailureCount(ImapSession session, ImapRequest request, Responder responder, HumanReadableText failed) {
Integer currentNumberOfFailures = (Integer) session.getAttribute(ATTRIBUTE_NUMBER_OF_FAILURES);
int failures;
Expand All @@ -253,10 +241,43 @@ protected static AuthenticationAttempt noDelegation(Username authenticationId, S
return new AuthenticationAttempt(Optional.empty(), authenticationId, password);
}

protected void authSuccess(Username username, ImapSession session, ImapRequest request, Responder responder) {
protected void authSuccess(ImapSession session, MailboxSession mailboxSession, ImapRequest request, Responder responder, String log) {
session.authenticated();
session.setMailboxSession(getMailboxManager().createSystemSession(username));
session.setMailboxSession(mailboxSession);
try {
provisionInbox(session, getMailboxManager(), mailboxSession);
} catch (MailboxException e) {
// TODO: Should this abort and cancel the authentication?
LOGGER.error("Provisioning mailboxes failed", e);
}

AuditTrail.Entry entry = AuditTrail.entry()
.username(() -> mailboxSession.getUser().asString())
.sessionId(() -> session.sessionId().asString())
.protocol("IMAP")
.action("AUTH")
.remoteIP(() -> Optional.ofNullable(session.getRemoteAddress()));
Optional<Username> assumedUser = mailboxSession.getLoggedInUser();
if (assumedUser.isPresent()) {
entry = entry.parameters(() -> ImmutableMap.of("delegatorUser", assumedUser.get().asString()));
}
entry.log(log);
okComplete(request, responder);
session.stopDetectingCommandInjection();
}

protected void authFailure(ImapSession session, ImapRequest request, Responder responder, HumanReadableText failed, Optional<Username> username, Optional<Username> assumedUser, String log) {
AuditTrail.Entry entry = AuditTrail.entry()
.username(() -> username.map(name -> name.asString()).orElse(null))
.sessionId(() -> session.sessionId().asString())
.protocol("IMAP")
.action("AUTH")
.remoteIP(() -> Optional.ofNullable(session.getRemoteAddress()));
if (assumedUser.isPresent()) {
entry = entry.parameters(() -> ImmutableMap.of("delegatorUser", assumedUser.get().asString()));
}
entry.log(log);
manageFailureCount(session, request, responder, failed);
}

protected static class AuthenticationAttempt {
Expand Down
Loading