diff --git a/sdk/identity/azure-identity/CHANGELOG.md b/sdk/identity/azure-identity/CHANGELOG.md index 1ff17797372a..f598114f8d12 100644 --- a/sdk/identity/azure-identity/CHANGELOG.md +++ b/sdk/identity/azure-identity/CHANGELOG.md @@ -9,6 +9,7 @@ - Added `regionalAuthority()` setter to `ClientSecretCredentialBuilder` and `ClientCertificateCredentialBuilder`. - If instead of a region, `RegionalAuthority.AutoDiscoverRegion` is specified as the value for `regionalAuthority`, MSAL will be used to attempt to discover the region. - A region can also be specified through the `AZURE_REGIONAL_AUTHORITY_NAME` environment variable. +- Added `loginHint()` setter to `InteractiveBrowserCredentialBuilder` which allows a username to be pre-selected for interactive logins. ## 1.3.1 (2021-06-08) diff --git a/sdk/identity/azure-identity/src/main/java/com/azure/identity/InteractiveBrowserCredential.java b/sdk/identity/azure-identity/src/main/java/com/azure/identity/InteractiveBrowserCredential.java index d76c7c5f95a5..a2033b54fa5c 100644 --- a/sdk/identity/azure-identity/src/main/java/com/azure/identity/InteractiveBrowserCredential.java +++ b/sdk/identity/azure-identity/src/main/java/com/azure/identity/InteractiveBrowserCredential.java @@ -35,6 +35,7 @@ public class InteractiveBrowserCredential implements TokenCredential { private final boolean automaticAuthentication; private final String authorityHost; private final String redirectUrl; + private final String loginHint; private final ClientLogger logger = new ClientLogger(InteractiveBrowserCredential.class); @@ -50,7 +51,8 @@ public class InteractiveBrowserCredential implements TokenCredential { * @param identityClientOptions the options for configuring the identity client */ InteractiveBrowserCredential(String clientId, String tenantId, Integer port, String redirectUrl, - boolean automaticAuthentication, IdentityClientOptions identityClientOptions) { + boolean automaticAuthentication, String loginHint, + IdentityClientOptions identityClientOptions) { this.port = port; this.redirectUrl = redirectUrl; identityClient = new IdentityClientBuilder() @@ -61,6 +63,7 @@ public class InteractiveBrowserCredential implements TokenCredential { cachedToken = new AtomicReference<>(); this.authorityHost = identityClientOptions.getAuthorityHost(); this.automaticAuthentication = automaticAuthentication; + this.loginHint = loginHint; if (identityClientOptions.getAuthenticationRecord() != null) { cachedToken.set(new MsalAuthenticationAccount(identityClientOptions.getAuthenticationRecord())); } @@ -81,7 +84,7 @@ public Mono getToken(TokenRequestContext request) { + "authentication is needed to acquire token. Call Authenticate to initiate the device " + "code authentication.", request))); } - return identityClient.authenticateWithBrowserInteraction(request, port, redirectUrl); + return identityClient.authenticateWithBrowserInteraction(request, port, redirectUrl, loginHint); })).map(this::updateCache) .doOnNext(token -> LoggingUtil.logTokenSuccess(logger, request)) .doOnError(error -> LoggingUtil.logTokenError(logger, request, error)); @@ -98,9 +101,10 @@ public Mono getToken(TokenRequestContext request) { * when credential was instantiated. */ public Mono authenticate(TokenRequestContext request) { - return Mono.defer(() -> identityClient.authenticateWithBrowserInteraction(request, port, redirectUrl)) - .map(this::updateCache) - .map(msalToken -> cachedToken.get().getAuthenticationRecord()); + return Mono.defer(() -> identityClient.authenticateWithBrowserInteraction( + request, port, redirectUrl, loginHint)) + .map(this::updateCache) + .map(msalToken -> cachedToken.get().getAuthenticationRecord()); } /** diff --git a/sdk/identity/azure-identity/src/main/java/com/azure/identity/InteractiveBrowserCredentialBuilder.java b/sdk/identity/azure-identity/src/main/java/com/azure/identity/InteractiveBrowserCredentialBuilder.java index 9f3719035f71..3bd002adaabe 100644 --- a/sdk/identity/azure-identity/src/main/java/com/azure/identity/InteractiveBrowserCredentialBuilder.java +++ b/sdk/identity/azure-identity/src/main/java/com/azure/identity/InteractiveBrowserCredentialBuilder.java @@ -16,6 +16,7 @@ public class InteractiveBrowserCredentialBuilder extends AadCredentialBuilderBas private Integer port; private boolean automaticAuthentication = true; private String redirectUrl; + private String loginHint; /** * Sets the port for the local HTTP server, for which {@code http://localhost:{port}} must be @@ -113,6 +114,19 @@ public InteractiveBrowserCredentialBuilder disableAutomaticAuthentication() { return this; } + /** + * Sets the username suggestion to pre-fill the login page's username/email address field. A user may still log in + * with a different username. + * + * @param loginHint the username suggestion to pre-fill the login page's username/email address field. + * + * @return An updated instance of this builder with login hint configured. + */ + public InteractiveBrowserCredentialBuilder loginHint(String loginHint) { + this.loginHint = loginHint; + return this; + } + /** * Creates a new {@link InteractiveBrowserCredential} with the current configurations. * @@ -123,6 +137,6 @@ public InteractiveBrowserCredential build() { String clientId = this.clientId != null ? this.clientId : IdentityConstants.DEVELOPER_SINGLE_SIGN_ON_ID; return new InteractiveBrowserCredential(clientId, tenantId, port, redirectUrl, automaticAuthentication, - identityClientOptions); + loginHint, identityClientOptions); } } diff --git a/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/IdentityClient.java b/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/IdentityClient.java index f3db181b5e9b..e408c4f3cb5b 100644 --- a/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/IdentityClient.java +++ b/sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/IdentityClient.java @@ -777,10 +777,12 @@ public Mono authenticateWithAuthorizationCode(TokenRequestContext req * * @param request the details of the token request * @param port the port on which the HTTP server is listening + * @param redirectUrl the redirect URL to listen on and receive security code + * @param loginHint the username suggestion to pre-fill the login page's username/email address field * @return a Publisher that emits an AccessToken */ public Mono authenticateWithBrowserInteraction(TokenRequestContext request, Integer port, - String redirectUrl) { + String redirectUrl, String loginHint) { URI redirectUri; String redirect; @@ -807,6 +809,10 @@ public Mono authenticateWithBrowserInteraction(TokenRequestContext re builder.claims(customClaimRequest); } + if (loginHint != null) { + builder.loginHint(loginHint); + } + Mono acquireToken = publicClientApplicationAccessor.getValue() .flatMap(pc -> Mono.fromFuture(() -> pc.acquireToken(builder.build()))); diff --git a/sdk/identity/azure-identity/src/test/java/com/azure/identity/InteractiveBrowserCredentialTest.java b/sdk/identity/azure-identity/src/test/java/com/azure/identity/InteractiveBrowserCredentialTest.java index fc4202ee54c9..fbd6ed7d5535 100644 --- a/sdk/identity/azure-identity/src/test/java/com/azure/identity/InteractiveBrowserCredentialTest.java +++ b/sdk/identity/azure-identity/src/test/java/com/azure/identity/InteractiveBrowserCredentialTest.java @@ -46,7 +46,7 @@ public void testValidInteractive() throws Exception { // mock IdentityClient identityClient = PowerMockito.mock(IdentityClient.class); - when(identityClient.authenticateWithBrowserInteraction(eq(request1), eq(port), eq(null))).thenReturn(TestUtils.getMockMsalToken(token1, expiresAt)); + when(identityClient.authenticateWithBrowserInteraction(eq(request1), eq(port), eq(null), eq(null))).thenReturn(TestUtils.getMockMsalToken(token1, expiresAt)); when(identityClient.authenticateWithPublicClientCache(any(), any())) .thenAnswer(invocation -> { TokenRequestContext argument = (TokenRequestContext) invocation.getArguments()[0]; @@ -85,7 +85,7 @@ public void testValidInteractiveViaRedirectUri() throws Exception { // mock IdentityClient identityClient = PowerMockito.mock(IdentityClient.class); - when(identityClient.authenticateWithBrowserInteraction(eq(request1), eq(null), eq(redirectUrl))).thenReturn(TestUtils.getMockMsalToken(token1, expiresAt)); + when(identityClient.authenticateWithBrowserInteraction(eq(request1), eq(null), eq(redirectUrl), eq(null))).thenReturn(TestUtils.getMockMsalToken(token1, expiresAt)); when(identityClient.authenticateWithPublicClientCache(any(), any())) .thenAnswer(invocation -> { TokenRequestContext argument = (TokenRequestContext) invocation.getArguments()[0]; @@ -104,11 +104,50 @@ public void testValidInteractiveViaRedirectUri() throws Exception { new InteractiveBrowserCredentialBuilder().redirectUrl(redirectUrl).clientId(CLIENT_ID).build(); StepVerifier.create(credential.getToken(request1)) .expectNextMatches(accessToken -> token1.equals(accessToken.getToken()) - && expiresAt.getSecond() == accessToken.getExpiresAt().getSecond()) + && expiresAt.getSecond() == accessToken.getExpiresAt().getSecond()) .verifyComplete(); StepVerifier.create(credential.getToken(request2)) .expectNextMatches(accessToken -> token2.equals(accessToken.getToken()) - && expiresAt.getSecond() == accessToken.getExpiresAt().getSecond()) + && expiresAt.getSecond() == accessToken.getExpiresAt().getSecond()) + .verifyComplete(); + } + + @Test + public void testValidInteractiveWithLoginHint() throws Exception { + // setup + String token1 = "token1"; + String token2 = "token2"; + TokenRequestContext request1 = new TokenRequestContext().addScopes("https://management.azure.com"); + TokenRequestContext request2 = new TokenRequestContext().addScopes("https://vault.azure.net"); + OffsetDateTime expiresAt = OffsetDateTime.now(ZoneOffset.UTC).plusHours(1); + String username = "user@foo.com"; + + // mock + IdentityClient identityClient = PowerMockito.mock(IdentityClient.class); + when(identityClient.authenticateWithBrowserInteraction(eq(request1), eq(null), eq(null), eq(username))).thenReturn(TestUtils.getMockMsalToken(token1, expiresAt)); + when(identityClient.authenticateWithPublicClientCache(any(), any())) + .thenAnswer(invocation -> { + TokenRequestContext argument = (TokenRequestContext) invocation.getArguments()[0]; + if (argument.getScopes().size() == 1 && argument.getScopes().get(0).equals(request2.getScopes().get(0))) { + return TestUtils.getMockMsalToken(token2, expiresAt); + } else if (argument.getScopes().size() == 1 && argument.getScopes().get(0).equals(request1.getScopes().get(0))) { + return Mono.error(new UnsupportedOperationException("nothing cached")); + } else { + throw new InvalidUseOfMatchersException(String.format("Argument %s does not match", (Object) argument)); + } + }); + PowerMockito.whenNew(IdentityClient.class).withAnyArguments().thenReturn(identityClient); + + // test + InteractiveBrowserCredential credential = + new InteractiveBrowserCredentialBuilder().loginHint(username).clientId(CLIENT_ID).build(); + StepVerifier.create(credential.getToken(request1)) + .expectNextMatches(accessToken -> token1.equals(accessToken.getToken()) + && expiresAt.getSecond() == accessToken.getExpiresAt().getSecond()) + .verifyComplete(); + StepVerifier.create(credential.getToken(request2)) + .expectNextMatches(accessToken -> token2.equals(accessToken.getToken()) + && expiresAt.getSecond() == accessToken.getExpiresAt().getSecond()) .verifyComplete(); } @@ -134,7 +173,7 @@ public void testValidAuthenticate() throws Exception { // mock IdentityClient identityClient = PowerMockito.mock(IdentityClient.class); - when(identityClient.authenticateWithBrowserInteraction(eq(request1), eq(port), eq(null))) + when(identityClient.authenticateWithBrowserInteraction(eq(request1), eq(port), eq(null), eq(null))) .thenReturn(TestUtils.getMockMsalToken(token1, expiresAt)); PowerMockito.whenNew(IdentityClient.class).withAnyArguments().thenReturn(identityClient); diff --git a/sdk/identity/azure-identity/src/test/java/com/azure/identity/implementation/IdentityClientIntegrationTests.java b/sdk/identity/azure-identity/src/test/java/com/azure/identity/implementation/IdentityClientIntegrationTests.java index 46825c3d6ac5..4d6ea9b4ede7 100644 --- a/sdk/identity/azure-identity/src/test/java/com/azure/identity/implementation/IdentityClientIntegrationTests.java +++ b/sdk/identity/azure-identity/src/test/java/com/azure/identity/implementation/IdentityClientIntegrationTests.java @@ -61,7 +61,7 @@ public void deviceCodeCanGetToken() { @Ignore("Integration tests") public void browserCanGetToken() { IdentityClient client = new IdentityClient("common", System.getenv(AZURE_CLIENT_ID), null, null, null, null, false, new IdentityClientOptions().setProxyOptions(new ProxyOptions(Type.HTTP, new InetSocketAddress("localhost", 8888)))); - MsalToken token = client.authenticateWithBrowserInteraction(request, 8765, null).block(); + MsalToken token = client.authenticateWithBrowserInteraction(request, 8765, null, null).block(); Assert.assertNotNull(token); Assert.assertNotNull(token.getToken()); Assert.assertNotNull(token.getExpiresAt()); diff --git a/sdk/identity/azure-identity/src/test/java/com/azure/identity/implementation/IdentityClientTests.java b/sdk/identity/azure-identity/src/test/java/com/azure/identity/implementation/IdentityClientTests.java index 99c5858b9f3c..3d11957ccd24 100644 --- a/sdk/identity/azure-identity/src/test/java/com/azure/identity/implementation/IdentityClientTests.java +++ b/sdk/identity/azure-identity/src/test/java/com/azure/identity/implementation/IdentityClientTests.java @@ -419,7 +419,7 @@ public void testBrowserAuthenicationCodeFlow() throws Exception { // test IdentityClientOptions options = new IdentityClientOptions(); IdentityClient client = new IdentityClientBuilder().tenantId(TENANT_ID).clientId(CLIENT_ID).identityClientOptions(options).build(); - StepVerifier.create(client.authenticateWithBrowserInteraction(request, 4567, null)) + StepVerifier.create(client.authenticateWithBrowserInteraction(request, 4567, null, null)) .expectNextMatches(accessToken -> token.equals(accessToken.getToken()) && expiresOn.getSecond() == accessToken.getExpiresAt().getSecond()) .verifyComplete();