Skip to content

Commit 1798b01

Browse files
committed
user key sign with client credential flow
1 parent 293eb1b commit 1798b01

File tree

12 files changed

+205
-28
lines changed

12 files changed

+205
-28
lines changed

ssh-hostkey-signer/pom.xml

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,18 @@
2020
<groupId>org.springframework.boot</groupId>
2121
<artifactId>spring-boot-starter-parent</artifactId>
2222
<version>3.4.4</version>
23-
<relativePath />
23+
<relativePath/>
2424
</parent>
2525

2626
<dependencies>
2727
<dependency>
2828
<groupId>org.springframework.boot</groupId>
2929
<artifactId>spring-boot-starter</artifactId>
3030
</dependency>
31+
<dependency>
32+
<groupId>org.springframework</groupId>
33+
<artifactId>spring-web</artifactId>
34+
</dependency>
3135
<dependency>
3236
<groupId>org.projectlombok</groupId>
3337
<artifactId>lombok</artifactId>
@@ -37,6 +41,10 @@
3741
<artifactId>ssh-signer-common-lib</artifactId>
3842
<version>${ssh-signer-common-lib.version}</version>
3943
</dependency>
44+
<dependency>
45+
<groupId>com.fasterxml.jackson.core</groupId>
46+
<artifactId>jackson-databind</artifactId>
47+
</dependency>
4048
</dependencies>
4149

