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.jwt.JwtCookieSession.*;
021import static org.forgerock.openig.util.Json.*;
022
023import java.security.GeneralSecurityException;
024import java.security.Key;
025import java.security.KeyPair;
026import java.security.KeyPairGenerator;
027import java.security.KeyStore;
028import java.security.NoSuchAlgorithmException;
029import java.security.PrivateKey;
030import java.security.PublicKey;
031import java.security.SecureRandom;
032import java.security.cert.Certificate;
033
034import org.forgerock.json.fluent.JsonValue;
035import org.forgerock.openig.heap.GenericHeapObject;
036import org.forgerock.openig.heap.GenericHeaplet;
037import org.forgerock.openig.heap.HeapException;
038import org.forgerock.openig.http.Exchange;
039import org.forgerock.openig.http.Session;
040import org.forgerock.openig.http.SessionFactory;
041
042/**
043 * A JwtSessionFactory is responsible to configure and create a {@link JwtCookieSession}.
044 *
045 * <pre>
046 *     {
047 *         "name": "JwtSession",
048 *         "type": "JwtSession",
049 *         "config": {
050 *             "keystore": "Ref To A KeyStore",
051 *             "alias": "PrivateKey Alias",
052 *             "password": "KeyStore/Key Password",
053 *             "cookieName": "OpenIG"
054 *         }
055 *     }
056 * </pre>
057 *
058 * All the session configuration is optional: if you omit everything, the appropriate keys will be generated and the
059 * cookie name used will be {@link JwtCookieSession#OPENIG_JWT_SESSION}.
060 *
061 * <p>
062 * The {@literal keystore} attribute is an optional attribute that references a {@link KeyStore} heap object. It will
063 * be used to obtain the required encryption keys. If omitted, the {@literal alias} and {@literal password}
064 * attributes will also be ignored, and a temporary key pair will be generated.
065 * <p>
066 * The {@literal alias} string attribute specifies the name of the private key to obtain from the KeyStore. It is
067 * only required when a {@literal keystore} is specified.
068 * <p>
069 * The {@literal password} static expression attribute specifies the password to use when reading the
070 * private key from the KeyStore. It is only required when a {@literal keystore} is specified.
071 * <p>
072 * The {@literal cookieName} optional string attribute specifies the name of the cookie used to store the encrypted JWT.
073 * If not set, {@link JwtCookieSession#OPENIG_JWT_SESSION} is used.
074 *
075 * @since 3.1
076 */
077public class JwtSessionFactory extends GenericHeapObject implements SessionFactory {
078
079    /**
080     * The pair of keys for JWT payload encryption/decryption.
081     */
082    private final KeyPair keyPair;
083
084    /**
085     * The name of the cookie to be used to session's content transmission.
086     */
087    private final String cookieName;
088
089    /**
090     * Builds a new JwtSessionFactory using the given KeyPair for session encryption, storing the opaque result in a
091     * cookie with the given name.
092     *
093     * @param keyPair
094     *         Private and public keys used for ciphering/deciphering
095     * @param cookieName
096     *         name of the cookie
097     */
098    public JwtSessionFactory(final KeyPair keyPair, final String cookieName) {
099        this.keyPair = keyPair;
100        this.cookieName = cookieName;
101    }
102
103    @Override
104    public Session build(final Exchange exchange) {
105        return new JwtCookieSession(exchange, keyPair, cookieName, logger);
106    }
107
108    /** Creates and initializes a jwt-session in a heap environment. */
109    public static class Heaplet extends GenericHeaplet {
110
111        /** RSA needs at least a 512 key length.*/
112        private static final int KEY_SIZE = 1024;
113
114        @Override
115        public Object create() throws HeapException {
116            KeyPair keyPair = null;
117            JsonValue keystoreValue = config.get("keystore");
118            if (!keystoreValue.isNull()) {
119                KeyStore keyStore = heap.resolve(keystoreValue, KeyStore.class);
120
121                String alias = config.get("alias").required().asString();
122                String password = evaluate(config.get("password").required());
123
124                try {
125                    Key key = keyStore.getKey(alias, password.toCharArray());
126                    if (key instanceof PrivateKey) {
127                        // Get certificate of private key
128                        Certificate cert = keyStore.getCertificate(alias);
129                        if (cert == null) {
130                            throw new HeapException(format("Cannot get Certificate[alias:%s] from KeyStore[ref:%s]",
131                                                           alias,
132                                                           keystoreValue.asString()));
133                        }
134
135                        // Get public key
136                        PublicKey publicKey = cert.getPublicKey();
137
138                        // Return a key pair
139                        keyPair = new KeyPair(publicKey, (PrivateKey) key);
140                    } else {
141                        throw new HeapException(format("Either no Key[alias:%s] is available in KeyStore[ref:%s], "
142                                                       + "or it is not a private key",
143                                                       alias,
144                                                       keystoreValue.asString()));
145                    }
146                } catch (GeneralSecurityException e) {
147                    throw new HeapException(format("Wrong password for Key[alias:%s] in KeyStore[ref:%s]",
148                                                   alias,
149                                                   keystoreValue.asString()),
150                                            e);
151                }
152            } else {
153                /*
154                 * No KeyStore provided: generate a new KeyPair by ourself. In
155                 * this case, 'alias' and 'password' attributes are ignored. JWT
156                 * session cookies will not be portable between OpenIG instances
157                 * config changes, and restarts.
158                 */
159                try {
160                    KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA");
161                    generator.initialize(KEY_SIZE, new SecureRandom());
162                    keyPair = generator.generateKeyPair();
163                } catch (NoSuchAlgorithmException e) {
164                    throw new HeapException("Cannot build a random KeyPair", e);
165                }
166
167                logger.warning("JWT session support has been enabled but no encryption keys have "
168                        + "been configured. A temporary key pair will be used but this means that "
169                        + "OpenIG will not be able to decrypt any JWT session cookies after a "
170                        + "configuration change, a server restart, nor will it be able to decrypt "
171                        + "JWT session cookies encrypted by another OpenIG server.");
172            }
173
174            // Create the session factory with the given KeyPair and cookie name
175            return new JwtSessionFactory(keyPair,
176                                         config.get("cookieName").defaultTo(OPENIG_JWT_SESSION).asString());
177        }
178    }
179}