| 1 | package edu.ucsb.cs156.frontiers.services; | |
| 2 | ||
| 3 | import com.fasterxml.jackson.core.JsonProcessingException; | |
| 4 | import com.fasterxml.jackson.databind.JsonNode; | |
| 5 | import com.fasterxml.jackson.databind.ObjectMapper; | |
| 6 | import edu.ucsb.cs156.frontiers.entities.Course; | |
| 7 | import edu.ucsb.cs156.frontiers.errors.NoLinkedOrganizationException; | |
| 8 | import io.jsonwebtoken.Jwts; | |
| 9 | import lombok.extern.slf4j.Slf4j; | |
| 10 | import org.springframework.beans.factory.annotation.Value; | |
| 11 | import org.springframework.boot.web.client.RestTemplateBuilder; | |
| 12 | import org.springframework.data.auditing.DateTimeProvider; | |
| 13 | import org.springframework.http.HttpEntity; | |
| 14 | import org.springframework.http.HttpHeaders; | |
| 15 | import org.springframework.http.HttpMethod; | |
| 16 | import org.springframework.http.ResponseEntity; | |
| 17 | import org.springframework.stereotype.Service; | |
| 18 | import org.springframework.web.client.RestTemplate; | |
| 19 | import java.security.KeyFactory; | |
| 20 | import java.security.NoSuchAlgorithmException; | |
| 21 | import java.security.interfaces.RSAPrivateKey; | |
| 22 | import java.security.spec.InvalidKeySpecException; | |
| 23 | import java.security.spec.PKCS8EncodedKeySpec; | |
| 24 | import java.time.Instant; | |
| 25 | import java.time.temporal.ChronoUnit; | |
| 26 | import java.util.Base64; | |
| 27 | import java.util.Date; | |
| 28 | ||
| 29 | @Service | |
| 30 | @Slf4j | |
| 31 | public class JwtService { | |
| 32 |     @Value("${app.private.key:no-key-present}") | |
| 33 |     private String privateKey; | |
| 34 | ||
| 35 |     @Value("${app.client.id:no-client-id}") | |
| 36 |     private String clientId; | |
| 37 | ||
| 38 |     private final RestTemplate restTemplate; | |
| 39 | ||
| 40 |     private final ObjectMapper objectMapper; | |
| 41 | ||
| 42 |     private final DateTimeProvider  dateTimeProvider; | |
| 43 | ||
| 44 |     public JwtService(RestTemplateBuilder restTemplateBuilder,  ObjectMapper objectMapper, DateTimeProvider dateTimeProvider) { | |
| 45 |         this.restTemplate = restTemplateBuilder.build(); | |
| 46 |         this.objectMapper = objectMapper; | |
| 47 |         this.dateTimeProvider = dateTimeProvider; | |
| 48 |     } | |
| 49 | ||
| 50 |     private RSAPrivateKey getPrivateKey() throws NoSuchAlgorithmException, InvalidKeySpecException { | |
| 51 |         String key = privateKey; | |
| 52 |         key = key.replace("-----BEGIN PRIVATE KEY-----", ""); | |
| 53 |         key = key.replace("-----END PRIVATE KEY-----", ""); | |
| 54 |         key = key.replaceAll(" ", ""); | |
| 55 |         key = key.replaceAll(System.lineSeparator(), ""); | |
| 56 |         PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(Base64.getDecoder().decode(key.getBytes())); | |
| 57 |         KeyFactory kf = KeyFactory.getInstance("RSA"); | |
| 58 | 
1
1. getPrivateKey : replaced return value with null for edu/ucsb/cs156/frontiers/services/JwtService::getPrivateKey → KILLED | 
        return (RSAPrivateKey) kf.generatePrivate(spec); | 
| 59 |     } | |
| 60 | ||
| 61 |     /** | |
| 62 |      * Method to retrieve a signed JWT that a service can use to authenticate with GitHub as an app installation without permissions to a specific organization. | |
| 63 |      * @return Signed JWT that expires in 5 minutes in the form of a String | |
| 64 |      * @throws InvalidKeySpecException if the key is invalid, the exception will be thrown. | |
| 65 |      */ | |
| 66 |     public String getJwt() throws NoSuchAlgorithmException, InvalidKeySpecException { | |
| 67 |         Instant currentTime = Instant.from(dateTimeProvider.getNow().get()); | |
| 68 |         String token = Jwts.builder() | |
| 69 |                 .issuedAt(Date.from(currentTime.minus(30, ChronoUnit.SECONDS))) | |
| 70 |                 .expiration(Date.from(currentTime.plus(5, ChronoUnit.MINUTES))) | |
| 71 |                 .issuer(clientId) | |
| 72 |                 .signWith(getPrivateKey(), Jwts.SIG.RS256) | |
| 73 |                 .compact(); | |
| 74 | 
1
1. getJwt : replaced return value with "" for edu/ucsb/cs156/frontiers/services/JwtService::getJwt → KILLED | 
        return token; | 
| 75 |     } | |
| 76 | ||
| 77 |     /** | |
| 78 |      * Method to retrieve a token to act as a particular app installation in a particular organization | |
| 79 |      * | |
| 80 |      * @param course ID of the particular app installation to act as | |
| 81 |      * @return Token accepted by GitHub to act as a particular installation. | |
| 82 |      */ | |
| 83 |     public String getInstallationToken(Course course) throws JsonProcessingException, NoSuchAlgorithmException, InvalidKeySpecException, NoLinkedOrganizationException { | |
| 84 | 
2
1. getInstallationToken : negated conditional → KILLED 2. getInstallationToken : negated conditional → KILLED  | 
        if(course.getOrgName() == null || course.getInstallationId() == null){ | 
| 85 |             throw new NoLinkedOrganizationException(course.getCourseName()); | |
| 86 |         }else { | |
| 87 |             String token = getJwt(); | |
| 88 |             String ENDPOINT = "https://api.github.com/app/installations/" + course.getOrgName() + "/access_tokens"; | |
| 89 |             HttpHeaders headers = new HttpHeaders(); | |
| 90 | 
1
1. getInstallationToken : removed call to org/springframework/http/HttpHeaders::add → KILLED | 
            headers.add("Authorization", "Bearer " + token); | 
| 91 | 
1
1. getInstallationToken : removed call to org/springframework/http/HttpHeaders::add → KILLED | 
            headers.add("Accept", "application/vnd.github+json"); | 
| 92 | 
1
1. getInstallationToken : removed call to org/springframework/http/HttpHeaders::add → KILLED | 
            headers.add("X-GitHub-Api-Version", "2022-11-28"); | 
| 93 |             HttpEntity<String> entity = new HttpEntity<>(headers); | |
| 94 |             ResponseEntity<String> response = restTemplate.exchange(ENDPOINT, HttpMethod.POST, entity, String.class); | |
| 95 |             JsonNode responseJson = objectMapper.readTree(response.getBody()); | |
| 96 |             String installationToken = responseJson.get("token").asText(); | |
| 97 | 
1
1. getInstallationToken : replaced return value with "" for edu/ucsb/cs156/frontiers/services/JwtService::getInstallationToken → KILLED | 
            return installationToken; | 
| 98 |         } | |
| 99 |     } | |
| 100 | } | |
Mutations | ||
| 58 | 
 
 1.1  | 
|
| 74 | 
 
 1.1  | 
|
| 84 | 
 
 1.1 2.2  | 
|
| 90 | 
 
 1.1  | 
|
| 91 | 
 
 1.1  | 
|
| 92 | 
 
 1.1  | 
|
| 97 | 
 
 1.1  |