diff --git a/servlet/spring-boot/java/jwt/login/README.adoc b/servlet/spring-boot/java/jwt/login/README.adoc new file mode 100644 index 0000000..061244a --- /dev/null +++ b/servlet/spring-boot/java/jwt/login/README.adoc @@ -0,0 +1,42 @@ += JWT Login Sample + +This sample demonstrates how to accept JWTs without using a separate authorization server. + +This approach is useful in REST APIs when a user would like to locally authenticate with a username and password and then use a JWT thereafter. + +[[usage]] +To use the application, first run it: + +```bash +./gradlew :servlet:spring-boot:java:jwt:login:bootRun +``` + +If you `POST` to the `/token` endpoint with the user `user/password`: + +```bash +curl -XPOST user:password@localhost:8080/token +``` + +Then the application responds with something similar to the following: + +```bash +eyJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJzZWxmIiwic3ViIjoidXNlciIsImV4cCI6MTYwNDA0MzA1MSwiaWF0IjoxNjA0MDA3MDUxfQ.yDF_JgSwl5sk21CF7AE1AYbYzRd5YYqe3MIgSWpgN0t2UqsjaaEDhmmICKizt-_0iZy8nkEpNnvgqv5bOHDhs7AXlYS1pg8dgPKuyfkhyVIKa3DhuGyb7tFjwJxHpr128BXf1Dbq-p7Njy46tbKsZhP5zGTjdXlqlAhR4Bl5Fxaxr7D0gdTVBVTlUp9DCy6l-pTBpsvHxShkjXJ0GHVpIZdB-c2e_K9PfTW5MDPcHekG9djnWPSEy-fRvKzTsyVFhdy-X3NXQWWkjFv9bNarV-bhxMlzqhujuaeXJGEqUZlkhBxTsqFr1N7XVcmhs3ECdjEyun2fUSge4BoC7budsQ +``` + +So, next, request the token and export it: + +```bash +export TOKEN=`curl -XPOST user:password@localhost:8080/token` +``` + +Finally, request `/`, including the bearer token for authentication: + +```bash +curl -H "Authorization: Bearer $TOKEN" localhost:8080 && echo +``` + +You should see a response like: + +```bash +Hello, user! +``` \ No newline at end of file diff --git a/servlet/spring-boot/java/jwt/login/build.gradle b/servlet/spring-boot/java/jwt/login/build.gradle new file mode 100644 index 0000000..96b6762 --- /dev/null +++ b/servlet/spring-boot/java/jwt/login/build.gradle @@ -0,0 +1,22 @@ +plugins { + id 'org.springframework.boot' version '2.3.3.RELEASE' + id 'io.spring.dependency-management' version '1.0.10.RELEASE' + id 'java' +} + +repositories { + mavenCentral() + maven { url "https://repo.spring.io/snapshot" } +} + +dependencies { + implementation 'org.springframework.boot:spring-boot-starter-oauth2-resource-server' + implementation 'org.springframework.boot:spring-boot-starter-web' + + testImplementation 'org.springframework.boot:spring-boot-starter-test' + testImplementation 'org.springframework.security:spring-security-test' +} + +tasks.withType(Test).configureEach { + useJUnitPlatform() +} diff --git a/servlet/spring-boot/java/jwt/login/gradle.properties b/servlet/spring-boot/java/jwt/login/gradle.properties new file mode 100644 index 0000000..ffc10d7 --- /dev/null +++ b/servlet/spring-boot/java/jwt/login/gradle.properties @@ -0,0 +1,2 @@ +version=5.5.0-SNAPSHOT +spring-security.version=5.5.0-SNAPSHOT diff --git a/servlet/spring-boot/java/jwt/login/gradle/wrapper/gradle-wrapper.jar b/servlet/spring-boot/java/jwt/login/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..62d4c05 Binary files /dev/null and b/servlet/spring-boot/java/jwt/login/gradle/wrapper/gradle-wrapper.jar differ diff --git a/servlet/spring-boot/java/jwt/login/gradle/wrapper/gradle-wrapper.properties b/servlet/spring-boot/java/jwt/login/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..be52383 --- /dev/null +++ b/servlet/spring-boot/java/jwt/login/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/servlet/spring-boot/java/jwt/login/gradlew b/servlet/spring-boot/java/jwt/login/gradlew new file mode 100755 index 0000000..fbd7c51 --- /dev/null +++ b/servlet/spring-boot/java/jwt/login/gradlew @@ -0,0 +1,185 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/servlet/spring-boot/java/jwt/login/gradlew.bat b/servlet/spring-boot/java/jwt/login/gradlew.bat new file mode 100644 index 0000000..a9f778a --- /dev/null +++ b/servlet/spring-boot/java/jwt/login/gradlew.bat @@ -0,0 +1,104 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/servlet/spring-boot/java/jwt/login/settings.gradle b/servlet/spring-boot/java/jwt/login/settings.gradle new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/servlet/spring-boot/java/jwt/login/settings.gradle @@ -0,0 +1 @@ + diff --git a/servlet/spring-boot/java/jwt/login/src/main/java/example/JwtLoginApplication.java b/servlet/spring-boot/java/jwt/login/src/main/java/example/JwtLoginApplication.java new file mode 100644 index 0000000..215e65a --- /dev/null +++ b/servlet/spring-boot/java/jwt/login/src/main/java/example/JwtLoginApplication.java @@ -0,0 +1,34 @@ +/* + * Copyright 2002-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package example; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * The application entry point. + * + * @author Josh Cummings + */ +@SpringBootApplication +public class JwtLoginApplication { + + public static void main(String[] args) { + SpringApplication.run(JwtLoginApplication.class, args); + } + +} diff --git a/servlet/spring-boot/java/jwt/login/src/main/java/example/RestConfig.java b/servlet/spring-boot/java/jwt/login/src/main/java/example/RestConfig.java new file mode 100644 index 0000000..f652560 --- /dev/null +++ b/servlet/spring-boot/java/jwt/login/src/main/java/example/RestConfig.java @@ -0,0 +1,80 @@ +/* + * Copyright 2002-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package example; + +import java.security.interfaces.RSAPublicKey; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.Customizer; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.config.annotation.web.configurers.oauth2.server.resource.OAuth2ResourceServerConfigurer; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.oauth2.jwt.JwtDecoder; +import org.springframework.security.oauth2.jwt.NimbusJwtDecoder; +import org.springframework.security.oauth2.server.resource.web.BearerTokenAuthenticationEntryPoint; +import org.springframework.security.oauth2.server.resource.web.access.BearerTokenAccessDeniedHandler; +import org.springframework.security.provisioning.InMemoryUserDetailsManager; + +/** + * Security configuration for the main application. + * + * @author Josh Cummings + */ +@Configuration +public class RestConfig extends WebSecurityConfigurerAdapter { + + @Value("${jwt.public.key}") + RSAPublicKey key; + + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http.authorizeRequests((authz) -> authz.anyRequest().authenticated()) + .csrf((csrf) -> csrf.ignoringAntMatchers("/token")) + .httpBasic(Customizer.withDefaults()) + .oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt) + .sessionManagement((session) -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .exceptionHandling((exceptions) -> exceptions + .authenticationEntryPoint(new BearerTokenAuthenticationEntryPoint()) + .accessDeniedHandler(new BearerTokenAccessDeniedHandler()) + ); + // @formatter:on + } + + @Bean + UserDetailsService users() { + // @formatter:off + return new InMemoryUserDetailsManager( + User.withUsername("user") + .password("{noop}password") + .authorities("app") + .build() + ); + // @formatter:on + } + + @Bean + JwtDecoder jwtDecoder() { + return NimbusJwtDecoder.withPublicKey(this.key).build(); + } + +} diff --git a/servlet/spring-boot/java/jwt/login/src/main/java/example/web/HelloController.java b/servlet/spring-boot/java/jwt/login/src/main/java/example/web/HelloController.java new file mode 100644 index 0000000..4b44dea --- /dev/null +++ b/servlet/spring-boot/java/jwt/login/src/main/java/example/web/HelloController.java @@ -0,0 +1,36 @@ +/* + * Copyright 2002-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package example.web; + +import org.springframework.security.core.Authentication; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * A controller for the hello resource. + * + * @author Josh Cummings + */ +@RestController +public class HelloController { + + @GetMapping("/") + public String hello(Authentication authentication) { + return "Hello, " + authentication.getName() + "!"; + } + +} diff --git a/servlet/spring-boot/java/jwt/login/src/main/java/example/web/TokenController.java b/servlet/spring-boot/java/jwt/login/src/main/java/example/web/TokenController.java new file mode 100644 index 0000000..5205422 --- /dev/null +++ b/servlet/spring-boot/java/jwt/login/src/main/java/example/web/TokenController.java @@ -0,0 +1,78 @@ +/* + * Copyright 2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package example.web; + +import java.security.interfaces.RSAPrivateKey; +import java.time.Instant; +import java.util.Date; +import java.util.stream.Collectors; + +import com.nimbusds.jose.JWSAlgorithm; +import com.nimbusds.jose.JWSHeader; +import com.nimbusds.jose.crypto.RSASSASigner; +import com.nimbusds.jwt.JWTClaimsSet; +import com.nimbusds.jwt.SignedJWT; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * A controller for the token resource. + * + * @author Josh Cummings + */ +@RestController +public class TokenController { + + @Value("${jwt.private.key}") + RSAPrivateKey key; + + @PostMapping("/token") + public String token(Authentication authentication) { + Instant now = Instant.now(); + long expiry = 36000L; + // @formatter:off + String scope = authentication.getAuthorities().stream() + .map(GrantedAuthority::getAuthority) + .collect(Collectors.joining(" ")); + JWTClaimsSet claims = new JWTClaimsSet.Builder() + .issuer("self") + .issueTime(new Date(now.toEpochMilli())) + .expirationTime(new Date(now.plusSeconds(expiry).toEpochMilli())) + .subject(authentication.getName()) + .claim("scope", scope) + .build(); + // @formatter:on + JWSHeader header = new JWSHeader.Builder(JWSAlgorithm.RS256).build(); + SignedJWT jwt = new SignedJWT(header, claims); + return sign(jwt).serialize(); + } + + SignedJWT sign(SignedJWT jwt) { + try { + jwt.sign(new RSASSASigner(this.key)); + return jwt; + } + catch (Exception ex) { + throw new IllegalArgumentException(ex); + } + } + +} diff --git a/servlet/spring-boot/java/jwt/login/src/main/resources/app.key b/servlet/spring-boot/java/jwt/login/src/main/resources/app.key new file mode 100644 index 0000000..5351007 --- /dev/null +++ b/servlet/spring-boot/java/jwt/login/src/main/resources/app.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDcWWomvlNGyQhA +iB0TcN3sP2VuhZ1xNRPxr58lHswC9Cbtdc2hiSbe/sxAvU1i0O8vaXwICdzRZ1JM +g1TohG9zkqqjZDhyw1f1Ic6YR/OhE6NCpqERy97WMFeW6gJd1i5inHj/W19GAbqK +LhSHGHqIjyo0wlBf58t+qFt9h/EFBVE/LAGQBsg/jHUQCxsLoVI2aSELGIw2oSDF +oiljwLaQl0n9khX5ZbiegN3OkqodzCYHwWyu6aVVj8M1W9RIMiKmKr09s/gf31Nc +3WjvjqhFo1rTuurWGgKAxJLL7zlJqAKjGWbIT4P6h/1Kwxjw6X23St3OmhsG6HIn ++jl1++MrAgMBAAECggEBAMf820wop3pyUOwI3aLcaH7YFx5VZMzvqJdNlvpg1jbE +E2Sn66b1zPLNfOIxLcBG8x8r9Ody1Bi2Vsqc0/5o3KKfdgHvnxAB3Z3dPh2WCDek +lCOVClEVoLzziTuuTdGO5/CWJXdWHcVzIjPxmK34eJXioiLaTYqN3XKqKMdpD0ZG +mtNTGvGf+9fQ4i94t0WqIxpMpGt7NM4RHy3+Onggev0zLiDANC23mWrTsUgect/7 +62TYg8g1bKwLAb9wCBT+BiOuCc2wrArRLOJgUkj/F4/gtrR9ima34SvWUyoUaKA0 +bi4YBX9l8oJwFGHbU9uFGEMnH0T/V0KtIB7qetReywkCgYEA9cFyfBIQrYISV/OA ++Z0bo3vh2aL0QgKrSXZ924cLt7itQAHNZ2ya+e3JRlTczi5mnWfjPWZ6eJB/8MlH +Gpn12o/POEkU+XjZZSPe1RWGt5g0S3lWqyx9toCS9ACXcN9tGbaqcFSVI73zVTRA +8J9grR0fbGn7jaTlTX2tnlOTQ60CgYEA5YjYpEq4L8UUMFkuj+BsS3u0oEBnzuHd +I9LEHmN+CMPosvabQu5wkJXLuqo2TxRnAznsA8R3pCLkdPGoWMCiWRAsCn979TdY +QbqO2qvBAD2Q19GtY7lIu6C35/enQWzJUMQE3WW0OvjLzZ0l/9mA2FBRR+3F9A1d +rBdnmv0c3TcCgYEAi2i+ggVZcqPbtgrLOk5WVGo9F1GqUBvlgNn30WWNTx4zIaEk +HSxtyaOLTxtq2odV7Kr3LGiKxwPpn/T+Ief+oIp92YcTn+VfJVGw4Z3BezqbR8lA +Uf/+HF5ZfpMrVXtZD4Igs3I33Duv4sCuqhEvLWTc44pHifVloozNxYfRfU0CgYBN +HXa7a6cJ1Yp829l62QlJKtx6Ymj95oAnQu5Ez2ROiZMqXRO4nucOjGUP55Orac1a +FiGm+mC/skFS0MWgW8evaHGDbWU180wheQ35hW6oKAb7myRHtr4q20ouEtQMdQIF +snV39G1iyqeeAsf7dxWElydXpRi2b68i3BIgzhzebQKBgQCdUQuTsqV9y/JFpu6H +c5TVvhG/ubfBspI5DhQqIGijnVBzFT//UfIYMSKJo75qqBEyP2EJSmCsunWsAFsM +TszuiGTkrKcZy9G0wJqPztZZl2F2+bJgnA6nBEV7g5PA4Af+QSmaIhRwqGDAuROR +47jndeyIaMTNETEmOnms+as17g== +-----END PRIVATE KEY----- \ No newline at end of file diff --git a/servlet/spring-boot/java/jwt/login/src/main/resources/app.pub b/servlet/spring-boot/java/jwt/login/src/main/resources/app.pub new file mode 100644 index 0000000..0b2ee7b --- /dev/null +++ b/servlet/spring-boot/java/jwt/login/src/main/resources/app.pub @@ -0,0 +1,9 @@ +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA3FlqJr5TRskIQIgdE3Dd +7D9lboWdcTUT8a+fJR7MAvQm7XXNoYkm3v7MQL1NYtDvL2l8CAnc0WdSTINU6IRv +c5Kqo2Q4csNX9SHOmEfzoROjQqahEcve1jBXluoCXdYuYpx4/1tfRgG6ii4Uhxh6 +iI8qNMJQX+fLfqhbfYfxBQVRPywBkAbIP4x1EAsbC6FSNmkhCxiMNqEgxaIpY8C2 +kJdJ/ZIV+WW4noDdzpKqHcwmB8FsrumlVY/DNVvUSDIipiq9PbP4H99TXN1o746o +RaNa07rq1hoCgMSSy+85SagCoxlmyE+D+of9SsMY8Ol9t0rdzpobBuhyJ/o5dfvj +KwIDAQAB +-----END PUBLIC KEY----- \ No newline at end of file diff --git a/servlet/spring-boot/java/jwt/login/src/main/resources/application.yml b/servlet/spring-boot/java/jwt/login/src/main/resources/application.yml new file mode 100644 index 0000000..ad5d385 --- /dev/null +++ b/servlet/spring-boot/java/jwt/login/src/main/resources/application.yml @@ -0,0 +1,10 @@ +logging: + level: + root: INFO + org.springframework.web: INFO + org.springframework.security: INFO +# org.springframework.boot.autoconfigure: DEBUG + +jwt: + private.key: classpath:app.key + public.key: classpath:app.pub \ No newline at end of file diff --git a/servlet/spring-boot/java/jwt/login/src/test/java/example/web/HelloControllerTests.java b/servlet/spring-boot/java/jwt/login/src/test/java/example/web/HelloControllerTests.java new file mode 100644 index 0000000..74919f1 --- /dev/null +++ b/servlet/spring-boot/java/jwt/login/src/test/java/example/web/HelloControllerTests.java @@ -0,0 +1,75 @@ +/* + * Copyright 2002-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package example.web; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; + +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * Tests for {@link HelloController} + * + * @author Josh Cummings + */ +@WebMvcTest({ HelloController.class, TokenController.class }) +public class HelloControllerTests { + + @Autowired + MockMvc mvc; + + @Test + void rootWhenAuthenticatedThenSaysHelloUser() throws Exception { + // @formatter:off + MvcResult result = this.mvc.perform(post("/token") + .with(httpBasic("user", "password"))) + .andExpect(status().isOk()) + .andReturn(); + + String token = result.getResponse().getContentAsString(); + + this.mvc.perform(get("/") + .header("Authorization", "Bearer " + token)) + .andExpect(content().string("Hello, user!")); + // @formatter:on + } + + @Test + void rootWhenUnauthenticatedThen401() throws Exception { + // @formatter:off + this.mvc.perform(get("/")) + .andExpect(status().isUnauthorized()); + // @formatter:on + } + + @Test + void tokenWhenBadCredentialsThen401() throws Exception { + // @formatter:off + this.mvc.perform(post("/token")) + .andExpect(status().isUnauthorized()); + // @formatter:on + } + +} diff --git a/settings.gradle b/settings.gradle index 769ccf1..21e2874 100644 --- a/settings.gradle +++ b/settings.gradle @@ -39,6 +39,7 @@ include ":servlet:java-configuration:max-sessions" include ":servlet:spring-boot:java:hello" include ":servlet:spring-boot:java:hello-security" include ":servlet:spring-boot:java:hello-security-explicit" +include ":servlet:spring-boot:java:jwt:login" include ":servlet:spring-boot:java:oauth2:login" include ":servlet:spring-boot:java:oauth2:resource-server:hello-security" include ":servlet:spring-boot:java:oauth2:resource-server:jwe"