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.openid.connect.web;
019
020import java.lang.reflect.Type;
021import java.sql.SQLIntegrityConstraintViolationException;
022import java.text.ParseException;
023import java.util.Collection;
024
025import javax.persistence.PersistenceException;
026
027import org.eclipse.persistence.exceptions.DatabaseException;
028import org.mitre.jwt.assertion.AssertionValidator;
029import org.mitre.oauth2.model.ClientDetailsEntity;
030import org.mitre.oauth2.model.ClientDetailsEntity.AppType;
031import org.mitre.oauth2.model.ClientDetailsEntity.AuthMethod;
032import org.mitre.oauth2.model.ClientDetailsEntity.SubjectType;
033import org.mitre.oauth2.model.PKCEAlgorithm;
034import org.mitre.oauth2.service.ClientDetailsEntityService;
035import org.mitre.oauth2.web.AuthenticationUtilities;
036import org.mitre.openid.connect.exception.ValidationException;
037import org.mitre.openid.connect.model.CachedImage;
038import org.mitre.openid.connect.service.ClientLogoLoadingService;
039import org.mitre.openid.connect.view.ClientEntityViewForAdmins;
040import org.mitre.openid.connect.view.ClientEntityViewForUsers;
041import org.mitre.openid.connect.view.HttpCodeView;
042import org.mitre.openid.connect.view.JsonEntityView;
043import org.mitre.openid.connect.view.JsonErrorView;
044import org.slf4j.Logger;
045import org.slf4j.LoggerFactory;
046import org.springframework.beans.factory.annotation.Autowired;
047import org.springframework.beans.factory.annotation.Qualifier;
048import org.springframework.http.HttpHeaders;
049import org.springframework.http.HttpStatus;
050import org.springframework.http.MediaType;
051import org.springframework.http.ResponseEntity;
052import org.springframework.security.access.prepost.PreAuthorize;
053import org.springframework.security.core.Authentication;
054import org.springframework.security.oauth2.common.util.OAuth2Utils;
055import org.springframework.stereotype.Controller;
056import org.springframework.ui.Model;
057import org.springframework.web.bind.annotation.PathVariable;
058import org.springframework.web.bind.annotation.RequestBody;
059import org.springframework.web.bind.annotation.RequestMapping;
060import org.springframework.web.bind.annotation.RequestMethod;
061import org.springframework.web.servlet.ModelAndView;
062
063import com.google.common.base.Strings;
064import com.google.common.collect.Sets;
065import com.google.gson.Gson;
066import com.google.gson.GsonBuilder;
067import com.google.gson.JsonDeserializationContext;
068import com.google.gson.JsonDeserializer;
069import com.google.gson.JsonElement;
070import com.google.gson.JsonObject;
071import com.google.gson.JsonParseException;
072import com.google.gson.JsonParser;
073import com.google.gson.JsonSyntaxException;
074import com.nimbusds.jose.Algorithm;
075import com.nimbusds.jose.EncryptionMethod;
076import com.nimbusds.jose.JWEAlgorithm;
077import com.nimbusds.jose.JWSAlgorithm;
078import com.nimbusds.jose.jwk.JWKSet;
079import com.nimbusds.jwt.JWT;
080import com.nimbusds.jwt.JWTClaimsSet;
081import com.nimbusds.jwt.JWTParser;
082
083import static org.mitre.oauth2.model.RegisteredClientFields.APPLICATION_TYPE;
084import static org.mitre.oauth2.model.RegisteredClientFields.CLAIMS_REDIRECT_URIS;
085import static org.mitre.oauth2.model.RegisteredClientFields.CLIENT_ID;
086import static org.mitre.oauth2.model.RegisteredClientFields.CLIENT_ID_ISSUED_AT;
087import static org.mitre.oauth2.model.RegisteredClientFields.CLIENT_NAME;
088import static org.mitre.oauth2.model.RegisteredClientFields.CLIENT_SECRET;
089import static org.mitre.oauth2.model.RegisteredClientFields.CLIENT_SECRET_EXPIRES_AT;
090import static org.mitre.oauth2.model.RegisteredClientFields.CLIENT_URI;
091import static org.mitre.oauth2.model.RegisteredClientFields.CONTACTS;
092import static org.mitre.oauth2.model.RegisteredClientFields.DEFAULT_ACR_VALUES;
093import static org.mitre.oauth2.model.RegisteredClientFields.DEFAULT_MAX_AGE;
094import static org.mitre.oauth2.model.RegisteredClientFields.GRANT_TYPES;
095import static org.mitre.oauth2.model.RegisteredClientFields.ID_TOKEN_ENCRYPTED_RESPONSE_ALG;
096import static org.mitre.oauth2.model.RegisteredClientFields.ID_TOKEN_ENCRYPTED_RESPONSE_ENC;
097import static org.mitre.oauth2.model.RegisteredClientFields.ID_TOKEN_SIGNED_RESPONSE_ALG;
098import static org.mitre.oauth2.model.RegisteredClientFields.INITIATE_LOGIN_URI;
099import static org.mitre.oauth2.model.RegisteredClientFields.JWKS;
100import static org.mitre.oauth2.model.RegisteredClientFields.JWKS_URI;
101import static org.mitre.oauth2.model.RegisteredClientFields.LOGO_URI;
102import static org.mitre.oauth2.model.RegisteredClientFields.POLICY_URI;
103import static org.mitre.oauth2.model.RegisteredClientFields.POST_LOGOUT_REDIRECT_URIS;
104import static org.mitre.oauth2.model.RegisteredClientFields.REDIRECT_URIS;
105import static org.mitre.oauth2.model.RegisteredClientFields.REGISTRATION_ACCESS_TOKEN;
106import static org.mitre.oauth2.model.RegisteredClientFields.REGISTRATION_CLIENT_URI;
107import static org.mitre.oauth2.model.RegisteredClientFields.REQUEST_OBJECT_SIGNING_ALG;
108import static org.mitre.oauth2.model.RegisteredClientFields.REQUEST_URIS;
109import static org.mitre.oauth2.model.RegisteredClientFields.REQUIRE_AUTH_TIME;
110import static org.mitre.oauth2.model.RegisteredClientFields.RESPONSE_TYPES;
111import static org.mitre.oauth2.model.RegisteredClientFields.SCOPE;
112import static org.mitre.oauth2.model.RegisteredClientFields.SECTOR_IDENTIFIER_URI;
113import static org.mitre.oauth2.model.RegisteredClientFields.SOFTWARE_STATEMENT;
114import static org.mitre.oauth2.model.RegisteredClientFields.SUBJECT_TYPE;
115import static org.mitre.oauth2.model.RegisteredClientFields.TOKEN_ENDPOINT_AUTH_METHOD;
116import static org.mitre.oauth2.model.RegisteredClientFields.TOKEN_ENDPOINT_AUTH_SIGNING_ALG;
117import static org.mitre.oauth2.model.RegisteredClientFields.TOS_URI;
118import static org.mitre.oauth2.model.RegisteredClientFields.USERINFO_ENCRYPTED_RESPONSE_ALG;
119import static org.mitre.oauth2.model.RegisteredClientFields.USERINFO_ENCRYPTED_RESPONSE_ENC;
120import static org.mitre.oauth2.model.RegisteredClientFields.USERINFO_SIGNED_RESPONSE_ALG;
121
122/**
123 * @author Michael Jett <mjett@mitre.org>
124 */
125
126@Controller
127@RequestMapping("/" + ClientAPI.URL)
128@PreAuthorize("hasRole('ROLE_USER')")
129public class ClientAPI {
130
131        public static final String URL = RootController.API_URL + "/clients";
132
133        @Autowired
134        private ClientDetailsEntityService clientService;
135
136        @Autowired
137        private ClientLogoLoadingService clientLogoLoadingService;
138
139        @Autowired
140        @Qualifier("clientAssertionValidator")
141        private AssertionValidator assertionValidator;
142
143        private JsonParser parser = new JsonParser();
144
145        private Gson gson = new GsonBuilder()
146                        .serializeNulls()
147                        .registerTypeAdapter(JWSAlgorithm.class, new JsonDeserializer<Algorithm>() {
148                                @Override
149                                public JWSAlgorithm deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
150                                        if (json.isJsonPrimitive()) {
151                                                return JWSAlgorithm.parse(json.getAsString());
152                                        } else {
153                                                return null;
154                                        }
155                                }
156                        })
157                        .registerTypeAdapter(JWEAlgorithm.class, new JsonDeserializer<Algorithm>() {
158                                @Override
159                                public JWEAlgorithm deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
160                                        if (json.isJsonPrimitive()) {
161                                                return JWEAlgorithm.parse(json.getAsString());
162                                        } else {
163                                                return null;
164                                        }
165                                }
166                        })
167                        .registerTypeAdapter(EncryptionMethod.class, new JsonDeserializer<Algorithm>() {
168                                @Override
169                                public EncryptionMethod deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
170                                        if (json.isJsonPrimitive()) {
171                                                return EncryptionMethod.parse(json.getAsString());
172                                        } else {
173                                                return null;
174                                        }
175                                }
176                        })
177                        .registerTypeAdapter(JWKSet.class, new JsonDeserializer<JWKSet>() {
178                                @Override
179                                public JWKSet deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
180                                        if (json.isJsonObject()) {
181                                                try {
182                                                        return JWKSet.parse(json.toString());
183                                                } catch (ParseException e) {
184                                                        return null;
185                                                }
186                                        } else {
187                                                return null;
188                                        }
189                                }
190                        })
191                        .registerTypeAdapter(JWT.class, new JsonDeserializer<JWT>() {
192                                @Override
193                                public JWT deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
194                                        if (json.isJsonPrimitive()) {
195                                                try {
196                                                        return JWTParser.parse(json.getAsString());
197                                                } catch (ParseException e) {
198                                                        return null;
199                                                }
200                                        } else {
201                                                return null;
202                                        }
203                                }
204                        })
205                        .registerTypeAdapter(PKCEAlgorithm.class, new JsonDeserializer<Algorithm>() {
206                                @Override
207                                public PKCEAlgorithm deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
208                                        if (json.isJsonPrimitive()) {
209                                                return PKCEAlgorithm.parse(json.getAsString());
210                                        } else {
211                                                return null;
212                                        }
213                                }
214                        })
215                        .setDateFormat("yyyy-MM-dd'T'HH:mm:ssZ")
216                        .create();
217
218        /**
219         * Logger for this class
220         */
221        private static final Logger logger = LoggerFactory.getLogger(ClientAPI.class);
222
223        /**
224         * Get a list of all clients
225         * @param modelAndView
226         * @return
227         */
228        @RequestMapping(method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
229        public String apiGetAllClients(Model model, Authentication auth) {
230
231                Collection<ClientDetailsEntity> clients = clientService.getAllClients();
232                model.addAttribute(JsonEntityView.ENTITY, clients);
233
234                if (AuthenticationUtilities.isAdmin(auth)) {
235                        return ClientEntityViewForAdmins.VIEWNAME;
236                } else {
237                        return ClientEntityViewForUsers.VIEWNAME;
238                }
239        }
240
241        /**
242         * Create a new client
243         * @param json
244         * @param m
245         * @param principal
246         * @return
247         */
248        @PreAuthorize("hasRole('ROLE_ADMIN')")
249        @RequestMapping(method = RequestMethod.POST, consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
250        public String apiAddClient(@RequestBody String jsonString, Model m, Authentication auth) {
251
252                JsonObject json = null;
253                ClientDetailsEntity client = null;
254
255                try {
256                        json = parser.parse(jsonString).getAsJsonObject();
257                        client = gson.fromJson(json, ClientDetailsEntity.class);
258                        client = validateSoftwareStatement(client);
259                } catch (JsonSyntaxException e) {
260                        logger.error("apiAddClient failed due to JsonSyntaxException", e);
261                        m.addAttribute(HttpCodeView.CODE, HttpStatus.BAD_REQUEST);
262                        m.addAttribute(JsonErrorView.ERROR_MESSAGE, "Could not save new client. The server encountered a JSON syntax exception. Contact a system administrator for assistance.");
263                        return JsonErrorView.VIEWNAME;
264                } catch (IllegalStateException e) {
265                        logger.error("apiAddClient failed due to IllegalStateException", e);
266                        m.addAttribute(HttpCodeView.CODE, HttpStatus.BAD_REQUEST);
267                        m.addAttribute(JsonErrorView.ERROR_MESSAGE, "Could not save new client. The server encountered an IllegalStateException. Refresh and try again - if the problem persists, contact a system administrator for assistance.");
268                        return JsonErrorView.VIEWNAME;
269                } catch (ValidationException e) {
270                        logger.error("apiUpdateClient failed due to ValidationException", e);
271                        m.addAttribute(HttpCodeView.CODE, HttpStatus.BAD_REQUEST);
272                        m.addAttribute(JsonErrorView.ERROR_MESSAGE, "Could not update client. The server encountered a ValidationException.");
273                        return JsonErrorView.VIEWNAME;
274                }
275
276                // if they leave the client identifier empty, force it to be generated
277                if (Strings.isNullOrEmpty(client.getClientId())) {
278                        client = clientService.generateClientId(client);
279                }
280
281                if (client.getTokenEndpointAuthMethod() == null ||
282                                client.getTokenEndpointAuthMethod().equals(AuthMethod.NONE)) {
283                        // we shouldn't have a secret for this client
284
285                        client.setClientSecret(null);
286
287                } else if (client.getTokenEndpointAuthMethod().equals(AuthMethod.SECRET_BASIC)
288                                || client.getTokenEndpointAuthMethod().equals(AuthMethod.SECRET_POST)
289                                || client.getTokenEndpointAuthMethod().equals(AuthMethod.SECRET_JWT)) {
290
291                        // if they've asked for us to generate a client secret (or they left it blank but require one), do so here
292                        if (json.has("generateClientSecret") && json.get("generateClientSecret").getAsBoolean()
293                                        || Strings.isNullOrEmpty(client.getClientSecret())) {
294                                client = clientService.generateClientSecret(client);
295                        }
296
297                } else if (client.getTokenEndpointAuthMethod().equals(AuthMethod.PRIVATE_KEY)) {
298
299                        if (Strings.isNullOrEmpty(client.getJwksUri()) && client.getJwks() == null) {
300                                logger.error("tried to create client with private key auth but no private key");
301                                m.addAttribute(HttpCodeView.CODE, HttpStatus.BAD_REQUEST);
302                                m.addAttribute(JsonErrorView.ERROR_MESSAGE, "Can not create a client with private key authentication without registering a key via the JWK Set URI or JWK Set Value.");
303                                return JsonErrorView.VIEWNAME;
304                        }
305
306                        // otherwise we shouldn't have a secret for this client
307                        client.setClientSecret(null);
308
309                } else {
310
311                        logger.error("unknown auth method");
312                        m.addAttribute(HttpCodeView.CODE, HttpStatus.BAD_REQUEST);
313                        m.addAttribute(JsonErrorView.ERROR_MESSAGE, "Unknown auth method requested");
314                        return JsonErrorView.VIEWNAME;
315
316
317                }
318
319                client.setDynamicallyRegistered(false);
320
321                try {
322                        ClientDetailsEntity newClient = clientService.saveNewClient(client);
323                        m.addAttribute(JsonEntityView.ENTITY, newClient);
324
325                        if (AuthenticationUtilities.isAdmin(auth)) {
326                                return ClientEntityViewForAdmins.VIEWNAME;
327                        } else {
328                                return ClientEntityViewForUsers.VIEWNAME;
329                        }
330                } catch (IllegalArgumentException e) {
331                        logger.error("Unable to save client: {}", e.getMessage());
332                        m.addAttribute(HttpCodeView.CODE, HttpStatus.BAD_REQUEST);
333                        m.addAttribute(JsonErrorView.ERROR_MESSAGE, "Unable to save client: " + e.getMessage());
334                        return JsonErrorView.VIEWNAME;
335                } catch (PersistenceException e) {
336                        Throwable cause = e.getCause();
337                        if (cause instanceof DatabaseException) {
338                                Throwable databaseExceptionCause = cause.getCause();
339                                if(databaseExceptionCause instanceof SQLIntegrityConstraintViolationException) {
340                                        logger.error("apiAddClient failed; duplicate client id entry found: {}", client.getClientId());
341                                        m.addAttribute(HttpCodeView.CODE, HttpStatus.CONFLICT);
342                                        m.addAttribute(JsonErrorView.ERROR_MESSAGE, "Unable to save client. Duplicate client id entry found: " + client.getClientId());
343                                        return JsonErrorView.VIEWNAME;
344                                }
345                        }
346                        throw e;
347                }
348        }
349
350        /**
351         * Update an existing client
352         * @param id
353         * @param jsonString
354         * @param m
355         * @param principal
356         * @return
357         */
358        @PreAuthorize("hasRole('ROLE_ADMIN')")
359        @RequestMapping(value="/{id}", method = RequestMethod.PUT, consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
360        public String apiUpdateClient(@PathVariable("id") Long id, @RequestBody String jsonString, Model m, Authentication auth) {
361
362                JsonObject json = null;
363                ClientDetailsEntity client = null;
364
365                try {
366                        // parse the client passed in (from JSON) and fetch the old client from the store
367                        json = parser.parse(jsonString).getAsJsonObject();
368                        client = gson.fromJson(json, ClientDetailsEntity.class);
369                        client = validateSoftwareStatement(client);
370                } catch (JsonSyntaxException e) {
371                        logger.error("apiUpdateClient failed due to JsonSyntaxException", e);
372                        m.addAttribute(HttpCodeView.CODE, HttpStatus.BAD_REQUEST);
373                        m.addAttribute(JsonErrorView.ERROR_MESSAGE, "Could not update client. The server encountered a JSON syntax exception. Contact a system administrator for assistance.");
374                        return JsonErrorView.VIEWNAME;
375                } catch (IllegalStateException e) {
376                        logger.error("apiUpdateClient failed due to IllegalStateException", e);
377                        m.addAttribute(HttpCodeView.CODE, HttpStatus.BAD_REQUEST);
378                        m.addAttribute(JsonErrorView.ERROR_MESSAGE, "Could not update client. The server encountered an IllegalStateException. Refresh and try again - if the problem persists, contact a system administrator for assistance.");
379                        return JsonErrorView.VIEWNAME;
380                } catch (ValidationException e) {
381                        logger.error("apiUpdateClient failed due to ValidationException", e);
382                        m.addAttribute(HttpCodeView.CODE, HttpStatus.BAD_REQUEST);
383                        m.addAttribute(JsonErrorView.ERROR_MESSAGE, "Could not update client. The server encountered a ValidationException.");
384                        return JsonErrorView.VIEWNAME;
385                }
386
387                ClientDetailsEntity oldClient = clientService.getClientById(id);
388
389                if (oldClient == null) {
390                        logger.error("apiUpdateClient failed; client with id " + id + " could not be found.");
391                        m.addAttribute(HttpCodeView.CODE, HttpStatus.NOT_FOUND);
392                        m.addAttribute(JsonErrorView.ERROR_MESSAGE, "Could not update client. The requested client with id " + id + "could not be found.");
393                        return JsonErrorView.VIEWNAME;
394                }
395
396                // if they leave the client identifier empty, force it to be generated
397                if (Strings.isNullOrEmpty(client.getClientId())) {
398                        client = clientService.generateClientId(client);
399                }
400
401                if (client.getTokenEndpointAuthMethod() == null ||
402                                client.getTokenEndpointAuthMethod().equals(AuthMethod.NONE)) {
403                        // we shouldn't have a secret for this client
404
405                        client.setClientSecret(null);
406
407                } else if (client.getTokenEndpointAuthMethod().equals(AuthMethod.SECRET_BASIC)
408                                || client.getTokenEndpointAuthMethod().equals(AuthMethod.SECRET_POST)
409                                || client.getTokenEndpointAuthMethod().equals(AuthMethod.SECRET_JWT)) {
410
411                        // if they've asked for us to generate a client secret (or they left it blank but require one), do so here
412                        if (json.has("generateClientSecret") && json.get("generateClientSecret").getAsBoolean()
413                                        || Strings.isNullOrEmpty(client.getClientSecret())) {
414                                client = clientService.generateClientSecret(client);
415                        }
416
417                } else if (client.getTokenEndpointAuthMethod().equals(AuthMethod.PRIVATE_KEY)) {
418
419                        if (Strings.isNullOrEmpty(client.getJwksUri()) && client.getJwks() == null) {
420                                logger.error("tried to create client with private key auth but no private key");
421                                m.addAttribute(HttpCodeView.CODE, HttpStatus.BAD_REQUEST);
422                                m.addAttribute(JsonErrorView.ERROR_MESSAGE, "Can not create a client with private key authentication without registering a key via the JWK Set URI or JWK Set Value.");
423                                return JsonErrorView.VIEWNAME;
424                        }
425
426                        // otherwise we shouldn't have a secret for this client
427                        client.setClientSecret(null);
428
429                } else {
430
431                        logger.error("unknown auth method");
432                        m.addAttribute(HttpCodeView.CODE, HttpStatus.BAD_REQUEST);
433                        m.addAttribute(JsonErrorView.ERROR_MESSAGE, "Unknown auth method requested");
434                        return JsonErrorView.VIEWNAME;
435
436
437                }
438
439                try {
440                        ClientDetailsEntity newClient = clientService.updateClient(oldClient, client);
441                        m.addAttribute(JsonEntityView.ENTITY, newClient);
442
443                        if (AuthenticationUtilities.isAdmin(auth)) {
444                                return ClientEntityViewForAdmins.VIEWNAME;
445                        } else {
446                                return ClientEntityViewForUsers.VIEWNAME;
447                        }
448                } catch (IllegalArgumentException e) {
449                        logger.error("Unable to save client: {}", e.getMessage());
450                        m.addAttribute(HttpCodeView.CODE, HttpStatus.BAD_REQUEST);
451                        m.addAttribute(JsonErrorView.ERROR_MESSAGE, "Unable to save client: " + e.getMessage());
452                        return JsonErrorView.VIEWNAME;
453                }
454        }
455
456        /**
457         * Delete a client
458         * @param id
459         * @param modelAndView
460         * @return
461         */
462        @PreAuthorize("hasRole('ROLE_ADMIN')")
463        @RequestMapping(value="/{id}", method=RequestMethod.DELETE)
464        public String apiDeleteClient(@PathVariable("id") Long id, ModelAndView modelAndView) {
465
466                ClientDetailsEntity client = clientService.getClientById(id);
467
468                if (client == null) {
469                        logger.error("apiDeleteClient failed; client with id " + id + " could not be found.");
470                        modelAndView.getModelMap().put(HttpCodeView.CODE, HttpStatus.NOT_FOUND);
471                        modelAndView.getModelMap().put(JsonErrorView.ERROR_MESSAGE, "Could not delete client. The requested client with id " + id + "could not be found.");
472                        return JsonErrorView.VIEWNAME;
473                } else {
474                        modelAndView.getModelMap().put(HttpCodeView.CODE, HttpStatus.OK);
475                        clientService.deleteClient(client);
476                }
477
478                return HttpCodeView.VIEWNAME;
479        }
480
481
482        /**
483         * Get an individual client
484         * @param id
485         * @param modelAndView
486         * @return
487         */
488        @RequestMapping(value="/{id}", method=RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
489        public String apiShowClient(@PathVariable("id") Long id, Model model, Authentication auth) {
490
491                ClientDetailsEntity client = clientService.getClientById(id);
492
493                if (client == null) {
494                        logger.error("apiShowClient failed; client with id " + id + " could not be found.");
495                        model.addAttribute(HttpCodeView.CODE, HttpStatus.NOT_FOUND);
496                        model.addAttribute(JsonErrorView.ERROR_MESSAGE, "The requested client with id " + id + " could not be found.");
497                        return JsonErrorView.VIEWNAME;
498                }
499
500                model.addAttribute(JsonEntityView.ENTITY, client);
501
502                if (AuthenticationUtilities.isAdmin(auth)) {
503                        return ClientEntityViewForAdmins.VIEWNAME;
504                } else {
505                        return ClientEntityViewForUsers.VIEWNAME;
506                }
507        }
508
509        /**
510         * Get the logo image for a client
511         * @param id
512         */
513        @RequestMapping(value = "/{id}/logo", method=RequestMethod.GET, produces = { MediaType.IMAGE_GIF_VALUE, MediaType.IMAGE_JPEG_VALUE, MediaType.IMAGE_PNG_VALUE })
514        public ResponseEntity<byte[]> getClientLogo(@PathVariable("id") Long id, Model model) {
515
516                ClientDetailsEntity client = clientService.getClientById(id);
517
518                if (client == null) {
519                        return new ResponseEntity<>(HttpStatus.NOT_FOUND);
520                } else if (Strings.isNullOrEmpty(client.getLogoUri())) {
521                        return new ResponseEntity<>(HttpStatus.NOT_FOUND);
522                } else {
523                        // get the image from cache
524                        CachedImage image = clientLogoLoadingService.getLogo(client);
525
526                        HttpHeaders headers = new HttpHeaders();
527                        headers.setContentType(MediaType.parseMediaType(image.getContentType()));
528                        headers.setContentLength(image.getLength());
529
530                        return new ResponseEntity<>(image.getData(), headers, HttpStatus.OK);
531                }
532        }
533
534        private ClientDetailsEntity validateSoftwareStatement(ClientDetailsEntity newClient) throws ValidationException {
535                if (newClient.getSoftwareStatement() != null) {
536                        if (assertionValidator.isValid(newClient.getSoftwareStatement())) {
537                                // we have a software statement and its envelope passed all the checks from our validator
538
539                                // swap out all of the client's fields for the associated parts of the software statement
540                                try {
541                                        JWTClaimsSet claimSet = newClient.getSoftwareStatement().getJWTClaimsSet();
542                                        for (String claim : claimSet.getClaims().keySet()) {
543                                                switch (claim) {
544                                                        case SOFTWARE_STATEMENT:
545                                                                throw new ValidationException("invalid_client_metadata", "Software statement can't include another software statement", HttpStatus.BAD_REQUEST);
546                                                        case CLAIMS_REDIRECT_URIS:
547                                                                newClient.setClaimsRedirectUris(Sets.newHashSet(claimSet.getStringListClaim(claim)));
548                                                                break;
549                                                        case CLIENT_SECRET_EXPIRES_AT:
550                                                                throw new ValidationException("invalid_client_metadata", "Software statement can't include a client secret expiration time", HttpStatus.BAD_REQUEST);
551                                                        case CLIENT_ID_ISSUED_AT:
552                                                                throw new ValidationException("invalid_client_metadata", "Software statement can't include a client ID issuance time", HttpStatus.BAD_REQUEST);
553                                                        case REGISTRATION_CLIENT_URI:
554                                                                throw new ValidationException("invalid_client_metadata", "Software statement can't include a client configuration endpoint", HttpStatus.BAD_REQUEST);
555                                                        case REGISTRATION_ACCESS_TOKEN:
556                                                                throw new ValidationException("invalid_client_metadata", "Software statement can't include a client registration access token", HttpStatus.BAD_REQUEST);
557                                                        case REQUEST_URIS:
558                                                                newClient.setRequestUris(Sets.newHashSet(claimSet.getStringListClaim(claim)));
559                                                                break;
560                                                        case POST_LOGOUT_REDIRECT_URIS:
561                                                                newClient.setPostLogoutRedirectUris(Sets.newHashSet(claimSet.getStringListClaim(claim)));
562                                                                break;
563                                                        case INITIATE_LOGIN_URI:
564                                                                newClient.setInitiateLoginUri(claimSet.getStringClaim(claim));
565                                                                break;
566                                                        case DEFAULT_ACR_VALUES:
567                                                                newClient.setDefaultACRvalues(Sets.newHashSet(claimSet.getStringListClaim(claim)));
568                                                                break;
569                                                        case REQUIRE_AUTH_TIME:
570                                                                newClient.setRequireAuthTime(claimSet.getBooleanClaim(claim));
571                                                                break;
572                                                        case DEFAULT_MAX_AGE:
573                                                                newClient.setDefaultMaxAge(claimSet.getIntegerClaim(claim));
574                                                                break;
575                                                        case TOKEN_ENDPOINT_AUTH_SIGNING_ALG:
576                                                                newClient.setTokenEndpointAuthSigningAlg(JWSAlgorithm.parse(claimSet.getStringClaim(claim)));
577                                                                break;
578                                                        case ID_TOKEN_ENCRYPTED_RESPONSE_ENC:
579                                                                newClient.setIdTokenEncryptedResponseEnc(EncryptionMethod.parse(claimSet.getStringClaim(claim)));
580                                                                break;
581                                                        case ID_TOKEN_ENCRYPTED_RESPONSE_ALG:
582                                                                newClient.setIdTokenEncryptedResponseAlg(JWEAlgorithm.parse(claimSet.getStringClaim(claim)));
583                                                                break;
584                                                        case ID_TOKEN_SIGNED_RESPONSE_ALG:
585                                                                newClient.setIdTokenSignedResponseAlg(JWSAlgorithm.parse(claimSet.getStringClaim(claim)));
586                                                                break;
587                                                        case USERINFO_ENCRYPTED_RESPONSE_ENC:
588                                                                newClient.setUserInfoEncryptedResponseEnc(EncryptionMethod.parse(claimSet.getStringClaim(claim)));
589                                                                break;
590                                                        case USERINFO_ENCRYPTED_RESPONSE_ALG:
591                                                                newClient.setUserInfoEncryptedResponseAlg(JWEAlgorithm.parse(claimSet.getStringClaim(claim)));
592                                                                break;
593                                                        case USERINFO_SIGNED_RESPONSE_ALG:
594                                                                newClient.setUserInfoSignedResponseAlg(JWSAlgorithm.parse(claimSet.getStringClaim(claim)));
595                                                                break;
596                                                        case REQUEST_OBJECT_SIGNING_ALG:
597                                                                newClient.setRequestObjectSigningAlg(JWSAlgorithm.parse(claimSet.getStringClaim(claim)));
598                                                                break;
599                                                        case SUBJECT_TYPE:
600                                                                newClient.setSubjectType(SubjectType.getByValue(claimSet.getStringClaim(claim)));
601                                                                break;
602                                                        case SECTOR_IDENTIFIER_URI:
603                                                                newClient.setSectorIdentifierUri(claimSet.getStringClaim(claim));
604                                                                break;
605                                                        case APPLICATION_TYPE:
606                                                                newClient.setApplicationType(AppType.getByValue(claimSet.getStringClaim(claim)));
607                                                                break;
608                                                        case JWKS_URI:
609                                                                newClient.setJwksUri(claimSet.getStringClaim(claim));
610                                                                break;
611                                                        case JWKS:
612                                                                newClient.setJwks(JWKSet.parse(claimSet.getJSONObjectClaim(claim).toJSONString()));
613                                                                break;
614                                                        case POLICY_URI:
615                                                                newClient.setPolicyUri(claimSet.getStringClaim(claim));
616                                                                break;
617                                                        case RESPONSE_TYPES:
618                                                                newClient.setResponseTypes(Sets.newHashSet(claimSet.getStringListClaim(claim)));
619                                                                break;
620                                                        case GRANT_TYPES:
621                                                                newClient.setGrantTypes(Sets.newHashSet(claimSet.getStringListClaim(claim)));
622                                                                break;
623                                                        case SCOPE:
624                                                                newClient.setScope(OAuth2Utils.parseParameterList(claimSet.getStringClaim(claim)));
625                                                                break;
626                                                        case TOKEN_ENDPOINT_AUTH_METHOD:
627                                                                newClient.setTokenEndpointAuthMethod(AuthMethod.getByValue(claimSet.getStringClaim(claim)));
628                                                                break;
629                                                        case TOS_URI:
630                                                                newClient.setTosUri(claimSet.getStringClaim(claim));
631                                                                break;
632                                                        case CONTACTS:
633                                                                newClient.setContacts(Sets.newHashSet(claimSet.getStringListClaim(claim)));
634                                                                break;
635                                                        case LOGO_URI:
636                                                                newClient.setLogoUri(claimSet.getStringClaim(claim));
637                                                                break;
638                                                        case CLIENT_URI:
639                                                                newClient.setClientUri(claimSet.getStringClaim(claim));
640                                                                break;
641                                                        case CLIENT_NAME:
642                                                                newClient.setClientName(claimSet.getStringClaim(claim));
643                                                                break;
644                                                        case REDIRECT_URIS:
645                                                                newClient.setRedirectUris(Sets.newHashSet(claimSet.getStringListClaim(claim)));
646                                                                break;
647                                                        case CLIENT_SECRET:
648                                                                throw new ValidationException("invalid_client_metadata", "Software statement can't contain client secret", HttpStatus.BAD_REQUEST);
649                                                        case CLIENT_ID:
650                                                                throw new ValidationException("invalid_client_metadata", "Software statement can't contain client ID", HttpStatus.BAD_REQUEST);
651
652                                                        default:
653                                                                logger.warn("Software statement contained unknown field: " + claim + " with value " + claimSet.getClaim(claim));
654                                                                break;
655                                                }
656                                        }
657
658                                        return newClient;
659                                } catch (ParseException e) {
660                                        throw new ValidationException("invalid_client_metadata", "Software statement claims didn't parse", HttpStatus.BAD_REQUEST);
661                                }
662                        } else {
663                                throw new ValidationException("invalid_client_metadata", "Software statement rejected by validator", HttpStatus.BAD_REQUEST);
664                        }
665                } else {
666                        // nothing to see here, carry on
667                        return newClient;
668                }
669
670        }
671
672}