├── .gitattributes ├── .gitignore ├── .mvn └── wrapper │ └── maven-wrapper.properties ├── README.md ├── mvnw ├── mvnw.cmd ├── pom.xml └── src ├── main ├── java │ └── dev │ │ └── danvega │ │ ├── Application.java │ │ ├── CsrfHiddenInput.java │ │ ├── CsrfTokenAdvice.java │ │ ├── DashboardController.java │ │ ├── LoginController.java │ │ └── SecurityConfig.java ├── jte │ ├── .jteroot │ ├── layout │ │ └── default.jte │ └── pages │ │ ├── dashboard.jte │ │ ├── home.jte │ │ └── login.jte └── resources │ └── application.yaml └── test └── java └── dev └── danvega └── JteLoginApplicationTests.java /.gitattributes: -------------------------------------------------------------------------------- 1 | /mvnw text eol=lf 2 | *.cmd text eol=crlf 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | target/ 3 | !.mvn/wrapper/maven-wrapper.jar 4 | !**/src/main/**/target/ 5 | !**/src/test/**/target/ 6 | 7 | ### STS ### 8 | .apt_generated 9 | .classpath 10 | .factorypath 11 | .project 12 | .settings 13 | .springBeans 14 | .sts4-cache 15 | 16 | ### IntelliJ IDEA ### 17 | .idea 18 | *.iws 19 | *.iml 20 | *.ipr 21 | 22 | ### NetBeans ### 23 | /nbproject/private/ 24 | /nbbuild/ 25 | /dist/ 26 | /nbdist/ 27 | /.nb-gradle/ 28 | build/ 29 | !**/src/main/**/build/ 30 | !**/src/test/**/build/ 31 | 32 | ### VS Code ### 33 | .vscode/ 34 | 35 | ### JTE ### 36 | /jte-classes/ 37 | -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, 12 | # software distributed under the License is distributed on an 13 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | # KIND, either express or implied. See the License for the 15 | # specific language governing permissions and limitations 16 | # under the License. 17 | wrapperVersion=3.3.2 18 | distributionType=only-script 19 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Spring Boot OAuth2 Login Demo 2 | 3 | This project demonstrates how to implement OAuth2 authentication in a Spring Boot application using custom login pages with JTE (Java Template Engine) and Tailwind CSS. It includes both traditional form login and OAuth2 login with Google and GitHub. 4 | 5 | ## Features 6 | 7 | - Custom login page using JTE and Tailwind CSS 8 | - Traditional username/password authentication 9 | - OAuth2 authentication with Google and GitHub 10 | - Protected dashboard page 11 | - User role display 12 | - Secure logout functionality 13 | - CSRF protection 14 | 15 | ## Prerequisites 16 | 17 | - Java 17 or later 18 | - Maven 19 | - Google Cloud account (for Google OAuth) 20 | - GitHub account (for GitHub OAuth) 21 | 22 | ## Quick Start 23 | 24 | 1. Clone the repository 25 | ```bash 26 | git clone 27 | cd spring-boot-oauth-demo 28 | ``` 29 | 30 | 2. Configure OAuth credentials (see OAuth Setup sections below) 31 | 32 | 3. Set environment variables 33 | ```bash 34 | export GOOGLE_CLIENT_ID=your_google_client_id 35 | export GOOGLE_CLIENT_SECRET=your_google_client_secret 36 | export GITHUB_CLIENT_ID=your_github_client_id 37 | export GITHUB_CLIENT_SECRET=your_github_client_secret 38 | ``` 39 | 40 | 4. Run the application 41 | ```bash 42 | mvn spring-boot:run 43 | ``` 44 | 45 | 5. Visit http://localhost:8080 46 | 47 | ## Default User Credentials 48 | 49 | The application comes with a default user for testing: 50 | - Username: `admin` 51 | - Password: `admin123` 52 | 53 | ## Google OAuth2 Setup 54 | 55 | 1. Go to [Google Cloud Console](https://console.cloud.google.com/) 56 | 57 | 2. Create a new project or select an existing one 58 | 59 | 3. Configure the OAuth consent screen: 60 | - Go to "APIs & Services" > "OAuth consent screen" 61 | - Choose "External" user type 62 | - Fill in required information: 63 | - App name 64 | - User support email 65 | - Developer contact information 66 | - Add scopes: email, profile, openid 67 | - Add test users if using external user type 68 | 69 | 4. Create OAuth2 credentials: 70 | - Go to "APIs & Services" > "Credentials" 71 | - Click "Create Credentials" > "OAuth client ID" 72 | - Choose "Web application" 73 | - Add these URLs: 74 | ``` 75 | Authorized JavaScript origins: 76 | http://localhost:8080 77 | 78 | Authorized redirect URIs: 79 | http://localhost:8080/login/oauth2/code/google 80 | ``` 81 | - Note your client ID and client secret 82 | 83 | ## GitHub OAuth Setup 84 | 85 | 1. Go to [GitHub Developer Settings](https://github.com/settings/developers) 86 | 87 | 2. Click "New OAuth App" 88 | 89 | 3. Fill in the application details: 90 | ``` 91 | Application name: Your App Name 92 | Homepage URL: http://localhost:8080 93 | Authorization callback URL: http://localhost:8080/login/oauth2/code/github 94 | ``` 95 | 96 | 4. Register the application 97 | 98 | 5. Note your client ID and generate a client secret 99 | 100 | ## Configuration 101 | 102 | Create or update `application.yml`: 103 | 104 | ```yaml 105 | spring: 106 | security: 107 | oauth2: 108 | client: 109 | registration: 110 | google: 111 | client-id: ${GOOGLE_CLIENT_ID} 112 | client-secret: ${GOOGLE_CLIENT_SECRET} 113 | scope: 114 | - email 115 | - profile 116 | github: 117 | client-id: ${GITHUB_CLIENT_ID} 118 | client-secret: ${GITHUB_CLIENT_SECRET} 119 | scope: 120 | - user:email 121 | - read:user 122 | 123 | ``` 124 | 125 | ## Project Structure 126 | 127 | ``` 128 | src/ 129 | main/ 130 | java/ 131 | com.example/ 132 | SecurityConfig.java # Spring Security configuration 133 | LoginController.java # Login handling 134 | DashboardController.java # Dashboard pages 135 | resources/ 136 | application.yml # Application configuration 137 | jte/ 138 | layout/ 139 | default.jte # Base template 140 | pages/ 141 | login.jte # Login page 142 | dashboard.jte # Dashboard page 143 | home.jte # Home page 144 | ``` 145 | 146 | ## Key Dependencies 147 | 148 | ```xml 149 | 150 | 151 | 152 | org.springframework.boot 153 | spring-boot-starter-web 154 | 155 | 156 | org.springframework.boot 157 | spring-boot-starter-security 158 | 159 | 160 | org.springframework.boot 161 | spring-boot-starter-oauth2-client 162 | 163 | 164 | 165 | 166 | gg.jte 167 | jte-spring-boot-starter 168 | 3.1.9 169 | 170 | 171 | ``` 172 | 173 | ## Troubleshooting 174 | 175 | ### OAuth2 Issues 176 | 177 | 1. Redirect URI Mismatch 178 | - Verify the exact URIs in your OAuth provider settings 179 | - For Google: `http://localhost:8080/login/oauth2/code/google` 180 | - For GitHub: `http://localhost:8080/login/oauth2/code/github` 181 | - No trailing slashes 182 | - Correct protocol (http/https) 183 | - Correct port number 184 | 185 | 2. Authentication Errors 186 | - Clear browser cookies and cache 187 | - Check environment variables are set correctly 188 | - Verify OAuth provider console settings 189 | - Check application logs for detailed error messages 190 | 191 | ### Common Issues 192 | 193 | 1. Login Page Not Loading 194 | - Verify JTE configuration 195 | - Check template paths 196 | - Clear browser cache 197 | 198 | 2. Authentication Not Working 199 | - Verify default user credentials 200 | - Check OAuth configuration 201 | - Ensure CSRF token is present in forms 202 | 203 | ## Security Considerations 204 | 205 | 1. Production Deployment 206 | - Use HTTPS 207 | - Update OAuth redirect URIs for production domain 208 | - Secure client secrets 209 | - Enable CSRF protection 210 | - Consider session management settings 211 | 212 | 2. OAuth Provider Setup 213 | - Restrict OAuth scopes to minimum required 214 | - Verify redirect URIs 215 | - Protect client secrets 216 | - Use environment variables 217 | -------------------------------------------------------------------------------- /mvnw: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # ---------------------------------------------------------------------------- 3 | # Licensed to the Apache Software Foundation (ASF) under one 4 | # or more contributor license agreements. See the NOTICE file 5 | # distributed with this work for additional information 6 | # regarding copyright ownership. The ASF licenses this file 7 | # to you under the Apache License, Version 2.0 (the 8 | # "License"); you may not use this file except in compliance 9 | # with the License. You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, 14 | # software distributed under the License is distributed on an 15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | # KIND, either express or implied. See the License for the 17 | # specific language governing permissions and limitations 18 | # under the License. 19 | # ---------------------------------------------------------------------------- 20 | 21 | # ---------------------------------------------------------------------------- 22 | # Apache Maven Wrapper startup batch script, version 3.3.2 23 | # 24 | # Optional ENV vars 25 | # ----------------- 26 | # JAVA_HOME - location of a JDK home dir, required when download maven via java source 27 | # MVNW_REPOURL - repo url base for downloading maven distribution 28 | # MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven 29 | # MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output 30 | # ---------------------------------------------------------------------------- 31 | 32 | set -euf 33 | [ "${MVNW_VERBOSE-}" != debug ] || set -x 34 | 35 | # OS specific support. 36 | native_path() { printf %s\\n "$1"; } 37 | case "$(uname)" in 38 | CYGWIN* | MINGW*) 39 | [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")" 40 | native_path() { cygpath --path --windows "$1"; } 41 | ;; 42 | esac 43 | 44 | # set JAVACMD and JAVACCMD 45 | set_java_home() { 46 | # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched 47 | if [ -n "${JAVA_HOME-}" ]; then 48 | if [ -x "$JAVA_HOME/jre/sh/java" ]; then 49 | # IBM's JDK on AIX uses strange locations for the executables 50 | JAVACMD="$JAVA_HOME/jre/sh/java" 51 | JAVACCMD="$JAVA_HOME/jre/sh/javac" 52 | else 53 | JAVACMD="$JAVA_HOME/bin/java" 54 | JAVACCMD="$JAVA_HOME/bin/javac" 55 | 56 | if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then 57 | echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2 58 | echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2 59 | return 1 60 | fi 61 | fi 62 | else 63 | JAVACMD="$( 64 | 'set' +e 65 | 'unset' -f command 2>/dev/null 66 | 'command' -v java 67 | )" || : 68 | JAVACCMD="$( 69 | 'set' +e 70 | 'unset' -f command 2>/dev/null 71 | 'command' -v javac 72 | )" || : 73 | 74 | if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then 75 | echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2 76 | return 1 77 | fi 78 | fi 79 | } 80 | 81 | # hash string like Java String::hashCode 82 | hash_string() { 83 | str="${1:-}" h=0 84 | while [ -n "$str" ]; do 85 | char="${str%"${str#?}"}" 86 | h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296)) 87 | str="${str#?}" 88 | done 89 | printf %x\\n $h 90 | } 91 | 92 | verbose() { :; } 93 | [ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; } 94 | 95 | die() { 96 | printf %s\\n "$1" >&2 97 | exit 1 98 | } 99 | 100 | trim() { 101 | # MWRAPPER-139: 102 | # Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds. 103 | # Needed for removing poorly interpreted newline sequences when running in more 104 | # exotic environments such as mingw bash on Windows. 105 | printf "%s" "${1}" | tr -d '[:space:]' 106 | } 107 | 108 | # parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties 109 | while IFS="=" read -r key value; do 110 | case "${key-}" in 111 | distributionUrl) distributionUrl=$(trim "${value-}") ;; 112 | distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;; 113 | esac 114 | done <"${0%/*}/.mvn/wrapper/maven-wrapper.properties" 115 | [ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in ${0%/*}/.mvn/wrapper/maven-wrapper.properties" 116 | 117 | case "${distributionUrl##*/}" in 118 | maven-mvnd-*bin.*) 119 | MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ 120 | case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in 121 | *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;; 122 | :Darwin*x86_64) distributionPlatform=darwin-amd64 ;; 123 | :Darwin*arm64) distributionPlatform=darwin-aarch64 ;; 124 | :Linux*x86_64*) distributionPlatform=linux-amd64 ;; 125 | *) 126 | echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2 127 | distributionPlatform=linux-amd64 128 | ;; 129 | esac 130 | distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip" 131 | ;; 132 | maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;; 133 | *) MVN_CMD="mvn${0##*/mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; 134 | esac 135 | 136 | # apply MVNW_REPOURL and calculate MAVEN_HOME 137 | # maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ 138 | [ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}" 139 | distributionUrlName="${distributionUrl##*/}" 140 | distributionUrlNameMain="${distributionUrlName%.*}" 141 | distributionUrlNameMain="${distributionUrlNameMain%-bin}" 142 | MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}" 143 | MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")" 144 | 145 | exec_maven() { 146 | unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || : 147 | exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD" 148 | } 149 | 150 | if [ -d "$MAVEN_HOME" ]; then 151 | verbose "found existing MAVEN_HOME at $MAVEN_HOME" 152 | exec_maven "$@" 153 | fi 154 | 155 | case "${distributionUrl-}" in 156 | *?-bin.zip | *?maven-mvnd-?*-?*.zip) ;; 157 | *) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;; 158 | esac 159 | 160 | # prepare tmp dir 161 | if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then 162 | clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; } 163 | trap clean HUP INT TERM EXIT 164 | else 165 | die "cannot create temp dir" 166 | fi 167 | 168 | mkdir -p -- "${MAVEN_HOME%/*}" 169 | 170 | # Download and Install Apache Maven 171 | verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." 172 | verbose "Downloading from: $distributionUrl" 173 | verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" 174 | 175 | # select .zip or .tar.gz 176 | if ! command -v unzip >/dev/null; then 177 | distributionUrl="${distributionUrl%.zip}.tar.gz" 178 | distributionUrlName="${distributionUrl##*/}" 179 | fi 180 | 181 | # verbose opt 182 | __MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR='' 183 | [ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v 184 | 185 | # normalize http auth 186 | case "${MVNW_PASSWORD:+has-password}" in 187 | '') MVNW_USERNAME='' MVNW_PASSWORD='' ;; 188 | has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;; 189 | esac 190 | 191 | if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then 192 | verbose "Found wget ... using wget" 193 | wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl" 194 | elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then 195 | verbose "Found curl ... using curl" 196 | curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl" 197 | elif set_java_home; then 198 | verbose "Falling back to use Java to download" 199 | javaSource="$TMP_DOWNLOAD_DIR/Downloader.java" 200 | targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName" 201 | cat >"$javaSource" <<-END 202 | public class Downloader extends java.net.Authenticator 203 | { 204 | protected java.net.PasswordAuthentication getPasswordAuthentication() 205 | { 206 | return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() ); 207 | } 208 | public static void main( String[] args ) throws Exception 209 | { 210 | setDefault( new Downloader() ); 211 | java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() ); 212 | } 213 | } 214 | END 215 | # For Cygwin/MinGW, switch paths to Windows format before running javac and java 216 | verbose " - Compiling Downloader.java ..." 217 | "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java" 218 | verbose " - Running Downloader.java ..." 219 | "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")" 220 | fi 221 | 222 | # If specified, validate the SHA-256 sum of the Maven distribution zip file 223 | if [ -n "${distributionSha256Sum-}" ]; then 224 | distributionSha256Result=false 225 | if [ "$MVN_CMD" = mvnd.sh ]; then 226 | echo "Checksum validation is not supported for maven-mvnd." >&2 227 | echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 228 | exit 1 229 | elif command -v sha256sum >/dev/null; then 230 | if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c >/dev/null 2>&1; then 231 | distributionSha256Result=true 232 | fi 233 | elif command -v shasum >/dev/null; then 234 | if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then 235 | distributionSha256Result=true 236 | fi 237 | else 238 | echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2 239 | echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 240 | exit 1 241 | fi 242 | if [ $distributionSha256Result = false ]; then 243 | echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2 244 | echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2 245 | exit 1 246 | fi 247 | fi 248 | 249 | # unzip and move 250 | if command -v unzip >/dev/null; then 251 | unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip" 252 | else 253 | tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" 254 | fi 255 | printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/mvnw.url" 256 | mv -- "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" 257 | 258 | clean || : 259 | exec_maven "$@" 260 | -------------------------------------------------------------------------------- /mvnw.cmd: -------------------------------------------------------------------------------- 1 | <# : batch portion 2 | @REM ---------------------------------------------------------------------------- 3 | @REM Licensed to the Apache Software Foundation (ASF) under one 4 | @REM or more contributor license agreements. See the NOTICE file 5 | @REM distributed with this work for additional information 6 | @REM regarding copyright ownership. The ASF licenses this file 7 | @REM to you under the Apache License, Version 2.0 (the 8 | @REM "License"); you may not use this file except in compliance 9 | @REM with the License. You may obtain a copy of the License at 10 | @REM 11 | @REM http://www.apache.org/licenses/LICENSE-2.0 12 | @REM 13 | @REM Unless required by applicable law or agreed to in writing, 14 | @REM software distributed under the License is distributed on an 15 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | @REM KIND, either express or implied. See the License for the 17 | @REM specific language governing permissions and limitations 18 | @REM under the License. 19 | @REM ---------------------------------------------------------------------------- 20 | 21 | @REM ---------------------------------------------------------------------------- 22 | @REM Apache Maven Wrapper startup batch script, version 3.3.2 23 | @REM 24 | @REM Optional ENV vars 25 | @REM MVNW_REPOURL - repo url base for downloading maven distribution 26 | @REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven 27 | @REM MVNW_VERBOSE - true: enable verbose log; others: silence the output 28 | @REM ---------------------------------------------------------------------------- 29 | 30 | @IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) 31 | @SET __MVNW_CMD__= 32 | @SET __MVNW_ERROR__= 33 | @SET __MVNW_PSMODULEP_SAVE=%PSModulePath% 34 | @SET PSModulePath= 35 | @FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @( 36 | IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) 37 | ) 38 | @SET PSModulePath=%__MVNW_PSMODULEP_SAVE% 39 | @SET __MVNW_PSMODULEP_SAVE= 40 | @SET __MVNW_ARG0_NAME__= 41 | @SET MVNW_USERNAME= 42 | @SET MVNW_PASSWORD= 43 | @IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*) 44 | @echo Cannot start maven from wrapper >&2 && exit /b 1 45 | @GOTO :EOF 46 | : end batch / begin powershell #> 47 | 48 | $ErrorActionPreference = "Stop" 49 | if ($env:MVNW_VERBOSE -eq "true") { 50 | $VerbosePreference = "Continue" 51 | } 52 | 53 | # calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties 54 | $distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl 55 | if (!$distributionUrl) { 56 | Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" 57 | } 58 | 59 | switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { 60 | "maven-mvnd-*" { 61 | $USE_MVND = $true 62 | $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip" 63 | $MVN_CMD = "mvnd.cmd" 64 | break 65 | } 66 | default { 67 | $USE_MVND = $false 68 | $MVN_CMD = $script -replace '^mvnw','mvn' 69 | break 70 | } 71 | } 72 | 73 | # apply MVNW_REPOURL and calculate MAVEN_HOME 74 | # maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ 75 | if ($env:MVNW_REPOURL) { 76 | $MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" } 77 | $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')" 78 | } 79 | $distributionUrlName = $distributionUrl -replace '^.*/','' 80 | $distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' 81 | $MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain" 82 | if ($env:MAVEN_USER_HOME) { 83 | $MAVEN_HOME_PARENT = "$env:MAVEN_USER_HOME/wrapper/dists/$distributionUrlNameMain" 84 | } 85 | $MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' 86 | $MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" 87 | 88 | if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { 89 | Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME" 90 | Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" 91 | exit $? 92 | } 93 | 94 | if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) { 95 | Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl" 96 | } 97 | 98 | # prepare tmp dir 99 | $TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile 100 | $TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir" 101 | $TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null 102 | trap { 103 | if ($TMP_DOWNLOAD_DIR.Exists) { 104 | try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } 105 | catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } 106 | } 107 | } 108 | 109 | New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null 110 | 111 | # Download and Install Apache Maven 112 | Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." 113 | Write-Verbose "Downloading from: $distributionUrl" 114 | Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" 115 | 116 | $webclient = New-Object System.Net.WebClient 117 | if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) { 118 | $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD) 119 | } 120 | [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 121 | $webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null 122 | 123 | # If specified, validate the SHA-256 sum of the Maven distribution zip file 124 | $distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum 125 | if ($distributionSha256Sum) { 126 | if ($USE_MVND) { 127 | Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." 128 | } 129 | Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash 130 | if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) { 131 | Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property." 132 | } 133 | } 134 | 135 | # unzip and move 136 | Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null 137 | Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null 138 | try { 139 | Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null 140 | } catch { 141 | if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) { 142 | Write-Error "fail to move MAVEN_HOME" 143 | } 144 | } finally { 145 | try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } 146 | catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } 147 | } 148 | 149 | Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" 150 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | org.springframework.boot 7 | spring-boot-starter-parent 8 | 3.3.4 9 | 10 | 11 | dev.danvega 12 | jte-login 13 | 0.0.1-SNAPSHOT 14 | jte-login 15 | Demo project for Spring Boot 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 23 31 | 32 | 33 | 34 | org.springframework.boot 35 | spring-boot-starter-oauth2-client 36 | 37 | 38 | org.springframework.boot 39 | spring-boot-starter-security 40 | 41 | 42 | org.springframework.boot 43 | spring-boot-starter-web 44 | 45 | 46 | gg.jte 47 | jte 48 | 3.1.12 49 | 50 | 51 | gg.jte 52 | jte-spring-boot-starter-3 53 | 3.1.12 54 | 55 | 56 | 57 | org.springframework.boot 58 | spring-boot-starter-test 59 | test 60 | 61 | 62 | org.springframework.security 63 | spring-security-test 64 | test 65 | 66 | 67 | 68 | 69 | 70 | 71 | gg.jte 72 | jte-maven-plugin 73 | 3.1.12 74 | 75 | 76 | jte-generate 77 | generate-sources 78 | 79 | generate 80 | 81 | 82 | ${project.basedir}/src/main/jte 83 | Html 84 | true 85 | ${project.build.outputDirectory} 86 | 87 | 88 | 89 | 90 | 91 | org.springframework.boot 92 | spring-boot-maven-plugin 93 | 94 | 95 | 96 | 97 | 98 | -------------------------------------------------------------------------------- /src/main/java/dev/danvega/Application.java: -------------------------------------------------------------------------------- 1 | package dev.danvega; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class Application { 8 | 9 | public static void main(String[] args) { 10 | SpringApplication.run(Application.class, args); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/dev/danvega/CsrfHiddenInput.java: -------------------------------------------------------------------------------- 1 | package dev.danvega; 2 | 3 | import gg.jte.Content; 4 | import gg.jte.TemplateOutput; 5 | import org.springframework.security.web.csrf.CsrfToken; 6 | 7 | public class CsrfHiddenInput implements Content { 8 | 9 | private final CsrfToken csrfToken; 10 | 11 | public CsrfHiddenInput(CsrfToken csrfToken) { 12 | this.csrfToken = csrfToken; 13 | } 14 | 15 | @Override 16 | public void writeTo(TemplateOutput templateOutput) { 17 | if (this.csrfToken != null) { 18 | templateOutput.writeContent("" 19 | .formatted(csrfToken.getParameterName(), csrfToken.getToken())); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/dev/danvega/CsrfTokenAdvice.java: -------------------------------------------------------------------------------- 1 | package dev.danvega; 2 | 3 | import jakarta.servlet.http.HttpServletRequest; 4 | import org.springframework.security.web.csrf.CsrfToken; 5 | import org.springframework.web.bind.annotation.ControllerAdvice; 6 | import org.springframework.web.bind.annotation.ModelAttribute; 7 | 8 | @ControllerAdvice 9 | public class CsrfTokenAdvice { 10 | 11 | @ModelAttribute("csrf") 12 | public CsrfToken csrf(HttpServletRequest request) { 13 | return (CsrfToken) request.getAttribute(CsrfToken.class.getName()); 14 | } 15 | 16 | @ModelAttribute("csrfHiddenInput") 17 | public CsrfHiddenInput csrfHiddenInput(HttpServletRequest request) { 18 | return new CsrfHiddenInput(csrf(request)); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/dev/danvega/DashboardController.java: -------------------------------------------------------------------------------- 1 | package dev.danvega; 2 | 3 | import jakarta.servlet.http.HttpServletRequest; 4 | import org.springframework.security.core.Authentication; 5 | import org.springframework.security.core.userdetails.UserDetails; 6 | import org.springframework.security.oauth2.core.user.OAuth2User; 7 | import org.springframework.security.web.csrf.CsrfToken; 8 | import org.springframework.stereotype.Controller; 9 | import org.springframework.ui.Model; 10 | import org.springframework.web.bind.annotation.GetMapping; 11 | 12 | @Controller 13 | public class DashboardController { 14 | 15 | @GetMapping("/dashboard") 16 | public String dashboard(Authentication authentication, HttpServletRequest request, Model model) { 17 | 18 | if (authentication.getPrincipal() instanceof UserDetails userDetails) { 19 | model.addAttribute("username", userDetails.getUsername()); 20 | model.addAttribute("authorities", userDetails.getAuthorities()); 21 | } else if (authentication.getPrincipal() instanceof OAuth2User oauth2User) { 22 | model.addAttribute("username", oauth2User.getAttribute("name")); 23 | model.addAttribute("email", oauth2User.getAttribute("email")); 24 | model.addAttribute("authorities", oauth2User.getAuthorities()); 25 | } 26 | 27 | // Add CSRF token 28 | CsrfToken csrf = (CsrfToken) request.getAttribute(CsrfToken.class.getName()); 29 | if (csrf != null) { 30 | model.addAttribute("csrf", csrf); 31 | } 32 | 33 | return "pages/dashboard"; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/dev/danvega/LoginController.java: -------------------------------------------------------------------------------- 1 | package dev.danvega; 2 | 3 | 4 | import jakarta.servlet.http.HttpServletRequest; 5 | import org.springframework.stereotype.Controller; 6 | import org.springframework.ui.Model; 7 | import org.springframework.web.bind.annotation.GetMapping; 8 | 9 | @Controller 10 | public class LoginController { 11 | 12 | @GetMapping("/login") 13 | public String login(HttpServletRequest request, Model model, String error, String logout) { 14 | 15 | if (error != null) { 16 | model.addAttribute("error", true); 17 | model.addAttribute("errorMessage", "Invalid username or password"); 18 | } 19 | 20 | return "pages/login"; 21 | } 22 | 23 | @GetMapping("/") 24 | public String home() { 25 | return "pages/home"; 26 | } 27 | } -------------------------------------------------------------------------------- /src/main/java/dev/danvega/SecurityConfig.java: -------------------------------------------------------------------------------- 1 | package dev.danvega; 2 | 3 | import org.springframework.context.annotation.Bean; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.security.config.annotation.web.builders.HttpSecurity; 6 | import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; 7 | import org.springframework.security.core.userdetails.User; 8 | import org.springframework.security.core.userdetails.UserDetails; 9 | import org.springframework.security.core.userdetails.UserDetailsService; 10 | import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; 11 | import org.springframework.security.crypto.password.PasswordEncoder; 12 | import org.springframework.security.provisioning.InMemoryUserDetailsManager; 13 | import org.springframework.security.web.SecurityFilterChain; 14 | 15 | @Configuration 16 | @EnableWebSecurity 17 | public class SecurityConfig { 18 | 19 | @Bean 20 | public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { 21 | http 22 | .authorizeHttpRequests(authorize -> authorize 23 | .requestMatchers("/", "/login", "/error").permitAll() 24 | .anyRequest().authenticated() 25 | ) 26 | .formLogin(form -> form 27 | .loginPage("/login") 28 | .defaultSuccessUrl("/dashboard", true) 29 | .permitAll() 30 | ) 31 | .oauth2Login(oauth2 -> oauth2 32 | .loginPage("/login") 33 | .defaultSuccessUrl("/dashboard", true) 34 | ) 35 | .logout(logout -> logout 36 | .logoutSuccessUrl("/") 37 | .permitAll() 38 | ); 39 | 40 | return http.build(); 41 | } 42 | 43 | @Bean 44 | public PasswordEncoder passwordEncoder() { 45 | return new BCryptPasswordEncoder(); 46 | } 47 | 48 | @Bean 49 | public UserDetailsService userDetailsService() { 50 | UserDetails defaultUser = User.builder() 51 | .username("admin") 52 | .password(passwordEncoder().encode("admin123")) 53 | .roles("ADMIN") 54 | .build(); 55 | 56 | return new InMemoryUserDetailsManager(defaultUser); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/main/jte/.jteroot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danvega/spring-boot-oauth-demo/8d254a10113e5b26fff7d0dc8bca56e8ef1a9171/src/main/jte/.jteroot -------------------------------------------------------------------------------- /src/main/jte/layout/default.jte: -------------------------------------------------------------------------------- 1 | @import org.springframework.security.web.csrf.CsrfToken 2 | 3 | @param gg.jte.Content content 4 | 5 | 6 | 7 | 8 | 9 | 10 | Spring Security Demo 11 | 12 | 13 | 14 | ${content} 15 | 16 | -------------------------------------------------------------------------------- /src/main/jte/pages/dashboard.jte: -------------------------------------------------------------------------------- 1 | @import org.springframework.security.core.GrantedAuthority 2 | @import java.util.Collection 3 | @import dev.danvega.CsrfHiddenInput 4 | 5 | @param String username = "" 6 | @param String email = null 7 | @param Collection authorities = null 8 | @param CsrfHiddenInput csrfHiddenInput 9 | 10 | @template.layout.default( 11 | content = @` 12 |
13 | 35 |
36 |
37 |
38 |

