Skip to content

Commit 63e8315

Browse files
committed
added support oidc provider using keycloak
1 parent 54d242a commit 63e8315

3 files changed

Lines changed: 232 additions & 2 deletions

File tree

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
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
package com.digitalsanctuary.spring.user.service;
2+
3+
import com.digitalsanctuary.spring.user.persistence.model.User;
4+
import com.digitalsanctuary.spring.user.persistence.repository.UserRepository;
5+
import lombok.RequiredArgsConstructor;
6+
import lombok.extern.slf4j.Slf4j;
7+
import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest;
8+
import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService;
9+
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
10+
import org.springframework.security.oauth2.client.userinfo.OAuth2UserService;
11+
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
12+
import org.springframework.security.oauth2.core.OAuth2Error;
13+
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
14+
import org.springframework.security.oauth2.core.user.OAuth2User;
15+
import org.springframework.stereotype.Service;
16+
17+
/**
18+
*
19+
* This class is an implementation of the OAuth2UserService interface that is used to handle Oidc logins for a Spring Security application. It
20+
* provides methods to handle successful Oidc logins, register new users with Oidc accounts, update existing users with new Oidc information,
21+
* and retrieve user information from an OidcUser object. This service is used in conjunction with Spring Security's OAuth2LoginConfigurer to enable
22+
* Oidc login functionality for a web application. The OAuth2LoginConfigurer configures Spring Security to authenticate users with an Oidc
23+
* provider, and uses this service to handle the authentication process and retrieve user information from the provider. This class is annotated with
24+
* the @Service annotation to indicate that it is a Spring service that should be automatically detected and instantiated by the Spring container.
25+
*
26+
* @see org.springframework.security.oauth2.client.registration.ClientRegistration
27+
* @see OAuth2UserService
28+
* @see OAuth2UserRequest
29+
* @see OAuth2User
30+
* @see org.springframework.security.oauth2.core.user.DefaultOAuth2User
31+
* @see org.springframework.security.oauth2.core.user.OAuth2UserAuthority
32+
* @see org.springframework.security.core.userdetails.UserDetails
33+
* @see org.springframework.security.core.userdetails.User
34+
* @see User
35+
* @see UserRepository
36+
*/
37+
@Slf4j
38+
@Service
39+
@RequiredArgsConstructor
40+
public class DSOidcUserService implements OAuth2UserService<OidcUserRequest, OidcUser> {
41+
42+
/** The user repository. */
43+
private final UserRepository userRepository;
44+
45+
OidcUserService defaultOidcUserService = new OidcUserService();
46+
47+
/**
48+
*
49+
* Handles a successful Oidc login. If the user is already registered, updates their account with any new information from the Oidc provider.
50+
* If the user is not already registered, creates a new user account with the information from the Oidc provider and saves it to the database.
51+
*
52+
* @param registrationId The registration ID for the Oidc provider.
53+
* @param oidcUser The OidcUser object containing information about the authenticated user.
54+
* @return A User object representing the authenticated user.
55+
*/
56+
public User handleOidcLoginSuccess(String registrationId, OidcUser oidcUser) {
57+
User user = null;
58+
if (registrationId.equalsIgnoreCase("keycloak")) {
59+
user = getUserFromKeycloakOidc2User(oidcUser);
60+
} else {
61+
log.error("Sorry! Login with " + registrationId + " is not supported yet.");
62+
throw new OAuth2AuthenticationException(new OAuth2Error("Login Exception"),
63+
"Sorry! Login with " + registrationId + " is not supported yet.");
64+
}
65+
if (user == null) {
66+
log.error("handleOidcLoginSuccess: user is null");
67+
throw new OAuth2AuthenticationException(new OAuth2Error("Login Exception"),
68+
"Sorry! An error occurred while processing your login request.");
69+
}
70+
log.debug("handleOidcLoginSuccess: looking up user with email: {}", user.getEmail());
71+
User existingUser = userRepository.findByEmail(user.getEmail());
72+
log.debug("handleOidcLoginSuccess: existingUser: {}", existingUser);
73+
if (existingUser != null && registrationId != null) {
74+
log.debug("handleOidcLoginSuccess: existingUser.getProvider(): {}", existingUser.getProvider());
75+
// If the user is already registered with a different auth provider (OAuth2 or Local), throw an exception.
76+
if (!existingUser.getProvider().toString().equals(registrationId.toUpperCase())) {
77+
log.debug("handleOidcLoginSuccess: ERROR! existingUser.getProvider(): {}", existingUser.getProvider());
78+
throw new OAuth2AuthenticationException(new OAuth2Error("User Registered With Alternate Provider"),
79+
"Looks like you're signed up with your " + existingUser.getProvider() + " account. Please use your "
80+
+ existingUser.getProvider() + " account to log in.");
81+
}
82+
existingUser = updateExistingUser(existingUser, user);
83+
return userRepository.save(existingUser);
84+
} else {
85+
log.debug("handleOidcLoginSuccess: registering new user with email: {}", user.getEmail());
86+
user = registerNewOidcUser(registrationId, user);
87+
return user;
88+
}
89+
}
90+
91+
/**
92+
*
93+
* Registers a new user with an Oidc account. Creates a new user account with the information from the Oidc provider and saves it to the
94+
* database.
95+
*
96+
* @param registrationId The registration ID for the Oidc provider.
97+
* @param user The User object representing the authenticated user.
98+
* @return A User object representing the newly registered user.
99+
*/
100+
private User registerNewOidcUser(String registrationId, User user) {
101+
User.Provider provider = User.Provider.valueOf(registrationId.toUpperCase());
102+
user.setProvider(provider);
103+
// user.setRoles(Collections.singletonList(roleRepository.findByName(RoleName.ROLE_USER)));
104+
// We will trust OAuth2 providers to provide us with a verified email address.
105+
user.setEnabled(true);
106+
return userRepository.save(user);
107+
}
108+
109+
/**
110+
*
111+
* Updates an existing user's account with any new information from the Oidc provider.
112+
*
113+
* @param existingUser The existing User object representing the user to be updated.
114+
* @param user The User object representing the authenticated user.
115+
* @return The updated User object.
116+
*/
117+
private User updateExistingUser(User existingUser, User user) {
118+
existingUser.setFirstName(user.getFirstName());
119+
existingUser.setLastName(user.getLastName());
120+
return existingUser;
121+
}
122+
123+
/**
124+
*
125+
* Retrieves user information from a Keycloak OidcUser object.
126+
*
127+
* @param principal The OidcUser object containing information about the authenticated user.
128+
* @return A User object representing the authenticated user.
129+
*/
130+
public User getUserFromKeycloakOidc2User(OidcUser principal) {
131+
log.debug("Getting user info from Keycloak Oidc provider with principal: {}", principal);
132+
if (principal == null) {
133+
return null;
134+
}
135+
log.debug("Principal attributes: {}", principal.getAttributes());
136+
User user = new User();
137+
/* user.setEmail(principal.getAttribute("email"));
138+
user.setFirstName(principal.getAttribute("given_name"));
139+
user.setLastName(principal.getAttribute("family_name"));*/
140+
user.setEmail(principal.getEmail());
141+
user.setFirstName(principal.getGivenName());
142+
user.setLastName(principal.getFamilyName());
143+
user.setProvider(User.Provider.KEYCLOAK);
144+
return user;
145+
}
146+
147+
148+
/**
149+
*
150+
* Loads user information from an Oidc provider and creates a UserDetails object representing the authenticated user.
151+
*
152+
* @param userRequest The OidcUserRequest object containing information about the user request.
153+
* @return A UserDetails object representing the authenticated user.
154+
* @throws OAuth2AuthenticationException If there is an error authenticating the user with the OAuth2 provider.
155+
*/
156+
@Override
157+
public OidcUser loadUser(OidcUserRequest userRequest) throws OAuth2AuthenticationException {
158+
log.debug("Loading user from OAuth2 provider with userRequest: {}", userRequest);
159+
OidcUser user = defaultOidcUserService.loadUser(userRequest);
160+
String registrationId = userRequest.getClientRegistration().getRegistrationId();
161+
log.debug("registrationId: " + registrationId);
162+
User dbUser = handleOidcLoginSuccess(registrationId, user);
163+
DSUserDetails dsUserDetails = DSUserDetails.builder()
164+
.user(dbUser)
165+
.oidcUserInfo(user.getUserInfo())
166+
.oidcIdToken(user.getIdToken())
167+
.grantedAuthorities(user.getAuthorities())
168+
.build();
169+
return dsUserDetails;
170+
}
171+
}

