diff --git a/.travis/build.sh b/.travis/build.sh index 3fc80b21..b104037a 100755 --- a/.travis/build.sh +++ b/.travis/build.sh @@ -38,13 +38,16 @@ mvn spotbugs:check # Also test examples build on different architectures (exclude ppc64le until fixed) if [ "$arch" != 'ppc64le' ]; then mvn clean install -f examples/docker $MAVEN_EXTRA_ARGS - cd examples/docker - set +e - ./spring/test-spring.sh - EXIT=$? - cd ../.. - exitIfError - set -e + + if [[ "$JAVA_MAJOR_VERSION" -ge "17" ]]; then + cd examples/docker + set +e + ./spring/test-spring.sh + EXIT=$? + cd ../.. + exitIfError + set -e + fi fi # Run testsuite diff --git a/examples/docker/kafka-oauth-strimzi/compose-spring-jwt.yml b/examples/docker/kafka-oauth-strimzi/compose-spring-jwt.yml new file mode 100644 index 00000000..df3c16df --- /dev/null +++ b/examples/docker/kafka-oauth-strimzi/compose-spring-jwt.yml @@ -0,0 +1,88 @@ +version: '3.5' + +services: + + #################################### KAFKA BROKER #################################### + kafka: + image: strimzi/example-kafka + build: kafka-oauth-strimzi/kafka/target + container_name: kafka + command: + - /bin/bash + - /opt/kafka/start_without_wait.sh + ports: + - 9092:9092 + + # javaagent debug port + #- 5005:5005 + + environment: + + # Java Debug + #KAFKA_DEBUG: y + #DEBUG_SUSPEND_FLAG: y + #JAVA_DEBUG_PORT: "*:5005" + + # + # KAFKA Configuration + # + LOG_DIR: /home/kafka/logs + #KAFKA_LOG_DIRS: /home/kafka/1 + + KAFKA_PORT: 9092 + KAFKA_ADVERTISED_HOST_NAME: kafka + + KAFKA_BROKER_ID: 1 + KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181 + KAFKA_LISTENERS: CLIENT://kafka:9092 + KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: CLIENT:SASL_PLAINTEXT + KAFKA_SASL_ENABLED_MECHANISMS: OAUTHBEARER + KAFKA_INTER_BROKER_LISTENER_NAME: CLIENT + KAFKA_SASL_MECHANISM_INTER_BROKER_PROTOCOL: OAUTHBEARER + + KAFKA_LISTENER_NAME_CLIENT_OAUTHBEARER_SASL_JAAS_CONFIG: "org.apache.kafka.common.security.oauthbearer.OAuthBearerLoginModule required;" + KAFKA_LISTENER_NAME_CLIENT_OAUTHBEARER_SASL_LOGIN_CALLBACK_HANDLER_CLASS: io.strimzi.kafka.oauth.client.JaasClientOauthLoginCallbackHandler + KAFKA_LISTENER_NAME_CLIENT_OAUTHBEARER_SASL_SERVER_CALLBACK_HANDLER_CLASS: io.strimzi.kafka.oauth.server.JaasServerOauthValidatorCallbackHandler + + KAFKA_SUPER_USERS: User:kafkabroker + + KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 + + + + # + # Strimzi OAuth Configuration + # + + # Authentication config for inter broker communication AND to authenticate to JWKS endpoint + # The JaasClientOauthLoginCallbackHandler for inter broker communication understands this as the client_id and secret for client_credentials + # While OAUTHBEARER can be used for inter broker communication it adds unnecessary complexity and lag. We really should be using mTLS instead. + # See: compose-spring.yml + + # The JaasServerOauthValidatorCallbackHandler understand this same configuration as client_id and secret to use + # with Basic authentication to JWKS endpoint. In this example JWKS endpoint is not protected so authentication is redundant. + # Nevertheless, the presence of these configuration options results in `Authorization: Basic ...` header sent to Spring Authorization Server + OAUTH_CLIENT_ID: "kafkabroker" + OAUTH_CLIENT_SECRET: "kafkabrokersecret" + OAUTH_TOKEN_ENDPOINT_URI: "http://${SPRING_HOST:-spring}:8080/oauth2/token" + + # Validation config + OAUTH_JWKS_ENDPOINT_URI: "http://${SPRING_HOST:-spring}:8080/oauth2/jwks" + OAUTH_JWKS_IGNORE_KEY_USE: "true" + OAUTH_VALID_ISSUER_URI: "http://${SPRING_HOST:-spring}:8080" + OAUTH_CHECK_ACCESS_TOKEN_TYPE: "false" + + # For start.sh script to know where the keycloak is listening + SPRING_HOST: ${SPRING_HOST:-spring} + depends_on: + - zookeeper + restart: on-failure + + zookeeper: + image: strimzi/example-zookeeper + build: kafka-oauth-strimzi/zookeeper/target + container_name: zookeeper + ports: + - 2181:2181 + environment: + LOG_DIR: /home/kafka/logs \ No newline at end of file diff --git a/examples/docker/kafka-oauth-strimzi/compose-spring.yml b/examples/docker/kafka-oauth-strimzi/compose-spring.yml index aac6b977..23ff293f 100644 --- a/examples/docker/kafka-oauth-strimzi/compose-spring.yml +++ b/examples/docker/kafka-oauth-strimzi/compose-spring.yml @@ -11,6 +11,7 @@ services: - /bin/bash - /opt/kafka/start_without_wait.sh ports: + - 9091:9091 - 9092:9092 # javaagent debug port @@ -29,44 +30,37 @@ services: LOG_DIR: /home/kafka/logs #KAFKA_LOG_DIRS: /home/kafka/1 + KAFKA_PORT: 9092 + KAFKA_ADVERTISED_HOST_NAME: kafka + KAFKA_BROKER_ID: 1 KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181 - KAFKA_LISTENERS: CLIENT://kafka:9092 - KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: CLIENT:SASL_PLAINTEXT - KAFKA_SASL_ENABLED_MECHANISMS: OAUTHBEARER - KAFKA_INTER_BROKER_LISTENER_NAME: CLIENT - KAFKA_SASL_MECHANISM_INTER_BROKER_PROTOCOL: OAUTHBEARER - - KAFKA_LISTENER_NAME_CLIENT_OAUTHBEARER_SASL_JAAS_CONFIG: "org.apache.kafka.common.security.oauthbearer.OAuthBearerLoginModule required;" - KAFKA_LISTENER_NAME_CLIENT_OAUTHBEARER_SASL_LOGIN_CALLBACK_HANDLER_CLASS: io.strimzi.kafka.oauth.client.JaasClientOauthLoginCallbackHandler - KAFKA_LISTENER_NAME_CLIENT_OAUTHBEARER_SASL_SERVER_CALLBACK_HANDLER_CLASS: io.strimzi.kafka.oauth.server.JaasServerOauthValidatorCallbackHandler - KAFKA_SUPER_USERS: User:service-account-kafka-broker + KAFKA_LISTENERS: REPLICATION://kafka:9091,CLIENT://kafka:9092 + KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: REPLICATION:SSL,CLIENT:SASL_PLAINTEXT - KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 + KAFKA_INTER_BROKER_LISTENER_NAME: REPLICATION + KAFKA_SSL_SECURE_RANDOM_IMPLEMENTATION: SHA1PRNG + KAFKA_SSL_ENDPOINT_IDENTIFICATION_ALGORITHM: "" + + KAFKA_LISTENER_NAME_REPLICATION_SSL_KEYSTORE_LOCATION: /tmp/kafka/cluster.keystore.p12 + KAFKA_LISTENER_NAME_REPLICATION_SSL_KEYSTORE_PASSWORD: Z_pkTh9xgZovK4t34cGB2o6afT4zZg0L + KAFKA_LISTENER_NAME_REPLICATION_SSL_KEYSTORE_TYPE: PKCS12 + KAFKA_LISTENER_NAME_REPLICATION_SSL_TRUSTSTORE_LOCATION: /tmp/kafka/cluster.truststore.p12 + KAFKA_LISTENER_NAME_REPLICATION_SSL_TRUSTSTORE_PASSWORD: Z_pkTh9xgZovK4t34cGB2o6afT4zZg0L + KAFKA_LISTENER_NAME_REPLICATION_SSL_TRUSTSTORE_TYPE: PKCS12 + KAFKA_LISTENER_NAME_REPLICATION_SSL_CLIENT_AUTH: required + KAFKA_LISTENER_NAME_CLIENT_SASL_ENABLED_MECHANISMS: OAUTHBEARER + KAFKA_LISTENER_NAME_CLIENT_OAUTHBEARER_SASL_JAAS_CONFIG: "org.apache.kafka.common.security.oauthbearer.OAuthBearerLoginModule required oauth.introspection.endpoint.uri=\"http://${SPRING_HOST:-spring}:8080/oauth2/introspect\" oauth.valid.issuer.uri=\"http://${SPRING_HOST:-spring}:8080\" oauth.client.id=\"kafkabroker\" oauth.client.secret=\"kafkabrokersecret\" oauth.access.token.is.jwt=\"false\" oauth.check.access.token.type=\"false\" unsecuredLoginStringClaim_sub=\"admin\" ;" + KAFKA_LISTENER_NAME_CLIENT_OAUTHBEARER_SASL_SERVER_CALLBACK_HANDLER_CLASS: io.strimzi.kafka.oauth.server.JaasServerOauthValidatorCallbackHandler + KAFKA_SUPER_USERS: User:CN=my-cluster-kafka,O=io.strimzi;User:CN=my-cluster-entity-operator,O=io.strimzi;User:CN=my-cluster-kafka-exporter,O=io.strimzi;User:kafkabroker - # - # Strimzi OAuth Configuration - # + KAFKA_CONNECTIONS_MAX_REAUTH_MS: 3600000 - # Authentication config - OAUTH_CLIENT_ID: "kafka" - OAUTH_CLIENT_SECRET: "kafkasecret" - OAUTH_TOKEN_ENDPOINT_URI: "http://${SPRING_HOST:-spring}:8080/oauth/token" - OAUTH_SCOPE: "any" - - # Validation config - OAUTH_INTROSPECTION_ENDPOINT_URI: "http://${SPRING_HOST:-spring}:8080/oauth/check_token" - OAUTH_CHECK_ISSUER: "false" - OAUTH_ACCESS_TOKEN_IS_JWT: "false" - OAUTH_CHECK_ACCESS_TOKEN_TYPE: "false" - - # username extraction from JWT token claim - OAUTH_USERNAME_CLAIM: user_name - OAUTH_FALLBACK_USERNAME_CLAIM: client_id - OAUTH_FALLBACK_USERNAME_PREFIX: client-account- + KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 + KAFKA_PRINCIPAL_BUILDER_CLASS: io.strimzi.kafka.oauth.server.OAuthKafkaPrincipalBuilder # For start.sh script to know where the keycloak is listening SPRING_HOST: ${SPRING_HOST:-spring} diff --git a/examples/docker/kafka-oauth-strimzi/kafka/jwt.sh b/examples/docker/kafka-oauth-strimzi/kafka/jwt.sh index 4e3a62d6..0bb55f83 100755 --- a/examples/docker/kafka-oauth-strimzi/kafka/jwt.sh +++ b/examples/docker/kafka-oauth-strimzi/kafka/jwt.sh @@ -8,7 +8,7 @@ fi IFS='.' read -r -a PARTS <<< "$1" echo "Head: " -echo $(echo -n "${PARTS[0]}" | base64 -d 2>/dev/null) +echo $(echo -n "${PARTS[0]}===" | base64 -d 2>/dev/null) echo echo "Payload: " -echo $(echo -n "${PARTS[1]}" | base64 -d 2>/dev/null) \ No newline at end of file +echo $(echo -n "${PARTS[1]}===" | base64 -d 2>/dev/null) \ No newline at end of file diff --git a/examples/docker/pom.xml b/examples/docker/pom.xml index eddebdca..b902a1a2 100644 --- a/examples/docker/pom.xml +++ b/examples/docker/pom.xml @@ -14,7 +14,7 @@ 3.1.1 3.1.0 - 9.31 + 9.37.2 1.0.0-SNAPSHOT @@ -90,6 +90,19 @@ kafka-oauth-strimzi - spring + + + + java-17 + + [17, + + + + kafka-oauth-strimzi + spring + + + diff --git a/examples/docker/spring/README.md b/examples/docker/spring/README.md index fae5b19c..e4646ff7 100644 --- a/examples/docker/spring/README.md +++ b/examples/docker/spring/README.md @@ -2,6 +2,8 @@ Spring Authorization Server =========================== This project builds and runs Spring Authorization Server as a docker container. +The accompanying Kafka broker example is configured to use this authorization server for Kafka authentication (not for Kafka authorization). +Client definitions are defined in code in [SecurityConfig.java](file://./src/main/java/io/strimzi/examples/spring/SecurityConfig.java) file. Building @@ -27,54 +29,118 @@ Using Make sure to add `spring` entry to your `/etc/hosts` as explained [here](../README.md#preparing). -Configure your Kafka broker with the following settings: +Spring Authorization Server exposes multiple OAuth2 / OIDC endpoints. For our purposes the important ones are: - oauth.introspection.endpoint.uri=http://spring:8080/oauth/check_token - oauth.token.endpoint.uri=http://spring:8080/oauth/token - oauth.client.id=kafka - oauth.client.secret=kafkasecret - oauth.scope=any +* The token endpoint: http://spring:8080/oauth2/token +* The signing keys endpoint: http://spring:8080/oauth2/jwks +* The introspection endpoint: http://spring:8080/oauth2/introspect + +This example demonstrates using `client_credentials` grant with two different clients. First client `kafkaclient` produces +JWT access tokens which can be validated by using the JWKS endpoint. The second client `kafkaclient2` produces opaque access tokens +which can only be validated by using the Introspection endpoint. + +For example, in order to use the JWKS endpoint you could configure your Kafka broker listener with the following JAAS config parameters: + + oauth.jwks.endpoint.uri=http://spring:8080/oauth2/jwks + oauth.jwks.ignore.key.use=true + oauth.valid.issuer.uri=http://spring:8080 + oauth.check.access.token.type=false + +In order to use the Introspection endpoint you could configure your Kafka broker listener with the following JAAS config parameters: + + oauth.introspect.endpoint.uri=http://spring:8080/oauth2/introspect + oauth.valid.issuer.uri=http://spring:8080 + oauth.client.id=kafkabroker + oauth.client.secret=kafkabrokersecret oauth.access.token.is.jwt=false - oauth.check.issuer=false - oauth.username.claim=user_name - oauth.fallback.username.claim=client_id - oauth.fallback.username.prefix=client-account- + oauth.check.access.token.type=false -Spring authorization server by default uses opaque tokens (non-JWT) which means validation has to use the introspection endpoint. -The example single-broker cluster uses OAuth2 for inter-broker communication as well as Kafka client communication which requires token endpoint to be configured. -Authorization server's Token endpoint requires `scope` to be specified. -The Introspection endpoint returns no information about issuer, so we have to disable that check. -User information is sent depending on the type of authentication. -If Kafka client sends a token obtained by a user using `password` grant, then `user_name` attribute -of introspection endpoint response contains user's username. If Kafka client sends a token obtained in as the client by using `client_credentials` grant, then no `user_name` is set, but `client_id` is. -By using username prefix we can quickly differentiate client accounts from user accounts - client_id with the same name as another username will become a different principal. -We can then use Kafka Simple ACL Authorization to define ACL policies based on authenticated principal. +### Token validation using the Introspection endpoint (JWT and opaque tokens) +You can run the prepared Kafka example from parent directory (`examples/docker`) - make sure the Spring Authorization Server is up before running this command: -You can run the prepared example from parent directory (`examples/docker`): + docker-compose -f compose.yml -f kafka-oauth-strimzi/compose-spring.yml up --build - docker-compose -f compose.yml -f kafka-oauth-strimzi/compose-spring.yml up - -Check the logging output of the Spring container for the default user's password: +You can authenticate as a Kafka client using a service account `kafkaclient` to obtain the access token: - docker logs spring | grep "Using generated security password:" + curl http://spring:8080/oauth2/token -d "grant_type=client_credentials&scope=profile" -u kafkaclient:kafkaclientsecret -Set the password as env var `PASSWORD`: +Or if you want to automate: - export PASSWORD=$(docker logs spring | grep "Using generated security password:" | awk '{print $NF}') + RESPONSE=$(curl -s http://spring:8080/oauth2/token -d "grant_type=client_credentials&scope=profile" -u kafkaclient:kafkaclientsecret) + ACCESS_TOKEN=$(echo "$RESPONSE" | sed -E -e "s#.*\"access_token\":\"([^\"]+)\",\".*#\1#") +You can take the role of the Kafka broker to check if the token is valid: -Configure your Kafka client by obtaining the token as user `user`: + curl http://spring:8080/oauth2/introspect -d "token=$ACCESS_TOKEN" -u kafkabroker:kafkabrokersecret - curl spring:8080/oauth/token -d "grant_type=password&scope=read&username=user&password=$PASSWORD" -u kafka:kafkasecret +In this case the returned access token was a JWT token which you can confirm by using the following helper script (from `examples/docker` directory): + ./kafka-oauth-strimzi/kafka/jwt.sh $ACCESS_TOKEN -Use the following configuration options to configure your client with the refresh token: - oauth.token.endpoint.uri=http://spring:8080/oauth/token - oauth.refresh.token=$REFRESH_TOKEN - oauth.scope=any - oauth.access.token.is.jwt=false +If you authenticate as client `kafkaclient2` the returned access token will be an opaque token (not JWT): + + curl http://spring:8080/oauth2/token -d "grant_type=client_credentials&scope=profile" -u kafkaclient2:kafkaclient2secret + +Or if you want to automate: + + RESPONSE2=$(curl -s http://spring:8080/oauth2/token -d "grant_type=client_credentials&scope=profile" -u kafkaclient2:kafkaclient2secret) + ACCESS_TOKEN2=$(echo "$RESPONSE2" | sed -E -e "s#.*\"access_token\":\"([^\"]+)\",\".*#\1#") + +You can again take the role of the Kafka broker to check if the token is valid: + + curl http://spring:8080/oauth2/introspect -d "token=$ACCESS_TOKEN2" -u kafkabroker:kafkabrokersecret + +You can not introspect the opaque token as it's just a random string without any internal structure that could be parsed. +Using the introspection endpoint is the only way to validate such a token. + + +### Connecting with a Kafka client + +You can try both JWT access token and the opaque access token with a Kafka client that connects to the example Kafka broker. + +Start a new kafka container for the client: + + docker run -ti --name kafka-client --network docker_default strimzi/example-kafka /bin/sh + +``` +$ echo 'security.protocol=SASL_PLAINTEXT +sasl.mechanism=OAUTHBEARER +sasl.jaas.config=org.apache.kafka.common.security.oauthbearer.OAuthBearerLoginModule required \ +oauth.client.id="kafkaclient" \ +oauth.client.secret="kafkaclientsecret" \ +oauth.token.endpoint.uri="http://spring:8080/oauth2/token" ; +sasl.login.callback.handler.class=io.strimzi.kafka.oauth.client.JaasClientOauthLoginCallbackHandler' > $HOME/client.properties +``` + +Check which topics exist: + + bin/kafka-topics.sh --bootstrap-server kafka:9092 --command-config ~/client.properties --list + + +Run a console kafka producer: + + bin/kafka-console-producer.sh --broker-list kafka:9092 --topic a_messages --producer.config=$HOME/client.properties + # type some messages then press CTRL-C or CTRL-D + + bin/kafka-console-consumer.sh --bootstrap-server kafka:9092 --topic a_messages --from-beginning --consumer.config $HOME/client.properties --group a_consumer_group_1 + # Press CTRL-C to exit + +You can also use `kafkaclient2` to see how it works with an opaque token. + + +### Token validation using JWT key signature checking (JWT tokens only, opaque tokens not supported) + +There is a Kafka broker configuration example where the listener is configured to use the JWKS endpoint. Such a setup can only be used with JWT tokens. + +You can run the prepared Kafka example from parent directory (`examples/docker`) - make sure the Spring Authorization Server is up before running this command: + + docker-compose -f compose.yml -f kafka-oauth-strimzi/compose-spring-jwt.yml up --build + +You can run the Kafka client using the same commands as in the previous chapter. +If you try to authenticate the client as `kafkaclient2` the authentication will fail. While the Kafka client will successfully obtain the token, +the validation of the token will fail on Kafka broker due to not being a JWT token. \ No newline at end of file diff --git a/examples/docker/spring/pom.xml b/examples/docker/spring/pom.xml index 3b7e2bcd..deeb0f66 100644 --- a/examples/docker/spring/pom.xml +++ b/examples/docker/spring/pom.xml @@ -1,159 +1,80 @@ - + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.2.3 + + - 4.0.0 + io.strimzi.oauth.docker + kafka-oauth-docker-spring + 1.0.0-SNAPSHOT - - org.springframework.boot - spring-boot-starter-parent - 2.6.15 - - + kafka-oauth-docker-spring + OAuth2 Example using Spring Authorization Server - io.strimzi.oauth.docker - kafka-oauth-docker-spring - 1.0.0-SNAPSHOT + + 17 + 3.1.0 + 0.40.2 + + + + org.springframework.boot + spring-boot-starter-oauth2-authorization-server + - - UTF-8 - 3.1.1 - 3.1.0 - 0.40.2 + + org.springframework.boot + spring-boot-starter-test + test + + - 2.6.8 - + + + + + org.apache.maven.plugins + maven-resources-plugin + ${plugins.resources.version} + + + io.fabric8 + docker-maven-plugin + ${fabric8-docker-plugin.version} + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + io.fabric8 + docker-maven-plugin + + + build + pre-integration-test + + build + + + + + + + strimzi/example-spring:latest + + + + + + - - - Apache License, Version 2.0 - https://www.apache.org/licenses/LICENSE-2.0.txt - - - - - - - org.springframework.security.oauth.boot - spring-security-oauth2-autoconfigure - ${spring.boot.oauth.version} - - - com.fasterxml.jackson.core - jackson-databind - 2.15.2 - - - com.fasterxml.jackson.core - jackson-core - 2.15.2 - - - org.bouncycastle - bcprov-jdk15on - 1.69 - - - org.yaml - snakeyaml - 1.33 - - - org.springframework - spring-beans - 5.3.20 - - - org.springframework - spring-context - 5.3.20 - - - org.springframework.security - spring-security-web - 5.5.7 - - - org.springframework.security - spring-security-crypto - 5.5.7 - - - - - - - org.springframework.security.oauth.boot - spring-security-oauth2-autoconfigure - - - org.springframework.boot - spring-boot-starter-web - - - - - - - - org.apache.maven.plugins - maven-resources-plugin - ${plugins.resources.version} - - - io.fabric8 - docker-maven-plugin - ${fabric8-docker-plugin.version} - - - - - - org.springframework.boot - spring-boot-maven-plugin - - - - repackage - - - - - - org.apache.maven.plugins - maven-dependency-plugin - ${plugins.dependency.version} - - - analyze-deps - verify - - analyze-only - - - - - - io.fabric8 - docker-maven-plugin - - - build - pre-integration-test - - build - - - - - - - strimzi/example-spring:latest - - - - - - - diff --git a/examples/docker/spring/src/main/java/io/strimzi/examples/spring/SecurityConfig.java b/examples/docker/spring/src/main/java/io/strimzi/examples/spring/SecurityConfig.java new file mode 100644 index 00000000..6927b977 --- /dev/null +++ b/examples/docker/spring/src/main/java/io/strimzi/examples/spring/SecurityConfig.java @@ -0,0 +1,143 @@ +package io.strimzi.examples.spring; + +import com.nimbusds.jose.jwk.JWKSet; +import com.nimbusds.jose.jwk.RSAKey; +import com.nimbusds.jose.jwk.source.ImmutableJWKSet; +import com.nimbusds.jose.jwk.source.JWKSource; +import com.nimbusds.jose.proc.SecurityContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.annotation.Order; +import org.springframework.security.config.Customizer; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.ClientAuthenticationMethod; +import org.springframework.security.oauth2.core.oidc.OidcScopes; +import org.springframework.security.oauth2.jwt.JwtDecoder; +import org.springframework.security.oauth2.server.authorization.client.InMemoryRegisteredClientRepository; +import org.springframework.security.oauth2.server.authorization.client.RegisteredClient; +import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository; +import org.springframework.security.oauth2.server.authorization.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration; +import org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2AuthorizationServerConfigurer; +import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings; +import org.springframework.security.oauth2.server.authorization.settings.OAuth2TokenFormat; +import org.springframework.security.oauth2.server.authorization.settings.TokenSettings; +import org.springframework.security.provisioning.InMemoryUserDetailsManager; +import org.springframework.security.web.SecurityFilterChain; + +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.interfaces.RSAPrivateKey; +import java.security.interfaces.RSAPublicKey; +import java.util.LinkedList; +import java.util.UUID; + +@Configuration(proxyBeanMethods = false) +@EnableWebSecurity +public class SecurityConfig { + + @Bean + @Order(1) + public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception { + + OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http); + + // Enable OpenID Connect 1.0 + // Provides User Info endpoint + http.getConfigurer(OAuth2AuthorizationServerConfigurer.class) + .oidc(Customizer.withDefaults()); + + // Accept access tokens for User Info and/or Client Registration + http.oauth2ResourceServer((resourceServer) -> resourceServer + .jwt(Customizer.withDefaults())); + + return http.build(); + } + + @Bean + public UserDetailsService userDetailsService() { + UserDetails userDetails = User.withDefaultPasswordEncoder() + .username("user") + .password("password") + .roles("USER") + .build(); + + return new InMemoryUserDetailsManager(userDetails); + } + + @Bean + public RegisteredClientRepository registeredClientRepository() { + + LinkedList clients = new LinkedList<>(); + clients.add(RegisteredClient.withId(UUID.randomUUID().toString()) + .clientId("kafkaclient") + .clientSecret("{noop}kafkaclientsecret") + .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC) + .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS) + .scope(OidcScopes.OPENID) + .scope(OidcScopes.PROFILE) + .build()); + + clients.add(RegisteredClient.withId(UUID.randomUUID().toString()) + .clientId("kafkaclient2") + .clientSecret("{noop}kafkaclient2secret") + .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC) + .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS) + .scope(OidcScopes.OPENID) + .scope(OidcScopes.PROFILE) + // The tokens for 'kafkaclient2' should be opaque, requiring the use of introspect endpoint + .tokenSettings(TokenSettings.builder().accessTokenFormat(OAuth2TokenFormat.REFERENCE).build()) + .build()); + + clients.add(RegisteredClient.withId(UUID.randomUUID().toString()) + .clientId("kafkabroker") + .clientSecret("{noop}kafkabrokersecret") + .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC) + .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS) + .build()); + + return new InMemoryRegisteredClientRepository(clients); + } + + @Bean + // Required for the JWKS endpoint + public JWKSource jwkSource() { + KeyPair keyPair = generateRsaKey(); + RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic(); + RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate(); + RSAKey rsaKey = new RSAKey.Builder(publicKey) + .privateKey(privateKey) + .keyID(UUID.randomUUID().toString()) + .build(); + JWKSet jwkSet = new JWKSet(rsaKey); + return new ImmutableJWKSet<>(jwkSet); + } + + private static KeyPair generateRsaKey() { + KeyPair keyPair; + try { + KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA"); + keyPairGenerator.initialize(2048); + keyPair = keyPairGenerator.generateKeyPair(); + } + catch (Exception ex) { + throw new IllegalStateException(ex); + } + return keyPair; + } + + @Bean + // Required for the User Info endpoint + public JwtDecoder jwtDecoder(JWKSource jwkSource) { + return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource); + } + + @Bean + public AuthorizationServerSettings authorizationServerSettings() { + return AuthorizationServerSettings.builder().build(); + } +} diff --git a/examples/docker/spring/src/main/java/io/strimzi/examples/spring/SimpleAuthorizationServerApplication.java b/examples/docker/spring/src/main/java/io/strimzi/examples/spring/SimpleAuthorizationServerApplication.java deleted file mode 100644 index fc6e3ebe..00000000 --- a/examples/docker/spring/src/main/java/io/strimzi/examples/spring/SimpleAuthorizationServerApplication.java +++ /dev/null @@ -1,13 +0,0 @@ -package io.strimzi.examples.spring; - -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer; - -@EnableAuthorizationServer -@SpringBootApplication -public class SimpleAuthorizationServerApplication { - public static void main(String[] args) { - SpringApplication.run(SimpleAuthorizationServerApplication.class, args); - } -} diff --git a/examples/docker/spring/src/main/java/io/strimzi/examples/spring/SpringAuthorizationServerApplication.java b/examples/docker/spring/src/main/java/io/strimzi/examples/spring/SpringAuthorizationServerApplication.java new file mode 100644 index 00000000..ca61a544 --- /dev/null +++ b/examples/docker/spring/src/main/java/io/strimzi/examples/spring/SpringAuthorizationServerApplication.java @@ -0,0 +1,15 @@ +package io.strimzi.examples.spring; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Import; +import org.springframework.security.oauth2.server.authorization.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration; + +@SpringBootApplication +public class SpringAuthorizationServerApplication { + + public static void main(String[] args) { + SpringApplication.run(SpringAuthorizationServerApplication.class, args); + } + +} \ No newline at end of file diff --git a/examples/docker/spring/src/main/resources/application.yml b/examples/docker/spring/src/main/resources/application.yml index f9a3a1ee..c889b87b 100644 --- a/examples/docker/spring/src/main/resources/application.yml +++ b/examples/docker/spring/src/main/resources/application.yml @@ -1,7 +1,11 @@ -security: - oauth2: - authorization: - check-token-access: isAuthenticated() - client: - client-id: kafka - client-secret: kafkasecret +server: + port: 8080 + +logging: + level: + root: INFO + org.springframework.web: INFO + org.springframework.security: DEBUG + org.springframework.security.authentication: DEBUG + org.springframework.security.oauth2: DEBUG +# org.springframework.boot.autoconfigure: DEBUG diff --git a/examples/docker/spring/src/test/java/io/strimzi/examples/spring/SpringAuthorizationServerApplicationTests.java b/examples/docker/spring/src/test/java/io/strimzi/examples/spring/SpringAuthorizationServerApplicationTests.java new file mode 100644 index 00000000..859ce405 --- /dev/null +++ b/examples/docker/spring/src/test/java/io/strimzi/examples/spring/SpringAuthorizationServerApplicationTests.java @@ -0,0 +1,13 @@ +package io.strimzi.examples.spring; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class SpringAuthorizationServerApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git a/examples/docker/spring/test-spring.sh b/examples/docker/spring/test-spring.sh index 530bcb1e..597d94ed 100755 --- a/examples/docker/spring/test-spring.sh +++ b/examples/docker/spring/test-spring.sh @@ -6,7 +6,7 @@ docker run -d --name spring strimzi/example-spring for i in {1..10} do sleep 1 - RESULT=$(docker logs spring | grep "Started SimpleAuthorizationServerApplication") + RESULT=$(docker logs spring | grep "Started SpringAuthorizationServerApplication") if [ "$RESULT" != "" ]; then docker rm -f spring exit 0 diff --git a/pom.xml b/pom.xml index 9a99aa7b..8c98a7bd 100644 --- a/pom.xml +++ b/pom.xml @@ -103,14 +103,14 @@ 3.3.0 4.7.3 1.6.3 - 3.6.1 + 3.7.0 2.15.3 2.15.3 2.9.0 4.13.2 1.7.36 3.12.4 - 9.31 + 9.37.2 diff --git a/testsuite/keycloak-authz-tests/src/main/java/io/strimzi/testsuite/oauth/authz/Common.java b/testsuite/keycloak-authz-tests/src/main/java/io/strimzi/testsuite/oauth/authz/Common.java index 32426d75..40739bc1 100644 --- a/testsuite/keycloak-authz-tests/src/main/java/io/strimzi/testsuite/oauth/authz/Common.java +++ b/testsuite/keycloak-authz-tests/src/main/java/io/strimzi/testsuite/oauth/authz/Common.java @@ -46,7 +46,7 @@ import static io.strimzi.kafka.oauth.common.OAuthAuthenticator.loginWithClientSecret; import static io.strimzi.kafka.oauth.common.OAuthAuthenticator.urlencode; -@SuppressFBWarnings({"THROWS_METHOD_THROWS_CLAUSE_BASIC_EXCEPTION", "THROWS_METHOD_THROWS_RUNTIMEEXCEPTION"}) +@SuppressFBWarnings({"THROWS_METHOD_THROWS_CLAUSE_BASIC_EXCEPTION", "THROWS_METHOD_THROWS_RUNTIMEEXCEPTION", "RV_RETURN_VALUE_IGNORED_NO_SIDE_EFFECT"}) public class Common { private static final Logger log = LoggerFactory.getLogger(Common.class); diff --git a/testsuite/mock-oauth-server/pom.xml b/testsuite/mock-oauth-server/pom.xml index 2d6095c9..f5220fa7 100644 --- a/testsuite/mock-oauth-server/pom.xml +++ b/testsuite/mock-oauth-server/pom.xml @@ -21,7 +21,7 @@ ../.. - 4.3.8 + 4.4.8 1.3.14 3.8.1 diff --git a/testsuite/pom.xml b/testsuite/pom.xml index 21ca0bc6..94d587e0 100644 --- a/testsuite/pom.xml +++ b/testsuite/pom.xml @@ -41,7 +41,7 @@ 2.22.1 0.40.2 - 1.17.3 + 1.19.6 4.13.2 4.7.0 2.15.2