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}