package org.apereo.cas.oidc.web.controllers; import org.apache.commons.lang3.StringUtils; import org.apereo.cas.authentication.principal.PrincipalFactory; import org.apereo.cas.authentication.principal.ServiceFactory; import org.apereo.cas.authentication.principal.WebApplicationService; import org.apereo.cas.configuration.CasConfigurationProperties; import org.apereo.cas.oidc.OidcConstants; import org.apereo.cas.oidc.dynareg.OidcClientRegistrationRequest; import org.apereo.cas.oidc.dynareg.OidcClientRegistrationResponse; import org.apereo.cas.services.OidcRegisteredService; import org.apereo.cas.services.ServicesManager; import org.apereo.cas.support.oauth.OAuth20GrantTypes; import org.apereo.cas.support.oauth.OAuth20ResponseTypes; import org.apereo.cas.support.oauth.profile.OAuth20ProfileScopeToAttributesFilter; import org.apereo.cas.support.oauth.validator.OAuth20Validator; import org.apereo.cas.support.oauth.web.endpoints.BaseOAuth20Controller; import org.apereo.cas.ticket.accesstoken.AccessTokenFactory; import org.apereo.cas.ticket.registry.TicketRegistry; import org.apereo.cas.util.gen.RandomStringGenerator; import org.apereo.cas.util.serialization.StringSerializer; import org.apereo.cas.web.support.CookieRetrievingCookieGenerator; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashSet; import java.util.Map; import java.util.Set; import java.util.stream.Collectors; /** * This is {@link OidcDynamicClientRegistrationEndpointController}. * * @author Misagh Moayyed * @since 5.1.0 */ public class OidcDynamicClientRegistrationEndpointController extends BaseOAuth20Controller { private static final Logger LOGGER = LoggerFactory.getLogger(OidcDynamicClientRegistrationEndpointController.class); private final StringSerializer<OidcClientRegistrationRequest> clientRegistrationRequestSerializer; private final RandomStringGenerator clientIdGenerator; private final RandomStringGenerator clientSecretGenerator; public OidcDynamicClientRegistrationEndpointController(final ServicesManager servicesManager, final TicketRegistry ticketRegistry, final OAuth20Validator validator, final AccessTokenFactory accessTokenFactory, final PrincipalFactory principalFactory, final ServiceFactory<WebApplicationService> webApplicationServiceServiceFactory, final StringSerializer<OidcClientRegistrationRequest> clientRegistrationRequestSerializer, final RandomStringGenerator clientIdGenerator, final RandomStringGenerator clientSecretGenerator, final OAuth20ProfileScopeToAttributesFilter scopeToAttributesFilter, final CasConfigurationProperties casProperties, final CookieRetrievingCookieGenerator ticketGrantingTicketCookieGenerator) { super(servicesManager, ticketRegistry, validator, accessTokenFactory, principalFactory, webApplicationServiceServiceFactory, scopeToAttributesFilter, casProperties, ticketGrantingTicketCookieGenerator); this.clientRegistrationRequestSerializer = clientRegistrationRequestSerializer; this.clientIdGenerator = clientIdGenerator; this.clientSecretGenerator = clientSecretGenerator; } /** * Handle request. * * @param jsonInput the json input * @param request the request * @param response the response * @return the model and view * @throws Exception the exception */ @PostMapping(value = '/' + OidcConstants.BASE_OIDC_URL + '/' + OidcConstants.REGISTRATION_URL, consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) public ResponseEntity<OidcClientRegistrationResponse> handleRequestInternal(@RequestBody final String jsonInput, final HttpServletRequest request, final HttpServletResponse response) throws Exception { try { final OidcClientRegistrationRequest registrationRequest = this.clientRegistrationRequestSerializer.from(jsonInput); LOGGER.debug("Received client registration request [{}]", registrationRequest); if (registrationRequest.getScopes().isEmpty()) { throw new Exception("Registration request does not contain any scope values"); } if (!registrationRequest.getScope().contains(OidcConstants.OPENID)) { throw new Exception("Registration request scopes do not contain [{}]" + OidcConstants.OPENID); } final OidcRegisteredService registeredService = new OidcRegisteredService(); registeredService.setName(registrationRequest.getClientName()); if (StringUtils.isNotBlank(registrationRequest.getJwksUri())) { registeredService.setJwks(registrationRequest.getJwksUri()); registeredService.setSignIdToken(true); } final String uri = registrationRequest.getRedirectUris().stream().findFirst().get(); registeredService.setServiceId(uri); registeredService.setClientId(clientIdGenerator.getNewString()); registeredService.setClientSecret(clientSecretGenerator.getNewString()); registeredService.setEvaluationOrder(Integer.MIN_VALUE); final Set<String> supportedScopes = new HashSet<>(casProperties.getAuthn().getOidc().getScopes()); supportedScopes.retainAll(registrationRequest.getScopes()); final OidcClientRegistrationResponse clientResponse = getClientRegistrationResponse(registrationRequest, registeredService); registeredService.setScopes(supportedScopes); final Set<String> processedScopes = new LinkedHashSet<>(supportedScopes); registeredService.setScopes(processedScopes); registeredService.setDescription("Dynamically registered service " .concat(registeredService.getName()) .concat(" with grant types ") .concat(clientResponse.getGrantTypes().stream().collect(Collectors.joining(","))) .concat(" and with scopes ") .concat(registeredService.getScopes().stream().collect(Collectors.joining(","))) .concat(" and response types ") .concat(clientResponse.getResponseTypes().stream().collect(Collectors.joining(",")))); registeredService.setDynamicallyRegistered(true); scopeToAttributesFilter.reconcile(registeredService); return new ResponseEntity<>(clientResponse, HttpStatus.CREATED); } catch (final Exception e) { LOGGER.error(e.getMessage(), e); final Map<String, String> map = new HashMap<>(); map.put("error", "invalid_client_metadata"); map.put("error_message", e.getMessage()); return new ResponseEntity(map, HttpStatus.BAD_REQUEST); } } /** * Gets client registration response. * * @param registrationRequest the registration request * @param registeredService the registered service * @return the client registration response */ protected OidcClientRegistrationResponse getClientRegistrationResponse(final OidcClientRegistrationRequest registrationRequest, final OidcRegisteredService registeredService) { final OidcClientRegistrationResponse clientResponse = new OidcClientRegistrationResponse(); clientResponse.setApplicationType("web"); clientResponse.setClientId(registeredService.getClientId()); clientResponse.setClientSecret(registeredService.getClientSecret()); clientResponse.setSubjectType("public"); clientResponse.setTokenEndpointAuthMethod(registrationRequest.getTokenEndpointAuthMethod()); clientResponse.setClientName(registeredService.getName()); clientResponse.setGrantTypes(Arrays.asList(OAuth20GrantTypes.AUTHORIZATION_CODE.name().toLowerCase(), OAuth20GrantTypes.REFRESH_TOKEN.name().toLowerCase())); clientResponse.setRedirectUris(Collections.singletonList(registeredService.getServiceId())); clientResponse.setResponseTypes(Collections.singletonList(OAuth20ResponseTypes.CODE.name().toLowerCase())); return clientResponse; } }