Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Tenant Profiles #263

Merged
merged 26 commits into from
Aug 31, 2020
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
36c8796
Merge pull request #245 from AzureAD/dev
SomkaPe Jun 3, 2020
a60641b
Merge pull request #249 from AzureAD/dev
SomkaPe Jun 10, 2020
0274b05
Classes for tenant profile functionality
Avery-Dunn Jul 21, 2020
7412180
Implement tenant profile feature
Avery-Dunn Jul 21, 2020
2982cb4
Tests for tenant profile feature
Avery-Dunn Jul 21, 2020
3f746c0
Merge branches 'avdunn/tenant-profiles' and 'dev' of https://github.c…
Avery-Dunn Aug 12, 2020
c7c53dc
Simplify tenant profile class structure
Avery-Dunn Aug 12, 2020
f92e166
1.6.2 release
SomkaPe Aug 17, 2020
00c920c
Classes for tenant profile redesign
Avery-Dunn Aug 21, 2020
42a8fc7
Tests for tenant profile redesign
Avery-Dunn Aug 21, 2020
7a0dfa4
Adjust sample cached ID tokens to have realistic headers
Avery-Dunn Aug 21, 2020
bd0cc29
Redesign how Tenant Pofiles are added to Accounts
Avery-Dunn Aug 21, 2020
9254615
New error code for JWT parse exceptions
Avery-Dunn Aug 21, 2020
1f898f6
Add claims and tenant profiles fields to Account
Avery-Dunn Aug 21, 2020
0e49632
Remove annotation excluding realm field from comparisons
Avery-Dunn Aug 21, 2020
49a3da9
Merge branch 'pesomka/release.1.6.2' of https://github.com/AzureAD/mi…
Avery-Dunn Aug 21, 2020
54341a6
Merge branch 'dev' of https://github.com/AzureAD/microsoft-authentica…
Avery-Dunn Aug 21, 2020
ed16f24
Use more generic token
Avery-Dunn Aug 21, 2020
f92983d
Remove ID token claims field from Account
Avery-Dunn Aug 25, 2020
1f66818
Minor changes for clarity
Avery-Dunn Aug 25, 2020
36e2638
Adjust tests for tenant profile design refactor
Avery-Dunn Aug 25, 2020
8424a43
Refactor tenant profile structure
Avery-Dunn Aug 25, 2020
f778f31
Minor fixes
Avery-Dunn Aug 25, 2020
ae56c04
Minor fixes
Avery-Dunn Aug 25, 2020
9a4b4f2
Minor fixes
Avery-Dunn Aug 25, 2020
d9fbf46
Simplify tenant profile class
Avery-Dunn Aug 31, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/main/java/com/microsoft/aad/msal4j/Account.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import lombok.Getter;
import lombok.Setter;
import lombok.experimental.Accessors;
import java.util.Map;

/**
* Representation of a single user account. If modifying this object, ensure it is compliant with
Expand All @@ -23,4 +24,6 @@ class Account implements IAccount {
String environment;

String username;

Map<String, ?> idTokenClaims;
Avery-Dunn marked this conversation as resolved.
Show resolved Hide resolved
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

@Accessors(fluent = true)
@Getter
Expand Down Expand Up @@ -49,6 +50,8 @@ ClientInfo clientInfo() {
@JsonProperty("authority_type")
protected String authorityType;

protected Map<String, ?> idTokenClaims;
Avery-Dunn marked this conversation as resolved.
Show resolved Hide resolved

String getKey() {

List<String> keyParts = new ArrayList<>();
Expand Down Expand Up @@ -77,6 +80,7 @@ static AccountCacheEntity create(String clientInfoStr, Authority requestAuthorit
account.localAccountId(localAccountId);
account.username(idToken.preferredUsername);
account.name(idToken.name);
account.idTokenClaims(idToken.tokenClaims());
Avery-Dunn marked this conversation as resolved.
Show resolved Hide resolved
}

return account;
Expand All @@ -101,6 +105,10 @@ static AccountCacheEntity create(String clientInfoStr, Authority requestAuthorit
}

IAccount toAccount(){
return new Account(homeAccountId, environment, username);
if (homeAccountId.contains(localAccountId)) {
return new Account(homeAccountId, environment, username, idTokenClaims);
} else {
return new TenantProfile(homeAccountId, environment, username, idTokenClaims, localAccountId);
}
}
}
6 changes: 6 additions & 0 deletions src/main/java/com/microsoft/aad/msal4j/IAccount.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

package com.microsoft.aad.msal4j;

import java.util.Map;
import java.util.Set;

/**
Expand All @@ -26,4 +27,9 @@ public interface IAccount {
* @return account username
*/
String username();

/**
* @return claims in ID token
*/
Map<String, ?> idTokenClaims();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.microsoft.aad.msal4j;

import java.util.Map;

public interface IMultiTenantAccount extends IAccount {

Map<String, ITenantProfile> getTenantProfiles();
}
5 changes: 5 additions & 0 deletions src/main/java/com/microsoft/aad/msal4j/ITenantProfile.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.microsoft.aad.msal4j;