4250
<build>
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package io.binarycodes.homelab;
2+
3+
import org.springframework.boot.context.properties.ConfigurationProperties;
4+
5+
@ConfigurationProperties(prefix = "app")
6+
public record ApplicationProperties(String serverUrl,
7+
String keycloakUrl,
8+
String realmName,
9+
String clientId,
10+
String clientSecret,
11+
String grantType,
12+
String tokenUrl) {
13+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package io.binarycodes.homelab;
2+
3+
import com.fasterxml.jackson.core.JsonProcessingException;
4+
import com.fasterxml.jackson.databind.ObjectMapper;
5+
import io.binarycodes.homelab.lib.DeviceFlowToken;
6+
import lombok.extern.log4j.Log4j2;
7+
import org.springframework.beans.factory.annotation.Autowired;
8+
import org.springframework.http.MediaType;
9+
import org.springframework.stereotype.Service;
10+
import org.springframework.util.LinkedMultiValueMap;
11+
import org.springframework.web.client.RestClient;
12+
13+
import java.nio.charset.StandardCharsets;
14+
import java.util.Optional;
15+
16+
@Log4j2
17+
@Service
18+
public class AuthService {
19+
private final ApplicationProperties applicationProperties;
20+
21+
@Autowired
22+
public AuthService(ApplicationProperties applicationProperties) {
23+
this.applicationProperties = applicationProperties;
24+
}
25+
26+
private RestClient getRestClient() {
27+
return RestClient.builder()
28+
.baseUrl(applicationProperties.keycloakUrl())
29+
.build();
30+
}
31+
32+
public Optional<String> fetchAuthToken() {
33+
var paramMap = new LinkedMultiValueMap<>();
34+
paramMap.add("client_id", applicationProperties.clientId());
35+
paramMap.add("client_secret", applicationProperties.clientSecret());
36+
paramMap.add("grant_type", applicationProperties.grantType());
37+
38+
var response = getRestClient().post()
39+
.uri(applicationProperties.tokenUrl())
40+
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
41+
.body(paramMap)
42+
.retrieve()
43+
.onStatus(status -> !status.is2xxSuccessful(), (httpRequest, httpResponse) -> {
44+
var errorResponse = new String(httpResponse.getBody()
45+
.readAllBytes(), StandardCharsets.UTF_8);
46+
log.debug(errorResponse);
47+
})
48+
.toEntity(String.class);
49+
50+
if (!response.getStatusCode()
51+
.is2xxSuccessful()) {
52+
return Optional.empty();
53+
}
54+
55+
try {
56+
var accessTokenResponse = new ObjectMapper().readValue(response.getBody(), DeviceFlowToken.class);
57+
return Optional.ofNullable(accessTokenResponse.getAccessToken());
58+
} catch (JsonProcessingException e) {
59+
log.error(e.getMessage(), e);
60+
}
61+
62+
return Optional.empty();
63+
}
64+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package io.binarycodes.homelab;
2+
3+
import io.binarycodes.homelab.lib.SignPublicKeyRequest;
4+
import io.binarycodes.homelab.lib.SignedPublicKeyDownload;
5+
import lombok.extern.log4j.Log4j2;
6+
import org.springframework.beans.factory.annotation.Autowired;
7+
import org.springframework.http.HttpHeaders;
8+
import org.springframework.http.MediaType;
9+
import org.springframework.stereotype.Service;
10+
import org.springframework.web.client.RestClient;
11+
12+
import java.io.IOException;
13+
import java.nio.file.Files;
14+
import java.nio.file.Path;
15+
16+
@Log4j2
17+
@Service
18+
public class SignerService {
19+
private final ApplicationProperties applicationProperties;
20+
21+
@Autowired
22+
public SignerService(ApplicationProperties applicationProperties) {
23+
this.applicationProperties = applicationProperties;
24+
}
25+
26+
public void signMyKey(String token, String publicKeyFilePath, String principalName) throws IOException {
27+
var absolutePublicKeyPath = Path.of(publicKeyFilePath)
28+
.toAbsolutePath();
29+
30+
var fileName = absolutePublicKeyPath.getFileName()
31+
.toString();
32+
var publicKey = Files.readString(absolutePublicKeyPath);
33+
34+
var signPublicKeyRequest = new SignPublicKeyRequest(fileName, publicKey, principalName);
35+
36+
var restClient = RestClient.builder()
37+
.baseUrl(applicationProperties.serverUrl())
38+
.defaultHeader(HttpHeaders.AUTHORIZATION, "Bearer %s".formatted(token))
39+
.build();
40+
41+
var response = restClient.post()
42+
.uri("/rest/key/hostSign")
43+
.contentType(MediaType.APPLICATION_JSON)
44+
.body(signPublicKeyRequest)
45+
.retrieve()
46+
.toEntity(SignedPublicKeyDownload.class);
47+
48+
var signedPublicKeyDownload = response.getBody();
49+
if (response.getStatusCode()
50+
.is2xxSuccessful() && signedPublicKeyDownload != null) {
51+
52+
var writeToPath = absolutePublicKeyPath.resolveSibling(signedPublicKeyDownload.filename());
53+
Files.writeString(writeToPath, signedPublicKeyDownload.signedKey());
54+
55+
log.info("Key is signed and placed at - " + signedPublicKeyDownload.filename());
56+
} else {
57+
log.error("Error processing request");
58+
log.error(response.getStatusCode()
59+
.getClass());
60+
}
61+
}
62+
63+
}

ssh-hostkey-signer/src/main/java/io/binarycodes/homelab/SpringBootConsoleApplication.java

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,38 @@
44
import org.springframework.boot.CommandLineRunner;
55
import org.springframework.boot.SpringApplication;
66
import org.springframework.boot.autoconfigure.SpringBootApplication;
7+
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
8+
9+
import java.io.IOException;
710

811
@Log4j2
912
@SpringBootApplication
13+
@ConfigurationPropertiesScan
1014
public class SpringBootConsoleApplication implements CommandLineRunner {
15+
private final SignerService signerService;
16+
private final AuthService authService;
17+
18+
public SpringBootConsoleApplication(SignerService signerService, AuthService authService) {
19+
this.signerService = signerService;
20+
this.authService = authService;
21+
}
1122

1223
public static void main(String[] args) {
1324
SpringApplication.run(SpringBootConsoleApplication.class, args);
1425
}
1526

1627
@Override
17-
public void run(String... args) throws Exception {
18-
log.info("started the command line app");
28+
public void run(String... args) {
29+
var tokenResponse = authService.fetchAuthToken();
30+
tokenResponse.ifPresentOrElse(token -> {
31+
try {
32+
signerService.signMyKey(token, args[0], args[1]);
33+
} catch (IOException e) {
34+
log.error(e.getMessage(), e);
35+
}
36+
}, () -> {
37+
log.error("Error getting auth token.");
38+
});
39+
1940
}
2041
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
APP_SERVER_URL: http://localhost:8080
2+
APP_KEYCLOAK_URL: http://localhost:8090
3+
APP_REALM_NAME: my-test-realm
4+
APP_CLIENT_ID: my-test-client
5+
APP_CLIENT_SECRET: UTRtYkyYN1nbgdPPbBru1FDVsE8ye5JE

ssh-hostkey-signer/src/main/resources/application.yml

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,22 @@ spring:
22
profiles:
33
active: @spring.profiles.active@
44
main:
5+
banner-mode: off
56
web-application-type: NONE
67

78
logging:
89
level:
9-
org: error
10+
io:
11+
binarycodes: error
12+
org:
13+
atmosphere: error
14+
springframework: error
1015

16+
app:
17+
server-url: ${APP_SERVER_URL}
18+
keycloak-url: ${APP_KEYCLOAK_URL}
19+
realm-name: ${APP_REALM_NAME}
20+
client-id: ${APP_CLIENT_ID}
21+
client-secret: ${APP_CLIENT_SECRET}
22+
grant-type: client_credentials
23+
token-url: /realms/${app.realm-name}/protocol/openid-connect/token

ssh-key-signer-server/src/main/resources/application-dev.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
OAUTH_CLIENT: my-test-client
2-
OAUTH_CLIENT_SECRET: fu3hZgzTNHjUIGzZETKuHwQzNT9ptbFG
2+
OAUTH_CLIENT_SECRET: UTRtYkyYN1nbgdPPbBru1FDVsE8ye5JE
33
OAUTH_URL: http://localhost:8090
44
OAUTH_REALM: my-test-realm
55

ssh-userkey-signer/pom.xml

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,12 +40,10 @@
4040
<groupId>org.springframework.boot</groupId>
4141
<artifactId>spring-boot-starter</artifactId>
4242
</dependency>
43-
4443
<dependency>
4544
<groupId>org.springframework</groupId>
4645
<artifactId>spring-web</artifactId>
4746
</dependency>
48-
4947
<dependency>
5048
<groupId>org.projectlombok</groupId>
5149
<artifactId>lombok</artifactId>
@@ -55,12 +53,10 @@
5553
<artifactId>ssh-signer-common-lib</artifactId>
5654
<version>${ssh-signer-common-lib.version}</version>
5755
</dependency>
58-
5956
<dependency>
6057
<groupId>com.fasterxml.jackson.core</groupId>
6158
<artifactId>jackson-databind</artifactId>
6259
</dependency>
63-
6460
<dependency>
6561
<groupId>pro.leaco.qrcode</groupId>
6662
<artifactId>console-qrcode</artifactId>

ssh-userkey-signer/src/main/java/io/binarycodes/homelab/AuthService.java

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,9 @@ public AuthService(ApplicationProperties applicationProperties) {
2929
this.applicationProperties = applicationProperties;
3030
}
3131

32-
33-
public String startDeviceFlowAuth() {
32+
public Optional<String> startDeviceFlowAuth() {
3433
return initiateDeviceFlow().map(this::displayLoginInfo)
35-
.map(this::waitForToken)
36-
.map(token -> token.orElse(""))
37-
.orElse("");
34+
.flatMap(this::waitForToken);
3835
}
3936

4037
private RestClient getRestClient() {
@@ -43,7 +40,6 @@ private RestClient getRestClient() {
4340
.build();
4441
}
4542

46-
4743
private DeviceFlowStartResponse displayLoginInfo(DeviceFlowStartResponse deviceFlowStartResponse) {
4844
System.out.println(deviceFlowStartResponse.getVerificationUriComplete());
4945

@@ -104,7 +100,6 @@ private Optional<String> fetchAuthToken(DeviceFlowStartResponse deviceFlowStartR
104100
return Optional.empty();
105101
}
106102

107-
108103
try {
109104
var accessTokenResponse = new ObjectMapper().readValue(response.getBody(), DeviceFlowToken.class);
110105
return Optional.ofNullable(accessTokenResponse.getAccessToken());
@@ -142,6 +137,4 @@ private Optional<DeviceFlowStartResponse> initiateDeviceFlow() {
142137

143138
return Optional.empty();
144139
}
145-
146-
147140
}

0 commit comments

Comments
 (0)