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 static org.mitre.util.JsonUtils.getAsBoolean;
024import static org.mitre.util.JsonUtils.getAsEncryptionMethodList;
025import static org.mitre.util.JsonUtils.getAsJweAlgorithmList;
026import static org.mitre.util.JsonUtils.getAsJwsAlgorithmList;
027import static org.mitre.util.JsonUtils.getAsString;
028import static org.mitre.util.JsonUtils.getAsStringList;
029
030import java.util.HashSet;
031import java.util.Set;
032import java.util.concurrent.ExecutionException;
033
034import org.apache.http.client.HttpClient;
035import org.apache.http.impl.client.HttpClientBuilder;
036import org.mitre.openid.connect.client.service.ServerConfigurationService;
037import org.mitre.openid.connect.config.ServerConfiguration;
038import org.slf4j.Logger;
039import org.slf4j.LoggerFactory;
040import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
041import org.springframework.security.authentication.AuthenticationServiceException;
042import org.springframework.web.client.RestTemplate;
043
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.JsonElement;
049import com.google.gson.JsonObject;
050import com.google.gson.JsonParser;
051
052/**
053 *
054 * Dynamically fetches OpenID Connect server configurations based on the issuer. Caches the server configurations.
055 *
056 * @author jricher
057 *
058 */
059public class DynamicServerConfigurationService implements ServerConfigurationService {
060
061        /**
062         * Logger for this class
063         */
064        private static final Logger logger = LoggerFactory.getLogger(DynamicServerConfigurationService.class);
065
066        // map of issuer -> server configuration, loaded dynamically from service discovery
067        private LoadingCache<String, ServerConfiguration> servers;
068
069        private Set<String> whitelist = new HashSet<>();
070        private Set<String> blacklist = new HashSet<>();
071
072        public DynamicServerConfigurationService() {
073                this(HttpClientBuilder.create().useSystemProperties().build());
074        }
075
076        public DynamicServerConfigurationService(HttpClient httpClient) {
077                // initialize the cache
078                servers = CacheBuilder.newBuilder().build(new OpenIDConnectServiceConfigurationFetcher(httpClient));
079        }
080
081        /**
082         * @return the whitelist
083         */
084        public Set<String> getWhitelist() {
085                return whitelist;
086        }
087
088        /**
089         * @param whitelist the whitelist to set
090         */
091        public void setWhitelist(Set<String> whitelist) {
092                this.whitelist = whitelist;
093        }
094
095        /**
096         * @return the blacklist
097         */
098        public Set<String> getBlacklist() {
099                return blacklist;
100        }
101
102        /**
103         * @param blacklist the blacklist to set
104         */
105        public void setBlacklist(Set<String> blacklist) {
106                this.blacklist = blacklist;
107        }
108
109        @Override
110        public ServerConfiguration getServerConfiguration(String issuer) {
111                try {
112
113                        if (!whitelist.isEmpty() && !whitelist.contains(issuer)) {
114                                throw new AuthenticationServiceException("Whitelist was nonempty, issuer was not in whitelist: " + issuer);
115                        }
116
117                        if (blacklist.contains(issuer)) {
118                                throw new AuthenticationServiceException("Issuer was in blacklist: " + issuer);
119                        }
120
121                        return servers.get(issuer);
122                } catch (UncheckedExecutionException | ExecutionException e) {
123                        logger.warn("Couldn't load configuration for " + issuer + ": " + e);
124                        return null;
125                }
126
127        }
128
129        /**
130         * @author jricher
131         *
132         */
133        private class OpenIDConnectServiceConfigurationFetcher extends CacheLoader<String, ServerConfiguration> {
134                private HttpComponentsClientHttpRequestFactory httpFactory;
135                private JsonParser parser = new JsonParser();
136
137                OpenIDConnectServiceConfigurationFetcher(HttpClient httpClient) {
138                        this.httpFactory = new HttpComponentsClientHttpRequestFactory(httpClient);
139                }
140
141                @Override
142                public ServerConfiguration load(String issuer) throws Exception {
143                        RestTemplate restTemplate = new RestTemplate(httpFactory);
144
145                        // data holder
146                        ServerConfiguration conf = new ServerConfiguration();
147
148                        // construct the well-known URI
149                        String url = issuer + "/.well-known/openid-configuration";
150
151                        // fetch the value
152                        String jsonString = restTemplate.getForObject(url, String.class);
153
154                        JsonElement parsed = parser.parse(jsonString);
155                        if (parsed.isJsonObject()) {
156
157                                JsonObject o = parsed.getAsJsonObject();
158
159                                // sanity checks
160                                if (!o.has("issuer")) {
161                                        throw new IllegalStateException("Returned object did not have an 'issuer' field");
162                                }
163
164                                if (!issuer.equals(o.get("issuer").getAsString())) {
165                                        logger.info("Issuer used for discover was " + issuer + " but final issuer is " + o.get("issuer").getAsString());
166                                }
167
168                                conf.setIssuer(o.get("issuer").getAsString());
169
170
171                                conf.setAuthorizationEndpointUri(getAsString(o, "authorization_endpoint"));
172                                conf.setTokenEndpointUri(getAsString(o, "token_endpoint"));
173                                conf.setJwksUri(getAsString(o, "jwks_uri"));
174                                conf.setUserInfoUri(getAsString(o, "userinfo_endpoint"));
175                                conf.setRegistrationEndpointUri(getAsString(o, "registration_endpoint"));
176                                conf.setIntrospectionEndpointUri(getAsString(o, "introspection_endpoint"));
177                                conf.setAcrValuesSupported(getAsStringList(o, "acr_values_supported"));
178                                conf.setCheckSessionIframe(getAsString(o, "check_session_iframe"));
179                                conf.setClaimsLocalesSupported(getAsStringList(o, "claims_locales_supported"));
180                                conf.setClaimsParameterSupported(getAsBoolean(o, "claims_parameter_supported"));
181                                conf.setClaimsSupported(getAsStringList(o, "claims_supported"));
182                                conf.setDisplayValuesSupported(getAsStringList(o, "display_values_supported"));
183                                conf.setEndSessionEndpoint(getAsString(o, "end_session_endpoint"));
184                                conf.setGrantTypesSupported(getAsStringList(o, "grant_types_supported"));
185                                conf.setIdTokenSigningAlgValuesSupported(getAsJwsAlgorithmList(o, "id_token_signing_alg_values_supported"));
186                                conf.setIdTokenEncryptionAlgValuesSupported(getAsJweAlgorithmList(o, "id_token_encryption_alg_values_supported"));
187                                conf.setIdTokenEncryptionEncValuesSupported(getAsEncryptionMethodList(o, "id_token_encryption_enc_values_supported"));
188                                conf.setOpPolicyUri(getAsString(o, "op_policy_uri"));
189                                conf.setOpTosUri(getAsString(o, "op_tos_uri"));
190                                conf.setRequestObjectEncryptionAlgValuesSupported(getAsJweAlgorithmList(o, "request_object_encryption_alg_values_supported"));
191                                conf.setRequestObjectEncryptionEncValuesSupported(getAsEncryptionMethodList(o, "request_object_encryption_enc_values_supported"));
192                                conf.setRequestObjectSigningAlgValuesSupported(getAsJwsAlgorithmList(o, "request_object_signing_alg_values_supported"));
193                                conf.setRequestParameterSupported(getAsBoolean(o, "request_parameter_supported"));
194                                conf.setRequestUriParameterSupported(getAsBoolean(o, "request_uri_parameter_supported"));
195                                conf.setResponseTypesSupported(getAsStringList(o, "response_types_supported"));
196                                conf.setScopesSupported(getAsStringList(o, "scopes_supported"));
197                                conf.setSubjectTypesSupported(getAsStringList(o, "subject_types_supported"));
198                                conf.setServiceDocumentation(getAsString(o, "service_documentation"));
199                                conf.setTokenEndpointAuthMethodsSupported(getAsStringList(o, "token_endpoint_auth_methods"));
200                                conf.setTokenEndpointAuthSigningAlgValuesSupported(getAsJwsAlgorithmList(o, "token_endpoint_auth_signing_alg_values_supported"));
201                                conf.setUiLocalesSupported(getAsStringList(o, "ui_locales_supported"));
202                                conf.setUserinfoEncryptionAlgValuesSupported(getAsJweAlgorithmList(o, "userinfo_encryption_alg_values_supported"));
203                                conf.setUserinfoEncryptionEncValuesSupported(getAsEncryptionMethodList(o, "userinfo_encryption_enc_values_supported"));
204                                conf.setUserinfoSigningAlgValuesSupported(getAsJwsAlgorithmList(o, "userinfo_signing_alg_values_supported"));
205
206                                return conf;
207                        } else {
208                                throw new IllegalStateException("Couldn't parse server discovery results for " + url);
209                        }
210
211                }
212
213        }
214
215}