public interface ITenantProfile extends IAccount {

}
25 changes: 25 additions & 0 deletions src/main/java/com/microsoft/aad/msal4j/IdToken.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
import com.nimbusds.jwt.JWTClaimsSet;

import java.text.ParseException;
import java.util.HashMap;
import java.util.Map;

class IdToken {

Expand Down Expand Up @@ -59,6 +61,29 @@ class IdToken {
@JsonProperty("unique_name")
protected String uniqueName;

/**
* Used to attach all claims in an ID token to an account object
*
* @return map of key/value pairs of claims in ID token
*/
protected Map<String, Object> tokenClaims() {
Avery-Dunn marked this conversation as resolved.
Show resolved Hide resolved
Map<String, Object> idTokenClaims = new HashMap<>();
idTokenClaims.put(ISSUER, this.issuer);
idTokenClaims.put(SUBJECT, this.subject);
idTokenClaims.put(AUDIENCE, this.audience);
idTokenClaims.put(EXPIRATION_TIME, this.expirationTime);
idTokenClaims.put(ISSUED_AT, this.issuedAt);
idTokenClaims.put(NOT_BEFORE, this.notBefore);
idTokenClaims.put(NAME, this.name);
idTokenClaims.put(PREFERRED_USERNAME, this.preferredUsername);
idTokenClaims.put(OBJECT_IDENTIFIER, this.objectIdentifier);
idTokenClaims.put(TENANT_IDENTIFIER, this.tenantIdentifier);
idTokenClaims.put(UPN, this.upn);
idTokenClaims.put(UNIQUE_NAME, this.uniqueName);

return idTokenClaims;
}

static IdToken createFromJWTClaims(final JWTClaimsSet claims) throws ParseException {
IdToken idToken = new IdToken();

Expand Down
22 changes: 22 additions & 0 deletions src/main/java/com/microsoft/aad/msal4j/MultiTenantAccount.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.microsoft.aad.msal4j;

import lombok.Getter;
import lombok.Setter;

import java.util.Map;

@Getter
@Setter
public class MultiTenantAccount extends Account implements IMultiTenantAccount {

private Map<String, ITenantProfile> tenantProfiles;
Avery-Dunn marked this conversation as resolved.
Show resolved Hide resolved

public MultiTenantAccount(String homeAccountId, String environment, String username, Map<String, ?> idTokenClaims) {
super(homeAccountId, environment, username, idTokenClaims);
}

@Override
public Map<String, ITenantProfile> getTenantProfiles() {
return tenantProfiles;
}
}
20 changes: 20 additions & 0 deletions src/main/java/com/microsoft/aad/msal4j/TenantProfile.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.microsoft.aad.msal4j;

import lombok.Getter;
import lombok.Setter;
import lombok.experimental.Accessors;

import java.util.Map;

@Accessors(fluent = true)
@Getter
@Setter
public class TenantProfile extends Account implements ITenantProfile{
Avery-Dunn marked this conversation as resolved.
Show resolved Hide resolved

String tenantId;

public TenantProfile(String homeAccountId, String environment, String username, Map<String, ?> idTokenClaims, String tenantId) {
super(homeAccountId, environment, username, idTokenClaims);
this.tenantId = tenantId;
}
}
48 changes: 47 additions & 1 deletion src/main/java/com/microsoft/aad/msal4j/TokenCache.java
Original file line number Diff line number Diff line change
Expand Up @@ -300,10 +300,56 @@ Set<IAccount> getAccounts(String clientId, Set<String> environmentAliases) {
build())) {
try {
lock.readLock().lock();
Set<IAccount> rootAccounts = new HashSet<>();

return accounts.values().stream().
//Filter accounts map into two sets: a set of home accounts (local UID = home UID),
// and a set of tenant profiles (local UID != home UID)
Set<IAccount> homeAccounts = accounts.values().stream().
filter(acc -> environmentAliases.contains(acc.environment())).
filter(acc -> acc.homeAccountId().contains(acc.localAccountId())).
Avery-Dunn marked this conversation as resolved.
Show resolved Hide resolved
collect(Collectors.mapping(AccountCacheEntity::toAccount, Collectors.toSet()));
Set<IAccount> tenantAccounts = accounts.values().stream().
filter(acc -> environmentAliases.contains(acc.environment())).
filter(acc -> !acc.homeAccountId().contains(acc.localAccountId())).
collect(Collectors.mapping(AccountCacheEntity::toAccount, Collectors.toSet()));

Iterator<IAccount> homeAcctsIterator = homeAccounts.iterator();
Iterator<IAccount> tenantAcctsIterator;
Map<String, ITenantProfile> tenantProfiles;
IAccount homeAcc, tenantAcc;

while (homeAcctsIterator.hasNext()) {
homeAcc = homeAcctsIterator.next();
tenantAcctsIterator = tenantAccounts.iterator();
tenantProfiles = new HashMap<>();

while (tenantAcctsIterator.hasNext()) {
tenantAcc = tenantAcctsIterator.next();

//If the tenant account's home ID matches a home ID (UID) in the list of home accounts, it is a
//tenant profile of that home account
if (tenantAcc.homeAccountId().contains(homeAcc.homeAccountId().substring(0, homeAcc.homeAccountId().indexOf(".")))) {
TenantProfile profile = (TenantProfile) tenantAcc;
tenantProfiles.put(profile.tenantId(), profile);
tenantAcctsIterator.remove();
}
}
if (tenantProfiles.isEmpty()) {
//If the home account has no tenant profiles, leave it as an Account object
rootAccounts.add(homeAcc);
} else {
//If the home account had some tenant profiles, transform the home Account object into a
//MultiTenantAccount object and attach its tenant profile list
MultiTenantAccount multAcct = new MultiTenantAccount(
homeAcc.homeAccountId(), homeAcc.environment(), homeAcc.username(), idTokens);
multAcct.setTenantProfiles(tenantProfiles);
rootAccounts.add(multAcct);
}
}
//Add any tenant accounts which did not have a corresponding home account to the list of root accounts
rootAccounts.addAll(tenantAccounts);

return rootAccounts;
Avery-Dunn marked this conversation as resolved.
Show resolved Hide resolved
} finally {
lock.readLock().unlock();
}
Expand Down
76 changes: 76 additions & 0 deletions src/test/java/com/microsoft/aad/msal4j/AccountTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

package com.microsoft.aad.msal4j;

import org.testng.Assert;
import org.testng.annotations.Test;
import java.io.IOException;
import java.net.URISyntaxException;
import java.util.Iterator;
import java.util.Map;

public class AccountTest {

@Test
public void testMultiTenantAccount_ClassTypes() throws IOException, URISyntaxException {

ITokenCacheAccessAspect accountCache = new CachePersistenceIT.TokenPersistence(
TestHelper.readResource(this.getClass(),
"/cache_data/multi-tenant-account-cache.json"));

PublicClientApplication app = PublicClientApplication.builder("uid1")
.setTokenCacheAccessAspect(accountCache).build();

Assert.assertEquals(app.getAccounts().join().size(), 3);
Iterator<IAccount> acctIterator = app.getAccounts().join().iterator();

IAccount curAccount;
IAccount account = null, multiAccount = null, tenantProfileAccount = null;
while (acctIterator.hasNext()) {
curAccount = acctIterator.next();
switch (curAccount.username()) {
case "Account":
account = curAccount;
break;
case "MultiAccount":
multiAccount = curAccount;
break;
case "TenantProfileNoHome":
tenantProfileAccount = curAccount;
break;
}
}

Assert.assertTrue(account instanceof Account);
Assert.assertTrue(multiAccount instanceof MultiTenantAccount);
Assert.assertTrue(tenantProfileAccount instanceof TenantProfile);
}

@Test
public void testMultiTenantAccount_AccessTenantProfile() throws IOException, URISyntaxException {

ITokenCacheAccessAspect accountCache = new CachePersistenceIT.TokenPersistence(
TestHelper.readResource(this.getClass(),
"/cache_data/multi-tenant-account-cache.json"));

PublicClientApplication app = PublicClientApplication.builder("uid1")
.setTokenCacheAccessAspect(accountCache).build();

Assert.assertEquals(app.getAccounts().join().size(), 3);
Iterator<IAccount> acctIterator = app.getAccounts().join().iterator();

IAccount curAccount;
while (acctIterator.hasNext()) {
curAccount = acctIterator.next();

if (curAccount.username().equals("MultiAccount")) {
Assert.assertTrue(curAccount instanceof MultiTenantAccount);
Map<String, ITenantProfile> tenantProfiles = ((MultiTenantAccount) curAccount).getTenantProfiles();
Assert.assertNotNull(tenantProfiles);
Assert.assertNotNull(tenantProfiles.get("tenantprofile1"));
Assert.assertEquals(tenantProfiles.get("tenantprofile1").username(), "TenantProfile");
}
}
}
}
37 changes: 37 additions & 0 deletions src/test/resources/cache_data/multi-tenant-account-cache.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
{
"Account": {
"uid1.utid1-login.windows.net-contoso": {
"username": "MultiAccount",
"local_account_id": "uid1",
"realm": "contoso",
"environment": "login.windows.net",
"home_account_id": "uid1.utid1",
"authority_type": "MSSTS"
},
"uid1.utid2-login.windows.net-contoso": {
"username": "TenantProfile",
"local_account_id": "tenantprofile1",
"realm": "contoso",
"environment": "login.windows.net",
"home_account_id": "uid1.utid1",
"authority_type": "MSSTS"
},
"uid2.utid2-login.windows.net-contoso": {
"username": "TenantProfileNoHome",
"local_account_id": "tenantprofile2",
"realm": "contoso",
"environment": "login.windows.net",
"home_account_id": "uid2.utid2",
"authority_type": "MSSTS"
},
"uid3.utid3-login.windows.net-contoso": {
"username": "Account",
"local_account_id": "uid3",
"realm": "contoso",
"environment": "login.windows.net",
"home_account_id": "uid3.utid3",
"authority_type": "MSSTS"
}
},
"AppMetadata": {}
}