├── .gitignore ├── README.md ├── build.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── src ├── main ├── java │ └── org │ │ └── c4isr │ │ └── delta │ │ └── cloudgateway │ │ ├── CloudGatewayApplication.java │ │ ├── OAuth2LoginController.java │ │ ├── filter │ │ ├── ExtNettyRoutingFilter.java │ │ └── PreOauth2SSOGatewayFilter.java │ │ └── jwt │ │ ├── JwtConfiguration.java │ │ ├── JwtOAuth2AuthenticationTokenConverter.java │ │ ├── JwtOAuth2User.java │ │ ├── JwtPublicKey.java │ │ ├── JwtReactiveOAuth2UserService.java │ │ └── JwtToOAuth2UserConverter.java └── resources │ ├── application.yml │ └── templates │ └── index.html └── test └── java └── org └── c4isr └── delta └── cloudgateway └── CloudGatewayApplicationTests.java /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | /build/ 3 | !gradle/wrapper/gradle-wrapper.jar 4 | 5 | ### STS ### 6 | .apt_generated 7 | .classpath 8 | .factorypath 9 | .project 10 | .settings 11 | .springBeans 12 | .sts4-cache 13 | 14 | ### IntelliJ IDEA ### 15 | .idea 16 | *.iws 17 | *.iml 18 | *.ipr 19 | /out/ 20 | 21 | ### NetBeans ### 22 | /nbproject/private/ 23 | /nbbuild/ 24 | /dist/ 25 | /nbdist/ 26 | /.nb-gradle/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Spring Cloud Gateway OAuth2 SSO Sample Application 2 | 3 | It`s a sample application which how to achieve SSO for your microservices with usage of new [Spring Cloud Gateway](https://spring.io/projects/spring-cloud-gateway) as your API gateway and also with [OAuth2](https://oauth.net/2/) authorization protocol and [JWT](https://jwt.io/) tokens. 4 | 5 | ##### Sample dependencies 6 | 7 | * Spring Boot 2.1 8 | * Spring Cloud Gateway 2.1.M1 9 | * Spring Security 5.1.1 10 | 11 | The main idea of this sample is to show how you can achieve functionality described in references below (where Zuul is used as your API Gateway) 12 | but with usage of new Spring Cloud Gateway provided with Spring Boot 2 and also with [reactive approach](https://projectreactor.io/) (a new feature of [Spring 5](https://spring.io/blog/2016/09/22/new-in-spring-5-functional-web-framework)). 13 | 14 | ##### References: 15 | * [spring OAuth2 SSO tutorial](https://spring.io/blog/2015/02/03/sso-with-oauth2-angular-js-and-spring-security-part-v) 16 | * [sso-spring-security-oauth2](https://www.baeldung.com/) 17 | * [jwt tokens example](https://github.com/monkey-codes/spring-boot-authentication) 18 | * [microservices-security-with-oauth2](https://piotrminkowski.wordpress.com/2017/02/22/microservices-security-with-oauth2/) 19 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext { 3 | springBootVersion = '2.1.0.RELEASE' 4 | } 5 | repositories { 6 | mavenCentral() 7 | maven { url "https://repo.spring.io/snapshot" } 8 | maven { url "https://repo.spring.io/milestone" } 9 | } 10 | dependencies { 11 | classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}") 12 | } 13 | } 14 | 15 | apply plugin: 'java' 16 | apply plugin: 'eclipse' 17 | apply plugin: 'org.springframework.boot' 18 | apply plugin: 'io.spring.dependency-management' 19 | 20 | group = 'org.c4isr.delta' 21 | version = '0.0.1-SNAPSHOT' 22 | sourceCompatibility = 1.8 23 | 24 | repositories { 25 | mavenCentral() 26 | maven { url "https://repo.spring.io/snapshot" } 27 | maven { url "https://repo.spring.io/milestone" } 28 | } 29 | 30 | 31 | ext { 32 | springCloudVersion = 'Greenwich.M1' 33 | } 34 | 35 | dependencies { 36 | implementation('org.springframework.boot:spring-boot-starter-actuator') 37 | implementation('org.springframework.boot:spring-boot-starter-aop') 38 | implementation('org.springframework.boot:spring-boot-starter-security') 39 | implementation('org.springframework.boot:spring-boot-starter-thymeleaf') 40 | // implementation('org.springframework.cloud:spring-cloud-starter-config') 41 | implementation('org.springframework.cloud:spring-cloud-starter-gateway') 42 | // implementation('org.springframework.cloud:spring-cloud-starter-netflix-eureka-client') 43 | implementation('org.springframework.cloud:spring-cloud-starter-oauth2') { 44 | exclude group: 'org.springframework.boot', module: 'spring-boot-starter-tomcat' 45 | } 46 | implementation('org.springframework.cloud:spring-cloud-starter-security') { 47 | exclude group: 'org.springframework.boot', module: 'spring-boot-starter-tomcat' 48 | } 49 | // https://mvnrepository.com/artifact/org.springframework.security/spring-security-oauth2-jose 50 | compile group: 'org.springframework.security', name: 'spring-security-oauth2-jose', version: '5.1.1.RELEASE' 51 | // https://mvnrepository.com/artifact/org.springframework.security/spring-security-oauth2-client 52 | compile group: 'org.springframework.security', name: 'spring-security-oauth2-client', version: '5.1.1.RELEASE' 53 | compile 'org.springframework.security.oauth.boot:spring-security-oauth2-autoconfigure:2.0.6.RELEASE' 54 | // https://mvnrepository.com/artifact/org.springframework.security/spring-security-config 55 | compile group: 'org.springframework.security', name: 'spring-security-config', version: '5.1.1.RELEASE' 56 | // https://mvnrepository.com/artifact/org.springframework.security/spring-security-oauth2-resource-server 57 | compile group: 'org.springframework.security', name: 'spring-security-oauth2-resource-server', version: '5.1.1.RELEASE' 58 | implementation('org.springframework.session:spring-session-core') 59 | runtimeOnly('org.springframework.boot:spring-boot-devtools') 60 | testImplementation('org.springframework.boot:spring-boot-starter-test') 61 | testImplementation('org.springframework.security:spring-security-test') 62 | } 63 | 64 | dependencyManagement { 65 | imports { 66 | mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}" 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/artemMartynenko/spring-cloud-gateway-oauth2-sso-sample-application/4bcb6e3d65a57958ebf2371cead991c09b59c79d/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Tue Feb 06 12:27:20 CET 2018 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.8.1-bin.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn ( ) { 37 | echo "$*" 38 | } 39 | 40 | die ( ) { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save ( ) { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'cloud-gateway' 2 | -------------------------------------------------------------------------------- /src/main/java/org/c4isr/delta/cloudgateway/CloudGatewayApplication.java: -------------------------------------------------------------------------------- 1 | package org.c4isr.delta.cloudgateway; 2 | 3 | import org.bouncycastle.util.io.pem.PemObject; 4 | import org.bouncycastle.util.io.pem.PemReader; 5 | import org.c4isr.delta.cloudgateway.jwt.JwtOAuth2AuthenticationTokenConverter; 6 | import org.c4isr.delta.cloudgateway.jwt.JwtPublicKey; 7 | import org.c4isr.delta.cloudgateway.jwt.JwtReactiveOAuth2UserService; 8 | import org.slf4j.Logger; 9 | import org.slf4j.LoggerFactory; 10 | import org.springframework.beans.factory.annotation.Value; 11 | import org.springframework.boot.SpringApplication; 12 | import org.springframework.boot.autoconfigure.SpringBootApplication; 13 | import org.springframework.context.annotation.Bean; 14 | import org.springframework.security.config.web.server.ServerHttpSecurity; 15 | import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; 16 | import org.springframework.security.oauth2.client.userinfo.ReactiveOAuth2UserService; 17 | import org.springframework.security.oauth2.core.user.OAuth2User; 18 | import org.springframework.security.oauth2.jwt.NimbusReactiveJwtDecoder; 19 | import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder; 20 | import org.springframework.security.web.server.SecurityWebFilterChain; 21 | import org.springframework.web.reactive.function.client.WebClient; 22 | import sun.security.rsa.RSAPublicKeyImpl; 23 | 24 | import java.io.IOException; 25 | import java.io.StringReader; 26 | import java.net.URI; 27 | import java.security.InvalidKeyException; 28 | import java.security.interfaces.RSAPublicKey; 29 | 30 | @SpringBootApplication 31 | public class CloudGatewayApplication { 32 | 33 | 34 | @Bean 35 | SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { 36 | http 37 | .authorizeExchange() 38 | .anyExchange() 39 | .authenticated() 40 | .and() 41 | .oauth2Login() 42 | .and() 43 | .oauth2ResourceServer() 44 | .jwt().jwtAuthenticationConverter(new JwtOAuth2AuthenticationTokenConverter()); 45 | return http.build(); 46 | } 47 | 48 | 49 | 50 | 51 | public static void main(String[] args) { 52 | SpringApplication.run(CloudGatewayApplication.class, args); 53 | 54 | } 55 | 56 | 57 | } 58 | -------------------------------------------------------------------------------- /src/main/java/org/c4isr/delta/cloudgateway/OAuth2LoginController.java: -------------------------------------------------------------------------------- 1 | package org.c4isr.delta.cloudgateway; 2 | 3 | import org.springframework.security.core.annotation.AuthenticationPrincipal; 4 | import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; 5 | import org.springframework.security.oauth2.client.annotation.RegisteredOAuth2AuthorizedClient; 6 | import org.springframework.security.oauth2.core.user.OAuth2User; 7 | import org.springframework.stereotype.Controller; 8 | import org.springframework.ui.Model; 9 | import org.springframework.web.bind.annotation.GetMapping; 10 | 11 | @Controller 12 | public class OAuth2LoginController { 13 | 14 | @GetMapping("/") 15 | public String index(Model model, 16 | @RegisteredOAuth2AuthorizedClient OAuth2AuthorizedClient authorizedClient, 17 | @AuthenticationPrincipal OAuth2User oauth2User) { 18 | model.addAttribute("userName", oauth2User.getName()); 19 | model.addAttribute("clientName", authorizedClient.getClientRegistration().getClientName()); 20 | model.addAttribute("userAttributes", oauth2User.getAttributes()); 21 | return "index"; 22 | } 23 | 24 | } -------------------------------------------------------------------------------- /src/main/java/org/c4isr/delta/cloudgateway/filter/ExtNettyRoutingFilter.java: -------------------------------------------------------------------------------- 1 | package org.c4isr.delta.cloudgateway.filter; 2 | 3 | import io.netty.handler.codec.http.DefaultHttpHeaders; 4 | import io.netty.handler.codec.http.HttpMethod; 5 | import org.slf4j.*; 6 | import org.springframework.beans.factory.ObjectProvider; 7 | import org.springframework.cloud.gateway.config.HttpClientProperties; 8 | import org.springframework.cloud.gateway.filter.GatewayFilterChain; 9 | import org.springframework.cloud.gateway.filter.NettyRoutingFilter; 10 | import org.springframework.cloud.gateway.filter.headers.HttpHeadersFilter; 11 | import org.springframework.cloud.gateway.support.TimeoutException; 12 | import org.springframework.context.annotation.Primary; 13 | import org.springframework.core.Ordered; 14 | import org.springframework.core.io.buffer.NettyDataBuffer; 15 | import org.springframework.http.HttpHeaders; 16 | import org.springframework.http.HttpStatus; 17 | import org.springframework.http.server.reactive.AbstractServerHttpResponse; 18 | import org.springframework.http.server.reactive.ServerHttpRequest; 19 | import org.springframework.http.server.reactive.ServerHttpResponse; 20 | import org.springframework.stereotype.Component; 21 | import org.springframework.web.server.ServerWebExchange; 22 | import reactor.core.publisher.Flux; 23 | import reactor.core.publisher.Mono; 24 | import reactor.netty.NettyPipeline; 25 | import reactor.netty.http.client.HttpClient; 26 | import reactor.netty.http.client.HttpClientResponse; 27 | 28 | import java.net.URI; 29 | import java.util.List; 30 | 31 | import static org.springframework.cloud.gateway.filter.headers.HttpHeadersFilter.filterRequest; 32 | import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.*; 33 | 34 | @Primary 35 | @Component 36 | public class ExtNettyRoutingFilter extends NettyRoutingFilter { 37 | 38 | private final HttpClient httpClient; 39 | private final ObjectProvider> headersFilters; 40 | private final HttpClientProperties properties; 41 | 42 | private static final Logger LOGGER = LoggerFactory.getLogger(ExtNettyRoutingFilter.class); 43 | 44 | public ExtNettyRoutingFilter(HttpClient httpClient, ObjectProvider> headersFilters, HttpClientProperties properties) { 45 | super(httpClient, headersFilters, properties); 46 | this.httpClient = httpClient; 47 | this.headersFilters = headersFilters; 48 | this.properties = properties; 49 | } 50 | 51 | 52 | 53 | @Override 54 | public int getOrder() { 55 | return Ordered.LOWEST_PRECEDENCE; 56 | } 57 | 58 | @Override 59 | public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) { 60 | 61 | URI requestUrl = exchange.getRequiredAttribute(GATEWAY_REQUEST_URL_ATTR); 62 | 63 | String scheme = requestUrl.getScheme(); 64 | if (isAlreadyRouted(exchange) || (!"http".equals(scheme) && !"https".equals(scheme))) { 65 | return chain.filter(exchange); 66 | } 67 | 68 | setAlreadyRouted(exchange); 69 | 70 | ServerHttpRequest request = exchange.getRequest(); 71 | 72 | final HttpMethod method = HttpMethod.valueOf(request.getMethod().toString()); 73 | final String url = requestUrl.toString(); 74 | 75 | HttpHeaders filtered = filterRequest(this.headersFilters.getIfAvailable(), 76 | exchange); 77 | 78 | final DefaultHttpHeaders httpHeaders = new DefaultHttpHeaders(); 79 | filtered.forEach(httpHeaders::set); 80 | 81 | String transferEncoding = request.getHeaders().getFirst(HttpHeaders.TRANSFER_ENCODING); 82 | boolean chunkedTransfer = "chunked".equalsIgnoreCase(transferEncoding); 83 | 84 | boolean preserveHost = exchange.getAttributeOrDefault(PRESERVE_HOST_HEADER_ATTRIBUTE, false); 85 | 86 | HttpClient client = this.httpClient.chunkedTransfer(chunkedTransfer); 87 | 88 | 89 | 90 | Flux responseFlux = client 91 | .request(method) 92 | .uri(url) 93 | .send((req, nettyOutbound) -> { 94 | req.headers(httpHeaders); 95 | 96 | if (preserveHost) { 97 | String host = request.getHeaders().getFirst(HttpHeaders.HOST); 98 | req.header(HttpHeaders.HOST, host); 99 | } 100 | 101 | return nettyOutbound 102 | .options(NettyPipeline.SendOptions::flushOnEach) 103 | .send(request.getBody().map(dataBuffer -> 104 | ((NettyDataBuffer) dataBuffer).getNativeBuffer())); 105 | }).responseConnection((res, connection) -> { 106 | ServerHttpResponse response = exchange.getResponse(); 107 | // put headers and status so filters can modify the response 108 | HttpHeaders headers = new HttpHeaders(); 109 | 110 | res.responseHeaders().forEach(entry -> headers.add(entry.getKey(), entry.getValue())); 111 | 112 | if (headers.getContentType() != null) { 113 | exchange.getAttributes().put(ORIGINAL_RESPONSE_CONTENT_TYPE_ATTR, headers.getContentType()); 114 | } 115 | 116 | HttpHeaders filteredResponseHeaders = HttpHeadersFilter.filter( 117 | this.headersFilters.getIfAvailable(), headers, exchange, HttpHeadersFilter.Type.RESPONSE); 118 | 119 | response.getHeaders().putAll(filteredResponseHeaders); 120 | HttpStatus status = HttpStatus.resolve(res.status().code()); 121 | if (status != null) { 122 | response.setStatusCode(status); 123 | } else if (response instanceof AbstractServerHttpResponse) { 124 | // https://jira.spring.io/browse/SPR-16748 125 | ((AbstractServerHttpResponse) response).setStatusCodeValue(res.status().code()); 126 | } else { 127 | throw new IllegalStateException("Unable to set status code on response: " + res.status().code() + ", " + response.getClass()); 128 | } 129 | 130 | // Defer committing the response until all route filters have run 131 | // Put client response as ServerWebExchange attribute and write response later NettyWriteResponseFilter 132 | exchange.getAttributes().put(CLIENT_RESPONSE_ATTR, res); 133 | exchange.getAttributes().put(CLIENT_RESPONSE_CONN_ATTR, connection); 134 | 135 | return Mono.just(res); 136 | }); 137 | 138 | if (properties.getResponseTimeout() != null) { 139 | //TODO: figure out how to make this a 504 140 | responseFlux = responseFlux.timeout(properties.getResponseTimeout(), 141 | Mono.error(new TimeoutException("Response took longer than timeout: " + 142 | properties.getResponseTimeout()))); 143 | } 144 | 145 | return responseFlux.then(chain.filter(exchange)); 146 | } 147 | 148 | 149 | } 150 | -------------------------------------------------------------------------------- /src/main/java/org/c4isr/delta/cloudgateway/filter/PreOauth2SSOGatewayFilter.java: -------------------------------------------------------------------------------- 1 | package org.c4isr.delta.cloudgateway.filter; 2 | 3 | import org.c4isr.delta.cloudgateway.jwt.JwtOAuth2User; 4 | import org.slf4j.Logger; 5 | import org.slf4j.LoggerFactory; 6 | import org.springframework.cloud.gateway.filter.GatewayFilter; 7 | import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory; 8 | import org.springframework.http.HttpHeaders; 9 | import org.springframework.http.server.reactive.ServerHttpRequest; 10 | import org.springframework.security.core.context.ReactiveSecurityContextHolder; 11 | import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; 12 | import org.springframework.stereotype.Component; 13 | 14 | import java.util.Objects; 15 | 16 | 17 | @Component("Oauth2SSOGatewayFilterFactory") 18 | public class PreOauth2SSOGatewayFilter extends AbstractGatewayFilterFactory { 19 | 20 | 21 | private final Logger LOGGER = LoggerFactory.getLogger(PreOauth2SSOGatewayFilter.class); 22 | 23 | 24 | public PreOauth2SSOGatewayFilter() { 25 | super(Config.class); 26 | } 27 | 28 | @Override 29 | public GatewayFilter apply(Config config) { 30 | return (exchange, chain) -> ReactiveSecurityContextHolder.getContext() 31 | .filter(Objects::nonNull) 32 | .map(securityContext -> securityContext.getAuthentication()) 33 | .filter(authentication -> authentication instanceof OAuth2AuthenticationToken) 34 | .map(authentication -> (OAuth2AuthenticationToken) authentication) 35 | .map(oAuth2Authentication -> oAuth2Authentication.getPrincipal()) 36 | .filter(oAuth2User -> Objects.nonNull(oAuth2User) && oAuth2User instanceof JwtOAuth2User) 37 | .map(o -> (JwtOAuth2User) o) 38 | .map(jwtOAuth2User -> jwtOAuth2User.getJwtTokenValue()) 39 | .map(bearerToken -> { 40 | ServerHttpRequest.Builder builder = exchange.getRequest().mutate(); 41 | builder.header(HttpHeaders.AUTHORIZATION, "Bearer " + bearerToken); 42 | ServerHttpRequest request = builder.build(); 43 | return exchange.mutate().request(request).build();}) 44 | .defaultIfEmpty(exchange) 45 | .flatMap(chain::filter); 46 | } 47 | 48 | 49 | public static class Config { 50 | 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /src/main/java/org/c4isr/delta/cloudgateway/jwt/JwtConfiguration.java: -------------------------------------------------------------------------------- 1 | package org.c4isr.delta.cloudgateway.jwt; 2 | 3 | import org.bouncycastle.util.io.pem.PemObject; 4 | import org.bouncycastle.util.io.pem.PemReader; 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | import org.springframework.beans.factory.annotation.Value; 8 | import org.springframework.context.annotation.Bean; 9 | import org.springframework.context.annotation.Configuration; 10 | import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; 11 | import org.springframework.security.oauth2.client.userinfo.ReactiveOAuth2UserService; 12 | import org.springframework.security.oauth2.core.user.OAuth2User; 13 | import org.springframework.security.oauth2.jwt.NimbusReactiveJwtDecoder; 14 | import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder; 15 | import org.springframework.web.reactive.function.client.WebClient; 16 | import sun.security.rsa.RSAPublicKeyImpl; 17 | 18 | import java.io.IOException; 19 | import java.io.StringReader; 20 | import java.net.URI; 21 | import java.security.InvalidKeyException; 22 | import java.security.interfaces.RSAPublicKey; 23 | 24 | @Configuration 25 | public class JwtConfiguration { 26 | 27 | private static final Logger LOGGER = LoggerFactory.getLogger(JwtConfiguration.class); 28 | 29 | 30 | @Value("${spring.security.oauth2.client.provider.delta.jwt-key-uri}") 31 | private String keyUri; 32 | 33 | 34 | @Bean 35 | ReactiveOAuth2UserService userService(ReactiveJwtDecoder jwtDecoder){ 36 | return new JwtReactiveOAuth2UserService(jwtDecoder); 37 | } 38 | 39 | @Bean 40 | ReactiveJwtDecoder jwtDecoder() throws IOException, InvalidKeyException { 41 | return WebClient.create().get().uri(URI.create(keyUri)) 42 | .exchange() 43 | .flatMap(clientResponse -> clientResponse.bodyToMono(JwtPublicKey.class)) 44 | .map(jwtPublicKey -> parsePublicKey(jwtPublicKey.getValue())) 45 | .map(NimbusReactiveJwtDecoder::new).block(); 46 | } 47 | 48 | 49 | 50 | private RSAPublicKey parsePublicKey(String keyValue) { 51 | PemReader pemReader = new PemReader(new StringReader(keyValue)); 52 | PemObject pem = null; 53 | try { 54 | pem = pemReader.readPemObject(); 55 | return new RSAPublicKeyImpl(pem.getContent()); 56 | } catch (IOException | InvalidKeyException e) { 57 | LOGGER.error("Unable to parse public key",e); 58 | } 59 | return null; 60 | } 61 | 62 | 63 | 64 | } 65 | -------------------------------------------------------------------------------- /src/main/java/org/c4isr/delta/cloudgateway/jwt/JwtOAuth2AuthenticationTokenConverter.java: -------------------------------------------------------------------------------- 1 | package org.c4isr.delta.cloudgateway.jwt; 2 | 3 | import org.springframework.core.convert.converter.Converter; 4 | import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; 5 | import org.springframework.security.oauth2.jwt.Jwt; 6 | import reactor.core.publisher.Mono; 7 | 8 | public class JwtOAuth2AuthenticationTokenConverter implements Converter> { 9 | 10 | 11 | private Converter jwtToOAuth2UserConverter = new JwtToOAuth2UserConverter(); 12 | 13 | @Override 14 | public Mono convert(Jwt jwt) { 15 | JwtOAuth2User jwtOAuth2User = jwtToOAuth2UserConverter.convert(jwt); 16 | OAuth2AuthenticationToken auth2AuthenticationToken = new OAuth2AuthenticationToken( jwtOAuth2User, jwtOAuth2User.getAuthorities(), (String) jwt.getClaims().get("client_id")); 17 | return Mono.just(auth2AuthenticationToken); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/org/c4isr/delta/cloudgateway/jwt/JwtOAuth2User.java: -------------------------------------------------------------------------------- 1 | package org.c4isr.delta.cloudgateway.jwt; 2 | 3 | import org.springframework.security.core.GrantedAuthority; 4 | import org.springframework.security.oauth2.core.user.DefaultOAuth2User; 5 | 6 | import java.util.Collection; 7 | import java.util.Map; 8 | 9 | public class JwtOAuth2User extends DefaultOAuth2User { 10 | /** 11 | * Constructs a {@code DefaultOAuth2User} using the provided parameters. 12 | * 13 | * @param authorities the authorities granted to the user 14 | * @param attributes the attributes about the user 15 | * @param nameAttributeKey the key used to access the user's "name" from {@link #getAttributes()} 16 | */ 17 | 18 | private final String jwtTokenValue; 19 | public JwtOAuth2User(Collection authorities, Map attributes, String nameAttributeKey, String jwtTokenValue) { 20 | super(authorities, attributes, nameAttributeKey); 21 | this.jwtTokenValue = jwtTokenValue; 22 | } 23 | 24 | public String getJwtTokenValue() { 25 | return jwtTokenValue; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/org/c4isr/delta/cloudgateway/jwt/JwtPublicKey.java: -------------------------------------------------------------------------------- 1 | package org.c4isr.delta.cloudgateway.jwt; 2 | 3 | public class JwtPublicKey { 4 | private String alg; 5 | private String value; 6 | 7 | 8 | public JwtPublicKey() { 9 | } 10 | 11 | public String getAlg() { 12 | return alg; 13 | } 14 | 15 | public void setAlg(String alg) { 16 | this.alg = alg; 17 | } 18 | 19 | public String getValue() { 20 | return value; 21 | } 22 | 23 | public void setValue(String value) { 24 | this.value = value; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/org/c4isr/delta/cloudgateway/jwt/JwtReactiveOAuth2UserService.java: -------------------------------------------------------------------------------- 1 | package org.c4isr.delta.cloudgateway.jwt; 2 | 3 | import org.springframework.core.convert.converter.Converter; 4 | import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; 5 | import org.springframework.security.oauth2.client.userinfo.ReactiveOAuth2UserService; 6 | import org.springframework.security.oauth2.core.OAuth2AuthenticationException; 7 | import org.springframework.security.oauth2.core.user.OAuth2User; 8 | import org.springframework.security.oauth2.jwt.Jwt; 9 | import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder; 10 | import reactor.core.publisher.Mono; 11 | 12 | public class JwtReactiveOAuth2UserService implements ReactiveOAuth2UserService { 13 | 14 | private final ReactiveJwtDecoder jwtDecoder; 15 | private final Converter jwtToUserConverter = new JwtToOAuth2UserConverter(); 16 | 17 | public JwtReactiveOAuth2UserService(ReactiveJwtDecoder jwtDecoder) { 18 | this.jwtDecoder = jwtDecoder; 19 | } 20 | 21 | @Override 22 | public Mono loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException { 23 | return jwtDecoder.decode(userRequest.getAccessToken() 24 | .getTokenValue()) 25 | .map(jwtToUserConverter::convert); 26 | } 27 | 28 | 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/org/c4isr/delta/cloudgateway/jwt/JwtToOAuth2UserConverter.java: -------------------------------------------------------------------------------- 1 | package org.c4isr.delta.cloudgateway.jwt; 2 | 3 | import org.springframework.core.convert.converter.Converter; 4 | import org.springframework.security.core.GrantedAuthority; 5 | import org.springframework.security.core.authority.SimpleGrantedAuthority; 6 | import org.springframework.security.oauth2.jwt.Jwt; 7 | 8 | import java.util.Collection; 9 | import java.util.Map; 10 | import java.util.stream.Collectors; 11 | 12 | public class JwtToOAuth2UserConverter implements Converter { 13 | 14 | 15 | 16 | 17 | @Override 18 | public JwtOAuth2User convert(Jwt jwt) { 19 | return new JwtOAuth2User(toGrantedAuthorities(jwt.getClaims()), jwt.getClaims(), "user_name", jwt.getTokenValue()); 20 | } 21 | 22 | 23 | 24 | private Collection toGrantedAuthorities(Map claims) { 25 | Collection stringAuthorities = (Collection) claims.get("authorities"); 26 | if (stringAuthorities != null) { 27 | return stringAuthorities.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList()); 28 | } else { 29 | return null; 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | security: 3 | oauth2: 4 | client: 5 | registration: 6 | example: 7 | client-id: yours-client-id 8 | client-secret: yours-client-secret 9 | authorization-grant-type: authorization_code 10 | redirect-uri-template: '{baseUrl}/login/oauth2/code/{registrationId}' 11 | clientName: gateway 12 | provider: 13 | example: 14 | authorization-uri: http://your-auth.service/oauth/authorize 15 | token-uri: https://your-auth.service/oauth/token 16 | jwt-key-uri: http://your-auth.service/oauth/token_key #optional you can also provide public key as property value in this application.yml but in this case you should override ReactiveJwtDecoder injection in JwtConfiguration.class 17 | userNameAttribute: user_name 18 | cloud: 19 | gateway: 20 | routes: 21 | - id: microservice1 22 | predicates: 23 | - Path=/monitor/** 24 | uri: "http://microservice1:8001" 25 | filters: 26 | - PreOauth2SSOGatewayFilter 27 | 28 | - id: microservice2 29 | predicates: 30 | - Path=/updates/** 31 | uri: "http://microservice2:8011" 32 | filters: 33 | - PreOauth2SSOGatewayFilter 34 | 35 | - id: microservice2-ws-soсkjs 36 | predicates: 37 | - Path=/updates/events/info/** 38 | uri: "http://localhost:8011" 39 | filters: 40 | - PreOauth2SSOGatewayFilter 41 | 42 | - id: microservice3-ws 43 | predicates: 44 | - Path=/updates/events/info/** 45 | uri: "ws://microservice3:8021" 46 | filters: 47 | - PreOauth2SSOGatewayFilter 48 | -------------------------------------------------------------------------------- /src/main/resources/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 20 | 21 | 22 | 23 | Spring Security - OAuth 2.0 Login 24 | 25 | 26 | 27 |
28 |
29 | User: 30 |
31 |
 
32 |
33 | Log Out 34 |
35 |
36 |

OAuth 2.0 Login with Spring Security

37 |
38 | You are successfully logged in 39 | via the OAuth 2.0 Client 40 |
41 |
 
42 |
43 | User Attributes: 44 |
    45 |
  • 46 | : 47 |
  • 48 |
49 |
50 | 51 | -------------------------------------------------------------------------------- /src/test/java/org/c4isr/delta/cloudgateway/CloudGatewayApplicationTests.java: -------------------------------------------------------------------------------- 1 | package org.c4isr.delta.cloudgateway; 2 | 3 | import org.junit.Test; 4 | import org.junit.runner.RunWith; 5 | import org.springframework.boot.test.context.SpringBootTest; 6 | import org.springframework.test.context.junit4.SpringRunner; 7 | 8 | @RunWith(SpringRunner.class) 9 | @SpringBootTest 10 | public class CloudGatewayApplicationTests { 11 | 12 | @Test 13 | public void contextLoads() { 14 | } 15 | 16 | } 17 | --------------------------------------------------------------------------------