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}