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