Migrating to keycloak for an app that uses spring security - spring

I'm looking for steps to keycloak for an Spring MVC app that uses spring security currently.
I wanted to use keycloak in Sitewhere.

I guess this is so simple if I would have read keycloak's document fully:). Any how here are the steps that I followed while migrating to keycloak in Sitewhere .
Follow the steps as given in keycloak doc for spring-security
Add the dependency to sitewhere-core & sitewhere-web pom.xml as stated in adapter installation
Also add the jboss-logging dependency in sitewhere-web's pom.xml since, keycloak spring adapter has a hardcode dependency for jboss-logging.
Modify applicationcontext.xml so that it can uses keycloak for both web & api, following the sample for api
<sec:http pattern="/api/**" entry-point-ref="keycloakAuthenticationEntryPoint">
<sec:custom-filter ref="keycloakPreAuthActionsFilter" before="LOGOUT_FILTER" />
<sec:custom-filter ref="keycloakAuthenticationProcessingFilter" before="FORM_LOGIN_FILTER" />
Modify LoginManager.java as follows
public static IUser getCurrentlyLoggedInUser() throws SiteWhereException {
Authentication KeyCloakAuth = SecurityContextHolder.getContext().getAuthentication();
if (KeyCloakAuth == null) {
throw new SiteWhereSystemException(ErrorCode.NotLoggedIn, ErrorLevel.ERROR,
HttpServletResponse.SC_FORBIDDEN);
}
KeycloakAccount keyAccount = ((KeycloakAuthenticationToken) KeyCloakAuth).getAccount();
String username = keyAccount.getKeycloakSecurityContext().getIdToken().getPreferredUsername();
String password = "";
IUser user = SiteWhere.getServer().getUserManagement().authenticate(username, password);
List<IGrantedAuthority> auths =
SiteWhere.getServer().getUserManagement().getGrantedAuthorities(user.getUsername());
SitewhereUserDetails details = new SitewhereUserDetails(user, auths);
Authentication auth = new SitewhereAuthentication(details, password);
if (!(auth instanceof SitewhereAuthentication)) {
throw new SiteWhereException("Authentication was not of expected type: "
+ SitewhereAuthentication.class.getName() + " found " + auth.getClass().getName()
+ " instead.");
}
return (IUser) ((SitewhereAuthentication) auth).getPrincipal();
}
Since, we have migrated our authentication to keycloak and for the fact that we will not get credentials of user in siterwhere it's better to void the code related to password validation in authentication method of IUserManagement. Following is the sample from MongoUserManagement.java
public IUser authenticate(String username, String password) throws SiteWhereException {
if (password == null) {
throw new SiteWhereSystemException(ErrorCode.InvalidPassword, ErrorLevel.ERROR,
HttpServletResponse.SC_BAD_REQUEST);
}
DBObject userObj = assertUser(username);
String inPassword = SiteWherePersistence.encodePassoword(password);
User match = MongoUser.fromDBObject(userObj);
//nullify authentication since we are using keycloak
/*if (!match.getHashedPassword().equals(inPassword)) {
throw new SiteWhereSystemException(ErrorCode.InvalidPassword, ErrorLevel.ERROR,
HttpServletResponse.SC_UNAUTHORIZED);
}*/
// Update last login date.
match.setLastLogin(new Date());
DBObject updated = MongoUser.toDBObject(match);
DBCollection users = getMongoClient().getUsersCollection();
BasicDBObject query = new BasicDBObject(MongoUser.PROP_USERNAME, username);
MongoPersistence.update(users, query, updated);
return match;}
Make sure you have respective roles for the users in keycloak that are more specific to sitewhere.
Change your home page so that it redirects to keycloak for authentication purpose. Following is the sample for redirection:
Tracer.start(TracerCategory.AdminUserInterface, "login", LOGGER);
try {
Map<String, Object> data = new HashMap<String, Object>();
data.put("version", VersionHelper.getVersion());
String keycloakConfig = environment.getProperty("AUTHSERVER_REDIRECTION_URL");
if (SiteWhere.getServer().getLifecycleStatus() == LifecycleStatus.Started) {
return new ModelAndView("redirect:"+keycloakConfig);
} else {
ServerStartupException failure = SiteWhere.getServer().getServerStartupError();
data.put("subsystem", failure.getDescription());
data.put("component", failure.getComponent().getLifecycleError().getMessage());
return new ModelAndView("noserver", data);
}
} finally {
Tracer.stop(LOGGER);
}

Related

Adding basic auth headers to all the requests in spring boot

