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-2015 ForgeRock AS. 015 */ 016 017package org.forgerock.openig.jwt; 018 019import static java.lang.String.*; 020import static org.forgerock.openig.heap.Keys.TIME_SERVICE_HEAP_KEY; 021import static org.forgerock.openig.jwt.JwtCookieSession.*; 022import static org.forgerock.openig.util.JsonValues.*; 023import static org.forgerock.util.time.Duration.duration; 024 025import java.io.IOException; 026import java.security.GeneralSecurityException; 027import java.security.Key; 028import java.security.KeyPair; 029import java.security.KeyPairGenerator; 030import java.security.KeyStore; 031import java.security.NoSuchAlgorithmException; 032import java.security.PrivateKey; 033import java.security.PublicKey; 034import java.security.SecureRandom; 035import java.security.cert.Certificate; 036 037import org.forgerock.http.protocol.Request; 038import org.forgerock.http.protocol.Response; 039import org.forgerock.http.session.Session; 040import org.forgerock.http.session.SessionManager; 041import org.forgerock.json.JsonValue; 042import org.forgerock.openig.heap.GenericHeapObject; 043import org.forgerock.openig.heap.GenericHeaplet; 044import org.forgerock.openig.heap.HeapException; 045import org.forgerock.util.time.Duration; 046import org.forgerock.util.time.TimeService; 047 048/** 049 * A JwtSessionManager is responsible to configure and create a {@link JwtCookieSession}. 050 * 051 * <pre> 052 * {@code 053 * { 054 * "name": "JwtSession", 055 * "type": "JwtSession", 056 * "config": { 057 * "keystore": "Ref To A KeyStore", 058 * "alias": "PrivateKey Alias", 059 * "password": "KeyStore/Key Password", 060 * "cookieName": "OpenIG", 061 * "sessionTimeout": "30 minutes" 062 * } 063 * } 064 * } 065 * </pre> 066 * 067 * All the session configuration is optional: if you omit everything, the appropriate keys will be generated and the 068 * cookie name used will be {@link JwtCookieSession#OPENIG_JWT_SESSION}. 069 * 070 * <p> 071 * The {@literal keystore} attribute is an optional attribute that references a {@link KeyStore} heap object. It will 072 * be used to obtain the required encryption keys. If omitted, the {@literal alias} and {@literal password} 073 * attributes will also be ignored, and a temporary key pair will be generated. 074 * <p> 075 * The {@literal alias} string attribute specifies the name of the private key to obtain from the KeyStore. It is 076 * only required when a {@literal keystore} is specified. 077 * <p> 078 * The {@literal password} static expression attribute specifies the password to use when reading the 079 * private key from the KeyStore. It is only required when a {@literal keystore} is specified. 080 * <p> 081 * The {@literal cookieName} optional string attribute specifies the name of the cookie used to store the encrypted JWT. 082 * If not set, {@link JwtCookieSession#OPENIG_JWT_SESSION} is used. 083 * <p> 084 * The {@literal sessionTimeout} optional duration attribute, specifies the amount of time before the cookie session 085 * expires. If not set, a default of 30 minutes is used. A duration of 0 is not valid and it will be limited to 086 * a maximum duration of approximately 10 years. 087 * 088 * @since 3.1 089 */ 090public class JwtSessionManager extends GenericHeapObject implements SessionManager { 091 092 /** 093 * Default sessionTimeout duration. 094 */ 095 public static final String DEFAULT_SESSION_TIMEOUT = "30 minutes"; 096 097 /** 098 * The maximum session timeout duration, allows for an expiry time of approx 10 years (does not take leap years 099 * into consideration). 100 */ 101 public static final Duration MAX_SESSION_TIMEOUT = Duration.duration("3650 days"); 102 103 /** 104 * The pair of keys for JWT payload encryption/decryption. 105 */ 106 private final KeyPair keyPair; 107 108 /** 109 * The name of the cookie to be used to session's content transmission. 110 */ 111 private final String cookieName; 112 113 /** 114 * The TimeService to use when setting the cookie session expiry time. 115 */ 116 private final TimeService timeService; 117 118 /** 119 * How long before the cookie session expires. 120 */ 121 private final Duration sessionTimeout; 122 123 /** 124 * Builds a new JwtSessionManager using the given KeyPair for session encryption, storing the opaque result in a 125 * cookie with the given name. 126 * 127 * @param keyPair 128 * Private and public keys used for ciphering/deciphering 129 * @param cookieName 130 * name of the cookie 131 * @param timeService 132 * TimeService to use when dealing with cookie sessions 133 * @param sessionTimeout 134 * The duration of the cookie session 135 */ 136 public JwtSessionManager(final KeyPair keyPair, 137 final String cookieName, 138 final TimeService timeService, 139 final Duration sessionTimeout) { 140 this.keyPair = keyPair; 141 this.cookieName = cookieName; 142 this.timeService = timeService; 143 this.sessionTimeout = sessionTimeout; 144 } 145 146 @Override 147 public Session load(final Request request) { 148 return new JwtCookieSession(request, keyPair, cookieName, logger, timeService, sessionTimeout); 149 } 150 151 @Override 152 public void save(Session session, Response response) throws IOException { 153 if (response != null) { 154 session.save(response); 155 } 156 } 157 158 /** Creates and initializes a jwt-session in a heap environment. */ 159 public static class Heaplet extends GenericHeaplet { 160 161 /** RSA needs at least a 512 key length.*/ 162 private static final int KEY_SIZE = 1024; 163 164 @Override 165 public Object create() throws HeapException { 166 KeyPair keyPair = null; 167 JsonValue keystoreValue = config.get("keystore"); 168 if (!keystoreValue.isNull()) { 169 KeyStore keyStore = heap.resolve(keystoreValue, KeyStore.class); 170 171 String alias = config.get("alias").required().asString(); 172 String password = evaluate(config.get("password").required()); 173 174 try { 175 Key key = keyStore.getKey(alias, password.toCharArray()); 176 if (key instanceof PrivateKey) { 177 // Get certificate of private key 178 Certificate cert = keyStore.getCertificate(alias); 179 if (cert == null) { 180 throw new HeapException(format("Cannot get Certificate[alias:%s] from KeyStore[ref:%s]", 181 alias, 182 keystoreValue.asString())); 183 } 184 185 // Get public key 186 PublicKey publicKey = cert.getPublicKey(); 187 188 // Return a key pair 189 keyPair = new KeyPair(publicKey, (PrivateKey) key); 190 } else { 191 throw new HeapException(format("Either no Key[alias:%s] is available in KeyStore[ref:%s], " 192 + "or it is not a private key", 193 alias, 194 keystoreValue.asString())); 195 } 196 } catch (GeneralSecurityException e) { 197 throw new HeapException(format("Wrong password for Key[alias:%s] in KeyStore[ref:%s]", 198 alias, 199 keystoreValue.asString()), 200 e); 201 } 202 } else { 203 /* 204 * No KeyStore provided: generate a new KeyPair by ourself. In 205 * this case, 'alias' and 'password' attributes are ignored. JWT 206 * session cookies will not be portable between OpenIG instances 207 * config changes, and restarts. 208 */ 209 try { 210 KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA"); 211 generator.initialize(KEY_SIZE, new SecureRandom()); 212 keyPair = generator.generateKeyPair(); 213 } catch (NoSuchAlgorithmException e) { 214 throw new HeapException("Cannot build a random KeyPair", e); 215 } 216 217 logger.warning("JWT session support has been enabled but no encryption keys have " 218 + "been configured. A temporary key pair will be used but this means that " 219 + "OpenIG will not be able to decrypt any JWT session cookies after a " 220 + "configuration change, a server restart, nor will it be able to decrypt " 221 + "JWT session cookies encrypted by another OpenIG server."); 222 } 223 224 TimeService timeService = heap.get(TIME_SERVICE_HEAP_KEY, TimeService.class); 225 226 final Duration sessionTimeout = 227 duration(config.get("sessionTimeout").defaultTo(DEFAULT_SESSION_TIMEOUT).asString()); 228 if (sessionTimeout.isZero()) { 229 throw new HeapException("sessionTimeout duration must be greater than 0"); 230 } 231 232 // Create the session factory with the given KeyPair and cookie name 233 return new JwtSessionManager(keyPair, 234 config.get("cookieName").defaultTo(OPENIG_JWT_SESSION).asString(), 235 timeService, 236 sessionTimeout); 237 } 238 } 239}