Repository: mina-sshd Updated Branches: refs/heads/master 5fc90bd04 -> 598c991fe
[SSHD-821] Support for async keyboard authentication Project: http://git-wip-us.apache.org/repos/asf/mina-sshd/repo Commit: http://git-wip-us.apache.org/repos/asf/mina-sshd/commit/5c1c8a98 Tree: http://git-wip-us.apache.org/repos/asf/mina-sshd/tree/5c1c8a98 Diff: http://git-wip-us.apache.org/repos/asf/mina-sshd/diff/5c1c8a98 Branch: refs/heads/master Commit: 5c1c8a9830ad5b622b15055bfc7205aa2fd53e98 Parents: 5fc90bd Author: Guillaume Nodet <gno...@apache.org> Authored: Wed Apr 18 14:17:28 2018 +0200 Committer: Guillaume Nodet <gno...@apache.org> Committed: Thu Apr 19 08:42:02 2018 +0200 ---------------------------------------------------------------------- .../sshd/server/auth/AsyncAuthException.java | 94 ++++++++++++ .../org/apache/sshd/server/auth/UserAuth.java | 6 +- .../auth/password/PasswordAuthenticator.java | 5 +- .../auth/pubkey/PublickeyAuthenticator.java | 4 +- .../server/session/ServerUserAuthService.java | 21 ++- .../server/auth/AsyncAuthInteractiveTest.java | 106 +++++++++++++ .../apache/sshd/server/auth/AsyncAuthTest.java | 103 +++++++++++++ .../sshd/server/auth/AsyncAuthTestBase.java | 147 +++++++++++++++++++ sshd-mina/pom.xml | 2 + 9 files changed, 483 insertions(+), 5 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/5c1c8a98/sshd-core/src/main/java/org/apache/sshd/server/auth/AsyncAuthException.java ---------------------------------------------------------------------- diff --git a/sshd-core/src/main/java/org/apache/sshd/server/auth/AsyncAuthException.java b/sshd-core/src/main/java/org/apache/sshd/server/auth/AsyncAuthException.java new file mode 100644 index 0000000..0a95986 --- /dev/null +++ b/sshd-core/src/main/java/org/apache/sshd/server/auth/AsyncAuthException.java @@ -0,0 +1,94 @@ +/* + * 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.sshd.server.auth; + +import java.lang.reflect.Array; +import java.util.function.Consumer; + +import org.apache.sshd.common.RuntimeSshException; + +/** + * + * + * @author <a href="mailto:d...@mina.apache.org">Apache MINA SSHD Project</a> + */ +public class AsyncAuthException extends RuntimeSshException { + + private static final long serialVersionUID = 6741236101797649869L; + + protected Object listener; + protected Boolean authed; + + public AsyncAuthException() { + super(); + } + + public void setAuthed(boolean authed) { + Object listener; + synchronized (this) { + if (this.authed != null) { + return; + } + this.authed = authed; + listener = this.listener; + } + if (listener != null) { + if (listener instanceof Consumer) { + asListener(listener).accept(authed); + } else { + int l = Array.getLength(listener); + for (int i = 0; i < l; i++) { + Consumer<Boolean> lst = asListener(Array.get(listener, i)); + if (lst != null) { + lst.accept(authed); + } + } + } + } + } + + @SuppressWarnings("unchecked") + protected static Consumer<Boolean> asListener(Object listener) { + return (Consumer<Boolean>) listener; + } + + public void addListener(Consumer<Boolean> listener) { + Boolean result; + synchronized (this) { + if (this.listener == null) { + this.listener = listener; + } else if (this.listener instanceof Consumer) { + this.listener = new Object[] {this.listener, listener }; + } else { + Object[] ol = (Object[]) this.listener; + int l = ol.length; + Object[] nl = new Object[l + 1]; + System.arraycopy(ol, 0, nl, 0, l); + nl[l] = listener; + this.listener = nl; + } + result = this.authed; + } + if (result != null) { + listener.accept(result); + } + } + +} http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/5c1c8a98/sshd-core/src/main/java/org/apache/sshd/server/auth/UserAuth.java ---------------------------------------------------------------------- diff --git a/sshd-core/src/main/java/org/apache/sshd/server/auth/UserAuth.java b/sshd-core/src/main/java/org/apache/sshd/server/auth/UserAuth.java index 91b919c..d26da38 100644 --- a/sshd-core/src/main/java/org/apache/sshd/server/auth/UserAuth.java +++ b/sshd-core/src/main/java/org/apache/sshd/server/auth/UserAuth.java @@ -42,9 +42,10 @@ public interface UserAuth extends ServerSessionHolder, UserAuthInstance<ServerSe * @param buffer the request buffer containing parameters specific to this request * @return <code>true</code> if the authentication succeeded, <code>false</code> if the authentication * failed and {@code null} if not finished yet + * @throws AsyncAuthException if the service is willing to perform an asynchronous authentication * @throws Exception if the authentication fails */ - Boolean auth(ServerSession session, String username, String service, Buffer buffer) throws Exception; + Boolean auth(ServerSession session, String username, String service, Buffer buffer) throws AsyncAuthException, Exception; /** * Handle another step in the authentication process. @@ -52,9 +53,10 @@ public interface UserAuth extends ServerSessionHolder, UserAuthInstance<ServerSe * @param buffer the request buffer containing parameters specific to this request * @return <code>true</code> if the authentication succeeded, <code>false</code> if the authentication * failed and {@code null} if not finished yet + * @throws AsyncAuthException if the service is willing to perform an asynchronous authentication * @throws Exception if the authentication fails */ - Boolean next(Buffer buffer) throws Exception; + Boolean next(Buffer buffer) throws AsyncAuthException, Exception; /** * Free any system resources used by the module. http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/5c1c8a98/sshd-core/src/main/java/org/apache/sshd/server/auth/password/PasswordAuthenticator.java ---------------------------------------------------------------------- diff --git a/sshd-core/src/main/java/org/apache/sshd/server/auth/password/PasswordAuthenticator.java b/sshd-core/src/main/java/org/apache/sshd/server/auth/password/PasswordAuthenticator.java index 902c128..fcc91e1 100644 --- a/sshd-core/src/main/java/org/apache/sshd/server/auth/password/PasswordAuthenticator.java +++ b/sshd-core/src/main/java/org/apache/sshd/server/auth/password/PasswordAuthenticator.java @@ -18,6 +18,7 @@ */ package org.apache.sshd.server.auth.password; +import org.apache.sshd.server.auth.AsyncAuthException; import org.apache.sshd.server.session.ServerSession; /** @@ -36,6 +37,8 @@ public interface PasswordAuthenticator { * @return {@code true} indicating if authentication succeeded * @throws PasswordChangeRequiredException If the password is expired or * not strong enough to suit the server's policy + * @throws AsyncAuthException If the authentication is performed asynchronously */ - boolean authenticate(String username, String password, ServerSession session) throws PasswordChangeRequiredException; + boolean authenticate(String username, String password, ServerSession session) + throws PasswordChangeRequiredException, AsyncAuthException; } http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/5c1c8a98/sshd-core/src/main/java/org/apache/sshd/server/auth/pubkey/PublickeyAuthenticator.java ---------------------------------------------------------------------- diff --git a/sshd-core/src/main/java/org/apache/sshd/server/auth/pubkey/PublickeyAuthenticator.java b/sshd-core/src/main/java/org/apache/sshd/server/auth/pubkey/PublickeyAuthenticator.java index 5146ea7..2d7a908 100644 --- a/sshd-core/src/main/java/org/apache/sshd/server/auth/pubkey/PublickeyAuthenticator.java +++ b/sshd-core/src/main/java/org/apache/sshd/server/auth/pubkey/PublickeyAuthenticator.java @@ -20,6 +20,7 @@ package org.apache.sshd.server.auth.pubkey; import java.security.PublicKey; +import org.apache.sshd.server.auth.AsyncAuthException; import org.apache.sshd.server.session.ServerSession; /** @@ -38,6 +39,7 @@ public interface PublickeyAuthenticator { * @param key the key * @param session the server session * @return a boolean indicating if authentication succeeded or not + * @throws AsyncAuthException If the authentication is performed asynchronously */ - boolean authenticate(String username, PublicKey key, ServerSession session); + boolean authenticate(String username, PublicKey key, ServerSession session) throws AsyncAuthException; } http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/5c1c8a98/sshd-core/src/main/java/org/apache/sshd/server/session/ServerUserAuthService.java ---------------------------------------------------------------------- diff --git a/sshd-core/src/main/java/org/apache/sshd/server/session/ServerUserAuthService.java b/sshd-core/src/main/java/org/apache/sshd/server/session/ServerUserAuthService.java index 28ea4d4..2c72b6d 100644 --- a/sshd-core/src/main/java/org/apache/sshd/server/session/ServerUserAuthService.java +++ b/sshd-core/src/main/java/org/apache/sshd/server/session/ServerUserAuthService.java @@ -55,6 +55,7 @@ import org.apache.sshd.common.util.closeable.AbstractCloseable; import org.apache.sshd.common.util.io.IoUtils; import org.apache.sshd.server.ServerAuthenticationManager; import org.apache.sshd.server.ServerFactoryManager; +import org.apache.sshd.server.auth.AsyncAuthException; import org.apache.sshd.server.auth.UserAuth; import org.apache.sshd.server.auth.UserAuthNoneFactory; import org.apache.sshd.server.auth.WelcomeBannerPhase; @@ -143,7 +144,7 @@ public class ServerUserAuthService extends AbstractCloseable implements Service, } @Override - public void process(int cmd, Buffer buffer) throws Exception { + public synchronized void process(int cmd, Buffer buffer) throws Exception { Boolean authed = Boolean.FALSE; ServerSession session = getServerSession(); boolean debugEnabled = log.isDebugEnabled(); @@ -196,6 +197,9 @@ public class ServerUserAuthService extends AbstractCloseable implements Service, currentAuth = ValidateUtils.checkNotNull(factory.create(), "No authenticator created for method=%s", method); try { authed = currentAuth.auth(session, username, service, buffer); + } catch (AsyncAuthException async) { + async.addListener(authenticated -> asyncAuth(cmd, buffer, authenticated)); + return; } catch (Exception e) { if (debugEnabled) { log.debug("process({}) Failed ({}) to authenticate using factory method={}: {}", @@ -228,6 +232,9 @@ public class ServerUserAuthService extends AbstractCloseable implements Service, buffer.rpos(buffer.rpos() - 1); try { authed = currentAuth.next(buffer); + } catch (AsyncAuthException async) { + async.addListener(authenticated -> asyncAuth(cmd, buffer, authenticated)); + return; } catch (Exception e) { // Continue if (debugEnabled) { @@ -249,6 +256,18 @@ public class ServerUserAuthService extends AbstractCloseable implements Service, } } + protected synchronized void asyncAuth(int cmd, Buffer buffer, boolean authed) { + try { + if (authed) { + handleAuthenticationSuccess(cmd, buffer); + } else { + handleAuthenticationFailure(cmd, buffer); + } + } catch (Exception e) { + log.warn("Error performing async authentication: {}", e.getMessage(), e); + } + } + protected void handleAuthenticationInProgress(int cmd, Buffer buffer) throws Exception { String username = (currentAuth == null) ? null : currentAuth.getUsername(); if (log.isDebugEnabled()) { http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/5c1c8a98/sshd-core/src/test/java/org/apache/sshd/server/auth/AsyncAuthInteractiveTest.java ---------------------------------------------------------------------- diff --git a/sshd-core/src/test/java/org/apache/sshd/server/auth/AsyncAuthInteractiveTest.java b/sshd-core/src/test/java/org/apache/sshd/server/auth/AsyncAuthInteractiveTest.java new file mode 100644 index 0000000..42763ef --- /dev/null +++ b/sshd-core/src/test/java/org/apache/sshd/server/auth/AsyncAuthInteractiveTest.java @@ -0,0 +1,106 @@ +/* + * 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.sshd.server.auth; + +import com.jcraft.jsch.ChannelShell; +import com.jcraft.jsch.JSch; +import com.jcraft.jsch.JSchException; +import com.jcraft.jsch.Session; +import com.jcraft.jsch.UserInfo; + +import org.junit.FixMethodOrder; +import org.junit.runners.MethodSorters; + +/** + * @author <a href="mailto:d...@mina.apache.org">Apache MINA SSHD Project</a> + */ +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +public class AsyncAuthInteractiveTest extends AsyncAuthTestBase { + + public AsyncAuthInteractiveTest() { + super(); + } + + protected boolean authenticate() throws Exception { + + JSch jsch = new JSch(); + Session session; + ChannelShell channel; + + session = jsch.getSession("whatever", "localhost", port); + session.setUserInfo(new UserInfo() { + @Override + public String getPassphrase() { + throw new UnsupportedOperationException(); + } + + @Override + public String getPassword() { + return "whocares"; + } + + @Override + public boolean promptPassword(String s) { + return true; + } + + @Override + public boolean promptPassphrase(String s) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean promptYesNo(String s) { + return true; + } + + @Override + public void showMessage(String s) { + // Do nothing + } + }); + try { + session.connect(); + } catch (JSchException e) { + switch (e.getMessage()) { + case "Auth cancel": + case "Auth fail": + return false; + default: + throw e; + } + } + channel = (ChannelShell) session.openChannel("shell"); + channel.connect(); + + try { + channel.disconnect(); + } catch (Exception ignore) { + // ignore + } + + try { + session.disconnect(); + } catch (Exception ignore) { + // ignore + } + + return true; + } +} http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/5c1c8a98/sshd-core/src/test/java/org/apache/sshd/server/auth/AsyncAuthTest.java ---------------------------------------------------------------------- diff --git a/sshd-core/src/test/java/org/apache/sshd/server/auth/AsyncAuthTest.java b/sshd-core/src/test/java/org/apache/sshd/server/auth/AsyncAuthTest.java new file mode 100644 index 0000000..5f8f591 --- /dev/null +++ b/sshd-core/src/test/java/org/apache/sshd/server/auth/AsyncAuthTest.java @@ -0,0 +1,103 @@ +/* + * 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.sshd.server.auth; + +import com.jcraft.jsch.ChannelShell; +import com.jcraft.jsch.JSch; +import com.jcraft.jsch.JSchException; +import com.jcraft.jsch.Session; +import com.jcraft.jsch.UserInfo; + +import org.junit.FixMethodOrder; +import org.junit.runners.MethodSorters; + +/** + * @author <a href="mailto:d...@mina.apache.org">Apache MINA SSHD Project</a> + */ +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +public class AsyncAuthTest extends AsyncAuthTestBase { + + public AsyncAuthTest() { + super(); + } + + protected boolean authenticate() throws Exception { + + JSch jsch = new JSch(); + Session session; + ChannelShell channel; + + session = jsch.getSession("whatever", "localhost", port); + session.setPassword("whocares"); + session.setUserInfo(new UserInfo() { + @Override + public String getPassphrase() { + return null; + } + + @Override + public String getPassword() { + return null; + } + + @Override + public boolean promptPassword(String s) { + return false; + } + + @Override + public boolean promptPassphrase(String s) { + return false; + } + + @Override + public boolean promptYesNo(String s) { + return true; + } // Accept all server keys + + @Override + public void showMessage(String s) { + // Do nothing + } + }); + try { + session.connect(); + } catch (JSchException e) { + if (e.getMessage().equals("Auth cancel")) { + return false; + } else { + throw e; + } + } + channel = (ChannelShell) session.openChannel("shell"); + channel.connect(); + + try { + channel.disconnect(); + } catch (Exception ignore) { + } + + try { + session.disconnect(); + } catch (Exception ignore) { + } + + return true; + } +} http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/5c1c8a98/sshd-core/src/test/java/org/apache/sshd/server/auth/AsyncAuthTestBase.java ---------------------------------------------------------------------- diff --git a/sshd-core/src/test/java/org/apache/sshd/server/auth/AsyncAuthTestBase.java b/sshd-core/src/test/java/org/apache/sshd/server/auth/AsyncAuthTestBase.java new file mode 100644 index 0000000..4328117 --- /dev/null +++ b/sshd-core/src/test/java/org/apache/sshd/server/auth/AsyncAuthTestBase.java @@ -0,0 +1,147 @@ +/* + * 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.sshd.server.auth; + +import java.io.File; + +import com.jcraft.jsch.JSchException; + +import org.apache.sshd.common.FactoryManager; +import org.apache.sshd.server.SshServer; +import org.apache.sshd.server.auth.password.PasswordAuthenticator; +import org.apache.sshd.server.keyprovider.SimpleGeneratorHostKeyProvider; +import org.apache.sshd.util.test.BaseTestSupport; +import org.apache.sshd.util.test.EchoShellFactory; +import org.junit.After; +import org.junit.Test; + +/** + * @author <a href="mailto:d...@mina.apache.org">Apache MINA SSHD Project</a> + */ +public abstract class AsyncAuthTestBase extends BaseTestSupport { + + SshServer server; + int port; + + private PasswordAuthenticator authenticator; + + public AsyncAuthTestBase() { + super(); + } + + public void startServer() throws Exception { + startServer(null); + } + + public void startServer(Integer timeout) throws Exception { + if (server != null) { + fail("Server already started"); + } + server = SshServer.setUpDefaultServer(); + if (timeout != null) { + server.getProperties().put(FactoryManager.AUTH_TIMEOUT, timeout.toString()); + } + server.setKeyPairProvider(new SimpleGeneratorHostKeyProvider(new File("hostkey.ser").toPath())); + server.setPasswordAuthenticator((username, password, session) -> authenticator.authenticate(username, password, session)); + server.setShellFactory(new EchoShellFactory()); + server.start(); + port = server.getPort(); + } + + @After + public void stopServer() throws Exception { + if (server != null) { + server.stop(); + } + server = null; + } + + @Test + public void testSyncAuthFailed() throws Exception { + startServer(); + authenticator = (username, x, sess) -> false; + assertFalse(authenticate()); + } + + @Test + public void testSyncAuthSucceeded() throws Exception { + startServer(); + authenticator = (username, x, sess) -> true; + assertTrue(authenticate()); + } + + @Test + public void testAsyncAuthFailed() throws Exception { + startServer(); + authenticator = (username, x, sess) -> async(200, false); + assertFalse(authenticate()); + } + + @Test + public void testAsyncAuthSucceeded() throws Exception { + startServer(); + authenticator = (username, x, sess) -> async(200, true); + assertTrue(authenticate()); + } + + @Test + public void testAsyncAuthTimeout() throws Exception { + startServer(500); + authenticator = (username, x, sess) -> asyncTimeout(); + try { + authenticate(); + } catch (JSchException e) { + assertTrue("Unexpected failure " + e.getMessage(), e.getMessage().startsWith("SSH_MSG_DISCONNECT")); + } + } + + @Test + public void testAsyncAuthSucceededAfterTimeout() throws Exception { + startServer(500); + authenticator = (username, x, sess) -> async(1000, true); + try { + authenticate(); + } catch (JSchException e) { + assertTrue("Unexpected failure " + e.getMessage(), e.getMessage().startsWith("SSH_MSG_DISCONNECT")); + } + } + + private boolean asyncTimeout() { + throw new AsyncAuthException(); + } + + private boolean async(int delay, boolean result) { + AsyncAuthException auth = new AsyncAuthException(); + new Thread(() -> doAsync(delay, result, auth)).start(); + throw auth; + } + + private void doAsync(int delay, boolean result, AsyncAuthException auth) { + try { + Thread.sleep(delay); + } catch (InterruptedException ignore) { + // ignore + } finally { + auth.setAuthed(result); + } + } + + protected abstract boolean authenticate() throws Exception; + +} http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/5c1c8a98/sshd-mina/pom.xml ---------------------------------------------------------------------- diff --git a/sshd-mina/pom.xml b/sshd-mina/pom.xml index 1000f8a..bb62820 100644 --- a/sshd-mina/pom.xml +++ b/sshd-mina/pom.xml @@ -180,6 +180,8 @@ <exclude>**/MacTest.java</exclude> <exclude>**/SpringConfigTest.java</exclude> <exclude>**/ConcurrentConnectionTest.java</exclude> + <exclude>**/AsyncAuthTest.java</exclude> + <exclude>**/AsyncAuthInteractiveTest.java</exclude> </excludes> <!-- No need to re-run core tests that do not involve session creation --> <excludedGroups>org.apache.sshd.util.test.NoIoTestCase</excludedGroups>