1616
1717package org .springframework .cloud .config .server .support ;
1818
19+ import com .fasterxml .jackson .databind .ObjectMapper ;
20+ import edu .umd .cs .findbugs .annotations .SuppressFBWarnings ;
21+ import io .jsonwebtoken .Jwts ;
1922import org .apache .commons .logging .Log ;
2023import org .apache .commons .logging .LogFactory ;
24+ import org .bouncycastle .asn1 .pkcs .PrivateKeyInfo ;
25+ import org .bouncycastle .openssl .PEMKeyPair ;
26+ import org .bouncycastle .openssl .PEMParser ;
2127import org .eclipse .jgit .transport .CredentialsProvider ;
2228import org .eclipse .jgit .transport .UsernamePasswordCredentialsProvider ;
23-
29+ import org .springframework .beans .factory .annotation .Value ;
30+ import org .springframework .http .HttpEntity ;
31+ import org .springframework .http .HttpHeaders ;
32+ import org .springframework .http .HttpMethod ;
33+ import org .springframework .http .ResponseEntity ;
2434import org .springframework .util .ClassUtils ;
35+ import org .springframework .web .client .RestTemplate ;
36+ import org .springframework .web .util .UriComponentsBuilder ;
37+
38+ import java .io .StringReader ;
39+ import java .security .KeyFactory ;
40+ import java .security .PrivateKey ;
41+ import java .security .spec .PKCS8EncodedKeySpec ;
42+ import java .time .Instant ;
43+ import java .time .temporal .ChronoUnit ;
44+ import java .util .Date ;
2545
2646import static org .springframework .util .StringUtils .hasText ;
2747
@@ -44,6 +64,25 @@ public class GitCredentialsProviderFactory {
4464 */
4565 protected boolean awsCodeCommitEnabled = true ;
4666
67+ @ Value ("${spring.cloud.config.server.git.app:false}" )
68+ public boolean isApp ;
69+
70+ @ Value ("${spring.cloud.config.server.git.appId:}" )
71+ public String appId ;
72+
73+ @ Value ("${spring.cloud.config.server.git.apiUri:}" )
74+ public String apiUri ;
75+
76+ @ Value ("${spring.cloud.config.server.git.installationId:}" )
77+ public String installationId ;
78+
79+ @ Value ("${spring.cloud.config.server.git.jwtExpirationMinutes:0}" )
80+ private int jwtExpirationMinutes ;
81+
82+ private String jwtToken ;
83+
84+ private final ObjectMapper mapper = new ObjectMapper ();
85+
4786 /**
4887 * Search for a credential provider that will handle the specified URI. If not found,
4988 * and the username or passphrase has text, then create a default using the provided
@@ -73,6 +112,7 @@ public CredentialsProvider createFor(String uri, String username, String passwor
73112 }
74113 else if (hasText (username ) && password != null ) {
75114 this .logger .debug ("Constructing UsernamePasswordCredentialsProvider for URI " + uri );
115+ password = appToken (username , password );
76116 provider = new UsernamePasswordCredentialsProvider (username , password .toCharArray ());
77117 }
78118 else if (hasText (passphrase )) {
@@ -116,4 +156,66 @@ public void setAwsCodeCommitEnabled(boolean awsCodeCommitEnabled) {
116156 this .awsCodeCommitEnabled = awsCodeCommitEnabled ;
117157 }
118158
159+ private String appToken (String username , String password ) {
160+ if (isApp && "x-access-token" .equals (username )) {
161+ try {
162+ // if jwtToken is null or expired, generate a new one and set it to the field, otherwise use the existing one
163+ if (jwtToken == null || Instant .parse (mapper .readTree (jwtToken ).get ("expires_at" ).asText ()).isBefore (Instant .now ())) {
164+ this .logger .debug ("Using app mode" );
165+ PrivateKey privateKey = loadPkcs1PrivateKey (password );
166+ HttpHeaders headers = new HttpHeaders ();
167+ headers .setBearerAuth (generateJwt (appId , privateKey ));
168+ headers .set ("Accept" , "application/vnd.github+json" );
169+ HttpEntity <String > entity = new HttpEntity <>(headers );
170+ RestTemplate restTemplate = new RestTemplate ();
171+ UriComponentsBuilder uriBuilder =
172+ UriComponentsBuilder .fromUriString (apiUri )
173+ .path ("app/installations" )
174+ .pathSegment (installationId )
175+ .path ("access_tokens" );
176+ ResponseEntity <String > response = restTemplate .exchange (
177+ uriBuilder .build ().toUriString (),
178+ HttpMethod .POST ,
179+ entity ,
180+ String .class
181+ );
182+ this .jwtToken = response .getBody ();
183+ }
184+ password = mapper .readTree (jwtToken ).get ("token" ).asText ();
185+ } catch (Exception e ) {
186+ this .logger .error ("Fehler beim Parsen des JWT Tokens" , e );
187+ }
188+ }
189+ return password ;
190+ }
191+
192+ private PrivateKey loadPkcs1PrivateKey (String password ) {
193+ try (PEMParser pemParser = new PEMParser (new StringReader (password ))) {
194+ Object object = pemParser .readObject ();
195+ PrivateKeyInfo privateKeyInfo ;
196+ if (object instanceof PEMKeyPair pemKeyPair ) {
197+ privateKeyInfo = pemKeyPair .getPrivateKeyInfo ();
198+ } else if (object instanceof PrivateKeyInfo privateKeyInfo1 ) {
199+ privateKeyInfo = privateKeyInfo1 ;
200+ } else {
201+ throw new IllegalArgumentException ("Unknown PEM object: " + object .getClass ());
202+ }
203+ byte [] pkcs8Bytes = privateKeyInfo .getEncoded ();
204+ PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec (pkcs8Bytes );
205+ KeyFactory kf = KeyFactory .getInstance ("RSA" );
206+ return kf .generatePrivate (spec );
207+ } catch (Exception e ) {
208+ throw new IllegalStateException ("An error occurred while loading the private key" , e );
209+ }
210+ }
211+
212+ private String generateJwt (String appId , PrivateKey privateKey ) {
213+ Instant now = Instant .now ();
214+ return Jwts .builder ()
215+ .issuer (appId )
216+ .issuedAt (Date .from (now ))
217+ .expiration (Date .from (now .plus (jwtExpirationMinutes , ChronoUnit .MINUTES )))
218+ .signWith (privateKey )
219+ .compact ();
220+ }
119221}
0 commit comments