Skip to content

Commit

Permalink
added Slack authentication support
Browse files Browse the repository at this point in the history
  • Loading branch information
albogdano committed Jul 7, 2019
1 parent 410dbe3 commit e03e867
Show file tree
Hide file tree
Showing 6 changed files with 286 additions and 0 deletions.
11 changes: 11 additions & 0 deletions para-core/src/main/java/com/erudika/para/core/User.java
Expand Up @@ -447,6 +447,15 @@ public boolean isMicrosoftUser() {
return StringUtils.startsWithIgnoreCase(identifier, Config.MICROSOFT_PREFIX);
}

/**
* Is the main identifier a Slack account id.
* @return true if user is signed in with a Microsoft account
*/
@JsonIgnore
public boolean isSlackUser() {
return StringUtils.startsWithIgnoreCase(identifier, Config.SLACK_PREFIX);
}

/**
* Is the main identifier a LDAP account.
* @return true if user is signed in with a LDAP account
Expand Down Expand Up @@ -509,6 +518,8 @@ public String getIdentityProvider() {
return "linkedin";
} else if (isMicrosoftUser()) {
return "microsoft";
} else if (isSlackUser()) {
return "slack";
} else if (isLDAPUser()) {
return "ldap";
} else if (isSAMLUser()) {
Expand Down
10 changes: 10 additions & 0 deletions para-core/src/main/java/com/erudika/para/utils/Config.java
Expand Up @@ -102,6 +102,8 @@ private Config() { }
public static final String GITHUB_PREFIX = "gh" + SEPARATOR;
/** Microsoft prefix - defaults to 'ms:'. */
public static final String MICROSOFT_PREFIX = "ms" + SEPARATOR;
/** Slack prefix - defaults to 'sl:'. */
public static final String SLACK_PREFIX = "sl" + SEPARATOR;
/** OAuth2 generic prefix - defaults to 'oa2:'. */
public static final String OAUTH2_PREFIX = "oa2" + SEPARATOR;
/** LDAP prefix - defaults to 'ldap:'. */
Expand Down Expand Up @@ -194,6 +196,14 @@ private Config() { }
* Microsoft app secret (for authentication).
*/
public static final String MICROSOFT_SECRET = getConfigParam("ms_secret", "");
/**
* Slack app id (for authentication).
*/
public static final String SLACK_APP_ID = getConfigParam("sl_app_id", "");
/**
* Slack app secret (for authentication).
*/
public static final String SLACK_SECRET = getConfigParam("sl_secret", "");
/**
* The identifier of the first administrator (can be email, OpenID, or Facebook user id).
*/
Expand Down
Expand Up @@ -31,6 +31,7 @@
import com.erudika.para.core.User;
import com.erudika.para.rest.RestUtils;
import com.erudika.para.security.filters.LdapAuthFilter;
import com.erudika.para.security.filters.SlackAuthFilter;
import com.erudika.para.utils.Config;
import com.erudika.para.utils.Utils;
import com.nimbusds.jwt.SignedJWT;
Expand Down Expand Up @@ -74,6 +75,7 @@ public class JWTRestfulAuthFilter extends GenericFilterBean {
private LinkedInAuthFilter linkedinAuth;
private TwitterAuthFilter twitterAuth;
private MicrosoftAuthFilter microsoftAuth;
private SlackAuthFilter slackAuth;
private GenericOAuth2Filter oauth2Auth;
private LdapAuthFilter ldapAuth;
private PasswordAuthFilter passwordAuth;
Expand Down Expand Up @@ -290,6 +292,8 @@ private UserAuthentication getOrCreateUser(App app, String identityProvider, Str
return twitterAuth.getOrCreateUser(app, accessToken);
} else if ("microsoft".equalsIgnoreCase(identityProvider)) {
return microsoftAuth.getOrCreateUser(app, accessToken);
} else if ("slack".equalsIgnoreCase(identityProvider)) {
return slackAuth.getOrCreateUser(app, accessToken);
} else if ("oauth2".equalsIgnoreCase(identityProvider)) {
return oauth2Auth.getOrCreateUser(app, accessToken);
} else if ("ldap".equalsIgnoreCase(identityProvider)) {
Expand Down
Expand Up @@ -30,6 +30,7 @@
import com.erudika.para.security.filters.LdapAuthFilter;
import com.erudika.para.security.filters.SAMLAuthFilter;
import com.erudika.para.security.filters.SAMLMetadataFilter;
import com.erudika.para.security.filters.SlackAuthFilter;
import com.erudika.para.utils.Config;
import com.typesafe.config.ConfigList;
import com.typesafe.config.ConfigObject;
Expand Down Expand Up @@ -76,6 +77,7 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter {
private final TwitterAuthFilter twitterFilter;
private final GitHubAuthFilter githubFilter;
private final MicrosoftAuthFilter microsoftFilter;
private final SlackAuthFilter slackFilter;
private final GenericOAuth2Filter oauth2Filter;
private final LdapAuthFilter ldapFilter;
private final SAMLAuthFilter samlFilter;
Expand All @@ -96,6 +98,7 @@ public SecurityConfig() {
twitterFilter = getInstance(TwitterAuthFilter.class);
githubFilter = getInstance(GitHubAuthFilter.class);
microsoftFilter = getInstance(MicrosoftAuthFilter.class);
slackFilter = getInstance(SlackAuthFilter.class);
oauth2Filter = getInstance(GenericOAuth2Filter.class);
ldapFilter = getInstance(LdapAuthFilter.class);
samlFilter = getInstance(SAMLAuthFilter.class);
Expand Down Expand Up @@ -232,6 +235,11 @@ private void registerAuthFilters(HttpSecurity http) throws Exception {
http.addFilterAfter(microsoftFilter, BasicAuthenticationFilter.class);
}

if (slackFilter != null) {
slackFilter.setAuthenticationManager(authenticationManager());
http.addFilterAfter(slackFilter, BasicAuthenticationFilter.class);
}

if (oauth2Filter != null) {
oauth2Filter.setAuthenticationManager(authenticationManager());
http.addFilterAfter(oauth2Filter, BasicAuthenticationFilter.class);
Expand Down
Expand Up @@ -30,6 +30,7 @@
import com.erudika.para.security.filters.LdapAuthFilter;
import com.erudika.para.security.filters.SAMLAuthFilter;
import com.erudika.para.security.filters.SAMLMetadataFilter;
import com.erudika.para.security.filters.SlackAuthFilter;
import com.erudika.para.utils.Config;
import com.google.inject.AbstractModule;
import com.google.inject.Provides;
Expand All @@ -56,6 +57,7 @@ public class SecurityModule extends AbstractModule {
private TwitterAuthFilter twitterFilter;
private GitHubAuthFilter githubFilter;
private MicrosoftAuthFilter microsoftFilter;
private SlackAuthFilter slackFilter;
private GenericOAuth2Filter oauth2Filter;
private LdapAuthFilter ldapFilter;
private SAMLAuthFilter samlFilter;
Expand Down Expand Up @@ -322,6 +324,27 @@ public void setMicrosoftFilter(MicrosoftAuthFilter microsoftFilter) {
this.microsoftFilter = microsoftFilter;
}

/**
* @return filter
*/
@Provides
public SlackAuthFilter getSlackFilter() {
if (slackFilter == null) {
slackFilter = new SlackAuthFilter("/" + SlackAuthFilter.SLACK_ACTION);
slackFilter.setAuthenticationSuccessHandler(getSuccessHandler());
slackFilter.setAuthenticationFailureHandler(getFailureHandler());
slackFilter.setRememberMeServices(getRemembeMeServices());
}
return slackFilter;
}

/**
* @param slackFilter filter
*/
public void setMicrosoftFilter(SlackAuthFilter slackFilter) {
this.slackFilter = slackFilter;
}

/**
* @return filter
*/
Expand Down
@@ -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();
}
}

0 comments on commit e03e867

Please sign in to comment.