diff --git a/.vscode/demo1/readme.md b/.vscode/demo1/readme.md new file mode 100644 index 000000000..5a9e25bf8 --- /dev/null +++ b/.vscode/demo1/readme.md @@ -0,0 +1,11 @@ +# MicroProfile generated Applications + +MicroProfile Starter has generated 2 MicroProfile applications for you. + +There are 2 projects generated so that the examples for the Rest Client and/or JWT Auth specification are more realistic in the sense that they actually call an endpoint within another service. + +. In the `service-a` directory, you can find an application with the major parts of the application. This can be seen as the 'client'. +. In the `service-b` directory, you can find some endpoints which will be called by code within the client application. This can be seen as the 'backend'. + +Have a look in the `readme.md` file in each directory which describes how each project can be built and run. + diff --git a/.vscode/demo1/service-a/pom.xml b/.vscode/demo1/service-a/pom.xml new file mode 100644 index 000000000..104a6d9a2 --- /dev/null +++ b/.vscode/demo1/service-a/pom.xml @@ -0,0 +1,101 @@ + + + 4.0.0 + Daddys + demo1-service-a + 1.0-SNAPSHOT + war + + 3.5.1 + 1.8 + false + 1.5.0 + 1.7.30 + 1.8 + demo1 + 1.7.30 + + + + org.eclipse.microprofile + microprofile + 4.1 + pom + provided + + + io.vertx + vertx-auth-jwt + 3.9.5 + + + + demo1 + + + + liberty + + true + + + + + maven-war-plugin + 3.3.2 + + false + + + + io.openliberty.tools + liberty-maven-plugin + ${openliberty.maven.version} + + + package-server + package + + create + install-feature + deploy + package + + + target/wlp-package + + + + + runnable + ${final.name} + + ${final.name} + https://server.example.com + / + + + + + + + + io.jaegertracing + jaeger-client + ${jaeger.client.version} + + + org.slf4j + slf4j-api + ${slf4j.api.version} + + + org.slf4j + slf4j-jdk14 + ${slf4j.jdk.version} + + + + + diff --git a/.vscode/demo1/service-a/readme.md b/.vscode/demo1/service-a/readme.md new file mode 100644 index 000000000..fc7a7d093 --- /dev/null +++ b/.vscode/demo1/service-a/readme.md @@ -0,0 +1,55 @@ +# MicroProfile generated Application + +## Introduction + +MicroProfile Starter has generated this MicroProfile application for you. + +The generation of the executable jar file can be performed by issuing the following command + + + mvn clean package + +This will create an executable jar file **demo1.jar** within the _target_ maven folder. This can be started by executing the following command + + java -jar target/demo1.jar + + + +### Liberty Dev Mode + +During development, you can use Liberty's development mode (dev mode) to code while observing and testing your changes on the fly. +With the dev mode, you can code along and watch the change reflected in the running server right away; +unit and integration tests are run on pressing Enter in the command terminal; you can attach a debugger to the running server at any time to step through your code. + + + mvn liberty:dev + + + + + +To launch the test page, open your browser at the following URL + + http://localhost:9080/index.html + + + +## Specification examples + +By default, there is always the creation of a JAX-RS application class to define the path on which the JAX-RS endpoints are available. + +Also, a simple Hello world endpoint is created, have a look at the class **HelloController**. + +More information on MicroProfile can be found [here](https://microprofile.io/) + + +### JWT Auth + +Using the OpenId Connect JWT token to pass authentication and authorization information to the JAX-RS endpoint. Specification [here](https://microprofile.io/project/eclipse/microprofile-rest-client) + +Have a look at the **TestSecureController** class which calls the protected endpoint on the secondary application. +The **ProtectedController** (secondary application) contains the protected endpoint since it contains the _@RolesAllowed_ annotation on the JAX-RS endpoint method. + +The _TestSecureController_ code creates a JWT based on the private key found within the resource directory. +However, any method to send a REST request with an appropriate header will work of course. Please feel free to change this code to your needs. + diff --git a/.vscode/demo1/service-a/src/main/java/Daddys/demo1/Demo1RestApplication.java b/.vscode/demo1/service-a/src/main/java/Daddys/demo1/Demo1RestApplication.java new file mode 100644 index 000000000..dd8336e29 --- /dev/null +++ b/.vscode/demo1/service-a/src/main/java/Daddys/demo1/Demo1RestApplication.java @@ -0,0 +1,11 @@ +package Daddys.demo1; + +import javax.ws.rs.ApplicationPath; +import javax.ws.rs.core.Application; + +/** + * + */ +@ApplicationPath("/data") +public class Demo1RestApplication extends Application { +} diff --git a/.vscode/demo1/service-a/src/main/java/Daddys/demo1/HelloController.java b/.vscode/demo1/service-a/src/main/java/Daddys/demo1/HelloController.java new file mode 100644 index 000000000..884c52a35 --- /dev/null +++ b/.vscode/demo1/service-a/src/main/java/Daddys/demo1/HelloController.java @@ -0,0 +1,18 @@ +package Daddys.demo1; + +import javax.inject.Singleton; +import javax.ws.rs.GET; +import javax.ws.rs.Path; + +/** + * + */ +@Path("/hello") +@Singleton +public class HelloController { + + @GET + public String sayHello() { + return "Hello World"; + } +} diff --git a/.vscode/demo1/service-a/src/main/java/Daddys/demo1/secure/MPJWTToken.java b/.vscode/demo1/service-a/src/main/java/Daddys/demo1/secure/MPJWTToken.java new file mode 100644 index 000000000..655acc212 --- /dev/null +++ b/.vscode/demo1/service-a/src/main/java/Daddys/demo1/secure/MPJWTToken.java @@ -0,0 +1,162 @@ +package Daddys.demo1.secure; + +import io.vertx.core.json.JsonArray; +import io.vertx.core.json.JsonObject; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Represent the MP Auth token + */ + +public class MPJWTToken { + + private String iss; // issuer + private String aud; // audience + private String jti; // Unique identifier + private Long exp; // expiration time + private Long iat; // issued at + private String sub; // subject + private String upn; // value for name in Principal + private String preferredUsername; // value for name in Principal + private List groups = new ArrayList<>(); + /* + "iss": "https://server.example.com", + "aud": "s6BhdRkqt3", + "jti": "a-123", + "exp": 1311281970, + "iat": 1311280970, + "sub": "24400320", + "upn": "jdoe@server.example.com", + "groups": ["red-group", "green-group", "admin-group", "admin"], + */ + + private List roles; + private Map additionalClaims; + + public String getIss() { + return iss; + } + + public void setIss(String iss) { + this.iss = iss; + } + + public String getAud() { + return aud; + } + + public void setAud(String aud) { + this.aud = aud; + } + + public String getJti() { + return jti; + } + + public void setJti(String jti) { + this.jti = jti; + } + + public Long getExp() { + return exp; + } + + public void setExp(Long exp) { + this.exp = exp; + } + + public Long getIat() { + return iat; + } + + public void setIat(Long iat) { + this.iat = iat; + } + + public String getSub() { + return sub; + } + + public void setSub(String sub) { + this.sub = sub; + } + + public String getUpn() { + return upn; + } + + public void setUpn(String upn) { + this.upn = upn; + } + + public String getPreferredUsername() { + return preferredUsername; + } + + public void setPreferredUsername(String preferredUsername) { + this.preferredUsername = preferredUsername; + } + + public List getGroups() { + return groups; + } + + public void setGroups(List groups) { + this.groups = groups; + } + + public List getRoles() { + return roles; + } + + public void setRoles(List roles) { + this.roles = roles; + } + + public Map getAdditionalClaims() { + return additionalClaims; + } + + public void setAdditionalClaims(Map additionalClaims) { + this.additionalClaims = additionalClaims; + } + + public void addAdditionalClaims(String key, String value) { + if (additionalClaims == null) { + additionalClaims = new HashMap<>(); + } + additionalClaims.put(key, value); + } + + public JsonObject toJSONString() { + JsonObject jsonObject = new JsonObject(); + + jsonObject.put("iss", iss); + jsonObject.put("aud", aud); + jsonObject.put("jti", jti); + jsonObject.put("exp", exp / 1000); + jsonObject.put("iat", iat / 1000); + jsonObject.put("sub", sub); + jsonObject.put("upn", upn); + jsonObject.put("preferred_username", preferredUsername); + + if (additionalClaims != null) { + for (Map.Entry entry : additionalClaims.entrySet()) { + jsonObject.put(entry.getKey(), entry.getValue()); + } + } + + JsonArray jsonArray = new JsonArray(); + for (String group : groups) { + jsonArray.add(group); + } + jsonObject.put("groups", jsonArray); + + return jsonObject; + } + +} diff --git a/.vscode/demo1/service-a/src/main/java/Daddys/demo1/secure/TestSecureController.java b/.vscode/demo1/service-a/src/main/java/Daddys/demo1/secure/TestSecureController.java new file mode 100644 index 000000000..e1ccab57a --- /dev/null +++ b/.vscode/demo1/service-a/src/main/java/Daddys/demo1/secure/TestSecureController.java @@ -0,0 +1,86 @@ +package Daddys.demo1.secure; + +import io.vertx.ext.auth.JWTOptions; +import io.vertx.ext.auth.PubSecKeyOptions; +import io.vertx.ext.auth.jwt.JWTAuth; +import io.vertx.ext.auth.jwt.JWTAuthOptions; + +import javax.annotation.PostConstruct; +import javax.enterprise.context.ApplicationScoped; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.client.WebTarget; +import javax.ws.rs.core.Response; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.UUID; + +@Path("/secured") +@ApplicationScoped +public class TestSecureController { + + private String key; + + @PostConstruct + public void init() { + key = readPemFile(); + } + + @GET + @Path("/test") + public String testSecureCall() { + if (key == null) { + throw new WebApplicationException("Unable to read privateKey.pem", 500); + } + String jwt = generateJWT(key); + WebTarget target = ClientBuilder.newClient().target("http://localhost:9081/data/protected"); + Response response = target.request().header("authorization", "Bearer " + jwt).buildGet().invoke(); + return String.format("Claim value within JWT of 'custom-value' : %s", response.readEntity(String.class)); + } + + private static String generateJWT(String key) { + JWTAuth provider = JWTAuth.create(null, new JWTAuthOptions() + .addPubSecKey(new PubSecKeyOptions() + .setAlgorithm("RS256") + .setSecretKey(key) + )); + + MPJWTToken token = new MPJWTToken(); + token.setAud("targetService"); + token.setIss("https://server.example.com"); // Must match the expected issues configuration values + token.setJti(UUID.randomUUID().toString()); + token.setSub("Jessie"); // Sub is required for WildFly Swarm + token.setUpn("Jessie"); + token.setIat(System.currentTimeMillis()); + token.setExp(System.currentTimeMillis() + 30000); // 30 Seconds expiration! + token.addAdditionalClaims("custom-value", "Jessie specific value"); + token.setGroups(Arrays.asList("user", "protected")); + + return provider.generateToken(new io.vertx.core.json.JsonObject().mergeIn(token.toJSONString()), new JWTOptions().setAlgorithm("RS256")); + } + + // NOTE: Expected format is PKCS#8 (BEGIN PRIVATE KEY) NOT PKCS#1 (BEGIN RSA PRIVATE KEY) + // See gencerts.sh + private static String readPemFile() { + StringBuilder sb = new StringBuilder(8192); + try (BufferedReader is = new BufferedReader( + new InputStreamReader( + TestSecureController.class.getResourceAsStream("/privateKey.pem"), StandardCharsets.US_ASCII))) { + String line; + while ((line = is.readLine()) != null) { + if (!line.startsWith("-")) { + sb.append(line); + sb.append('\n'); + } + } + } catch (IOException e) { + e.printStackTrace(); + } + return sb.toString(); + } +} diff --git a/.vscode/demo1/service-a/src/main/liberty/config/server.xml b/.vscode/demo1/service-a/src/main/liberty/config/server.xml new file mode 100644 index 000000000..604ac15a0 --- /dev/null +++ b/.vscode/demo1/service-a/src/main/liberty/config/server.xml @@ -0,0 +1,26 @@ + + + + + microProfile-4.1 + + + + + + + + + + + + + + + + + + + diff --git a/.vscode/demo1/service-a/src/main/resources/.gitkeep b/.vscode/demo1/service-a/src/main/resources/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/.vscode/demo1/service-a/src/main/resources/META-INF/microprofile-config.properties b/.vscode/demo1/service-a/src/main/resources/META-INF/microprofile-config.properties new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/.vscode/demo1/service-a/src/main/resources/META-INF/microprofile-config.properties @@ -0,0 +1 @@ + diff --git a/.vscode/demo1/service-a/src/main/resources/privateKey.pem b/.vscode/demo1/service-a/src/main/resources/privateKey.pem new file mode 100644 index 000000000..326b510f7 --- /dev/null +++ b/.vscode/demo1/service-a/src/main/resources/privateKey.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDPAO3J/p486N/+ +gQslxfsC4Vct7cAVMi7v3I6mTXDizw+JrpMTRNVUcnVf0ymKIxGHktbnqtFJBfkf +lIdLIxM5PwPB23J0ehklONNq8zGqjFZQI9TU1H3e12Jt7AUn4TKQaPXtBQalybb6 +ayT7RhiNNrUGGNzH+wL1ak8jVAEFpLuFcfojJwshF3myNfXInAqRIGCEeQBFyZrh +8RK+PxoGOiYmXJJgxDxtfr0cMVa7y0gdPuSqEDQND+vt8kVQk47cK8eGH1ypoPfk +emeNOpjjDRaJIuoDgXEOCQxHVT1qN4OjWOfAYRZXtfvy6xrP+rS5o0gqg/1tZbF7 +XUmrm46vAgMBAAECggEBAMehqrFKF4rAPvzvsDN+ikPN08icZ8lJO1DhUMT7HCnv +7JkoPfiwQlgNhjqip4XrqgUoTI7hArK8yvN0x0FkEy77IYF8RBYmhkeKVQHohXZn +nvnshF24i6cz6l395z79hEkWoE0zsqSCMy+v0tttT1Iod03o/kryPXk2TBnS8qVf +6nv/ohuu2S5KI0b4skxGSM60kA96h6ngCcq4pkwHEtnwJmF2xz4EO+zb558yOF5A +OznjKxTbNH3RsCGG1bMWSvy4qIihdJRDUAeKKdtAZOeLgUrSFHUDxKwCLFIESde0 +JP9uXuUlAb8WOnsL4z0jbgx1+ctI0UOIKTJc8sErXrECgYEA+S8cSpgED416ydrt +WWmETeW2sPUJsmDfQhQxUoxhSypDTDBGHGBZzvLSoWCxJBnuY8teiDzDAXh7nHMs +SZA1vT9hZu+qWzgdU+0SK6r0FyY5RFBIHB468fO6S5mJm0wqmy1J94Voaeofij1q +1pE8/tniLKciP2BnkX7lu2F1E4cCgYEA1Kp0Kz0WwUcu2UnmKOOzqIsA9mwucKAF +zmGA1c8q+Cx2dIgeHQjWtl3SWs0qVps74YEZUYbxmG/M9MHHGbRq7w8hwaSpeFt+ +DswiscELag4PuEHn6yGdJyPnXBoERnOvVPlkdDts6NPvLtfcBBkgPqhpvhObf9vf +Cv+trWWExZkCgYAlmY86Tj/mnOGXTdqcsEhPfMcZYpApA2cM0IE0xIv1zJXFDE+3 +/m3uxUM1KKLyIJuRIWHNSuXd9fEpBVP8ca86NDMdVjKtewUp4c7pGe2lBJaFkVug +KouYcL9+otdZwJ95NNdBazb7LGG/+U6Cu/2pMvVm6X1IdOKL2MsPgEArRwKBgQDA +midfyZHENg2t6Qmz2pUpfcq/YrakdakMgq3F9jw6Szp0y5pKPWkH/Oy4I7vGeAzB +bMRbW9WOcyKyQJVrKET4gUHXOKPrRyFhkWuShP0rbdS60aWTA/xqKFAuz7kzfS47 +zSo3QmKecuLaD9FJPOBBHxG1fdiE8cKNGYZX1etrcQKBgHhnkwmCTxXdimQTswE1 +nytAbdXEUSqLU2sZT3sQq497j6uOjcBlZekPtibOuyQLFmeKIMZMI1j0AARbu+ZJ +T7SwLL4cjsws838VujJzXPGxvbp3twvWpZHsUvp2eX/8LiWexL5TPj4jkHkkholH +H675T5VXIbt4N8LR1fHThOJB +-----END PRIVATE KEY----- diff --git a/.vscode/demo1/service-a/src/main/webapp/.gitkeep b/.vscode/demo1/service-a/src/main/webapp/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/.vscode/demo1/service-a/src/main/webapp/WEB-INF/beans.xml b/.vscode/demo1/service-a/src/main/webapp/WEB-INF/beans.xml new file mode 100644 index 000000000..2777559c2 --- /dev/null +++ b/.vscode/demo1/service-a/src/main/webapp/WEB-INF/beans.xml @@ -0,0 +1,6 @@ + + + diff --git a/.vscode/demo1/service-a/src/main/webapp/index.html b/.vscode/demo1/service-a/src/main/webapp/index.html new file mode 100644 index 000000000..5dbbbb987 --- /dev/null +++ b/.vscode/demo1/service-a/src/main/webapp/index.html @@ -0,0 +1,35 @@ + + + + + Eclipse MicroProfile demo + + + +

