Last active
December 2, 2016 12:06
-
-
Save albogdano/808e26d337f1a4b9fb4d82754d24491c to your computer and use it in GitHub Desktop.
Authentication filter for Para, for handling authentication requests to a generic OAuth 2.0 identity provider
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
/* | |
* Copyright 2013-2016 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://github.com/erudika | |
*/ | |
package com.erudika.para.security; | |
import com.eaio.uuid.UUID; | |
import com.erudika.para.core.utils.ParaObjectUtils; | |
import com.erudika.para.core.User; | |
import com.erudika.para.utils.Config; | |
import com.erudika.para.utils.Utils; | |
import com.fasterxml.jackson.databind.ObjectReader; | |
import java.io.IOException; | |
import java.net.URLEncoder; | |
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.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.client.CloseableHttpClient; | |
import org.apache.http.impl.client.HttpClients; | |
import org.apache.http.util.EntityUtils; | |
import org.springframework.security.authentication.AuthenticationServiceException; | |
import org.springframework.security.authentication.BadCredentialsException; | |
import org.springframework.security.authentication.LockedException; | |
import org.springframework.security.core.Authentication; | |
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter; | |
/** | |
* A filter that handles authentication requests to a generic OAuth 2.0 identity provider | |
* @author Alex Bogdanovski [alex@erudika.com] | |
*/ | |
public class GenericOAuth2Filter extends AbstractAuthenticationProcessingFilter { | |
private final CloseableHttpClient httpclient; | |
private final ObjectReader jreader; | |
private static final String DOMAIN = Config.getConfigParam("security.oauth.domain", "invalid.co"); | |
private static final String PROFILE_URL = Config.getConfigParam("security.oauth.profile_url", ""); | |
private static final String TOKEN_URL = Config.getConfigParam("security.oauth.token_url", ""); | |
private static final String PAYLOAD = "code={0}&redirect_uri={1}" | |
+ "&scope={2}&client_id={3}&client_secret={4}&grant_type=authorization_code"; | |
/** | |
* The default filter mapping. | |
*/ | |
public static final String OAUTH2_ACTION = "oauth2_auth"; | |
/** | |
* Default constructor. | |
* @param defaultFilterProcessesUrl the url of the filter | |
*/ | |
public GenericOAuth2Filter(final String defaultFilterProcessesUrl) { | |
super(defaultFilterProcessesUrl); | |
this.jreader = ParaObjectUtils.getJsonReader(Map.class); | |
this.httpclient = HttpClients.createDefault(); | |
} | |
/** | |
* 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(OAUTH2_ACTION)) { | |
String authCode = request.getParameter("code"); | |
if (!StringUtils.isBlank(authCode)) { | |
String appid = request.getParameter("appid"); | |
String redirectURI = request.getRequestURL().toString() + (appid == null ? "" : "?appid=" + appid); | |
String[] keys = SecurityUtils.getCustomAuthSettings(appid, Config.OAUTH2_PREFIX, request); | |
String entity = Utils.formatMessage(PAYLOAD, | |
URLEncoder.encode(authCode, "UTF-8"), | |
URLEncoder.encode(redirectURI, "UTF-8"), | |
URLEncoder.encode(Config.getConfigParam("security.oauth.scope", ""), "UTF-8"), | |
keys[0], keys[1]); | |
String acceptHeader = Config.getConfigParam("security.oauth.accept_header", ""); | |
HttpPost tokenPost = new HttpPost(TOKEN_URL); | |
tokenPost.setHeader(HttpHeaders.CONTENT_TYPE, "application/x-www-form-urlencoded"); | |
tokenPost.setEntity(new StringEntity(entity, "UTF-8")); | |
if (!StringUtils.isBlank(acceptHeader)) { | |
tokenPost.setHeader(HttpHeaders.ACCEPT, acceptHeader); | |
} | |
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(appid, (String) token.get("access_token")); | |
} | |
EntityUtils.consumeQuietly(resp1.getEntity()); | |
} | |
} | |
} | |
User user = SecurityUtils.getAuthenticatedUser(userAuth); | |
if (userAuth == null || user == null || user.getIdentifier() == null) { | |
throw new BadCredentialsException("Bad credentials."); | |
} else if (!user.getActive()) { | |
throw new LockedException("Account is locked."); | |
} | |
return userAuth; | |
} | |
/** | |
* Calls an external API to get the user profile using a given access token. | |
* @param appid app identifier of the parent app, use null for root app | |
* @param accessToken access token | |
* @return {@link UserAuthentication} object or null if something went wrong | |
* @throws IOException ex | |
*/ | |
public UserAuthentication getOrCreateUser(String appid, String accessToken) throws IOException { | |
UserAuthentication userAuth = null; | |
if (accessToken != null) { | |
User user = new User(); | |
user.setAppid(appid); | |
String acceptHeader = Config.getConfigParam("security.oauth.accept_header", ""); | |
HttpGet profileGet = new HttpGet(PROFILE_URL); | |
profileGet.setHeader(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken); | |
if (!StringUtils.isBlank(acceptHeader)) { | |
profileGet.setHeader(HttpHeaders.ACCEPT, acceptHeader); | |
} | |
CloseableHttpResponse resp2 = httpclient.execute(profileGet); | |
HttpEntity respEntity = resp2.getEntity(); | |
String ctype = resp2.getFirstHeader(HttpHeaders.CONTENT_TYPE).getValue(); | |
if (respEntity != null && Utils.isJsonType(ctype)) { | |
Map<String, Object> profile = jreader.readValue(respEntity.getContent()); | |
String accountIdParam = Config.getConfigParam("security.oauth.parameters.id", "id"); | |
String pictureParam = Config.getConfigParam("security.oauth.parameters.picture", "picture"); | |
String emailParam = Config.getConfigParam("security.oauth.parameters.email", "email"); | |
String nameParam = Config.getConfigParam("security.oauth.parameters.name", "name"); | |
if (profile != null && profile.containsKey(accountIdParam)) { | |
String oauthAccountId = (String) profile.get(accountIdParam); | |
String pic = (String) profile.get(pictureParam); | |
String email = (String) profile.get(emailParam); | |
String name = (String) profile.get(nameParam); | |
user.setIdentifier(Config.OAUTH2_PREFIX.concat(oauthAccountId)); | |
user = User.readUserForIdentifier(user); | |
if (user == null) { | |
//user is new | |
user = new User(); | |
user.setActive(true); | |
user.setAppid(appid); | |
user.setEmail(StringUtils.isBlank(email) ? oauthAccountId + "@" + DOMAIN : email); | |
user.setName(StringUtils.isBlank(name) ? "No Name" : name); | |
user.setPassword(new UUID().toString()); | |
user.setPicture(getPicture(pic)); | |
user.setIdentifier(Config.OAUTH2_PREFIX.concat(oauthAccountId)); | |
String id = user.create(); | |
if (id == null) { | |
throw new AuthenticationServiceException("Authentication failed: cannot create new user."); | |
} | |
} else { | |
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 (update) { | |
user.update(); | |
} | |
} | |
userAuth = new UserAuthentication(new AuthenticatedUserDetails(user)); | |
} | |
EntityUtils.consumeQuietly(respEntity); | |
} | |
} | |
return userAuth; | |
} | |
private static String getPicture(String pic) { | |
if (pic != null) { | |
if (pic.indexOf("?") > 0) { | |
// user picture migth contain size parameters - remove them | |
return pic.substring(0, pic.indexOf("?")); | |
} else { | |
return pic; | |
} | |
} | |
return null; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment