| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you 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. |
| */ |
| package org.apache.knox.gateway.pac4j.session; |
| |
| import org.apache.commons.codec.binary.Base64; |
| import org.apache.commons.io.IOUtils; |
| import org.apache.knox.gateway.pac4j.filter.Pac4jDispatcherFilter; |
| import org.apache.knox.gateway.services.security.CryptoService; |
| import org.apache.knox.gateway.services.security.EncryptionResult; |
| import org.apache.knox.gateway.util.Urls; |
| import org.pac4j.core.context.ContextHelper; |
| import org.pac4j.core.context.Cookie; |
| import org.pac4j.core.context.JEEContext; |
| import org.pac4j.core.context.WebContext; |
| import org.pac4j.core.context.session.SessionStore; |
| import org.pac4j.core.exception.TechnicalException; |
| import org.pac4j.core.profile.CommonProfile; |
| import org.pac4j.core.util.JavaSerializationHelper; |
| import org.pac4j.core.util.Pac4jConstants; |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| |
| import java.io.ByteArrayInputStream; |
| import java.io.ByteArrayOutputStream; |
| import java.io.IOException; |
| import java.io.Serializable; |
| import java.util.Map; |
| import java.util.Optional; |
| import java.util.zip.GZIPInputStream; |
| import java.util.zip.GZIPOutputStream; |
| |
| /** |
| * Specific session store where data are saved into cookies (and not in memory). |
| * Each data is encrypted and base64 encoded before being saved as a cookie (for security reasons). |
| * |
| * @since 0.8.0 |
| */ |
| public class KnoxSessionStore<C extends WebContext> implements SessionStore<C> { |
| |
| private static final Logger logger = LoggerFactory.getLogger(KnoxSessionStore.class); |
| |
| public static final String PAC4J_PASSWORD = "pac4j.password"; |
| |
| public static final String PAC4J_SESSION_PREFIX = "pac4j.session."; |
| |
| private final JavaSerializationHelper javaSerializationHelper; |
| |
| private final CryptoService cryptoService; |
| |
| private final String clusterName; |
| |
| private final String domainSuffix; |
| |
| public KnoxSessionStore(final CryptoService cryptoService, final String clusterName, final String domainSuffix) { |
| javaSerializationHelper = new JavaSerializationHelper(); |
| this.cryptoService = cryptoService; |
| this.clusterName = clusterName; |
| this.domainSuffix = domainSuffix; |
| } |
| |
| |
| @Override |
| public String getOrCreateSessionId(WebContext context) { |
| return null; |
| } |
| |
| private Serializable uncompressDecryptBase64(final String v) { |
| if (v != null && !v.isEmpty()) { |
| byte[] bytes = Base64.decodeBase64(v); |
| EncryptionResult result = EncryptionResult.fromByteArray(bytes); |
| byte[] clear = cryptoService.decryptForCluster(this.clusterName, |
| PAC4J_PASSWORD, |
| result.cipher, |
| result.iv, |
| result.salt); |
| if (clear != null) { |
| try { |
| return javaSerializationHelper.deserializeFromBytes(unCompress(clear)); |
| } catch (IOException e) { |
| throw new TechnicalException(e); |
| } |
| } |
| } |
| return null; |
| } |
| |
| @Override |
| public Optional<Object> get(WebContext context, String key) { |
| final Cookie cookie = ContextHelper.getCookie(context, PAC4J_SESSION_PREFIX + key); |
| Object value = null; |
| if (cookie != null) { |
| value = uncompressDecryptBase64(cookie.getValue()); |
| } |
| logger.debug("Get from session: {} = {}", key, value); |
| return Optional.ofNullable(value); |
| } |
| |
| private String compressEncryptBase64(final Object o) { |
| if (o == null || o.equals("") |
| || (o instanceof Map<?,?> && ((Map<?,?>)o).isEmpty())) { |
| return null; |
| } else { |
| byte[] bytes = javaSerializationHelper.serializeToBytes((Serializable) o); |
| |
| /* compress the data */ |
| try { |
| bytes = compress(bytes); |
| |
| if(bytes.length > 3000) { |
| logger.warn("Cookie too big, it might not be properly set"); |
| } |
| |
| } catch (final IOException e) { |
| throw new TechnicalException(e); |
| } |
| |
| EncryptionResult result = cryptoService.encryptForCluster(this.clusterName, PAC4J_PASSWORD, bytes); |
| return Base64.encodeBase64String(result.toByteAray()); |
| } |
| } |
| |
| @Override |
| public void set(WebContext context, String key, Object value) { |
| Object profile = value; |
| Cookie cookie; |
| |
| if (value == null) { |
| cookie = new Cookie(PAC4J_SESSION_PREFIX + key, null); |
| } else { |
| if (key.contentEquals(Pac4jConstants.USER_PROFILES)) { |
| /* trim the profile object */ |
| profile = clearUserProfile(value); |
| } |
| logger.debug("Save in session: {} = {}", key, profile); |
| cookie = new Cookie(PAC4J_SESSION_PREFIX + key, |
| compressEncryptBase64(profile)); |
| } |
| try { |
| String domain = Urls |
| .getDomainName(context.getFullRequestURL(), this.domainSuffix); |
| if (domain == null) { |
| domain = context.getServerName(); |
| } |
| cookie.setDomain(domain); |
| } catch (final Exception e) { |
| throw new TechnicalException(e); |
| } |
| cookie.setHttpOnly(true); |
| cookie.setSecure(ContextHelper.isHttpsOrSecure(context)); |
| |
| /* |
| * set the correct path for setting pac4j profile cookie. |
| * This is because, Pac4jDispatcherFilter.PAC4J_CALLBACK_PARAMETER in the path |
| * indicates callback when ? cannot be used. |
| */ |
| if (context.getPath() != null && context.getPath() |
| .contains(Pac4jDispatcherFilter.PAC4J_CALLBACK_PARAMETER)) { |
| |
| final String[] parts = ((JEEContext) context).getNativeRequest().getRequestURI() |
| .split( |
| "websso"+ Pac4jDispatcherFilter.URL_PATH_SEPARATOR + Pac4jDispatcherFilter.PAC4J_CALLBACK_PARAMETER); |
| |
| cookie.setPath(parts[0]); |
| |
| } |
| context.addResponseCookie(cookie); |
| } |
| |
| /** |
| * A function used to compress the data using GZIP |
| * @param data data to be compressed |
| * @return gziped data |
| * @since 1.1.0 |
| */ |
| private static byte[] compress(final byte[] data) throws IOException { |
| try (ByteArrayOutputStream byteStream = new ByteArrayOutputStream(data.length)) { |
| try(GZIPOutputStream gzip = new GZIPOutputStream(byteStream)) { |
| gzip.write(data); |
| } |
| return byteStream.toByteArray(); |
| } |
| } |
| |
| /** |
| * Decompress the data compressed using gzip |
| * |
| * @param data data to be decompressed |
| * @return uncompressed data |
| * @throws IOException exception if can't decompress |
| * @since 1.1.0 |
| */ |
| private static byte[] unCompress(final byte[] data) throws IOException { |
| try (ByteArrayInputStream inputStream = new ByteArrayInputStream(data); |
| GZIPInputStream gzip = new GZIPInputStream(inputStream)) { |
| return IOUtils.toByteArray(gzip); |
| } |
| } |
| |
| /** |
| * Keep only the fileds that are needed for Pac4J. |
| * Used to reduce the cookie size. |
| * @param value profile object |
| * @return trimmed profile object |
| * @since 1.3.0 |
| */ |
| private Object clearUserProfile(final Object value) { |
| if(value instanceof Map<?,?>) { |
| final Map<String, CommonProfile> profiles = (Map<String, CommonProfile>) value; |
| profiles.forEach((name, profile) -> profile.removeLoginData()); |
| return profiles; |
| } else { |
| final CommonProfile profile = (CommonProfile) value; |
| profile.removeLoginData(); |
| return profile; |
| } |
| } |
| |
| @Override |
| public Optional<SessionStore<C>> buildFromTrackableSession(WebContext arg0, Object arg1) { |
| return Optional.empty(); |
| } |
| |
| @Override |
| public boolean destroySession(WebContext arg0) { |
| return false; |
| } |
| |
| @Override |
| public Optional getTrackableSession(WebContext arg0) { |
| return Optional.empty(); |
| } |
| |
| @Override |
| public boolean renewSession(final WebContext context) { |
| return false; |
| } |
| } |