diff --git a/msal4j-sdk/src/integrationtest/java/com.microsoft.aad.msal4j/AuthorizationCodeIT.java b/msal4j-sdk/src/integrationtest/java/com.microsoft.aad.msal4j/AuthorizationCodeIT.java index 744a461f..c786e3a5 100644 --- a/msal4j-sdk/src/integrationtest/java/com.microsoft.aad.msal4j/AuthorizationCodeIT.java +++ b/msal4j-sdk/src/integrationtest/java/com.microsoft.aad.msal4j/AuthorizationCodeIT.java @@ -90,6 +90,45 @@ public void acquireTokenWithAuthorizationCode_B2C_Local(String environment) { assertAcquireTokenB2C(user); } + @Test + public void acquireTokenWithAuthorizationCode_CiamCud() throws Exception { + String authorityCud = "https://login.msidlabsciam.com/fe362aec-5d43-45d1-b730-9755e60dc3b9/v2.0/"; + User user = labUserProvider.getCiamCudUser(); + + PublicClientApplication pca = PublicClientApplication.builder( + user.getAppId()). + oidcAuthority(authorityCud). + build(); + + assertEquals("https://login.msidlabsciam.com/fe362aec-5d43-45d1-b730-9755e60dc3b9/v2.0/.well-known/openid-configuration", + pca.authenticationAuthority.canonicalAuthorityUrl.toString()); + assertEquals("https://login.msidlabsciam.com/fe362aec-5d43-45d1-b730-9755e60dc3b9/oauth2/v2.0/authorize", + pca.authenticationAuthority.authorizationEndpoint); + + String authCode = acquireAuthorizationCodeAutomated(user, pca, null); + + IAuthenticationResult result = pca.acquireToken(AuthorizationCodeParameters + .builder(authCode, + new URI(TestConstants.LOCALHOST + httpListener.port())) + .scopes(Collections.singleton("user.read")) + .build()) + .get(); + + assertNotNull(result); + assertNotNull(result.accessToken()); + assertNotNull(result.idToken()); + assertEquals(user.getUpn(), result.account().username()); + + IAuthenticationResult resultSilent = pca.acquireTokenSilently(SilentParameters + .builder(Collections.singleton("user.read"), result.account()) + .build()) + .get(); + + assertNotNull(resultSilent); + assertEquals(resultSilent.accessToken(), result.accessToken()); + assertEquals(resultSilent.account().username(), result.account().username()); + } + private void assertAcquireTokenADFS2019(User user) { PublicClientApplication pca; try { @@ -261,6 +300,8 @@ private String buildAuthenticationCodeURL(AbstractClientApplicationBase app, Map scope = TestConstants.B2C_LAB_SCOPE; } else if (authorityType == AuthorityType.ADFS) { scope = TestConstants.ADFS_SCOPE; + } else if (authorityType == AuthorityType.OIDC) { + scope = TestConstants.USER_READ_SCOPE; } else { throw new RuntimeException("Authority type not recognized"); } diff --git a/msal4j-sdk/src/integrationtest/java/com.microsoft.aad.msal4j/ClientCredentialsIT.java b/msal4j-sdk/src/integrationtest/java/com.microsoft.aad.msal4j/ClientCredentialsIT.java index 67a70f66..2a77eda5 100644 --- a/msal4j-sdk/src/integrationtest/java/com.microsoft.aad.msal4j/ClientCredentialsIT.java +++ b/msal4j-sdk/src/integrationtest/java/com.microsoft.aad.msal4j/ClientCredentialsIT.java @@ -88,6 +88,25 @@ void acquireTokenClientCredentials_ClientSecret_Ciam() throws Exception { assertNotNull(result.accessToken()); } + @Test + void acquireTokenClientCredentials_Certificate_CiamCud() throws Exception { + String authorityCud = "https://login.msidlabsciam.com/fe362aec-5d43-45d1-b730-9755e60dc3b9/v2.0/"; + String clientId = "b244c86f-ed88-45bf-abda-6b37aa482c79"; + + ConfidentialClientApplication cca = ConfidentialClientApplication.builder( + clientId, CertificateHelper.getClientCertificate()) + .oidcAuthority(authorityCud) + .build(); + + IAuthenticationResult result = cca.acquireToken(ClientCredentialParameters + .builder(Collections.singleton(TestConstants.DEFAULT_SCOPE)) + .build()) + .get(); + + assertNotNull(result); + assertNotNull(result.accessToken()); + } + @Test void acquireTokenClientCredentials_Callback() throws Exception { String clientId = TestConstants.MSIDLAB_CLIENT_ID; diff --git a/msal4j-sdk/src/integrationtest/java/com.microsoft.aad.msal4j/SeleniumTest.java b/msal4j-sdk/src/integrationtest/java/com.microsoft.aad.msal4j/SeleniumTest.java index f5fe21cb..907e1192 100644 --- a/msal4j-sdk/src/integrationtest/java/com.microsoft.aad.msal4j/SeleniumTest.java +++ b/msal4j-sdk/src/integrationtest/java/com.microsoft.aad.msal4j/SeleniumTest.java @@ -48,7 +48,7 @@ void runSeleniumAutomatedLogin(User user, AbstractClientApplicationBase app) { SeleniumExtensions.performADOrCiamLogin(seleniumDriver, user); } else if (authorityType == AuthorityType.ADFS) { SeleniumExtensions.performADFS2019Login(seleniumDriver, user); - } else if (authorityType == AuthorityType.CIAM) { + } else if (authorityType == AuthorityType.CIAM || authorityType == AuthorityType.OIDC) { SeleniumExtensions.performADOrCiamLogin(seleniumDriver, user); } } diff --git a/msal4j-sdk/src/integrationtest/java/labapi/FederationProvider.java b/msal4j-sdk/src/integrationtest/java/labapi/FederationProvider.java index 36127464..a4eed1bf 100644 --- a/msal4j-sdk/src/integrationtest/java/labapi/FederationProvider.java +++ b/msal4j-sdk/src/integrationtest/java/labapi/FederationProvider.java @@ -9,6 +9,7 @@ public class FederationProvider { public static final String ADFS_4 = "adfsv4"; public static final String ADFS_2019 = "adfsv2019"; public static final String CIAM = "ciam"; + public static final String CIAMCUD = "ciamcud"; } diff --git a/msal4j-sdk/src/integrationtest/java/labapi/LabUserProvider.java b/msal4j-sdk/src/integrationtest/java/labapi/LabUserProvider.java index b3d633d4..8fd5b307 100644 --- a/msal4j-sdk/src/integrationtest/java/labapi/LabUserProvider.java +++ b/msal4j-sdk/src/integrationtest/java/labapi/LabUserProvider.java @@ -113,6 +113,14 @@ public User getCiamUser() { return getLabUser(query); } + public User getCiamCudUser() { + UserQueryParameters query = new UserQueryParameters(); + query.parameters.put(UserQueryParameters.FEDERATION_PROVIDER, FederationProvider.CIAMCUD); + query.parameters.put(UserQueryParameters.SIGN_IN_AUDIENCE, "azureadmyorg"); + + return getLabUser(query); + } + public User getLabUser(UserQueryParameters userQuery) { if (userCache.containsKey(userQuery)) { return userCache.get(userQuery); diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AadInstanceDiscoveryProvider.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AadInstanceDiscoveryProvider.java index e85f30c4..502aa090 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AadInstanceDiscoveryProvider.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AadInstanceDiscoveryProvider.java @@ -372,4 +372,4 @@ private static void validate(AadInstanceDiscoveryResponse aadInstanceDiscoveryRe throw new MsalServiceException(aadInstanceDiscoveryResponse); } } -} +} \ No newline at end of file diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AbstractClientApplicationBase.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AbstractClientApplicationBase.java index b31f5e64..c2649e7a 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AbstractClientApplicationBase.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AbstractClientApplicationBase.java @@ -4,22 +4,17 @@ package com.microsoft.aad.msal4j; import com.nimbusds.oauth2.sdk.auth.ClientAuthentication; -import lombok.AccessLevel; import lombok.Getter; import lombok.experimental.Accessors; -import org.slf4j.Logger; import javax.net.ssl.SSLSocketFactory; import java.net.MalformedURLException; import java.net.Proxy; import java.net.URL; import java.util.Collections; -import java.util.HashMap; -import java.util.List; import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutorService; -import java.util.function.Consumer; import static com.microsoft.aad.msal4j.ParameterValidationUtils.validateNotBlank; import static com.microsoft.aad.msal4j.ParameterValidationUtils.validateNotNull; @@ -283,6 +278,24 @@ public T b2cAuthority(String val) throws MalformedURLException { return self(); } + /** + * Set a known authority corresponding to a generic OpenIdConnect Identity Provider. + * MSAL will append ".well-known/openid-configuration" to the authority to retrieve the OIDC metadata and determine the endpoints. + * + * @param val a string value of authority + * @return instance of the Builder on which method was called + */ + public T oidcAuthority(String val) throws MalformedURLException { + authority = Authority.enforceTrailingSlash(val); + URL authorityURL = new URL(authority); + + authenticationAuthority = new OidcAuthority(authorityURL); + + Authority.validateAuthority(authenticationAuthority.canonicalAuthorityUrl()); + + return self(); + } + /** * Set a boolean value telling the application if the authority needs to be verified * against a list of known authorities. Authority is only validated when: @@ -550,5 +563,11 @@ public T correlationId(String val) { authenticationAuthority.host, aadAadInstanceDiscoveryResponse); } + + if (authenticationAuthority.authorityType == AuthorityType.OIDC) { + ((OidcAuthority) authenticationAuthority).setAuthorityProperties( + OidcDiscoveryProvider.performOidcDiscovery( + (OidcAuthority) authenticationAuthority, this)); + } } } \ No newline at end of file diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/Authority.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/Authority.java index 5644db45..3b30de41 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/Authority.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/Authority.java @@ -13,7 +13,6 @@ /** * Represents Authentication Authority responsible for issuing access tokens. */ - @Accessors(fluent = true) @Getter(AccessLevel.PACKAGE) abstract class Authority { @@ -63,7 +62,7 @@ static Authority createAuthority(URL authorityUrl) throws MalformedURLException{ createdAuthority = new B2CAuthority(authorityUrl); } else if (authorityType == AuthorityType.ADFS) { createdAuthority = new ADFSAuthority(authorityUrl); - } else if(authorityType == AuthorityType.CIAM){ + } else if (authorityType == AuthorityType.CIAM) { createdAuthority = new CIAMAuthority(authorityUrl); } else { throw new IllegalArgumentException("Unsupported Authority Type"); @@ -79,7 +78,7 @@ static AuthorityType detectAuthorityType(URL authorityUrl) { final String path = authorityUrl.getPath().substring(1); if (StringHelper.isBlank(path)) { - if(isCiamAuthority(authorityUrl.getHost())){ + if (isCiamAuthority(authorityUrl.getHost())) { return AuthorityType.CIAM; } throw new IllegalArgumentException( diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AuthorityType.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AuthorityType.java index f686f2f2..83bf5960 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AuthorityType.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/AuthorityType.java @@ -4,5 +4,5 @@ package com.microsoft.aad.msal4j; enum AuthorityType { - AAD, ADFS, B2C, CIAM + AAD, ADFS, B2C, CIAM, OIDC } diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/CIAMAuthority.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/CIAMAuthority.java index 5d4795a6..b2c1aed8 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/CIAMAuthority.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/CIAMAuthority.java @@ -6,7 +6,7 @@ import java.net.MalformedURLException; import java.net.URL; -public class CIAMAuthority extends Authority{ +public class CIAMAuthority extends Authority { public static final String CIAM_HOST_SEGMENT = ".ciamlogin.com"; @@ -23,20 +23,22 @@ public class CIAMAuthority extends Authority{ CIAMAuthority(URL authorityUrl) throws MalformedURLException { super(transformAuthority(authorityUrl), AuthorityType.CIAM); setAuthorityProperties(); - this.authority = String.format(CIAM_AUTHORITY_FORMAT,host,tenant); + this.authority = String.format(CIAM_AUTHORITY_FORMAT, host, tenant); } - /** This method takes a CIAM authority string of format "tenant.ciamlogin.com" or "https://tenant.ciamlogin.com" - and converts it into a full authority url with a path segment of format "/tenant.onmicrosoft.com" + /** + * This method takes a CIAM authority string of format "tenant.ciamlogin.com" or "https://tenant.ciamlogin.com" + * and converts it into a full authority url with a path segment of format "/tenant.onmicrosoft.com" + * * @param originalAuthority authority to be transformed * @return full CIAM authority with path */ protected static URL transformAuthority(URL originalAuthority) throws MalformedURLException { String host = originalAuthority.getHost() + originalAuthority.getPath(); String transformedAuthority = originalAuthority.toString(); - if(originalAuthority.getPath().equals("/")){ + if (originalAuthority.getPath().equals("/")) { int ciamHostIndex = host.indexOf(CIAMAuthority.CIAM_HOST_SEGMENT); - String tenant = host.substring(0 , ciamHostIndex); + String tenant = host.substring(0, ciamHostIndex); transformedAuthority = originalAuthority + tenant + ".onmicrosoft.com/"; } return new URL(transformedAuthority); diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/HttpHelper.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/HttpHelper.java index 1106a59f..f514f519 100644 --- a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/HttpHelper.java +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/HttpHelper.java @@ -105,6 +105,22 @@ IHttpResponse executeHttpRequest(HttpRequest httpRequest, return httpResponse; } + IHttpResponse executeHttpRequest(HttpRequest httpRequest) { + IHttpResponse httpResponse; + + try { + httpResponse = executeHttpRequestWithRetries(httpRequest, httpClient); + } catch (Exception e) { + throw new MsalClientException(e); + } + + if (httpResponse.headers() != null) { + HttpHelper.verifyReturnedCorrelationId(httpRequest, httpResponse); + } + + return httpResponse; + } + private String getRequestThumbprint(RequestContext requestContext) { StringBuilder sb = new StringBuilder(); sb.append(requestContext.clientId() + POINT_DELIMITER); diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/OidcAuthority.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/OidcAuthority.java new file mode 100644 index 00000000..63759f3b --- /dev/null +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/OidcAuthority.java @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.aad.msal4j; + +import java.net.MalformedURLException; +import java.net.URL; + +public class OidcAuthority extends Authority { + //Part of the OpenIdConnect standard, this is appended to the authority to create the endpoint that has OIDC metadata + static final String WELL_KNOWN_OPENID_CONFIGURATION = ".well-known/openid-configuration"; + private static final String AUTHORITY_FORMAT = "https://%s/%s/"; + + OidcAuthority(URL authorityUrl) throws MalformedURLException { + super(createOidcDiscoveryUrl(authorityUrl), AuthorityType.OIDC); + + this.authority = String.format(AUTHORITY_FORMAT, host, tenant); + } + + private static URL createOidcDiscoveryUrl(URL originalAuthority) throws MalformedURLException { + String authority = originalAuthority.toString(); + authority += WELL_KNOWN_OPENID_CONFIGURATION; + + return new URL(authority); + } + + void setAuthorityProperties(OidcDiscoveryResponse instanceDiscoveryResponse) { + this.authorizationEndpoint = instanceDiscoveryResponse.authorizationEndpoint(); + this.tokenEndpoint = instanceDiscoveryResponse.tokenEndpoint(); + this.deviceCodeEndpoint = instanceDiscoveryResponse.deviceCodeEndpoint(); + this.selfSignedJwtAudience = this.tokenEndpoint; + } +} diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/OidcDiscoveryProvider.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/OidcDiscoveryProvider.java new file mode 100644 index 00000000..f9fdbea9 --- /dev/null +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/OidcDiscoveryProvider.java @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.aad.msal4j; + +class OidcDiscoveryProvider { + + static OidcDiscoveryResponse performOidcDiscovery(OidcAuthority authority, AbstractClientApplicationBase clientApplication) { + HttpRequest httpRequest = new HttpRequest( + HttpMethod.GET, + authority.canonicalAuthorityUrl.toString()); + + IHttpResponse httpResponse = ((HttpHelper)clientApplication.serviceBundle.getHttpHelper()).executeHttpRequest(httpRequest); + + OidcDiscoveryResponse response = JsonHelper.convertJsonToObject(httpResponse.body(), OidcDiscoveryResponse.class); + + if (httpResponse.statusCode() != HttpHelper.HTTP_STATUS_200) { + throw MsalServiceExceptionFactory.fromHttpResponse(httpResponse); + } + + return response; + } +} diff --git a/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/OidcDiscoveryResponse.java b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/OidcDiscoveryResponse.java new file mode 100644 index 00000000..13e51bf5 --- /dev/null +++ b/msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/OidcDiscoveryResponse.java @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.aad.msal4j; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.experimental.Accessors; + +@Accessors(fluent = true) +@Getter(AccessLevel.PACKAGE) +class OidcDiscoveryResponse { + + @JsonProperty("authorization_endpoint") + private String authorizationEndpoint; + + @JsonProperty("token_endpoint") + private String tokenEndpoint; + + @JsonProperty("device_authorization_endpoint") + private String deviceCodeEndpoint; +}