001/*******************************************************************************
002 * Copyright 2017 The MIT Internet Trust Consortium
003 *
004 * Portions copyright 2011-2013 The MITRE Corporation
005 *
006 * Licensed under the Apache License, Version 2.0 (the "License");
007 * you may not use this file except in compliance with the License.
008 * You may obtain a copy of the License at
009 *
010 *   http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software
013 * distributed under the License is distributed on an "AS IS" BASIS,
014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015 * See the License for the specific language governing permissions and
016 * limitations under the License.
017 *******************************************************************************/
018/**
019 *
020 */
021package org.mitre.openid.connect.client.service.impl;
022
023import java.util.HashSet;
024import java.util.Set;
025import java.util.concurrent.ExecutionException;
026
027import javax.servlet.http.HttpServletRequest;
028
029import org.apache.http.client.HttpClient;
030import org.apache.http.client.utils.URIBuilder;
031import org.apache.http.impl.client.HttpClientBuilder;
032import org.mitre.discovery.util.WebfingerURLNormalizer;
033import org.mitre.openid.connect.client.model.IssuerServiceResponse;
034import org.mitre.openid.connect.client.service.IssuerService;
035import org.slf4j.Logger;
036import org.slf4j.LoggerFactory;
037import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
038import org.springframework.security.authentication.AuthenticationServiceException;
039import org.springframework.web.client.RestClientException;
040import org.springframework.web.client.RestTemplate;
041import org.springframework.web.util.UriComponents;
042
043import com.google.common.base.Strings;
044import com.google.common.cache.CacheBuilder;
045import com.google.common.cache.CacheLoader;
046import com.google.common.cache.LoadingCache;
047import com.google.common.util.concurrent.UncheckedExecutionException;
048import com.google.gson.JsonArray;
049import com.google.gson.JsonElement;
050import com.google.gson.JsonObject;
051import com.google.gson.JsonParseException;
052import com.google.gson.JsonParser;
053
054/**
055 * Use Webfinger to discover the appropriate issuer for a user-given input string.
056 * @author jricher
057 *
058 */
059public class WebfingerIssuerService implements IssuerService {
060
061        /**
062         * Logger for this class
063         */
064        private static final Logger logger = LoggerFactory.getLogger(WebfingerIssuerService.class);
065
066        // map of user input -> issuer, loaded dynamically from webfinger discover
067        private LoadingCache<String, LoadingResult> issuers;
068
069        // private data shuttle class to get back two bits of info from the cache loader
070        private class LoadingResult {
071                public String loginHint;
072                public String issuer;
073                public LoadingResult(String loginHint, String issuer) {
074                        this.loginHint = loginHint;
075                        this.issuer = issuer;
076                }
077        }
078
079        private Set<String> whitelist = new HashSet<>();
080        private Set<String> blacklist = new HashSet<>();
081
082        /**
083         * Name of the incoming parameter to check for discovery purposes.
084         */
085        private String parameterName = "identifier";
086
087        /**
088         * URL of the page to forward to if no identifier is given.
089         */
090        private String loginPageUrl;
091
092        /**
093         * Strict enfocement of "https"
094         */
095        private boolean forceHttps = true;
096
097        public WebfingerIssuerService() {
098                this(HttpClientBuilder.create().useSystemProperties().build());
099        }
100
101        public WebfingerIssuerService(HttpClient httpClient) {
102                issuers = CacheBuilder.newBuilder().build(new WebfingerIssuerFetcher(httpClient));
103        }
104
105        /* (non-Javadoc)
106         * @see org.mitre.openid.connect.client.service.IssuerService#getIssuer(javax.servlet.http.HttpServletRequest)
107         */
108        @Override
109        public IssuerServiceResponse getIssuer(HttpServletRequest request) {
110
111                String identifier = request.getParameter(parameterName);
112                if (!Strings.isNullOrEmpty(identifier)) {
113                        try {
114                                LoadingResult lr = issuers.get(identifier);
115                                if (!whitelist.isEmpty() && !whitelist.contains(lr.issuer)) {
116                                        throw new AuthenticationServiceException("Whitelist was nonempty, issuer was not in whitelist: " + lr.issuer);
117                                }
118
119                                if (blacklist.contains(lr.issuer)) {
120                                        throw new AuthenticationServiceException("Issuer was in blacklist: " + lr.issuer);
121                                }
122
123                                return new IssuerServiceResponse(lr.issuer, lr.loginHint, request.getParameter("target_link_uri"));
124                        } catch (UncheckedExecutionException | ExecutionException e) {
125                                logger.warn("Issue fetching issuer for user input: " + identifier + ": " + e.getMessage());
126                                return null;
127                        }
128
129                } else {
130                        logger.warn("No user input given, directing to login page: " + loginPageUrl);
131                        return new IssuerServiceResponse(loginPageUrl);
132                }
133        }
134
135        /**
136         * @return the parameterName
137         */
138        public String getParameterName() {
139                return parameterName;
140        }
141
142        /**
143         * @param parameterName the parameterName to set
144         */
145        public void setParameterName(String parameterName) {
146                this.parameterName = parameterName;
147        }
148
149
150        /**
151         * @return the loginPageUrl
152         */
153        public String getLoginPageUrl() {
154                return loginPageUrl;
155        }
156
157        /**
158         * @param loginPageUrl the loginPageUrl to set
159         */
160        public void setLoginPageUrl(String loginPageUrl) {
161                this.loginPageUrl = loginPageUrl;
162        }
163
164        /**
165         * @return the whitelist
166         */
167        public Set<String> getWhitelist() {
168                return whitelist;
169        }
170
171        /**
172         * @param whitelist the whitelist to set
173         */
174        public void setWhitelist(Set<String> whitelist) {
175                this.whitelist = whitelist;
176        }
177
178        /**
179         * @return the blacklist
180         */
181        public Set<String> getBlacklist() {
182                return blacklist;
183        }
184
185        /**
186         * @param blacklist the blacklist to set
187         */
188        public void setBlacklist(Set<String> blacklist) {
189                this.blacklist = blacklist;
190        }
191
192        /**
193         * @return the forceHttps
194         */
195        public boolean isForceHttps() {
196                return forceHttps;
197        }
198
199        /**
200         * @param forceHttps the forceHttps to set
201         */
202        public void setForceHttps(boolean forceHttps) {
203                this.forceHttps = forceHttps;
204        }
205
206        /**
207         * @author jricher
208         *
209         */
210        private class WebfingerIssuerFetcher extends CacheLoader<String, LoadingResult> {
211                private HttpComponentsClientHttpRequestFactory httpFactory;
212                private JsonParser parser = new JsonParser();
213
214                WebfingerIssuerFetcher(HttpClient httpClient) {
215                        this.httpFactory = new HttpComponentsClientHttpRequestFactory(httpClient);
216                }
217
218                @Override
219                public LoadingResult load(String identifier) throws Exception {
220
221                        UriComponents key = WebfingerURLNormalizer.normalizeResource(identifier);
222
223                        RestTemplate restTemplate = new RestTemplate(httpFactory);
224                        // construct the URL to go to
225
226                        String scheme = key.getScheme();
227
228                        // preserving http scheme is strictly for demo system use only.
229                        if (!Strings.isNullOrEmpty(scheme) &&scheme.equals("http")) {
230                                if (forceHttps) {
231                                        throw new IllegalArgumentException("Scheme must not be 'http'");
232                                } else {
233                                        logger.warn("Webfinger endpoint MUST use the https URI scheme, overriding by configuration");
234                                        scheme = "http://"; // add on colon and slashes.
235                                }
236                        } else {
237                                // otherwise we don't know the scheme, assume HTTPS
238                                scheme = "https://";
239                        }
240
241                        // do a webfinger lookup
242                        URIBuilder builder = new URIBuilder(scheme
243                                        + key.getHost()
244                                        + (key.getPort() >= 0 ? ":" + key.getPort() : "")
245                                        + Strings.nullToEmpty(key.getPath())
246                                        + "/.well-known/webfinger"
247                                        + (Strings.isNullOrEmpty(key.getQuery()) ? "" : "?" + key.getQuery())
248                                        );
249                        builder.addParameter("resource", identifier);
250                        builder.addParameter("rel", "http://openid.net/specs/connect/1.0/issuer");
251
252                        try {
253
254                                // do the fetch
255                                logger.info("Loading: " + builder.toString());
256                                String webfingerResponse = restTemplate.getForObject(builder.build(), String.class);
257
258                                JsonElement json = parser.parse(webfingerResponse);
259
260                                if (json != null && json.isJsonObject()) {
261                                        // find the issuer
262                                        JsonArray links = json.getAsJsonObject().get("links").getAsJsonArray();
263                                        for (JsonElement link : links) {
264                                                if (link.isJsonObject()) {
265                                                        JsonObject linkObj = link.getAsJsonObject();
266                                                        if (linkObj.has("href")
267                                                                        && linkObj.has("rel")
268                                                                        && linkObj.get("rel").getAsString().equals("http://openid.net/specs/connect/1.0/issuer")) {
269
270                                                                // we found the issuer, return it
271                                                                String href = linkObj.get("href").getAsString();
272
273                                                                if (identifier.equals(href)
274                                                                                || identifier.startsWith("http")) {
275                                                                        // try to avoid sending a URL as the login hint
276                                                                        return new LoadingResult(null, href);
277                                                                } else {
278                                                                        // otherwise pass back whatever the user typed as a login hint
279                                                                        return new LoadingResult(identifier, href);
280                                                                }
281                                                        }
282                                                }
283                                        }
284                                }
285                        } catch (JsonParseException | RestClientException e) {
286                                logger.warn("Failure in fetching webfinger input", e.getMessage());
287                        }
288
289                        // we couldn't find it!
290
291                        if (key.getScheme().equals("http") || key.getScheme().equals("https")) {
292                                // if it looks like HTTP then punt: return the input, hope for the best
293                                logger.warn("Returning normalized input string as issuer, hoping for the best: " + identifier);
294                                return new LoadingResult(null, identifier);
295                        } else {
296                                // if it's not HTTP, give up
297                                logger.warn("Couldn't find issuer: " + identifier);
298                                throw new IllegalArgumentException();
299                        }
300
301                }
302
303        }
304
305}