├── .gitignore ├── LICENSE.txt ├── README.md ├── build.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── libs └── .gitignore └── src ├── main ├── java │ └── com │ │ └── futureprocessing │ │ └── spring │ │ ├── Application.java │ │ ├── ContainerConfiguration.java │ │ ├── api │ │ ├── ApiController.java │ │ ├── auth │ │ │ └── AuthenticationController.java │ │ └── samplestuff │ │ │ ├── SampleController.java │ │ │ └── ServiceGateway.java │ │ ├── domain │ │ ├── CurrentlyLoggedUser.java │ │ ├── DomainUser.java │ │ └── Stuff.java │ │ └── infrastructure │ │ ├── AuthenticatedExternalServiceProvider.java │ │ ├── AuthenticatedExternalWebService.java │ │ ├── ServiceGatewayBase.java │ │ ├── ServiceGatewayImpl.java │ │ ├── externalwebservice │ │ ├── ExternalWebServiceStub.java │ │ └── SomeExternalServiceAuthenticator.java │ │ └── security │ │ ├── AuthenticationFilter.java │ │ ├── AuthenticationWithToken.java │ │ ├── BackendAdminUsernamePasswordAuthenticationProvider.java │ │ ├── BackendAdminUsernamePasswordAuthenticationToken.java │ │ ├── DomainUsernamePasswordAuthenticationProvider.java │ │ ├── ExternalServiceAuthenticator.java │ │ ├── ManagementEndpointAuthenticationFilter.java │ │ ├── SecurityConfig.java │ │ ├── TokenAuthenticationProvider.java │ │ ├── TokenResponse.java │ │ └── TokenService.java └── resources │ ├── application.properties │ ├── ehcache.xml │ ├── logback.xml │ └── private │ └── keystorejks └── test └── java └── com └── futureprocessing └── integration └── SecurityTest.java /.gitignore: -------------------------------------------------------------------------------- 1 | .classpath 2 | .gradle/ 3 | .project 4 | .settings/ 5 | bin/ 6 | build/ 7 | .idea/workspace.xml 8 | **/*.log 9 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Future Processing Sp. z o.o. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | spring-boot-security-example 2 | ============================ 3 | 4 | This project demonstrates usage of Spring-Boot with Spring-Security using Java configuration with Integration Tests 5 | 6 | Please read my post on our [Technical Blog](http://www.future-processing.pl/blog/exploring-spring-boot-and-spring-security-custom-token-based-authentication-of-rest-services-with-spring-security-and-pinch-of-spring-java-configuration-and-spring-integration-testing/) 7 | 8 | License 9 | ======= 10 | 11 | Copyright 2015 Future Processing Sp. z o.o. 12 | 13 | Licensed under The MIT License (MIT), see LICENSE.txt for details. -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext { 3 | springBootVersion = '1.1.6.RELEASE' 4 | } 5 | repositories { 6 | maven { url "http://repo.spring.io/libs-release" } 7 | maven { url "http://repo.spring.io/libs-snapshot" } 8 | mavenCentral() 9 | mavenLocal() 10 | } 11 | dependencies { 12 | classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}") 13 | } 14 | } 15 | 16 | apply plugin: 'java' 17 | apply plugin: 'eclipse' 18 | apply plugin: 'idea' 19 | apply plugin: 'spring-boot' 20 | apply plugin: 'application' 21 | 22 | sourceCompatibility = 1.8 23 | 24 | compileJava { 25 | targetCompatibility = 1.8 26 | } 27 | 28 | applicationDefaultJvmArgs = [ 29 | "-Dkeystore.file=src/main/resources/private/keystorejks", "-Dkeystore.pass=password" 30 | ] 31 | 32 | mainClassName = "com.futureprocessing.spring.Application" 33 | 34 | repositories { 35 | mavenCentral() 36 | maven { url "http://repo.spring.io/libs-release" } 37 | maven { url "http://repo.spring.io/libs-snapshot" } 38 | maven { url "http://maven.springframework.org/milestone" } 39 | } 40 | 41 | dependencies { 42 | compile fileTree(dir: 'libs', include: ['*.jar']) 43 | 44 | compile 'org.slf4j:slf4j-api:1.7.7' 45 | compile 'org.slf4j:jcl-over-slf4j:1.7.7' 46 | compile 'ch.qos.logback:logback-core:1.1.2' 47 | compile 'ch.qos.logback:logback-classic:1.1.2' 48 | 49 | compile 'org.springframework.boot:spring-boot-starter-web' 50 | compile 'org.springframework.boot:spring-boot-starter-tomcat' 51 | compile 'org.springframework.boot:spring-boot-starter-security' 52 | compile 'org.springframework.boot:spring-boot-starter-actuator' 53 | compile 'org.springframework.boot:spring-boot-starter-aop' 54 | 55 | compile 'commons-codec:commons-codec:1.4' 56 | compile('commons-httpclient:commons-httpclient:3.1') { 57 | exclude group: 'commons-logging' 58 | } 59 | compile 'commons-io:commons-io:2.4' 60 | 61 | compile 'org.apache.httpcomponents:httpclient:4.3.4' 62 | compile 'org.apache.httpcomponents:httpcore:4.0.1' 63 | 64 | compile 'com.google.guava:guava:17.0' 65 | compile 'joda-time:joda-time:2.4' 66 | compile('net.sf.ehcache:ehcache-core:2.6.9') { 67 | exclude group: 'commons-logging' 68 | } 69 | 70 | testCompile 'org.springframework.boot:spring-boot-starter-test' 71 | testCompile 'junit:junit' 72 | testCompile 'org.assertj:assertj-core:1.7.0' 73 | testCompile 'org.mockito:mockito-core:1.10.8' 74 | testCompile 'com.jayway.restassured:rest-assured:2.3.4' 75 | } 76 | 77 | jar { 78 | baseName = 'springsecuritytest' 79 | } 80 | 81 | task wrapper(type: Wrapper) { 82 | gradleVersion = '2.1' 83 | } -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FutureProcessing/spring-boot-security-example/7006da9dc087da461824be3440a626349e4e128a/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Wed Nov 12 10:12:30 CET 2014 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=http\://services.gradle.org/distributions/gradle-2.1-all.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # For Cygwin, ensure paths are in UNIX format before anything is touched. 46 | if $cygwin ; then 47 | [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 48 | fi 49 | 50 | # Attempt to set APP_HOME 51 | # Resolve links: $0 may be a link 52 | PRG="$0" 53 | # Need this for relative symlinks. 54 | while [ -h "$PRG" ] ; do 55 | ls=`ls -ld "$PRG"` 56 | link=`expr "$ls" : '.*-> \(.*\)$'` 57 | if expr "$link" : '/.*' > /dev/null; then 58 | PRG="$link" 59 | else 60 | PRG=`dirname "$PRG"`"/$link" 61 | fi 62 | done 63 | SAVED="`pwd`" 64 | cd "`dirname \"$PRG\"`/" >&- 65 | APP_HOME="`pwd -P`" 66 | cd "$SAVED" >&- 67 | 68 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 69 | 70 | # Determine the Java command to use to start the JVM. 71 | if [ -n "$JAVA_HOME" ] ; then 72 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 73 | # IBM's JDK on AIX uses strange locations for the executables 74 | JAVACMD="$JAVA_HOME/jre/sh/java" 75 | else 76 | JAVACMD="$JAVA_HOME/bin/java" 77 | fi 78 | if [ ! -x "$JAVACMD" ] ; then 79 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 80 | 81 | Please set the JAVA_HOME variable in your environment to match the 82 | location of your Java installation." 83 | fi 84 | else 85 | JAVACMD="java" 86 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 87 | 88 | Please set the JAVA_HOME variable in your environment to match the 89 | location of your Java installation." 90 | fi 91 | 92 | # Increase the maximum file descriptors if we can. 93 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 94 | MAX_FD_LIMIT=`ulimit -H -n` 95 | if [ $? -eq 0 ] ; then 96 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 97 | MAX_FD="$MAX_FD_LIMIT" 98 | fi 99 | ulimit -n $MAX_FD 100 | if [ $? -ne 0 ] ; then 101 | warn "Could not set maximum file descriptor limit: $MAX_FD" 102 | fi 103 | else 104 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 105 | fi 106 | fi 107 | 108 | # For Darwin, add options to specify how the application appears in the dock 109 | if $darwin; then 110 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 111 | fi 112 | 113 | # For Cygwin, switch paths to Windows format before running java 114 | if $cygwin ; then 115 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 116 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 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 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 158 | function splitJvmOpts() { 159 | JVM_OPTS=("$@") 160 | } 161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 163 | 164 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 165 | -------------------------------------------------------------------------------- /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 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 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 Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /libs/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FutureProcessing/spring-boot-security-example/7006da9dc087da461824be3440a626349e4e128a/libs/.gitignore -------------------------------------------------------------------------------- /src/main/java/com/futureprocessing/spring/Application.java: -------------------------------------------------------------------------------- 1 | package com.futureprocessing.spring; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.EnableAutoConfiguration; 5 | import org.springframework.boot.context.properties.EnableConfigurationProperties; 6 | import org.springframework.context.annotation.ComponentScan; 7 | import org.springframework.context.annotation.Configuration; 8 | import org.springframework.web.servlet.config.annotation.EnableWebMvc; 9 | 10 | @Configuration 11 | @EnableWebMvc 12 | @ComponentScan 13 | @EnableAutoConfiguration 14 | @EnableConfigurationProperties 15 | public class Application { 16 | 17 | public static void main(String[] args) { 18 | SpringApplication.run(Application.class, args); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/com/futureprocessing/spring/ContainerConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.futureprocessing.spring; 2 | 3 | import org.apache.coyote.http11.Http11NioProtocol; 4 | import org.springframework.beans.factory.annotation.Value; 5 | import org.springframework.boot.context.embedded.ConfigurableEmbeddedServletContainer; 6 | import org.springframework.boot.context.embedded.EmbeddedServletContainerCustomizer; 7 | import org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainerFactory; 8 | import org.springframework.context.annotation.Bean; 9 | import org.springframework.context.annotation.Configuration; 10 | 11 | import java.io.File; 12 | 13 | @Configuration 14 | public class ContainerConfiguration { 15 | 16 | @Bean 17 | EmbeddedServletContainerCustomizer containerCustomizer( 18 | @Value("${keystore.file}") String keystoreFile, 19 | @Value("${server.port}") final String serverPort, 20 | @Value("${keystore.pass}") final String keystorePass) 21 | throws Exception { 22 | 23 | // This is boiler plate code to setup https on embedded Tomcat 24 | // with Spring Boot: 25 | 26 | final String absoluteKeystoreFile = new File(keystoreFile) 27 | .getAbsolutePath(); 28 | 29 | return new EmbeddedServletContainerCustomizer() { 30 | @Override 31 | public void customize(ConfigurableEmbeddedServletContainer container) { 32 | TomcatEmbeddedServletContainerFactory tomcat = (TomcatEmbeddedServletContainerFactory) container; 33 | tomcat.addConnectorCustomizers(connector -> { 34 | connector.setPort(Integer.parseInt(serverPort)); 35 | connector.setSecure(true); 36 | connector.setScheme("https"); 37 | 38 | Http11NioProtocol proto = (Http11NioProtocol) connector 39 | .getProtocolHandler(); 40 | proto.setSSLEnabled(true); 41 | 42 | proto.setKeystoreFile(absoluteKeystoreFile); 43 | proto.setKeystorePass(keystorePass); 44 | proto.setKeystoreType("JKS"); 45 | proto.setKeyAlias("tomcat"); 46 | }); 47 | } 48 | }; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/com/futureprocessing/spring/api/ApiController.java: -------------------------------------------------------------------------------- 1 | package com.futureprocessing.spring.api; 2 | 3 | public abstract class ApiController { 4 | private static final String API_PATH = "/api/v1"; 5 | 6 | public static final String AUTHENTICATE_URL = API_PATH + "/authenticate"; 7 | public static final String STUFF_URL = API_PATH + "/stuff"; 8 | 9 | // Spring Boot Actuator services 10 | public static final String AUTOCONFIG_ENDPOINT = "/autoconfig"; 11 | public static final String BEANS_ENDPOINT = "/beans"; 12 | public static final String CONFIGPROPS_ENDPOINT = "/configprops"; 13 | public static final String ENV_ENDPOINT = "/env"; 14 | public static final String MAPPINGS_ENDPOINT = "/mappings"; 15 | public static final String METRICS_ENDPOINT = "/metrics"; 16 | public static final String SHUTDOWN_ENDPOINT = "/shutdown"; 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/futureprocessing/spring/api/auth/AuthenticationController.java: -------------------------------------------------------------------------------- 1 | package com.futureprocessing.spring.api.auth; 2 | 3 | import com.futureprocessing.spring.api.ApiController; 4 | import org.springframework.web.bind.annotation.RequestMapping; 5 | import org.springframework.web.bind.annotation.RequestMethod; 6 | import org.springframework.web.bind.annotation.RestController; 7 | 8 | @RestController 9 | public class AuthenticationController extends ApiController { 10 | 11 | @RequestMapping(value = AUTHENTICATE_URL, method = RequestMethod.POST) 12 | public String authenticate() { 13 | return "This is just for in-code-documentation purposes and Rest API reference documentation." + 14 | "Servlet will never get to this point as Http requests are processed by AuthenticationFilter." + 15 | "Nonetheless to authenticate Domain User POST request with X-Auth-Username and X-Auth-Password headers " + 16 | "is mandatory to this URL. If username and password are correct valid token will be returned (just json string in response) " + 17 | "This token must be present in X-Auth-Token header in all requests for all other URLs, including logout." + 18 | "Authentication can be issued multiple times and each call results in new ticket."; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/com/futureprocessing/spring/api/samplestuff/SampleController.java: -------------------------------------------------------------------------------- 1 | package com.futureprocessing.spring.api.samplestuff; 2 | 3 | import com.futureprocessing.spring.api.ApiController; 4 | import com.futureprocessing.spring.domain.CurrentlyLoggedUser; 5 | import com.futureprocessing.spring.domain.DomainUser; 6 | import com.futureprocessing.spring.domain.Stuff; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.security.access.prepost.PreAuthorize; 9 | import org.springframework.web.bind.annotation.RequestBody; 10 | import org.springframework.web.bind.annotation.RequestMapping; 11 | import org.springframework.web.bind.annotation.RequestMethod; 12 | import org.springframework.web.bind.annotation.RestController; 13 | 14 | import java.util.List; 15 | 16 | @RestController 17 | @PreAuthorize("hasAuthority('ROLE_DOMAIN_USER')") 18 | public class SampleController extends ApiController { 19 | private final ServiceGateway serviceGateway; 20 | 21 | @Autowired 22 | public SampleController(ServiceGateway serviceGateway) { 23 | this.serviceGateway = serviceGateway; 24 | } 25 | 26 | @RequestMapping(value = STUFF_URL, method = RequestMethod.GET) 27 | public List getSomeStuff() { 28 | return serviceGateway.getSomeStuff(); 29 | } 30 | 31 | @RequestMapping(value = STUFF_URL, method = RequestMethod.POST) 32 | public void createStuff(@RequestBody Stuff newStuff, @CurrentlyLoggedUser DomainUser domainUser) { 33 | serviceGateway.createStuff(newStuff, domainUser); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/com/futureprocessing/spring/api/samplestuff/ServiceGateway.java: -------------------------------------------------------------------------------- 1 | package com.futureprocessing.spring.api.samplestuff; 2 | 3 | import com.futureprocessing.spring.domain.DomainUser; 4 | import com.futureprocessing.spring.domain.Stuff; 5 | 6 | import java.util.List; 7 | 8 | public interface ServiceGateway { 9 | List getSomeStuff(); 10 | 11 | void createStuff(Stuff newStuff, DomainUser domainUser); 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/futureprocessing/spring/domain/CurrentlyLoggedUser.java: -------------------------------------------------------------------------------- 1 | package com.futureprocessing.spring.domain; 2 | 3 | import org.springframework.security.web.bind.annotation.AuthenticationPrincipal; 4 | 5 | import java.lang.annotation.ElementType; 6 | import java.lang.annotation.Retention; 7 | import java.lang.annotation.RetentionPolicy; 8 | import java.lang.annotation.Target; 9 | 10 | @Target({ElementType.PARAMETER, ElementType.TYPE}) 11 | @Retention(RetentionPolicy.RUNTIME) 12 | @AuthenticationPrincipal 13 | public @interface CurrentlyLoggedUser { 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/com/futureprocessing/spring/domain/DomainUser.java: -------------------------------------------------------------------------------- 1 | package com.futureprocessing.spring.domain; 2 | 3 | public class DomainUser { 4 | private String username; 5 | 6 | public DomainUser(String username) { 7 | this.username = username; 8 | } 9 | 10 | public String getUsername() { 11 | return username; 12 | } 13 | 14 | @Override 15 | public String toString() { 16 | return username; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/com/futureprocessing/spring/domain/Stuff.java: -------------------------------------------------------------------------------- 1 | package com.futureprocessing.spring.domain; 2 | 3 | public class Stuff { 4 | 5 | private String description; 6 | private DomainUser owner; 7 | private String details; 8 | 9 | public String getDescription() { 10 | return description; 11 | } 12 | 13 | public void setDescription(String description) { 14 | this.description = description; 15 | } 16 | 17 | public DomainUser getOwner() { 18 | return owner; 19 | } 20 | 21 | public void setOwner(DomainUser owner) { 22 | this.owner = owner; 23 | } 24 | 25 | public String getDetails() { 26 | return details; 27 | } 28 | 29 | public void setDetails(String details) { 30 | this.details = details; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/com/futureprocessing/spring/infrastructure/AuthenticatedExternalServiceProvider.java: -------------------------------------------------------------------------------- 1 | package com.futureprocessing.spring.infrastructure; 2 | 3 | import org.springframework.security.core.context.SecurityContextHolder; 4 | import org.springframework.stereotype.Component; 5 | 6 | @Component 7 | public class AuthenticatedExternalServiceProvider { 8 | 9 | public AuthenticatedExternalWebService provide() { 10 | return (AuthenticatedExternalWebService) SecurityContextHolder.getContext().getAuthentication(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/futureprocessing/spring/infrastructure/AuthenticatedExternalWebService.java: -------------------------------------------------------------------------------- 1 | package com.futureprocessing.spring.infrastructure; 2 | 3 | import com.futureprocessing.spring.infrastructure.externalwebservice.ExternalWebServiceStub; 4 | import com.futureprocessing.spring.infrastructure.security.AuthenticationWithToken; 5 | import org.springframework.security.core.GrantedAuthority; 6 | 7 | import java.util.Collection; 8 | 9 | public class AuthenticatedExternalWebService extends AuthenticationWithToken { 10 | 11 | private ExternalWebServiceStub externalWebService; 12 | 13 | public AuthenticatedExternalWebService(Object aPrincipal, Object aCredentials, Collection anAuthorities) { 14 | super(aPrincipal, aCredentials, anAuthorities); 15 | } 16 | 17 | public void setExternalWebService(ExternalWebServiceStub externalWebService) { 18 | this.externalWebService = externalWebService; 19 | } 20 | 21 | public ExternalWebServiceStub getExternalWebService() { 22 | return externalWebService; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/com/futureprocessing/spring/infrastructure/ServiceGatewayBase.java: -------------------------------------------------------------------------------- 1 | package com.futureprocessing.spring.infrastructure; 2 | 3 | import com.futureprocessing.spring.infrastructure.externalwebservice.ExternalWebServiceStub; 4 | 5 | public abstract class ServiceGatewayBase { 6 | private AuthenticatedExternalServiceProvider authenticatedExternalServiceProvider; 7 | 8 | public ServiceGatewayBase(AuthenticatedExternalServiceProvider authenticatedExternalServiceProvider) { 9 | this.authenticatedExternalServiceProvider = authenticatedExternalServiceProvider; 10 | } 11 | 12 | protected ExternalWebServiceStub externalService() { 13 | return authenticatedExternalServiceProvider.provide().getExternalWebService(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/futureprocessing/spring/infrastructure/ServiceGatewayImpl.java: -------------------------------------------------------------------------------- 1 | package com.futureprocessing.spring.infrastructure; 2 | 3 | import com.futureprocessing.spring.api.samplestuff.ServiceGateway; 4 | import com.futureprocessing.spring.domain.DomainUser; 5 | import com.futureprocessing.spring.domain.Stuff; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.stereotype.Component; 8 | 9 | import java.util.List; 10 | 11 | @Component 12 | public class ServiceGatewayImpl extends ServiceGatewayBase implements ServiceGateway { 13 | 14 | @Autowired 15 | public ServiceGatewayImpl(AuthenticatedExternalServiceProvider authenticatedExternalServiceProvider) { 16 | super(authenticatedExternalServiceProvider); 17 | } 18 | 19 | @Override 20 | public List getSomeStuff() { 21 | String stuffFromExternalWebService = externalService().getSomeStuff(); 22 | // do some processing, create return list 23 | return null; 24 | } 25 | 26 | @Override 27 | public void createStuff(Stuff newStuff, DomainUser domainUser) { 28 | // do some processing, store domainUser in newStuff, send newStuff over the wire to external web service etc. 29 | } 30 | } 31 | 32 | -------------------------------------------------------------------------------- /src/main/java/com/futureprocessing/spring/infrastructure/externalwebservice/ExternalWebServiceStub.java: -------------------------------------------------------------------------------- 1 | package com.futureprocessing.spring.infrastructure.externalwebservice; 2 | 3 | public class ExternalWebServiceStub { 4 | 5 | public String getSomeStuff() { 6 | return "From external WebService"; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/com/futureprocessing/spring/infrastructure/externalwebservice/SomeExternalServiceAuthenticator.java: -------------------------------------------------------------------------------- 1 | package com.futureprocessing.spring.infrastructure.externalwebservice; 2 | 3 | import com.futureprocessing.spring.domain.DomainUser; 4 | import com.futureprocessing.spring.infrastructure.AuthenticatedExternalWebService; 5 | import com.futureprocessing.spring.infrastructure.security.ExternalServiceAuthenticator; 6 | import org.springframework.security.core.authority.AuthorityUtils; 7 | 8 | public class SomeExternalServiceAuthenticator implements ExternalServiceAuthenticator { 9 | 10 | @Override 11 | public AuthenticatedExternalWebService authenticate(String username, String password) { 12 | ExternalWebServiceStub externalWebService = new ExternalWebServiceStub(); 13 | 14 | // Do all authentication mechanisms required by external web service protocol and validated response. 15 | // Throw descendant of Spring AuthenticationException in case of unsucessful authentication. For example BadCredentialsException 16 | 17 | // ... 18 | // ... 19 | 20 | // If authentication to external service succeeded then create authenticated wrapper with proper Principal and GrantedAuthorities. 21 | // GrantedAuthorities may come from external service authentication or be hardcoded at our layer as they are here with ROLE_DOMAIN_USER 22 | AuthenticatedExternalWebService authenticatedExternalWebService = new AuthenticatedExternalWebService(new DomainUser(username), null, 23 | AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_DOMAIN_USER")); 24 | authenticatedExternalWebService.setExternalWebService(externalWebService); 25 | 26 | return authenticatedExternalWebService; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/com/futureprocessing/spring/infrastructure/security/AuthenticationFilter.java: -------------------------------------------------------------------------------- 1 | package com.futureprocessing.spring.infrastructure.security; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import com.futureprocessing.spring.api.ApiController; 5 | import com.google.common.base.Optional; 6 | import com.google.common.base.Strings; 7 | import org.slf4j.Logger; 8 | import org.slf4j.LoggerFactory; 9 | import org.slf4j.MDC; 10 | import org.springframework.security.authentication.AuthenticationManager; 11 | import org.springframework.security.authentication.InternalAuthenticationServiceException; 12 | import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; 13 | import org.springframework.security.authentication.encoding.MessageDigestPasswordEncoder; 14 | import org.springframework.security.core.Authentication; 15 | import org.springframework.security.core.AuthenticationException; 16 | import org.springframework.security.core.context.SecurityContextHolder; 17 | import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken; 18 | import org.springframework.web.filter.GenericFilterBean; 19 | import org.springframework.web.util.UrlPathHelper; 20 | 21 | import javax.servlet.FilterChain; 22 | import javax.servlet.ServletException; 23 | import javax.servlet.ServletRequest; 24 | import javax.servlet.ServletResponse; 25 | import javax.servlet.http.HttpServletRequest; 26 | import javax.servlet.http.HttpServletResponse; 27 | import java.io.IOException; 28 | 29 | public class AuthenticationFilter extends GenericFilterBean { 30 | 31 | private final static Logger logger = LoggerFactory.getLogger(AuthenticationFilter.class); 32 | public static final String TOKEN_SESSION_KEY = "token"; 33 | public static final String USER_SESSION_KEY = "user"; 34 | private AuthenticationManager authenticationManager; 35 | 36 | public AuthenticationFilter(AuthenticationManager authenticationManager) { 37 | this.authenticationManager = authenticationManager; 38 | } 39 | 40 | @Override 41 | public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { 42 | HttpServletRequest httpRequest = asHttp(request); 43 | HttpServletResponse httpResponse = asHttp(response); 44 | 45 | Optional username = Optional.fromNullable(httpRequest.getHeader("X-Auth-Username")); 46 | Optional password = Optional.fromNullable(httpRequest.getHeader("X-Auth-Password")); 47 | Optional token = Optional.fromNullable(httpRequest.getHeader("X-Auth-Token")); 48 | 49 | String resourcePath = new UrlPathHelper().getPathWithinApplication(httpRequest); 50 | 51 | try { 52 | if (postToAuthenticate(httpRequest, resourcePath)) { 53 | logger.debug("Trying to authenticate user {} by X-Auth-Username method", username); 54 | processUsernamePasswordAuthentication(httpResponse, username, password); 55 | return; 56 | } 57 | 58 | if (token.isPresent()) { 59 | logger.debug("Trying to authenticate user by X-Auth-Token method. Token: {}", token); 60 | processTokenAuthentication(token); 61 | } 62 | 63 | logger.debug("AuthenticationFilter is passing request down the filter chain"); 64 | addSessionContextToLogging(); 65 | chain.doFilter(request, response); 66 | } catch (InternalAuthenticationServiceException internalAuthenticationServiceException) { 67 | SecurityContextHolder.clearContext(); 68 | logger.error("Internal authentication service exception", internalAuthenticationServiceException); 69 | httpResponse.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); 70 | } catch (AuthenticationException authenticationException) { 71 | SecurityContextHolder.clearContext(); 72 | httpResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED, authenticationException.getMessage()); 73 | } finally { 74 | MDC.remove(TOKEN_SESSION_KEY); 75 | MDC.remove(USER_SESSION_KEY); 76 | } 77 | } 78 | 79 | private void addSessionContextToLogging() { 80 | Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); 81 | String tokenValue = "EMPTY"; 82 | if (authentication != null && !Strings.isNullOrEmpty(authentication.getDetails().toString())) { 83 | MessageDigestPasswordEncoder encoder = new MessageDigestPasswordEncoder("SHA-1"); 84 | tokenValue = encoder.encodePassword(authentication.getDetails().toString(), "not_so_random_salt"); 85 | } 86 | MDC.put(TOKEN_SESSION_KEY, tokenValue); 87 | 88 | String userValue = "EMPTY"; 89 | if (authentication != null && !Strings.isNullOrEmpty(authentication.getPrincipal().toString())) { 90 | userValue = authentication.getPrincipal().toString(); 91 | } 92 | MDC.put(USER_SESSION_KEY, userValue); 93 | } 94 | 95 | private HttpServletRequest asHttp(ServletRequest request) { 96 | return (HttpServletRequest) request; 97 | } 98 | 99 | private HttpServletResponse asHttp(ServletResponse response) { 100 | return (HttpServletResponse) response; 101 | } 102 | 103 | private boolean postToAuthenticate(HttpServletRequest httpRequest, String resourcePath) { 104 | return ApiController.AUTHENTICATE_URL.equalsIgnoreCase(resourcePath) && httpRequest.getMethod().equals("POST"); 105 | } 106 | 107 | private void processUsernamePasswordAuthentication(HttpServletResponse httpResponse, Optional username, Optional password) throws IOException { 108 | Authentication resultOfAuthentication = tryToAuthenticateWithUsernameAndPassword(username, password); 109 | SecurityContextHolder.getContext().setAuthentication(resultOfAuthentication); 110 | httpResponse.setStatus(HttpServletResponse.SC_OK); 111 | TokenResponse tokenResponse = new TokenResponse(resultOfAuthentication.getDetails().toString()); 112 | String tokenJsonResponse = new ObjectMapper().writeValueAsString(tokenResponse); 113 | httpResponse.addHeader("Content-Type", "application/json"); 114 | httpResponse.getWriter().print(tokenJsonResponse); 115 | } 116 | 117 | private Authentication tryToAuthenticateWithUsernameAndPassword(Optional username, Optional password) { 118 | UsernamePasswordAuthenticationToken requestAuthentication = new UsernamePasswordAuthenticationToken(username, password); 119 | return tryToAuthenticate(requestAuthentication); 120 | } 121 | 122 | private void processTokenAuthentication(Optional token) { 123 | Authentication resultOfAuthentication = tryToAuthenticateWithToken(token); 124 | SecurityContextHolder.getContext().setAuthentication(resultOfAuthentication); 125 | } 126 | 127 | private Authentication tryToAuthenticateWithToken(Optional token) { 128 | PreAuthenticatedAuthenticationToken requestAuthentication = new PreAuthenticatedAuthenticationToken(token, null); 129 | return tryToAuthenticate(requestAuthentication); 130 | } 131 | 132 | private Authentication tryToAuthenticate(Authentication requestAuthentication) { 133 | Authentication responseAuthentication = authenticationManager.authenticate(requestAuthentication); 134 | if (responseAuthentication == null || !responseAuthentication.isAuthenticated()) { 135 | throw new InternalAuthenticationServiceException("Unable to authenticate Domain User for provided credentials"); 136 | } 137 | logger.debug("User successfully authenticated"); 138 | return responseAuthentication; 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /src/main/java/com/futureprocessing/spring/infrastructure/security/AuthenticationWithToken.java: -------------------------------------------------------------------------------- 1 | package com.futureprocessing.spring.infrastructure.security; 2 | 3 | import org.springframework.security.core.GrantedAuthority; 4 | import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken; 5 | 6 | import java.util.Collection; 7 | 8 | public class AuthenticationWithToken extends PreAuthenticatedAuthenticationToken { 9 | public AuthenticationWithToken(Object aPrincipal, Object aCredentials) { 10 | super(aPrincipal, aCredentials); 11 | } 12 | 13 | public AuthenticationWithToken(Object aPrincipal, Object aCredentials, Collection anAuthorities) { 14 | super(aPrincipal, aCredentials, anAuthorities); 15 | } 16 | 17 | public void setToken(String token) { 18 | setDetails(token); 19 | } 20 | 21 | public String getToken() { 22 | return (String)getDetails(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/com/futureprocessing/spring/infrastructure/security/BackendAdminUsernamePasswordAuthenticationProvider.java: -------------------------------------------------------------------------------- 1 | package com.futureprocessing.spring.infrastructure.security; 2 | 3 | import com.google.common.base.Optional; 4 | import org.springframework.beans.factory.annotation.Value; 5 | import org.springframework.security.authentication.AuthenticationProvider; 6 | import org.springframework.security.authentication.BadCredentialsException; 7 | import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; 8 | import org.springframework.security.core.Authentication; 9 | import org.springframework.security.core.AuthenticationException; 10 | import org.springframework.security.core.authority.AuthorityUtils; 11 | 12 | public class BackendAdminUsernamePasswordAuthenticationProvider implements AuthenticationProvider { 13 | 14 | public static final String INVALID_BACKEND_ADMIN_CREDENTIALS = "Invalid Backend Admin Credentials"; 15 | 16 | @Value("${backend.admin.username}") 17 | private String backendAdminUsername; 18 | 19 | @Value("${backend.admin.password}") 20 | private String backendAdminPassword; 21 | 22 | @Override 23 | public Authentication authenticate(Authentication authentication) throws AuthenticationException { 24 | Optional username = (Optional) authentication.getPrincipal(); 25 | Optional password = (Optional) authentication.getCredentials(); 26 | 27 | if (credentialsMissing(username, password) || credentialsInvalid(username, password)) { 28 | throw new BadCredentialsException(INVALID_BACKEND_ADMIN_CREDENTIALS); 29 | } 30 | 31 | return new UsernamePasswordAuthenticationToken(username.get(), null, 32 | AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_BACKEND_ADMIN")); 33 | } 34 | 35 | private boolean credentialsMissing(Optional username, Optional password) { 36 | return !username.isPresent() || !password.isPresent(); 37 | } 38 | 39 | private boolean credentialsInvalid(Optional username, Optional password) { 40 | return !isBackendAdmin(username.get()) || !password.get().equals(backendAdminPassword); 41 | } 42 | 43 | private boolean isBackendAdmin(String username) { 44 | return backendAdminUsername.equals(username); 45 | } 46 | 47 | @Override 48 | public boolean supports(Class authentication) { 49 | return authentication.equals(BackendAdminUsernamePasswordAuthenticationToken.class); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/com/futureprocessing/spring/infrastructure/security/BackendAdminUsernamePasswordAuthenticationToken.java: -------------------------------------------------------------------------------- 1 | package com.futureprocessing.spring.infrastructure.security; 2 | 3 | import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; 4 | 5 | public class BackendAdminUsernamePasswordAuthenticationToken extends UsernamePasswordAuthenticationToken { 6 | public BackendAdminUsernamePasswordAuthenticationToken(Object principal, Object credentials) { 7 | super(principal, credentials); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/com/futureprocessing/spring/infrastructure/security/DomainUsernamePasswordAuthenticationProvider.java: -------------------------------------------------------------------------------- 1 | package com.futureprocessing.spring.infrastructure.security; 2 | 3 | import com.google.common.base.Optional; 4 | import org.springframework.security.authentication.AuthenticationProvider; 5 | import org.springframework.security.authentication.BadCredentialsException; 6 | import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; 7 | import org.springframework.security.core.Authentication; 8 | import org.springframework.security.core.AuthenticationException; 9 | 10 | public class DomainUsernamePasswordAuthenticationProvider implements AuthenticationProvider { 11 | 12 | private TokenService tokenService; 13 | private ExternalServiceAuthenticator externalServiceAuthenticator; 14 | 15 | public DomainUsernamePasswordAuthenticationProvider(TokenService tokenService, ExternalServiceAuthenticator externalServiceAuthenticator) { 16 | this.tokenService = tokenService; 17 | this.externalServiceAuthenticator = externalServiceAuthenticator; 18 | } 19 | 20 | @Override 21 | public Authentication authenticate(Authentication authentication) throws AuthenticationException { 22 | Optional username = (Optional) authentication.getPrincipal(); 23 | Optional password = (Optional) authentication.getCredentials(); 24 | 25 | if (!username.isPresent() || !password.isPresent()) { 26 | throw new BadCredentialsException("Invalid Domain User Credentials"); 27 | } 28 | 29 | AuthenticationWithToken resultOfAuthentication = externalServiceAuthenticator.authenticate(username.get(), password.get()); 30 | String newToken = tokenService.generateNewToken(); 31 | resultOfAuthentication.setToken(newToken); 32 | tokenService.store(newToken, resultOfAuthentication); 33 | 34 | return resultOfAuthentication; 35 | } 36 | 37 | @Override 38 | public boolean supports(Class authentication) { 39 | return authentication.equals(UsernamePasswordAuthenticationToken.class); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/com/futureprocessing/spring/infrastructure/security/ExternalServiceAuthenticator.java: -------------------------------------------------------------------------------- 1 | package com.futureprocessing.spring.infrastructure.security; 2 | 3 | public interface ExternalServiceAuthenticator { 4 | 5 | AuthenticationWithToken authenticate(String username, String password); 6 | } 7 | -------------------------------------------------------------------------------- /src/main/java/com/futureprocessing/spring/infrastructure/security/ManagementEndpointAuthenticationFilter.java: -------------------------------------------------------------------------------- 1 | package com.futureprocessing.spring.infrastructure.security; 2 | 3 | import com.futureprocessing.spring.api.ApiController; 4 | import com.google.common.base.Optional; 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | import org.springframework.security.authentication.AuthenticationManager; 8 | import org.springframework.security.authentication.InternalAuthenticationServiceException; 9 | import org.springframework.security.core.Authentication; 10 | import org.springframework.security.core.AuthenticationException; 11 | import org.springframework.security.core.context.SecurityContextHolder; 12 | import org.springframework.web.filter.GenericFilterBean; 13 | import org.springframework.web.util.UrlPathHelper; 14 | 15 | import javax.servlet.FilterChain; 16 | import javax.servlet.ServletException; 17 | import javax.servlet.ServletRequest; 18 | import javax.servlet.ServletResponse; 19 | import javax.servlet.http.HttpServletRequest; 20 | import javax.servlet.http.HttpServletResponse; 21 | import java.io.IOException; 22 | import java.util.HashSet; 23 | import java.util.Set; 24 | 25 | public class ManagementEndpointAuthenticationFilter extends GenericFilterBean { 26 | 27 | private final static Logger logger = LoggerFactory.getLogger(ManagementEndpointAuthenticationFilter.class); 28 | private AuthenticationManager authenticationManager; 29 | private Set managementEndpoints; 30 | 31 | public ManagementEndpointAuthenticationFilter(AuthenticationManager authenticationManager) { 32 | this.authenticationManager = authenticationManager; 33 | prepareManagementEndpointsSet(); 34 | } 35 | 36 | private void prepareManagementEndpointsSet() { 37 | managementEndpoints = new HashSet<>(); 38 | managementEndpoints.add(ApiController.AUTOCONFIG_ENDPOINT); 39 | managementEndpoints.add(ApiController.BEANS_ENDPOINT); 40 | managementEndpoints.add(ApiController.CONFIGPROPS_ENDPOINT); 41 | managementEndpoints.add(ApiController.ENV_ENDPOINT); 42 | managementEndpoints.add(ApiController.MAPPINGS_ENDPOINT); 43 | managementEndpoints.add(ApiController.METRICS_ENDPOINT); 44 | managementEndpoints.add(ApiController.SHUTDOWN_ENDPOINT); 45 | } 46 | 47 | @Override 48 | public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { 49 | HttpServletRequest httpRequest = asHttp(request); 50 | HttpServletResponse httpResponse = asHttp(response); 51 | 52 | Optional username = Optional.fromNullable(httpRequest.getHeader("X-Auth-Username")); 53 | Optional password = Optional.fromNullable(httpRequest.getHeader("X-Auth-Password")); 54 | 55 | String resourcePath = new UrlPathHelper().getPathWithinApplication(httpRequest); 56 | 57 | try { 58 | if (postToManagementEndpoints(resourcePath)) { 59 | logger.debug("Trying to authenticate user {} for management endpoint by X-Auth-Username method", username); 60 | processManagementEndpointUsernamePasswordAuthentication(username, password); 61 | } 62 | 63 | logger.debug("ManagementEndpointAuthenticationFilter is passing request down the filter chain"); 64 | chain.doFilter(request, response); 65 | } catch (AuthenticationException authenticationException) { 66 | SecurityContextHolder.clearContext(); 67 | httpResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED, authenticationException.getMessage()); 68 | } 69 | } 70 | 71 | private HttpServletRequest asHttp(ServletRequest request) { 72 | return (HttpServletRequest) request; 73 | } 74 | 75 | private HttpServletResponse asHttp(ServletResponse response) { 76 | return (HttpServletResponse) response; 77 | } 78 | 79 | private boolean postToManagementEndpoints(String resourcePath) { 80 | return managementEndpoints.contains(resourcePath); 81 | } 82 | 83 | private void processManagementEndpointUsernamePasswordAuthentication(Optional username, Optional password) throws IOException { 84 | Authentication resultOfAuthentication = tryToAuthenticateWithUsernameAndPassword(username, password); 85 | SecurityContextHolder.getContext().setAuthentication(resultOfAuthentication); 86 | } 87 | 88 | private Authentication tryToAuthenticateWithUsernameAndPassword(Optional username, Optional password) { 89 | BackendAdminUsernamePasswordAuthenticationToken requestAuthentication = new BackendAdminUsernamePasswordAuthenticationToken(username, password); 90 | return tryToAuthenticate(requestAuthentication); 91 | } 92 | 93 | private Authentication tryToAuthenticate(Authentication requestAuthentication) { 94 | Authentication responseAuthentication = authenticationManager.authenticate(requestAuthentication); 95 | if (responseAuthentication == null || !responseAuthentication.isAuthenticated()) { 96 | throw new InternalAuthenticationServiceException("Unable to authenticate Backend Admin for provided credentials"); 97 | } 98 | logger.debug("Backend Admin successfully authenticated"); 99 | return responseAuthentication; 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/main/java/com/futureprocessing/spring/infrastructure/security/SecurityConfig.java: -------------------------------------------------------------------------------- 1 | package com.futureprocessing.spring.infrastructure.security; 2 | 3 | import com.futureprocessing.spring.api.ApiController; 4 | import com.futureprocessing.spring.infrastructure.externalwebservice.SomeExternalServiceAuthenticator; 5 | import org.springframework.beans.factory.annotation.Value; 6 | import org.springframework.context.annotation.Bean; 7 | import org.springframework.context.annotation.Configuration; 8 | import org.springframework.scheduling.annotation.EnableScheduling; 9 | import org.springframework.security.authentication.AuthenticationProvider; 10 | import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; 11 | import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; 12 | import org.springframework.security.config.annotation.web.builders.HttpSecurity; 13 | import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; 14 | import org.springframework.security.config.annotation.web.servlet.configuration.EnableWebMvcSecurity; 15 | import org.springframework.security.config.http.SessionCreationPolicy; 16 | import org.springframework.security.web.AuthenticationEntryPoint; 17 | import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; 18 | 19 | import javax.servlet.http.HttpServletResponse; 20 | 21 | @Configuration 22 | @EnableWebMvcSecurity 23 | @EnableScheduling 24 | @EnableGlobalMethodSecurity(prePostEnabled = true) 25 | public class SecurityConfig extends WebSecurityConfigurerAdapter { 26 | 27 | @Value("${backend.admin.role}") 28 | private String backendAdminRole; 29 | 30 | @Override 31 | protected void configure(HttpSecurity http) throws Exception { 32 | http. 33 | csrf().disable(). 34 | sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS). 35 | and(). 36 | authorizeRequests(). 37 | antMatchers(actuatorEndpoints()).hasRole(backendAdminRole). 38 | anyRequest().authenticated(). 39 | and(). 40 | anonymous().disable(). 41 | exceptionHandling().authenticationEntryPoint(unauthorizedEntryPoint()); 42 | 43 | http.addFilterBefore(new AuthenticationFilter(authenticationManager()), BasicAuthenticationFilter.class). 44 | addFilterBefore(new ManagementEndpointAuthenticationFilter(authenticationManager()), BasicAuthenticationFilter.class); 45 | } 46 | 47 | private String[] actuatorEndpoints() { 48 | return new String[]{ApiController.AUTOCONFIG_ENDPOINT, ApiController.BEANS_ENDPOINT, ApiController.CONFIGPROPS_ENDPOINT, 49 | ApiController.ENV_ENDPOINT, ApiController.MAPPINGS_ENDPOINT, 50 | ApiController.METRICS_ENDPOINT, ApiController.SHUTDOWN_ENDPOINT}; 51 | } 52 | 53 | @Override 54 | protected void configure(AuthenticationManagerBuilder auth) throws Exception { 55 | auth.authenticationProvider(domainUsernamePasswordAuthenticationProvider()). 56 | authenticationProvider(backendAdminUsernamePasswordAuthenticationProvider()). 57 | authenticationProvider(tokenAuthenticationProvider()); 58 | } 59 | 60 | @Bean 61 | public TokenService tokenService() { 62 | return new TokenService(); 63 | } 64 | 65 | @Bean 66 | public ExternalServiceAuthenticator someExternalServiceAuthenticator() { 67 | return new SomeExternalServiceAuthenticator(); 68 | } 69 | 70 | @Bean 71 | public AuthenticationProvider domainUsernamePasswordAuthenticationProvider() { 72 | return new DomainUsernamePasswordAuthenticationProvider(tokenService(), someExternalServiceAuthenticator()); 73 | } 74 | 75 | @Bean 76 | public AuthenticationProvider backendAdminUsernamePasswordAuthenticationProvider() { 77 | return new BackendAdminUsernamePasswordAuthenticationProvider(); 78 | } 79 | 80 | @Bean 81 | public AuthenticationProvider tokenAuthenticationProvider() { 82 | return new TokenAuthenticationProvider(tokenService()); 83 | } 84 | 85 | @Bean 86 | public AuthenticationEntryPoint unauthorizedEntryPoint() { 87 | return (request, response, authException) -> response.sendError(HttpServletResponse.SC_UNAUTHORIZED); 88 | } 89 | } -------------------------------------------------------------------------------- /src/main/java/com/futureprocessing/spring/infrastructure/security/TokenAuthenticationProvider.java: -------------------------------------------------------------------------------- 1 | package com.futureprocessing.spring.infrastructure.security; 2 | 3 | import com.google.common.base.Optional; 4 | import org.springframework.security.authentication.AuthenticationProvider; 5 | import org.springframework.security.authentication.BadCredentialsException; 6 | import org.springframework.security.core.Authentication; 7 | import org.springframework.security.core.AuthenticationException; 8 | import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken; 9 | 10 | public class TokenAuthenticationProvider implements AuthenticationProvider { 11 | 12 | private TokenService tokenService; 13 | 14 | public TokenAuthenticationProvider(TokenService tokenService) { 15 | this.tokenService = tokenService; 16 | } 17 | 18 | @Override 19 | public Authentication authenticate(Authentication authentication) throws AuthenticationException { 20 | Optional token = (Optional) authentication.getPrincipal(); 21 | if (!token.isPresent() || token.get().isEmpty()) { 22 | throw new BadCredentialsException("Invalid token"); 23 | } 24 | if (!tokenService.contains(token.get())) { 25 | throw new BadCredentialsException("Invalid token or token expired"); 26 | } 27 | return tokenService.retrieve(token.get()); 28 | } 29 | 30 | @Override 31 | public boolean supports(Class authentication) { 32 | return authentication.equals(PreAuthenticatedAuthenticationToken.class); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/com/futureprocessing/spring/infrastructure/security/TokenResponse.java: -------------------------------------------------------------------------------- 1 | package com.futureprocessing.spring.infrastructure.security; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | 5 | public class TokenResponse { 6 | @JsonProperty 7 | private String token; 8 | 9 | public TokenResponse() { 10 | } 11 | 12 | public TokenResponse(String token) { 13 | this.token = token; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/futureprocessing/spring/infrastructure/security/TokenService.java: -------------------------------------------------------------------------------- 1 | package com.futureprocessing.spring.infrastructure.security; 2 | 3 | import net.sf.ehcache.Cache; 4 | import net.sf.ehcache.CacheManager; 5 | import net.sf.ehcache.Element; 6 | import org.slf4j.Logger; 7 | import org.slf4j.LoggerFactory; 8 | import org.springframework.scheduling.annotation.Scheduled; 9 | import org.springframework.security.core.Authentication; 10 | 11 | import java.util.UUID; 12 | 13 | public class TokenService { 14 | 15 | private static final Logger logger = LoggerFactory.getLogger(TokenService.class); 16 | private static final Cache restApiAuthTokenCache = CacheManager.getInstance().getCache("restApiAuthTokenCache"); 17 | public static final int HALF_AN_HOUR_IN_MILLISECONDS = 30 * 60 * 1000; 18 | 19 | @Scheduled(fixedRate = HALF_AN_HOUR_IN_MILLISECONDS) 20 | public void evictExpiredTokens() { 21 | logger.info("Evicting expired tokens"); 22 | restApiAuthTokenCache.evictExpiredElements(); 23 | } 24 | 25 | public String generateNewToken() { 26 | return UUID.randomUUID().toString(); 27 | } 28 | 29 | public void store(String token, Authentication authentication) { 30 | restApiAuthTokenCache.put(new Element(token, authentication)); 31 | } 32 | 33 | public boolean contains(String token) { 34 | return restApiAuthTokenCache.get(token) != null; 35 | } 36 | 37 | public Authentication retrieve(String token) { 38 | return (Authentication) restApiAuthTokenCache.get(token).getObjectValue(); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | server.port=8443 2 | server.session-timeout=60 3 | security.basic.enabled=false 4 | endpoints.shutdown.enabled=true 5 | keystore.file=src/main/resources/private/keystorejks 6 | keystore.pass=password 7 | backend.admin.username=backend_admin 8 | backend.admin.password=remember_to_change_me_by_external_property_on_deploy 9 | backend.admin.role=BACKEND_ADMIN -------------------------------------------------------------------------------- /src/main/resources/ehcache.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 10 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | .%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg %n 6 | 7 | 8 | 9 | TRACE 10 | 11 | 12 | 13 | 14 | 15 | 16 | ${user.home}/logs/spring_security_test.%d{yyyy-MM-dd}.log 17 | 18 | 90 19 | 20 | 21 | .%d{HH:mm:ss.SSS} [%thread] [user="%X{user}" token="%X{token}"] %-5level %logger{36} - %msg %n 22 | 23 | 24 | 25 | TRACE 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /src/main/resources/private/keystorejks: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FutureProcessing/spring-boot-security-example/7006da9dc087da461824be3440a626349e4e128a/src/main/resources/private/keystorejks -------------------------------------------------------------------------------- /src/test/java/com/futureprocessing/integration/SecurityTest.java: -------------------------------------------------------------------------------- 1 | package com.futureprocessing.integration; 2 | 3 | import com.futureprocessing.spring.Application; 4 | import com.futureprocessing.spring.api.ApiController; 5 | import com.futureprocessing.spring.api.samplestuff.ServiceGateway; 6 | import com.futureprocessing.spring.infrastructure.AuthenticatedExternalWebService; 7 | import com.futureprocessing.spring.infrastructure.security.ExternalServiceAuthenticator; 8 | import com.jayway.restassured.RestAssured; 9 | import com.jayway.restassured.response.ValidatableResponse; 10 | import org.junit.Before; 11 | import org.junit.Test; 12 | import org.junit.runner.RunWith; 13 | import org.mockito.BDDMockito; 14 | import org.mockito.Mockito; 15 | import org.springframework.beans.factory.annotation.Autowired; 16 | import org.springframework.beans.factory.annotation.Value; 17 | import org.springframework.boot.test.IntegrationTest; 18 | import org.springframework.boot.test.SpringApplicationConfiguration; 19 | import org.springframework.context.annotation.Bean; 20 | import org.springframework.context.annotation.Configuration; 21 | import org.springframework.context.annotation.Primary; 22 | import org.springframework.http.HttpStatus; 23 | import org.springframework.security.authentication.BadCredentialsException; 24 | import org.springframework.security.core.authority.AuthorityUtils; 25 | import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; 26 | import org.springframework.test.context.web.WebAppConfiguration; 27 | 28 | import static com.jayway.restassured.RestAssured.given; 29 | import static com.jayway.restassured.RestAssured.when; 30 | import static org.hamcrest.core.IsEqual.equalTo; 31 | import static org.mockito.Matchers.anyString; 32 | import static org.mockito.Matchers.eq; 33 | import static org.mockito.Mockito.mock; 34 | 35 | @RunWith(SpringJUnit4ClassRunner.class) 36 | @SpringApplicationConfiguration(classes = {Application.class, SecurityTest.SecurityTestConfig.class}) 37 | @WebAppConfiguration 38 | @IntegrationTest("server.port:0") 39 | public class SecurityTest { 40 | 41 | private static final String X_AUTH_USERNAME = "X-Auth-Username"; 42 | private static final String X_AUTH_PASSWORD = "X-Auth-Password"; 43 | private static final String X_AUTH_TOKEN = "X-Auth-Token"; 44 | 45 | @Value("${local.server.port}") 46 | int port; 47 | 48 | @Value("${keystore.file}") 49 | String keystoreFile; 50 | 51 | @Value("${keystore.pass}") 52 | String keystorePass; 53 | 54 | @Autowired 55 | ExternalServiceAuthenticator mockedExternalServiceAuthenticator; 56 | 57 | @Autowired 58 | ServiceGateway mockedServiceGateway; 59 | 60 | @Configuration 61 | public static class SecurityTestConfig { 62 | @Bean 63 | public ExternalServiceAuthenticator someExternalServiceAuthenticator() { 64 | return mock(ExternalServiceAuthenticator.class); 65 | } 66 | 67 | @Bean 68 | @Primary 69 | public ServiceGateway serviceGateway() { 70 | return mock(ServiceGateway.class); 71 | } 72 | } 73 | 74 | @Before 75 | public void setup() { 76 | RestAssured.baseURI = "https://localhost"; 77 | RestAssured.keystore(keystoreFile, keystorePass); 78 | RestAssured.port = port; 79 | Mockito.reset(mockedExternalServiceAuthenticator, mockedServiceGateway); 80 | } 81 | 82 | @Test 83 | public void healthEndpoint_isAvailableToEveryone() { 84 | when().get("/health"). 85 | then().statusCode(HttpStatus.OK.value()).body("status", equalTo("UP")); 86 | } 87 | 88 | @Test 89 | public void metricsEndpoint_withoutBackendAdminCredentials_returnsUnauthorized() { 90 | when().get("/metrics"). 91 | then().statusCode(HttpStatus.UNAUTHORIZED.value()); 92 | } 93 | 94 | @Test 95 | public void metricsEndpoint_withInvalidBackendAdminCredentials_returnsUnauthorized() { 96 | String username = "test_user_2"; 97 | String password = "InvalidPassword"; 98 | given().header(X_AUTH_USERNAME, username).header(X_AUTH_PASSWORD, password). 99 | when().get("/metrics"). 100 | then().statusCode(HttpStatus.UNAUTHORIZED.value()); 101 | } 102 | 103 | @Test 104 | public void metricsEndpoint_withCorrectBackendAdminCredentials_returnsOk() { 105 | String username = "backend_admin"; 106 | String password = "remember_to_change_me_by_external_property_on_deploy"; 107 | given().header(X_AUTH_USERNAME, username).header(X_AUTH_PASSWORD, password). 108 | when().get("/metrics"). 109 | then().statusCode(HttpStatus.OK.value()); 110 | } 111 | 112 | @Test 113 | public void authenticate_withoutPassword_returnsUnauthorized() { 114 | given().header(X_AUTH_USERNAME, "SomeUser"). 115 | when().post(ApiController.AUTHENTICATE_URL). 116 | then().statusCode(HttpStatus.UNAUTHORIZED.value()); 117 | 118 | BDDMockito.verifyNoMoreInteractions(mockedExternalServiceAuthenticator); 119 | } 120 | 121 | @Test 122 | public void authenticate_withoutUsername_returnsUnauthorized() { 123 | given().header(X_AUTH_PASSWORD, "SomePassword"). 124 | when().post(ApiController.AUTHENTICATE_URL). 125 | then().statusCode(HttpStatus.UNAUTHORIZED.value()); 126 | 127 | BDDMockito.verifyNoMoreInteractions(mockedExternalServiceAuthenticator); 128 | } 129 | 130 | @Test 131 | public void authenticate_withoutUsernameAndPassword_returnsUnauthorized() { 132 | when().post(ApiController.AUTHENTICATE_URL). 133 | then().statusCode(HttpStatus.UNAUTHORIZED.value()); 134 | 135 | BDDMockito.verifyNoMoreInteractions(mockedExternalServiceAuthenticator); 136 | } 137 | 138 | @Test 139 | public void authenticate_withValidUsernameAndPassword_returnsToken() { 140 | authenticateByUsernameAndPasswordAndGetToken(); 141 | } 142 | 143 | @Test 144 | public void authenticate_withInvalidUsernameOrPassword_returnsUnauthorized() { 145 | String username = "test_user_2"; 146 | String password = "InvalidPassword"; 147 | 148 | BDDMockito.when(mockedExternalServiceAuthenticator.authenticate(anyString(), anyString())). 149 | thenThrow(new BadCredentialsException("Invalid Credentials")); 150 | 151 | given().header(X_AUTH_USERNAME, username).header(X_AUTH_PASSWORD, password). 152 | when().post(ApiController.AUTHENTICATE_URL). 153 | then().statusCode(HttpStatus.UNAUTHORIZED.value()); 154 | } 155 | 156 | @Test 157 | public void gettingStuff_withoutToken_returnsUnauthorized() { 158 | when().get(ApiController.STUFF_URL). 159 | then().statusCode(HttpStatus.UNAUTHORIZED.value()); 160 | } 161 | 162 | @Test 163 | public void gettingStuff_withInvalidToken_returnsUnathorized() { 164 | given().header(X_AUTH_TOKEN, "InvalidToken"). 165 | when().get(ApiController.STUFF_URL). 166 | then().statusCode(HttpStatus.UNAUTHORIZED.value()); 167 | } 168 | 169 | @Test 170 | public void gettingStuff_withValidToken_returnsData() { 171 | String generatedToken = authenticateByUsernameAndPasswordAndGetToken(); 172 | 173 | given().header(X_AUTH_TOKEN, generatedToken). 174 | when().get(ApiController.STUFF_URL). 175 | then().statusCode(HttpStatus.OK.value()); 176 | } 177 | 178 | private String authenticateByUsernameAndPasswordAndGetToken() { 179 | String username = "test_user_2"; 180 | String password = "ValidPassword"; 181 | 182 | AuthenticatedExternalWebService authenticationWithToken = new AuthenticatedExternalWebService(username, null, 183 | AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_DOMAIN_USER")); 184 | BDDMockito.when(mockedExternalServiceAuthenticator.authenticate(eq(username), eq(password))). 185 | thenReturn(authenticationWithToken); 186 | 187 | ValidatableResponse validatableResponse = given().header(X_AUTH_USERNAME, username). 188 | header(X_AUTH_PASSWORD, password). 189 | when().post(ApiController.AUTHENTICATE_URL). 190 | then().statusCode(HttpStatus.OK.value()); 191 | String generatedToken = authenticationWithToken.getToken(); 192 | validatableResponse.body("token", equalTo(generatedToken)); 193 | 194 | return generatedToken; 195 | } 196 | } 197 | --------------------------------------------------------------------------------