src/main/java/com/digitalsanctuary/spring/user/service/DSUserDetails.java

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,13 @@
44
import java.util.Collection;
55
import java.util.HashMap;
66
import java.util.Map;
7+
8+
import lombok.Builder;
79
import org.springframework.security.core.GrantedAuthority;
810
import org.springframework.security.core.userdetails.UserDetails;
11+
import org.springframework.security.oauth2.core.oidc.OidcIdToken;
12+
import org.springframework.security.oauth2.core.oidc.OidcUserInfo;
13+
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
914
import org.springframework.security.oauth2.core.user.OAuth2User;
1015
import com.digitalsanctuary.spring.user.persistence.model.User;
1116
import lombok.ToString;
@@ -36,7 +41,7 @@
3641
* }</pre>
3742
*/
3843
@ToString
39-
public class DSUserDetails implements UserDetails, OAuth2User {
44+
public class DSUserDetails implements UserDetails, OAuth2User, OidcUser {
4045

4146
/** The Constant serialVersionUID. */
4247
private static final long serialVersionUID = 5286810064622508389L;
@@ -50,6 +55,12 @@ public class DSUserDetails implements UserDetails, OAuth2User {
5055
/** The attributes. */
5156
private Map<String, Object> attributes;
5257

58+
/** The Oidc user properties. */
59+
private OidcUserInfo oidcUserInfo;
60+
61+
/** The Oidc user token. */
62+
private OidcIdToken oidcIdToken;
63+
5364
/**
5465
* Instantiates a new DS user details.
5566
*
@@ -71,6 +82,34 @@ public DSUserDetails(User user) {
7182
this(user, null);
7283
}
7384

85+
/**
86+
* Instantiates a new DS user details.
87+
*
88+
* @param user the user
89+
* @param oidcUserInfo containing claims about the user
90+
* @param oidcIdToken containing claims about the user
91+
* @param grantedAuthorities the granted authorities (optional, default = empty list)
92+
*/
93+
@Builder
94+
public DSUserDetails(User user, OidcUserInfo oidcUserInfo, OidcIdToken oidcIdToken, Collection<? extends GrantedAuthority> grantedAuthorities) {
95+
this.user = user;
96+
this.oidcUserInfo = oidcUserInfo;
97+
this.oidcIdToken = oidcIdToken;
98+
this.grantedAuthorities = grantedAuthorities != null ? grantedAuthorities : new ArrayList<>();
99+
}
100+
101+
/**
102+
* Instantiates a new DS user details.
103+
*
104+
* @param user the user
105+
* @param oidcUserInfo containing claims about the user
106+
* @param oidcIdToken containing claims about the user
107+
*/
108+
@Builder
109+
public DSUserDetails(User user, OidcUserInfo oidcUserInfo, OidcIdToken oidcIdToken) {
110+
this(user, oidcUserInfo, oidcIdToken, null);
111+
}
112+
74113
/**
75114
* Gets the authorities.
76115
*
@@ -160,4 +199,18 @@ public String getName() {
160199
return user.getFullName();
161200
}
162201

202+
@Override
203+
public Map<String, Object> getClaims() {
204+
return oidcUserInfo.getClaims();
205+
}
206+
207+
@Override
208+
public OidcUserInfo getUserInfo() {
209+
return oidcUserInfo;
210+
}
211+
212+
@Override
213+
public OidcIdToken getIdToken() {
214+
return oidcIdToken;
215+
}
163216
}

0 commit comments

Comments
 (0)