Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
6 changed files
with
286 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
230 changes: 230 additions & 0 deletions
230
para-server/src/main/java/com/erudika/para/security/filters/SlackAuthFilter.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,230 @@ | ||
/* | ||
* Copyright 2013-2019 Erudika. https://erudika.com | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
* | ||
* For issues and patches go to: https://slack.com/erudika | ||
*/ | ||
package com.erudika.para.security.filters; | ||
|
||
import com.erudika.para.Para; | ||
import com.erudika.para.core.App; | ||
import com.erudika.para.core.utils.ParaObjectUtils; | ||
import com.erudika.para.core.User; | ||
import com.erudika.para.security.AuthenticatedUserDetails; | ||
import com.erudika.para.security.SecurityUtils; | ||
import com.erudika.para.security.UserAuthentication; | ||
import com.erudika.para.utils.Config; | ||
import com.erudika.para.utils.Utils; | ||
import com.fasterxml.jackson.databind.ObjectReader; | ||
import java.io.IOException; | ||
import java.util.Map; | ||
import javax.servlet.http.HttpServletRequest; | ||
import javax.servlet.http.HttpServletResponse; | ||
import org.apache.commons.lang3.StringUtils; | ||
import org.apache.http.HttpEntity; | ||
import org.apache.http.HttpHeaders; | ||
import org.apache.http.client.config.CookieSpecs; | ||
import org.apache.http.client.config.RequestConfig; | ||
import org.apache.http.client.methods.CloseableHttpResponse; | ||
import org.apache.http.client.methods.HttpGet; | ||
import org.apache.http.client.methods.HttpPost; | ||
import org.apache.http.entity.StringEntity; | ||
import org.apache.http.impl.NoConnectionReuseStrategy; | ||
import org.apache.http.impl.client.CloseableHttpClient; | ||
import org.apache.http.impl.client.HttpClientBuilder; | ||
import org.apache.http.util.EntityUtils; | ||
import org.springframework.security.authentication.AuthenticationServiceException; | ||
import org.springframework.security.core.Authentication; | ||
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter; | ||
|
||
/** | ||
* A filter that handles authentication requests to Slack. | ||
* @author Alex Bogdanovski [alex@erudika.com] | ||
*/ | ||
public class SlackAuthFilter extends AbstractAuthenticationProcessingFilter { | ||
|
||
private final CloseableHttpClient httpclient; | ||
private final ObjectReader jreader; | ||
private static final String PROFILE_URL = "https://slack.com/api/users.identity?token={0}"; | ||
private static final String TOKEN_URL = "https://slack.com/api/oauth.access"; | ||
private static final String PAYLOAD = "code={0}&redirect_uri={1}&client_id={2}&client_secret={3}"; | ||
|
||
/** | ||
* The default filter mapping. | ||
*/ | ||
public static final String SLACK_ACTION = "slack_auth"; | ||
|
||
/** | ||
* Default constructor. | ||
* @param defaultFilterProcessesUrl the url of the filter | ||
*/ | ||
public SlackAuthFilter(final String defaultFilterProcessesUrl) { | ||
super(defaultFilterProcessesUrl); | ||
this.jreader = ParaObjectUtils.getJsonReader(Map.class); | ||
int timeout = 30 * 1000; | ||
this.httpclient = HttpClientBuilder.create(). | ||
setConnectionReuseStrategy(new NoConnectionReuseStrategy()). | ||
setDefaultRequestConfig(RequestConfig.custom(). | ||
setConnectTimeout(timeout). | ||
setConnectionRequestTimeout(timeout). | ||
setCookieSpec(CookieSpecs.STANDARD). | ||
setSocketTimeout(timeout). | ||
build()). | ||
build(); | ||
} | ||
|
||
/** | ||
* Handles an authentication request. | ||
* @param request HTTP request | ||
* @param response HTTP response | ||
* @return an authentication object that contains the principal object if successful. | ||
* @throws IOException ex | ||
*/ | ||
@Override | ||
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) | ||
throws IOException { | ||
final String requestURI = request.getRequestURI(); | ||
UserAuthentication userAuth = null; | ||
|
||
if (requestURI.endsWith(SLACK_ACTION)) { | ||
String authCode = request.getParameter("code"); | ||
if (!StringUtils.isBlank(authCode)) { | ||
String appid = SecurityUtils.getAppidFromAuthRequest(request); | ||
String redirectURI = SecurityUtils.getRedirectUrl(request); | ||
App app = Para.getDAO().read(App.id(appid == null ? Config.getRootAppIdentifier() : appid)); | ||
String[] keys = SecurityUtils.getOAuthKeysForApp(app, Config.SLACK_PREFIX); | ||
String entity = Utils.formatMessage(PAYLOAD, authCode, Utils.urlEncode(redirectURI), keys[0], keys[1]); | ||
|
||
HttpPost tokenPost = new HttpPost(TOKEN_URL); | ||
tokenPost.setHeader(HttpHeaders.CONTENT_TYPE, "application/x-www-form-urlencoded"); | ||
tokenPost.setHeader(HttpHeaders.ACCEPT, "application/json"); | ||
tokenPost.setEntity(new StringEntity(entity, "UTF-8")); | ||
try (CloseableHttpResponse resp1 = httpclient.execute(tokenPost)) { | ||
if (resp1 != null && resp1.getEntity() != null) { | ||
Map<String, Object> token = jreader.readValue(resp1.getEntity().getContent()); | ||
if (token != null && token.containsKey("access_token")) { | ||
userAuth = getOrCreateUser(app, (String) token.get("access_token")); | ||
} | ||
EntityUtils.consumeQuietly(resp1.getEntity()); | ||
} | ||
} | ||
} | ||
} | ||
|
||
return SecurityUtils.checkIfActive(userAuth, SecurityUtils.getAuthenticatedUser(userAuth), true); | ||
} | ||
|
||
/** | ||
* Calls the Slack API to get the user profile using a given access token. | ||
* @param app the app where the user will be created, use null for root app | ||
* @param accessToken access token | ||
* @return {@link UserAuthentication} object or null if something went wrong | ||
* @throws IOException ex | ||
*/ | ||
@SuppressWarnings("unchecked") | ||
public UserAuthentication getOrCreateUser(App app, String accessToken) throws IOException { | ||
UserAuthentication userAuth = null; | ||
User user = new User(); | ||
if (accessToken != null) { | ||
HttpGet profileGet = new HttpGet(Utils.formatMessage(PROFILE_URL, accessToken)); | ||
profileGet.setHeader(HttpHeaders.ACCEPT, "application/json"); | ||
Map<String, Object> profile = null; | ||
|
||
try (CloseableHttpResponse resp2 = httpclient.execute(profileGet)) { | ||
HttpEntity respEntity = resp2.getEntity(); | ||
if (respEntity != null) { | ||
profile = jreader.readValue(respEntity.getContent()); | ||
EntityUtils.consumeQuietly(respEntity); | ||
} | ||
} | ||
|
||
if (profile != null && profile.containsKey("user")) { | ||
Map<String, Object> userData = (Map<String, Object>) profile.get("user"); | ||
Map<String, Object> teamData = (Map<String, Object>) profile.get("team"); | ||
String slackId = (String) userData.get("id"); | ||
String pic = (String) userData.get("image_512"); | ||
String email = (String) userData.get("email"); | ||
String name = (String) userData.get("name"); | ||
String team = "default"; | ||
if (teamData != null && teamData.containsKey("id")) { | ||
team = (String) teamData.get("id"); | ||
if (teamData.containsKey("name")) { | ||
team = team.concat(Config.SEPARATOR).concat(Utils.base64enc(((String) | ||
teamData.get("name")).getBytes(Config.DEFAULT_ENCODING))); | ||
} | ||
} | ||
|
||
user.setAppid(getAppid(app)); | ||
user.setIdentifier(Config.SLACK_PREFIX + slackId + Config.SEPARATOR + team); | ||
user.setEmail(email); | ||
user = User.readUserForIdentifier(user); | ||
if (user == null) { | ||
//user is new | ||
user = new User(); | ||
user.setActive(true); | ||
user.setAppid(getAppid(app)); | ||
user.setEmail(StringUtils.isBlank(email) ? Utils.getNewId() + "@slack.com" : email); | ||
user.setName(StringUtils.isBlank(name) ? "No Name" : name); | ||
user.setPassword(Utils.generateSecurityToken()); | ||
user.setPicture(getPicture(pic)); | ||
user.setIdentifier(Config.SLACK_PREFIX + slackId); | ||
String id = user.create(); | ||
if (id == null) { | ||
throw new AuthenticationServiceException("Authentication failed: cannot create new user."); | ||
} | ||
} else { | ||
if (updateUserInfo(user, pic, email, name)) { | ||
user.update(); | ||
} | ||
} | ||
userAuth = new UserAuthentication(new AuthenticatedUserDetails(user)); | ||
} | ||
} | ||
return SecurityUtils.checkIfActive(userAuth, user, false); | ||
} | ||
|
||
private boolean updateUserInfo(User user, String pic, String email, String name) { | ||
String picture = getPicture(pic); | ||
boolean update = false; | ||
if (!StringUtils.equals(user.getPicture(), picture)) { | ||
user.setPicture(picture); | ||
update = true; | ||
} | ||
if (!StringUtils.isBlank(email) && !StringUtils.equals(user.getEmail(), email)) { | ||
user.setEmail(email); | ||
update = true; | ||
} | ||
if (!StringUtils.isBlank(name) && !StringUtils.equals(user.getName(), name)) { | ||
user.setName(name); | ||
update = true; | ||
} | ||
return update; | ||
} | ||
|
||
private static String getPicture(String pic) { | ||
if (pic != null) { | ||
if (pic.contains("?")) { | ||
// user picture migth contain size parameters - remove them | ||
return pic.substring(0, pic.indexOf('?')); | ||
} else { | ||
return pic; | ||
} | ||
} | ||
return null; | ||
} | ||
|
||
private String getAppid(App app) { | ||
return (app == null) ? null : app.getAppIdentifier(); | ||
} | ||
} |