JwtService.java
package edu.ucsb.cs156.frontiers.services;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import edu.ucsb.cs156.frontiers.entities.Course;
import edu.ucsb.cs156.frontiers.errors.NoLinkedOrganizationException;
import io.jsonwebtoken.Jwts;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.interfaces.RSAPrivateKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Base64;
import java.util.Date;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.data.auditing.DateTimeProvider;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
@Service
@Slf4j
public class JwtService {
@Value("${app.private.key:no-key-present}")
private String privateKey;
@Value("${app.client.id:no-client-id}")
private String clientId;
private final RestTemplate restTemplate;
private final ObjectMapper objectMapper;
private final DateTimeProvider dateTimeProvider;
public JwtService(
RestTemplateBuilder restTemplateBuilder,
ObjectMapper objectMapper,
DateTimeProvider dateTimeProvider) {
this.restTemplate = restTemplateBuilder.build();
this.objectMapper = objectMapper;
this.dateTimeProvider = dateTimeProvider;
}
private RSAPrivateKey getPrivateKey() throws NoSuchAlgorithmException, InvalidKeySpecException {
String key = privateKey;
key = key.replace("-----BEGIN PRIVATE KEY-----", "");
key = key.replace("-----END PRIVATE KEY-----", "");
key = key.replaceAll(" ", "");
key = key.replaceAll(System.lineSeparator(), "");
PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(Base64.getDecoder().decode(key.getBytes()));
KeyFactory kf = KeyFactory.getInstance("RSA");
return (RSAPrivateKey) kf.generatePrivate(spec);
}
/**
* 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.
*
* @return Signed JWT that expires in 5 minutes in the form of a String
* @throws InvalidKeySpecException if the key is invalid, the exception will be thrown.
*/
public String getJwt() throws NoSuchAlgorithmException, InvalidKeySpecException {
Instant currentTime = Instant.from(dateTimeProvider.getNow().get());
String token =
Jwts.builder()
.issuedAt(Date.from(currentTime.minus(30, ChronoUnit.SECONDS)))
.expiration(Date.from(currentTime.plus(5, ChronoUnit.MINUTES)))
.issuer(clientId)
.signWith(getPrivateKey(), Jwts.SIG.RS256)
.compact();
return token;
}
/**
* Method to retrieve a token to act as a particular app installation in a particular organization
*
* @param course ID of the particular app installation to act as
* @return Token accepted by GitHub to act as a particular installation.
*/
public String getInstallationToken(Course course)
throws JsonProcessingException,
NoSuchAlgorithmException,
InvalidKeySpecException,
NoLinkedOrganizationException {
if (course.getOrgName() == null || course.getInstallationId() == null) {
throw new NoLinkedOrganizationException(course.getCourseName());
} else {
String token = getJwt();
String ENDPOINT =
"https://api.github.com/app/installations/"
+ course.getInstallationId()
+ "/access_tokens";
HttpHeaders headers = new HttpHeaders();
headers.add("Authorization", "Bearer " + token);
headers.add("Accept", "application/vnd.github+json");
headers.add("X-GitHub-Api-Version", "2022-11-28");
HttpEntity<String> entity = new HttpEntity<>(headers);
ResponseEntity<String> response =
restTemplate.exchange(ENDPOINT, HttpMethod.POST, entity, String.class);
JsonNode responseJson = objectMapper.readTree(response.getBody());
String installationToken = responseJson.get("token").asText();
return installationToken;
}
}
}