User Information

39 |
40 |
41 |

Username

42 |

${username}

43 |
44 | @if(authorities != null && !authorities.isEmpty()) 45 |
46 |

Roles

47 |
48 | @for(var authority : authorities) 49 | 50 | ${authority.getAuthority()} 51 | 52 | @endfor 53 |
54 |
55 | @endif 56 | @if(email != null) 57 |
58 |

Email

59 |

${email}

60 |
61 | @endif 62 |
63 |
64 |
65 |
66 |
67 | `) 68 | -------------------------------------------------------------------------------- /src/main/jte/pages/home.jte: -------------------------------------------------------------------------------- 1 | @template.layout.default(content = @` 2 |
3 |
4 |

Welcome

5 | 11 |
12 |
13 | `) -------------------------------------------------------------------------------- /src/main/jte/pages/login.jte: -------------------------------------------------------------------------------- 1 | @import dev.danvega.CsrfHiddenInput 2 | 3 | @param Boolean error = false 4 | @param String errorMessage = null 5 | @param CsrfHiddenInput csrfHiddenInput 6 | 7 | @template.layout.default( 8 | content = @` 9 |
10 |
11 |
12 |

13 | Sign in to your account 14 |

15 |
16 | 17 | @if(error) 18 | 21 | @endif 22 | 23 | 24 |
25 | ${csrfHiddenInput} 26 |
27 |
28 | 31 |
32 |
33 | 36 |
37 |
38 | 39 |
40 | 44 |
45 |
46 | 47 | 48 |
49 |
50 |
51 |
52 |
53 |
54 | Or continue with 55 |
56 |
57 | 58 | 73 |
74 |
75 |
76 | `) -------------------------------------------------------------------------------- /src/main/resources/application.yaml: -------------------------------------------------------------------------------- 1 | spring: 2 | application: 3 | name: jte-login 4 | security: 5 | oauth2: 6 | client: 7 | registration: 8 | google: 9 | client-id: ${GOOGLE_CLIENT_ID} 10 | client-secret: ${GOOGLE_CLIENT_SECRET} 11 | scope: 12 | - email 13 | - profile 14 | github: 15 | client-id: ${GITHUB_CLIENT_ID} 16 | client-secret: ${GITHUB_CLIENT_SECRET} 17 | scope: 18 | - user:email 19 | - read:user 20 | 21 | gg: 22 | jte: 23 | developmentMode: true 24 | 25 | logging: 26 | level: 27 | org: 28 | springframework: 29 | security: ERROR #change to DEBUG or INFO for more information about spring security 30 | -------------------------------------------------------------------------------- /src/test/java/dev/danvega/JteLoginApplicationTests.java: -------------------------------------------------------------------------------- 1 | package dev.danvega; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.springframework.boot.test.context.SpringBootTest; 5 | 6 | @SpringBootTest 7 | class JteLoginApplicationTests { 8 | 9 | @Test 10 | void contextLoads() { 11 | } 12 | 13 | } 14 | --------------------------------------------------------------------------------