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 *******************************************************************************/
018package org.mitre.oauth2.service.impl;
019
020import java.math.BigInteger;
021import java.security.SecureRandom;
022import java.util.ArrayList;
023import java.util.Collection;
024import java.util.Date;
025import java.util.List;
026import java.util.Set;
027import java.util.UUID;
028import java.util.concurrent.ExecutionException;
029import java.util.concurrent.TimeUnit;
030
031import org.apache.commons.codec.binary.Base64;
032import org.apache.http.client.HttpClient;
033import org.apache.http.impl.client.HttpClientBuilder;
034import org.mitre.oauth2.model.ClientDetailsEntity;
035import org.mitre.oauth2.model.ClientDetailsEntity.AuthMethod;
036import org.mitre.oauth2.model.SystemScope;
037import org.mitre.oauth2.repository.OAuth2ClientRepository;
038import org.mitre.oauth2.repository.OAuth2TokenRepository;
039import org.mitre.oauth2.service.ClientDetailsEntityService;
040import org.mitre.oauth2.service.SystemScopeService;
041import org.mitre.openid.connect.config.ConfigurationPropertiesBean;
042import org.mitre.openid.connect.model.WhitelistedSite;
043import org.mitre.openid.connect.service.ApprovedSiteService;
044import org.mitre.openid.connect.service.BlacklistedSiteService;
045import org.mitre.openid.connect.service.StatsService;
046import org.mitre.openid.connect.service.WhitelistedSiteService;
047import org.mitre.uma.model.ResourceSet;
048import org.mitre.uma.service.ResourceSetService;
049import org.slf4j.Logger;
050import org.slf4j.LoggerFactory;
051import org.springframework.beans.factory.annotation.Autowired;
052import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
053import org.springframework.security.oauth2.common.exceptions.InvalidClientException;
054import org.springframework.security.oauth2.common.exceptions.OAuth2Exception;
055import org.springframework.stereotype.Service;
056import org.springframework.web.client.RestTemplate;
057import org.springframework.web.util.UriComponents;
058import org.springframework.web.util.UriComponentsBuilder;
059
060import com.google.common.base.Strings;
061import com.google.common.cache.CacheBuilder;
062import com.google.common.cache.CacheLoader;
063import com.google.common.cache.LoadingCache;
064import com.google.common.util.concurrent.UncheckedExecutionException;
065import com.google.gson.JsonElement;
066import com.google.gson.JsonParser;
067
068@Service
069public class DefaultOAuth2ClientDetailsEntityService implements ClientDetailsEntityService {
070
071        /**
072         * Logger for this class
073         */
074        private static final Logger logger = LoggerFactory.getLogger(DefaultOAuth2ClientDetailsEntityService.class);
075
076        @Autowired
077        private OAuth2ClientRepository clientRepository;
078
079        @Autowired
080        private OAuth2TokenRepository tokenRepository;
081
082        @Autowired
083        private ApprovedSiteService approvedSiteService;
084
085        @Autowired
086        private WhitelistedSiteService whitelistedSiteService;
087
088        @Autowired
089        private BlacklistedSiteService blacklistedSiteService;
090
091        @Autowired
092        private SystemScopeService scopeService;
093
094        @Autowired
095        private StatsService statsService;
096
097        @Autowired
098        private ResourceSetService resourceSetService;
099
100        @Autowired
101        private ConfigurationPropertiesBean config;
102
103        // map of sector URI -> list of redirect URIs
104        private LoadingCache<String, List<String>> sectorRedirects = CacheBuilder.newBuilder()
105                        .expireAfterAccess(1, TimeUnit.HOURS)
106                        .maximumSize(100)
107                        .build(new SectorIdentifierLoader(HttpClientBuilder.create().useSystemProperties().build()));
108
109        @Override
110        public ClientDetailsEntity saveNewClient(ClientDetailsEntity client) {
111                if (client.getId() != null) { // if it's not null, it's already been saved, this is an error
112                        throw new IllegalArgumentException("Tried to save a new client with an existing ID: " + client.getId());
113                }
114
115                if (client.getRegisteredRedirectUri() != null) {
116                        for (String uri : client.getRegisteredRedirectUri()) {
117                                if (blacklistedSiteService.isBlacklisted(uri)) {
118                                        throw new IllegalArgumentException("Client URI is blacklisted: " + uri);
119                                }
120                        }
121                }
122
123                // assign a random clientid if it's empty
124                // NOTE: don't assign a random client secret without asking, since public clients have no secret
125                if (Strings.isNullOrEmpty(client.getClientId())) {
126                        client = generateClientId(client);
127                }
128
129                // make sure that clients with the "refresh_token" grant type have the "offline_access" scope, and vice versa
130                ensureRefreshTokenConsistency(client);
131
132                // make sure we don't have both a JWKS and a JWKS URI
133                ensureKeyConsistency(client);
134
135                // check consistency when using HEART mode
136                checkHeartMode(client);
137
138                // timestamp this to right now
139                client.setCreatedAt(new Date());
140
141
142                // check the sector URI
143                checkSectorIdentifierUri(client);
144
145
146                ensureNoReservedScopes(client);
147
148                ClientDetailsEntity c = clientRepository.saveClient(client);
149
150                statsService.resetCache();
151
152                return c;
153        }
154
155        /**
156         * Make sure the client has only one type of key registered
157         * @param client
158         */
159        private void ensureKeyConsistency(ClientDetailsEntity client) {
160                if (client.getJwksUri() != null && client.getJwks() != null) {
161                        // a client can only have one key type or the other, not both
162                        throw new IllegalArgumentException("A client cannot have both JWKS URI and JWKS value");
163                }
164        }
165
166        /**
167         * Make sure the client doesn't request any system reserved scopes
168         */
169        private void ensureNoReservedScopes(ClientDetailsEntity client) {
170                // make sure a client doesn't get any special system scopes
171                Set<SystemScope> requestedScope = scopeService.fromStrings(client.getScope());
172
173                requestedScope = scopeService.removeReservedScopes(requestedScope);
174
175                client.setScope(scopeService.toStrings(requestedScope));
176        }
177
178        /**
179         * Load the sector identifier URI if it exists and check the redirect URIs against it
180         * @param client
181         */
182        private void checkSectorIdentifierUri(ClientDetailsEntity client) {
183                if (!Strings.isNullOrEmpty(client.getSectorIdentifierUri())) {
184                        try {
185                                List<String> redirects = sectorRedirects.get(client.getSectorIdentifierUri());
186
187                                if (client.getRegisteredRedirectUri() != null) {
188                                        for (String uri : client.getRegisteredRedirectUri()) {
189                                                if (!redirects.contains(uri)) {
190                                                        throw new IllegalArgumentException("Requested Redirect URI " + uri + " is not listed at sector identifier " + redirects);
191                                                }
192                                        }
193                                }
194
195                        } catch (UncheckedExecutionException | ExecutionException e) {
196                                throw new IllegalArgumentException("Unable to load sector identifier URI " + client.getSectorIdentifierUri() + ": " + e.getMessage());
197                        }
198                }
199        }
200
201        /**
202         * Make sure the client has the appropriate scope and grant type.
203         * @param client
204         */
205        private void ensureRefreshTokenConsistency(ClientDetailsEntity client) {
206                if (client.getAuthorizedGrantTypes().contains("refresh_token")
207                                || client.getScope().contains(SystemScopeService.OFFLINE_ACCESS)) {
208                        client.getScope().add(SystemScopeService.OFFLINE_ACCESS);
209                        client.getAuthorizedGrantTypes().add("refresh_token");
210                }
211        }
212
213        /**
214         * If HEART mode is enabled, make sure the client meets the requirements:
215         *  - Only one of authorization_code, implicit, or client_credentials can be used at a time
216         *  - A redirect_uri must be registered with either authorization_code or implicit
217         *  - A key must be registered
218         *  - A client secret must not be generated
219         *  - authorization_code and client_credentials must use the private_key authorization method
220         * @param client
221         */
222        private void checkHeartMode(ClientDetailsEntity client) {
223                if (config.isHeartMode()) {
224                        if (client.getGrantTypes().contains("authorization_code")) {
225                                // make sure we don't have incompatible grant types
226                                if (client.getGrantTypes().contains("implicit") || client.getGrantTypes().contains("client_credentials")) {
227                                        throw new IllegalArgumentException("[HEART mode] Incompatible grant types");
228                                }
229
230                                // make sure we've got the right authentication method
231                                if (client.getTokenEndpointAuthMethod() == null || !client.getTokenEndpointAuthMethod().equals(AuthMethod.PRIVATE_KEY)) {
232                                        throw new IllegalArgumentException("[HEART mode] Authorization code clients must use the private_key authentication method");
233                                }
234
235                                // make sure we've got a redirect URI
236                                if (client.getRedirectUris().isEmpty()) {
237                                        throw new IllegalArgumentException("[HEART mode] Authorization code clients must register at least one redirect URI");
238                                }
239                        }
240
241                        if (client.getGrantTypes().contains("implicit")) {
242                                // make sure we don't have incompatible grant types
243                                if (client.getGrantTypes().contains("authorization_code") || client.getGrantTypes().contains("client_credentials") || client.getGrantTypes().contains("refresh_token")) {
244                                        throw new IllegalArgumentException("[HEART mode] Incompatible grant types");
245                                }
246
247                                // make sure we've got the right authentication method
248                                if (client.getTokenEndpointAuthMethod() == null || !client.getTokenEndpointAuthMethod().equals(AuthMethod.NONE)) {
249                                        throw new IllegalArgumentException("[HEART mode] Implicit clients must use the none authentication method");
250                                }
251
252                                // make sure we've got a redirect URI
253                                if (client.getRedirectUris().isEmpty()) {
254                                        throw new IllegalArgumentException("[HEART mode] Implicit clients must register at least one redirect URI");
255                                }
256                        }
257
258                        if (client.getGrantTypes().contains("client_credentials")) {
259                                // make sure we don't have incompatible grant types
260                                if (client.getGrantTypes().contains("authorization_code") || client.getGrantTypes().contains("implicit") || client.getGrantTypes().contains("refresh_token")) {
261                                        throw new IllegalArgumentException("[HEART mode] Incompatible grant types");
262                                }
263
264                                // make sure we've got the right authentication method
265                                if (client.getTokenEndpointAuthMethod() == null || !client.getTokenEndpointAuthMethod().equals(AuthMethod.PRIVATE_KEY)) {
266                                        throw new IllegalArgumentException("[HEART mode] Client credentials clients must use the private_key authentication method");
267                                }
268
269                                // make sure we've got a redirect URI
270                                if (!client.getRedirectUris().isEmpty()) {
271                                        throw new IllegalArgumentException("[HEART mode] Client credentials clients must not register a redirect URI");
272                                }
273
274                        }
275
276                        if (client.getGrantTypes().contains("password")) {
277                                throw new IllegalArgumentException("[HEART mode] Password grant type is forbidden");
278                        }
279
280                        // make sure we don't have a client secret
281                        if (!Strings.isNullOrEmpty(client.getClientSecret())) {
282                                throw new IllegalArgumentException("[HEART mode] Client secrets are not allowed");
283                        }
284
285                        // make sure we've got a key registered
286                        if (client.getJwks() == null && Strings.isNullOrEmpty(client.getJwksUri())) {
287                                throw new IllegalArgumentException("[HEART mode] All clients must have a key registered");
288                        }
289
290                        // make sure our redirect URIs each fit one of the allowed categories
291                        if (client.getRedirectUris() != null && !client.getRedirectUris().isEmpty()) {
292                                boolean localhost = false;
293                                boolean remoteHttps = false;
294                                boolean customScheme = false;
295                                for (String uri : client.getRedirectUris()) {
296                                        UriComponents components = UriComponentsBuilder.fromUriString(uri).build();
297                                        if (components.getScheme() == null) {
298                                                // this is a very unknown redirect URI
299                                                customScheme = true;
300                                        } else if (components.getScheme().equals("http")) {
301                                                // http scheme, check for localhost
302                                                if (components.getHost().equals("localhost") || components.getHost().equals("127.0.0.1")) {
303                                                        localhost = true;
304                                                } else {
305                                                        throw new IllegalArgumentException("[HEART mode] Can't have an http redirect URI on non-local host");
306                                                }
307                                        } else if (components.getScheme().equals("https")) {
308                                                remoteHttps = true;
309                                        } else {
310                                                customScheme = true;
311                                        }
312                                }
313
314                                // now we make sure the client has a URI in only one of each of the three categories
315                                if (!((localhost ^ remoteHttps ^ customScheme)
316                                                && !(localhost && remoteHttps && customScheme))) {
317                                        throw new IllegalArgumentException("[HEART mode] Can't have more than one class of redirect URI");
318                                }
319                        }
320
321                }
322        }
323
324        /**
325         * Get the client by its internal ID
326         */
327        @Override
328        public ClientDetailsEntity getClientById(Long id) {
329                ClientDetailsEntity client = clientRepository.getById(id);
330
331                return client;
332        }
333
334        /**
335         * Get the client for the given ClientID
336         */
337        @Override
338        public ClientDetailsEntity loadClientByClientId(String clientId) throws OAuth2Exception, InvalidClientException, IllegalArgumentException {
339                if (!Strings.isNullOrEmpty(clientId)) {
340                        ClientDetailsEntity client = clientRepository.getClientByClientId(clientId);
341                        if (client == null) {
342                                throw new InvalidClientException("Client with id " + clientId + " was not found");
343                        }
344                        else {
345                                return client;
346                        }
347                }
348
349                throw new IllegalArgumentException("Client id must not be empty!");
350        }
351
352        /**
353         * Delete a client and all its associated tokens
354         */
355        @Override
356        public void deleteClient(ClientDetailsEntity client) throws InvalidClientException {
357
358                if (clientRepository.getById(client.getId()) == null) {
359                        throw new InvalidClientException("Client with id " + client.getClientId() + " was not found");
360                }
361
362                // clean out any tokens that this client had issued
363                tokenRepository.clearTokensForClient(client);
364
365                // clean out any approved sites for this client
366                approvedSiteService.clearApprovedSitesForClient(client);
367
368                // clear out any whitelisted sites for this client
369                WhitelistedSite whitelistedSite = whitelistedSiteService.getByClientId(client.getClientId());
370                if (whitelistedSite != null) {
371                        whitelistedSiteService.remove(whitelistedSite);
372                }
373
374                // clear out resource sets registered for this client
375                Collection<ResourceSet> resourceSets = resourceSetService.getAllForClient(client);
376                for (ResourceSet rs : resourceSets) {
377                        resourceSetService.remove(rs);
378                }
379
380                // take care of the client itself
381                clientRepository.deleteClient(client);
382
383                statsService.resetCache();
384
385        }
386
387        /**
388         * Update the oldClient with information from the newClient. The
389         * id from oldClient is retained.
390         *
391         * Checks to make sure the refresh grant type and
392         * the scopes are set appropriately.
393         *
394         * Checks to make sure the redirect URIs aren't blacklisted.
395         *
396         * Attempts to load the redirect URI (possibly cached) to check the
397         * sector identifier against the contents there.
398         *
399         *
400         */
401        @Override
402        public ClientDetailsEntity updateClient(ClientDetailsEntity oldClient, ClientDetailsEntity newClient) throws IllegalArgumentException {
403                if (oldClient != null && newClient != null) {
404
405                        for (String uri : newClient.getRegisteredRedirectUri()) {
406                                if (blacklistedSiteService.isBlacklisted(uri)) {
407                                        throw new IllegalArgumentException("Client URI is blacklisted: " + uri);
408                                }
409                        }
410
411                        // if the client is flagged to allow for refresh tokens, make sure it's got the right scope
412                        ensureRefreshTokenConsistency(newClient);
413
414                        // make sure we don't have both a JWKS and a JWKS URI
415                        ensureKeyConsistency(newClient);
416
417                        // check consistency when using HEART mode
418                        checkHeartMode(newClient);
419
420                        // check the sector URI
421                        checkSectorIdentifierUri(newClient);
422
423                        // make sure a client doesn't get any special system scopes
424                        ensureNoReservedScopes(newClient);
425
426                        return clientRepository.updateClient(oldClient.getId(), newClient);
427                }
428                throw new IllegalArgumentException("Neither old client or new client can be null!");
429        }
430
431        /**
432         * Get all clients in the system
433         */
434        @Override
435        public Collection<ClientDetailsEntity> getAllClients() {
436                return clientRepository.getAllClients();
437        }
438
439        /**
440         * Generates a clientId for the given client and sets it to the client's clientId field. Returns the client that was passed in, now with id set.
441         */
442        @Override
443        public ClientDetailsEntity generateClientId(ClientDetailsEntity client) {
444                client.setClientId(UUID.randomUUID().toString());
445                return client;
446        }
447
448        /**
449         * Generates a new clientSecret for the given client and sets it to the client's clientSecret field. Returns the client that was passed in, now with secret set.
450         */
451        @Override
452        public ClientDetailsEntity generateClientSecret(ClientDetailsEntity client) {
453                if (config.isHeartMode()) {
454                        logger.error("[HEART mode] Can't generate a client secret, skipping step; client won't be saved due to invalid configuration");
455                        client.setClientSecret(null);
456                } else {
457                        client.setClientSecret(Base64.encodeBase64URLSafeString(new BigInteger(512, new SecureRandom()).toByteArray()).replace("=", ""));
458                }
459                return client;
460        }
461
462        /**
463         * Utility class to load a sector identifier's set of authorized redirect URIs.
464         *
465         * @author jricher
466         *
467         */
468        private class SectorIdentifierLoader extends CacheLoader<String, List<String>> {
469                private HttpComponentsClientHttpRequestFactory httpFactory;
470                private RestTemplate restTemplate;
471                private JsonParser parser = new JsonParser();
472
473                SectorIdentifierLoader(HttpClient httpClient) {
474                        this.httpFactory = new HttpComponentsClientHttpRequestFactory(httpClient);
475                        this.restTemplate = new RestTemplate(httpFactory);
476                }
477
478                @Override
479                public List<String> load(String key) throws Exception {
480
481                        if (!key.startsWith("https")) {
482                                if (config.isForceHttps()) {
483                                        throw new IllegalArgumentException("Sector identifier must start with https: " + key);
484                                }
485                                logger.error("Sector identifier doesn't start with https, loading anyway...");
486                        }
487
488                        // key is the sector URI
489                        String jsonString = restTemplate.getForObject(key, String.class);
490                        JsonElement json = parser.parse(jsonString);
491
492                        if (json.isJsonArray()) {
493                                List<String> redirectUris = new ArrayList<>();
494                                for (JsonElement el : json.getAsJsonArray()) {
495                                        redirectUris.add(el.getAsString());
496                                }
497
498                                logger.info("Found " + redirectUris + " for sector " + key);
499
500                                return redirectUris;
501                        } else {
502                                throw new IllegalArgumentException("JSON Format Error");
503                        }
504
505                }
506
507        }
508
509}