Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Spring boot security #155

Merged
merged 12 commits into from Sep 17, 2020
Merged

Spring boot security #155

merged 12 commits into from Sep 17, 2020

Conversation

jvmlet
Copy link
Collaborator

@jvmlet jvmlet commented Sep 17, 2020

No description provided.

@jvmlet jvmlet merged commit ca82199 into master Sep 17, 2020
@jvmlet jvmlet deleted the security branch September 17, 2020 11:11
@ST-DDT
Copy link

ST-DDT commented Sep 17, 2020

This implementation might be vulnerable to the bug described here:
#61 (comment)
I'll try to write a test for it to confirm it.
EDIT: Do you have a test for a failed authentication that I could easily extend for that? (Run the success and failing calls concurrently)

@jvmlet
Copy link
Collaborator Author

jvmlet commented Sep 17, 2020

Yes, search for concurrency test in demo project
Here it is
https://github.com/LogNet/grpc-spring-boot-starter/blob/master/grpc-spring-boot-starter-demo/src/test/java/org/lognet/springboot/grpc/auth/JwtRoleTest.java

@ST-DDT
Copy link

ST-DDT commented Sep 17, 2020

Implementation is vulnerable

I can confirm the same bug applies.
Here is the test I used to confirm it.
Please check it for usage errors as I'm not familiar with your library.

package org.lognet.springboot.grpc.auth;

import static org.junit.Assert.assertThrows;
import static org.junit.jupiter.api.Assertions.assertAll;
import static org.junit.jupiter.api.Assertions.assertEquals;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.lognet.springboot.grpc.GrpcServerTestBase;
import org.lognet.springboot.grpc.demo.DemoApp;
import org.lognet.springboot.grpc.security.AuthCallCredentials;
import org.lognet.springboot.grpc.security.AuthHeader;
import org.lognet.springboot.grpc.security.EnableGrpcSecurity;
import org.lognet.springboot.grpc.security.GrpcSecurity;
import org.lognet.springboot.grpc.security.GrpcSecurityConfigurerAdapter;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.annotation.Import;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.test.context.junit4.SpringRunner;

import com.google.protobuf.Empty;

import io.grpc.Status;
import io.grpc.StatusRuntimeException;
import io.grpc.examples.SecuredGreeterGrpc;
import io.grpc.examples.GreeterGrpc.GreeterFutureStub;
import lombok.extern.slf4j.Slf4j;

@SpringBootTest(classes = DemoApp.class, properties = "spring.cloud.service-registry.auto-registration.enabled=false")
@RunWith(SpringRunner.class)
@Import({ ConcurrentAuthConfigTest.TestCfg.class })
@Slf4j
public class ConcurrentAuthConfigTest extends GrpcServerTestBase {

	private AuthCallCredentials callCredentials = new AuthCallCredentials(
			AuthHeader.builder().basic("test", "test".getBytes()));

	@TestConfiguration
	static class TestCfg {

		@EnableGrpcSecurity
		public class DemoGrpcSecurityConfig extends GrpcSecurityConfigurerAdapter {

			@Override
			public void configure(GrpcSecurity builder) throws Exception {
				DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
				List<GrantedAuthority> roles = Arrays.asList(new SimpleGrantedAuthority("ROLE_TEST"));
				UserDetailsService users = new InMemoryUserDetailsManager(new User("test", "test", roles));
				provider.setUserDetailsService(users);
				provider.setPasswordEncoder(NoOpPasswordEncoder.getInstance());

				builder.authenticationProvider(provider);
				builder.authorizeRequests().anyMethod().authenticated();
			}

		}
	}