MicroProfile

+ +Hello JAX-RS endpoint
+ + + + + + + + + + +

JWT Auth

+Call Secured endpoint with JWT in Authorization Header
+ + + + + + + + + + + + \ No newline at end of file diff --git a/.vscode/demo1/service-b/pom.xml b/.vscode/demo1/service-b/pom.xml new file mode 100644 index 000000000..049614de0 --- /dev/null +++ b/.vscode/demo1/service-b/pom.xml @@ -0,0 +1,96 @@ + + + 4.0.0 + Daddys + demo1-service-b + 1.0-SNAPSHOT + war + + 3.5.1 + 1.8 + false + 1.5.0 + 1.7.30 + 1.8 + demo1 + 1.7.30 + + + + org.eclipse.microprofile + microprofile + 4.1 + pom + provided + + + + demo1 + + + + liberty + + true + + + + + maven-war-plugin + 3.3.2 + + false + + + + io.openliberty.tools + liberty-maven-plugin + ${openliberty.maven.version} + + + package-server + package + + create + install-feature + deploy + package + + + target/wlp-package + + + + + runnable + ${final.name} + + ${final.name} + https://server.example.com + / + + + + + + + + io.jaegertracing + jaeger-client + ${jaeger.client.version} + + + org.slf4j + slf4j-api + ${slf4j.api.version} + + + org.slf4j + slf4j-jdk14 + ${slf4j.jdk.version} + + + + + diff --git a/.vscode/demo1/service-b/readme.md b/.vscode/demo1/service-b/readme.md new file mode 100644 index 000000000..976012414 --- /dev/null +++ b/.vscode/demo1/service-b/readme.md @@ -0,0 +1,39 @@ +# MicroProfile generated Application + +## Introduction + +MicroProfile Starter has generated this MicroProfile application for you containing some endpoints which are called from the main application (see the `service-a` directory) + +The generation of the executable jar file can be performed by issuing the following command + + mvn clean package + +This will create an executable jar file **demo1.jar** within the _target_ maven folder. This can be started by executing the following command + java -jar target/demo1.jar + + + +### Liberty's Dev Mode + +During development, you can use Liberty's development mode (dev mode) to code while observing and testing your changes on the fly. +With the dev mode, you can code along and watch the change reflected in the running server right away; +unit and integration tests are run on pressing Enter in the command terminal; you can attach a debugger to the running server at any time to step through your code. + + + mvn liberty:dev + + + + +## Specification examples + + +### JWT Auth + +Have a look at the **TestSecureController** class (main application) which calls the protected endpoint on the secondary application. +The **ProtectedController** contains the protected endpoint since it contains the _@RolesAllowed_ annotation on the JAX-RS endpoint method. + +The _TestSecureController_ code creates a JWT based on the private key found within the resource directory. +However, any method to send a REST request with an appropriate header will work of course. Please feel free to change this code to your needs. + + diff --git a/.vscode/demo1/service-b/src/main/java/Daddys/demo1/Demo1RestApplication.java b/.vscode/demo1/service-b/src/main/java/Daddys/demo1/Demo1RestApplication.java new file mode 100644 index 000000000..0ab63887b --- /dev/null +++ b/.vscode/demo1/service-b/src/main/java/Daddys/demo1/Demo1RestApplication.java @@ -0,0 +1,19 @@ +package Daddys.demo1; + +import org.eclipse.microprofile.auth.LoginConfig; + +import javax.annotation.security.DeclareRoles; + +import javax.ws.rs.ApplicationPath; +import javax.ws.rs.core.Application; + +/** + * + */ +@ApplicationPath("/data") + +@LoginConfig(authMethod = "MP-JWT", realmName = "jwt-jaspi") +@DeclareRoles({"protected"}) + +public class Demo1RestApplication extends Application { +} diff --git a/.vscode/demo1/service-b/src/main/java/Daddys/demo1/secure/ProtectedController.java b/.vscode/demo1/service-b/src/main/java/Daddys/demo1/secure/ProtectedController.java new file mode 100644 index 000000000..16677309d --- /dev/null +++ b/.vscode/demo1/service-b/src/main/java/Daddys/demo1/secure/ProtectedController.java @@ -0,0 +1,28 @@ +package Daddys.demo1.secure; + +import org.eclipse.microprofile.jwt.Claim; +import org.eclipse.microprofile.jwt.ClaimValue; + +import javax.annotation.security.RolesAllowed; +import javax.enterprise.context.RequestScoped; +import javax.inject.Inject; +import javax.ws.rs.GET; +import javax.ws.rs.Path; + +/** + * + */ +@Path("/protected") +@RequestScoped +public class ProtectedController { + + @Inject + @Claim("custom-value") + private ClaimValue custom; + + @GET + @RolesAllowed("protected") + public String getJWTBasedValue() { + return "Protected Resource; Custom value : " + custom.getValue(); + } +} diff --git a/.vscode/demo1/service-b/src/main/liberty/config/resources/security/public.jks b/.vscode/demo1/service-b/src/main/liberty/config/resources/security/public.jks new file mode 100644 index 000000000..4c633832e Binary files /dev/null and b/.vscode/demo1/service-b/src/main/liberty/config/resources/security/public.jks differ diff --git a/.vscode/demo1/service-b/src/main/liberty/config/server.xml b/.vscode/demo1/service-b/src/main/liberty/config/server.xml new file mode 100644 index 000000000..1698d9669 --- /dev/null +++ b/.vscode/demo1/service-b/src/main/liberty/config/server.xml @@ -0,0 +1,24 @@ + + + + + microProfile-4.1 + + + + + + + + + + + + + + + + + diff --git a/.vscode/demo1/service-b/src/main/liberty/server/resources/security/public.jks b/.vscode/demo1/service-b/src/main/liberty/server/resources/security/public.jks new file mode 100644 index 000000000..4c633832e Binary files /dev/null and b/.vscode/demo1/service-b/src/main/liberty/server/resources/security/public.jks differ diff --git a/.vscode/demo1/service-b/src/main/resources/.gitkeep b/.vscode/demo1/service-b/src/main/resources/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/.vscode/demo1/service-b/src/main/webapp/WEB-INF/beans.xml b/.vscode/demo1/service-b/src/main/webapp/WEB-INF/beans.xml new file mode 100644 index 000000000..2777559c2 --- /dev/null +++ b/.vscode/demo1/service-b/src/main/webapp/WEB-INF/beans.xml @@ -0,0 +1,6 @@ + + + diff --git a/.vscode/launch.json b/.vscode/launch.json index 6de8b630d..facee70b5 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -4,6 +4,9 @@ // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "configurations": [ + + Node + { "type": "java", "name": "Debug (Attach)",