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 2015 ForgeRock AS.
015 */
016package org.forgerock.openig.filter.oauth2.client;
017
018import static java.lang.String.format;
019import static org.forgerock.http.protocol.Status.CREATED;
020import static org.forgerock.json.JsonValue.field;
021import static org.forgerock.json.JsonValue.json;
022import static org.forgerock.json.JsonValue.object;
023import static org.forgerock.openig.filter.oauth2.client.ClientRegistration.CLIENT_REG_KEY;
024import static org.forgerock.openig.filter.oauth2.client.Issuer.ISSUER_KEY;
025import static org.forgerock.openig.filter.oauth2.client.OAuth2Utils.getJsonContent;
026import static org.forgerock.openig.http.Responses.blockingCall;
027import static org.forgerock.openig.http.Responses.newInternalServerError;
028import static org.forgerock.util.Reject.checkNotNull;
029import static org.forgerock.util.promise.Promises.newResultPromise;
030
031import java.net.URI;
032
033import org.forgerock.http.Filter;
034import org.forgerock.http.Handler;
035import org.forgerock.http.protocol.Request;
036import org.forgerock.http.protocol.Response;
037import org.forgerock.json.JsonValue;
038import org.forgerock.openig.heap.Heap;
039import org.forgerock.openig.heap.HeapException;
040import org.forgerock.services.context.AttributesContext;
041import org.forgerock.services.context.Context;
042import org.forgerock.util.promise.NeverThrowsException;
043import org.forgerock.util.promise.Promise;
044
045/**
046 * The client registration filter is the way to dynamically register an OpenID
047 * Connect Relying Party with the End-User's OpenID Provider.
048 * <p>
049 * All OpenID metadata must be included in the <b>{@link OAuth2ClientFilter}</b> configuration,
050 * in the <b>"metadata" attribute</b>. Note that for dynamic client registration,
051 * only the "redirect_uris" attribute is mandatory.
052 * </p>
053 *
054 * Note: When using OpenAM, the "scopes" may be specified to this configuration but
055 * it must be defined as: "scopes"(array of string), which differs from
056 * the OAuth2 metadata "scope" (a string containing a space separated list of scope values).
057 *
058 * <br>
059 * Note for developers: The suffix is added to the issuer name to compose the
060 * client registration name in the current heap. When automatically called by
061 * the {@link OAuth2ClientFilter}, this name is {@literal IssuerName} + {@literal OAuth2ClientFilterName}
062 * This is required in order to retrieve the Client Registration when performing
063 * dynamic client registration.
064 *
065 * @see <a href="https://openid.net/specs/openid-connect-registration-1_0.html">
066 *      OpenID Connect Dynamic Client Registration 1.0</a>
067 * @see <a
068 *      href="https://openid.net/specs/openid-connect-registration-1_0.html#ClientMetadata">
069 *      OpenID Connect Dynamic Client Registration 1.0 </a>
070 * @see <a
071 *      href="https://tools.ietf.org/html/draft-ietf-oauth-dyn-reg-30#section-2">
072 *      OAuth 2.0 Dynamic Client Registration Protocol </a>
073 */
074public class ClientRegistrationFilter implements Filter {
075    private final Handler registrationHandler;
076    private final Heap heap;
077    private final JsonValue config;
078    private final String suffix;
079
080    /**
081     * Creates a new dynamic registration filter.
082     *
083     * @param registrationHandler
084     *            The handler to perform the dynamic registration to the AS.
085     * @param config
086     *            Can contain any client metadata attributes that the client
087     *            chooses to specify for itself during the registration. Must
088     *            contains the 'redirect_uris' attributes.
089     * @param heap
090     *            A reference to the current heap.
091     * @param suffix
092     *            The name of the client registration in the heap will be
093     *            {@literal IssuerName} + {@literal suffix}. Must not be {@code null}.
094     */
095    public ClientRegistrationFilter(final Handler registrationHandler,
096                                    final JsonValue config,
097                                    final Heap heap,
098                                    final String suffix) {
099        this.registrationHandler = registrationHandler;
100        this.config = config;
101        this.heap = heap;
102        this.suffix = checkNotNull(suffix);
103    }
104
105    @Override
106    public Promise<Response, NeverThrowsException> filter(Context context,
107                                                          Request request,
108                                                          Handler next) {
109        try {
110            AttributesContext attributesContext = context.asContext(AttributesContext.class);
111            final Issuer issuer = (Issuer) attributesContext.getAttributes().get(ISSUER_KEY);
112            if (issuer != null) {
113                ClientRegistration cr = heap.get(issuer.getName() + suffix, ClientRegistration.class);
114                if (cr == null) {
115                    if (!config.isDefined("redirect_uris")) {
116                        throw new RegistrationException(
117                                "Cannot perform dynamic registration: 'redirect_uris' should be defined");
118                    }
119                    if (issuer.getRegistrationEndpoint() == null) {
120                        throw new RegistrationException(format("Registration is not supported by the issuer '%s'",
121                                issuer));
122                    }
123                    final JsonValue registeredClientConfiguration = performDynamicClientRegistration(context, config,
124                            issuer.getRegistrationEndpoint());
125                    cr = heap.resolve(
126                            createClientRegistrationDeclaration(registeredClientConfiguration, issuer.getName()),
127                            ClientRegistration.class);
128                }
129                attributesContext.getAttributes().put(CLIENT_REG_KEY, cr);
130            } else {
131                throw new RegistrationException("Cannot retrieve issuer from the context");
132            }
133        } catch (RegistrationException e) {
134            return newResultPromise(newInternalServerError(e));
135        } catch (HeapException e) {
136            return newResultPromise(
137                    newInternalServerError("Cannot inject inlined Client Registration declaration to heap", e));
138        }
139        return next.handle(context, request);
140    }
141
142    private JsonValue createClientRegistrationDeclaration(final JsonValue configuration, final String issuerName) {
143        configuration.put("issuer", issuerName);
144        return json(object(
145                        field("name", issuerName + suffix),
146                        field("type", "ClientRegistration"),
147                        field("config", configuration)));
148    }
149
150    JsonValue performDynamicClientRegistration(final Context context,
151                                               final JsonValue clientRegistrationConfiguration,
152                                               final URI registrationEndpoint) throws RegistrationException {
153        final Request request = new Request();
154        request.setMethod("POST");
155        request.setUri(registrationEndpoint);
156        request.setEntity(clientRegistrationConfiguration.asMap());
157
158        final Response response;
159        try {
160            response = blockingCall(registrationHandler, context, request);
161        } catch (InterruptedException e) {
162            throw new RegistrationException(format("Interrupted while waiting for '%s' response", request.getUri()), e);
163        }
164        if (!CREATED.equals(response.getStatus())) {
165            throw new RegistrationException("Cannot perform dynamic registration: this can be caused "
166                                            + "by the distant server(busy, offline...) "
167                                            + "or a malformed registration response.");
168        }
169        try {
170            return getJsonContent(response);
171        } catch (OAuth2ErrorException e) {
172            throw new RegistrationException("Cannot perform dynamic registration: invalid response JSON content.");
173        }
174    }
175}