/* * Copyright 2013-2020 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.erudika.para.Para; import com.erudika.para.core.App; import com.erudika.para.core.ParaObject; import com.erudika.para.core.User; import com.erudika.para.rest.Signer; import com.erudika.para.security.filters.SAMLAuthFilter; import com.erudika.para.utils.BufferedRequestWrapper; import com.erudika.para.utils.Config; import com.erudika.para.utils.Utils; import com.nimbusds.jose.JOSEException; import com.nimbusds.jose.JWSAlgorithm; import com.nimbusds.jose.JWSHeader; import com.nimbusds.jose.JWSSigner; import com.nimbusds.jose.JWSVerifier; import com.nimbusds.jose.crypto.MACSigner; import com.nimbusds.jose.crypto.MACVerifier; import com.nimbusds.jwt.JWTClaimsSet; import com.nimbusds.jwt.SignedJWT; import java.io.IOException; import java.io.InputStream; import java.text.ParseException; import java.util.Arrays; import java.util.Date; import java.util.Enumeration; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpSession; import javax.ws.rs.core.HttpHeaders; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.LockedException; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; /** * Utility class with helper methods for authentication. * @author Alex Bogdanovski [[email protected]] */ public final class SecurityUtils { private static final Logger logger = LoggerFactory.getLogger(SecurityUtils.class); private SecurityUtils() { } /** * Extracts a User object from the security context. * @return an authenticated user or null if a user is not authenticated */ public static User getAuthenticatedUser() { return getAuthenticatedUser(SecurityContextHolder.getContext().getAuthentication()); } /** * Extracts a User object from the security context. * @param auth the authentication object * @return an authenticated user or null if a user is not authenticated */ public static User getAuthenticatedUser(Authentication auth) { User user = null; if (auth != null && auth.isAuthenticated() && auth.getPrincipal() instanceof AuthenticatedUserDetails) { user = ((AuthenticatedUserDetails) auth.getPrincipal()).getUser(); } return user; } /** * Extracts a App object from the security context. * @return an authenticated app or null if a app is not authenticated */ public static App getAuthenticatedApp() { App app = null; if (SecurityContextHolder.getContext().getAuthentication() != null) { Authentication auth = SecurityContextHolder.getContext().getAuthentication(); if (auth.isAuthenticated() && auth.getPrincipal() instanceof App) { app = (App) auth.getPrincipal(); } } return app; } /** * @return returns the current app associated with the authenticated user */ public static App getAppFromJWTAuthentication() { App app = null; if (SecurityContextHolder.getContext().getAuthentication() != null) { Authentication auth = SecurityContextHolder.getContext().getAuthentication(); if (auth instanceof JWTAuthentication) { app = ((JWTAuthentication) auth).getApp(); } } return app; } /** * @return returns the current app associated with the authenticated user */ public static App getAppFromLdapAuthentication() { App app = null; if (SecurityContextHolder.getContext().getAuthentication() != null) { Authentication auth = SecurityContextHolder.getContext().getAuthentication(); if (auth instanceof LDAPAuthentication) { app = ((LDAPAuthentication) auth).getApp(); } } return app; } /** * Returns the current authenticated {@link App} object. * * @return an App object or null */ public static App getPrincipalApp() { App app = SecurityUtils.getAuthenticatedApp(); if (app != null) { return app; } // avoid reading app from DB if it's found in the security context app = SecurityUtils.getAppFromJWTAuthentication(); if (app != null) { return app; } app = SecurityUtils.getAppFromLdapAuthentication(); if (app != null) { return app; } User user = SecurityUtils.getAuthenticatedUser(); if (user != null) { return Para.getDAO().read(Config.getRootAppIdentifier(), App.id(user.getAppid())); } logger.warn("Unauthenticated request - app not found in security context."); return null; } /** * An app can edit itself or delete itself. It can't read, edit, overwrite or delete other apps, unless it is the * root app. * * @param app an app * @param object another object * @return true if app passes the check */ public static boolean checkImplicitAppPermissions(App app, ParaObject object) { if (app != null && object != null) { return isNotAnApp(object.getType()) || app.getId().equals(object.getId()) || app.isRootApp(); } return false; } /** * @param type some type * @return true if type of object is not "app" */ public static boolean isNotAnApp(String type) { return !StringUtils.equals(type, Utils.type(App.class)); } /** * @param type some type */ public static void warnIfUserTypeDetected(String type) { if (Utils.type(User.class).equals(type)) { logger.warn("Users should be created through /jwt_auth or through an authentication filter."); } } /** * Check if a user can modify an object. If there's no user principal found, this returns true. * * @param app app in context * @param object some object * @return true if user is the owner/creator of the object. */ public static boolean checkIfUserCanModifyObject(App app, ParaObject object) { User user = SecurityUtils.getAuthenticatedUser(); if (user != null && app != null && object != null) { if (app.permissionsContainOwnKeyword(user, object)) { return user.canModify(object); } } return true; // skip } /** * Clears the session. Deletes cookies and clears the security context. * @param req HTTP request */ public static void clearSession(HttpServletRequest req) { SecurityContextHolder.clearContext(); if (req != null) { HttpSession session = req.getSession(false); if (session != null) { session.invalidate(); } } } /** * Validates a JWT token. * @param secret secret used for generating the token * @param jwt token to validate * @return true if token is valid */ public static boolean isValidJWToken(String secret, SignedJWT jwt) { try { if (secret != null && jwt != null) { JWSVerifier verifier = new MACVerifier(secret); if (jwt.verify(verifier)) { Date referenceTime = new Date(); JWTClaimsSet claims = jwt.getJWTClaimsSet(); Date expirationTime = claims.getExpirationTime(); Date notBeforeTime = claims.getNotBeforeTime(); boolean expired = expirationTime == null || expirationTime.before(referenceTime); boolean notYetValid = notBeforeTime != null && notBeforeTime.after(referenceTime); return !(expired || notYetValid); } } } catch (JOSEException e) { logger.warn(null, e); } catch (ParseException ex) { logger.warn(null, ex); } return false; } /** * Generates a new "super" JWT token for apps. * @param app the app object * @return a new JWT or null */ public static SignedJWT generateSuperJWToken(App app) { return generateJWToken(null, app); } /** * Generates a new JWT token. * @param user a User object belonging to the app * @param app the app object * @return a new JWT or null */ public static SignedJWT generateJWToken(User user, App app) { if (app != null) { try { Date now = new Date(); JWTClaimsSet.Builder claimsSet = new JWTClaimsSet.Builder(); String userSecret = ""; claimsSet.issueTime(now); claimsSet.expirationTime(new Date(now.getTime() + (app.getTokenValiditySec() * 1000))); claimsSet.notBeforeTime(now); claimsSet.claim("refresh", getNextRefresh(app.getTokenValiditySec())); claimsSet.claim(Config._APPID, app.getId()); if (user != null) { claimsSet.subject(user.getId()); claimsSet.claim("idp", user.getIdentityProvider()); userSecret = user.getTokenSecret(); } JWSSigner signer = new MACSigner(app.getSecret() + userSecret); SignedJWT signedJWT = new SignedJWT(new JWSHeader(JWSAlgorithm.HS256), claimsSet.build()); signedJWT.sign(signer); return signedJWT; } catch (JOSEException e) { logger.warn("Unable to sign JWT: {}.", e.getMessage()); } } return null; } /** * Decides when the next token refresh should be. * @param tokenValiditySec token validity period * @return a refresh timestamp to be used by API clients */ private static long getNextRefresh(long tokenValiditySec) { long interval = Config.JWT_REFRESH_INTERVAL_SEC; // estimate when the next token refresh should be // usually every hour, or halfway until the time it expires if (tokenValiditySec < (2 * interval)) { interval = (tokenValiditySec / 2); } return System.currentTimeMillis() + (interval * 1000); } /** * Return the OAuth app ID and secret key for a given app by reading the app settings, or the config file. * @param app the app in which to look for these keys * @param prefix a service prefix: "fb" for facebook, "tw" for twitter etc. See {@link Config} * @return an array ["app_id", "secret_key"] or ["", ""] */ public static String[] getOAuthKeysForApp(App app, String prefix) { prefix = StringUtils.removeEnd(prefix + "", Config.SEPARATOR); String appIdKey = prefix + "_app_id"; String secretKey = prefix + "_secret"; String[] keys = new String[]{"", ""}; if (app != null) { Map<String, Object> settings = app.getSettings(); if (settings.containsKey(appIdKey) && settings.containsKey(secretKey)) { keys[0] = settings.get(appIdKey) + ""; keys[1] = settings.get(secretKey) + ""; } else if (app.isRootApp()) { keys[0] = Config.getConfigParam(appIdKey, ""); keys[1] = Config.getConfigParam(secretKey, ""); } } return keys; } /** * Returns a map of LDAP configuration properties for a given app, read from app.settings or config file. * @param app the app in which to look for these keys * @return a map of keys and values */ public static Map<String, String> getLdapSettingsForApp(App app) { Map<String, String> ldapSettings = new HashMap<>(); if (app != null) { ldapSettings.put("security.ldap.server_url", "ldap://localhost:8389/"); ldapSettings.put("security.ldap.active_directory_domain", ""); ldapSettings.put("security.ldap.base_dn", "dc=springframework,dc=org"); ldapSettings.put("security.ldap.bind_dn", ""); ldapSettings.put("security.ldap.bind_pass", ""); ldapSettings.put("security.ldap.user_search_base", ""); ldapSettings.put("security.ldap.user_search_filter", "(cn={0})"); ldapSettings.put("security.ldap.user_dn_pattern", "uid={0}"); ldapSettings.put("security.ldap.password_attribute", "userPassword"); //ldapSettings.put("security.ldap.compare_passwords", "false"); //don't remove comment Map<String, Object> settings = app.getSettings(); for (Map.Entry<String, String> entry : ldapSettings.entrySet()) { if (settings.containsKey(entry.getKey())) { entry.setValue(settings.get(entry.getKey()) + ""); } else if (app.isRootApp()) { entry.setValue(Config.getConfigParam(entry.getKey(), entry.getValue())); } } } return ldapSettings; } /** * Returns the value of the app setting, read from from app.settings or from the config file if app is root. * @param app the app in which to look for these keys * @param key setting key * @param defaultValue default value * @return the value of the configuration property as string */ public static String getSettingForApp(App app, String key, String defaultValue) { if (app != null) { Map<String, Object> settings = app.getSettings(); if (settings.containsKey(key)) { return String.valueOf(settings.getOrDefault(key, defaultValue)); } else if (app.isRootApp()) { return Config.getConfigParam(key, defaultValue); } } return defaultValue; } /** * Checks if account is active. * @param userAuth user authentication object * @param user user object * @param throwException throw or not * @return the authentication object if {@code user.active == true} */ public static UserAuthentication checkIfActive(UserAuthentication userAuth, User user, boolean throwException) { if (userAuth == null || user == null || user.getIdentifier() == null) { if (throwException) { throw new BadCredentialsException("Bad credentials."); } else { logger.debug("Bad credentials. {}", userAuth); return null; } } else if (!user.getActive()) { if (throwException) { throw new LockedException("Account " + user.getId() + " (" + user.getAppid() + "/" + user.getIdentifier() + ") is locked."); } else { logger.warn("Account {} ({}/{}) is locked.", user.getId(), user.getAppid(), user.getIdentifier()); return null; } } return userAuth; } /** * Validates the signature of the request. * @param incoming the incoming HTTP request containing a signature * @param secretKey the app's secret key * @return true if the signature is valid */ public static boolean isValidSignature(HttpServletRequest incoming, String secretKey) { if (incoming == null || StringUtils.isBlank(secretKey)) { return false; } String auth = incoming.getHeader(HttpHeaders.AUTHORIZATION); String givenSig = StringUtils.substringAfter(auth, "Signature="); String sigHeaders = StringUtils.substringBetween(auth, "SignedHeaders=", ","); String credential = StringUtils.substringBetween(auth, "Credential=", ","); String accessKey = StringUtils.substringBefore(credential, "/"); if (StringUtils.isBlank(auth)) { givenSig = incoming.getParameter("X-Amz-Signature"); sigHeaders = incoming.getParameter("X-Amz-SignedHeaders"); credential = incoming.getParameter("X-Amz-Credential"); accessKey = StringUtils.substringBefore(credential, "/"); } Set<String> headersUsed = new HashSet<>(Arrays.asList(sigHeaders.split(";"))); Map<String, String> headers = new HashMap<>(); for (Enumeration<String> e = incoming.getHeaderNames(); e.hasMoreElements();) { String head = e.nextElement().toLowerCase(); if (headersUsed.contains(head)) { headers.put(head, incoming.getHeader(head)); } } Map<String, String> params = new HashMap<>(); for (Map.Entry<String, String[]> param : incoming.getParameterMap().entrySet()) { params.put(param.getKey(), param.getValue()[0]); } String path = incoming.getRequestURI(); String endpoint = StringUtils.removeEndIgnoreCase(incoming.getRequestURL().toString(), path); String httpMethod = incoming.getMethod(); InputStream entity; try { entity = new BufferedRequestWrapper(incoming).getInputStream(); if (entity.available() <= 0) { entity = null; } } catch (IOException ex) { logger.error(null, ex); entity = null; } Signer signer = new Signer(); Map<String, String> sig = signer.sign(httpMethod, endpoint, path, headers, params, entity, accessKey, secretKey); String auth2 = sig.get(HttpHeaders.AUTHORIZATION); String recreatedSig = StringUtils.substringAfter(auth2, "Signature="); boolean signaturesMatch = StringUtils.equals(givenSig, recreatedSig); if (Config.getConfigBoolean("debug_request_signatures", false)) { logger.info("Incoming client signature for request {} {}: {} == {} calculated by server, matching: {}", httpMethod, path, givenSig, recreatedSig, signaturesMatch); } return signaturesMatch; } /** * @param request HTTP request * @return the URL with the correct protocol, read from X-Forwarded-Proto and CloudFront-Forwarded-Proto headers. */ public static String getRedirectUrl(HttpServletRequest request) { String url = request.getRequestURL().toString(); if (!StringUtils.isBlank(request.getHeader("X-Forwarded-Proto"))) { return request.getHeader("X-Forwarded-Proto") + url.substring(url.indexOf(':')); } else if (!StringUtils.isBlank(request.getHeader("CloudFront-Forwarded-Proto"))) { return request.getHeader("CloudFront-Forwarded-Proto") + url.substring(url.indexOf(':')); } return url; } /** * @param request HTTP request * @return the appid if it's present in either the 'state' or 'appid' query parameters */ public static String getAppidFromAuthRequest(HttpServletRequest request) { String appid1 = request.getParameter("state"); String appid2 = request.getParameter(Config._APPID); if (StringUtils.isBlank(appid1) && StringUtils.isBlank(appid2)) { if (StringUtils.startsWith(request.getRequestURI(), SAMLAuthFilter.SAML_ACTION + "/")) { return StringUtils.trimToNull(request.getRequestURI().substring(SAMLAuthFilter.SAML_ACTION.length() + 1)); } else { return null; } } else if (!StringUtils.isBlank(appid1)) { return StringUtils.trimToNull(appid1); } else { return StringUtils.trimToNull(appid2); } } }