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.io.UnsupportedEncodingException;
021import java.text.ParseException;
022import java.util.Date;
023import java.util.HashSet;
024import java.util.Set;
025import java.util.concurrent.TimeUnit;
026
027import org.mitre.jwt.assertion.AssertionValidator;
028import org.mitre.oauth2.model.ClientDetailsEntity;
029import org.mitre.oauth2.model.ClientDetailsEntity.AppType;
030import org.mitre.oauth2.model.ClientDetailsEntity.AuthMethod;
031import org.mitre.oauth2.model.ClientDetailsEntity.SubjectType;
032import org.mitre.oauth2.model.OAuth2AccessTokenEntity;
033import org.mitre.oauth2.model.RegisteredClient;
034import org.mitre.oauth2.model.SystemScope;
035import org.mitre.oauth2.service.ClientDetailsEntityService;
036import org.mitre.oauth2.service.OAuth2TokenEntityService;
037import org.mitre.oauth2.service.SystemScopeService;
038import org.mitre.openid.connect.ClientDetailsEntityJsonProcessor;
039import org.mitre.openid.connect.config.ConfigurationPropertiesBean;
040import org.mitre.openid.connect.exception.ValidationException;
041import org.mitre.openid.connect.service.BlacklistedSiteService;
042import org.mitre.openid.connect.service.OIDCTokenService;
043import org.mitre.openid.connect.view.ClientInformationResponseView;
044import org.mitre.openid.connect.view.HttpCodeView;
045import org.mitre.openid.connect.view.JsonErrorView;
046import org.slf4j.Logger;
047import org.slf4j.LoggerFactory;
048import org.springframework.beans.factory.annotation.Autowired;
049import org.springframework.beans.factory.annotation.Qualifier;
050import org.springframework.http.HttpStatus;
051import org.springframework.http.MediaType;
052import org.springframework.security.access.prepost.PreAuthorize;
053import org.springframework.security.oauth2.common.util.OAuth2Utils;
054import org.springframework.security.oauth2.provider.OAuth2Authentication;
055import org.springframework.security.oauth2.provider.authentication.OAuth2AuthenticationDetails;
056import org.springframework.stereotype.Controller;
057import org.springframework.ui.Model;
058import org.springframework.web.bind.annotation.PathVariable;
059import org.springframework.web.bind.annotation.RequestBody;
060import org.springframework.web.bind.annotation.RequestMapping;
061import org.springframework.web.bind.annotation.RequestMethod;
062import org.springframework.web.util.UriUtils;
063
064import com.google.common.base.Strings;
065import com.google.common.collect.ImmutableSet;
066import com.google.common.collect.Sets;
067import com.google.gson.JsonSyntaxException;
068import com.nimbusds.jose.EncryptionMethod;
069import com.nimbusds.jose.JWEAlgorithm;
070import com.nimbusds.jose.JWSAlgorithm;
071import com.nimbusds.jose.jwk.JWKSet;
072import com.nimbusds.jwt.JWTClaimsSet;
073
074import static org.mitre.oauth2.model.RegisteredClientFields.APPLICATION_TYPE;
075import static org.mitre.oauth2.model.RegisteredClientFields.CLAIMS_REDIRECT_URIS;
076import static org.mitre.oauth2.model.RegisteredClientFields.CLIENT_ID;
077import static org.mitre.oauth2.model.RegisteredClientFields.CLIENT_ID_ISSUED_AT;
078import static org.mitre.oauth2.model.RegisteredClientFields.CLIENT_NAME;
079import static org.mitre.oauth2.model.RegisteredClientFields.CLIENT_SECRET;
080import static org.mitre.oauth2.model.RegisteredClientFields.CLIENT_SECRET_EXPIRES_AT;
081import static org.mitre.oauth2.model.RegisteredClientFields.CLIENT_URI;
082import static org.mitre.oauth2.model.RegisteredClientFields.CONTACTS;
083import static org.mitre.oauth2.model.RegisteredClientFields.DEFAULT_ACR_VALUES;
084import static org.mitre.oauth2.model.RegisteredClientFields.DEFAULT_MAX_AGE;
085import static org.mitre.oauth2.model.RegisteredClientFields.GRANT_TYPES;
086import static org.mitre.oauth2.model.RegisteredClientFields.ID_TOKEN_ENCRYPTED_RESPONSE_ALG;
087import static org.mitre.oauth2.model.RegisteredClientFields.ID_TOKEN_ENCRYPTED_RESPONSE_ENC;
088import static org.mitre.oauth2.model.RegisteredClientFields.ID_TOKEN_SIGNED_RESPONSE_ALG;
089import static org.mitre.oauth2.model.RegisteredClientFields.INITIATE_LOGIN_URI;
090import static org.mitre.oauth2.model.RegisteredClientFields.JWKS;
091import static org.mitre.oauth2.model.RegisteredClientFields.JWKS_URI;
092import static org.mitre.oauth2.model.RegisteredClientFields.LOGO_URI;
093import static org.mitre.oauth2.model.RegisteredClientFields.POLICY_URI;
094import static org.mitre.oauth2.model.RegisteredClientFields.POST_LOGOUT_REDIRECT_URIS;
095import static org.mitre.oauth2.model.RegisteredClientFields.REDIRECT_URIS;
096import static org.mitre.oauth2.model.RegisteredClientFields.REGISTRATION_ACCESS_TOKEN;
097import static org.mitre.oauth2.model.RegisteredClientFields.REGISTRATION_CLIENT_URI;
098import static org.mitre.oauth2.model.RegisteredClientFields.REQUEST_OBJECT_SIGNING_ALG;
099import static org.mitre.oauth2.model.RegisteredClientFields.REQUEST_URIS;
100import static org.mitre.oauth2.model.RegisteredClientFields.REQUIRE_AUTH_TIME;
101import static org.mitre.oauth2.model.RegisteredClientFields.RESPONSE_TYPES;
102import static org.mitre.oauth2.model.RegisteredClientFields.SCOPE;
103import static org.mitre.oauth2.model.RegisteredClientFields.SECTOR_IDENTIFIER_URI;
104import static org.mitre.oauth2.model.RegisteredClientFields.SOFTWARE_STATEMENT;
105import static org.mitre.oauth2.model.RegisteredClientFields.SUBJECT_TYPE;
106import static org.mitre.oauth2.model.RegisteredClientFields.TOKEN_ENDPOINT_AUTH_METHOD;
107import static org.mitre.oauth2.model.RegisteredClientFields.TOKEN_ENDPOINT_AUTH_SIGNING_ALG;
108import static org.mitre.oauth2.model.RegisteredClientFields.TOS_URI;
109import static org.mitre.oauth2.model.RegisteredClientFields.USERINFO_ENCRYPTED_RESPONSE_ALG;
110import static org.mitre.oauth2.model.RegisteredClientFields.USERINFO_ENCRYPTED_RESPONSE_ENC;
111import static org.mitre.oauth2.model.RegisteredClientFields.USERINFO_SIGNED_RESPONSE_ALG;
112
113@Controller
114@RequestMapping(value = DynamicClientRegistrationEndpoint.URL)
115public class DynamicClientRegistrationEndpoint {
116
117        public static final String URL = "register";
118
119        @Autowired
120        private ClientDetailsEntityService clientService;
121
122        @Autowired
123        private OAuth2TokenEntityService tokenService;
124
125        @Autowired
126        private SystemScopeService scopeService;
127
128        @Autowired
129        private BlacklistedSiteService blacklistService;
130
131        @Autowired
132        private ConfigurationPropertiesBean config;
133
134        @Autowired
135        private OIDCTokenService connectTokenService;
136
137        @Autowired
138        @Qualifier("clientAssertionValidator")
139        private AssertionValidator assertionValidator;
140
141        /**
142         * Logger for this class
143         */
144        private static final Logger logger = LoggerFactory.getLogger(DynamicClientRegistrationEndpoint.class);
145
146        /**
147         * Create a new Client, issue a client ID, and create a registration access token.
148         * @param jsonString
149         * @param m
150         * @param p
151         * @return
152         */
153        @RequestMapping(method = RequestMethod.POST, consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
154        public String registerNewClient(@RequestBody String jsonString, Model m) {
155
156                ClientDetailsEntity newClient = null;
157                try {
158                        newClient = ClientDetailsEntityJsonProcessor.parse(jsonString);
159                } catch (JsonSyntaxException e) {
160                        // bad parse
161                        // didn't parse, this is a bad request
162                        logger.error("registerNewClient failed; submitted JSON is malformed");
163                        m.addAttribute(HttpCodeView.CODE, HttpStatus.BAD_REQUEST); // http 400
164                        return HttpCodeView.VIEWNAME;
165                }
166
167                if (newClient != null) {
168                        // it parsed!
169
170                        //
171                        // Now do some post-processing consistency checks on it
172                        //
173
174                        // clear out any spurious id/secret (clients don't get to pick)
175                        newClient.setClientId(null);
176                        newClient.setClientSecret(null);
177
178                        // do validation on the fields
179                        try {
180                                newClient = validateSoftwareStatement(newClient); // need to handle the software statement first because it might override requested values
181                                newClient = validateScopes(newClient);
182                                newClient = validateResponseTypes(newClient);
183                                newClient = validateGrantTypes(newClient);
184                                newClient = validateRedirectUris(newClient);
185                                newClient = validateAuth(newClient);
186                        } catch (ValidationException ve) {
187                                // validation failed, return an error
188                                m.addAttribute(JsonErrorView.ERROR, ve.getError());
189                                m.addAttribute(JsonErrorView.ERROR_MESSAGE, ve.getErrorDescription());
190                                m.addAttribute(HttpCodeView.CODE, ve.getStatus());
191                                return JsonErrorView.VIEWNAME;
192                        }
193
194                        if (newClient.getTokenEndpointAuthMethod() == null) {
195                                newClient.setTokenEndpointAuthMethod(AuthMethod.SECRET_BASIC);
196                        }
197
198                        if (newClient.getTokenEndpointAuthMethod() == AuthMethod.SECRET_BASIC ||
199                                        newClient.getTokenEndpointAuthMethod() == AuthMethod.SECRET_JWT ||
200                                        newClient.getTokenEndpointAuthMethod() == AuthMethod.SECRET_POST) {
201
202                                // we need to generate a secret
203                                newClient = clientService.generateClientSecret(newClient);
204                        }
205
206                        // set some defaults for token timeouts
207                        if (config.isHeartMode()) {
208                                // heart mode has different defaults depending on primary grant type
209                                if (newClient.getGrantTypes().contains("authorization_code")) {
210                                        newClient.setAccessTokenValiditySeconds((int)TimeUnit.HOURS.toSeconds(1)); // access tokens good for 1hr
211                                        newClient.setIdTokenValiditySeconds((int)TimeUnit.MINUTES.toSeconds(5)); // id tokens good for 5min
212                                        newClient.setRefreshTokenValiditySeconds((int)TimeUnit.HOURS.toSeconds(24)); // refresh tokens good for 24hr
213                                } else if (newClient.getGrantTypes().contains("implicit")) {
214                                        newClient.setAccessTokenValiditySeconds((int)TimeUnit.MINUTES.toSeconds(15)); // access tokens good for 15min
215                                        newClient.setIdTokenValiditySeconds((int)TimeUnit.MINUTES.toSeconds(5)); // id tokens good for 5min
216                                        newClient.setRefreshTokenValiditySeconds(0); // no refresh tokens
217                                } else if (newClient.getGrantTypes().contains("client_credentials")) {
218                                        newClient.setAccessTokenValiditySeconds((int)TimeUnit.HOURS.toSeconds(6)); // access tokens good for 6hr
219                                        newClient.setIdTokenValiditySeconds(0); // no id tokens
220                                        newClient.setRefreshTokenValiditySeconds(0); // no refresh tokens
221                                }
222                        } else {
223                                newClient.setAccessTokenValiditySeconds((int)TimeUnit.HOURS.toSeconds(1)); // access tokens good for 1hr
224                                newClient.setIdTokenValiditySeconds((int)TimeUnit.MINUTES.toSeconds(10)); // id tokens good for 10min
225                                newClient.setRefreshTokenValiditySeconds(null); // refresh tokens good until revoked
226                        }
227
228                        // this client has been dynamically registered (obviously)
229                        newClient.setDynamicallyRegistered(true);
230
231                        // this client can't do token introspection
232                        newClient.setAllowIntrospection(false);
233
234                        // now save it
235                        try {
236                                ClientDetailsEntity savedClient = clientService.saveNewClient(newClient);
237
238                                // generate the registration access token
239                                OAuth2AccessTokenEntity token = connectTokenService.createRegistrationAccessToken(savedClient);
240                                token = tokenService.saveAccessToken(token);
241
242                                // send it all out to the view
243
244                                RegisteredClient registered = new RegisteredClient(savedClient, token.getValue(), config.getIssuer() + "register/" + UriUtils.encodePathSegment(savedClient.getClientId(), "UTF-8"));
245                                m.addAttribute("client", registered);
246                                m.addAttribute(HttpCodeView.CODE, HttpStatus.CREATED); // http 201
247
248                                return ClientInformationResponseView.VIEWNAME;
249                        } catch (UnsupportedEncodingException e) {
250                                logger.error("Unsupported encoding", e);
251                                m.addAttribute(HttpCodeView.CODE, HttpStatus.INTERNAL_SERVER_ERROR);
252                                return HttpCodeView.VIEWNAME;
253                        } catch (IllegalArgumentException e) {
254                                logger.error("Couldn't save client", e);
255
256                                m.addAttribute(JsonErrorView.ERROR, "invalid_client_metadata");
257                                m.addAttribute(JsonErrorView.ERROR_MESSAGE, "Unable to save client due to invalid or inconsistent metadata.");
258                                m.addAttribute(HttpCodeView.CODE, HttpStatus.BAD_REQUEST); // http 400
259
260                                return JsonErrorView.VIEWNAME;
261                        }
262                } else {
263                        // didn't parse, this is a bad request
264                        logger.error("registerNewClient failed; submitted JSON is malformed");
265                        m.addAttribute(HttpCodeView.CODE, HttpStatus.BAD_REQUEST); // http 400
266
267                        return HttpCodeView.VIEWNAME;
268                }
269
270        }
271
272        /**
273         * Get the meta information for a client.
274         * @param clientId
275         * @param m
276         * @param auth
277         * @return
278         */
279        @PreAuthorize("hasRole('ROLE_CLIENT') and #oauth2.hasScope('" + SystemScopeService.REGISTRATION_TOKEN_SCOPE + "')")
280        @RequestMapping(value = "/{id}", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
281        public String readClientConfiguration(@PathVariable("id") String clientId, Model m, OAuth2Authentication auth) {
282
283                ClientDetailsEntity client = clientService.loadClientByClientId(clientId);
284
285                if (client != null && client.getClientId().equals(auth.getOAuth2Request().getClientId())) {
286
287                        try {
288                                OAuth2AccessTokenEntity token = rotateRegistrationTokenIfNecessary(auth, client);
289                                RegisteredClient registered = new RegisteredClient(client, token.getValue(), config.getIssuer() + "register/" +  UriUtils.encodePathSegment(client.getClientId(), "UTF-8"));
290
291                                // send it all out to the view
292                                m.addAttribute("client", registered);
293                                m.addAttribute(HttpCodeView.CODE, HttpStatus.OK); // http 200
294
295                                return ClientInformationResponseView.VIEWNAME;
296                        } catch (UnsupportedEncodingException e) {
297                                logger.error("Unsupported encoding", e);
298                                m.addAttribute(HttpCodeView.CODE, HttpStatus.INTERNAL_SERVER_ERROR);
299                                return HttpCodeView.VIEWNAME;
300                        }
301
302                } else {
303                        // client mismatch
304                        logger.error("readClientConfiguration failed, client ID mismatch: "
305                                        + clientId + " and " + auth.getOAuth2Request().getClientId() + " do not match.");
306                        m.addAttribute(HttpCodeView.CODE, HttpStatus.FORBIDDEN); // http 403
307
308                        return HttpCodeView.VIEWNAME;
309                }
310        }
311
312        /**
313         * Update the metainformation for a given client.
314         * @param clientId
315         * @param jsonString
316         * @param m
317         * @param auth
318         * @return
319         */
320        @PreAuthorize("hasRole('ROLE_CLIENT') and #oauth2.hasScope('" + SystemScopeService.REGISTRATION_TOKEN_SCOPE + "')")
321        @RequestMapping(value = "/{id}", method = RequestMethod.PUT, produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE)
322        public String updateClient(@PathVariable("id") String clientId, @RequestBody String jsonString, Model m, OAuth2Authentication auth) {
323
324
325                ClientDetailsEntity newClient = null;
326                try {
327                        newClient = ClientDetailsEntityJsonProcessor.parse(jsonString);
328                } catch (JsonSyntaxException e) {
329                        // bad parse
330                        // didn't parse, this is a bad request
331                        logger.error("updateClient failed; submitted JSON is malformed");
332                        m.addAttribute(HttpCodeView.CODE, HttpStatus.BAD_REQUEST); // http 400
333                        return HttpCodeView.VIEWNAME;
334                }
335                ClientDetailsEntity oldClient = clientService.loadClientByClientId(clientId);
336
337                if (newClient != null && oldClient != null  // we have an existing client and the new one parsed
338                                && oldClient.getClientId().equals(auth.getOAuth2Request().getClientId()) // the client passed in the URI matches the one in the auth
339                                && oldClient.getClientId().equals(newClient.getClientId()) // the client passed in the body matches the one in the URI
340                                ) {
341
342                        // a client can't ask to update its own client secret to any particular value
343                        newClient.setClientSecret(oldClient.getClientSecret());
344
345                        // we need to copy over all of the local and SECOAUTH fields
346                        newClient.setAccessTokenValiditySeconds(oldClient.getAccessTokenValiditySeconds());
347                        newClient.setIdTokenValiditySeconds(oldClient.getIdTokenValiditySeconds());
348                        newClient.setRefreshTokenValiditySeconds(oldClient.getRefreshTokenValiditySeconds());
349                        newClient.setDynamicallyRegistered(true); // it's still dynamically registered
350                        newClient.setAllowIntrospection(false); // dynamically registered clients can't do introspection -- use the resource registration instead
351                        newClient.setAuthorities(oldClient.getAuthorities());
352                        newClient.setClientDescription(oldClient.getClientDescription());
353                        newClient.setCreatedAt(oldClient.getCreatedAt());
354                        newClient.setReuseRefreshToken(oldClient.isReuseRefreshToken());
355
356                        // do validation on the fields
357                        try {
358                                newClient = validateSoftwareStatement(newClient); // need to handle the software statement first because it might override requested values
359                                newClient = validateScopes(newClient);
360                                newClient = validateResponseTypes(newClient);
361                                newClient = validateGrantTypes(newClient);
362                                newClient = validateRedirectUris(newClient);
363                                newClient = validateAuth(newClient);
364                        } catch (ValidationException ve) {
365                                // validation failed, return an error
366                                m.addAttribute(JsonErrorView.ERROR, ve.getError());
367                                m.addAttribute(JsonErrorView.ERROR_MESSAGE, ve.getErrorDescription());
368                                m.addAttribute(HttpCodeView.CODE, ve.getStatus());
369                                return JsonErrorView.VIEWNAME;
370                        }
371
372                        try {
373                                // save the client
374                                ClientDetailsEntity savedClient = clientService.updateClient(oldClient, newClient);
375
376                                OAuth2AccessTokenEntity token = rotateRegistrationTokenIfNecessary(auth, savedClient);
377
378                                RegisteredClient registered = new RegisteredClient(savedClient, token.getValue(), config.getIssuer() + "register/" + UriUtils.encodePathSegment(savedClient.getClientId(), "UTF-8"));
379
380                                // send it all out to the view
381                                m.addAttribute("client", registered);
382                                m.addAttribute(HttpCodeView.CODE, HttpStatus.OK); // http 200
383
384                                return ClientInformationResponseView.VIEWNAME;
385                        } catch (UnsupportedEncodingException e) {
386                                logger.error("Unsupported encoding", e);
387                                m.addAttribute(HttpCodeView.CODE, HttpStatus.INTERNAL_SERVER_ERROR);
388                                return HttpCodeView.VIEWNAME;
389                        } catch (IllegalArgumentException e) {
390                                logger.error("Couldn't save client", e);
391
392                                m.addAttribute(JsonErrorView.ERROR, "invalid_client_metadata");
393                                m.addAttribute(JsonErrorView.ERROR_MESSAGE, "Unable to save client due to invalid or inconsistent metadata.");
394                                m.addAttribute(HttpCodeView.CODE, HttpStatus.BAD_REQUEST); // http 400
395
396                                return JsonErrorView.VIEWNAME;
397                        }
398                } else {
399                        // client mismatch
400                        logger.error("updateClient failed, client ID mismatch: "
401                                        + clientId + " and " + auth.getOAuth2Request().getClientId() + " do not match.");
402                        m.addAttribute(HttpCodeView.CODE, HttpStatus.FORBIDDEN); // http 403
403
404                        return HttpCodeView.VIEWNAME;
405                }
406        }
407
408        /**
409         * Delete the indicated client from the system.
410         * @param clientId
411         * @param m
412         * @param auth
413         * @return
414         */
415        @PreAuthorize("hasRole('ROLE_CLIENT') and #oauth2.hasScope('" + SystemScopeService.REGISTRATION_TOKEN_SCOPE + "')")
416        @RequestMapping(value = "/{id}", method = RequestMethod.DELETE, produces = MediaType.APPLICATION_JSON_VALUE)
417        public String deleteClient(@PathVariable("id") String clientId, Model m, OAuth2Authentication auth) {
418
419                ClientDetailsEntity client = clientService.loadClientByClientId(clientId);
420
421                if (client != null && client.getClientId().equals(auth.getOAuth2Request().getClientId())) {
422
423                        clientService.deleteClient(client);
424
425                        m.addAttribute(HttpCodeView.CODE, HttpStatus.NO_CONTENT); // http 204
426
427                        return HttpCodeView.VIEWNAME;
428                } else {
429                        // client mismatch
430                        logger.error("readClientConfiguration failed, client ID mismatch: "
431                                        + clientId + " and " + auth.getOAuth2Request().getClientId() + " do not match.");
432                        m.addAttribute(HttpCodeView.CODE, HttpStatus.FORBIDDEN); // http 403
433
434                        return HttpCodeView.VIEWNAME;
435                }
436        }
437
438        private ClientDetailsEntity validateScopes(ClientDetailsEntity newClient) throws ValidationException {
439                // scopes that the client is asking for
440                Set<SystemScope> requestedScopes = scopeService.fromStrings(newClient.getScope());
441
442                // the scopes that the client can have must be a subset of the dynamically allowed scopes
443                Set<SystemScope> allowedScopes = scopeService.removeRestrictedAndReservedScopes(requestedScopes);
444
445                // if the client didn't ask for any, give them the defaults
446                if (allowedScopes == null || allowedScopes.isEmpty()) {
447                        allowedScopes = scopeService.getDefaults();
448                }
449
450                newClient.setScope(scopeService.toStrings(allowedScopes));
451
452                return newClient;
453        }
454
455        private ClientDetailsEntity validateResponseTypes(ClientDetailsEntity newClient) throws ValidationException {
456                if (newClient.getResponseTypes() == null) {
457                        newClient.setResponseTypes(new HashSet<String>());
458                }
459                return newClient;
460        }
461
462        private ClientDetailsEntity validateGrantTypes(ClientDetailsEntity newClient) throws ValidationException {
463                // set default grant types if needed
464                if (newClient.getGrantTypes() == null || newClient.getGrantTypes().isEmpty()) {
465                        if (newClient.getScope().contains("offline_access")) { // client asked for offline access
466                                newClient.setGrantTypes(Sets.newHashSet("authorization_code", "refresh_token")); // allow authorization code and refresh token grant types by default
467                        } else {
468                                newClient.setGrantTypes(Sets.newHashSet("authorization_code")); // allow authorization code grant type by default
469                        }
470                        if (config.isDualClient()) {
471                                Set<String> extendedGrandTypes = newClient.getGrantTypes();
472                                extendedGrandTypes.add("client_credentials");
473                                newClient.setGrantTypes(extendedGrandTypes);
474                        }
475                }
476
477                // filter out unknown grant types
478                // TODO: make this a pluggable service
479                Set<String> requestedGrantTypes = new HashSet<>(newClient.getGrantTypes());
480                requestedGrantTypes.retainAll(
481                                ImmutableSet.of("authorization_code", "implicit",
482                                                "password", "client_credentials", "refresh_token",
483                                                "urn:ietf:params:oauth:grant_type:redelegate"));
484
485                // don't allow "password" grant type for dynamic registration
486                if (newClient.getGrantTypes().contains("password")) {
487                        // return an error, you can't dynamically register for the password grant
488                        throw new ValidationException("invalid_client_metadata", "The password grant type is not allowed in dynamic registration on this server.", HttpStatus.BAD_REQUEST);
489                }
490
491                // don't allow clients to have multiple incompatible grant types and scopes
492                if (newClient.getGrantTypes().contains("authorization_code")) {
493
494                        // check for incompatible grants
495                        if (newClient.getGrantTypes().contains("implicit") ||
496                                        (!config.isDualClient() && newClient.getGrantTypes().contains("client_credentials"))) {
497                                // return an error, you can't have these grant types together
498                                throw new ValidationException("invalid_client_metadata", "Incompatible grant types requested: " + newClient.getGrantTypes(), HttpStatus.BAD_REQUEST);
499                        }
500
501                        if (newClient.getResponseTypes().contains("token")) {
502                                // return an error, you can't have this grant type and response type together
503                                throw new ValidationException("invalid_client_metadata", "Incompatible response types requested: " + newClient.getGrantTypes() + " / " + newClient.getResponseTypes(), HttpStatus.BAD_REQUEST);
504                        }
505
506                        newClient.getResponseTypes().add("code");
507                }
508
509                if (newClient.getGrantTypes().contains("implicit")) {
510
511                        // check for incompatible grants
512                        if (newClient.getGrantTypes().contains("authorization_code") ||
513                                        (!config.isDualClient() && newClient.getGrantTypes().contains("client_credentials"))) {
514                                // return an error, you can't have these grant types together
515                                throw new ValidationException("invalid_client_metadata", "Incompatible grant types requested: " + newClient.getGrantTypes(), HttpStatus.BAD_REQUEST);
516                        }
517
518                        if (newClient.getResponseTypes().contains("code")) {
519                                // return an error, you can't have this grant type and response type together
520                                throw new ValidationException("invalid_client_metadata", "Incompatible response types requested: " + newClient.getGrantTypes() + " / " + newClient.getResponseTypes(), HttpStatus.BAD_REQUEST);
521                        }
522
523                        newClient.getResponseTypes().add("token");
524
525                        // don't allow refresh tokens in implicit clients
526                        newClient.getGrantTypes().remove("refresh_token");
527                        newClient.getScope().remove(SystemScopeService.OFFLINE_ACCESS);
528                }
529
530                if (newClient.getGrantTypes().contains("client_credentials")) {
531
532                        // check for incompatible grants
533                        if (!config.isDualClient() &&
534                                        (newClient.getGrantTypes().contains("authorization_code") || newClient.getGrantTypes().contains("implicit"))) {
535                                // return an error, you can't have these grant types together
536                                throw new ValidationException("invalid_client_metadata", "Incompatible grant types requested: " + newClient.getGrantTypes(), HttpStatus.BAD_REQUEST);
537                        }
538
539                        if (!newClient.getResponseTypes().isEmpty()) {
540                                // return an error, you can't have this grant type and response type together
541                                throw new ValidationException("invalid_client_metadata", "Incompatible response types requested: " + newClient.getGrantTypes() + " / " + newClient.getResponseTypes(), HttpStatus.BAD_REQUEST);
542                        }
543
544                        // don't allow refresh tokens or id tokens in client_credentials clients
545                        newClient.getGrantTypes().remove("refresh_token");
546                        newClient.getScope().remove(SystemScopeService.OFFLINE_ACCESS);
547                        newClient.getScope().remove(SystemScopeService.OPENID_SCOPE);
548                }
549
550                if (newClient.getGrantTypes().isEmpty()) {
551                        // return an error, you need at least one grant type selected
552                        throw new ValidationException("invalid_client_metadata", "Clients must register at least one grant type.", HttpStatus.BAD_REQUEST);
553                }
554                return newClient;
555        }
556
557        private ClientDetailsEntity validateRedirectUris(ClientDetailsEntity newClient) throws ValidationException {
558                // check to make sure this client registered a redirect URI if using a redirect flow
559                if (newClient.getGrantTypes().contains("authorization_code") || newClient.getGrantTypes().contains("implicit")) {
560                        if (newClient.getRedirectUris() == null || newClient.getRedirectUris().isEmpty()) {
561                                // return an error
562                                throw new ValidationException("invalid_redirect_uri", "Clients using a redirect-based grant type must register at least one redirect URI.", HttpStatus.BAD_REQUEST);
563                        }
564
565                        for (String uri : newClient.getRedirectUris()) {
566                                if (blacklistService.isBlacklisted(uri)) {
567                                        // return an error
568                                        throw new ValidationException("invalid_redirect_uri", "Redirect URI is not allowed: " + uri, HttpStatus.BAD_REQUEST);
569                                }
570
571                                if (uri.contains("#")) {
572                                        // if it contains the hash symbol then it has a fragment, which isn't allowed
573                                        throw new ValidationException("invalid_redirect_uri", "Redirect URI can not have a fragment", HttpStatus.BAD_REQUEST);
574                                }
575                        }
576                }
577
578                return newClient;
579        }
580
581        private ClientDetailsEntity validateAuth(ClientDetailsEntity newClient) throws ValidationException {
582                if (newClient.getTokenEndpointAuthMethod() == null) {
583                        newClient.setTokenEndpointAuthMethod(AuthMethod.SECRET_BASIC);
584                }
585
586                if (newClient.getTokenEndpointAuthMethod() == AuthMethod.SECRET_BASIC ||
587                                newClient.getTokenEndpointAuthMethod() == AuthMethod.SECRET_JWT ||
588                                newClient.getTokenEndpointAuthMethod() == AuthMethod.SECRET_POST) {
589
590                        if (Strings.isNullOrEmpty(newClient.getClientSecret())) {
591                                // no secret yet, we need to generate a secret
592                                newClient = clientService.generateClientSecret(newClient);
593                        }
594                } else if (newClient.getTokenEndpointAuthMethod() == AuthMethod.PRIVATE_KEY) {
595                        if (Strings.isNullOrEmpty(newClient.getJwksUri()) && newClient.getJwks() == null) {
596                                throw new ValidationException("invalid_client_metadata", "JWK Set URI required when using private key authentication", HttpStatus.BAD_REQUEST);
597                        }
598
599                        newClient.setClientSecret(null);
600                } else if (newClient.getTokenEndpointAuthMethod() == AuthMethod.NONE) {
601                        newClient.setClientSecret(null);
602                } else {
603                        throw new ValidationException("invalid_client_metadata", "Unknown authentication method", HttpStatus.BAD_REQUEST);
604                }
605                return newClient;
606        }
607
608
609        /**
610         * @param newClient
611         * @return
612         * @throws ValidationException
613         */
614        private ClientDetailsEntity validateSoftwareStatement(ClientDetailsEntity newClient) throws ValidationException {
615                if (newClient.getSoftwareStatement() != null) {
616                        if (assertionValidator.isValid(newClient.getSoftwareStatement())) {
617                                // we have a software statement and its envelope passed all the checks from our validator
618
619                                // swap out all of the client's fields for the associated parts of the software statement
620                                try {
621                                        JWTClaimsSet claimSet = newClient.getSoftwareStatement().getJWTClaimsSet();
622                                        for (String claim : claimSet.getClaims().keySet()) {
623                                                switch (claim) {
624                                                        case SOFTWARE_STATEMENT:
625                                                                throw new ValidationException("invalid_client_metadata", "Software statement can't include another software statement", HttpStatus.BAD_REQUEST);
626                                                        case CLAIMS_REDIRECT_URIS:
627                                                                newClient.setClaimsRedirectUris(Sets.newHashSet(claimSet.getStringListClaim(claim)));
628                                                                break;
629                                                        case CLIENT_SECRET_EXPIRES_AT:
630                                                                throw new ValidationException("invalid_client_metadata", "Software statement can't include a client secret expiration time", HttpStatus.BAD_REQUEST);
631                                                        case CLIENT_ID_ISSUED_AT:
632                                                                throw new ValidationException("invalid_client_metadata", "Software statement can't include a client ID issuance time", HttpStatus.BAD_REQUEST);
633                                                        case REGISTRATION_CLIENT_URI:
634                                                                throw new ValidationException("invalid_client_metadata", "Software statement can't include a client configuration endpoint", HttpStatus.BAD_REQUEST);
635                                                        case REGISTRATION_ACCESS_TOKEN:
636                                                                throw new ValidationException("invalid_client_metadata", "Software statement can't include a client registration access token", HttpStatus.BAD_REQUEST);
637                                                        case REQUEST_URIS:
638                                                                newClient.setRequestUris(Sets.newHashSet(claimSet.getStringListClaim(claim)));
639                                                                break;
640                                                        case POST_LOGOUT_REDIRECT_URIS:
641                                                                newClient.setPostLogoutRedirectUris(Sets.newHashSet(claimSet.getStringListClaim(claim)));
642                                                                break;
643                                                        case INITIATE_LOGIN_URI:
644                                                                newClient.setInitiateLoginUri(claimSet.getStringClaim(claim));
645                                                                break;
646                                                        case DEFAULT_ACR_VALUES:
647                                                                newClient.setDefaultACRvalues(Sets.newHashSet(claimSet.getStringListClaim(claim)));
648                                                                break;
649                                                        case REQUIRE_AUTH_TIME:
650                                                                newClient.setRequireAuthTime(claimSet.getBooleanClaim(claim));
651                                                                break;
652                                                        case DEFAULT_MAX_AGE:
653                                                                newClient.setDefaultMaxAge(claimSet.getIntegerClaim(claim));
654                                                                break;
655                                                        case TOKEN_ENDPOINT_AUTH_SIGNING_ALG:
656                                                                newClient.setTokenEndpointAuthSigningAlg(JWSAlgorithm.parse(claimSet.getStringClaim(claim)));
657                                                                break;
658                                                        case ID_TOKEN_ENCRYPTED_RESPONSE_ENC:
659                                                                newClient.setIdTokenEncryptedResponseEnc(EncryptionMethod.parse(claimSet.getStringClaim(claim)));
660                                                                break;
661                                                        case ID_TOKEN_ENCRYPTED_RESPONSE_ALG:
662                                                                newClient.setIdTokenEncryptedResponseAlg(JWEAlgorithm.parse(claimSet.getStringClaim(claim)));
663                                                                break;
664                                                        case ID_TOKEN_SIGNED_RESPONSE_ALG:
665                                                                newClient.setIdTokenSignedResponseAlg(JWSAlgorithm.parse(claimSet.getStringClaim(claim)));
666                                                                break;
667                                                        case USERINFO_ENCRYPTED_RESPONSE_ENC:
668                                                                newClient.setUserInfoEncryptedResponseEnc(EncryptionMethod.parse(claimSet.getStringClaim(claim)));
669                                                                break;
670                                                        case USERINFO_ENCRYPTED_RESPONSE_ALG:
671                                                                newClient.setUserInfoEncryptedResponseAlg(JWEAlgorithm.parse(claimSet.getStringClaim(claim)));
672                                                                break;
673                                                        case USERINFO_SIGNED_RESPONSE_ALG:
674                                                                newClient.setUserInfoSignedResponseAlg(JWSAlgorithm.parse(claimSet.getStringClaim(claim)));
675                                                                break;
676                                                        case REQUEST_OBJECT_SIGNING_ALG:
677                                                                newClient.setRequestObjectSigningAlg(JWSAlgorithm.parse(claimSet.getStringClaim(claim)));
678                                                                break;
679                                                        case SUBJECT_TYPE:
680                                                                newClient.setSubjectType(SubjectType.getByValue(claimSet.getStringClaim(claim)));
681                                                                break;
682                                                        case SECTOR_IDENTIFIER_URI:
683                                                                newClient.setSectorIdentifierUri(claimSet.getStringClaim(claim));
684                                                                break;
685                                                        case APPLICATION_TYPE:
686                                                                newClient.setApplicationType(AppType.getByValue(claimSet.getStringClaim(claim)));
687                                                                break;
688                                                        case JWKS_URI:
689                                                                newClient.setJwksUri(claimSet.getStringClaim(claim));
690                                                                break;
691                                                        case JWKS:
692                                                                newClient.setJwks(JWKSet.parse(claimSet.getJSONObjectClaim(claim).toJSONString()));
693                                                                break;
694                                                        case POLICY_URI:
695                                                                newClient.setPolicyUri(claimSet.getStringClaim(claim));
696                                                                break;
697                                                        case RESPONSE_TYPES:
698                                                                newClient.setResponseTypes(Sets.newHashSet(claimSet.getStringListClaim(claim)));
699                                                                break;
700                                                        case GRANT_TYPES:
701                                                                newClient.setGrantTypes(Sets.newHashSet(claimSet.getStringListClaim(claim)));
702                                                                break;
703                                                        case SCOPE:
704                                                                newClient.setScope(OAuth2Utils.parseParameterList(claimSet.getStringClaim(claim)));
705                                                                break;
706                                                        case TOKEN_ENDPOINT_AUTH_METHOD:
707                                                                newClient.setTokenEndpointAuthMethod(AuthMethod.getByValue(claimSet.getStringClaim(claim)));
708                                                                break;
709                                                        case TOS_URI:
710                                                                newClient.setTosUri(claimSet.getStringClaim(claim));
711                                                                break;
712                                                        case CONTACTS:
713                                                                newClient.setContacts(Sets.newHashSet(claimSet.getStringListClaim(claim)));
714                                                                break;
715                                                        case LOGO_URI:
716                                                                newClient.setLogoUri(claimSet.getStringClaim(claim));
717                                                                break;
718                                                        case CLIENT_URI:
719                                                                newClient.setClientUri(claimSet.getStringClaim(claim));
720                                                                break;
721                                                        case CLIENT_NAME:
722                                                                newClient.setClientName(claimSet.getStringClaim(claim));
723                                                                break;
724                                                        case REDIRECT_URIS:
725                                                                newClient.setRedirectUris(Sets.newHashSet(claimSet.getStringListClaim(claim)));
726                                                                break;
727                                                        case CLIENT_SECRET:
728                                                                throw new ValidationException("invalid_client_metadata", "Software statement can't contain client secret", HttpStatus.BAD_REQUEST);
729                                                        case CLIENT_ID:
730                                                                throw new ValidationException("invalid_client_metadata", "Software statement can't contain client ID", HttpStatus.BAD_REQUEST);
731
732                                                        default:
733                                                                logger.warn("Software statement contained unknown field: " + claim + " with value " + claimSet.getClaim(claim));
734                                                                break;
735                                                }
736                                        }
737
738                                        return newClient;
739                                } catch (ParseException e) {
740                                        throw new ValidationException("invalid_client_metadata", "Software statement claims didn't parse", HttpStatus.BAD_REQUEST);
741                                }
742                        } else {
743                                throw new ValidationException("invalid_client_metadata", "Software statement rejected by validator", HttpStatus.BAD_REQUEST);
744                        }
745                } else {
746                        // nothing to see here, carry on
747                        return newClient;
748                }
749
750        }
751
752
753        /*
754         * Rotates the registration token if it's expired, otherwise returns it
755         */
756        private OAuth2AccessTokenEntity rotateRegistrationTokenIfNecessary(OAuth2Authentication auth, ClientDetailsEntity client) {
757
758                OAuth2AuthenticationDetails details = (OAuth2AuthenticationDetails) auth.getDetails();
759                OAuth2AccessTokenEntity token = tokenService.readAccessToken(details.getTokenValue());
760
761                if (config.getRegTokenLifeTime() != null) {
762
763                        try {
764                                // Re-issue the token if it has been issued before [currentTime - validity]
765                                Date validToDate = new Date(System.currentTimeMillis() - config.getRegTokenLifeTime() * 1000);
766                                if(token.getJwt().getJWTClaimsSet().getIssueTime().before(validToDate)) {
767                                        logger.info("Rotating the registration access token for " + client.getClientId());
768                                        tokenService.revokeAccessToken(token);
769                                        OAuth2AccessTokenEntity newToken = connectTokenService.createRegistrationAccessToken(client);
770                                        tokenService.saveAccessToken(newToken);
771                                        return newToken;
772                                } else {
773                                        // it's not expired, keep going
774                                        return token;
775                                }
776                        } catch (ParseException e) {
777                                logger.error("Couldn't parse a known-valid token?", e);
778                                return token;
779                        }
780                } else {
781                        // tokens don't expire, just return it
782                        return token;
783                }
784        }
785
786}