I am new to Spring boot application development.
I need to add the basic auth headers to all the api requests in spring boot.
Can any one share the valid documentation of how I proceed
It depends on what kind of auth u require
for something like self auth token it would look something like
public String controllerFunction(#RequestHeader("Auth-header") String authToken){
if (authToken == null) {
log.error("Self token authentication failed");
throw new Exception(TOKEN_NOT_FOUND);
}
if (!"auth_password".equals(authToken)) {
log.error("Self token authentication failed");
throw new Exception(AUTH_FAILED);
}
log.info("Self token authentication successful");
}
If it's unique to individual users u will have to fetch the "auth_password" from your database for that particular user and validate it
To use it in globally you can build annotations like this
#Before("#annotation(tokenValidation)")
public void beforeAdvice(TokenValidation tokenValidation) {
String authToken = request.getHeader("Auth-header");
if (authToken == null) {
log.error("Self token authentication failed");
throw new Exception(TOKEN_NOT_FOUND);
}
if (!"auth_password".equals(authToken)) {
log.error("Self token authentication failed");
throw new Exception(AUTH_FAILED);
}
log.info("Self token authentication successful");
}
U might have to look up how to implement the annotations in spring boot but this is a basic concept.
and in the controllers, u just have to do
#tokenValidation
public String controllerFunction(String authToken){
//your code;
}

AWS cognito sign in without password

I am making an application using AWS cognito and Spring Boot. After registering, users confirm their account by email or SMS activation code. After they confirm their account, can I do an automatic session login? Can I start a session without a password only for confirmation cases?
Yes, you can perform login for the user without a password using Custom Authentication Flow.
You will have to add Lambda Triggers to handle your custom auth flow.
In application, you will have to use AdminInitiateAuth API call.
Here is some code example to understand the general idea:
public void auth(String username) {
AwsBasicCredentials awsCreds = AwsBasicCredentials.create(AWS_KEY,
AWS_SECRET);
CognitoIdentityProviderClient identityProviderClient =
CognitoIdentityProviderClient.builder()
.credentialsProvider(StaticCredentialsProvider.create(awsCreds))
.region(Region.of(REGION))
.build();
final Map<String, String> authParams = new HashMap<>();
authParams.put("USERNAME", username);
authParams.put("SECRET_HASH", calculateSecretHash(CLIENT_ID,
CLIENT_SECRET, username));
final AdminInitiateAuthRequest authRequest = AdminInitiateAuthRequest.builder()
.authFlow(AuthFlowType.CUSTOM_AUTH)
.clientId(CLIENT_ID)
.userPoolId(POOL_ID)
.authParameters(authParams)
.build();
AdminInitiateAuthResponse result = identityProviderClient.adminInitiateAuth(authRequest);
System.out.println(result.authenticationResult().accessToken());
System.out.println(result.authenticationResult().idToken());
}
private String calculateSecretHash(String userPoolClientId, String userPoolClientSecret, String userName) {
final String HMAC_SHA256_ALGORITHM = "HmacSHA256";
SecretKeySpec signingKey = new SecretKeySpec(
userPoolClientSecret.getBytes(StandardCharsets.UTF_8),
HMAC_SHA256_ALGORITHM);
try {
Mac mac = Mac.getInstance(HMAC_SHA256_ALGORITHM);
mac.init(signingKey);
mac.update(userName.getBytes(StandardCharsets.UTF_8));
byte[] rawHmac = mac.doFinal(userPoolClientId.getBytes(StandardCharsets.UTF_8));
return Base64.getEncoder().encodeToString(rawHmac);
} catch (Exception e) {
throw new RuntimeException("Error while calculating ");
}
}
You will also need to add dependecies for AWS SDK:
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>aws-core</artifactId>
<version>2.13.57</version>
</dependency>
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>cognitoidentityprovider</artifactId>
<version>2.13.57</version>
</dependency>
And add Lambda for "Define Auth Challange" trigger of you user pool:
exports.handler = async (event) => {
// Don't do any checks just say that authentication is successfull
event.response.issueTokens = true;
event.response.failAuthentication = false;
return event;
};
The answer of Yuriy P is perfect.
Additionally, in the case Cognito's client application does not set for password, you can ignore "SECRET_HASH"
The code is simplifier as below:
public AdminInitiateAuthResult adminInitiateAuth(String account, String clientId) {
final Map<String, String> authParams = new HashMap<>();
authParams.put("USERNAME", account);
final AdminInitiateAuthRequest authRequest = new AdminInitiateAuthRequest();
authRequest.setAuthFlow(AuthFlowType.CUSTOM_AUTH);
authRequest.setClientId(clientId);
authRequest.setUserPoolId(poolId);
authRequest.setAuthParameters(authParams);
AdminInitiateAuthResult result = cognitoIdentityClient.adminInitiateAuth(authRequest);
System.out.println("accessToken:" + result.getAuthenticationResult().getAccessToken());
System.out.println("idToken:" +result.getAuthenticationResult().getIdToken());
return result;
}

