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.OK;
020import static org.forgerock.openig.filter.oauth2.client.OAuth2Utils.getJsonContent;
021import static org.forgerock.openig.heap.Keys.CLIENT_HANDLER_HEAP_KEY;
022import static org.forgerock.openig.http.Responses.blockingCall;
023import static org.forgerock.openig.util.JsonValues.evaluate;
024import static org.forgerock.openig.util.JsonValues.firstOf;
025
026import java.net.URI;
027import java.net.URISyntaxException;
028import java.util.Collections;
029import java.util.LinkedList;
030import java.util.List;
031import java.util.regex.Pattern;
032import java.util.regex.PatternSyntaxException;
033
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.json.JsonValueException;
039import org.forgerock.openig.heap.GenericHeaplet;
040import org.forgerock.openig.heap.HeapException;
041import org.forgerock.services.context.AttributesContext;
042import org.forgerock.services.context.RootContext;
043import org.forgerock.util.Reject;
044
045/**
046 * A configuration for an OpenID Connect Issuer. Two approaches to create the
047 * Issuer:
048 * <p>
049 * With an OpenId well-known end-point:
050 * </p>
051 *
052 * <pre>
053 * {@code
054 * {
055 *   "wellKnownEndpoint"            : uriExpression,   [REQUIRED]
056 *   "issuerHandler"                : handler          [OPTIONAL - by default it uses the 'ClientHandler'
057 *                                                                 provided in heap.]
058 *   "supportedDomains"             : [ patterns ]     [OPTIONAL - if this issuer supports other domain names]
059 * }
060 * }
061 * </pre>
062 *
063 * The 'supportedDomains' are the other domain names supported by this issuer,
064 * their format can include use of regular-expression patterns.
065 * Nota: Declaring these domains in the configuration should be as simple as
066 * possible, without any schemes or end slash i.e.:
067 *
068 * <pre>{@code
069 * GOOD: [ "openam.com", "openam.com:8092", "register.server.com", "allopenamdomains.*" ]
070 * BAD : [ "http://openam.com", "openam.com:8092/", "http://openam.com/" ]
071 * }
072 * </pre>
073 *
074 * <p>For example, use this kind of configuration if the end-points are not known:
075 *
076 * <pre>
077 * {@code
078 * {
079 *     "name": "openam",
080 *     "type": "Issuer",
081 *     "config": {
082 *          "wellKnownEndpoint": "http://www.example.com:8081/openam/oauth2/.well-known/openid-configuration"
083 *          "supportedDomains" : [ "openam.com", "openam.com:8092", "register.server.com" ]
084 *     }
085 * }
086 * }
087 * </pre>
088 *
089 * <br>
090 * <p>
091 * Use this configuration if the end-points are known. The well-known end-point
092 * is optional as the value will be saved but no request will be performed on
093 * this end-point.
094 * </p>
095 *
096 * <pre>
097 * {@code
098 * {
099 *   "authorizeEndpoint"            : uriExpression,   [REQUIRED]
100 *   "tokenEndpoint"                : uriExpression,   [REQUIRED]
101 *   "registrationEndpoint"         : uriExpression,   [OPTIONAL - allows dynamic client registration]
102 *   "userInfoEndpoint"             : uriExpression    [OPTIONAL - default is no user info]
103 *   "wellKnownEndpoint"            : uriExpression    [OPTIONAL]
104 *   "supportedDomains"             : [ patterns ]     [OPTIONAL - if this issuer supports other domain names]
105 * }
106 * }
107 * </pre>
108 *
109 * For example:
110 *
111 * <pre>
112 * {@code
113 * {
114 *     "name": "openam",
115 *     "type": "Issuer",
116 *     "config": {
117 *          "authorizeEndpoint": "http://www.example.com:8081/openam/oauth2/authorize",
118 *          "tokenEndpoint": "http://www.example.com:8081/openam/oauth2/access_token",
119 *          "userInfoEndpoint": "http://www.example.com:8081/openam/oauth2/userinfo"
120 *     }
121 * }
122 * }
123 * </pre>
124 */
125public final class Issuer {
126    /** The key used to store this issuer in the context. */
127    public static final String ISSUER_KEY = "issuer";
128
129    private final String name;
130    private final URI authorizeEndpoint;
131    private final URI tokenEndpoint;
132    private final URI registrationEndpoint;
133    private final URI userInfoEndpoint;
134    private URI wellKnownEndpoint;
135    private final List<Pattern> supportedDomains;
136
137    /**
138     * Creates an issuer with the specified name and configuration.
139     *
140     * @param name
141     *            The name of this Issuer. When the issuer is created by
142     *            discovery, the issuer name is given by the metadata "issuer",
143     *            not null.
144     * @param config
145     *            The configuration of this issuer, not null.
146     */
147    public Issuer(final String name, final JsonValue config) {
148        Reject.ifNull(name, config);
149        this.name = name;
150        this.authorizeEndpoint = firstOf(config, "authorizeEndpoint", "authorization_endpoint").required().asURI();
151        this.tokenEndpoint = firstOf(config, "tokenEndpoint", "token_endpoint").required().asURI();
152        this.registrationEndpoint = firstOf(config, "registrationEndpoint", "registration_endpoint").asURI();
153        this.userInfoEndpoint = firstOf(config, "userInfoEndpoint", "userinfo_endpoint").asURI();
154        this.wellKnownEndpoint = config.get("wellKnownEndpoint").asURI();
155        this.supportedDomains = extractPatterns(config.get("supportedDomains").expect(List.class).asList(String.class));
156    }
157
158    /**
159     * Returns the name of this issuer.
160     *
161     * @return the name of this issuer.
162     */
163    public String getName() {
164        return name;
165    }
166
167    /**
168     * Returns the authorize end-point of this issuer.
169     *
170     * @return the authorize end-point of this issuer.
171     */
172    public URI getAuthorizeEndpoint() {
173        return authorizeEndpoint;
174    }
175
176    /**
177     * Returns the token end-point of this issuer.
178     *
179     * @return the token end-point of this issuer.
180     */
181    public URI getTokenEndpoint() {
182        return tokenEndpoint;
183    }
184
185    /**
186     * Returns the registration end-point of this issuer.
187     *
188     * @return the registration end-point of this issuer.
189     */
190    public URI getRegistrationEndpoint() {
191        return registrationEndpoint;
192    }
193
194    /**
195     * Returns the user end-point of this issuer.
196     *
197     * @return the user end-point of this issuer.
198     */
199    public URI getUserInfoEndpoint() {
200        return userInfoEndpoint;
201    }
202
203    /**
204     * Returns the well-known end-point of this issuer.
205     *
206     * @return the well-known end-point of this issuer.
207     */
208    public URI getWellKnownEndpoint() {
209        return wellKnownEndpoint;
210    }
211
212    /**
213     * Returns {@code true} if this issuer has a user info end-point.
214     *
215     * @return {@code true} if this issuer has a user info end-point.
216     */
217    public boolean hasUserInfoEndpoint() {
218        return userInfoEndpoint != null;
219    }
220
221    /**
222     * Returns the unmodifiable list of the supported domain names.
223     *
224     * @return A unmodifiable list of the supported domain names.
225     */
226    public List<Pattern> getSupportedDomains() {
227        return Collections.unmodifiableList(supportedDomains);
228    }
229
230    /**
231     * Builds a new Issuer based on the given well-known URI.
232     *
233     * @param name
234     *            The issuer's identifier. Usually, it's the host name or a
235     *            given name.
236     * @param wellKnownUri
237     *            The well-known URI of this issuer.
238     * @param supportedDomains
239     *            List of the supported domains for this issuer.
240     * @param handler
241     *            The issuer handler that does the call to the given well-known
242     *            URI.
243     * @return An OpenID issuer.
244     * @throws DiscoveryException
245     *             If an error occurred when retrieving the JSON content from
246     *             the server response.
247     */
248    public static Issuer build(final String name,
249                               final URI wellKnownUri,
250                               final List<String> supportedDomains,
251                               final Handler handler) throws DiscoveryException {
252        final Request request = new Request();
253        request.setMethod("GET");
254        request.setUri(wellKnownUri);
255
256        Response response;
257        try {
258            response = blockingCall(handler, new AttributesContext(new RootContext()), request);
259        } catch (InterruptedException e) {
260            throw new DiscoveryException(format("Interrupted while waiting for '%s' response", wellKnownUri), e);
261        }
262
263        if (!OK.equals(response.getStatus())) {
264            throw new DiscoveryException("Unable to read well-known OpenID Configuration from '"
265                    + wellKnownUri + "'", response.getCause());
266        }
267        JsonValue config = null;
268        try {
269            config = getJsonContent(response);
270        } catch (OAuth2ErrorException | JsonValueException e) {
271            throw new DiscoveryException("Cannot read JSON", e);
272        }
273        return new Issuer(name, config.put("supportedDomains", supportedDomains));
274    }
275
276    private static List<Pattern> extractPatterns(final List<String> from) {
277        final List<Pattern> patterns = new LinkedList<>();
278        if (from != null) {
279            for (String s : from) {
280                try {
281                    s = new StringBuilder("(http|https)://").append(s).append("/$").toString();
282                    patterns.add(Pattern.compile(s));
283                } catch (final PatternSyntaxException ex) {
284                    // Ignore
285                }
286            }
287        }
288        return patterns;
289    }
290
291    /** Creates and initializes an Issuer object in a heap environment. */
292    public static class Heaplet extends GenericHeaplet {
293        @Override
294        public Object create() throws HeapException {
295            final Handler issuerHandler = heap.resolve(config.get("issuerHandler")
296                                                             .defaultTo(CLIENT_HANDLER_HEAP_KEY),
297                                                       Handler.class);
298            final List<String> supportedDomains = config.get("supportedDomains").asList(String.class);
299            if (config.isDefined("wellKnownEndpoint")
300                    && !config.isDefined("authorizeEndpoint")
301                    && !config.isDefined("tokenEndpoint")) {
302                try {
303                    final URI wellKnownEndpoint = new URI(evaluate(config.get("wellKnownEndpoint")));
304                    return build(this.name, wellKnownEndpoint, supportedDomains, issuerHandler);
305                } catch (DiscoveryException | URISyntaxException e) {
306                    throw new HeapException(e);
307                }
308            }
309            return new Issuer(this.name, evaluate(config, logger));
310        }
311    }
312}