From 1d6a1712b41b45b70dcb8f32f0354931888d6bf1 Mon Sep 17 00:00:00 2001 From: "wangziyuan.12" Date: Thu, 7 Dec 2023 15:41:50 -0500 Subject: [PATCH 01/16] init authentication-service --- @core/authentication-service/.gitignore | 33 ++ .../.mvn/wrapper/maven-wrapper.jar | Bin 0 -> 62547 bytes .../.mvn/wrapper/maven-wrapper.properties | 2 + @core/authentication-service/mvnw | 308 ++++++++++++++++++ @core/authentication-service/mvnw.cmd | 205 ++++++++++++ @core/authentication-service/pom.xml | 41 +++ .../AuthenticationServiceApplication.java | 13 + .../src/main/resources/application.properties | 1 + ...AuthenticationServiceApplicationTests.java | 13 + 9 files changed, 616 insertions(+) create mode 100644 @core/authentication-service/.gitignore create mode 100644 @core/authentication-service/.mvn/wrapper/maven-wrapper.jar create mode 100644 @core/authentication-service/.mvn/wrapper/maven-wrapper.properties create mode 100755 @core/authentication-service/mvnw create mode 100644 @core/authentication-service/mvnw.cmd create mode 100644 @core/authentication-service/pom.xml create mode 100644 @core/authentication-service/src/main/java/org/flowio/authenticationservice/AuthenticationServiceApplication.java create mode 100644 @core/authentication-service/src/main/resources/application.properties create mode 100644 @core/authentication-service/src/test/java/org/flowio/authenticationservice/AuthenticationServiceApplicationTests.java diff --git a/@core/authentication-service/.gitignore b/@core/authentication-service/.gitignore new file mode 100644 index 0000000..549e00a --- /dev/null +++ b/@core/authentication-service/.gitignore @@ -0,0 +1,33 @@ +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ diff --git a/@core/authentication-service/.mvn/wrapper/maven-wrapper.jar b/@core/authentication-service/.mvn/wrapper/maven-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..cb28b0e37c7d206feb564310fdeec0927af4123a GIT binary patch literal 62547 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 A?*IS* literal 0 HcmV?d00001 diff --git a/@core/authentication-service/.mvn/wrapper/maven-wrapper.properties b/@core/authentication-service/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..5f0536e --- /dev/null +++ b/@core/authentication-service/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,2 @@ +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.5/apache-maven-3.9.5-bin.zip +wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar diff --git a/@core/authentication-service/mvnw b/@core/authentication-service/mvnw new file mode 100755 index 0000000..66df285 --- /dev/null +++ b/@core/authentication-service/mvnw @@ -0,0 +1,308 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Apache Maven Wrapper startup batch script, version 3.2.0 +# +# Required ENV vars: +# ------------------ +# JAVA_HOME - location of a JDK home dir +# +# Optional ENV vars +# ----------------- +# MAVEN_OPTS - parameters passed to the Java VM when running Maven +# e.g. to debug Maven itself, use +# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# ---------------------------------------------------------------------------- + +if [ -z "$MAVEN_SKIP_RC" ] ; then + + if [ -f /usr/local/etc/mavenrc ] ; then + . /usr/local/etc/mavenrc + fi + + if [ -f /etc/mavenrc ] ; then + . /etc/mavenrc + fi + + if [ -f "$HOME/.mavenrc" ] ; then + . "$HOME/.mavenrc" + fi + +fi + +# OS specific support. $var _must_ be set to either true or false. +cygwin=false; +darwin=false; +mingw=false +case "$(uname)" in + CYGWIN*) cygwin=true ;; + MINGW*) mingw=true;; + Darwin*) darwin=true + # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home + # See https://developer.apple.com/library/mac/qa/qa1170/_index.html + if [ -z "$JAVA_HOME" ]; then + if [ -x "/usr/libexec/java_home" ]; then + JAVA_HOME="$(/usr/libexec/java_home)"; export JAVA_HOME + else + JAVA_HOME="/Library/Java/Home"; export JAVA_HOME + fi + fi + ;; +esac + +if [ -z "$JAVA_HOME" ] ; then + if [ -r /etc/gentoo-release ] ; then + JAVA_HOME=$(java-config --jre-home) + fi +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched +if $cygwin ; then + [ -n "$JAVA_HOME" ] && + JAVA_HOME=$(cygpath --unix "$JAVA_HOME") + [ -n "$CLASSPATH" ] && + CLASSPATH=$(cygpath --path --unix "$CLASSPATH") +fi + +# For Mingw, ensure paths are in UNIX format before anything is touched +if $mingw ; then + [ -n "$JAVA_HOME" ] && [ -d "$JAVA_HOME" ] && + JAVA_HOME="$(cd "$JAVA_HOME" || (echo "cannot cd into $JAVA_HOME."; exit 1); pwd)" +fi + +if [ -z "$JAVA_HOME" ]; then + javaExecutable="$(which javac)" + if [ -n "$javaExecutable" ] && ! [ "$(expr "\"$javaExecutable\"" : '\([^ ]*\)')" = "no" ]; then + # readlink(1) is not available as standard on Solaris 10. + readLink=$(which readlink) + if [ ! "$(expr "$readLink" : '\([^ ]*\)')" = "no" ]; then + if $darwin ; then + javaHome="$(dirname "\"$javaExecutable\"")" + javaExecutable="$(cd "\"$javaHome\"" && pwd -P)/javac" + else + javaExecutable="$(readlink -f "\"$javaExecutable\"")" + fi + javaHome="$(dirname "\"$javaExecutable\"")" + javaHome=$(expr "$javaHome" : '\(.*\)/bin') + JAVA_HOME="$javaHome" + export JAVA_HOME + fi + fi +fi + +if [ -z "$JAVACMD" ] ; then + 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 + else + JAVACMD="$(\unset -f command 2>/dev/null; \command -v java)" + fi +fi + +if [ ! -x "$JAVACMD" ] ; then + echo "Error: JAVA_HOME is not defined correctly." >&2 + echo " We cannot execute $JAVACMD" >&2 + exit 1 +fi + +if [ -z "$JAVA_HOME" ] ; then + echo "Warning: JAVA_HOME environment variable is not set." +fi + +# traverses directory structure from process work directory to filesystem root +# first directory with .mvn subdirectory is considered project base directory +find_maven_basedir() { + if [ -z "$1" ] + then + echo "Path not specified to find_maven_basedir" + return 1 + fi + + basedir="$1" + wdir="$1" + while [ "$wdir" != '/' ] ; do + if [ -d "$wdir"/.mvn ] ; then + basedir=$wdir + break + fi + # workaround for JBEAP-8937 (on Solaris 10/Sparc) + if [ -d "${wdir}" ]; then + wdir=$(cd "$wdir/.." || exit 1; pwd) + fi + # end of workaround + done + printf '%s' "$(cd "$basedir" || exit 1; pwd)" +} + +# concatenates all lines of a file +concat_lines() { + if [ -f "$1" ]; then + # Remove \r in case we run on Windows within Git Bash + # and check out the repository with auto CRLF management + # enabled. Otherwise, we may read lines that are delimited with + # \r\n and produce $'-Xarg\r' rather than -Xarg due to word + # splitting rules. + tr -s '\r\n' ' ' < "$1" + fi +} + +log() { + if [ "$MVNW_VERBOSE" = true ]; then + printf '%s\n' "$1" + fi +} + +BASE_DIR=$(find_maven_basedir "$(dirname "$0")") +if [ -z "$BASE_DIR" ]; then + exit 1; +fi + +MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}; export MAVEN_PROJECTBASEDIR +log "$MAVEN_PROJECTBASEDIR" + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +wrapperJarPath="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" +if [ -r "$wrapperJarPath" ]; then + log "Found $wrapperJarPath" +else + log "Couldn't find $wrapperJarPath, downloading it ..." + + if [ -n "$MVNW_REPOURL" ]; then + wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + else + wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + fi + while IFS="=" read -r key value; do + # Remove '\r' from value to allow usage on windows as IFS does not consider '\r' as a separator ( considers space, tab, new line ('\n'), and custom '=' ) + safeValue=$(echo "$value" | tr -d '\r') + case "$key" in (wrapperUrl) wrapperUrl="$safeValue"; break ;; + esac + done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" + log "Downloading from: $wrapperUrl" + + if $cygwin; then + wrapperJarPath=$(cygpath --path --windows "$wrapperJarPath") + fi + + if command -v wget > /dev/null; then + log "Found wget ... using wget" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--quiet" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + wget $QUIET "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + else + wget $QUIET --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + fi + elif command -v curl > /dev/null; then + log "Found curl ... using curl" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--silent" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + curl $QUIET -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + else + curl $QUIET --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + fi + else + log "Falling back to using Java to download" + javaSource="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.java" + javaClass="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.class" + # For Cygwin, switch paths to Windows format before running javac + if $cygwin; then + javaSource=$(cygpath --path --windows "$javaSource") + javaClass=$(cygpath --path --windows "$javaClass") + fi + if [ -e "$javaSource" ]; then + if [ ! -e "$javaClass" ]; then + log " - Compiling MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/javac" "$javaSource") + fi + if [ -e "$javaClass" ]; then + log " - Running MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$wrapperUrl" "$wrapperJarPath") || rm -f "$wrapperJarPath" + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +# If specified, validate the SHA-256 sum of the Maven wrapper jar file +wrapperSha256Sum="" +while IFS="=" read -r key value; do + case "$key" in (wrapperSha256Sum) wrapperSha256Sum=$value; break ;; + esac +done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" +if [ -n "$wrapperSha256Sum" ]; then + wrapperSha256Result=false + if command -v sha256sum > /dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | sha256sum -c > /dev/null 2>&1; then + wrapperSha256Result=true + fi + elif command -v shasum > /dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | shasum -a 256 -c > /dev/null 2>&1; then + wrapperSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." + echo "Please install either command, or disable validation by removing 'wrapperSha256Sum' from your maven-wrapper.properties." + exit 1 + fi + if [ $wrapperSha256Result = false ]; then + echo "Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised." >&2 + echo "Investigate or delete $wrapperJarPath to attempt a clean download." >&2 + echo "If you updated your Maven version, you need to update the specified wrapperSha256Sum property." >&2 + exit 1 + fi +fi + +MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" + +# For Cygwin, switch paths to Windows format before running java +if $cygwin; then + [ -n "$JAVA_HOME" ] && + JAVA_HOME=$(cygpath --path --windows "$JAVA_HOME") + [ -n "$CLASSPATH" ] && + CLASSPATH=$(cygpath --path --windows "$CLASSPATH") + [ -n "$MAVEN_PROJECTBASEDIR" ] && + MAVEN_PROJECTBASEDIR=$(cygpath --path --windows "$MAVEN_PROJECTBASEDIR") +fi + +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $*" +export MAVEN_CMD_LINE_ARGS + +WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +# shellcheck disable=SC2086 # safe args +exec "$JAVACMD" \ + $MAVEN_OPTS \ + $MAVEN_DEBUG_OPTS \ + -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ + "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/@core/authentication-service/mvnw.cmd b/@core/authentication-service/mvnw.cmd new file mode 100644 index 0000000..95ba6f5 --- /dev/null +++ b/@core/authentication-service/mvnw.cmd @@ -0,0 +1,205 @@ +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. 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, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Apache Maven Wrapper startup batch script, version 3.2.0 +@REM +@REM Required ENV vars: +@REM JAVA_HOME - location of a JDK home dir +@REM +@REM Optional ENV vars +@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending +@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven +@REM e.g. to debug Maven itself, use +@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM ---------------------------------------------------------------------------- + +@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' +@echo off +@REM set title of command window +title %0 +@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' +@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% + +@REM set %HOME% to equivalent of $HOME +if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") + +@REM Execute a user defined script before this one +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre +@REM check for pre script, once with legacy .bat ending and once with .cmd ending +if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* +if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* +:skipRcPre + +@setlocal + +set ERROR_CODE=0 + +@REM To isolate internal variables from possible post scripts, we use another setlocal +@setlocal + +@REM ==== START VALIDATION ==== +if not "%JAVA_HOME%" == "" goto OkJHome + +echo. +echo Error: JAVA_HOME not found in your environment. >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +:OkJHome +if exist "%JAVA_HOME%\bin\java.exe" goto init + +echo. +echo Error: JAVA_HOME is set to an invalid directory. >&2 +echo JAVA_HOME = "%JAVA_HOME%" >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +@REM ==== END VALIDATION ==== + +:init + +@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". +@REM Fallback to current working directory if not found. + +set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% +IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir + +set EXEC_DIR=%CD% +set WDIR=%EXEC_DIR% +:findBaseDir +IF EXIST "%WDIR%"\.mvn goto baseDirFound +cd .. +IF "%WDIR%"=="%CD%" goto baseDirNotFound +set WDIR=%CD% +goto findBaseDir + +:baseDirFound +set MAVEN_PROJECTBASEDIR=%WDIR% +cd "%EXEC_DIR%" +goto endDetectBaseDir + +:baseDirNotFound +set MAVEN_PROJECTBASEDIR=%EXEC_DIR% +cd "%EXEC_DIR%" + +:endDetectBaseDir + +IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig + +@setlocal EnableExtensions EnableDelayedExpansion +for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a +@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% + +:endReadAdditionalConfig + +SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" +set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" +set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperUrl" SET WRAPPER_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + if "%MVNW_VERBOSE%" == "true" ( + echo Found %WRAPPER_JAR% + ) +) else ( + if not "%MVNW_REPOURL%" == "" ( + SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + ) + if "%MVNW_VERBOSE%" == "true" ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %WRAPPER_URL% + ) + + powershell -Command "&{"^ + "$webclient = new-object System.Net.WebClient;"^ + "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ + "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ + "}"^ + "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%WRAPPER_URL%', '%WRAPPER_JAR%')"^ + "}" + if "%MVNW_VERBOSE%" == "true" ( + echo Finished downloading %WRAPPER_JAR% + ) +) +@REM End of extension + +@REM If specified, validate the SHA-256 sum of the Maven wrapper jar file +SET WRAPPER_SHA_256_SUM="" +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperSha256Sum" SET WRAPPER_SHA_256_SUM=%%B +) +IF NOT %WRAPPER_SHA_256_SUM%=="" ( + powershell -Command "&{"^ + "$hash = (Get-FileHash \"%WRAPPER_JAR%\" -Algorithm SHA256).Hash.ToLower();"^ + "If('%WRAPPER_SHA_256_SUM%' -ne $hash){"^ + " Write-Output 'Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised.';"^ + " Write-Output 'Investigate or delete %WRAPPER_JAR% to attempt a clean download.';"^ + " Write-Output 'If you updated your Maven version, you need to update the specified wrapperSha256Sum property.';"^ + " exit 1;"^ + "}"^ + "}" + if ERRORLEVEL 1 goto error +) + +@REM Provide a "standardized" way to retrieve the CLI args that will +@REM work with both Windows and non-Windows executions. +set MAVEN_CMD_LINE_ARGS=%* + +%MAVEN_JAVA_EXE% ^ + %JVM_CONFIG_MAVEN_PROPS% ^ + %MAVEN_OPTS% ^ + %MAVEN_DEBUG_OPTS% ^ + -classpath %WRAPPER_JAR% ^ + "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ + %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +if ERRORLEVEL 1 goto error +goto end + +:error +set ERROR_CODE=1 + +:end +@endlocal & set ERROR_CODE=%ERROR_CODE% + +if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost +@REM check for post script, once with legacy .bat ending and once with .cmd ending +if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" +if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" +:skipRcPost + +@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' +if "%MAVEN_BATCH_PAUSE%"=="on" pause + +if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% + +cmd /C exit /B %ERROR_CODE% diff --git a/@core/authentication-service/pom.xml b/@core/authentication-service/pom.xml new file mode 100644 index 0000000..ba6303b --- /dev/null +++ b/@core/authentication-service/pom.xml @@ -0,0 +1,41 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.2.0 + + + org.flowio + authentication-service + 0.0.1-SNAPSHOT + authentication-service + authentication-service + + 17 + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + diff --git a/@core/authentication-service/src/main/java/org/flowio/authenticationservice/AuthenticationServiceApplication.java b/@core/authentication-service/src/main/java/org/flowio/authenticationservice/AuthenticationServiceApplication.java new file mode 100644 index 0000000..6727cc9 --- /dev/null +++ b/@core/authentication-service/src/main/java/org/flowio/authenticationservice/AuthenticationServiceApplication.java @@ -0,0 +1,13 @@ +package org.flowio.authenticationservice; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class AuthenticationServiceApplication { + + public static void main(String[] args) { + SpringApplication.run(AuthenticationServiceApplication.class, args); + } + +} diff --git a/@core/authentication-service/src/main/resources/application.properties b/@core/authentication-service/src/main/resources/application.properties new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/@core/authentication-service/src/main/resources/application.properties @@ -0,0 +1 @@ + diff --git a/@core/authentication-service/src/test/java/org/flowio/authenticationservice/AuthenticationServiceApplicationTests.java b/@core/authentication-service/src/test/java/org/flowio/authenticationservice/AuthenticationServiceApplicationTests.java new file mode 100644 index 0000000..d461076 --- /dev/null +++ b/@core/authentication-service/src/test/java/org/flowio/authenticationservice/AuthenticationServiceApplicationTests.java @@ -0,0 +1,13 @@ +package org.flowio.authenticationservice; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class AuthenticationServiceApplicationTests { + + @Test + void contextLoads() { + } + +} From bea76cbb6458bd9455447fb30c9f643ca2fcc4c7 Mon Sep 17 00:00:00 2001 From: "wangziyuan.12" Date: Sat, 9 Dec 2023 11:33:14 -0500 Subject: [PATCH 02/16] add Authentication Logic --- @core/authentication-service/pom.xml | 59 +++++++++++++- .../AuthenticationServiceApplication.java | 13 ---- .../Entities/LoginUser.java | 74 ++++++++++++++++++ .../Entities/LoginUserRepository.java | 11 +++ .../authenticationservice/Entities/Role.java | 6 ++ .../auth/AuthenticationController.java | 30 +++++++ .../auth/AuthenticationRequest.java | 19 +++++ .../auth/AuthenticationResponse.java | 17 ++++ .../auth/AuthenticationService.java | 65 ++++++++++++++++ .../auth/RegisterRequest.java | 20 +++++ .../config/ApplicationConfig.java | 50 ++++++++++++ .../config/JWTAuthenticationFilter.java | 64 +++++++++++++++ .../config/JwtService.java | 78 +++++++++++++++++++ .../config/SecurityConfig.java | 45 +++++++++++ .../demo/DemoController.java | 16 ++++ .../src/main/resources/application.properties | 1 - .../src/main/resources/application.yml | 15 ++++ 17 files changed, 567 insertions(+), 16 deletions(-) delete mode 100644 @core/authentication-service/src/main/java/org/flowio/authenticationservice/AuthenticationServiceApplication.java create mode 100644 @core/authentication-service/src/main/java/org/flowio/authenticationservice/Entities/LoginUser.java create mode 100644 @core/authentication-service/src/main/java/org/flowio/authenticationservice/Entities/LoginUserRepository.java create mode 100644 @core/authentication-service/src/main/java/org/flowio/authenticationservice/Entities/Role.java create mode 100644 @core/authentication-service/src/main/java/org/flowio/authenticationservice/auth/AuthenticationController.java create mode 100644 @core/authentication-service/src/main/java/org/flowio/authenticationservice/auth/AuthenticationRequest.java create mode 100644 @core/authentication-service/src/main/java/org/flowio/authenticationservice/auth/AuthenticationResponse.java create mode 100644 @core/authentication-service/src/main/java/org/flowio/authenticationservice/auth/AuthenticationService.java create mode 100644 @core/authentication-service/src/main/java/org/flowio/authenticationservice/auth/RegisterRequest.java create mode 100644 @core/authentication-service/src/main/java/org/flowio/authenticationservice/config/ApplicationConfig.java create mode 100644 @core/authentication-service/src/main/java/org/flowio/authenticationservice/config/JWTAuthenticationFilter.java create mode 100644 @core/authentication-service/src/main/java/org/flowio/authenticationservice/config/JwtService.java create mode 100644 @core/authentication-service/src/main/java/org/flowio/authenticationservice/config/SecurityConfig.java create mode 100644 @core/authentication-service/src/main/java/org/flowio/authenticationservice/demo/DemoController.java delete mode 100644 @core/authentication-service/src/main/resources/application.properties create mode 100644 @core/authentication-service/src/main/resources/application.yml diff --git a/@core/authentication-service/pom.xml b/@core/authentication-service/pom.xml index ba6303b..d98db1d 100644 --- a/@core/authentication-service/pom.xml +++ b/@core/authentication-service/pom.xml @@ -1,6 +1,6 @@ + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 org.springframework.boot @@ -17,25 +17,80 @@ 17 + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.boot + spring-boot-starter-security + org.springframework.boot spring-boot-starter-web + + org.postgresql + postgresql + 42.2.27 + runtime + + + org.projectlombok + lombok + true + + + io.jsonwebtoken + jjwt-api + 0.11.5 + + + io.jsonwebtoken + jjwt-impl + 0.11.5 + + + io.jsonwebtoken + jjwt-jackson + 0.11.5 + org.springframework.boot spring-boot-starter-test test + + org.springframework.security + spring-security-test + test + + + org.codehaus.mojo + exec-maven-plugin + 3.1.1 + + org.flowio.authenticationservice.AuthenticationServiceApplication + + org.springframework.boot spring-boot-maven-plugin + + + + org.projectlombok + lombok + + + - + \ No newline at end of file diff --git a/@core/authentication-service/src/main/java/org/flowio/authenticationservice/AuthenticationServiceApplication.java b/@core/authentication-service/src/main/java/org/flowio/authenticationservice/AuthenticationServiceApplication.java deleted file mode 100644 index 6727cc9..0000000 --- a/@core/authentication-service/src/main/java/org/flowio/authenticationservice/AuthenticationServiceApplication.java +++ /dev/null @@ -1,13 +0,0 @@ -package org.flowio.authenticationservice; - -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; - -@SpringBootApplication -public class AuthenticationServiceApplication { - - public static void main(String[] args) { - SpringApplication.run(AuthenticationServiceApplication.class, args); - } - -} diff --git a/@core/authentication-service/src/main/java/org/flowio/authenticationservice/Entities/LoginUser.java b/@core/authentication-service/src/main/java/org/flowio/authenticationservice/Entities/LoginUser.java new file mode 100644 index 0000000..5d4edb3 --- /dev/null +++ b/@core/authentication-service/src/main/java/org/flowio/authenticationservice/Entities/LoginUser.java @@ -0,0 +1,74 @@ +package org.flowio.authenticationservice.Entities; + +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +import java.util.Collection; +import java.util.List; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Entity +@Table(indexes = { + @Index(name = "idx_loginEmail", columnList = "loginEmail") +}) +// don't use "User" because of the ambiguity to between the postgres one +public class LoginUser implements UserDetails { + @Id + @GeneratedValue + private Long id; + + private String firstname; + private String lastname; + + private String password; + + @Column(name = "loginEmail", unique = true) + private String loginEmail; + + @Enumerated(EnumType.STRING) + private Role role; + + @Override + public Collection getAuthorities() { + return List.of(new SimpleGrantedAuthority(role.name())); + } + + @Override + public String getPassword() { + return password; + } + + @Override + public String getUsername() { + return loginEmail; + } + + @Override + public boolean isAccountNonExpired() { + return true; + } + + @Override + public boolean isAccountNonLocked() { + return true; + } + + @Override + public boolean isCredentialsNonExpired() { + return true; + } + + @Override + public boolean isEnabled() { + return true; + } +} diff --git a/@core/authentication-service/src/main/java/org/flowio/authenticationservice/Entities/LoginUserRepository.java b/@core/authentication-service/src/main/java/org/flowio/authenticationservice/Entities/LoginUserRepository.java new file mode 100644 index 0000000..ed6ef9d --- /dev/null +++ b/@core/authentication-service/src/main/java/org/flowio/authenticationservice/Entities/LoginUserRepository.java @@ -0,0 +1,11 @@ +package org.flowio.authenticationservice.Entities; + +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface LoginUserRepository extends JpaRepository { + Optional findByLoginEmail(String loginEmail); + + +} diff --git a/@core/authentication-service/src/main/java/org/flowio/authenticationservice/Entities/Role.java b/@core/authentication-service/src/main/java/org/flowio/authenticationservice/Entities/Role.java new file mode 100644 index 0000000..6b0cc81 --- /dev/null +++ b/@core/authentication-service/src/main/java/org/flowio/authenticationservice/Entities/Role.java @@ -0,0 +1,6 @@ +package org.flowio.authenticationservice.Entities; + +public enum Role { + USER, + ADMIN +} diff --git a/@core/authentication-service/src/main/java/org/flowio/authenticationservice/auth/AuthenticationController.java b/@core/authentication-service/src/main/java/org/flowio/authenticationservice/auth/AuthenticationController.java new file mode 100644 index 0000000..c745ca6 --- /dev/null +++ b/@core/authentication-service/src/main/java/org/flowio/authenticationservice/auth/AuthenticationController.java @@ -0,0 +1,30 @@ +package org.flowio.authenticationservice.auth; + +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/v1/auth") +@RequiredArgsConstructor +public class AuthenticationController { + + private final AuthenticationService service; + + @PostMapping("/register") + public ResponseEntity register( + @RequestBody RegisterRequest request + ) { + return ResponseEntity.ok(service.register(request)); + } + + @PostMapping("/authenticate") + public ResponseEntity register( + @RequestBody AuthenticationRequest request + ) { + return ResponseEntity.ok(service.authenticate(request)); + } +} diff --git a/@core/authentication-service/src/main/java/org/flowio/authenticationservice/auth/AuthenticationRequest.java b/@core/authentication-service/src/main/java/org/flowio/authenticationservice/auth/AuthenticationRequest.java new file mode 100644 index 0000000..af11ffa --- /dev/null +++ b/@core/authentication-service/src/main/java/org/flowio/authenticationservice/auth/AuthenticationRequest.java @@ -0,0 +1,19 @@ +package org.flowio.authenticationservice.auth; + + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class AuthenticationRequest { + + private String loginEmail; + + private String password; + +} diff --git a/@core/authentication-service/src/main/java/org/flowio/authenticationservice/auth/AuthenticationResponse.java b/@core/authentication-service/src/main/java/org/flowio/authenticationservice/auth/AuthenticationResponse.java new file mode 100644 index 0000000..8e316c3 --- /dev/null +++ b/@core/authentication-service/src/main/java/org/flowio/authenticationservice/auth/AuthenticationResponse.java @@ -0,0 +1,17 @@ +package org.flowio.authenticationservice.auth; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class AuthenticationResponse { + + private String jwtToken; + private String message; + +} diff --git a/@core/authentication-service/src/main/java/org/flowio/authenticationservice/auth/AuthenticationService.java b/@core/authentication-service/src/main/java/org/flowio/authenticationservice/auth/AuthenticationService.java new file mode 100644 index 0000000..8c27dcb --- /dev/null +++ b/@core/authentication-service/src/main/java/org/flowio/authenticationservice/auth/AuthenticationService.java @@ -0,0 +1,65 @@ +package org.flowio.authenticationservice.auth; + + +import lombok.RequiredArgsConstructor; +import org.flowio.authenticationservice.Entities.LoginUser; +import org.flowio.authenticationservice.Entities.LoginUserRepository; +import org.flowio.authenticationservice.Entities.Role; +import org.flowio.authenticationservice.config.JwtService; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class AuthenticationService { + + private final LoginUserRepository loginUserRepository; + private final PasswordEncoder passwordEncoder; + private final JwtService jwtService; + private final AuthenticationManager authenticationManager; + + + public AuthenticationResponse register(RegisterRequest request) { + + if( + request.getFirstname() == null + || request.getLastname() == null + || request.getLoginEmail() == null + || request.getPassword() == null + ) { + return AuthenticationResponse.builder() + .message("Firstname, lastname, login email and password must not be null") + .build(); + } + + var loginUser = LoginUser.builder() + .firstname(request.getFirstname()) + .lastname(request.getLastname()) + .loginEmail(request.getLoginEmail()) + .password(passwordEncoder.encode(request.getPassword())) + .role(Role.USER) + .build(); + loginUserRepository.save(loginUser); + var jwtToken = jwtService.generateJwtToken(loginUser); + return AuthenticationResponse.builder() + .jwtToken(jwtToken) + .build(); + } + + public AuthenticationResponse authenticate(AuthenticationRequest request) { + authenticationManager.authenticate( + new UsernamePasswordAuthenticationToken( + request.getLoginEmail(), + request.getPassword() + ) + ); + var loginUser = loginUserRepository.findByLoginEmail(request.getLoginEmail()) + .orElseThrow(); + var jwtToken = jwtService.generateJwtToken(loginUser); + return AuthenticationResponse.builder() + .jwtToken(jwtToken) + .build(); + } +} diff --git a/@core/authentication-service/src/main/java/org/flowio/authenticationservice/auth/RegisterRequest.java b/@core/authentication-service/src/main/java/org/flowio/authenticationservice/auth/RegisterRequest.java new file mode 100644 index 0000000..ea27db7 --- /dev/null +++ b/@core/authentication-service/src/main/java/org/flowio/authenticationservice/auth/RegisterRequest.java @@ -0,0 +1,20 @@ +package org.flowio.authenticationservice.auth; + + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class RegisterRequest { + + private String firstname; + private String lastname; + private String loginEmail; + private String password; + +} diff --git a/@core/authentication-service/src/main/java/org/flowio/authenticationservice/config/ApplicationConfig.java b/@core/authentication-service/src/main/java/org/flowio/authenticationservice/config/ApplicationConfig.java new file mode 100644 index 0000000..f9fc6d0 --- /dev/null +++ b/@core/authentication-service/src/main/java/org/flowio/authenticationservice/config/ApplicationConfig.java @@ -0,0 +1,50 @@ +package org.flowio.authenticationservice.config; + + +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import org.flowio.authenticationservice.Entities.LoginUserRepository; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.authentication.dao.DaoAuthenticationProvider; +import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; + +@Configuration +@RequiredArgsConstructor +public class ApplicationConfig { + + private final LoginUserRepository loginUserRepository; + + @Bean + public UserDetailsService userDetailsService() { + return username -> loginUserRepository.findByLoginEmail(username) + .orElseThrow(() -> new UsernameNotFoundException("User not found by user Email.")); + } + + @Bean + public AuthenticationProvider authenticationProvider() { + DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider(); + authenticationProvider.setUserDetailsService(userDetailsService()); + authenticationProvider.setPasswordEncoder(passwordEncoder()); + return authenticationProvider; + } + + @Bean + @NonNull + public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception { + return config.getAuthenticationManager(); + } + + @Bean + @NonNull + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } +} + diff --git a/@core/authentication-service/src/main/java/org/flowio/authenticationservice/config/JWTAuthenticationFilter.java b/@core/authentication-service/src/main/java/org/flowio/authenticationservice/config/JWTAuthenticationFilter.java new file mode 100644 index 0000000..e26baf2 --- /dev/null +++ b/@core/authentication-service/src/main/java/org/flowio/authenticationservice/config/JWTAuthenticationFilter.java @@ -0,0 +1,64 @@ +package org.flowio.authenticationservice.config; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.lang.NonNull; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.web.authentication.WebAuthenticationDetails; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + +@Component +@RequiredArgsConstructor +public class JWTAuthenticationFilter extends OncePerRequestFilter { + + private final JwtService jwtService; + private final UserDetailsService userDetailsService; + + @Override + protected void doFilterInternal( + @NonNull HttpServletRequest request, + @NonNull HttpServletResponse response, + @NonNull FilterChain filterChain + ) throws ServletException, IOException { + final String authHeader = request.getHeader("Authorization"); + final String jwt, userEmail; + + + if (authHeader == null || ! authHeader.startsWith("Bearer ")) { + filterChain.doFilter(request, response); + return; + } + + jwt = authHeader.substring(7); // after "Bearer " + userEmail = jwtService.extractUserEmail(jwt); + + // create new user + if (userEmail != null && SecurityContextHolder.getContext().getAuthentication() == null) { + UserDetails userDetails = this.userDetailsService.loadUserByUsername(userEmail); + if (jwtService.isTokenValid(jwt, userDetails)) { + UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken( + userDetails, + null, + userDetails.getAuthorities() + ); + authToken.setDetails( + new WebAuthenticationDetailsSource().buildDetails(request) + ); + SecurityContextHolder.getContext().setAuthentication(authToken); + } + } + filterChain.doFilter(request, response); + + } +} diff --git a/@core/authentication-service/src/main/java/org/flowio/authenticationservice/config/JwtService.java b/@core/authentication-service/src/main/java/org/flowio/authenticationservice/config/JwtService.java new file mode 100644 index 0000000..cc05a12 --- /dev/null +++ b/@core/authentication-service/src/main/java/org/flowio/authenticationservice/config/JwtService.java @@ -0,0 +1,78 @@ +package org.flowio.authenticationservice.config; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.io.Decoders; +import io.jsonwebtoken.security.Keys; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.stereotype.Service; + +import java.security.Key; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Function; + +@Service +public class JwtService { + + private static final String SECRET_KEY = "404E635266556A586E3272357538782F413F4428472B4B6250645367566B5970"; // todo use environment variable + + + public String extractUserEmail(String jwtToken) { + return extractClaim(jwtToken, Claims::getSubject); + } + + public T extractClaim(String jwtToken, Function claimsResolver) { + final Claims claims = extractAllClaims(jwtToken); + return claimsResolver.apply(claims); + } + + public String generateJwtToken(UserDetails userDetails){ + return generateJwtToken(new HashMap<>(), userDetails); + } + + public String generateJwtToken( + Map extraClaims, + UserDetails userDetails + ) { + return Jwts + .builder() + .setClaims(extraClaims) + .setSubject(userDetails.getUsername()) + .setIssuedAt(new Date(System.currentTimeMillis())) + .setExpiration(new Date(System.currentTimeMillis() + 1000 * 60)) // an hour + .signWith(getSignInKey(), SignatureAlgorithm.HS256) + .compact(); + } + + public boolean isTokenValid(String jwtToken, UserDetails userDetails) { + final String username = extractUserEmail(jwtToken); + return (username.equals((userDetails.getUsername())) && ! isTokenExpired(jwtToken)); + } + + private boolean isTokenExpired(String jwtToken) { + return extractExpiration(jwtToken).before(new Date()); + } + + private Date extractExpiration(String jwtToken) { + return extractClaim(jwtToken, Claims::getExpiration); + } + + private Claims extractAllClaims(String token) { + return Jwts + .parserBuilder() + .setSigningKey(getSignInKey()) + .build() + .parseClaimsJws(token) + .getBody(); + } + + private Key getSignInKey() { + byte[] keyBytes = Decoders.BASE64.decode(SECRET_KEY); + return Keys.hmacShaKeyFor(keyBytes); + } + + +} diff --git a/@core/authentication-service/src/main/java/org/flowio/authenticationservice/config/SecurityConfig.java b/@core/authentication-service/src/main/java/org/flowio/authenticationservice/config/SecurityConfig.java new file mode 100644 index 0000000..3897f84 --- /dev/null +++ b/@core/authentication-service/src/main/java/org/flowio/authenticationservice/config/SecurityConfig.java @@ -0,0 +1,45 @@ +package org.flowio.authenticationservice.config; + +import jakarta.servlet.Filter; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.config.Customizer; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; + +@Configuration +@EnableWebSecurity +@RequiredArgsConstructor +public class SecurityConfig { + + private final JWTAuthenticationFilter jwtAuthFilter; + private final AuthenticationProvider authenticationProvider; + + @Bean + public SecurityFilterChain securityFilterChain (HttpSecurity http) throws Exception { + http + .csrf() + .disable() + .authorizeHttpRequests() + .requestMatchers("/api/v1/auth/**") + .permitAll() + .anyRequest() + .authenticated() + .and() + .sessionManagement() + .sessionCreationPolicy(SessionCreationPolicy.STATELESS) + .and() + .authenticationProvider(authenticationProvider) + .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class); + + + + return http.build(); + + } +} diff --git a/@core/authentication-service/src/main/java/org/flowio/authenticationservice/demo/DemoController.java b/@core/authentication-service/src/main/java/org/flowio/authenticationservice/demo/DemoController.java new file mode 100644 index 0000000..34b5501 --- /dev/null +++ b/@core/authentication-service/src/main/java/org/flowio/authenticationservice/demo/DemoController.java @@ -0,0 +1,16 @@ +package org.flowio.authenticationservice.demo; + + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/api/v1/demo-controller") +public class DemoController { + + @GetMapping + public ResponseEntity sayHello() { + return ResponseEntity.ok("Hello from secured endpoint"); + } + +} diff --git a/@core/authentication-service/src/main/resources/application.properties b/@core/authentication-service/src/main/resources/application.properties deleted file mode 100644 index 8b13789..0000000 --- a/@core/authentication-service/src/main/resources/application.properties +++ /dev/null @@ -1 +0,0 @@ - diff --git a/@core/authentication-service/src/main/resources/application.yml b/@core/authentication-service/src/main/resources/application.yml new file mode 100644 index 0000000..0d877a7 --- /dev/null +++ b/@core/authentication-service/src/main/resources/application.yml @@ -0,0 +1,15 @@ +spring: + datasource: + url: jdbc:postgresql://localhost:5432/jwt_security + username: postgres + password: mysecretpassword + driver-class-name: org.postgresql.Driver + jpa: + hibernate: + ddl-auto: create-drop + show-sql: true + properties: + hibernate: + format_sql: true + database: postgresql + database-platform: org.hibernate.dialect.PostgresSQLDialect \ No newline at end of file From d9e47d1c83b06c4b45e792f0ffde615b85ae45c4 Mon Sep 17 00:00:00 2001 From: "wangziyuan.12" Date: Sat, 9 Dec 2023 18:24:55 -0500 Subject: [PATCH 03/16] features: add docker compose --- .../AuthenticationServiceApplication.java | 13 +++++++++++++ .../auth/AuthenticationResponse.java | 2 +- .../auth/AuthenticationService.java | 2 +- .../src/main/resources/application.yml | 5 +++-- .../AuthenticationServiceApplicationTests.java | 13 ------------- 5 files changed, 18 insertions(+), 17 deletions(-) create mode 100644 @core/authentication-service/src/main/java/org/flowio/authenticationservice/AuthenticationServiceApplication.java delete mode 100644 @core/authentication-service/src/test/java/org/flowio/authenticationservice/AuthenticationServiceApplicationTests.java diff --git a/@core/authentication-service/src/main/java/org/flowio/authenticationservice/AuthenticationServiceApplication.java b/@core/authentication-service/src/main/java/org/flowio/authenticationservice/AuthenticationServiceApplication.java new file mode 100644 index 0000000..6727cc9 --- /dev/null +++ b/@core/authentication-service/src/main/java/org/flowio/authenticationservice/AuthenticationServiceApplication.java @@ -0,0 +1,13 @@ +package org.flowio.authenticationservice; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class AuthenticationServiceApplication { + + public static void main(String[] args) { + SpringApplication.run(AuthenticationServiceApplication.class, args); + } + +} diff --git a/@core/authentication-service/src/main/java/org/flowio/authenticationservice/auth/AuthenticationResponse.java b/@core/authentication-service/src/main/java/org/flowio/authenticationservice/auth/AuthenticationResponse.java index 8e316c3..a747f88 100644 --- a/@core/authentication-service/src/main/java/org/flowio/authenticationservice/auth/AuthenticationResponse.java +++ b/@core/authentication-service/src/main/java/org/flowio/authenticationservice/auth/AuthenticationResponse.java @@ -12,6 +12,6 @@ public class AuthenticationResponse { private String jwtToken; - private String message; +// private String message; } diff --git a/@core/authentication-service/src/main/java/org/flowio/authenticationservice/auth/AuthenticationService.java b/@core/authentication-service/src/main/java/org/flowio/authenticationservice/auth/AuthenticationService.java index 8c27dcb..42b8713 100644 --- a/@core/authentication-service/src/main/java/org/flowio/authenticationservice/auth/AuthenticationService.java +++ b/@core/authentication-service/src/main/java/org/flowio/authenticationservice/auth/AuthenticationService.java @@ -30,7 +30,7 @@ public AuthenticationResponse register(RegisterRequest request) { || request.getPassword() == null ) { return AuthenticationResponse.builder() - .message("Firstname, lastname, login email and password must not be null") +// .message("Firstname, lastname, login email and password must not be null") .build(); } diff --git a/@core/authentication-service/src/main/resources/application.yml b/@core/authentication-service/src/main/resources/application.yml index 0d877a7..4745606 100644 --- a/@core/authentication-service/src/main/resources/application.yml +++ b/@core/authentication-service/src/main/resources/application.yml @@ -1,6 +1,6 @@ spring: datasource: - url: jdbc:postgresql://localhost:5432/jwt_security + url: jdbc:postgresql://some-postgres:5432/flowio-authentication username: postgres password: mysecretpassword driver-class-name: org.postgresql.Driver @@ -11,5 +11,6 @@ spring: properties: hibernate: format_sql: true + dialect: org.hibernate.dialect.PostgreSQLDialect database: postgresql - database-platform: org.hibernate.dialect.PostgresSQLDialect \ No newline at end of file + database-platform: org.hibernate.dialect.PostgreSQLDialect \ No newline at end of file diff --git a/@core/authentication-service/src/test/java/org/flowio/authenticationservice/AuthenticationServiceApplicationTests.java b/@core/authentication-service/src/test/java/org/flowio/authenticationservice/AuthenticationServiceApplicationTests.java deleted file mode 100644 index d461076..0000000 --- a/@core/authentication-service/src/test/java/org/flowio/authenticationservice/AuthenticationServiceApplicationTests.java +++ /dev/null @@ -1,13 +0,0 @@ -package org.flowio.authenticationservice; - -import org.junit.jupiter.api.Test; -import org.springframework.boot.test.context.SpringBootTest; - -@SpringBootTest -class AuthenticationServiceApplicationTests { - - @Test - void contextLoads() { - } - -} From c4e2c841b6bc69c3e134c9979d17a44f8fea7a1a Mon Sep 17 00:00:00 2001 From: "wangziyuan.12" Date: Sat, 9 Dec 2023 18:25:09 -0500 Subject: [PATCH 04/16] features: add docker compose --- @core/authentication-service/Dockerfile | 10 ++++++++ .../authentication-service/docker-compose.yml | 24 +++++++++++++++++++ 2 files changed, 34 insertions(+) create mode 100644 @core/authentication-service/Dockerfile create mode 100644 @core/authentication-service/docker-compose.yml diff --git a/@core/authentication-service/Dockerfile b/@core/authentication-service/Dockerfile new file mode 100644 index 0000000..653e011 --- /dev/null +++ b/@core/authentication-service/Dockerfile @@ -0,0 +1,10 @@ +FROM maven:3.8.3-openjdk-17 as build +WORKDIR /app +COPY pom.xml . +COPY src ./src +RUN mvn clean package + +FROM openjdk:17-jdk-slim +WORKDIR /app +COPY --from=build /app/target/authentication-service-0.0.1-SNAPSHOT.jar /app +ENTRYPOINT ["java","-jar","/app/authentication-service-0.0.1-SNAPSHOT.jar"] diff --git a/@core/authentication-service/docker-compose.yml b/@core/authentication-service/docker-compose.yml new file mode 100644 index 0000000..f5452b8 --- /dev/null +++ b/@core/authentication-service/docker-compose.yml @@ -0,0 +1,24 @@ +version: '3.1' + +services: + some-postgres: + image: postgres + environment: + POSTGRES_PASSWORD: mysecretpassword + POSTGRES_DB: flowio-authentication + ports: + - 5432:5432 + networks: + - flowio-auth-net + + authentication-service: + build: . + ports: + - 8080:8080 + networks: + - flowio-auth-net + + + +networks: + flowio-auth-net: From 3b422a52ebdd526a6776e4e0050b90af51f93fdb Mon Sep 17 00:00:00 2001 From: "wangziyuan.12" Date: Sat, 9 Dec 2023 18:27:31 -0500 Subject: [PATCH 05/16] update: gitignore --- .gitignore | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 5a9ca0f..ed3cb28 100644 --- a/.gitignore +++ b/.gitignore @@ -166,4 +166,8 @@ ormconfig.json # Local env files # ################### .local.env -.local.env.* \ No newline at end of file +.local.env.* + +# idea # +########## +.idea/ \ No newline at end of file From fe5fb7efdf8ae2f1acdad5f3582268145e4860e1 Mon Sep 17 00:00:00 2001 From: "wangziyuan.12" Date: Sat, 9 Dec 2023 20:58:12 -0500 Subject: [PATCH 06/16] update: remove deprecated methods --- .../config/SecurityConfig.java | 30 +++++++------------ 1 file changed, 11 insertions(+), 19 deletions(-) diff --git a/@core/authentication-service/src/main/java/org/flowio/authenticationservice/config/SecurityConfig.java b/@core/authentication-service/src/main/java/org/flowio/authenticationservice/config/SecurityConfig.java index 3897f84..c284b3c 100644 --- a/@core/authentication-service/src/main/java/org/flowio/authenticationservice/config/SecurityConfig.java +++ b/@core/authentication-service/src/main/java/org/flowio/authenticationservice/config/SecurityConfig.java @@ -8,6 +8,7 @@ import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; @@ -21,25 +22,16 @@ public class SecurityConfig { private final AuthenticationProvider authenticationProvider; @Bean - public SecurityFilterChain securityFilterChain (HttpSecurity http) throws Exception { - http - .csrf() - .disable() - .authorizeHttpRequests() - .requestMatchers("/api/v1/auth/**") - .permitAll() - .anyRequest() - .authenticated() - .and() - .sessionManagement() - .sessionCreationPolicy(SessionCreationPolicy.STATELESS) - .and() + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + return http + .csrf(AbstractHttpConfigurer::disable) + .authorizeHttpRequests(authorizeRequests -> authorizeRequests + .requestMatchers("/api/v1/auth/**").permitAll() + .anyRequest().authenticated()) + .sessionManagement(sessionManagement -> sessionManagement + .sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .authenticationProvider(authenticationProvider) - .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class); - - - - return http.build(); - + .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class) + .build(); } } From de5684b38d5cee1e13a4dc4bf5fe697f56cad572 Mon Sep 17 00:00:00 2001 From: "wangziyuan.12" Date: Sat, 9 Dec 2023 21:10:59 -0500 Subject: [PATCH 07/16] update: reorganize packages --- .../flowio/authenticationservice/Entities/Role.java | 6 ------ .../config/ApplicationConfig.java | 2 +- .../config/JWTAuthenticationFilter.java | 3 +-- .../AuthenticationController.java | 6 +++++- .../{demo => controller}/DemoController.java | 6 +++--- .../{auth => dto}/AuthenticationRequest.java | 2 +- .../{auth => dto}/AuthenticationResponse.java | 2 +- .../{auth => dto}/RegisterRequest.java | 2 +- .../{Entities => model}/LoginUser.java | 2 +- .../org/flowio/authenticationservice/model/Role.java | 6 ++++++ .../LoginUserRepository.java | 3 ++- .../{auth => service}/AuthenticationService.java | 12 +++++++----- .../{config => service}/JwtService.java | 2 +- 13 files changed, 30 insertions(+), 24 deletions(-) delete mode 100644 @core/authentication-service/src/main/java/org/flowio/authenticationservice/Entities/Role.java rename @core/authentication-service/src/main/java/org/flowio/authenticationservice/{auth => controller}/AuthenticationController.java (74%) rename @core/authentication-service/src/main/java/org/flowio/authenticationservice/{demo => controller}/DemoController.java (63%) rename @core/authentication-service/src/main/java/org/flowio/authenticationservice/{auth => dto}/AuthenticationRequest.java (85%) rename @core/authentication-service/src/main/java/org/flowio/authenticationservice/{auth => dto}/AuthenticationResponse.java (85%) rename @core/authentication-service/src/main/java/org/flowio/authenticationservice/{auth => dto}/RegisterRequest.java (87%) rename @core/authentication-service/src/main/java/org/flowio/authenticationservice/{Entities => model}/LoginUser.java (96%) create mode 100644 @core/authentication-service/src/main/java/org/flowio/authenticationservice/model/Role.java rename @core/authentication-service/src/main/java/org/flowio/authenticationservice/{Entities => repository}/LoginUserRepository.java (68%) rename @core/authentication-service/src/main/java/org/flowio/authenticationservice/{auth => service}/AuthenticationService.java (84%) rename @core/authentication-service/src/main/java/org/flowio/authenticationservice/{config => service}/JwtService.java (98%) diff --git a/@core/authentication-service/src/main/java/org/flowio/authenticationservice/Entities/Role.java b/@core/authentication-service/src/main/java/org/flowio/authenticationservice/Entities/Role.java deleted file mode 100644 index 6b0cc81..0000000 --- a/@core/authentication-service/src/main/java/org/flowio/authenticationservice/Entities/Role.java +++ /dev/null @@ -1,6 +0,0 @@ -package org.flowio.authenticationservice.Entities; - -public enum Role { - USER, - ADMIN -} diff --git a/@core/authentication-service/src/main/java/org/flowio/authenticationservice/config/ApplicationConfig.java b/@core/authentication-service/src/main/java/org/flowio/authenticationservice/config/ApplicationConfig.java index f9fc6d0..f7709a3 100644 --- a/@core/authentication-service/src/main/java/org/flowio/authenticationservice/config/ApplicationConfig.java +++ b/@core/authentication-service/src/main/java/org/flowio/authenticationservice/config/ApplicationConfig.java @@ -3,7 +3,7 @@ import lombok.NonNull; import lombok.RequiredArgsConstructor; -import org.flowio.authenticationservice.Entities.LoginUserRepository; +import org.flowio.authenticationservice.repository.LoginUserRepository; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.AuthenticationManager; diff --git a/@core/authentication-service/src/main/java/org/flowio/authenticationservice/config/JWTAuthenticationFilter.java b/@core/authentication-service/src/main/java/org/flowio/authenticationservice/config/JWTAuthenticationFilter.java index e26baf2..0819e72 100644 --- a/@core/authentication-service/src/main/java/org/flowio/authenticationservice/config/JWTAuthenticationFilter.java +++ b/@core/authentication-service/src/main/java/org/flowio/authenticationservice/config/JWTAuthenticationFilter.java @@ -5,13 +5,12 @@ import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; +import org.flowio.authenticationservice.service.JwtService; import org.springframework.lang.NonNull; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.web.authentication.WebAuthenticationDetails; import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; import org.springframework.stereotype.Component; import org.springframework.web.filter.OncePerRequestFilter; diff --git a/@core/authentication-service/src/main/java/org/flowio/authenticationservice/auth/AuthenticationController.java b/@core/authentication-service/src/main/java/org/flowio/authenticationservice/controller/AuthenticationController.java similarity index 74% rename from @core/authentication-service/src/main/java/org/flowio/authenticationservice/auth/AuthenticationController.java rename to @core/authentication-service/src/main/java/org/flowio/authenticationservice/controller/AuthenticationController.java index c745ca6..a3e4eef 100644 --- a/@core/authentication-service/src/main/java/org/flowio/authenticationservice/auth/AuthenticationController.java +++ b/@core/authentication-service/src/main/java/org/flowio/authenticationservice/controller/AuthenticationController.java @@ -1,6 +1,10 @@ -package org.flowio.authenticationservice.auth; +package org.flowio.authenticationservice.controller; import lombok.RequiredArgsConstructor; +import org.flowio.authenticationservice.dto.AuthenticationRequest; +import org.flowio.authenticationservice.dto.AuthenticationResponse; +import org.flowio.authenticationservice.service.AuthenticationService; +import org.flowio.authenticationservice.dto.RegisterRequest; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; diff --git a/@core/authentication-service/src/main/java/org/flowio/authenticationservice/demo/DemoController.java b/@core/authentication-service/src/main/java/org/flowio/authenticationservice/controller/DemoController.java similarity index 63% rename from @core/authentication-service/src/main/java/org/flowio/authenticationservice/demo/DemoController.java rename to @core/authentication-service/src/main/java/org/flowio/authenticationservice/controller/DemoController.java index 34b5501..5ebee95 100644 --- a/@core/authentication-service/src/main/java/org/flowio/authenticationservice/demo/DemoController.java +++ b/@core/authentication-service/src/main/java/org/flowio/authenticationservice/controller/DemoController.java @@ -1,15 +1,15 @@ -package org.flowio.authenticationservice.demo; +package org.flowio.authenticationservice.controller; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @RestController -@RequestMapping("/api/v1/demo-controller") +@RequestMapping("/api/v1/ping") public class DemoController { @GetMapping - public ResponseEntity sayHello() { + public ResponseEntity pingPong() { return ResponseEntity.ok("Hello from secured endpoint"); } diff --git a/@core/authentication-service/src/main/java/org/flowio/authenticationservice/auth/AuthenticationRequest.java b/@core/authentication-service/src/main/java/org/flowio/authenticationservice/dto/AuthenticationRequest.java similarity index 85% rename from @core/authentication-service/src/main/java/org/flowio/authenticationservice/auth/AuthenticationRequest.java rename to @core/authentication-service/src/main/java/org/flowio/authenticationservice/dto/AuthenticationRequest.java index af11ffa..af53b5e 100644 --- a/@core/authentication-service/src/main/java/org/flowio/authenticationservice/auth/AuthenticationRequest.java +++ b/@core/authentication-service/src/main/java/org/flowio/authenticationservice/dto/AuthenticationRequest.java @@ -1,4 +1,4 @@ -package org.flowio.authenticationservice.auth; +package org.flowio.authenticationservice.dto; import lombok.AllArgsConstructor; diff --git a/@core/authentication-service/src/main/java/org/flowio/authenticationservice/auth/AuthenticationResponse.java b/@core/authentication-service/src/main/java/org/flowio/authenticationservice/dto/AuthenticationResponse.java similarity index 85% rename from @core/authentication-service/src/main/java/org/flowio/authenticationservice/auth/AuthenticationResponse.java rename to @core/authentication-service/src/main/java/org/flowio/authenticationservice/dto/AuthenticationResponse.java index a747f88..9b5b3b8 100644 --- a/@core/authentication-service/src/main/java/org/flowio/authenticationservice/auth/AuthenticationResponse.java +++ b/@core/authentication-service/src/main/java/org/flowio/authenticationservice/dto/AuthenticationResponse.java @@ -1,4 +1,4 @@ -package org.flowio.authenticationservice.auth; +package org.flowio.authenticationservice.dto; import lombok.AllArgsConstructor; import lombok.Builder; diff --git a/@core/authentication-service/src/main/java/org/flowio/authenticationservice/auth/RegisterRequest.java b/@core/authentication-service/src/main/java/org/flowio/authenticationservice/dto/RegisterRequest.java similarity index 87% rename from @core/authentication-service/src/main/java/org/flowio/authenticationservice/auth/RegisterRequest.java rename to @core/authentication-service/src/main/java/org/flowio/authenticationservice/dto/RegisterRequest.java index ea27db7..baf1862 100644 --- a/@core/authentication-service/src/main/java/org/flowio/authenticationservice/auth/RegisterRequest.java +++ b/@core/authentication-service/src/main/java/org/flowio/authenticationservice/dto/RegisterRequest.java @@ -1,4 +1,4 @@ -package org.flowio.authenticationservice.auth; +package org.flowio.authenticationservice.dto; import lombok.AllArgsConstructor; diff --git a/@core/authentication-service/src/main/java/org/flowio/authenticationservice/Entities/LoginUser.java b/@core/authentication-service/src/main/java/org/flowio/authenticationservice/model/LoginUser.java similarity index 96% rename from @core/authentication-service/src/main/java/org/flowio/authenticationservice/Entities/LoginUser.java rename to @core/authentication-service/src/main/java/org/flowio/authenticationservice/model/LoginUser.java index 5d4edb3..eee93c7 100644 --- a/@core/authentication-service/src/main/java/org/flowio/authenticationservice/Entities/LoginUser.java +++ b/@core/authentication-service/src/main/java/org/flowio/authenticationservice/model/LoginUser.java @@ -1,4 +1,4 @@ -package org.flowio.authenticationservice.Entities; +package org.flowio.authenticationservice.model; import jakarta.persistence.*; import lombok.AllArgsConstructor; diff --git a/@core/authentication-service/src/main/java/org/flowio/authenticationservice/model/Role.java b/@core/authentication-service/src/main/java/org/flowio/authenticationservice/model/Role.java new file mode 100644 index 0000000..fda7bd3 --- /dev/null +++ b/@core/authentication-service/src/main/java/org/flowio/authenticationservice/model/Role.java @@ -0,0 +1,6 @@ +package org.flowio.authenticationservice.model; + +public enum Role { + USER, + ADMIN +} diff --git a/@core/authentication-service/src/main/java/org/flowio/authenticationservice/Entities/LoginUserRepository.java b/@core/authentication-service/src/main/java/org/flowio/authenticationservice/repository/LoginUserRepository.java similarity index 68% rename from @core/authentication-service/src/main/java/org/flowio/authenticationservice/Entities/LoginUserRepository.java rename to @core/authentication-service/src/main/java/org/flowio/authenticationservice/repository/LoginUserRepository.java index ed6ef9d..d2cb0c6 100644 --- a/@core/authentication-service/src/main/java/org/flowio/authenticationservice/Entities/LoginUserRepository.java +++ b/@core/authentication-service/src/main/java/org/flowio/authenticationservice/repository/LoginUserRepository.java @@ -1,5 +1,6 @@ -package org.flowio.authenticationservice.Entities; +package org.flowio.authenticationservice.repository; +import org.flowio.authenticationservice.model.LoginUser; import org.springframework.data.jpa.repository.JpaRepository; import java.util.Optional; diff --git a/@core/authentication-service/src/main/java/org/flowio/authenticationservice/auth/AuthenticationService.java b/@core/authentication-service/src/main/java/org/flowio/authenticationservice/service/AuthenticationService.java similarity index 84% rename from @core/authentication-service/src/main/java/org/flowio/authenticationservice/auth/AuthenticationService.java rename to @core/authentication-service/src/main/java/org/flowio/authenticationservice/service/AuthenticationService.java index 42b8713..3d3cfcc 100644 --- a/@core/authentication-service/src/main/java/org/flowio/authenticationservice/auth/AuthenticationService.java +++ b/@core/authentication-service/src/main/java/org/flowio/authenticationservice/service/AuthenticationService.java @@ -1,11 +1,13 @@ -package org.flowio.authenticationservice.auth; +package org.flowio.authenticationservice.service; import lombok.RequiredArgsConstructor; -import org.flowio.authenticationservice.Entities.LoginUser; -import org.flowio.authenticationservice.Entities.LoginUserRepository; -import org.flowio.authenticationservice.Entities.Role; -import org.flowio.authenticationservice.config.JwtService; +import org.flowio.authenticationservice.dto.AuthenticationRequest; +import org.flowio.authenticationservice.dto.AuthenticationResponse; +import org.flowio.authenticationservice.dto.RegisterRequest; +import org.flowio.authenticationservice.model.LoginUser; +import org.flowio.authenticationservice.repository.LoginUserRepository; +import org.flowio.authenticationservice.model.Role; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.crypto.password.PasswordEncoder; diff --git a/@core/authentication-service/src/main/java/org/flowio/authenticationservice/config/JwtService.java b/@core/authentication-service/src/main/java/org/flowio/authenticationservice/service/JwtService.java similarity index 98% rename from @core/authentication-service/src/main/java/org/flowio/authenticationservice/config/JwtService.java rename to @core/authentication-service/src/main/java/org/flowio/authenticationservice/service/JwtService.java index cc05a12..2c5d9ee 100644 --- a/@core/authentication-service/src/main/java/org/flowio/authenticationservice/config/JwtService.java +++ b/@core/authentication-service/src/main/java/org/flowio/authenticationservice/service/JwtService.java @@ -1,4 +1,4 @@ -package org.flowio.authenticationservice.config; +package org.flowio.authenticationservice.service; import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; From a8fe20ada98bf07e4d58818e723c627c509bd3dd Mon Sep 17 00:00:00 2001 From: "wangziyuan.12" Date: Mon, 11 Dec 2023 12:08:16 -0500 Subject: [PATCH 08/16] update: add README.md --- @core/authentication-service/README.md | 120 ++++++++++++++++++ .../controller/AuthenticationController.java | 2 + ...ontroller.java => PingPongController.java} | 4 +- .../dto/AuthenticationResponse.java | 2 +- .../service/AuthenticationService.java | 2 +- 5 files changed, 126 insertions(+), 4 deletions(-) create mode 100644 @core/authentication-service/README.md rename @core/authentication-service/src/main/java/org/flowio/authenticationservice/controller/{DemoController.java => PingPongController.java} (74%) diff --git a/@core/authentication-service/README.md b/@core/authentication-service/README.md new file mode 100644 index 0000000..7073123 --- /dev/null +++ b/@core/authentication-service/README.md @@ -0,0 +1,120 @@ +# JWT Token-Based Authentication Service +This README provides information about a JWT token-based authentication service. This service allows for user registration and login, issuing refreshing tokens and access tokens to clients. It is secured using Spring Security, built with Spring Boot version 3.2.0, and requires JDK 17. The service uses PostgreSQL to store user information. + +## How to Run +### 1. Set up Secret_Key environment (TODO) + + + +### 2. Docker Compose Up + +To run the service, use Docker: + +```bash +docker-compose up +``` + + + +## APIs + +### 1. Register + +Register a new user with the service. + +- **Endpoint:** `POST http://localhost:8080/api/v1/auth/register` + +- Request Body: + + ```json + { + "firstname": "fname", + "lastname": "lname", + "loginEmail": "test@test.com", + "password": "1234" + } + ``` + +- Curl Command: + + ```bash + curl -X POST http://localhost:8080/api/v1/auth/register \ + -H 'Content-Type: application/json' \ + -d '{"firstname": "fname", "lastname": "lname", "loginEmail": "test@test.com", "password": "1234"}' + ``` + +- Typical Response: + + ```json + { + "jwtToken": "abcdefg" + } + ``` + +### 2. Authenticate + +Authenticate a user and retrieve a JWT token. + +- **Endpoint:** `POST http://localhost:8080/api/v1/auth/authenticate` + +- Request Body: + + ```json + { + "loginEmail": "test@test.com", + "password": "1234" + } + ``` + +- Curl Command: + + ```bash + curl -X POST http://localhost:8080/api/v1/auth/authenticate \ + -H 'Content-Type: application/json' \ + -d '{"loginEmail": "test@test.com", "password": "1234"}' + ``` + +- Typical Response: + + ```json + { + "jwtToken": "abcdefg" + } + ``` + +### 3. Refresh Access Token (TODO) + + + + + +### 4. PingPong (Authenticated Endpoint) + +Check the service status. Requires the user to be logged in. + +- **Endpoint:** `GET http://localhost:8080/api/v1/ping` + +- Curl Command: + + ```bash + curl -X GET http://localhost:8080/api/v1/ping \ + -H 'Authorization: Bearer [Your JWT Token]' + ``` + +- Typical Response: + + ``` + Pong + ``` + + + +## Security Notes +Ensure that your application secrets and database credentials are securely managed and not hard-coded or exposed in your code or Docker configuration. Use environment variables or secure secret management solutions. + + + +## To-Do List + +1. **Read the Secret Key from the Environment:** Modify the application to securely read the `SECRET_KEY` used for JWT token generation and verification from the environment variables. This approach enhances security by avoiding hard-coding sensitive information. +2. **Respond with Refreshing Token:** Implement functionality to issue a refreshing token alongside the access token during the authentication process. This token can be used by clients to obtain a new access token when the current one expires, without requiring user credentials again. \ No newline at end of file diff --git a/@core/authentication-service/src/main/java/org/flowio/authenticationservice/controller/AuthenticationController.java b/@core/authentication-service/src/main/java/org/flowio/authenticationservice/controller/AuthenticationController.java index a3e4eef..8207442 100644 --- a/@core/authentication-service/src/main/java/org/flowio/authenticationservice/controller/AuthenticationController.java +++ b/@core/authentication-service/src/main/java/org/flowio/authenticationservice/controller/AuthenticationController.java @@ -6,6 +6,8 @@ import org.flowio.authenticationservice.service.AuthenticationService; import org.flowio.authenticationservice.dto.RegisterRequest; import org.springframework.http.ResponseEntity; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; diff --git a/@core/authentication-service/src/main/java/org/flowio/authenticationservice/controller/DemoController.java b/@core/authentication-service/src/main/java/org/flowio/authenticationservice/controller/PingPongController.java similarity index 74% rename from @core/authentication-service/src/main/java/org/flowio/authenticationservice/controller/DemoController.java rename to @core/authentication-service/src/main/java/org/flowio/authenticationservice/controller/PingPongController.java index 5ebee95..3090ec2 100644 --- a/@core/authentication-service/src/main/java/org/flowio/authenticationservice/controller/DemoController.java +++ b/@core/authentication-service/src/main/java/org/flowio/authenticationservice/controller/PingPongController.java @@ -6,11 +6,11 @@ @RestController @RequestMapping("/api/v1/ping") -public class DemoController { +public class PingPongController { @GetMapping public ResponseEntity pingPong() { - return ResponseEntity.ok("Hello from secured endpoint"); + return ResponseEntity.ok("Pong"); } } diff --git a/@core/authentication-service/src/main/java/org/flowio/authenticationservice/dto/AuthenticationResponse.java b/@core/authentication-service/src/main/java/org/flowio/authenticationservice/dto/AuthenticationResponse.java index 9b5b3b8..e460d99 100644 --- a/@core/authentication-service/src/main/java/org/flowio/authenticationservice/dto/AuthenticationResponse.java +++ b/@core/authentication-service/src/main/java/org/flowio/authenticationservice/dto/AuthenticationResponse.java @@ -12,6 +12,6 @@ public class AuthenticationResponse { private String jwtToken; -// private String message; + private String message; } diff --git a/@core/authentication-service/src/main/java/org/flowio/authenticationservice/service/AuthenticationService.java b/@core/authentication-service/src/main/java/org/flowio/authenticationservice/service/AuthenticationService.java index 3d3cfcc..c0a9ee5 100644 --- a/@core/authentication-service/src/main/java/org/flowio/authenticationservice/service/AuthenticationService.java +++ b/@core/authentication-service/src/main/java/org/flowio/authenticationservice/service/AuthenticationService.java @@ -32,7 +32,7 @@ public AuthenticationResponse register(RegisterRequest request) { || request.getPassword() == null ) { return AuthenticationResponse.builder() -// .message("Firstname, lastname, login email and password must not be null") + .message("Firstname, lastname, login email and password must not be null") .build(); } From e42d817c27b1534a77ef695c50a5acb8af680e29 Mon Sep 17 00:00:00 2001 From: "wangziyuan.12" Date: Mon, 11 Dec 2023 12:24:30 -0500 Subject: [PATCH 09/16] bugfix: check if register attributes are null or empty --- @core/authentication-service/README.md | 8 ++++-- .../dto/AuthenticationResponse.java | 4 +-- .../service/AuthenticationService.java | 27 +++++++++++-------- 3 files changed, 24 insertions(+), 15 deletions(-) diff --git a/@core/authentication-service/README.md b/@core/authentication-service/README.md index 7073123..2772edc 100644 --- a/@core/authentication-service/README.md +++ b/@core/authentication-service/README.md @@ -47,7 +47,9 @@ Register a new user with the service. ```json { - "jwtToken": "abcdefg" + "refreshToken": "1234567", + "accessToken": "abcdefg". + "message": "Successfully registered." } ``` @@ -78,7 +80,9 @@ Authenticate a user and retrieve a JWT token. ```json { - "jwtToken": "abcdefg" + "refreshToken": "1234567", + "accessToken": "abcdefg". + "message": "Successfully authenticated." } ``` diff --git a/@core/authentication-service/src/main/java/org/flowio/authenticationservice/dto/AuthenticationResponse.java b/@core/authentication-service/src/main/java/org/flowio/authenticationservice/dto/AuthenticationResponse.java index e460d99..71caf25 100644 --- a/@core/authentication-service/src/main/java/org/flowio/authenticationservice/dto/AuthenticationResponse.java +++ b/@core/authentication-service/src/main/java/org/flowio/authenticationservice/dto/AuthenticationResponse.java @@ -10,8 +10,8 @@ @AllArgsConstructor @NoArgsConstructor public class AuthenticationResponse { - - private String jwtToken; + private String refreshToken; + private String accessToken; private String message; } diff --git a/@core/authentication-service/src/main/java/org/flowio/authenticationservice/service/AuthenticationService.java b/@core/authentication-service/src/main/java/org/flowio/authenticationservice/service/AuthenticationService.java index c0a9ee5..1431f5f 100644 --- a/@core/authentication-service/src/main/java/org/flowio/authenticationservice/service/AuthenticationService.java +++ b/@core/authentication-service/src/main/java/org/flowio/authenticationservice/service/AuthenticationService.java @@ -13,6 +13,8 @@ import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; +import java.util.stream.Stream; + @Service @RequiredArgsConstructor public class AuthenticationService { @@ -25,14 +27,15 @@ public class AuthenticationService { public AuthenticationResponse register(RegisterRequest request) { - if( - request.getFirstname() == null - || request.getLastname() == null - || request.getLoginEmail() == null - || request.getPassword() == null - ) { + if(Stream.of( + request.getFirstname(), + request.getLastname(), + request.getLoginEmail(), + request.getPassword() + ) + .anyMatch(str -> str == null || str.isEmpty())) { return AuthenticationResponse.builder() - .message("Firstname, lastname, login email and password must not be null") + .message("Firstname, lastname, login email and password must not be null or empty.") .build(); } @@ -44,9 +47,10 @@ public AuthenticationResponse register(RegisterRequest request) { .role(Role.USER) .build(); loginUserRepository.save(loginUser); - var jwtToken = jwtService.generateJwtToken(loginUser); + var accessToken = jwtService.generateJwtToken(loginUser); return AuthenticationResponse.builder() - .jwtToken(jwtToken) + .accessToken(accessToken) + .message("Successfully registered.") .build(); } @@ -59,9 +63,10 @@ public AuthenticationResponse authenticate(AuthenticationRequest request) { ); var loginUser = loginUserRepository.findByLoginEmail(request.getLoginEmail()) .orElseThrow(); - var jwtToken = jwtService.generateJwtToken(loginUser); + var accessToken = jwtService.generateJwtToken(loginUser); return AuthenticationResponse.builder() - .jwtToken(jwtToken) + .accessToken(accessToken) + .message("Successfully authenticated.") .build(); } } From 8e42533f7da5c2e6c1e9859f1eeaa9e466951def Mon Sep 17 00:00:00 2001 From: "wangziyuan.12" Date: Mon, 11 Dec 2023 16:21:12 -0500 Subject: [PATCH 10/16] feature: added refresh token related features. --- @core/authentication-service/README.md | 90 ++++++++++++++----- .../config/JWTAuthenticationFilter.java | 26 +++++- .../controller/AuthenticationController.java | 23 ++++- .../dto/AuthenticationRequest.java | 4 +- .../service/AuthenticationService.java | 75 +++++++++++++++- .../service/JwtService.java | 29 ++++-- 6 files changed, 207 insertions(+), 40 deletions(-) diff --git a/@core/authentication-service/README.md b/@core/authentication-service/README.md index 2772edc..5d7b2b5 100644 --- a/@core/authentication-service/README.md +++ b/@core/authentication-service/README.md @@ -20,27 +20,38 @@ docker-compose up ### 1. Register -Register a new user with the service. +Register a new user with the service, +and retrieve an accessToken and a refreshToken. -- **Endpoint:** `POST http://localhost:8080/api/v1/auth/register` +- Curl Command: -- Request Body: + ```bash + curl -X POST http://localhost:8080/api/v1/auth/register \ + -H 'Content-Type: application/json' \ + -d '{"firstname": "fname", "lastname": "lname", "loginEmail": "test@test.com", "password": "1234"}' + ``` + +- Typical Response: ```json { - "firstname": "fname", - "lastname": "lname", - "loginEmail": "test@test.com", - "password": "1234" + "refreshToken": "1234567", + "accessToken": "abcdefg", + "message": "Successfully registered." } ``` +### 2. Authenticate with Email address and Password + +Authenticate a user with Email address and Password, +and retrieve an access token and a refresh token. + - Curl Command: ```bash - curl -X POST http://localhost:8080/api/v1/auth/register \ + curl -X POST http://localhost:8080/api/v1/auth/authenticate \ -H 'Content-Type: application/json' \ - -d '{"firstname": "fname", "lastname": "lname", "loginEmail": "test@test.com", "password": "1234"}' + -d '{"loginEmail": "test@test.com", "password": "1234"}' ``` - Typical Response: @@ -48,51 +59,82 @@ Register a new user with the service. ```json { "refreshToken": "1234567", - "accessToken": "abcdefg". - "message": "Successfully registered." + "accessToken": "abcdefg", + "message": "Successfully authenticated." } ``` -### 2. Authenticate +### 3. Get Access Token -Authenticate a user and retrieve a JWT token. +Get a new AccessToken using RefreshToken, +it won't revoke the old AccessToken. -- **Endpoint:** `POST http://localhost:8080/api/v1/auth/authenticate` +- Curl Command: + + ```bash + curl -X POST http://localhost:8080/api/v1/auth/get-access-token \ + -H 'Content-Type: application/json' \ + -d '{"refreshToken": "1234567"}' + ``` -- Request Body: +- Typical Response: ```json { - "loginEmail": "test@test.com", - "password": "1234" + "refreshToken": "null", + "accessToken": "bcdefgh", + "message": "Access token successfully generated." } ``` +### 4. Renew Refresh Token + +Get a new RefreshToken using RefreshToken, +it will revoke the old RefreshToken. + + - Curl Command: ```bash - curl -X POST http://localhost:8080/api/v1/auth/authenticate \ + curl -X POST http://localhost:8080/api/v1/auth/renew-refresh-token \ -H 'Content-Type: application/json' \ - -d '{"loginEmail": "test@test.com", "password": "1234"}' + -d '{"refreshToken": "1234567"}' ``` - Typical Response: ```json { - "refreshToken": "1234567", - "accessToken": "abcdefg". - "message": "Successfully authenticated." + "refreshToken": "2345678", + "accessToken": "null", + "message": "Refresh token successfully renewed." } ``` -### 3. Refresh Access Token (TODO) +### 5. Revoke Refresh Token (TODO) +Revoke a RefreshToken by sending the token you want to revoke. +- Curl Command: + ```bash + curl -X POST http://localhost:8080/api/v1/auth/revoke-refresh-token \ + -H 'Content-Type: application/json' \ + -d '{"refreshToken": "1234567"}' + ``` + +- Typical Response: + + ```json + { + "refreshToken": "null", + "accessToken": "null", + "message": "Successfully revoked." + } + ``` -### 4. PingPong (Authenticated Endpoint) +### 6. PingPong (Authenticated Endpoint) Check the service status. Requires the user to be logged in. diff --git a/@core/authentication-service/src/main/java/org/flowio/authenticationservice/config/JWTAuthenticationFilter.java b/@core/authentication-service/src/main/java/org/flowio/authenticationservice/config/JWTAuthenticationFilter.java index 0819e72..f5b2b37 100644 --- a/@core/authentication-service/src/main/java/org/flowio/authenticationservice/config/JWTAuthenticationFilter.java +++ b/@core/authentication-service/src/main/java/org/flowio/authenticationservice/config/JWTAuthenticationFilter.java @@ -24,6 +24,18 @@ public class JWTAuthenticationFilter extends OncePerRequestFilter { private final JwtService jwtService; private final UserDetailsService userDetailsService; + /** + * This method is used to filter incoming requests and authenticate them. + * It extracts the JWT token from the Authorization header and validates it. + * If the token is valid, it sets the authentication in the security context. + * If the token is invalid or not present, it forwards the request to the next filter in the chain. + * + * @param request the incoming HTTP request + * @param response the outgoing HTTP response + * @param filterChain the chain of filters that the request should go through + * @throws ServletException if the request could not be handled + * @throws IOException if an input or output error is detected when the servlet handles the request + */ @Override protected void doFilterInternal( @NonNull HttpServletRequest request, @@ -33,18 +45,27 @@ protected void doFilterInternal( final String authHeader = request.getHeader("Authorization"); final String jwt, userEmail; - + // Check if the Authorization header is present and starts with "Bearer " if (authHeader == null || ! authHeader.startsWith("Bearer ")) { filterChain.doFilter(request, response); return; } + // Extract the JWT token from the Authorization header jwt = authHeader.substring(7); // after "Bearer " + if (! "accessToken".equals(jwtService.extractTokenType(jwt))){ + filterChain.doFilter(request, response); + return; + } + + // Extract the user email from the JWT token userEmail = jwtService.extractUserEmail(jwt); - // create new user + // If the user email is not null and there is no authentication in the security context, + // load the user details and validate the token if (userEmail != null && SecurityContextHolder.getContext().getAuthentication() == null) { UserDetails userDetails = this.userDetailsService.loadUserByUsername(userEmail); + // If the token is valid, set the authentication in the security context if (jwtService.isTokenValid(jwt, userDetails)) { UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken( userDetails, @@ -57,6 +78,7 @@ protected void doFilterInternal( SecurityContextHolder.getContext().setAuthentication(authToken); } } + // Forward the request to the next filter in the chain filterChain.doFilter(request, response); } diff --git a/@core/authentication-service/src/main/java/org/flowio/authenticationservice/controller/AuthenticationController.java b/@core/authentication-service/src/main/java/org/flowio/authenticationservice/controller/AuthenticationController.java index 8207442..bd5ed48 100644 --- a/@core/authentication-service/src/main/java/org/flowio/authenticationservice/controller/AuthenticationController.java +++ b/@core/authentication-service/src/main/java/org/flowio/authenticationservice/controller/AuthenticationController.java @@ -6,8 +6,6 @@ import org.flowio.authenticationservice.service.AuthenticationService; import org.flowio.authenticationservice.dto.RegisterRequest; import org.springframework.http.ResponseEntity; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; @@ -33,4 +31,25 @@ public ResponseEntity register( ) { return ResponseEntity.ok(service.authenticate(request)); } + + @PostMapping("/get-access-token") + public ResponseEntity getAccessToken( + @RequestBody AuthenticationRequest request + ) { + return ResponseEntity.ok(service.getAccessTokenUsingRefreshToken(request)); + } + + @PostMapping("/renew-refresh-token") + public ResponseEntity renewRefreshToken( + @RequestBody AuthenticationRequest request + ) { + return ResponseEntity.ok(service.renewRefreshToken(request)); + } + + @PostMapping("/revoke-refresh-token") + public ResponseEntity revokeRefreshToken( + @RequestBody AuthenticationRequest request + ) { + return ResponseEntity.ok(service.revokeRefreshToken(request)); + } } diff --git a/@core/authentication-service/src/main/java/org/flowio/authenticationservice/dto/AuthenticationRequest.java b/@core/authentication-service/src/main/java/org/flowio/authenticationservice/dto/AuthenticationRequest.java index af53b5e..3342956 100644 --- a/@core/authentication-service/src/main/java/org/flowio/authenticationservice/dto/AuthenticationRequest.java +++ b/@core/authentication-service/src/main/java/org/flowio/authenticationservice/dto/AuthenticationRequest.java @@ -11,9 +11,7 @@ @AllArgsConstructor @NoArgsConstructor public class AuthenticationRequest { - private String loginEmail; - private String password; - + private String refreshToken; } diff --git a/@core/authentication-service/src/main/java/org/flowio/authenticationservice/service/AuthenticationService.java b/@core/authentication-service/src/main/java/org/flowio/authenticationservice/service/AuthenticationService.java index 1431f5f..0fbb207 100644 --- a/@core/authentication-service/src/main/java/org/flowio/authenticationservice/service/AuthenticationService.java +++ b/@core/authentication-service/src/main/java/org/flowio/authenticationservice/service/AuthenticationService.java @@ -13,6 +13,7 @@ import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; +import java.util.HashMap; import java.util.stream.Stream; @Service @@ -24,6 +25,8 @@ public class AuthenticationService { private final JwtService jwtService; private final AuthenticationManager authenticationManager; + private final int accessTokenExpirationTime = 1000 * 15; // 15 mins + private final int refreshTokenExpirationTime = 1000 * 60 * 24; // 1 day public AuthenticationResponse register(RegisterRequest request) { @@ -47,9 +50,19 @@ public AuthenticationResponse register(RegisterRequest request) { .role(Role.USER) .build(); loginUserRepository.save(loginUser); - var accessToken = jwtService.generateJwtToken(loginUser); - return AuthenticationResponse.builder() + var accessToken = jwtService.generateJwtToken( + new HashMap<>(){{ put("tokenType", "accessToken"); }}, + accessTokenExpirationTime, + loginUser + ); + var refreshToken = jwtService.generateJwtToken( + new HashMap<>(){{ put("tokenType", "refreshToken"); }}, + refreshTokenExpirationTime, + loginUser + ); + return AuthenticationResponse.builder() .accessToken(accessToken) + .refreshToken(refreshToken) .message("Successfully registered.") .build(); } @@ -63,10 +76,64 @@ public AuthenticationResponse authenticate(AuthenticationRequest request) { ); var loginUser = loginUserRepository.findByLoginEmail(request.getLoginEmail()) .orElseThrow(); - var accessToken = jwtService.generateJwtToken(loginUser); - return AuthenticationResponse.builder() + var accessToken = jwtService.generateJwtToken( + new HashMap<>(){{ put("tokenType", "accessToken"); }}, + accessTokenExpirationTime, + loginUser + ); + var refreshToken = jwtService.generateJwtToken( + new HashMap<>(){{ put("tokenType", "refreshToken"); }}, + refreshTokenExpirationTime, + loginUser + ); + return AuthenticationResponse.builder() .accessToken(accessToken) + .refreshToken(refreshToken) .message("Successfully authenticated.") .build(); } + + public AuthenticationResponse getAccessTokenUsingRefreshToken(AuthenticationRequest request) { + var refreshToken = request.getRefreshToken(); + if (! jwtService.isTokenValid(refreshToken) || ! "refreshToken".equals(jwtService.extractTokenType(refreshToken))) { + return AuthenticationResponse.builder() + .message("Invalid refresh token.") + .build(); + } + var accessToken = jwtService.generateJwtToken( + new HashMap<>(){{ put("tokenType", "accessToken"); }}, + accessTokenExpirationTime, + jwtService.extractUserEmail(refreshToken) + ); + return AuthenticationResponse.builder() + .accessToken(accessToken) + .message("Access token successfully generated.") + .build(); + + } + + public AuthenticationResponse renewRefreshToken(AuthenticationRequest request) { + var refreshToken = request.getRefreshToken(); + if (! jwtService.isTokenValid(refreshToken) || ! "refreshToken".equals(jwtService.extractTokenType(refreshToken))) { + return AuthenticationResponse.builder() + .message("Invalid refresh token.") + .build(); + } + revokeRefreshToken(request); + + var newRefreshToken = jwtService.generateJwtToken( + new HashMap<>(){{ put("tokenType", "refreshToken"); }}, + refreshTokenExpirationTime, + jwtService.extractUserEmail(refreshToken) + ); + return AuthenticationResponse.builder() + .refreshToken(newRefreshToken) + .message("Refresh token successfully renewed.") + .build(); + + } + + public AuthenticationResponse revokeRefreshToken(AuthenticationRequest request) {// TODO + return null; + } } diff --git a/@core/authentication-service/src/main/java/org/flowio/authenticationservice/service/JwtService.java b/@core/authentication-service/src/main/java/org/flowio/authenticationservice/service/JwtService.java index 2c5d9ee..f69a82f 100644 --- a/@core/authentication-service/src/main/java/org/flowio/authenticationservice/service/JwtService.java +++ b/@core/authentication-service/src/main/java/org/flowio/authenticationservice/service/JwtService.java @@ -29,20 +29,29 @@ public T extractClaim(String jwtToken, Function claimsResolver) { return claimsResolver.apply(claims); } - public String generateJwtToken(UserDetails userDetails){ - return generateJwtToken(new HashMap<>(), userDetails); - } +// public String generateJwtToken(UserDetails userDetails){ +// return generateJwtToken(new HashMap<>(), userDetails); +// } public String generateJwtToken( Map extraClaims, + int expirationIn, UserDetails userDetails + ) { + return generateJwtToken(extraClaims, expirationIn, userDetails.getUsername()); + } + + public String generateJwtToken( + Map extraClaims, + int expirationIn, + String username ) { return Jwts .builder() .setClaims(extraClaims) - .setSubject(userDetails.getUsername()) + .setSubject(username) .setIssuedAt(new Date(System.currentTimeMillis())) - .setExpiration(new Date(System.currentTimeMillis() + 1000 * 60)) // an hour + .setExpiration(new Date(System.currentTimeMillis() + expirationIn)) // an hour .signWith(getSignInKey(), SignatureAlgorithm.HS256) .compact(); } @@ -52,10 +61,20 @@ public boolean isTokenValid(String jwtToken, UserDetails userDetails) { return (username.equals((userDetails.getUsername())) && ! isTokenExpired(jwtToken)); } + public boolean isTokenValid(String jwtToken) { + return ! isTokenExpired(jwtToken); + } + private boolean isTokenExpired(String jwtToken) { return extractExpiration(jwtToken).before(new Date()); } + + public String extractTokenType(String jwtToken) { + var tokenType = extractClaim(jwtToken, claims -> claims.get("tokenType")); + return tokenType.toString(); + } + private Date extractExpiration(String jwtToken) { return extractClaim(jwtToken, Claims::getExpiration); } From 365b8ead8ba3c5465b161c9d755f5d3282a5b86b Mon Sep 17 00:00:00 2001 From: "wangziyuan.12" Date: Mon, 11 Dec 2023 22:54:37 -0500 Subject: [PATCH 11/16] feature: added refresh token firstIat for storage --- @core/authentication-service/README.md | 41 ++++++++---- .../config/JWTAuthenticationFilter.java | 41 ++++++++---- .../service/AuthenticationService.java | 64 ++++++++++--------- .../service/JwtService.java | 18 ------ 4 files changed, 88 insertions(+), 76 deletions(-) diff --git a/@core/authentication-service/README.md b/@core/authentication-service/README.md index 5d7b2b5..e4c11e7 100644 --- a/@core/authentication-service/README.md +++ b/@core/authentication-service/README.md @@ -1,5 +1,20 @@ # JWT Token-Based Authentication Service -This README provides information about a JWT token-based authentication service. This service allows for user registration and login, issuing refreshing tokens and access tokens to clients. It is secured using Spring Security, built with Spring Boot version 3.2.0, and requires JDK 17. The service uses PostgreSQL to store user information. +This README provides information about a JWT token-based authentication service. + +### Features: +1. One-time refresh token / refresh token rotation +(using the refresh token multiple times will invalidate the entire refresh token family.) +2. Access token +3. User registration +4. User Login + +### Tech Stacks: + +1. Spring Boot 3.2.0 +2. JDK 17 +3. Spring Security +4. PostgreSQL for User information +5. Redis for Refresh Token ## How to Run ### 1. Set up Secret_Key environment (TODO) @@ -35,8 +50,8 @@ and retrieve an accessToken and a refreshToken. ```json { - "refreshToken": "1234567", - "accessToken": "abcdefg", + "refreshToken": "new token", + "accessToken": "new token", "message": "Successfully registered." } ``` @@ -58,31 +73,31 @@ and retrieve an access token and a refresh token. ```json { - "refreshToken": "1234567", - "accessToken": "abcdefg", + "refreshToken": "new token", + "accessToken": "new token", "message": "Successfully authenticated." } ``` ### 3. Get Access Token -Get a new AccessToken using RefreshToken, -it won't revoke the old AccessToken. +Get a new AccessToken using a one time RefreshToken, +it won't revoke the old AccessToken, but will revoke the one time RefreshToken. - Curl Command: ```bash curl -X POST http://localhost:8080/api/v1/auth/get-access-token \ -H 'Content-Type: application/json' \ - -d '{"refreshToken": "1234567"}' + -d '{"refreshToken": "refresh-token"}' ``` - Typical Response: ```json { - "refreshToken": "null", - "accessToken": "bcdefgh", + "refreshToken": "new token", + "accessToken": "new token", "message": "Access token successfully generated." } ``` @@ -98,14 +113,14 @@ it will revoke the old RefreshToken. ```bash curl -X POST http://localhost:8080/api/v1/auth/renew-refresh-token \ -H 'Content-Type: application/json' \ - -d '{"refreshToken": "1234567"}' + -d '{"refreshToken": "token"}' ``` - Typical Response: ```json { - "refreshToken": "2345678", + "refreshToken": "new token", "accessToken": "null", "message": "Refresh token successfully renewed." } @@ -121,7 +136,7 @@ Revoke a RefreshToken by sending the token you want to revoke. ```bash curl -X POST http://localhost:8080/api/v1/auth/revoke-refresh-token \ -H 'Content-Type: application/json' \ - -d '{"refreshToken": "1234567"}' + -d '{"refreshToken": "token"}' ``` - Typical Response: diff --git a/@core/authentication-service/src/main/java/org/flowio/authenticationservice/config/JWTAuthenticationFilter.java b/@core/authentication-service/src/main/java/org/flowio/authenticationservice/config/JWTAuthenticationFilter.java index f5b2b37..f1e1468 100644 --- a/@core/authentication-service/src/main/java/org/flowio/authenticationservice/config/JWTAuthenticationFilter.java +++ b/@core/authentication-service/src/main/java/org/flowio/authenticationservice/config/JWTAuthenticationFilter.java @@ -16,6 +16,7 @@ import org.springframework.web.filter.OncePerRequestFilter; import java.io.IOException; +import java.util.Objects; @Component @RequiredArgsConstructor @@ -63,21 +64,33 @@ protected void doFilterInternal( // If the user email is not null and there is no authentication in the security context, // load the user details and validate the token - if (userEmail != null && SecurityContextHolder.getContext().getAuthentication() == null) { - UserDetails userDetails = this.userDetailsService.loadUserByUsername(userEmail); - // If the token is valid, set the authentication in the security context - if (jwtService.isTokenValid(jwt, userDetails)) { - UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken( - userDetails, - null, - userDetails.getAuthorities() - ); - authToken.setDetails( - new WebAuthenticationDetailsSource().buildDetails(request) - ); - SecurityContextHolder.getContext().setAuthentication(authToken); - } + if (userEmail == null || SecurityContextHolder.getContext().getAuthentication() != null) { + filterChain.doFilter(request, response); + return; } + + UserDetails userDetails = this.userDetailsService.loadUserByUsername(userEmail); + // If the token is valid, set the authentication in the security context + + final String jwtUserEmail = jwtService.extractUserEmail(jwt); + if (!Objects.equals(userDetails.getUsername(), jwtUserEmail)) { + filterChain.doFilter(request, response); + return; + } + + + if (jwtService.isTokenValid(jwt)) { + UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken( + userDetails, + null, + userDetails.getAuthorities() + ); + authToken.setDetails( + new WebAuthenticationDetailsSource().buildDetails(request) + ); + SecurityContextHolder.getContext().setAuthentication(authToken); + } + // Forward the request to the next filter in the chain filterChain.doFilter(request, response); diff --git a/@core/authentication-service/src/main/java/org/flowio/authenticationservice/service/AuthenticationService.java b/@core/authentication-service/src/main/java/org/flowio/authenticationservice/service/AuthenticationService.java index 0fbb207..225849a 100644 --- a/@core/authentication-service/src/main/java/org/flowio/authenticationservice/service/AuthenticationService.java +++ b/@core/authentication-service/src/main/java/org/flowio/authenticationservice/service/AuthenticationService.java @@ -13,6 +13,7 @@ import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; +import java.util.Date; import java.util.HashMap; import java.util.stream.Stream; @@ -28,6 +29,30 @@ public class AuthenticationService { private final int accessTokenExpirationTime = 1000 * 15; // 15 mins private final int refreshTokenExpirationTime = 1000 * 60 * 24; // 1 day + private String buildAccessToken(LoginUser loginUser) { + return buildAccessToken(loginUser.getLoginEmail()); + } + + private String buildAccessToken(String loginEmail) { + return jwtService.generateJwtToken( + new HashMap<>(){{ put("tokenType", "accessToken"); }}, + accessTokenExpirationTime, + loginEmail + ); + } + + private String buildRefreshToken(LoginUser loginUser) { + return buildRefreshToken(loginUser.getLoginEmail()); + } + + private String buildRefreshToken(String loginEmail) { + return jwtService.generateJwtToken( + new HashMap<>(){{ put("tokenType", "refreshToken"); put("firstIat", new Date(System.currentTimeMillis())); }}, + refreshTokenExpirationTime, + loginEmail + ); + } + public AuthenticationResponse register(RegisterRequest request) { if(Stream.of( @@ -50,16 +75,8 @@ public AuthenticationResponse register(RegisterRequest request) { .role(Role.USER) .build(); loginUserRepository.save(loginUser); - var accessToken = jwtService.generateJwtToken( - new HashMap<>(){{ put("tokenType", "accessToken"); }}, - accessTokenExpirationTime, - loginUser - ); - var refreshToken = jwtService.generateJwtToken( - new HashMap<>(){{ put("tokenType", "refreshToken"); }}, - refreshTokenExpirationTime, - loginUser - ); + var accessToken = buildAccessToken(loginUser); + var refreshToken = buildRefreshToken(loginUser); return AuthenticationResponse.builder() .accessToken(accessToken) .refreshToken(refreshToken) @@ -76,16 +93,8 @@ public AuthenticationResponse authenticate(AuthenticationRequest request) { ); var loginUser = loginUserRepository.findByLoginEmail(request.getLoginEmail()) .orElseThrow(); - var accessToken = jwtService.generateJwtToken( - new HashMap<>(){{ put("tokenType", "accessToken"); }}, - accessTokenExpirationTime, - loginUser - ); - var refreshToken = jwtService.generateJwtToken( - new HashMap<>(){{ put("tokenType", "refreshToken"); }}, - refreshTokenExpirationTime, - loginUser - ); + var accessToken = buildAccessToken(loginUser); + var refreshToken = buildRefreshToken(loginUser); return AuthenticationResponse.builder() .accessToken(accessToken) .refreshToken(refreshToken) @@ -100,13 +109,11 @@ public AuthenticationResponse getAccessTokenUsingRefreshToken(AuthenticationRequ .message("Invalid refresh token.") .build(); } - var accessToken = jwtService.generateJwtToken( - new HashMap<>(){{ put("tokenType", "accessToken"); }}, - accessTokenExpirationTime, - jwtService.extractUserEmail(refreshToken) - ); + var accessToken = buildAccessToken(jwtService.extractUserEmail(refreshToken)); + var newRefreshToken = buildRefreshToken(jwtService.extractUserEmail(refreshToken)); return AuthenticationResponse.builder() .accessToken(accessToken) + .refreshToken(newRefreshToken) .message("Access token successfully generated.") .build(); @@ -120,12 +127,7 @@ public AuthenticationResponse renewRefreshToken(AuthenticationRequest request) { .build(); } revokeRefreshToken(request); - - var newRefreshToken = jwtService.generateJwtToken( - new HashMap<>(){{ put("tokenType", "refreshToken"); }}, - refreshTokenExpirationTime, - jwtService.extractUserEmail(refreshToken) - ); + var newRefreshToken = buildRefreshToken(jwtService.extractUserEmail(refreshToken)); return AuthenticationResponse.builder() .refreshToken(newRefreshToken) .message("Refresh token successfully renewed.") diff --git a/@core/authentication-service/src/main/java/org/flowio/authenticationservice/service/JwtService.java b/@core/authentication-service/src/main/java/org/flowio/authenticationservice/service/JwtService.java index f69a82f..3fd4537 100644 --- a/@core/authentication-service/src/main/java/org/flowio/authenticationservice/service/JwtService.java +++ b/@core/authentication-service/src/main/java/org/flowio/authenticationservice/service/JwtService.java @@ -5,7 +5,6 @@ import io.jsonwebtoken.SignatureAlgorithm; import io.jsonwebtoken.io.Decoders; import io.jsonwebtoken.security.Keys; -import org.springframework.security.core.userdetails.UserDetails; import org.springframework.stereotype.Service; import java.security.Key; @@ -29,18 +28,6 @@ public T extractClaim(String jwtToken, Function claimsResolver) { return claimsResolver.apply(claims); } -// public String generateJwtToken(UserDetails userDetails){ -// return generateJwtToken(new HashMap<>(), userDetails); -// } - - public String generateJwtToken( - Map extraClaims, - int expirationIn, - UserDetails userDetails - ) { - return generateJwtToken(extraClaims, expirationIn, userDetails.getUsername()); - } - public String generateJwtToken( Map extraClaims, int expirationIn, @@ -56,11 +43,6 @@ public String generateJwtToken( .compact(); } - public boolean isTokenValid(String jwtToken, UserDetails userDetails) { - final String username = extractUserEmail(jwtToken); - return (username.equals((userDetails.getUsername())) && ! isTokenExpired(jwtToken)); - } - public boolean isTokenValid(String jwtToken) { return ! isTokenExpired(jwtToken); } From 417b18fe3b3955485676e58a91eed2ff79cfca19 Mon Sep 17 00:00:00 2001 From: "wangziyuan.12" Date: Tue, 12 Dec 2023 15:31:15 -0500 Subject: [PATCH 12/16] feature: one-time refresh token, and refresh revocation --- @core/authentication-service/README.md | 35 ++--- .../authentication-service/docker-compose.yml | 7 + @core/authentication-service/pom.xml | 16 +++ .../config/RedisConfig.java | 38 ++++++ .../config/SecurityConfig.java | 3 +- .../controller/AuthenticationController.java | 7 - .../JWTAuthenticationFilter.java | 2 +- .../service/AuthenticationService.java | 120 ++++++++++++++---- .../service/JwtService.java | 25 +++- .../service/RedisService.java | 24 ++++ .../src/main/resources/application.yml | 14 +- 11 files changed, 225 insertions(+), 66 deletions(-) create mode 100644 @core/authentication-service/src/main/java/org/flowio/authenticationservice/config/RedisConfig.java rename @core/authentication-service/src/main/java/org/flowio/authenticationservice/{config => filter}/JWTAuthenticationFilter.java (98%) create mode 100644 @core/authentication-service/src/main/java/org/flowio/authenticationservice/service/RedisService.java diff --git a/@core/authentication-service/README.md b/@core/authentication-service/README.md index e4c11e7..f5c7f72 100644 --- a/@core/authentication-service/README.md +++ b/@core/authentication-service/README.md @@ -11,10 +11,10 @@ This README provides information about a JWT token-based authentication service. ### Tech Stacks: 1. Spring Boot 3.2.0 -2. JDK 17 +2. Java 17 3. Spring Security -4. PostgreSQL for User information -5. Redis for Refresh Token +4. PostgreSQL for user information +5. Redis for refresh token ## How to Run ### 1. Set up Secret_Key environment (TODO) @@ -102,32 +102,15 @@ it won't revoke the old AccessToken, but will revoke the one time RefreshToken. } ``` -### 4. Renew Refresh Token - -Get a new RefreshToken using RefreshToken, -it will revoke the old RefreshToken. - - -- Curl Command: - - ```bash - curl -X POST http://localhost:8080/api/v1/auth/renew-refresh-token \ - -H 'Content-Type: application/json' \ - -d '{"refreshToken": "token"}' - ``` - -- Typical Response: - ```json { - "refreshToken": "new token", - "accessToken": "null", - "message": "Refresh token successfully renewed." + "refreshToken": null, + "accessToken": null, + "message": "The refresh token has exceeded the usage limit." } ``` - -### 5. Revoke Refresh Token (TODO) +### 4. Revoke Refresh Token Revoke a RefreshToken by sending the token you want to revoke. @@ -145,11 +128,11 @@ Revoke a RefreshToken by sending the token you want to revoke. { "refreshToken": "null", "accessToken": "null", - "message": "Successfully revoked." + "message": "Revoked." } ``` -### 6. PingPong (Authenticated Endpoint) +### 5. PingPong (Authenticated Endpoint) Check the service status. Requires the user to be logged in. diff --git a/@core/authentication-service/docker-compose.yml b/@core/authentication-service/docker-compose.yml index f5452b8..b18e530 100644 --- a/@core/authentication-service/docker-compose.yml +++ b/@core/authentication-service/docker-compose.yml @@ -11,6 +11,13 @@ services: networks: - flowio-auth-net + redis: + image: redis:latest + ports: + - 6379:6379 + networks: + - flowio-auth-net + authentication-service: build: . ports: diff --git a/@core/authentication-service/pom.xml b/@core/authentication-service/pom.xml index d98db1d..5f5f181 100644 --- a/@core/authentication-service/pom.xml +++ b/@core/authentication-service/pom.xml @@ -66,6 +66,22 @@ spring-security-test test + + org.springframework.boot + spring-boot-starter-data-redis + 2.7.2 + + + org.apache.maven + maven-model + 3.2.5 + + + redis.clients + jedis + 5.0.2 + jar + diff --git a/@core/authentication-service/src/main/java/org/flowio/authenticationservice/config/RedisConfig.java b/@core/authentication-service/src/main/java/org/flowio/authenticationservice/config/RedisConfig.java new file mode 100644 index 0000000..5f6c028 --- /dev/null +++ b/@core/authentication-service/src/main/java/org/flowio/authenticationservice/config/RedisConfig.java @@ -0,0 +1,38 @@ +package org.flowio.authenticationservice.config; + +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.connection.RedisPassword; +import org.springframework.data.redis.connection.RedisStandaloneConfiguration; +import org.springframework.data.redis.connection.jedis.JedisConnectionFactory; + + +@Configuration +@RequiredArgsConstructor +public class RedisConfig { + + + @Value("${spring.data.redis.host}") + private String redisHost; + + @Value("${spring.data.redis.port}") + private int redisPort; + + @Value("${spring.data.redis.password}") + private String redisPassword; + + @Value("${spring.data.redis.database}") + private int redisDatabase; + + @Bean + public RedisConnectionFactory redisConnectionFactory() { + RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration(redisHost, redisPort); + redisStandaloneConfiguration.setPassword(RedisPassword.of(redisPassword)); + redisStandaloneConfiguration.setDatabase(redisDatabase); + return new JedisConnectionFactory(redisStandaloneConfiguration); + } + +} diff --git a/@core/authentication-service/src/main/java/org/flowio/authenticationservice/config/SecurityConfig.java b/@core/authentication-service/src/main/java/org/flowio/authenticationservice/config/SecurityConfig.java index c284b3c..730d28b 100644 --- a/@core/authentication-service/src/main/java/org/flowio/authenticationservice/config/SecurityConfig.java +++ b/@core/authentication-service/src/main/java/org/flowio/authenticationservice/config/SecurityConfig.java @@ -1,11 +1,10 @@ package org.flowio.authenticationservice.config; -import jakarta.servlet.Filter; import lombok.RequiredArgsConstructor; +import org.flowio.authenticationservice.filter.JWTAuthenticationFilter; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.AuthenticationProvider; -import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; diff --git a/@core/authentication-service/src/main/java/org/flowio/authenticationservice/controller/AuthenticationController.java b/@core/authentication-service/src/main/java/org/flowio/authenticationservice/controller/AuthenticationController.java index bd5ed48..84b69bd 100644 --- a/@core/authentication-service/src/main/java/org/flowio/authenticationservice/controller/AuthenticationController.java +++ b/@core/authentication-service/src/main/java/org/flowio/authenticationservice/controller/AuthenticationController.java @@ -39,13 +39,6 @@ public ResponseEntity getAccessToken( return ResponseEntity.ok(service.getAccessTokenUsingRefreshToken(request)); } - @PostMapping("/renew-refresh-token") - public ResponseEntity renewRefreshToken( - @RequestBody AuthenticationRequest request - ) { - return ResponseEntity.ok(service.renewRefreshToken(request)); - } - @PostMapping("/revoke-refresh-token") public ResponseEntity revokeRefreshToken( @RequestBody AuthenticationRequest request diff --git a/@core/authentication-service/src/main/java/org/flowio/authenticationservice/config/JWTAuthenticationFilter.java b/@core/authentication-service/src/main/java/org/flowio/authenticationservice/filter/JWTAuthenticationFilter.java similarity index 98% rename from @core/authentication-service/src/main/java/org/flowio/authenticationservice/config/JWTAuthenticationFilter.java rename to @core/authentication-service/src/main/java/org/flowio/authenticationservice/filter/JWTAuthenticationFilter.java index f1e1468..4d79412 100644 --- a/@core/authentication-service/src/main/java/org/flowio/authenticationservice/config/JWTAuthenticationFilter.java +++ b/@core/authentication-service/src/main/java/org/flowio/authenticationservice/filter/JWTAuthenticationFilter.java @@ -1,4 +1,4 @@ -package org.flowio.authenticationservice.config; +package org.flowio.authenticationservice.filter; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; diff --git a/@core/authentication-service/src/main/java/org/flowio/authenticationservice/service/AuthenticationService.java b/@core/authentication-service/src/main/java/org/flowio/authenticationservice/service/AuthenticationService.java index 225849a..0d1f9c7 100644 --- a/@core/authentication-service/src/main/java/org/flowio/authenticationservice/service/AuthenticationService.java +++ b/@core/authentication-service/src/main/java/org/flowio/authenticationservice/service/AuthenticationService.java @@ -13,6 +13,7 @@ import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; +import java.text.SimpleDateFormat; import java.util.Date; import java.util.HashMap; import java.util.stream.Stream; @@ -26,8 +27,12 @@ public class AuthenticationService { private final JwtService jwtService; private final AuthenticationManager authenticationManager; - private final int accessTokenExpirationTime = 1000 * 15; // 15 mins - private final int refreshTokenExpirationTime = 1000 * 60 * 24; // 1 day + private final RedisService redisService; + + private final long accessTokenExpirationTime = 1000 * 60 * 10; // 10 mins + private final long refreshTokenExpirationTime = 1000 * 60 * 60 * 24; // 1 day + + private String buildAccessToken(LoginUser loginUser) { return buildAccessToken(loginUser.getLoginEmail()); @@ -41,18 +46,34 @@ private String buildAccessToken(String loginEmail) { ); } - private String buildRefreshToken(LoginUser loginUser) { - return buildRefreshToken(loginUser.getLoginEmail()); + private String buildRefreshToken(LoginUser loginUser, String firstIat) { + return buildRefreshToken(loginUser.getLoginEmail(), firstIat); } - private String buildRefreshToken(String loginEmail) { + private String buildRefreshToken(String loginEmail, String firstIat) { return jwtService.generateJwtToken( - new HashMap<>(){{ put("tokenType", "refreshToken"); put("firstIat", new Date(System.currentTimeMillis())); }}, + new HashMap<>(){{ + put("tokenType", "refreshToken"); + put("firstIat", firstIat); + put("thisIat", System.currentTimeMillis()); + }}, refreshTokenExpirationTime, loginEmail ); } + private void addRTokenLastUsage(String key){ + redisService.addKeyValue( + key, + System.currentTimeMillis(), + refreshTokenExpirationTime + ); + } + + private Long getRTokenLastUsage(String key){ + return redisService.getValue(key); + } + public AuthenticationResponse register(RegisterRequest request) { if(Stream.of( @@ -76,7 +97,7 @@ public AuthenticationResponse register(RegisterRequest request) { .build(); loginUserRepository.save(loginUser); var accessToken = buildAccessToken(loginUser); - var refreshToken = buildRefreshToken(loginUser); + var refreshToken = buildRefreshToken(loginUser, String.valueOf(System.currentTimeMillis())); return AuthenticationResponse.builder() .accessToken(accessToken) .refreshToken(refreshToken) @@ -94,7 +115,7 @@ public AuthenticationResponse authenticate(AuthenticationRequest request) { var loginUser = loginUserRepository.findByLoginEmail(request.getLoginEmail()) .orElseThrow(); var accessToken = buildAccessToken(loginUser); - var refreshToken = buildRefreshToken(loginUser); + var refreshToken = buildRefreshToken(loginUser, String.valueOf(System.currentTimeMillis())); return AuthenticationResponse.builder() .accessToken(accessToken) .refreshToken(refreshToken) @@ -102,40 +123,85 @@ public AuthenticationResponse authenticate(AuthenticationRequest request) { .build(); } - public AuthenticationResponse getAccessTokenUsingRefreshToken(AuthenticationRequest request) { - var refreshToken = request.getRefreshToken(); - if (! jwtService.isTokenValid(refreshToken) || ! "refreshToken".equals(jwtService.extractTokenType(refreshToken))) { - return AuthenticationResponse.builder() + public AuthenticationResponse getAccessTokenUsingRefreshToken(AuthenticationRequest request){ + + final var refreshToken = request.getRefreshToken(); + + final String userEmail = jwtService.extractUserEmail(refreshToken); + final Long firstIat = jwtService.extractFirstIat(refreshToken); + final Long thisIat = jwtService.extractThisIat(refreshToken); + final String tokenType = jwtService.extractTokenType(refreshToken); + + + if ( + ! "refreshToken".equals(tokenType) + || firstIat == null + || thisIat == null + || userEmail == null + || ! jwtService.isTokenValid(refreshToken) + ) { + return AuthenticationResponse.builder() .message("Invalid refresh token.") .build(); } - var accessToken = buildAccessToken(jwtService.extractUserEmail(refreshToken)); - var newRefreshToken = buildRefreshToken(jwtService.extractUserEmail(refreshToken)); + + // check if issuedAt before lastUsageDate (invalid / exceeded the usage limit / revoked) + + final Long lastUsage = getRTokenLastUsage(userEmail + firstIat); + + System.out.println("firstIat: " + firstIat ); + System.out.println("lastUsage: " + lastUsage ); + System.out.println("thisIat: " + thisIat ); + + if (lastUsage != null && lastUsage > thisIat){ + // add last usage to redis + addRTokenLastUsage(userEmail + firstIat); + + return AuthenticationResponse.builder() + .message("The refresh token has exceeded the usage limit.") + .build(); + } + + + // refresh last usage + addRTokenLastUsage(userEmail + firstIat); + + + var newRefreshToken = buildRefreshToken(userEmail, String.valueOf(firstIat)); + var accessToken = buildAccessToken(userEmail); + + return AuthenticationResponse.builder() .accessToken(accessToken) .refreshToken(newRefreshToken) - .message("Access token successfully generated.") + .message("Succeed.") .build(); } - public AuthenticationResponse renewRefreshToken(AuthenticationRequest request) { - var refreshToken = request.getRefreshToken(); - if (! jwtService.isTokenValid(refreshToken) || ! "refreshToken".equals(jwtService.extractTokenType(refreshToken))) { + public AuthenticationResponse revokeRefreshToken(AuthenticationRequest request) { + final var refreshToken = request.getRefreshToken(); + + final String userEmail = jwtService.extractUserEmail(refreshToken); + final Long firstIat = jwtService.extractFirstIat(refreshToken); + final String tokenType = jwtService.extractTokenType(refreshToken); + + if ( + ! "refreshToken".equals(tokenType) + || firstIat == null + || userEmail == null + || ! jwtService.isTokenValid(refreshToken) + ) { return AuthenticationResponse.builder() .message("Invalid refresh token.") .build(); } - revokeRefreshToken(request); - var newRefreshToken = buildRefreshToken(jwtService.extractUserEmail(refreshToken)); - return AuthenticationResponse.builder() - .refreshToken(newRefreshToken) - .message("Refresh token successfully renewed.") - .build(); - } + // add last usage to redis + addRTokenLastUsage(userEmail + firstIat); - public AuthenticationResponse revokeRefreshToken(AuthenticationRequest request) {// TODO - return null; + return AuthenticationResponse.builder() + .message("Revoked.") + .build(); } } diff --git a/@core/authentication-service/src/main/java/org/flowio/authenticationservice/service/JwtService.java b/@core/authentication-service/src/main/java/org/flowio/authenticationservice/service/JwtService.java index 3fd4537..725bd0a 100644 --- a/@core/authentication-service/src/main/java/org/flowio/authenticationservice/service/JwtService.java +++ b/@core/authentication-service/src/main/java/org/flowio/authenticationservice/service/JwtService.java @@ -19,6 +19,7 @@ public class JwtService { private static final String SECRET_KEY = "404E635266556A586E3272357538782F413F4428472B4B6250645367566B5970"; // todo use environment variable + public String extractUserEmail(String jwtToken) { return extractClaim(jwtToken, Claims::getSubject); } @@ -30,14 +31,14 @@ public T extractClaim(String jwtToken, Function claimsResolver) { public String generateJwtToken( Map extraClaims, - int expirationIn, + long expirationIn, String username ) { return Jwts .builder() .setClaims(extraClaims) .setSubject(username) - .setIssuedAt(new Date(System.currentTimeMillis())) +// .setIssuedAt(new Date(System.currentTimeMillis())) .setExpiration(new Date(System.currentTimeMillis() + expirationIn)) // an hour .signWith(getSignInKey(), SignatureAlgorithm.HS256) .compact(); @@ -57,10 +58,30 @@ public String extractTokenType(String jwtToken) { return tokenType.toString(); } + public Long extractFirstIat(String jwtToken) { + var iat = extractClaim(jwtToken, claims -> claims.get("firstIat")); + if (iat != null) { + return Long.parseLong(iat.toString()); + } + return null; + } + + public Long extractThisIat(String jwtToken) { + var iat = extractClaim(jwtToken, claims -> claims.get("thisIat")); + if (iat != null) { + return Long.parseLong(iat.toString()); + } + return null; + } + private Date extractExpiration(String jwtToken) { return extractClaim(jwtToken, Claims::getExpiration); } + public Date extractIssuedAt(String jwtToken) { + return extractClaim(jwtToken, Claims::getIssuedAt); + } + private Claims extractAllClaims(String token) { return Jwts .parserBuilder() diff --git a/@core/authentication-service/src/main/java/org/flowio/authenticationservice/service/RedisService.java b/@core/authentication-service/src/main/java/org/flowio/authenticationservice/service/RedisService.java new file mode 100644 index 0000000..5e1145a --- /dev/null +++ b/@core/authentication-service/src/main/java/org/flowio/authenticationservice/service/RedisService.java @@ -0,0 +1,24 @@ +package org.flowio.authenticationservice.service; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Service; + +import java.util.concurrent.TimeUnit; + +@Service +public class RedisService { + @Autowired + private StringRedisTemplate template; + + public void addKeyValue(String key, Long value, long timeout) { + template.opsForValue().set(key, value.toString(), timeout, TimeUnit.MILLISECONDS); + } + + public Long getValue(String key) { + String value = template.opsForValue().get(key); + return value != null ? Long.parseLong(value) : null; + } +} + diff --git a/@core/authentication-service/src/main/resources/application.yml b/@core/authentication-service/src/main/resources/application.yml index 4745606..30f7878 100644 --- a/@core/authentication-service/src/main/resources/application.yml +++ b/@core/authentication-service/src/main/resources/application.yml @@ -13,4 +13,16 @@ spring: format_sql: true dialect: org.hibernate.dialect.PostgreSQLDialect database: postgresql - database-platform: org.hibernate.dialect.PostgreSQLDialect \ No newline at end of file + database-platform: org.hibernate.dialect.PostgreSQLDialect + data: + redis: + host: redis + port: 6379 + database: 0 + password: + jedis: + pool: + max-wait: -1 + max-idle: 10 + min-idle: 0 + timeout: 1000 From cb59835dae107da817b3b0b52aa52731cfb30a67 Mon Sep 17 00:00:00 2001 From: "wangziyuan.12" Date: Thu, 14 Dec 2023 12:36:35 -0500 Subject: [PATCH 13/16] refactor: refactor codes --- @core/authentication-service/README.md | 6 +- .../filter/JWTAuthenticationFilter.java | 26 +++-- .../service/AuthenticationService.java | 108 +++++------------- .../service/JwtService.java | 45 ++++++-- .../service/RedisService.java | 39 ++++++- 5 files changed, 119 insertions(+), 105 deletions(-) diff --git a/@core/authentication-service/README.md b/@core/authentication-service/README.md index f5c7f72..df74d1b 100644 --- a/@core/authentication-service/README.md +++ b/@core/authentication-service/README.md @@ -52,7 +52,7 @@ and retrieve an accessToken and a refreshToken. { "refreshToken": "new token", "accessToken": "new token", - "message": "Successfully registered." + "message": "Succeed." } ``` @@ -75,7 +75,7 @@ and retrieve an access token and a refresh token. { "refreshToken": "new token", "accessToken": "new token", - "message": "Successfully authenticated." + "message": "Succeed." } ``` @@ -98,7 +98,7 @@ it won't revoke the old AccessToken, but will revoke the one time RefreshToken. { "refreshToken": "new token", "accessToken": "new token", - "message": "Access token successfully generated." + "message": "Succeed." } ``` diff --git a/@core/authentication-service/src/main/java/org/flowio/authenticationservice/filter/JWTAuthenticationFilter.java b/@core/authentication-service/src/main/java/org/flowio/authenticationservice/filter/JWTAuthenticationFilter.java index 4d79412..0dbe6bf 100644 --- a/@core/authentication-service/src/main/java/org/flowio/authenticationservice/filter/JWTAuthenticationFilter.java +++ b/@core/authentication-service/src/main/java/org/flowio/authenticationservice/filter/JWTAuthenticationFilter.java @@ -5,6 +5,7 @@ import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; +import org.flowio.authenticationservice.dto.AuthenticationResponse; import org.flowio.authenticationservice.service.JwtService; import org.springframework.lang.NonNull; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; @@ -78,18 +79,23 @@ protected void doFilterInternal( return; } + if (! jwtService.isTokenValid(jwt)) { + filterChain.doFilter(request, response); + return; + } - if (jwtService.isTokenValid(jwt)) { - UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken( - userDetails, - null, - userDetails.getAuthorities() - ); - authToken.setDetails( + + // valid token + + UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken( + userDetails, + null, + userDetails.getAuthorities() + ); + authToken.setDetails( new WebAuthenticationDetailsSource().buildDetails(request) - ); - SecurityContextHolder.getContext().setAuthentication(authToken); - } + ); + SecurityContextHolder.getContext().setAuthentication(authToken); // Forward the request to the next filter in the chain filterChain.doFilter(request, response); diff --git a/@core/authentication-service/src/main/java/org/flowio/authenticationservice/service/AuthenticationService.java b/@core/authentication-service/src/main/java/org/flowio/authenticationservice/service/AuthenticationService.java index 0d1f9c7..1566237 100644 --- a/@core/authentication-service/src/main/java/org/flowio/authenticationservice/service/AuthenticationService.java +++ b/@core/authentication-service/src/main/java/org/flowio/authenticationservice/service/AuthenticationService.java @@ -13,8 +13,6 @@ import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; -import java.text.SimpleDateFormat; -import java.util.Date; import java.util.HashMap; import java.util.stream.Stream; @@ -26,53 +24,24 @@ public class AuthenticationService { private final PasswordEncoder passwordEncoder; private final JwtService jwtService; private final AuthenticationManager authenticationManager; - private final RedisService redisService; + // TODO read from yml private final long accessTokenExpirationTime = 1000 * 60 * 10; // 10 mins private final long refreshTokenExpirationTime = 1000 * 60 * 60 * 24; // 1 day - - private String buildAccessToken(LoginUser loginUser) { - return buildAccessToken(loginUser.getLoginEmail()); - } - - private String buildAccessToken(String loginEmail) { - return jwtService.generateJwtToken( - new HashMap<>(){{ put("tokenType", "accessToken"); }}, - accessTokenExpirationTime, - loginEmail - ); - } - - private String buildRefreshToken(LoginUser loginUser, String firstIat) { - return buildRefreshToken(loginUser.getLoginEmail(), firstIat); + private AuthenticationResponse buildTokenResponse(String userEmail, String familyId) { + var newRefreshToken = jwtService.buildRefreshToken(userEmail, familyId); + var accessToken = jwtService.buildAccessToken(userEmail, familyId); + return AuthenticationResponse.builder() + .accessToken(accessToken) + .refreshToken(newRefreshToken) + .message("Succeed.") + .build(); } - private String buildRefreshToken(String loginEmail, String firstIat) { - return jwtService.generateJwtToken( - new HashMap<>(){{ - put("tokenType", "refreshToken"); - put("firstIat", firstIat); - put("thisIat", System.currentTimeMillis()); - }}, - refreshTokenExpirationTime, - loginEmail - ); - } - private void addRTokenLastUsage(String key){ - redisService.addKeyValue( - key, - System.currentTimeMillis(), - refreshTokenExpirationTime - ); - } - - private Long getRTokenLastUsage(String key){ - return redisService.getValue(key); - } public AuthenticationResponse register(RegisterRequest request) { @@ -82,7 +51,7 @@ public AuthenticationResponse register(RegisterRequest request) { request.getLoginEmail(), request.getPassword() ) - .anyMatch(str -> str == null || str.isEmpty())) { + .anyMatch(str -> str == null || str.length() <= 2)) { return AuthenticationResponse.builder() .message("Firstname, lastname, login email and password must not be null or empty.") .build(); @@ -96,13 +65,7 @@ public AuthenticationResponse register(RegisterRequest request) { .role(Role.USER) .build(); loginUserRepository.save(loginUser); - var accessToken = buildAccessToken(loginUser); - var refreshToken = buildRefreshToken(loginUser, String.valueOf(System.currentTimeMillis())); - return AuthenticationResponse.builder() - .accessToken(accessToken) - .refreshToken(refreshToken) - .message("Successfully registered.") - .build(); + return buildTokenResponse(loginUser.getLoginEmail(), String.valueOf(System.currentTimeMillis())); } public AuthenticationResponse authenticate(AuthenticationRequest request) { @@ -114,13 +77,7 @@ public AuthenticationResponse authenticate(AuthenticationRequest request) { ); var loginUser = loginUserRepository.findByLoginEmail(request.getLoginEmail()) .orElseThrow(); - var accessToken = buildAccessToken(loginUser); - var refreshToken = buildRefreshToken(loginUser, String.valueOf(System.currentTimeMillis())); - return AuthenticationResponse.builder() - .accessToken(accessToken) - .refreshToken(refreshToken) - .message("Successfully authenticated.") - .build(); + return buildTokenResponse(loginUser.getLoginEmail(), String.valueOf(System.currentTimeMillis())); } public AuthenticationResponse getAccessTokenUsingRefreshToken(AuthenticationRequest request){ @@ -128,15 +85,16 @@ public AuthenticationResponse getAccessTokenUsingRefreshToken(AuthenticationRequ final var refreshToken = request.getRefreshToken(); final String userEmail = jwtService.extractUserEmail(refreshToken); - final Long firstIat = jwtService.extractFirstIat(refreshToken); - final Long thisIat = jwtService.extractThisIat(refreshToken); + final Long familyId = jwtService.extractFamilyId(refreshToken); + final String familyIdStr = String.valueOf(familyId); + final Long myIat = jwtService.extractMyIat(refreshToken); final String tokenType = jwtService.extractTokenType(refreshToken); if ( ! "refreshToken".equals(tokenType) - || firstIat == null - || thisIat == null + || familyId == null + || myIat == null || userEmail == null || ! jwtService.isTokenValid(refreshToken) ) { @@ -147,15 +105,13 @@ public AuthenticationResponse getAccessTokenUsingRefreshToken(AuthenticationRequ // check if issuedAt before lastUsageDate (invalid / exceeded the usage limit / revoked) - final Long lastUsage = getRTokenLastUsage(userEmail + firstIat); - System.out.println("firstIat: " + firstIat ); - System.out.println("lastUsage: " + lastUsage ); - System.out.println("thisIat: " + thisIat ); + final Long lastUsage = redisService.getRefreshTLastUsage(userEmail, familyIdStr); - if (lastUsage != null && lastUsage > thisIat){ + if (lastUsage != null && lastUsage > myIat){ // add last usage to redis - addRTokenLastUsage(userEmail + firstIat); + redisService.addRefreshTLastUsage(userEmail, familyIdStr); + redisService.addAccessTLastUsage(userEmail, familyIdStr); return AuthenticationResponse.builder() .message("The refresh token has exceeded the usage limit.") @@ -164,31 +120,22 @@ public AuthenticationResponse getAccessTokenUsingRefreshToken(AuthenticationRequ // refresh last usage - addRTokenLastUsage(userEmail + firstIat); - - - var newRefreshToken = buildRefreshToken(userEmail, String.valueOf(firstIat)); - var accessToken = buildAccessToken(userEmail); - - - return AuthenticationResponse.builder() - .accessToken(accessToken) - .refreshToken(newRefreshToken) - .message("Succeed.") - .build(); + redisService.addRefreshTLastUsage(userEmail, familyIdStr); + return buildTokenResponse(userEmail, familyIdStr); } public AuthenticationResponse revokeRefreshToken(AuthenticationRequest request) { final var refreshToken = request.getRefreshToken(); final String userEmail = jwtService.extractUserEmail(refreshToken); - final Long firstIat = jwtService.extractFirstIat(refreshToken); + final Long familyId = jwtService.extractFamilyId(refreshToken); + final String familyIdStr = String.valueOf(familyId); final String tokenType = jwtService.extractTokenType(refreshToken); if ( ! "refreshToken".equals(tokenType) - || firstIat == null + || familyId == null || userEmail == null || ! jwtService.isTokenValid(refreshToken) ) { @@ -198,7 +145,8 @@ public AuthenticationResponse revokeRefreshToken(AuthenticationRequest request) } // add last usage to redis - addRTokenLastUsage(userEmail + firstIat); + redisService.addRefreshTLastUsage(userEmail, familyIdStr); + redisService.addAccessTLastUsage(userEmail, familyIdStr); return AuthenticationResponse.builder() .message("Revoked.") diff --git a/@core/authentication-service/src/main/java/org/flowio/authenticationservice/service/JwtService.java b/@core/authentication-service/src/main/java/org/flowio/authenticationservice/service/JwtService.java index 725bd0a..357375d 100644 --- a/@core/authentication-service/src/main/java/org/flowio/authenticationservice/service/JwtService.java +++ b/@core/authentication-service/src/main/java/org/flowio/authenticationservice/service/JwtService.java @@ -5,6 +5,7 @@ import io.jsonwebtoken.SignatureAlgorithm; import io.jsonwebtoken.io.Decoders; import io.jsonwebtoken.security.Keys; +import org.flowio.authenticationservice.model.LoginUser; import org.springframework.stereotype.Service; import java.security.Key; @@ -18,7 +19,37 @@ public class JwtService { private static final String SECRET_KEY = "404E635266556A586E3272357538782F413F4428472B4B6250645367566B5970"; // todo use environment variable + // TODO read from yml + private final long accessTokenExpirationTime = 1000 * 60 * 10; // 10 mins + private final long refreshTokenExpirationTime = 1000 * 60 * 60 * 24; // 1 day + private String buildAccessToken(LoginUser loginUser, String familyId) { + return buildAccessToken(loginUser.getLoginEmail(), familyId); + } + + String buildAccessToken(String loginEmail, String familyId) { + return generateJwtToken( + new HashMap<>(){{ + put("tokenType", "accessToken"); + put("familyId", familyId); + put("myIat", String.valueOf(System.currentTimeMillis())); + }}, + accessTokenExpirationTime, + loginEmail + ); + } + + String buildRefreshToken(String loginEmail, String familyId) { + return generateJwtToken( + new HashMap<>() {{ + put("tokenType", "refreshToken"); + put("familyId", familyId); + put("myIat", String.valueOf(System.currentTimeMillis())); + }}, + refreshTokenExpirationTime, + loginEmail + ); + } public String extractUserEmail(String jwtToken) { return extractClaim(jwtToken, Claims::getSubject); @@ -29,7 +60,7 @@ public T extractClaim(String jwtToken, Function claimsResolver) { return claimsResolver.apply(claims); } - public String generateJwtToken( + private String generateJwtToken( Map extraClaims, long expirationIn, String username @@ -58,16 +89,16 @@ public String extractTokenType(String jwtToken) { return tokenType.toString(); } - public Long extractFirstIat(String jwtToken) { - var iat = extractClaim(jwtToken, claims -> claims.get("firstIat")); + public Long extractFamilyId(String jwtToken) { + var iat = extractClaim(jwtToken, claims -> claims.get("familyId")); if (iat != null) { return Long.parseLong(iat.toString()); } return null; } - public Long extractThisIat(String jwtToken) { - var iat = extractClaim(jwtToken, claims -> claims.get("thisIat")); + public Long extractMyIat(String jwtToken) { + var iat = extractClaim(jwtToken, claims -> claims.get("myIat")); if (iat != null) { return Long.parseLong(iat.toString()); } @@ -78,10 +109,6 @@ private Date extractExpiration(String jwtToken) { return extractClaim(jwtToken, Claims::getExpiration); } - public Date extractIssuedAt(String jwtToken) { - return extractClaim(jwtToken, Claims::getIssuedAt); - } - private Claims extractAllClaims(String token) { return Jwts .parserBuilder() diff --git a/@core/authentication-service/src/main/java/org/flowio/authenticationservice/service/RedisService.java b/@core/authentication-service/src/main/java/org/flowio/authenticationservice/service/RedisService.java index 5e1145a..d99272c 100644 --- a/@core/authentication-service/src/main/java/org/flowio/authenticationservice/service/RedisService.java +++ b/@core/authentication-service/src/main/java/org/flowio/authenticationservice/service/RedisService.java @@ -1,7 +1,6 @@ package org.flowio.authenticationservice.service; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Service; @@ -9,16 +8,50 @@ @Service public class RedisService { + + // TODO read from yml + private final long accessTokenExpirationTime = 1000 * 60 * 10; // 10 mins + private final long refreshTokenExpirationTime = 1000 * 60 * 60 * 24; // 1 day + @Autowired private StringRedisTemplate template; - public void addKeyValue(String key, Long value, long timeout) { + private void addKeyValue(String key, Long value, long timeout) { template.opsForValue().set(key, value.toString(), timeout, TimeUnit.MILLISECONDS); } - public Long getValue(String key) { + private Long getValue(String key) { String value = template.opsForValue().get(key); return value != null ? Long.parseLong(value) : null; } + + + void addRefreshTLastUsage(String userEmail, String familyId){ + String key = userEmail + familyId + 'R'; + addKeyValue( + key, + System.currentTimeMillis(), + refreshTokenExpirationTime + ); + } + + void addAccessTLastUsage(String userEmail, String familyId){ + String key = userEmail + familyId + 'A'; + addKeyValue( + key, + System.currentTimeMillis(), + accessTokenExpirationTime + ); + } + + Long getRefreshTLastUsage(String userEmail, String familyId){ + String key = userEmail + familyId + 'R'; + return getValue(key); + } + + Long getAccessTLastUsage(String userEmail, String familyId){ + String key = userEmail + familyId + 'A'; + return getValue(key); + } } From 8de9ea7a71e49480b4cf2601a4cf77c60e868dba Mon Sep 17 00:00:00 2001 From: "wangziyuan.12" Date: Thu, 14 Dec 2023 20:52:25 -0500 Subject: [PATCH 14/16] bugfix: fix race condition in refresh token rotation --- .../controller/AuthenticationController.java | 4 +- .../service/AuthenticationService.java | 16 ++---- .../service/RedisService.java | 49 +++++++++++-------- 3 files changed, 35 insertions(+), 34 deletions(-) diff --git a/@core/authentication-service/src/main/java/org/flowio/authenticationservice/controller/AuthenticationController.java b/@core/authentication-service/src/main/java/org/flowio/authenticationservice/controller/AuthenticationController.java index 84b69bd..cc1c0cf 100644 --- a/@core/authentication-service/src/main/java/org/flowio/authenticationservice/controller/AuthenticationController.java +++ b/@core/authentication-service/src/main/java/org/flowio/authenticationservice/controller/AuthenticationController.java @@ -35,14 +35,14 @@ public ResponseEntity register( @PostMapping("/get-access-token") public ResponseEntity getAccessToken( @RequestBody AuthenticationRequest request - ) { + ) throws Exception { return ResponseEntity.ok(service.getAccessTokenUsingRefreshToken(request)); } @PostMapping("/revoke-refresh-token") public ResponseEntity revokeRefreshToken( @RequestBody AuthenticationRequest request - ) { + ) throws Exception { return ResponseEntity.ok(service.revokeRefreshToken(request)); } } diff --git a/@core/authentication-service/src/main/java/org/flowio/authenticationservice/service/AuthenticationService.java b/@core/authentication-service/src/main/java/org/flowio/authenticationservice/service/AuthenticationService.java index 1566237..9376512 100644 --- a/@core/authentication-service/src/main/java/org/flowio/authenticationservice/service/AuthenticationService.java +++ b/@core/authentication-service/src/main/java/org/flowio/authenticationservice/service/AuthenticationService.java @@ -13,7 +13,6 @@ import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; -import java.util.HashMap; import java.util.stream.Stream; @Service @@ -80,7 +79,7 @@ public AuthenticationResponse authenticate(AuthenticationRequest request) { return buildTokenResponse(loginUser.getLoginEmail(), String.valueOf(System.currentTimeMillis())); } - public AuthenticationResponse getAccessTokenUsingRefreshToken(AuthenticationRequest request){ + public AuthenticationResponse getAccessTokenUsingRefreshToken(AuthenticationRequest request) throws Exception { final var refreshToken = request.getRefreshToken(); @@ -106,22 +105,15 @@ public AuthenticationResponse getAccessTokenUsingRefreshToken(AuthenticationRequ // check if issuedAt before lastUsageDate (invalid / exceeded the usage limit / revoked) - final Long lastUsage = redisService.getRefreshTLastUsage(userEmail, familyIdStr); + final Long lastUsage = redisService.getAndUpdateTokenLastUsage(userEmail, familyIdStr, 'R'); if (lastUsage != null && lastUsage > myIat){ - // add last usage to redis - redisService.addRefreshTLastUsage(userEmail, familyIdStr); - redisService.addAccessTLastUsage(userEmail, familyIdStr); - return AuthenticationResponse.builder() .message("The refresh token has exceeded the usage limit.") .build(); } - // refresh last usage - redisService.addRefreshTLastUsage(userEmail, familyIdStr); - return buildTokenResponse(userEmail, familyIdStr); } @@ -145,8 +137,8 @@ public AuthenticationResponse revokeRefreshToken(AuthenticationRequest request) } // add last usage to redis - redisService.addRefreshTLastUsage(userEmail, familyIdStr); - redisService.addAccessTLastUsage(userEmail, familyIdStr); +// redisService.getAndUpdateTokenLastUsage(userEmail, familyIdStr, 'R'); + redisService.addTokenLastUsage(userEmail, familyIdStr, 'R'); return AuthenticationResponse.builder() .message("Revoked.") diff --git a/@core/authentication-service/src/main/java/org/flowio/authenticationservice/service/RedisService.java b/@core/authentication-service/src/main/java/org/flowio/authenticationservice/service/RedisService.java index d99272c..bd5f56d 100644 --- a/@core/authentication-service/src/main/java/org/flowio/authenticationservice/service/RedisService.java +++ b/@core/authentication-service/src/main/java/org/flowio/authenticationservice/service/RedisService.java @@ -1,6 +1,9 @@ package org.flowio.authenticationservice.service; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.dao.DataAccessException; +import org.springframework.data.redis.core.RedisOperations; +import org.springframework.data.redis.core.SessionCallback; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Service; @@ -13,9 +16,22 @@ public class RedisService { private final long accessTokenExpirationTime = 1000 * 60 * 10; // 10 mins private final long refreshTokenExpirationTime = 1000 * 60 * 60 * 24; // 1 day + private final int lockTimeout = 1000; + @Autowired private StringRedisTemplate template; + + // Locking methods + public boolean acquireLock(String lockKey) { + String lockValue = "lock"; + return Boolean.TRUE.equals(template.opsForValue().setIfAbsent(lockKey, lockValue, lockTimeout, TimeUnit.MILLISECONDS)); + } + + public void releaseLock(String lockKey) { + template.delete(lockKey); + } + private void addKeyValue(String key, Long value, long timeout) { template.opsForValue().set(key, value.toString(), timeout, TimeUnit.MILLISECONDS); } @@ -25,33 +41,26 @@ private Long getValue(String key) { return value != null ? Long.parseLong(value) : null; } + public Long getSetWithTimeout(String key, Long newValue, long timeout) { + String oldValueAsString = template.opsForValue().getAndSet(key, newValue.toString()); + template.expire(key, timeout, TimeUnit.MILLISECONDS); + return oldValueAsString != null ? Long.parseLong(oldValueAsString) : null; + } - void addRefreshTLastUsage(String userEmail, String familyId){ - String key = userEmail + familyId + 'R'; - addKeyValue( - key, - System.currentTimeMillis(), - refreshTokenExpirationTime - ); + Long getAndUpdateTokenLastUsage(String userEmail, String familyId, Character appendix) { + String Key = userEmail + familyId + appendix; + return getSetWithTimeout(Key, System.currentTimeMillis(), refreshTokenExpirationTime); } - void addAccessTLastUsage(String userEmail, String familyId){ - String key = userEmail + familyId + 'A'; + + void addTokenLastUsage(String userEmail, String familyId, Character appendix){ + String valueKey = userEmail + familyId + appendix; addKeyValue( - key, + valueKey, System.currentTimeMillis(), - accessTokenExpirationTime + refreshTokenExpirationTime ); } - Long getRefreshTLastUsage(String userEmail, String familyId){ - String key = userEmail + familyId + 'R'; - return getValue(key); - } - - Long getAccessTLastUsage(String userEmail, String familyId){ - String key = userEmail + familyId + 'A'; - return getValue(key); - } } From 908f863027ce383626192caa0ad624a289f07648 Mon Sep 17 00:00:00 2001 From: "wangziyuan.12" Date: Thu, 14 Dec 2023 22:49:10 -0500 Subject: [PATCH 15/16] feature: read secret_key from the environment --- @core/authentication-service/README.md | 7 +++++-- .../authentication-service/docker-compose.yml | 2 ++ .../config/JwtConfig.java | 17 ++++++++++++++++ .../service/JwtService.java | 15 +++++++------- .../service/RedisService.java | 20 ------------------- .../src/main/resources/application.yml | 7 +++++++ 6 files changed, 39 insertions(+), 29 deletions(-) create mode 100644 @core/authentication-service/src/main/java/org/flowio/authenticationservice/config/JwtConfig.java diff --git a/@core/authentication-service/README.md b/@core/authentication-service/README.md index df74d1b..10e63d5 100644 --- a/@core/authentication-service/README.md +++ b/@core/authentication-service/README.md @@ -17,9 +17,12 @@ This README provides information about a JWT token-based authentication service. 5. Redis for refresh token ## How to Run -### 1. Set up Secret_Key environment (TODO) - +### 1. Set up Secret_Key environment +**Replace the Secret key below in production.** +```bash +export SECRET_KEY='404E635266556A586E3272357538782F413F4428472B4B6250645367566B5970' +``` ### 2. Docker Compose Up diff --git a/@core/authentication-service/docker-compose.yml b/@core/authentication-service/docker-compose.yml index b18e530..320cd39 100644 --- a/@core/authentication-service/docker-compose.yml +++ b/@core/authentication-service/docker-compose.yml @@ -24,6 +24,8 @@ services: - 8080:8080 networks: - flowio-auth-net + environment: + - SECRET_KEY=${SECRET_KEY} diff --git a/@core/authentication-service/src/main/java/org/flowio/authenticationservice/config/JwtConfig.java b/@core/authentication-service/src/main/java/org/flowio/authenticationservice/config/JwtConfig.java new file mode 100644 index 0000000..473174c --- /dev/null +++ b/@core/authentication-service/src/main/java/org/flowio/authenticationservice/config/JwtConfig.java @@ -0,0 +1,17 @@ +package org.flowio.authenticationservice.config; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Configuration +@ConfigurationProperties(prefix = "jwt") +@RequiredArgsConstructor +@Getter +public class JwtConfig { + + @Value("${jwt.secretKey}") + private String secretKey; +} \ No newline at end of file diff --git a/@core/authentication-service/src/main/java/org/flowio/authenticationservice/service/JwtService.java b/@core/authentication-service/src/main/java/org/flowio/authenticationservice/service/JwtService.java index 357375d..d641a37 100644 --- a/@core/authentication-service/src/main/java/org/flowio/authenticationservice/service/JwtService.java +++ b/@core/authentication-service/src/main/java/org/flowio/authenticationservice/service/JwtService.java @@ -5,7 +5,9 @@ import io.jsonwebtoken.SignatureAlgorithm; import io.jsonwebtoken.io.Decoders; import io.jsonwebtoken.security.Keys; -import org.flowio.authenticationservice.model.LoginUser; +import lombok.RequiredArgsConstructor; + +import org.flowio.authenticationservice.config.JwtConfig; import org.springframework.stereotype.Service; import java.security.Key; @@ -15,18 +17,17 @@ import java.util.function.Function; @Service +@RequiredArgsConstructor public class JwtService { - private static final String SECRET_KEY = "404E635266556A586E3272357538782F413F4428472B4B6250645367566B5970"; // todo use environment variable + + private final JwtConfig jwtConfig; + // TODO read from yml private final long accessTokenExpirationTime = 1000 * 60 * 10; // 10 mins private final long refreshTokenExpirationTime = 1000 * 60 * 60 * 24; // 1 day - private String buildAccessToken(LoginUser loginUser, String familyId) { - return buildAccessToken(loginUser.getLoginEmail(), familyId); - } - String buildAccessToken(String loginEmail, String familyId) { return generateJwtToken( new HashMap<>(){{ @@ -119,7 +120,7 @@ private Claims extractAllClaims(String token) { } private Key getSignInKey() { - byte[] keyBytes = Decoders.BASE64.decode(SECRET_KEY); + byte[] keyBytes = Decoders.BASE64.decode(jwtConfig.getSecretKey()); return Keys.hmacShaKeyFor(keyBytes); } diff --git a/@core/authentication-service/src/main/java/org/flowio/authenticationservice/service/RedisService.java b/@core/authentication-service/src/main/java/org/flowio/authenticationservice/service/RedisService.java index bd5f56d..c6136ba 100644 --- a/@core/authentication-service/src/main/java/org/flowio/authenticationservice/service/RedisService.java +++ b/@core/authentication-service/src/main/java/org/flowio/authenticationservice/service/RedisService.java @@ -1,9 +1,6 @@ package org.flowio.authenticationservice.service; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.dao.DataAccessException; -import org.springframework.data.redis.core.RedisOperations; -import org.springframework.data.redis.core.SessionCallback; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Service; @@ -16,31 +13,14 @@ public class RedisService { private final long accessTokenExpirationTime = 1000 * 60 * 10; // 10 mins private final long refreshTokenExpirationTime = 1000 * 60 * 60 * 24; // 1 day - private final int lockTimeout = 1000; - @Autowired private StringRedisTemplate template; - // Locking methods - public boolean acquireLock(String lockKey) { - String lockValue = "lock"; - return Boolean.TRUE.equals(template.opsForValue().setIfAbsent(lockKey, lockValue, lockTimeout, TimeUnit.MILLISECONDS)); - } - - public void releaseLock(String lockKey) { - template.delete(lockKey); - } - private void addKeyValue(String key, Long value, long timeout) { template.opsForValue().set(key, value.toString(), timeout, TimeUnit.MILLISECONDS); } - private Long getValue(String key) { - String value = template.opsForValue().get(key); - return value != null ? Long.parseLong(value) : null; - } - public Long getSetWithTimeout(String key, Long newValue, long timeout) { String oldValueAsString = template.opsForValue().getAndSet(key, newValue.toString()); template.expire(key, timeout, TimeUnit.MILLISECONDS); diff --git a/@core/authentication-service/src/main/resources/application.yml b/@core/authentication-service/src/main/resources/application.yml index 30f7878..5178d6b 100644 --- a/@core/authentication-service/src/main/resources/application.yml +++ b/@core/authentication-service/src/main/resources/application.yml @@ -1,3 +1,6 @@ +jwt: + secretKey: ${SECRET_KEY} + spring: datasource: url: jdbc:postgresql://some-postgres:5432/flowio-authentication @@ -26,3 +29,7 @@ spring: max-idle: 10 min-idle: 0 timeout: 1000 + +server: + port: 8080 + From 497dbccce5eac0836d69b463ccfd1bfd09c82b3c Mon Sep 17 00:00:00 2001 From: your_username Date: Sat, 30 Dec 2023 20:55:49 -0500 Subject: [PATCH 16/16] refactor: Add try catch in authenticate --- .../service/AuthenticationService.java | 33 ++++++++++++++----- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/@core/authentication-service/src/main/java/org/flowio/authenticationservice/service/AuthenticationService.java b/@core/authentication-service/src/main/java/org/flowio/authenticationservice/service/AuthenticationService.java index 9376512..14e6003 100644 --- a/@core/authentication-service/src/main/java/org/flowio/authenticationservice/service/AuthenticationService.java +++ b/@core/authentication-service/src/main/java/org/flowio/authenticationservice/service/AuthenticationService.java @@ -67,18 +67,35 @@ public AuthenticationResponse register(RegisterRequest request) { return buildTokenResponse(loginUser.getLoginEmail(), String.valueOf(System.currentTimeMillis())); } + /** + * Authenticate user and return access token and refresh token. + * @param request login email and password + * @return access token and refresh token + */ public AuthenticationResponse authenticate(AuthenticationRequest request) { - authenticationManager.authenticate( - new UsernamePasswordAuthenticationToken( - request.getLoginEmail(), - request.getPassword() - ) - ); - var loginUser = loginUserRepository.findByLoginEmail(request.getLoginEmail()) + try { + authenticationManager.authenticate( + new UsernamePasswordAuthenticationToken( + request.getLoginEmail(), + request.getPassword() + ) + ); + var loginUser = loginUserRepository.findByLoginEmail(request.getLoginEmail()) .orElseThrow(); - return buildTokenResponse(loginUser.getLoginEmail(), String.valueOf(System.currentTimeMillis())); + return buildTokenResponse(loginUser.getLoginEmail(), String.valueOf(System.currentTimeMillis())); + } catch (Exception e) { + return AuthenticationResponse.builder() + .message("Invalid login email or password.") + .build(); + } } + /** + * Get access token using refresh token. + * @param request AuthenticationRequest + * @return access token and refresh token + * @throws Exception + */ public AuthenticationResponse getAccessTokenUsingRefreshToken(AuthenticationRequest request) throws Exception { final var refreshToken = request.getRefreshToken();