001/* 002 * The contents of this file are subject to the terms of the Common Development and 003 * Distribution License (the License). You may not use this file except in compliance with the 004 * License. 005 * 006 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the 007 * specific language governing permission and limitations under the License. 008 * 009 * When distributing Covered Software, include this CDDL Header Notice in each file and include 010 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL 011 * Header, with the fields enclosed by brackets [] replaced by your own identifying 012 * information: "Portions copyright [year] [name of copyright owner]". 013 * 014 * Copyright 2014 ForgeRock AS. 015 */ 016 017package org.forgerock.openig.jwt; 018 019import static java.lang.String.*; 020import static org.forgerock.openig.util.Json.*; 021 022import java.io.IOException; 023import java.security.KeyPair; 024import java.util.Collection; 025import java.util.LinkedHashMap; 026import java.util.List; 027import java.util.Map; 028import java.util.Set; 029 030import org.forgerock.json.jose.builders.EncryptedJwtBuilder; 031import org.forgerock.json.jose.builders.JwtBuilderFactory; 032import org.forgerock.json.jose.builders.JwtClaimsSetBuilder; 033import org.forgerock.json.jose.common.JwtReconstruction; 034import org.forgerock.json.jose.exceptions.JweDecryptionException; 035import org.forgerock.json.jose.jwe.EncryptedJwt; 036import org.forgerock.json.jose.jwe.EncryptionMethod; 037import org.forgerock.json.jose.jwe.JweAlgorithm; 038import org.forgerock.json.jose.jwt.JwtClaimsSet; 039import org.forgerock.openig.http.Cookie; 040import org.forgerock.openig.http.Exchange; 041import org.forgerock.openig.http.Session; 042import org.forgerock.openig.jwt.dirty.DirtyCollection; 043import org.forgerock.openig.jwt.dirty.DirtyListener; 044import org.forgerock.openig.jwt.dirty.DirtySet; 045import org.forgerock.openig.log.Logger; 046import org.forgerock.util.MapDecorator; 047 048/** 049 * Represents an OpenIG {@link Session} that will be stored as an encrypted JSON Web Token in a Cookie. 050 * The generated JWT is encrypted with the {@link JweAlgorithm#RSAES_PKCS1_V1_5} algorithm and {@link 051 * EncryptionMethod#A128CBC_HS256} method. 052 */ 053public class JwtCookieSession extends MapDecorator<String, Object> implements Session, DirtyListener { 054 055 /** 056 * Name of the cookie that will store the JWT session. 057 */ 058 public static final String OPENIG_JWT_SESSION = "openig-jwt-session"; 059 060 /** 061 * {@literal exchange.request} will be used to read existing cookie (if any), and {@literal exchange.response} will 062 * be used to write the new cookie value. 063 */ 064 private final Exchange exchange; 065 066 /** 067 * Know how to rebuild a JWT from a String. 068 */ 069 private final JwtReconstruction reader = new JwtReconstruction(); 070 071 /** 072 * Factory for JWT. 073 */ 074 private final JwtBuilderFactory factory = new JwtBuilderFactory(); 075 076 /** 077 * Marker used to detect if the session was used or not. 078 */ 079 private boolean dirty = false; 080 081 /** 082 * Name to be used for the JWT Cookie. 083 */ 084 private final String cookieName; 085 086 /** 087 * Logger used to output warnings about session's size. 088 */ 089 private final Logger logger; 090 091 /** 092 * Used for decryption/encryption of session's content. 093 */ 094 private final KeyPair pair; 095 096 /** 097 * Builds a new JwtCookieSession that will manage the given Exchange's session. 098 * 099 * @param exchange 100 * Exchange used to access {@literal Cookie} and {@literal Set-Cookie} headers. 101 * @param pair 102 * Secret key used to sign the JWT payload. 103 * @param cookieName 104 * Name to be used for the JWT Cookie. 105 * @param logger 106 * Logger 107 */ 108 public JwtCookieSession(final Exchange exchange, 109 final KeyPair pair, 110 final String cookieName, 111 final Logger logger) { 112 super(new LinkedHashMap<String, Object>()); 113 this.exchange = exchange; 114 this.pair = pair; 115 this.cookieName = cookieName; 116 this.logger = logger; 117 118 // TODO Make this lazy (intercept read methods) 119 loadJwtSession(); 120 } 121 122 /** 123 * Load the session's content from the cookie (if any). 124 */ 125 private void loadJwtSession() { 126 Cookie cookie = findJwtSessionCookie(); 127 if (cookie != null) { 128 try { 129 EncryptedJwt jwt = reader.reconstructJwt(cookie.getValue(), EncryptedJwt.class); 130 jwt.decrypt(pair.getPrivate()); 131 JwtClaimsSet claimsSet = jwt.getClaimsSet(); 132 for (String key : claimsSet.keys()) { 133 // directly use super to avoid session be marked as dirty 134 super.put(key, claimsSet.getClaim(key)); 135 } 136 } catch (JweDecryptionException e) { 137 dirty = true; // Force cookie expiration / overwrite. 138 logger.warning(format("The JWT Session Cookie '%s' could not be decrypted. This " 139 + "may be because temporary encryption keys have been used or if the " 140 + "configured encryption keys have changed since the JWT Session Cookie " 141 + "was created", cookieName)); 142 logger.debug(e); 143 } catch (Exception e) { 144 dirty = true; // Force cookie expiration / overwrite. 145 logger.warning(format("Cannot rebuild JWT Session from Cookie '%s'", cookieName)); 146 logger.debug(e); 147 } 148 } 149 } 150 151 @Override 152 public void onElementsRemoved() { 153 dirty = true; 154 } 155 156 @Override 157 public Object put(final String key, final Object value) { 158 // Put null into a key, results in the complete entry removal 159 if (value == null) { 160 return remove(key); 161 } 162 // Verify that the given value is JSON compatible 163 // This will throw an Exception if not 164 checkJsonCompatibility(key, value); 165 166 // Mark the session as dirty 167 dirty = true; 168 169 // Store the value 170 return super.put(key, value); 171 } 172 173 @Override 174 public void putAll(final Map<? extends String, ?> m) { 175 for (Entry<? extends String, ?> entry : m.entrySet()) { 176 put(entry.getKey(), entry.getValue()); 177 } 178 } 179 180 @Override 181 public Object remove(final Object key) { 182 dirty = true; 183 return super.remove(key); 184 } 185 186 @Override 187 public void clear() { 188 dirty = true; 189 super.clear(); 190 } 191 192 @Override 193 public Set<String> keySet() { 194 return new DirtySet<String>(super.keySet(), this); 195 } 196 197 @Override 198 public Collection<Object> values() { 199 return new DirtyCollection<Object>(super.values(), this); 200 } 201 202 @Override 203 public Set<Entry<String, Object>> entrySet() { 204 return new DirtySet<Entry<String, Object>>(super.entrySet(), this); 205 } 206 207 @Override 208 public void close() throws IOException { 209 // Only build the JWT session if the session is dirty 210 if (dirty) { 211 // Update the Set-Cookie header 212 final String value; 213 if (isEmpty()) { 214 value = buildExpiredJwtCookie(); 215 } else { 216 value = buildJwtCookie(); 217 if (value.length() > 4096) { 218 throw new IOException( 219 format("JWT session is too large (%d chars), failing the request because " 220 + "session does not support serialized content that is larger than 4KB " 221 + "(Http Cookie limitation)", value.length())); 222 } 223 if (value.length() > 3072) { 224 logger.warning(format( 225 "Current JWT session's size (%d chars) is quite close to the 4KB limit. Maybe " 226 + "consider using the traditional Http-based session (the default), or place" 227 + "less objects in the session", value.length())); 228 } 229 } 230 exchange.response.getHeaders().add("Set-Cookie", value); 231 } 232 233 } 234 235 private String buildExpiredJwtCookie() { 236 return format("%s=; Path=/; Max-Age=-1", cookieName); 237 } 238 239 private String buildJwtCookie() { 240 return format("%s=%s; Path=%s", cookieName, buildJwtSession(), "/"); 241 } 242 243 /** 244 * Builds a JWT from the session's content. 245 */ 246 private String buildJwtSession() { 247 EncryptedJwtBuilder jwtBuilder = factory.jwe(pair.getPublic()); 248 JwtClaimsSetBuilder claimsBuilder = factory.claims(); 249 claimsBuilder.claims(this); 250 jwtBuilder.claims(claimsBuilder.build()); 251 jwtBuilder.headers() 252 .alg(JweAlgorithm.RSAES_PKCS1_V1_5) 253 .enc(EncryptionMethod.A128CBC_HS256); 254 return jwtBuilder.build(); 255 } 256 257 /** 258 * Find if there is an existing cookie storing a JWT session. 259 * 260 * @return a {@link Cookie} if found, {@literal null} otherwise. 261 */ 262 private Cookie findJwtSessionCookie() { 263 List<Cookie> cookies = exchange.request.getCookies().get(cookieName); 264 if (cookies != null) { 265 return cookies.get(0); 266 } 267 return null; 268 } 269}