How to retrieve attributes and username sent by the CAS server with Spring Security

I have a spring boot application, which is MVC in nature. All page of this application are being authenticated by CAS SSO.
I have used "spring-security-cas" as described at https://www.baeldung.com/spring-security-cas-sso
Everything working fine as expected. However, I have one problem - that is, I cannot retrieve attributes
and username sent by the CAS server in the following #Bean. What need I do to retrieve all the attributes
and and username sent by the CAS server?
#Bean
public CasAuthenticationProvider casAuthenticationProvider() {
CasAuthenticationProvider provider = new CasAuthenticationProvider();
provider.setServiceProperties(serviceProperties());
provider.setTicketValidator(ticketValidator());
provider.setUserDetailsService(
s -> new User("casuser", "Mellon", true, true, true, true,
AuthorityUtils.createAuthorityList("ROLE_ADMIN")));
provider.setKey("CAS_PROVIDER_LOCALHOST_9000");
return provider;
}
First you will need to configure the attributeRepository source and the attributes to be retrieved, in attributeRepository section in CAS server, like:
cas.authn.attributeRepository.jdbc[0].singleRow=false
cas.authn.attributeRepository.jdbc[0].sql=SELECT * FROM USERATTRS WHERE {0}
cas.authn.attributeRepository.jdbc[0].username=username
cas.authn.attributeRepository.jdbc[0].role=role
cas.authn.attributeRepository.jdbc[0].email=email
cas.authn.attributeRepository.jdbc[0].url=jdbc:hsqldb:hsql://localhost:9001/xdb
cas.authn.attributeRepository.jdbc[0].columnMappings.attrname=attrvalue
cas.authn.attributeRepository.defaultAttributesToRelease=username,email,role
Check this example from CAS blog.
Then you need to implement an AuthenticationUserDetailsService at the service to read attributes returned from CAS authentication, something like:
#Component
public class CasUserDetailService implements AuthenticationUserDetailsService {
#Override
public UserDetails loadUserDetails(Authentication authentication) throws UsernameNotFoundException {
CasAssertionAuthenticationToken casAssertionAuthenticationToken = (CasAssertionAuthenticationToken) authentication;
AttributePrincipal principal = casAssertionAuthenticationToken.getAssertion().getPrincipal();
Map attributes = principal.getAttributes();
String uname = (String) attributes.get("username");
String email = (String) attributes.get("email");
String role = (String) attributes.get("role");
String username = authentication.getName();
Collection<SimpleGrantedAuthority> collection = new ArrayList<SimpleGrantedAuthority>();
collection.add(new SimpleGrantedAuthority(role));
return new User(username, "", collection);
}
}
Then, adjust your authenticationProvider with provider.setAuthenticationUserDetailsService(casUserDetailService);

How to get google user email to whitelist users when authenticating using Spring Boot OAuth2 against Google

