001/*******************************************************************************
002 * Copyright 2017 The MIT Internet Trust Consortium
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *   http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 *******************************************************************************/
016package org.mitre.openid.connect.web;
017
018import java.io.UnsupportedEncodingException;
019import java.text.ParseException;
020import java.util.Date;
021import java.util.HashSet;
022import java.util.Set;
023
024import org.mitre.oauth2.model.ClientDetailsEntity;
025import org.mitre.oauth2.model.ClientDetailsEntity.AuthMethod;
026import org.mitre.oauth2.model.OAuth2AccessTokenEntity;
027import org.mitre.oauth2.model.RegisteredClient;
028import org.mitre.oauth2.model.SystemScope;
029import org.mitre.oauth2.service.ClientDetailsEntityService;
030import org.mitre.oauth2.service.OAuth2TokenEntityService;
031import org.mitre.oauth2.service.SystemScopeService;
032import org.mitre.openid.connect.ClientDetailsEntityJsonProcessor;
033import org.mitre.openid.connect.config.ConfigurationPropertiesBean;
034import org.mitre.openid.connect.exception.ValidationException;
035import org.mitre.openid.connect.service.OIDCTokenService;
036import org.mitre.openid.connect.view.ClientInformationResponseView;
037import org.mitre.openid.connect.view.HttpCodeView;
038import org.mitre.openid.connect.view.JsonErrorView;
039import org.slf4j.Logger;
040import org.slf4j.LoggerFactory;
041import org.springframework.beans.factory.annotation.Autowired;
042import org.springframework.http.HttpStatus;
043import org.springframework.http.MediaType;
044import org.springframework.security.access.prepost.PreAuthorize;
045import org.springframework.security.oauth2.provider.OAuth2Authentication;
046import org.springframework.security.oauth2.provider.authentication.OAuth2AuthenticationDetails;
047import org.springframework.stereotype.Controller;
048import org.springframework.ui.Model;
049import org.springframework.web.bind.annotation.PathVariable;
050import org.springframework.web.bind.annotation.RequestBody;
051import org.springframework.web.bind.annotation.RequestMapping;
052import org.springframework.web.bind.annotation.RequestMethod;
053import org.springframework.web.util.UriUtils;
054
055import com.google.common.base.Strings;
056import com.google.gson.JsonSyntaxException;
057
058@Controller
059@RequestMapping(value = ProtectedResourceRegistrationEndpoint.URL)
060public class ProtectedResourceRegistrationEndpoint {
061
062        /**
063         *
064         */
065        public static final String URL = "resource";
066
067        @Autowired
068        private ClientDetailsEntityService clientService;
069
070        @Autowired
071        private OAuth2TokenEntityService tokenService;
072
073        @Autowired
074        private SystemScopeService scopeService;
075
076        @Autowired
077        private ConfigurationPropertiesBean config;
078
079        @Autowired
080        private OIDCTokenService connectTokenService;
081
082        /**
083         * Logger for this class
084         */
085        private static final Logger logger = LoggerFactory.getLogger(ProtectedResourceRegistrationEndpoint.class);
086
087        /**
088         * Create a new Client, issue a client ID, and create a registration access token.
089         * @param jsonString
090         * @param m
091         * @param p
092         * @return
093         */
094        @RequestMapping(method = RequestMethod.POST, consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
095        public String registerNewProtectedResource(@RequestBody String jsonString, Model m) {
096
097                ClientDetailsEntity newClient = null;
098                try {
099                        newClient = ClientDetailsEntityJsonProcessor.parse(jsonString);
100                } catch (JsonSyntaxException e) {
101                        // bad parse
102                        // didn't parse, this is a bad request
103                        logger.error("registerNewProtectedResource failed; submitted JSON is malformed");
104                        m.addAttribute(HttpCodeView.CODE, HttpStatus.BAD_REQUEST); // http 400
105                        return HttpCodeView.VIEWNAME;
106                }
107
108                if (newClient != null) {
109                        // it parsed!
110
111                        //
112                        // Now do some post-processing consistency checks on it
113                        //
114
115                        // clear out any spurious id/secret (clients don't get to pick)
116                        newClient.setClientId(null);
117                        newClient.setClientSecret(null);
118
119                        // do validation on the fields
120                        try {
121                                newClient = validateScopes(newClient);
122                                newClient = validateAuth(newClient);
123                        } catch (ValidationException ve) {
124                                // validation failed, return an error
125                                m.addAttribute(JsonErrorView.ERROR, ve.getError());
126                                m.addAttribute(JsonErrorView.ERROR_MESSAGE, ve.getErrorDescription());
127                                m.addAttribute(HttpCodeView.CODE, ve.getStatus());
128                                return JsonErrorView.VIEWNAME;
129                        }
130
131
132                        // no grant types are allowed
133                        newClient.setGrantTypes(new HashSet<String>());
134                        newClient.setResponseTypes(new HashSet<String>());
135                        newClient.setRedirectUris(new HashSet<String>());
136
137                        // don't issue tokens to this client
138                        newClient.setAccessTokenValiditySeconds(0);
139                        newClient.setIdTokenValiditySeconds(0);
140                        newClient.setRefreshTokenValiditySeconds(0);
141
142                        // clear out unused fields
143                        newClient.setDefaultACRvalues(new HashSet<String>());
144                        newClient.setDefaultMaxAge(null);
145                        newClient.setIdTokenEncryptedResponseAlg(null);
146                        newClient.setIdTokenEncryptedResponseEnc(null);
147                        newClient.setIdTokenSignedResponseAlg(null);
148                        newClient.setInitiateLoginUri(null);
149                        newClient.setPostLogoutRedirectUris(null);
150                        newClient.setRequestObjectSigningAlg(null);
151                        newClient.setRequireAuthTime(null);
152                        newClient.setReuseRefreshToken(false);
153                        newClient.setSectorIdentifierUri(null);
154                        newClient.setSubjectType(null);
155                        newClient.setUserInfoEncryptedResponseAlg(null);
156                        newClient.setUserInfoEncryptedResponseEnc(null);
157                        newClient.setUserInfoSignedResponseAlg(null);
158
159                        // this client has been dynamically registered (obviously)
160                        newClient.setDynamicallyRegistered(true);
161
162                        // this client has access to the introspection endpoint
163                        newClient.setAllowIntrospection(true);
164
165                        // now save it
166                        try {
167                                ClientDetailsEntity savedClient = clientService.saveNewClient(newClient);
168
169                                // generate the registration access token
170                                OAuth2AccessTokenEntity token = connectTokenService.createResourceAccessToken(savedClient);
171                                tokenService.saveAccessToken(token);
172
173                                // send it all out to the view
174
175                                RegisteredClient registered = new RegisteredClient(savedClient, token.getValue(), config.getIssuer() + "resource/" + UriUtils.encodePathSegment(savedClient.getClientId(), "UTF-8"));
176                                m.addAttribute("client", registered);
177                                m.addAttribute(HttpCodeView.CODE, HttpStatus.CREATED); // http 201
178
179                                return ClientInformationResponseView.VIEWNAME;
180                        } catch (UnsupportedEncodingException e) {
181                                logger.error("Unsupported encoding", e);
182                                m.addAttribute(HttpCodeView.CODE, HttpStatus.INTERNAL_SERVER_ERROR);
183                                return HttpCodeView.VIEWNAME;
184                        } catch (IllegalArgumentException e) {
185                                logger.error("Couldn't save client", e);
186
187                                m.addAttribute(JsonErrorView.ERROR, "invalid_client_metadata");
188                                m.addAttribute(JsonErrorView.ERROR_MESSAGE, "Unable to save client due to invalid or inconsistent metadata.");
189                                m.addAttribute(HttpCodeView.CODE, HttpStatus.BAD_REQUEST); // http 400
190
191                                return JsonErrorView.VIEWNAME;
192                        }
193                } else {
194                        // didn't parse, this is a bad request
195                        logger.error("registerNewClient failed; submitted JSON is malformed");
196                        m.addAttribute(HttpCodeView.CODE, HttpStatus.BAD_REQUEST); // http 400
197
198                        return HttpCodeView.VIEWNAME;
199                }
200
201        }
202
203        private ClientDetailsEntity validateScopes(ClientDetailsEntity newClient) throws ValidationException {
204                // scopes that the client is asking for
205                Set<SystemScope> requestedScopes = scopeService.fromStrings(newClient.getScope());
206
207                // the scopes that the client can have must be a subset of the dynamically allowed scopes
208                Set<SystemScope> allowedScopes = scopeService.removeRestrictedAndReservedScopes(requestedScopes);
209
210                // if the client didn't ask for any, give them the defaults
211                if (allowedScopes == null || allowedScopes.isEmpty()) {
212                        allowedScopes = scopeService.getDefaults();
213                }
214
215                newClient.setScope(scopeService.toStrings(allowedScopes));
216
217                return newClient;
218        }
219
220        /**
221         * Get the meta information for a client.
222         * @param clientId
223         * @param m
224         * @param auth
225         * @return
226         */
227        @PreAuthorize("hasRole('ROLE_CLIENT') and #oauth2.hasScope('" + SystemScopeService.RESOURCE_TOKEN_SCOPE + "')")
228        @RequestMapping(value = "/{id}", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
229        public String readResourceConfiguration(@PathVariable("id") String clientId, Model m, OAuth2Authentication auth) {
230
231                ClientDetailsEntity client = clientService.loadClientByClientId(clientId);
232
233                if (client != null && client.getClientId().equals(auth.getOAuth2Request().getClientId())) {
234
235
236
237                        try {
238                                // possibly update the token
239                                OAuth2AccessTokenEntity token = fetchValidRegistrationToken(auth, client);
240
241                                RegisteredClient registered = new RegisteredClient(client, token.getValue(), config.getIssuer() + "resource/" +  UriUtils.encodePathSegment(client.getClientId(), "UTF-8"));
242
243                                // send it all out to the view
244                                m.addAttribute("client", registered);
245                                m.addAttribute(HttpCodeView.CODE, HttpStatus.OK); // http 200
246
247                                return ClientInformationResponseView.VIEWNAME;
248                        } catch (UnsupportedEncodingException e) {
249                                logger.error("Unsupported encoding", e);
250                                m.addAttribute(HttpCodeView.CODE, HttpStatus.INTERNAL_SERVER_ERROR);
251                                return HttpCodeView.VIEWNAME;
252                        }
253                } else {
254                        // client mismatch
255                        logger.error("readResourceConfiguration failed, client ID mismatch: "
256                                        + clientId + " and " + auth.getOAuth2Request().getClientId() + " do not match.");
257                        m.addAttribute(HttpCodeView.CODE, HttpStatus.FORBIDDEN); // http 403
258
259                        return HttpCodeView.VIEWNAME;
260                }
261        }
262
263        /**
264         * Update the metainformation for a given client.
265         * @param clientId
266         * @param jsonString
267         * @param m
268         * @param auth
269         * @return
270         */
271        @PreAuthorize("hasRole('ROLE_CLIENT') and #oauth2.hasScope('" + SystemScopeService.RESOURCE_TOKEN_SCOPE + "')")
272        @RequestMapping(value = "/{id}", method = RequestMethod.PUT, produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE)
273        public String updateProtectedResource(@PathVariable("id") String clientId, @RequestBody String jsonString, Model m, OAuth2Authentication auth) {
274
275
276                ClientDetailsEntity newClient = null;
277                try {
278                        newClient = ClientDetailsEntityJsonProcessor.parse(jsonString);
279                } catch (JsonSyntaxException e) {
280                        // bad parse
281                        // didn't parse, this is a bad request
282                        logger.error("updateProtectedResource failed; submitted JSON is malformed");
283                        m.addAttribute(HttpCodeView.CODE, HttpStatus.BAD_REQUEST); // http 400
284                        return HttpCodeView.VIEWNAME;
285                }
286
287                ClientDetailsEntity oldClient = clientService.loadClientByClientId(clientId);
288
289                if (newClient != null && oldClient != null  // we have an existing client and the new one parsed
290                                && oldClient.getClientId().equals(auth.getOAuth2Request().getClientId()) // the client passed in the URI matches the one in the auth
291                                && oldClient.getClientId().equals(newClient.getClientId()) // the client passed in the body matches the one in the URI
292                                ) {
293
294                        // a client can't ask to update its own client secret to any particular value
295                        newClient.setClientSecret(oldClient.getClientSecret());
296
297                        newClient.setCreatedAt(oldClient.getCreatedAt());
298
299                        // no grant types are allowed
300                        newClient.setGrantTypes(new HashSet<String>());
301                        newClient.setResponseTypes(new HashSet<String>());
302                        newClient.setRedirectUris(new HashSet<String>());
303
304                        // don't issue tokens to this client
305                        newClient.setAccessTokenValiditySeconds(0);
306                        newClient.setIdTokenValiditySeconds(0);
307                        newClient.setRefreshTokenValiditySeconds(0);
308
309                        // clear out unused fields
310                        newClient.setDefaultACRvalues(new HashSet<String>());
311                        newClient.setDefaultMaxAge(null);
312                        newClient.setIdTokenEncryptedResponseAlg(null);
313                        newClient.setIdTokenEncryptedResponseEnc(null);
314                        newClient.setIdTokenSignedResponseAlg(null);
315                        newClient.setInitiateLoginUri(null);
316                        newClient.setPostLogoutRedirectUris(null);
317                        newClient.setRequestObjectSigningAlg(null);
318                        newClient.setRequireAuthTime(null);
319                        newClient.setReuseRefreshToken(false);
320                        newClient.setSectorIdentifierUri(null);
321                        newClient.setSubjectType(null);
322                        newClient.setUserInfoEncryptedResponseAlg(null);
323                        newClient.setUserInfoEncryptedResponseEnc(null);
324                        newClient.setUserInfoSignedResponseAlg(null);
325
326                        // this client has been dynamically registered (obviously)
327                        newClient.setDynamicallyRegistered(true);
328
329                        // this client has access to the introspection endpoint
330                        newClient.setAllowIntrospection(true);
331
332                        // do validation on the fields
333                        try {
334                                newClient = validateScopes(newClient);
335                                newClient = validateAuth(newClient);
336                        } catch (ValidationException ve) {
337                                // validation failed, return an error
338                                m.addAttribute(JsonErrorView.ERROR, ve.getError());
339                                m.addAttribute(JsonErrorView.ERROR_MESSAGE, ve.getErrorDescription());
340                                m.addAttribute(HttpCodeView.CODE, ve.getStatus());
341                                return JsonErrorView.VIEWNAME;
342                        }
343
344
345                        try {
346                                // save the client
347                                ClientDetailsEntity savedClient = clientService.updateClient(oldClient, newClient);
348
349                                // possibly update the token
350                                OAuth2AccessTokenEntity token = fetchValidRegistrationToken(auth, savedClient);
351
352                                RegisteredClient registered = new RegisteredClient(savedClient, token.getValue(), config.getIssuer() + "resource/" + UriUtils.encodePathSegment(savedClient.getClientId(), "UTF-8"));
353
354                                // send it all out to the view
355                                m.addAttribute("client", registered);
356                                m.addAttribute(HttpCodeView.CODE, HttpStatus.OK); // http 200
357
358                                return ClientInformationResponseView.VIEWNAME;
359                        } catch (UnsupportedEncodingException e) {
360                                logger.error("Unsupported encoding", e);
361                                m.addAttribute(HttpCodeView.CODE, HttpStatus.INTERNAL_SERVER_ERROR);
362                                return HttpCodeView.VIEWNAME;
363                        } catch (IllegalArgumentException e) {
364                                logger.error("Couldn't save client", e);
365
366                                m.addAttribute(JsonErrorView.ERROR, "invalid_client_metadata");
367                                m.addAttribute(JsonErrorView.ERROR_MESSAGE, "Unable to save client due to invalid or inconsistent metadata.");
368                                m.addAttribute(HttpCodeView.CODE, HttpStatus.BAD_REQUEST); // http 400
369
370                                return JsonErrorView.VIEWNAME;
371                        }
372                } else {
373                        // client mismatch
374                        logger.error("updateProtectedResource" +
375                                        " failed, client ID mismatch: "
376                                        + clientId + " and " + auth.getOAuth2Request().getClientId() + " do not match.");
377                        m.addAttribute(HttpCodeView.CODE, HttpStatus.FORBIDDEN); // http 403
378
379                        return HttpCodeView.VIEWNAME;
380                }
381        }
382
383        /**
384         * Delete the indicated client from the system.
385         * @param clientId
386         * @param m
387         * @param auth
388         * @return
389         */
390        @PreAuthorize("hasRole('ROLE_CLIENT') and #oauth2.hasScope('" + SystemScopeService.RESOURCE_TOKEN_SCOPE + "')")
391        @RequestMapping(value = "/{id}", method = RequestMethod.DELETE, produces = MediaType.APPLICATION_JSON_VALUE)
392        public String deleteResource(@PathVariable("id") String clientId, Model m, OAuth2Authentication auth) {
393
394                ClientDetailsEntity client = clientService.loadClientByClientId(clientId);
395
396                if (client != null && client.getClientId().equals(auth.getOAuth2Request().getClientId())) {
397
398                        clientService.deleteClient(client);
399
400                        m.addAttribute(HttpCodeView.CODE, HttpStatus.NO_CONTENT); // http 204
401
402                        return HttpCodeView.VIEWNAME;
403                } else {
404                        // client mismatch
405                        logger.error("readClientConfiguration failed, client ID mismatch: "
406                                        + clientId + " and " + auth.getOAuth2Request().getClientId() + " do not match.");
407                        m.addAttribute(HttpCodeView.CODE, HttpStatus.FORBIDDEN); // http 403
408
409                        return HttpCodeView.VIEWNAME;
410                }
411        }
412
413        private ClientDetailsEntity validateAuth(ClientDetailsEntity newClient) throws ValidationException {
414                if (newClient.getTokenEndpointAuthMethod() == null) {
415                        newClient.setTokenEndpointAuthMethod(AuthMethod.SECRET_BASIC);
416                }
417
418                if (newClient.getTokenEndpointAuthMethod() == AuthMethod.SECRET_BASIC ||
419                                newClient.getTokenEndpointAuthMethod() == AuthMethod.SECRET_JWT ||
420                                newClient.getTokenEndpointAuthMethod() == AuthMethod.SECRET_POST) {
421
422                        if (Strings.isNullOrEmpty(newClient.getClientSecret())) {
423                                // no secret yet, we need to generate a secret
424                                newClient = clientService.generateClientSecret(newClient);
425                        }
426                } else if (newClient.getTokenEndpointAuthMethod() == AuthMethod.PRIVATE_KEY) {
427                        if (Strings.isNullOrEmpty(newClient.getJwksUri()) && newClient.getJwks() == null) {
428                                throw new ValidationException("invalid_client_metadata", "JWK Set URI required when using private key authentication", HttpStatus.BAD_REQUEST);
429                        }
430
431                        newClient.setClientSecret(null);
432                } else if (newClient.getTokenEndpointAuthMethod() == AuthMethod.NONE) {
433                        newClient.setClientSecret(null);
434                } else {
435                        throw new ValidationException("invalid_client_metadata", "Unknown authentication method", HttpStatus.BAD_REQUEST);
436                }
437                return newClient;
438        }
439
440        private OAuth2AccessTokenEntity fetchValidRegistrationToken(OAuth2Authentication auth, ClientDetailsEntity client) {
441
442                OAuth2AuthenticationDetails details = (OAuth2AuthenticationDetails) auth.getDetails();
443                OAuth2AccessTokenEntity token = tokenService.readAccessToken(details.getTokenValue());
444
445                if (config.getRegTokenLifeTime() != null) {
446
447                        try {
448                                // Re-issue the token if it has been issued before [currentTime - validity]
449                                Date validToDate = new Date(System.currentTimeMillis() - config.getRegTokenLifeTime() * 1000);
450                                if(token.getJwt().getJWTClaimsSet().getIssueTime().before(validToDate)) {
451                                        logger.info("Rotating the registration access token for " + client.getClientId());
452                                        tokenService.revokeAccessToken(token);
453                                        OAuth2AccessTokenEntity newToken = connectTokenService.createResourceAccessToken(client);
454                                        tokenService.saveAccessToken(newToken);
455                                        return newToken;
456                                } else {
457                                        // it's not expired, keep going
458                                        return token;
459                                }
460                        } catch (ParseException e) {
461                                logger.error("Couldn't parse a known-valid token?", e);
462                                return token;
463                        }
464                } else {
465                        // tokens don't expire, just return it
466                        return token;
467                }
468        }
469
470}