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}