I want to whitelist users connecting to my OAuth2 client, and I can't figure out how to get the user name (specifically the Google email address).
I created a Spring Boot OAuth2 application based on a Spring Tutorial
https://spring.io/guides/tutorials/spring-boot-oauth2/#_social_login_manual
I'm authenticating against Google (successfully). I want to determine the user email address so I can whitelist authenticating users.
This website,
http://www.baeldung.com/spring-security-openid-connect#filter
suggests that I can unpack the "id_token" I get back from Google, something like this:
/**
* logic to unpack a ConnectID id_token like what we get from Google -
* see "Spring Security and OpenID Connect" - heading '4. Custom OpenID Connect Filter':
* http://www.baeldung.com/spring-security-openid-connect#filter
*
* #param oa2token
* #return
*/
private static UsernamePasswordAuthenticationToken getOpenIDDataForToken(OAuth2AccessToken oa2token)
{
try {
String idToken = oa2token.getAdditionalInformation().get("id_token").toString();
Jwt tokenDecoded = JwtHelper.decode(idToken);
Map<String, String> authInfo = new ObjectMapper().readValue(tokenDecoded.getClaims(), Map.class);
OpenIdConnectUserDetails user = new OpenIdConnectUserDetails(authInfo, oa2token);
return new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities());
} catch (InvalidTokenException e) {
throw new BadCredentialsException("Could not obtain user details from token", e);
}
}
but I can't get this code to compile - I can't figure out how to get class JtwHelper!
I searched around and the following might be the right Maven dependency:
<!-- https://mvnrepository.com/artifact/org.springframework.security/spring-security-jwt -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-jwt</artifactId>
</dependency>
but adding this to my pom.xml doesn't help - I don't get a real Jar file back in my .m2 repository - I get a text file!!! and bottom line, Eclipse doesn't resolve the type JwtHelper.
Help? I'm not sure where I've gone wrong.
Looks like an answer on this SO page had my answer (thanks #user2802927):
How to get custom user info from OAuth2 authorization server /user endpoint
Here's the code:
Principal principal = servlet_request.getUserPrincipal();
try {
if (principal != null) {
OAuth2Authentication oAuth2Authentication = (OAuth2Authentication) principal;
Authentication authentication = oAuth2Authentication.getUserAuthentication();
Map<String, String> details = new LinkedHashMap<>();
details = (Map<String, String>) authentication.getDetails();
Map<String, String> map = new LinkedHashMap<>();
map.put("email", details.get("email"));
logger.debug("details map is: {}", map);
}
} catch (Exception e) {
logger.error("dumping principal " + principal + "failed, exception: ", e );
}
The output showed that I found success - the user's email address!!!
2017-05-23 11:48:26.751 DEBUG 7687 --- [nio-8443-exec-1] ication$$EnhancerBySpringCGLIB$$91415b85 :
details map is: {email=myemailaddress#gmail.com}

Issue with Concurrent sessions using Hibernate And Spring 2.5

I am developing an app with Spring 2.5.6, Spring Security 2.0.4 and Hibernate 3.2.6. In my application a user can login from different browsers with the same credentials concurrently. There is an option to change the email id of the user. Controller which handle this request is as follow :-
#RequestMapping(value="/changeEmail", method= RequestMethod.POST)
public void changeEmail(HttpServletRequest request, HttpServletResponse response, Principal principal) throws IOException
{
System.out.println("We got the Request");
boolean hasError = false;
String currentEmail = request.getParameter("currentEmail");
String newEmail = request.getParameter("newEmail");
String confirmEmail = request.getParameter("confirmEmail");
String password = request.getParameter("password");
System.out.println("Old Email = " + currentEmail + ", New Email = " + newEmail + ", Confirm Email = " + confirmEmail + ", Password = " + password);
Map<String, String> errorsMap = new HashMap<String,String>();
UserFormResponse callBackResponse = new UserFormResponse("",errorsMap);
if(currentEmail == null || currentEmail.equals(""))
{
hasError = true;
errorsMap.put("currentEmail", "Enter Your Current Email");
}
if(newEmail == null || newEmail.equals(""))
{
hasError = true;
errorsMap.put("newEmail", "Enter Your New Email");
}
if(confirmEmail == null || confirmEmail.equals("") || !confirmEmail.equals(newEmail))
{
hasError = true;
errorsMap.put("confirmEmail", "Confirm Your Email");
}
if(password == null || password.equals(""))
{
hasError = true;
errorsMap.put("password", "Enter Your Password");
}
if(!hasError)
{
System.out.print("Should be printed : " + callBackResponse.getStatus());
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
Object myUser = (auth != null) ? auth.getPrincipal() : null;
if (myUser instanceof User) {
User user = (User) myUser;
if(!user.getPassword().equals(password))
{
callBackResponse.setStatus("Wrong Password");
errorsMap.put("password", "Enter Correct Password");
}
else if(!user.getUserEmail().equals(currentEmail))
{
callBackResponse.setStatus("Wrong Email");
errorsMap.put("currentEmail", "Enter Your Current Email");
}
else
{
user.setUserEmail(newEmail);
getHibernateTemplate().update(user);
callBackResponse.setStatus("Success");
}
}
else
{
callBackResponse.setStatus("You must be logged in");
}
}
else
{
callBackResponse.setStatus("Kindly fill all fields");
}
callBackResponse.setErrorsMap(errorsMap);
System.out.println(callBackResponse.getStatus());
System.out.println(callBackResponse.getErrorsMap());
objectMapper.writeValue(response.getOutputStream(),callBackResponse);
}
Email is being changed perfectly when I check the database from the backend. But main issue is if user have two concurrent sessions then email changed in one session is not being reflected in other session untill other session is not being invalidate. Is there any solution so that changes during one session can be reflected to another concurrent session ?
Note : please don't suggest to change the version of spring and hibernate as it is mandatory to develop this application in spring 2.5.6.
First define HttpSessionEventPublisher in web.xml
<listener>
<listener-class>org.springframework.security.web.session.HttpSessionEventPublisher</listener-class>
</listener>
Then define <session-management> in your spring security.xml file.
Now, use SessionRegistry in your controller method to invalidate all sessions. Below code retrieves all active sessions.
List<SessionInformation> activeSessions = new ArrayList<SessionInformation>();
for (Object principal : sessionRegistry.getAllPrincipals()) {
for (SessionInformation session : sessionRegistry.getAllSessions(principal, false)) {
activeSessions.add(session);
}
}
On Each active session, you can call expireNow() method to expire or invalidate them.

Resources