Skip to content

Commit

Permalink
Add login hint support to InteractiveBrowserCredential (Azure#22693)
Browse files Browse the repository at this point in the history
  • Loading branch information
jianghaolu authored Jul 1, 2021
1 parent 35c8516 commit 57f30a8
Show file tree
Hide file tree
Showing 7 changed files with 78 additions and 14 deletions.
1 change: 1 addition & 0 deletions sdk/identity/azure-identity/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);


Expand All @@ -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()
Expand All @@ -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()));
}
Expand All @@ -81,7 +84,7 @@ public Mono<AccessToken> 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));
Expand All @@ -98,9 +101,10 @@ public Mono<AccessToken> getToken(TokenRequestContext request) {
* when credential was instantiated.
*/
public Mono<AuthenticationRecord> 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());
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
*
Expand All @@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -777,10 +777,12 @@ public Mono<MsalToken> 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<MsalToken> authenticateWithBrowserInteraction(TokenRequestContext request, Integer port,
String redirectUrl) {
String redirectUrl, String loginHint) {
URI redirectUri;
String redirect;

Expand All @@ -807,6 +809,10 @@ public Mono<MsalToken> authenticateWithBrowserInteraction(TokenRequestContext re
builder.claims(customClaimRequest);
}

if (loginHint != null) {
builder.loginHint(loginHint);
}

Mono<IAuthenticationResult> acquireToken = publicClientApplicationAccessor.getValue()
.flatMap(pc -> Mono.fromFuture(() -> pc.acquireToken(builder.build())));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand Down Expand Up @@ -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];
Expand All @@ -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();
}

Expand All @@ -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);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down

0 comments on commit 57f30a8

Please sign in to comment.