Skip to content

Commit 2782eef

Browse files
authored
Merge pull request #144 from anirbandas18/main
issue-137-Add-Keycloak-Authentication-Support
2 parents 1c5597e + eaf888e commit 2782eef

14 files changed

Lines changed: 317 additions & 39 deletions

File tree

.vscode/settings.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,5 @@
99
"debug.javascript.defaultRuntimeExecutable": {
1010
"pwa-node": "/Users/devon/.local/share/mise/shims/node"
1111
},
12-
"python.defaultInterpreterPath": "/Users/devon/.local/share/mise/shims/python"
12+
"python.defaultInterpreterPath": "/Users/devon/.local/share/mise/installs/python/3.13.1/bin/python"
1313
}

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,12 @@
1+
## [3.0.1] - 2025-02-01
2+
### Features
3+
- The controller path mappings are now configurable.
4+
5+
### Fixes
6+
- Fixed the bug where the schema.sql file was in a location that caused it to be automatically executed, it has now been migrated from the resources to the db-scripts directory.
7+
8+
9+
110
## [3.0.0] - 2025-01-12
211
### Features
312
- Converted project from a simple framework to a Maven library with a separate demo app (#136).

README.md

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ The framework provides support for the following features:
4141
- Database-backed user store using Spring JPA.
4242
- SSO support for Google
4343
- SSO support for Facebook
44+
- SSO support for Keycloak
4445
- Configuration options to control anonymous access, whitelist URIs, and protect specific URIs requiring a logged-in user session.
4546
- CSRF protection enabled by default, with example jQuery AJAX calls passing the CSRF token from the Thymeleaf page context.
4647
- Audit event framework for recording and logging security events, customizable to store audit events in a database or publish them via a REST API.
@@ -54,7 +55,7 @@ The framework provides support for the following features:
5455
This Framework is now available as a library on Maven Central. You can add it to your Gradle project by adding the following dependency to your `build.gradle` file:
5556

5657
```groovy
57-
implementation 'com.digitalsanctuary:ds-spring-user-framework:3.0.0'
58+
implementation 'com.digitalsanctuary:ds-spring-user-framework:3.0.1'
5859
```
5960

6061
Or to your Maven project by adding it to your `pom.xml` file:
@@ -63,7 +64,7 @@ Or to your Maven project by adding it to your `pom.xml` file:
6364
<dependency>
6465
<groupId>com.digitalsanctuary</groupId>
6566
<artifactId>ds-spring-user-framework</artifactId>
66-
<version>3.0.0</version>
67+
<version>3.0.1</version>
6768
</dependency>
6869
```
6970

@@ -91,7 +92,7 @@ If you set your JPA Hibernate ddl-auto property to "create" it will create the t
9192
If you are not using automatic schema updates or Flyway, you can set up your database manually using the provided `schema.sql` file:
9293

9394
```bash
94-
mysql -u username -p database_name < src/main/resources/schema.sql
95+
mysql -u username -p database_name < db-scripts/mariadb-schema.sql
9596
```
9697

9798
Flyway support will be coming soon. This will allow you to automatically update your database schema as you deploy new versions of your application.
@@ -102,7 +103,7 @@ The framework sends emails for verification links, forgot password flow, etc...
102103

103104

104105
### SSO OAuth2 with Google and Facebook
105-
The framework supports SSO OAuth2 with Google and Facebook. To enable this you need to configure the client id and secret for each provider. This is done in the application.yml (or application.properties) file using the [Spring Security OAuth2 properties](https://docs.spring.io/spring-security/reference/servlet/oauth2/login/core.html). You can see the example configuration in the Demo Project's `application.yml` file.
106+
The framework supports SSO OAuth2 with Google, Facebook and Keycloak. To enable this you need to configure the client id and secret for each provider. This is done in the application.yml (or application.properties) file using the [Spring Security OAuth2 properties](https://docs.spring.io/spring-security/reference/servlet/oauth2/login/core.html). You can see the example configuration in the Demo Project's `application.yml` file.
106107

107108
Here is a quick example for your reference:
108109

@@ -120,9 +121,13 @@ spring:
120121
client-id: YOUR_FACEBOOK_CLIENT_ID
121122
client-secret: YOUR_FACEBOOK_CLIENT_SECRET
122123
redirect-uri: "{baseUrl}/login/oauth2/code/facebook"
124+
keycloak:
125+
client-id: YOUR_KEYCLOAK_CLIENT_ID
126+
client-secret: YOUR_KEYCLOAK_CLIENT_SECRET
127+
redirect-uri: "{baseUrl}/login/oauth2/code/keycloak"
123128
```
124129
125-
For public OAuth you will need a public hostname and HTTPS enabled. You can use ngrok or Cloudflare tunnels to create a public hostname and tunnel to your local machine during development. You can then use the ngrok hostname in your Google and Facebook developer console configuration.
130+
For public OAuth you will need a public hostname and HTTPS enabled. You can use ngrok or Cloudflare tunnels to create a public hostname and tunnel to your local machine during development. You can then use the ngrok hostname in your Google, Facebook and Keycloak developer console configuration.
126131
127132
128133

build.gradle

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ dependencies {
4343
compileOnly 'org.springframework.boot:spring-boot-starter-thymeleaf'
4444
compileOnly "org.springframework.boot:spring-boot-starter-web:$springBootVersion"
4545
compileOnly 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6:3.1.3.RELEASE'
46-
compileOnly 'nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect:3.3.0'
46+
compileOnly 'nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect:3.4.0'
4747

4848
// Other dependencies
4949
runtimeOnly 'org.springframework.boot:spring-boot-devtools'
@@ -52,7 +52,7 @@ dependencies {
5252
implementation 'org.passay:passay:1.6.6'
5353
implementation 'com.google.guava:guava:33.4.0-jre'
5454
compileOnly 'org.springframework.boot:spring-boot-starter-actuator'
55-
compileOnly 'jakarta.validation:jakarta.validation-api:3.1.0'
55+
compileOnly 'jakarta.validation:jakarta.validation-api:3.1.1'
5656

5757
// Lombok dependencies
5858
compileOnly "org.projectlombok:lombok:$lombokVersion"
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ CREATE TABLE `user_account` (
6464
`last_name` VARCHAR(255) DEFAULT NULL,
6565
`locked` BIT(1) NOT NULL,
6666
`password` VARCHAR(60) DEFAULT NULL,
67-
`provider` ENUM('LOCAL','FACEBOOK','GOOGLE','APPLE') DEFAULT NULL,
67+
`provider` ENUM('LOCAL','FACEBOOK','GOOGLE','APPLE','KEYCLOAK') DEFAULT NULL,
6868
`registration_date` DATETIME(6) DEFAULT NULL,
6969
`failed_login_attempts` INT(11) NOT NULL,
7070
`locked_date` DATETIME(6) DEFAULT NULL,

gradle.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
version=3.0.1-SNAPSHOT
1+
version=3.0.2-SNAPSHOT

src/main/java/com/digitalsanctuary/spring/user/controller/UserActionController.java

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@
2121
import lombok.extern.slf4j.Slf4j;
2222

2323
/**
24-
* The UserActionController handles non-API, non-Page requests like token validation links from emails.
24+
* The UserActionController handles non-API, non-Page requests like token
25+
* validation links from emails.
2526
*/
2627
@Slf4j
2728
@RequiredArgsConstructor
@@ -56,20 +57,23 @@ public class UserActionController {
5657
private String forgotPasswordChangeURI;
5758

5859
/**
59-
* Validate a forgot password token link from an email, and if valid, show the change password page.
60+
* Validate a forgot password token link from an email, and if valid, show the
61+
* change password page.
6062
*
6163
* @param request the request
62-
* @param model the model
63-
* @param token the token
64+
* @param model the model
65+
* @param token the token
6466
* @return the model and view
6567
*/
66-
@GetMapping("/user/changePassword")
67-
public ModelAndView showChangePasswordPage(final HttpServletRequest request, final ModelMap model, @RequestParam("token") final String token) {
68+
@GetMapping("${user.security.changePasswordURI:/user/changePassword}")
69+
public ModelAndView showChangePasswordPage(final HttpServletRequest request, final ModelMap model,
70+
@RequestParam("token") final String token) {
6871
log.debug("UserAPI.showChangePasswordPage: called with token: {}", token);
6972
final TokenValidationResult result = userService.validatePasswordResetToken(token);
7073
log.debug("UserAPI.showChangePasswordPage:" + "result: {}", result);
7174
AuditEvent changePasswordAuditEvent = AuditEvent.builder().source(this).sessionId(request.getSession().getId())
72-
.ipAddress(UserUtils.getClientIP(request)).userAgent(request.getHeader("User-Agent")).action("showChangePasswordPage")
75+
.ipAddress(UserUtils.getClientIP(request)).userAgent(request.getHeader("User-Agent"))
76+
.action("showChangePasswordPage")
7377
.actionStatus("Success").message("Requested. Result:" + result).build();
7478

7579
eventPublisher.publishEvent(changePasswordAuditEvent);
@@ -85,15 +89,16 @@ public ModelAndView showChangePasswordPage(final HttpServletRequest request, fin
8589
}
8690

8791
/**
88-
* Validate a forgot password token link from an email, and if valid, show the registration success page.
92+
* Validate a forgot password token link from an email, and if valid, show the
93+
* registration success page.
8994
*
9095
* @param request the request
91-
* @param model the model
92-
* @param token the token
96+
* @param model the model
97+
* @param token the token
9398
* @return the model and view
9499
* @throws UnsupportedEncodingException the unsupported encoding exception
95100
*/
96-
@GetMapping("/user/registrationConfirm")
101+
@GetMapping("${user.security.registrationConfirmURI:/user/registrationConfirm}")
97102
public ModelAndView confirmRegistration(final HttpServletRequest request, final ModelMap model,
98103
@RequestParam("token") final String token) throws UnsupportedEncodingException {
99104
log.debug("UserAPI.confirmRegistration: called with token: {}", token);
@@ -107,8 +112,10 @@ public ModelAndView confirmRegistration(final HttpServletRequest request, final
107112
userService.authWithoutPassword(user);
108113
userVerificationService.deleteVerificationToken(token);
109114

110-
AuditEvent registrationAuditEvent = AuditEvent.builder().source(this).user(user).sessionId(request.getSession().getId())
111-
.ipAddress(UserUtils.getClientIP(request)).userAgent(request.getHeader("User-Agent")).action("Registration Confirmation")
115+
AuditEvent registrationAuditEvent = AuditEvent.builder().source(this).user(user)
116+
.sessionId(request.getSession().getId())
117+
.ipAddress(UserUtils.getClientIP(request)).userAgent(request.getHeader("User-Agent"))
118+
.action("Registration Confirmation")
112119
.actionStatus("Success").message("Registration Confirmed. User logged in.").build();
113120

114121
eventPublisher.publishEvent(registrationAuditEvent);

src/main/java/com/digitalsanctuary/spring/user/controller/UserPageController.java

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ public class UserPageController {
2727
@Value("${user.registration.googleEnabled}")
2828
private boolean googleEnabled;
2929

30+
@Value("${user.registration.keycloakEnabled}")
31+
private boolean keycloakEnabled;
32+
3033
/**
3134
* Login Page.
3235
*
@@ -36,7 +39,7 @@ public class UserPageController {
3639
*
3740
* @return the string
3841
*/
39-
@GetMapping("/user/login.html")
42+
@GetMapping("${user.security.loginPageURI:/user/login.html}")
4043
public String login(@AuthenticationPrincipal DSUserDetails userDetails, HttpSession session, ModelMap model) {
4144
log.debug("UserPageController.login:" + "userDetails: {}", userDetails);
4245
if (session != null && session.getAttribute("error.message") != null) {
@@ -45,6 +48,7 @@ public String login(@AuthenticationPrincipal DSUserDetails userDetails, HttpSess
4548
}
4649
model.addAttribute("googleEnabled", googleEnabled);
4750
model.addAttribute("facebookEnabled", facebookEnabled);
51+
model.addAttribute("keycloakEnabled", keycloakEnabled);
4852
return "user/login";
4953
}
5054

@@ -56,7 +60,7 @@ public String login(@AuthenticationPrincipal DSUserDetails userDetails, HttpSess
5660
* @param model the model
5761
* @return the string
5862
*/
59-
@GetMapping("/user/register.html")
63+
@GetMapping("${user.security.registrationURI:/user/register.html}")
6064
public String register(@AuthenticationPrincipal DSUserDetails userDetails, HttpSession session, ModelMap model) {
6165
log.debug("UserPageController.register:" + "userDetails: {}", userDetails);
6266
if (session != null && session.getAttribute("error.message") != null) {
@@ -65,6 +69,7 @@ public String register(@AuthenticationPrincipal DSUserDetails userDetails, HttpS
6569
}
6670
model.addAttribute("googleEnabled", googleEnabled);
6771
model.addAttribute("facebookEnabled", facebookEnabled);
72+
model.addAttribute("keycloakEnabled", keycloakEnabled);
6873
return "user/register";
6974
}
7075

@@ -73,7 +78,7 @@ public String register(@AuthenticationPrincipal DSUserDetails userDetails, HttpS
7378
*
7479
* @return the string
7580
*/
76-
@GetMapping("/user/registration-pending-verification.html")
81+
@GetMapping("${user.security.registrationPendingURI:/user/registration-pending-verification.html}")
7782
public String registrationPending() {
7883
return "user/registration-pending-verification";
7984
}
@@ -87,8 +92,9 @@ public String registrationPending() {
8792
*
8893
* @return the string
8994
*/
90-
@GetMapping("/user/registration-complete.html")
91-
public String registrationComplete(@AuthenticationPrincipal DSUserDetails userDetails, HttpSession session, ModelMap model) {
95+
@GetMapping("${user.security.registrationSuccessURI:/user/registration-complete.html}")
96+
public String registrationComplete(@AuthenticationPrincipal DSUserDetails userDetails, HttpSession session,
97+
ModelMap model) {
9298
log.debug("UserPageController.registrationComplete:" + "userDetails: {}", userDetails);
9399
return "user/registration-complete";
94100
}
@@ -98,7 +104,7 @@ public String registrationComplete(@AuthenticationPrincipal DSUserDetails userDe
98104
*
99105
* @return the string
100106
*/
101-
@GetMapping("/user/request-new-verification-email.html")
107+
@GetMapping("${user.security.registrationNewVerificationURI:/user/request-new-verification-email.html}")
102108
public String requestNewVerificationEMail() {
103109
return "user/request-new-verification-email";
104110
}
@@ -108,7 +114,7 @@ public String requestNewVerificationEMail() {
108114
*
109115
* @return the string
110116
*/
111-
@GetMapping("/user/forgot-password.html")
117+
@GetMapping("${user.security.forgotPasswordURI:/user/forgot-password.html}")
112118
public String forgotPassword() {
113119
return "user/forgot-password";
114120
}
@@ -118,7 +124,7 @@ public String forgotPassword() {
118124
*
119125
* @return the string
120126
*/
121-
@GetMapping("/user/forgot-password-pending-verification.html")
127+
@GetMapping("${user.security.forgotPasswordPendingURI:/user/forgot-password-pending-verification.html}")
122128
public String forgotPasswordPendingVerification() {
123129
return "user/forgot-password-pending-verification";
124130
}
@@ -128,7 +134,7 @@ public String forgotPasswordPendingVerification() {
128134
*
129135
* @return the string
130136
*/
131-
@GetMapping("/user/forgot-password-change.html")
137+
@GetMapping("${user.security.forgotPasswordChangeURI:/user/forgot-password-change.html}")
132138
public String forgotPasswordChange() {
133139
return "user/forgot-password-change";
134140
}
@@ -140,8 +146,9 @@ public String forgotPasswordChange() {
140146
* @param model the model
141147
* @return String
142148
*/
143-
@GetMapping("/user/update-user.html")
144-
public String updateUser(@AuthenticationPrincipal DSUserDetails userDetails, final HttpServletRequest request, final ModelMap model) {
149+
@GetMapping("${user.security.updateUserURI:/user/update-user.html}")
150+
public String updateUser(@AuthenticationPrincipal DSUserDetails userDetails, final HttpServletRequest request,
151+
final ModelMap model) {
145152
if (userDetails != null) {
146153
User user = userDetails.getUser();
147154
UserDto userDto = new UserDto();
@@ -157,7 +164,7 @@ public String updateUser(@AuthenticationPrincipal DSUserDetails userDetails, fin
157164
*
158165
* @return the string
159166
*/
160-
@GetMapping("/user/update-password.html")
167+
@GetMapping("${user.security.updatePasswordURI:/user/update-password.html}")
161168
public String updatePassword() {
162169
return "user/update-password";
163170
}
@@ -167,7 +174,7 @@ public String updatePassword() {
167174
*
168175
* @return the string
169176
*/
170-
@GetMapping("/user/delete-account.html")
177+
@GetMapping("${user.security.deleteAccountURI:/user/delete-account.html}")
171178
public String deleteAccount() {
172179
return "user/delete-account";
173180
}

src/main/java/com/digitalsanctuary/spring/user/persistence/model/User.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,12 @@ public enum Provider {
4040
/**
4141
* Login using Apple as the authentication provider.
4242
*/
43-
APPLE
43+
APPLE,
44+
45+
/**
46+
* Login using Keycloak as the authentication provider.
47+
*/
48+
KEYCLOAK
4449
}
4550

4651
/** The id. */

src/main/java/com/digitalsanctuary/spring/user/security/WebSecurityConfig.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
import java.util.Collections;
77
import java.util.List;
88
import java.util.stream.Collectors;
9+
10+
import com.digitalsanctuary.spring.user.service.DSOidcUserService;
911
import org.springframework.beans.factory.annotation.Value;
1012
import org.springframework.context.ApplicationEventPublisher;
1113
import org.springframework.context.annotation.Bean;
@@ -113,6 +115,7 @@ public class WebSecurityConfig {
113115
private final LogoutSuccessService logoutSuccessService;
114116
private final RolesAndPrivilegesConfig rolesAndPrivilegesConfig;
115117
private final DSOAuth2UserService dsOAuth2UserService;
118+
private final DSOidcUserService dsOidcUserService;
116119

117120
/**
118121
*
@@ -183,7 +186,10 @@ private void setupOAuth2(HttpSecurity http) throws Exception {
183186
request.getSession().setAttribute("error.message", exception.getMessage());
184187
response.sendRedirect(loginPageURI);
185188
// handler.onAuthenticationFailure(request, response, exception);
186-
}).userInfoEndpoint(userInfo -> userInfo.userService(dsOAuth2UserService)));
189+
}).userInfoEndpoint(userInfo -> {
190+
userInfo.userService(dsOAuth2UserService);
191+
userInfo.oidcUserService(dsOidcUserService);
192+
}));
187193
}
188194

189195
// Commenting this out to try adding /error to the unprotected URIs list instead

0 commit comments

Comments
 (0)