1616
1717package org .springframework .cloud .config .server .support ;
1818
19+ import java .io .StringReader ;
20+ import java .security .KeyFactory ;
21+ import java .security .PrivateKey ;
22+ import java .security .spec .PKCS8EncodedKeySpec ;
23+ import java .time .Instant ;
24+ import java .time .temporal .ChronoUnit ;
25+ import java .util .Date ;
26+
27+ import com .fasterxml .jackson .databind .ObjectMapper ;
28+ import io .jsonwebtoken .Jwts ;
1929import org .apache .commons .logging .Log ;
2030import org .apache .commons .logging .LogFactory ;
31+ import org .bouncycastle .asn1 .pkcs .PrivateKeyInfo ;
32+ import org .bouncycastle .openssl .PEMKeyPair ;
33+ import org .bouncycastle .openssl .PEMParser ;
2134import org .eclipse .jgit .transport .CredentialsProvider ;
2235import org .eclipse .jgit .transport .UsernamePasswordCredentialsProvider ;
2336
37+ import org .springframework .beans .factory .annotation .Value ;
38+ import org .springframework .http .HttpEntity ;
39+ import org .springframework .http .HttpHeaders ;
40+ import org .springframework .http .HttpMethod ;
41+ import org .springframework .http .ResponseEntity ;
2442import org .springframework .util .ClassUtils ;
43+ import org .springframework .web .client .RestTemplate ;
44+ import org .springframework .web .util .UriComponentsBuilder ;
2545
2646import static org .springframework .util .StringUtils .hasText ;
2747
@@ -44,6 +64,40 @@ public class GitCredentialsProviderFactory {
4464 */
4565 protected boolean awsCodeCommitEnabled = true ;
4666
67+ /**
68+ * If the GitHub App mode should be activated.
69+ */
70+ @ Value ("${spring.cloud.config.server.git.app:false}" )
71+ public boolean isApp ;
72+
73+ /**
74+ * The id of the GitHub app.
75+ */
76+ @ Value ("${spring.cloud.config.server.git.appId:}" )
77+ public String appId ;
78+
79+ /**
80+ * The uri of the GitHub api.
81+ */
82+ @ Value ("${spring.cloud.config.server.git.apiUri:}" )
83+ public String apiUri ;
84+
85+ /**
86+ * The installation id of the GitHub app.
87+ */
88+ @ Value ("${spring.cloud.config.server.git.installationId:}" )
89+ public String installationId ;
90+
91+ /**
92+ * The expiration minutes for the jwt token.
93+ */
94+ @ Value ("${spring.cloud.config.server.git.jwtExpirationMinutes:0}" )
95+ private int jwtExpirationMinutes ;
96+
97+ private String jwtToken ;
98+
99+ private final ObjectMapper mapper = new ObjectMapper ();
100+
47101 /**
48102 * Search for a credential provider that will handle the specified URI. If not found,
49103 * and the username or passphrase has text, then create a default using the provided
@@ -73,6 +127,7 @@ public CredentialsProvider createFor(String uri, String username, String passwor
73127 }
74128 else if (hasText (username ) && password != null ) {
75129 this .logger .debug ("Constructing UsernamePasswordCredentialsProvider for URI " + uri );
130+ password = appToken (username , password );
76131 provider = new UsernamePasswordCredentialsProvider (username , password .toCharArray ());
77132 }
78133 else if (hasText (passphrase )) {
@@ -116,4 +171,90 @@ public void setAwsCodeCommitEnabled(boolean awsCodeCommitEnabled) {
116171 this .awsCodeCommitEnabled = awsCodeCommitEnabled ;
117172 }
118173
174+ /**
175+ * Gets the app token to be used to perform GitHub repository calls with.
176+ *
177+ * @param username the username to authenticate with
178+ * @param password the password to authenticate with
179+ * @return the token to be used as password
180+ */
181+ private String appToken (String username , String password ) {
182+ if (isApp && "x-access-token" .equals (username )) {
183+ this .logger .debug ("Using GitHub App mode for authentication - please ensure that you use the private key as password." );
184+ try {
185+ // if jwtToken is null or expired, generate a new one and set it to the field, otherwise use the existing one
186+ if (jwtToken == null || Instant .parse (mapper .readTree (jwtToken ).get ("expires_at" ).asText ()).isBefore (Instant .now ())) {
187+ PrivateKey privateKey = loadPkcs1PrivateKey (password );
188+ HttpHeaders headers = new HttpHeaders ();
189+ headers .setBearerAuth (generateJwt (appId , privateKey ));
190+ headers .set ("Accept" , "application/vnd.github+json" );
191+ HttpEntity <String > entity = new HttpEntity <>(headers );
192+ RestTemplate restTemplate = new RestTemplate ();
193+ UriComponentsBuilder uriBuilder =
194+ UriComponentsBuilder .fromUriString (apiUri )
195+ .path ("app/installations" )
196+ .pathSegment (installationId )
197+ .path ("access_tokens" );
198+ ResponseEntity <String > response = restTemplate .exchange (
199+ uriBuilder .build ().toUriString (),
200+ HttpMethod .POST ,
201+ entity ,
202+ String .class
203+ );
204+ this .jwtToken = response .getBody ();
205+ }
206+ password = mapper .readTree (jwtToken ).get ("token" ).asText ();
207+ }
208+ catch (Exception e ) {
209+ this .logger .error ("Fehler beim Parsen des JWT Tokens" , e );
210+ }
211+ }
212+ return password ;
213+ }
214+
215+ /**
216+ * Converts the password into a private key.
217+ *
218+ * @param password the password to convert
219+ * @return the PrivateKey
220+ */
221+ private PrivateKey loadPkcs1PrivateKey (String password ) {
222+ try (PEMParser pemParser = new PEMParser (new StringReader (password ))) {
223+ Object object = pemParser .readObject ();
224+ PrivateKeyInfo privateKeyInfo ;
225+ if (object instanceof PEMKeyPair pemKeyPair ) {
226+ privateKeyInfo = pemKeyPair .getPrivateKeyInfo ();
227+ }
228+ else if (object instanceof PrivateKeyInfo privateKeyInfo1 ) {
229+ privateKeyInfo = privateKeyInfo1 ;
230+ }
231+ else {
232+ throw new IllegalArgumentException ("Unknown PEM object: " + object .getClass ());
233+ }
234+ byte [] pkcs8Bytes = privateKeyInfo .getEncoded ();
235+ PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec (pkcs8Bytes );
236+ KeyFactory kf = KeyFactory .getInstance ("RSA" );
237+ return kf .generatePrivate (spec );
238+ }
239+ catch (Exception e ) {
240+ throw new IllegalStateException ("An error occurred while loading the private key" , e );
241+ }
242+ }
243+
244+ /**
245+ * Generates a jwt token based on the appId and the privateKey.
246+ *
247+ * @param appId the id of the GitHub App
248+ * @param privateKey the private key to sign with
249+ * @return the jwt to be used for the api call
250+ */
251+ private String generateJwt (String appId , PrivateKey privateKey ) {
252+ Instant now = Instant .now ();
253+ return Jwts .builder ()
254+ .issuer (appId )
255+ .issuedAt (Date .from (now ))
256+ .expiration (Date .from (now .plus (jwtExpirationMinutes , ChronoUnit .MINUTES )))
257+ .signWith (privateKey )
258+ .compact ();
259+ }
119260}
0 commit comments