diff --git a/azure-pipelines-ci.yml b/azure-pipelines-ci.yml index d724d2886..1a06c4291 100644 --- a/azure-pipelines-ci.yml +++ b/azure-pipelines-ci.yml @@ -149,7 +149,7 @@ stages: script: | cp android_client/app/build/outputs/apk/release/app-release.apk common/src/main/resources/record_release.apk -force - task: Gradle@2 - displayName: Build center + displayName: Build center (with MISE) inputs: gradleWrapperFile: 'gradlew' tasks: 'center:bootJar --stacktrace' @@ -158,6 +158,8 @@ stages: jdkVersionOption: '1.11' sonarQubeRunAnalysis: false spotBugsAnalysis: false + options: '-PIDDPUsername=IdentityDivision -PIDDPPassword=$(IDDPPassword) -PenableMISE=true' + condition: eq(variables.fullBuild, 'true') - task: Gradle@2 displayName: Build agent inputs: @@ -205,6 +207,23 @@ stages: Contents: '*.jar' TargetFolder: '$(Build.ArtifactStagingDirectory)/center_deploy' condition: eq(variables.fullBuild, 'true') + - task: Gradle@2 + displayName: Build center (without MISE) + inputs: + gradleWrapperFile: 'gradlew' + tasks: 'center:bootJar --stacktrace' + publishJUnitResults: false + javaHomeOption: 'JDKVersion' + jdkVersionOption: '1.11' + sonarQubeRunAnalysis: false + spotBugsAnalysis: false + - task: CopyFiles@2 + displayName: Copy center jar + inputs: + SourceFolder: 'center/build/libs/' + Contents: '*.jar' + TargetFolder: '$(Build.ArtifactStagingDirectory)/center_publish' + condition: eq(variables.fullBuild, 'true') - task: Gradle@2 displayName: Package Mac installer inputs: diff --git a/center/build.gradle b/center/build.gradle index 5bd640792..ebf138b90 100644 --- a/center/build.gradle +++ b/center/build.gradle @@ -14,6 +14,16 @@ bootJar.dependsOn("checkstyleMain") repositories { mavenCentral() + if (project.hasProperty("enableMISE")) { + maven { + url 'https://identitydivision.pkgs.visualstudio.com/_packaging/IDDP/maven/v1' + name 'IDDP' + credentials(PasswordCredentials) + authentication { + basic(BasicAuthentication) + } + } + } } bootJar { @@ -67,6 +77,9 @@ dependencies { compileOnly 'org.projectlombok:lombok:1.18.20' annotationProcessor 'org.projectlombok:lombok:1.18.20' + if (project.hasProperty("enableMISE")) { + compile(group: 'com.microsoft.identity.service.essentials', name: 'java-adapter', version: '1.32.0') + } } import org.apache.tools.ant.taskdefs.condition.Os diff --git a/center/src/main/java/com/microsoft/hydralab/center/util/AuthUtil.java b/center/src/main/java/com/microsoft/hydralab/center/util/AuthUtil.java index 012e1bbe8..4b4585fdc 100644 --- a/center/src/main/java/com/microsoft/hydralab/center/util/AuthUtil.java +++ b/center/src/main/java/com/microsoft/hydralab/center/util/AuthUtil.java @@ -61,12 +61,17 @@ public class AuthUtil { String ignoreUri; @Value("${spring.security.oauth2.client.registration.azure-client.scope:}") String scope; + @Value("${spring.security.oauth2.client.provider.azure-ad.miseEnabled:false}") + boolean miseEnabled; Map urlMapping = null; private static final org.slf4j.Logger LOGGER = LoggerFactory.getLogger(AuthUtil.class); public boolean isValidToken(String token) { LOGGER.info("Starting token validation..."); + if (miseEnabled) { + return validateTokenWithMISE(token); + } return validateTokenWithPublicKey(token) && validateAudienceAndExpiredTime(token); } @@ -113,6 +118,89 @@ private boolean validateTokenWithPublicKey(String token) { } } + public boolean validateTokenWithMISE(String token) { + LOGGER.info("Starting MISE token validation..."); + + try { + // Mise mise = Mise.createClient(); + Class miseClass = Class.forName("com.microsoft.identity.service.essentials.Mise"); + Object mise = miseClass.getMethod("createClient").invoke(null); + + // mise.assignLogMessageCallback(new Mise.ILogCallback() {...}, null); + Class logLevelClass = Class.forName("com.microsoft.identity.service.essentials.MiseLogLevel"); + Class iLogCallbackClass = Class.forName("com.microsoft.identity.service.essentials.Mise$ILogCallback"); + + Object logCallback = java.lang.reflect.Proxy.newProxyInstance( + iLogCallbackClass.getClassLoader(), + new Class[]{iLogCallbackClass}, + (proxy, method, args) -> { + String methodName = method.getName(); + if ("callback".equals(methodName)) { + Object level = args[0]; + String message = (String) args[1]; + // Print all log levels for simplicity + LOGGER.info(message); + } + return null; + } + ); + + miseClass.getMethod("assignLogMessageCallback", iLogCallbackClass, Object.class) + .invoke(mise, logCallback, null); + + // Configure MISE + JSONObject config = new JSONObject(); + JSONObject azureAd = new JSONObject(); + azureAd.put("Instance", instanceUri); + azureAd.put("ClientId", clientId); + azureAd.put("TenantId", tenantId); + String[] audiences = audience.split(","); + azureAd.put("Audiences", audiences); + JSONObject logging = new JSONObject(); + logging.put("logLevel", "Debug"); + azureAd.put("Logging", logging); + config.put("AzureAd", azureAd); + + miseClass.getMethod("configure", String.class, String.class) + .invoke(mise, config.toString(), null); + + // MiseValidationInput miseValidationInput = new MiseValidationInput(); + Class miseValidationInputClass = Class.forName("com.microsoft.identity.service.essentials.MiseValidationInput"); + Object miseValidationInput = miseValidationInputClass.getDeclaredConstructor().newInstance(); + + miseValidationInputClass.getField("authorizationHeader").set(miseValidationInput, "Bearer " + token); + miseValidationInputClass.getField("originalMethodHeader").set(miseValidationInput, "GET"); + miseValidationInputClass.getField("originalUriHeader").set(miseValidationInput, "https://myapi.com/api/values"); + + // try (MiseValidationResult validationResult = mise.validate(miseValidationInput)) { ... } + Object validationResult = miseClass.getMethod("validate", miseValidationInputClass) + .invoke(mise, miseValidationInput); + + Class miseValidationResultClass = Class.forName("com.microsoft.identity.service.essentials.MiseValidationResult"); + int statusCode = (int) miseValidationResultClass.getMethod("getHttpResponseStatusCode").invoke(validationResult); + LOGGER.info("Status code " + statusCode); + + String errorDescription = (String) miseValidationResultClass.getMethod("getErrorDescription").invoke(validationResult); + if (errorDescription != null) { + LOGGER.error("Error message " + errorDescription); + } + + // Close validationResult if AutoCloseable + if (validationResult instanceof AutoCloseable) { + ((AutoCloseable) validationResult).close(); + } + if (statusCode != 200) { + LOGGER.error("MISE token validation failed with status code: " + statusCode); + return false; + } + LOGGER.info("MISE token validation passed"); + } catch (Exception e) { + e.printStackTrace(); + return false; + } + return true; + } + private PublicKey getPublicKey(JWSObject jwsObject, JWKSet jwkSet) throws JOSEException { JWSAlgorithm algorithm = jwsObject.getHeader().getAlgorithm(); if (!algorithm.equals(JWSAlgorithm.RS256)) { @@ -228,7 +316,7 @@ public String getLoginUserDisplayName(String accessToken) { public String getLoginUrl() { String loginUrl = authorizationUri + "?client_id=" + clientId + "&response_type=code+id_token&redirect_uri=" + redirectUri + - "&response_mode=form_post&nonce="+ UUID.randomUUID() +"&scope=" + scope; + "&response_mode=form_post&nonce=" + UUID.randomUUID() + "&scope=" + scope; return loginUrl; } diff --git a/center/src/main/resources/application-release.yml b/center/src/main/resources/application-release.yml index 9e6a8e497..4bf382daf 100644 --- a/center/src/main/resources/application-release.yml +++ b/center/src/main/resources/application-release.yml @@ -12,6 +12,7 @@ spring: tenant-id: ${MICROSOFT_PROVIDER_TENANT_ID} audience: ${MICROSOFT_PROVIDER_AUDIENCE} instance-uri: ${MICROSOFT_PROVIDER_INSTANCE_URI} + mise-enabled: ${MICROSOFT_MISE_ENABLED:false} registration: azure-client: provider: azure-ad