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-2016 ForgeRock AS.
015*/
016
017package org.forgerock.jaspi.modules.openid.resolvers;
018
019import static org.forgerock.caf.authentication.framework.AuthenticationFramework.LOG;
020
021import java.net.URL;
022import java.security.Key;
023import java.util.HashMap;
024import java.util.Map;
025
026import org.forgerock.jaspi.modules.openid.exceptions.FailedToLoadJWKException;
027import org.forgerock.jaspi.modules.openid.exceptions.InvalidSignatureException;
028import org.forgerock.jaspi.modules.openid.exceptions.OpenIdConnectVerificationException;
029import org.forgerock.jaspi.modules.openid.helpers.JWKSetParser;
030import org.forgerock.jaspi.modules.openid.helpers.SimpleHTTPClient;
031import org.forgerock.json.jose.jws.SignedJwt;
032import org.forgerock.json.jose.jws.SigningManager;
033
034/**
035 * This class exists to allow Open Id Providers to supply or promote a JWK exposure point for
036 * their public keys. We convert the exposed keys they provide according to the algorithm
037 * defined by their JWK and offer their keys in a map key'd on their keyId.
038 *
039 * The map of keys is loaded on construction, and reloaded each time an Open Id token is
040 * passed in to this resolver whose keyId does not exist within the list that we currently have.
041 *
042 * This means that we will cache the keys for as long as they are valid, and as soon as we
043 * receive a request to verify using a key which we don't have we discard our current keys and
044 * re-fill our map.
045 */
046public class JWKOpenIdResolverImpl extends BaseOpenIdResolver {
047
048    private final SigningManager signingManager;
049
050    private final URL jwkUrl;
051
052    private final Map<String, Key> keyMap = new HashMap<>();
053
054    private final JWKSetParser jwkParser;
055
056    /**
057     * Constructor using provided timeout values to generate the
058     * {@link SimpleHTTPClient} used for communicating over HTTP.
059     *
060     * @param issuer The issuer (provider) of the Open Id Connect id token
061     * @param jwkUrl the URL from which we will attempt to read and parse our JWKSet
062     * @param readTimeout the read timeout associated with HTTP requests
063     * @param connTimeout the connection timeout associated with HTTP requests
064     * @throws FailedToLoadJWKException if there were issues resolving or parsing the JWK
065     */
066    public JWKOpenIdResolverImpl(final String issuer, final URL jwkUrl, final int readTimeout,
067                                 final int connTimeout) throws FailedToLoadJWKException {
068        super(issuer);
069
070        this.signingManager = new SigningManager();
071        jwkParser = new JWKSetParser(readTimeout, connTimeout);
072        this.jwkUrl = jwkUrl;
073
074        try {
075            reloadKeys();
076        } catch (FailedToLoadJWKException e) {
077            LOG.debug("Unable to load keys from the JWK over HTTP");
078            throw new FailedToLoadJWKException("Unable to load keys from the JWK over HTTP", e);
079        }
080    }
081
082    /**
083     * Constructor using an already-created {@link SimpleHTTPClient}.
084     *
085     * @param issuer The issuer (provider) of the Open Id Connect id token
086     * @param jwkUrl The URL from which we will attempt to read and parse our JWKSet
087     * @param httpClient The http client through which we will attempt to read the jwkUrl
088     * @throws FailedToLoadJWKException if there were issues resolving or parsing the JWK.
089     */
090    public JWKOpenIdResolverImpl(final String issuer, final URL jwkUrl, final SimpleHTTPClient httpClient)
091            throws FailedToLoadJWKException {
092        super(issuer);
093
094        this.signingManager = new SigningManager();
095        jwkParser = new JWKSetParser(httpClient);
096        this.jwkUrl = jwkUrl;
097
098        try {
099            reloadKeys();
100        } catch (FailedToLoadJWKException e) {
101            LOG.debug("Unable to load keys from the JWK over HTTP");
102            throw new FailedToLoadJWKException("Unable to load keys from the JWK over HTTP", e);
103        }
104    }
105
106
107    /**
108     * Test constructor using an already-created JwkParser.
109     *
110     * @param issuer The issuer (provider) of the Open Id Connect id token
111     * @param jwkUrl The URL from which we will attempt to read and parse our JWKSet
112     */
113    JWKOpenIdResolverImpl(final String issuer, final URL jwkUrl, final JWKSetParser jwkParser)
114            throws FailedToLoadJWKException {
115        super(issuer);
116
117        this.signingManager = new SigningManager();
118        this.jwkParser = jwkParser;
119        this.jwkUrl = jwkUrl;
120
121        try {
122            reloadKeys();
123        } catch (FailedToLoadJWKException e) {
124            LOG.debug("Unable to load keys from the JWK over HTTP");
125            throw new FailedToLoadJWKException("Unable to load keys from the JWK over HTTP", e);
126        }
127    }
128
129    /**
130     * {@inheritDoc}
131     */
132    @Override
133    public void validateIdentity(final SignedJwt idClaim) throws OpenIdConnectVerificationException {
134        super.validateIdentity(idClaim);
135        verifySignature(idClaim);
136    }
137
138    /**
139     * Verifies that the JWS was signed by the supplied key. Throws an exception otherwise.
140     *
141     * @param idClaim The JWS to verify
142     * @throws InvalidSignatureException If the JWS supplied does not match the key for this resolver
143     * @throws FailedToLoadJWKException If the JWK supplied cannot be loaded from its remote location
144     */
145    public void verifySignature(final SignedJwt idClaim) throws InvalidSignatureException,
146            FailedToLoadJWKException {
147
148        final Key key;
149
150        synchronized (keyMap) {
151            if (!keyMap.containsKey(idClaim.getHeader().getKeyId())) {
152                reloadKeys();
153            }
154        }
155
156        key = keyMap.get(idClaim.getHeader().getKeyId());
157        if (key == null || !idClaim.verify(createSigningHandlerForKey(signingManager, key))) {
158            LOG.debug("JWS unable to be verified");
159            throw new InvalidSignatureException("JWS unable to be verified");
160        }
161    }
162
163    /**
164     * Communicates with the configured server, attempting to download the latest keyset
165     * for use.
166     *
167     * @throws FailedToLoadJWKException if there were issues parsing the supplied URL
168     */
169    private void reloadKeys() throws FailedToLoadJWKException {
170        synchronized (keyMap) {
171            keyMap.clear();
172            keyMap.putAll(jwkParser.generateMapFromJWK(jwkUrl));
173        }
174    }
175
176}