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.jaspi.modules.openid; 018 019import static javax.security.auth.message.AuthStatus.SEND_FAILURE; 020import static javax.security.auth.message.AuthStatus.SEND_SUCCESS; 021import static org.forgerock.caf.authentication.framework.AuthenticationFramework.LOG; 022import static org.forgerock.util.promise.Promises.newExceptionPromise; 023import static org.forgerock.util.promise.Promises.newResultPromise; 024 025import javax.security.auth.Subject; 026import javax.security.auth.callback.Callback; 027import javax.security.auth.callback.CallbackHandler; 028import javax.security.auth.callback.UnsupportedCallbackException; 029import javax.security.auth.message.AuthException; 030import javax.security.auth.message.AuthStatus; 031import javax.security.auth.message.MessagePolicy; 032import javax.security.auth.message.callback.CallerPrincipalCallback; 033import java.io.IOException; 034import java.util.Arrays; 035import java.util.Collection; 036import java.util.List; 037import java.util.Map; 038 039import org.forgerock.caf.authentication.api.AsyncServerAuthModule; 040import org.forgerock.caf.authentication.api.AuthenticationException; 041import org.forgerock.caf.authentication.api.MessageInfoContext; 042import org.forgerock.http.protocol.Request; 043import org.forgerock.http.protocol.Response; 044import org.forgerock.jaspi.modules.openid.exceptions.OpenIdConnectVerificationException; 045import org.forgerock.jaspi.modules.openid.resolvers.OpenIdResolver; 046import org.forgerock.jaspi.modules.openid.resolvers.service.OpenIdResolverService; 047import org.forgerock.jaspi.modules.openid.resolvers.service.OpenIdResolverServiceConfigurator; 048import org.forgerock.jaspi.modules.openid.resolvers.service.OpenIdResolverServiceConfiguratorImpl; 049import org.forgerock.jaspi.modules.openid.resolvers.service.OpenIdResolverServiceImpl; 050import org.forgerock.json.jose.common.JwtReconstruction; 051import org.forgerock.json.jose.exceptions.InvalidJwtException; 052import org.forgerock.json.jose.exceptions.JwtReconstructionException; 053import org.forgerock.json.jose.jws.SignedJwt; 054import org.forgerock.json.jose.jwt.JwtClaimsSet; 055import org.forgerock.util.promise.Promise; 056 057/** 058 * OpenID Connect module that allows access when a valid OpenID Connect JWT which 059 * our server trusts is presented in the specific header field. 060 */ 061public class OpenIdConnectModule implements AsyncServerAuthModule { 062 063 /** 064 * Default read timeout for HTTP connections. 065 */ 066 private static final int DEFAULT_READ_TIMEOUT = 5_000; 067 068 /** 069 * Default connection timeout for HTTP connections. 070 */ 071 private static final int DEFAULT_CONN_TIMEOUT = 5_000; 072 073 /** 074 * Lookup key for the configured HTTP connection's read timeout for this module. 075 */ 076 public static final String READ_TIMEOUT_KEY = "readTimeout"; 077 078 /** 079 * Lookup key for the configured HTTP connection's connection timeout for this module. 080 */ 081 public static final String CONNECTION_TIMEOUT_KEY = "connectionTimeout"; 082 083 /** 084 * Lookup key for the configured HTTP header used by this module to locate JWSs. 085 */ 086 public static final String HEADER_KEY = "openIdConnectHeader"; 087 088 /** 089 * Lookup key for the configured resolvers which will be used by this module. 090 */ 091 public static final String RESOLVERS_KEY = "resolvers"; 092 093 private final JwtReconstruction constructor; 094 private final OpenIdResolverServiceConfigurator serviceConfigurator; 095 096 private String openIdConnectHeader; 097 098 private OpenIdResolverService resolverService; 099 100 private CallbackHandler callbackHandler; 101 102 /** 103 * Default constructor. 104 */ 105 public OpenIdConnectModule() { 106 constructor = new JwtReconstruction(); 107 serviceConfigurator = new OpenIdResolverServiceConfiguratorImpl(); 108 } 109 110 /** 111 * Used for tests. 112 * 113 * @param serviceConfigurator Configurator device for setting up our resolver service 114 * @param constructor Builder for creating our JWTs out of their representation 115 */ 116 OpenIdConnectModule(final OpenIdResolverServiceConfigurator serviceConfigurator, 117 final JwtReconstruction constructor, 118 final OpenIdResolverService service, 119 final CallbackHandler callback, 120 final String openIdConnectHeader) { 121 this.serviceConfigurator = serviceConfigurator; 122 this.constructor = constructor; 123 this.resolverService = service; 124 this.callbackHandler = callback; 125 this.openIdConnectHeader = openIdConnectHeader; 126 } 127 128 @Override 129 public String getModuleId() { 130 return "OpenIdConnect"; 131 } 132 133 /** 134 * {@inheritDoc} 135 */ 136 @Override 137 public Promise<Void, AuthenticationException> initialize(MessagePolicy requestPolicy, MessagePolicy responsePolicy, 138 CallbackHandler callbackHandler, Map<String, Object> config) { 139 140 this.openIdConnectHeader = (String) config.get(OpenIdConnectModule.HEADER_KEY); 141 this.callbackHandler = callbackHandler; 142 143 Integer readTimeout = (Integer) config.get(OpenIdConnectModule.READ_TIMEOUT_KEY); 144 Integer connTimeout = (Integer) config.get(OpenIdConnectModule.CONNECTION_TIMEOUT_KEY); 145 146 if (openIdConnectHeader == null || openIdConnectHeader.isEmpty()) { 147 LOG.debug("OpenIdConnectModule config is invalid. You must include the header key parameter"); 148 return newExceptionPromise(new AuthenticationException("OpenIdConnectModule configuration is invalid.")); 149 } 150 151 if (readTimeout == null || readTimeout < 0) { 152 LOG.debug("Read Timeout setting invalid, set to default: {}", DEFAULT_READ_TIMEOUT); 153 readTimeout = DEFAULT_READ_TIMEOUT; 154 } 155 156 if (connTimeout == null || connTimeout < 0) { 157 LOG.debug("Connection Timeout setting invalid, set to default: {}", DEFAULT_CONN_TIMEOUT); 158 connTimeout = DEFAULT_CONN_TIMEOUT; 159 } 160 161 final List<Map<String, String>> resolvers = 162 (List<Map<String, String>>) config.get(OpenIdConnectModule.RESOLVERS_KEY); 163 164 resolverService = new OpenIdResolverServiceImpl(readTimeout, connTimeout); 165 166 //if we weren't able to set up the service, or any one of the supplied resolver configs was invalid, 167 //error out here 168 if (!serviceConfigurator.configureService(resolverService, resolvers)) { 169 LOG.debug("OpenIdConnectModule config is invalid. You must configure at least one valid resolver."); 170 return newExceptionPromise(new AuthenticationException("OpenIdConnectModule configuration is invalid.")); 171 } 172 173 return newResultPromise(null); 174 } 175 176 /** 177 * Attempts to retrieve the value of the specified OpenID Connect header from the messageInfo, then 178 * converts this to a Jwt and attempts to decrypt. If both these steps succeed, we verify the Jwt 179 * through the {@link org.forgerock.jaspi.modules.openid.resolvers.OpenIdResolver} interface 180 * to ensure that we are the intended audience, the token has not expired and the issuer was an expected source. 181 * 182 * If all of these validate, we return SUCCESS, otherwise SEND_FAILURE. 183 * 184 * @param messageInfo {@inheritDoc} 185 * @param clientSubject {@inheritDoc} 186 * @param serviceSubject {@inheritDoc} 187 * @return A Promise completed with AuthStatus.SUCCESS if everything validates or with AuthStatus.SEND_FAILURE 188 * in the case of a failure, or completed with an exception if there are issues handling the request caused 189 * by improper config. 190 */ 191 @Override 192 public Promise<AuthStatus, AuthenticationException> validateRequest(MessageInfoContext messageInfo, 193 Subject clientSubject, Subject serviceSubject) { 194 195 final Request request = messageInfo.getRequest(); 196 final String jwtValue = request.getHeaders().getFirst(openIdConnectHeader); 197 198 if (jwtValue == null || jwtValue.isEmpty()) { 199 return newResultPromise(SEND_FAILURE); 200 } 201 202 final SignedJwt retrievedJwt; 203 204 try { 205 retrievedJwt = constructor.reconstructJwt(jwtValue, SignedJwt.class); 206 } catch (InvalidJwtException ije) { 207 LOG.debug("Invalid JWS in supplied header", ije); 208 return newResultPromise(SEND_FAILURE); 209 } catch (JwtReconstructionException jre) { 210 LOG.debug("Unable to reconstruct JWS from supplied header", jre); 211 return newResultPromise(SEND_FAILURE); 212 } 213 214 final JwtClaimsSet jwtClaimSet = retrievedJwt.getClaimsSet(); 215 216 OpenIdResolver resolver = resolverService.getResolverForIssuer(jwtClaimSet.getIssuer()); 217 218 //if no resolver for this issuer found, abort 219 if (resolver == null) { 220 LOG.debug("No resolver found for the issuer: {}", jwtClaimSet.getIssuer()); 221 return newResultPromise(SEND_FAILURE); 222 } 223 224 try { 225 resolver.validateIdentity(retrievedJwt); 226 227 callbackHandler.handle(new Callback[]{ 228 new CallerPrincipalCallback(clientSubject, jwtClaimSet.getSubject()) 229 }); 230 231 } catch (OpenIdConnectVerificationException oice) { 232 LOG.debug("Unable to validate authenticated identity from JWT.", oice); 233 return newResultPromise(SEND_FAILURE); 234 } catch (IOException | UnsupportedCallbackException e) { 235 LOG.debug("Error setting user principal", e); 236 return newExceptionPromise(new AuthenticationException(e.getMessage())); 237 } 238 239 return newResultPromise(AuthStatus.SUCCESS); 240 } 241 242 /** 243 * Sends SEND_SUCCESS automatically. As we're on our way out of the system at this point, there's 244 * no need to hold them up, or append anything new to the response. 245 * 246 * @param messageInfo {@inheritDoc} 247 * @param subject {@inheritDoc} 248 * @return {@inheritDoc} 249 * @throws AuthException {@inheritDoc} 250 */ 251 @Override 252 public Promise<AuthStatus, AuthenticationException> secureResponse(MessageInfoContext messageInfo, 253 Subject subject) { 254 return newResultPromise(SEND_SUCCESS); 255 } 256 257 /** 258 * Nothing to clean. 259 * 260 * @param messageInfo {@inheritDoc} 261 * @param subject {@inheritDoc} 262 * @throws AuthException {@inheritDoc} 263 */ 264 @Override 265 public Promise<Void, AuthenticationException> cleanSubject(MessageInfoContext messageInfo, Subject subject) { 266 return newResultPromise(null); 267 } 268 269 /** 270 * {@inheritDoc} 271 */ 272 @Override 273 public Collection<Class<?>> getSupportedMessageTypes() { 274 return Arrays.asList(new Class<?>[]{Request.class, Response.class}); 275 } 276}