package org.apereo.cas.support.rest; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectWriter; import org.apereo.cas.CasProtocolConstants; import org.apereo.cas.CentralAuthenticationService; import org.apereo.cas.authentication.AuthenticationException; import org.apereo.cas.authentication.AuthenticationResult; import org.apereo.cas.authentication.AuthenticationResultBuilder; import org.apereo.cas.authentication.AuthenticationSystemSupport; import org.apereo.cas.authentication.Credential; import org.apereo.cas.authentication.DefaultAuthenticationResultBuilder; import org.apereo.cas.authentication.principal.Service; import org.apereo.cas.authentication.principal.ServiceFactory; import org.apereo.cas.ticket.InvalidTicketException; import org.apereo.cas.ticket.ServiceTicket; import org.apereo.cas.ticket.TicketGrantingTicket; import org.apereo.cas.ticket.registry.TicketRegistrySupport; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.util.MultiValueMap; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; import javax.servlet.http.HttpServletRequest; import java.net.URI; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.stream.Collectors; /** * {@link RestController} implementation of CAS' REST API. * <p> * This class implements main CAS RESTful resource for vending/deleting TGTs and vending STs: * </p> * <ul> * <li>{@code POST /v1/tickets}</li> * <li>{@code POST /v1/tickets/{TGT-id}}</li> * <li>{@code GET /v1/tickets/{TGT-id}}</li> * <li>{@code DELETE /v1/tickets/{TGT-id}}</li> * </ul> * * @author Dmitriy Kopylenko * @since 4.1.0 */ @RestController("ticketResourceRestController") public class TicketsResource { private static final Logger LOGGER = LoggerFactory.getLogger(TicketsResource.class); private static final String DOCTYPE_AND_TITLE = "<!DOCTYPE HTML PUBLIC \\\"-//IETF//DTD HTML 2.0//EN\\\"><html><head><title>"; private static final String CLOSE_TITLE_AND_OPEN_FORM = "</title></head><body><h1>TGT Created</h1><form action=\""; private static final String TGT_CREATED_TITLE_CONTENT = HttpStatus.CREATED.toString() + ' ' + HttpStatus.CREATED.getReasonPhrase(); private static final String DOCTYPE_AND_OPENING_FORM = DOCTYPE_AND_TITLE + TGT_CREATED_TITLE_CONTENT + CLOSE_TITLE_AND_OPEN_FORM; private static final String REST_OF_THE_FORM_AND_CLOSING_TAGS = "\" method=\"POST\">Service:<input type=\"text\" name=\"service\" value=\"\"><br><input " + "type=\"submit\" value=\"Submit\"></form></body></html>"; private static final int SUCCESSFUL_TGT_CREATED_INITIAL_LENGTH = DOCTYPE_AND_OPENING_FORM.length() + REST_OF_THE_FORM_AND_CLOSING_TAGS.length(); private final CentralAuthenticationService centralAuthenticationService; private final AuthenticationSystemSupport authenticationSystemSupport; private final CredentialFactory credentialFactory; private final ServiceFactory webApplicationServiceFactory; private final TicketRegistrySupport ticketRegistrySupport; private final ObjectWriter jacksonPrettyWriter = new ObjectMapper().findAndRegisterModules().writer().withDefaultPrettyPrinter(); public TicketsResource(final AuthenticationSystemSupport authenticationSystemSupport, final CredentialFactory credentialFactory, final TicketRegistrySupport ticketRegistrySupport, final ServiceFactory webApplicationServiceFactory, final CentralAuthenticationService centralAuthenticationService) { this.authenticationSystemSupport = authenticationSystemSupport; this.credentialFactory = credentialFactory; this.ticketRegistrySupport = ticketRegistrySupport; this.webApplicationServiceFactory = webApplicationServiceFactory; this.centralAuthenticationService = centralAuthenticationService; } /** * Create new ticket granting ticket. * * @param requestBody username and password application/x-www-form-urlencoded values * @param request raw HttpServletRequest used to call this method * @return ResponseEntity representing RESTful response * @throws JsonProcessingException in case of JSON parsing failure */ @PostMapping(value = "/v1/tickets", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE) public ResponseEntity<String> createTicketGrantingTicket(@RequestBody final MultiValueMap<String, String> requestBody, final HttpServletRequest request) throws JsonProcessingException { try { final Credential credential = this.credentialFactory.fromRequestBody(requestBody); final AuthenticationResult authenticationResult = this.authenticationSystemSupport.handleAndFinalizeSingleAuthenticationTransaction(null, credential); final TicketGrantingTicket tgtId = this.centralAuthenticationService.createTicketGrantingTicket(authenticationResult); final URI ticketReference = new URI(request.getRequestURL().toString() + '/' + tgtId.getId()); final HttpHeaders headers = new HttpHeaders(); headers.setLocation(ticketReference); headers.setContentType(MediaType.TEXT_HTML); final String tgtUrl = ticketReference.toString(); final String response = new StringBuilder(SUCCESSFUL_TGT_CREATED_INITIAL_LENGTH + tgtUrl.length()) .append(DOCTYPE_AND_OPENING_FORM) .append(tgtUrl) .append(REST_OF_THE_FORM_AND_CLOSING_TAGS) .toString(); return new ResponseEntity<>(response, headers, HttpStatus.CREATED); } catch (final AuthenticationException e) { final List<String> authnExceptions = e.getHandlerErrors().values().stream() .map(Class::getSimpleName) .collect(Collectors.toList()); final Map<String, List<String>> errorsMap = new HashMap<>(); errorsMap.put("authentication_exceptions", authnExceptions); LOGGER.error("[{}] Caused by: [{}]", e.getMessage(), authnExceptions, e); try { return new ResponseEntity<>(this.jacksonPrettyWriter.writeValueAsString(errorsMap), HttpStatus.UNAUTHORIZED); } catch (final JsonProcessingException exception) { LOGGER.error(e.getMessage(), e); return new ResponseEntity<>(e.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR); } } catch (final BadRequestException e) { LOGGER.error(e.getMessage(), e); return new ResponseEntity<>(e.getMessage(), HttpStatus.BAD_REQUEST); } catch (final Throwable e) { LOGGER.error(e.getMessage(), e); return new ResponseEntity<>(e.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR); } } /** * Determine the status of a given ticket id, whether it's valid, exists, expired, etc. * * @param id ticket id * @return {@link ResponseEntity} representing RESTful response */ @GetMapping(value = "/v1/tickets/{id:.+}") public ResponseEntity<String> getTicketStatus(@PathVariable("id") final String id) { try { this.centralAuthenticationService.getTicket(id); return new ResponseEntity<>(id, HttpStatus.OK); } catch (final InvalidTicketException e) { return new ResponseEntity<>("Ticket could not be found", HttpStatus.NOT_FOUND); } catch (final Exception e) { LOGGER.error(e.getMessage(), e); return new ResponseEntity<>(e.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR); } } /** * Create new service ticket. * * @param requestBody service application/x-www-form-urlencoded value * @param tgtId ticket granting ticket id URI path param * @return {@link ResponseEntity} representing RESTful response */ @PostMapping(value = "/v1/tickets/{tgtId:.+}", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE) public ResponseEntity<String> createServiceTicket(@RequestBody final MultiValueMap<String, String> requestBody, @PathVariable("tgtId") final String tgtId) { try { final String serviceId = requestBody.getFirst(CasProtocolConstants.PARAMETER_SERVICE); final AuthenticationResultBuilder builder = new DefaultAuthenticationResultBuilder( this.authenticationSystemSupport.getPrincipalElectionStrategy()); final Service service = this.webApplicationServiceFactory.createService(serviceId); final AuthenticationResult authenticationResult = builder.collect(this.ticketRegistrySupport.getAuthenticationFrom(tgtId)).build(service); final ServiceTicket serviceTicketId = this.centralAuthenticationService.grantServiceTicket(tgtId, service, authenticationResult); return new ResponseEntity<>(serviceTicketId.getId(), HttpStatus.OK); } catch (final InvalidTicketException e) { return new ResponseEntity<>("TicketGrantingTicket could not be found", HttpStatus.NOT_FOUND); } catch (final Exception e) { LOGGER.error(e.getMessage(), e); return new ResponseEntity<>(e.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR); } } /** * Destroy ticket granting ticket. * * @param tgtId ticket granting ticket id URI path param * @return {@link ResponseEntity} representing RESTful response. Signals * {@link HttpStatus#OK} when successful. */ @DeleteMapping(value = "/v1/tickets/{tgtId:.+}") public ResponseEntity<String> deleteTicketGrantingTicket(@PathVariable("tgtId") final String tgtId) { this.centralAuthenticationService.destroyTicketGrantingTicket(tgtId); return new ResponseEntity<>(tgtId, HttpStatus.OK); } }