	@Test
	public void concurrentTest() throws InterruptedException {
		System.out.println();

		final SecuredGreeterGrpc.SecuredGreeterBlockingStub unsecuredFutureStub = SecuredGreeterGrpc
				.newBlockingStub(selectedChanel);
		final SecuredGreeterGrpc.SecuredGreeterBlockingStub securedFutureStub = unsecuredFutureStub
				.withCallCredentials(callCredentials);

		int parallelTests = 10;

		List<Thread> threads = new ArrayList<>();
		// Number of threads that passed the test
		AtomicInteger successCounter = new AtomicInteger(0);
		AtomicInteger failureCounter = new AtomicInteger(0);

		Runnable authenticated = () -> {
			securedFutureStub.sayAuthHello(Empty.getDefaultInstance()).getMessage();
//			log.warn("Call passed");
		};
		Runnable unauthenticated = () -> {
			StatusRuntimeException err = assertThrows(StatusRuntimeException.class,
					() -> unsecuredFutureStub.sayAuthHello(Empty.getDefaultInstance()).getMessage());
			assertEquals(Status.Code.UNAUTHENTICATED, err.getStatus().getCode());
//			log.warn("Call blocked");
		};

		// Check that the assertions work as is (single threaded)
		authenticated.run();
		unauthenticated.run();

		for (int i = 0; i < parallelTests; i++) {
			Thread success = new Thread(() -> {

				for (int j = 0; j < 1000; j++) {
					authenticated.run();
				}
				successCounter.incrementAndGet();
				log.info("All passed");
			});
			success.setUncaughtExceptionHandler((thread, ex) -> {
				log.error("SECURITY ???", ex);
			});
			threads.add(success);

			Thread failure = new Thread(() -> {

				for (int j = 0; j < 1000; j++) {
					unauthenticated.run();
				}
				failureCounter.incrementAndGet();
				log.info("All passed");
			});
			failure.setUncaughtExceptionHandler((thread, ex) -> {
				log.error("SECURITY BYPASSED", ex);
			});

			threads.add(failure);
		}

		Collections.shuffle(threads);
		for (Thread thread : threads) {
			thread.start();
		}
		for (Thread thread : threads) {
			thread.join();
		}

		assertAll(() -> assertEquals(parallelTests, successCounter.get()),
				() -> assertEquals(parallelTests, failureCounter.get()));
	}

	@Override
	protected GreeterFutureStub beforeGreeting(GreeterFutureStub stub) {
		return stub.withCallCredentials(callCredentials);
	}

}

You will encounter the following errors:

Bypassed security:
User deauthenticated before the call is fully processed:

java.lang.NullPointerException: null
	at org.lognet.springboot.grpc.demo.SecuredGreeterService.sayAuthHello(SecuredGreeterService.java:22) ~[main/:na]
	at io.grpc.examples.SecuredGreeterGrpc$MethodHandlers.invoke(SecuredGreeterGrpc.java:209) ~[main/:na]
	at io.grpc.stub.ServerCalls$UnaryServerCallHandler$UnaryServerCallListener.onHalfClose(ServerCalls.java:172) ~[grpc-stub-1.29.0.jar:1.29.0]
	at java.base/java.util.Optional.ifPresent(Optional.java:183) ~[na:na]
	at org.lognet.springboot.grpc.security.SecurityInterceptor$SecurityServerCallListener.onHalfClose(SecurityInterceptor.java:43) ~[main/:na]
	at io.grpc.internal.ServerCallImpl$ServerStreamListenerImpl.halfClosed(ServerCallImpl.java:331) ~[grpc-core-1.29.0.jar:1.29.0]
	at io.grpc.internal.ServerImpl$JumpToApplicationThreadServerStreamListener$1HalfClosed.runInContext(ServerImpl.java:818) ~[grpc-core-1.29.0.jar:1.29.0]
	at io.grpc.internal.ContextRunnable.run(ContextRunnable.java:37) ~[grpc-core-1.29.0.jar:1.29.0]
	at io.grpc.internal.SerializingExecutor.run(SerializingExecutor.java:123) ~[grpc-core-1.29.0.jar:1.29.0]
	at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128) ~[na:na]
	at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628) ~[na:na]
	at java.base/java.lang.Thread.run(Thread.java:834) ~[na:na]

PS: I had a really hard time running the tests without docker/consul.

EDIT:
Due to the structure of your interceptor you cannot (?) bypass authentication, but can still obtain a different users authentication (or loose your own as shown in the above stacktrace). Especially for streaming calls.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants