Skip to content

Commit 0b20b14

Browse files
Avery-DunnSomkaPe
andauthored
Tenant Profiles (#263)
* Classes for tenant profile functionality * Implement tenant profile feature * Tests for tenant profile feature * Simplify tenant profile class structure * 1.6.2 release * Classes for tenant profile redesign * Tests for tenant profile redesign * Adjust sample cached ID tokens to have realistic headers * Redesign how Tenant Pofiles are added to Accounts * New error code for JWT parse exceptions * Add claims and tenant profiles fields to Account * Remove annotation excluding realm field from comparisons * Use more generic token * Remove ID token claims field from Account * Minor changes for clarity * Adjust tests for tenant profile design refactor * Refactor tenant profile structure * Minor fixes * Minor fixes * Minor fixes * Simplify tenant profile class Co-authored-by: SomkaPe <[email protected]>
1 parent fce61b2 commit 0b20b14

File tree

13 files changed

+280
-8
lines changed

13 files changed

+280
-8
lines changed

src/integrationtest/java/com.microsoft.aad.msal4j/TokenCacheIT.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ public void twoAccountsInCache_SameUserDifferentTenants_RemoveAccountTest() thro
146146
.get();
147147

148148
// There should be two tokens in cache, with same accounts except for tenant
149-
Assert.assertEquals(pca2.getAccounts().join().size() , 2);
149+
Assert.assertEquals(pca2.getAccounts().join().iterator().next().getTenantProfiles().size() , 2);
150150

151151
IAccount account = pca2.getAccounts().get().iterator().next();
152152

src/main/java/com/microsoft/aad/msal4j/Account.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import lombok.Getter;
88
import lombok.Setter;
99
import lombok.experimental.Accessors;
10+
import java.util.Map;
1011

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

2526
String username;
27+
28+
Map<String, ITenantProfile> tenantProfiles;
29+
30+
public Map<String, ITenantProfile> getTenantProfiles() {
31+
return tenantProfiles;
32+
}
2633
}

src/main/java/com/microsoft/aad/msal4j/AccountCacheEntity.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import java.io.Serializable;
1111
import java.util.ArrayList;
1212
import java.util.List;
13+
import java.util.Map;
1314

1415
@Accessors(fluent = true)
1516
@Getter
@@ -26,7 +27,6 @@ class AccountCacheEntity implements Serializable {
2627
@JsonProperty("environment")
2728
protected String environment;
2829

29-
@EqualsAndHashCode.Exclude
3030
@JsonProperty("realm")
3131
protected String realm;
3232

@@ -101,6 +101,6 @@ static AccountCacheEntity create(String clientInfoStr, Authority requestAuthorit
101101
}
102102

103103
IAccount toAccount(){
104-
return new Account(homeAccountId, environment, username);
104+
return new Account(homeAccountId, environment, username, null);
105105
}
106106
}

src/main/java/com/microsoft/aad/msal4j/AuthenticationErrorCode.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,4 +100,9 @@ public class AuthenticationErrorCode {
100100
* A JSON processing failure, indicating the JSON provided to MSAL is of invalid format.
101101
*/
102102
public final static String INVALID_JSON = "invalid_json";
103+
104+
/**
105+
* A JWT parsing failure, indicating the JWT provided to MSAL is of invalid format.
106+
*/
107+
public final static String INVALID_JWT = "invalid_jwt";
103108
}

src/main/java/com/microsoft/aad/msal4j/IAccount.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
package com.microsoft.aad.msal4j;
55

6-
import java.util.Set;
6+
import java.util.Map;
77

88
/**
99
* Interface representing a single user account. An IAccount is returned in the {@link IAuthenticationResult}
@@ -26,4 +26,12 @@ public interface IAccount {
2626
* @return account username
2727
*/
2828
String username();
29+
30+
/**
31+
* Map of {@link ITenantProfile} objects related to this account, the keys of the map are the tenant ID values and
32+
* match the 'realm' key of an ID token
33+
*
34+
* @return tenant profiles
35+
*/
36+
Map<String, ITenantProfile> getTenantProfiles();
2937
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
package com.microsoft.aad.msal4j;
5+
6+
import java.util.Map;
7+
8+
/**
9+
* Interface representing a single tenant profile. ITenantProfiles are made available through the
10+
* {@link IAccount#getTenantProfiles()} method of an Account
11+
*
12+
*/
13+
public interface ITenantProfile {
14+
15+
/**
16+
* A map of claims taken from an ID token. Keys and values will follow the structure of a JSON Web Token
17+
*
18+
* @return Map claims in id token
19+
*/
20+
Map<String, ?> getClaims();
21+
22+
}

src/main/java/com/microsoft/aad/msal4j/IdToken.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
import com.nimbusds.jwt.JWTClaimsSet;
88

99
import java.text.ParseException;
10+
import java.util.HashMap;
11+
import java.util.Map;
1012

1113
class IdToken {
1214

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
package com.microsoft.aad.msal4j;
5+
6+
import lombok.AllArgsConstructor;
7+
import lombok.Getter;
8+
import lombok.Setter;
9+
import lombok.experimental.Accessors;
10+
import java.util.Map;
11+
12+
/**
13+
* Representation of a single tenant profile
14+
*/
15+
@Accessors(fluent = true)
16+
@Getter
17+
@Setter
18+
@AllArgsConstructor
19+
class TenantProfile implements ITenantProfile {
20+
21+
Map<String, ?> idTokenClaims;
22+
23+
public Map<String, ?> getClaims() {
24+
return idTokenClaims;
25+
}
26+
}

src/main/java/com/microsoft/aad/msal4j/TokenCache.java

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,10 @@
66
import com.fasterxml.jackson.annotation.JsonProperty;
77
import com.fasterxml.jackson.databind.JsonNode;
88
import com.fasterxml.jackson.databind.node.ObjectNode;
9+
import com.nimbusds.jwt.JWTParser;
910

1011
import java.io.IOException;
12+
import java.text.ParseException;
1113
import java.util.*;
1214
import java.util.concurrent.locks.ReadWriteLock;
1315
import java.util.concurrent.locks.ReentrantReadWriteLock;
@@ -300,16 +302,63 @@ Set<IAccount> getAccounts(String clientId, Set<String> environmentAliases) {
300302
build())) {
301303
try {
302304
lock.readLock().lock();
305+
Map<String, IAccount> rootAccounts = new HashMap<>();
303306

304-
return accounts.values().stream().
307+
Set<AccountCacheEntity> accountsCached = accounts.values().stream().
305308
filter(acc -> environmentAliases.contains(acc.environment())).
306-
collect(Collectors.mapping(AccountCacheEntity::toAccount, Collectors.toSet()));
309+
collect(Collectors.toSet());
310+
311+
for (AccountCacheEntity accCached : accountsCached) {
312+
313+
IdTokenCacheEntity idToken = idTokens.get(getIdTokenKey(
314+
accCached.homeAccountId(),
315+
accCached.environment(),
316+
clientId,
317+
accCached.realm()));
318+
319+
Map<String, ?> idTokenClaims = null;
320+
if (idToken != null) {
321+
idTokenClaims = JWTParser.parse(idToken.secret()).getJWTClaimsSet().getClaims();
322+
}
323+
324+
ITenantProfile profile = new TenantProfile(idTokenClaims);
325+
326+
if (rootAccounts.get(accCached.homeAccountId()) == null) {
327+
IAccount acc = accCached.toAccount();
328+
((Account) acc).tenantProfiles = new HashMap<>();
329+
((Account) acc).tenantProfiles().put(accCached.realm(), profile);
330+
331+
rootAccounts.put(accCached.homeAccountId(), acc);
332+
} else {
333+
((Account)rootAccounts.get(accCached.homeAccountId())).tenantProfiles.put(accCached.realm(), profile);
334+
if (accCached.homeAccountId().contains(accCached.localAccountId())) {
335+
((Account) rootAccounts.get(accCached.homeAccountId())).username(accCached.username());
336+
}
337+
}
338+
}
339+
340+
return new HashSet<>(rootAccounts.values());
341+
} catch (ParseException e) {
342+
throw new MsalClientException("Cached JWT could not be parsed: " + e.getMessage(), AuthenticationErrorCode.INVALID_JWT);
307343
} finally {
308344
lock.readLock().unlock();
309345
}
310346
}
311347
}
312348

349+
/**
350+
* Returns a String representing a key of a cached ID token, formatted in the same way as {@link IdTokenCacheEntity#getKey}
351+
*
352+
* @return String representing a possible key of a cached ID token
353+
*/
354+
private String getIdTokenKey(String homeAccountId, String environment, String clientId, String realm) {
355+
return String.join(Constants.CACHE_KEY_SEPARATOR,
356+
Arrays.asList(homeAccountId,
357+
environment,
358+
"idtoken", clientId,
359+
realm, "")).toLowerCase();
360+
}
361+
313362
/**
314363
* @return familyId status of application
315364
*/

src/samples/cache/sample_cache.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
"realm": "contoso",
3939
"environment": "login.windows.net",
4040
"credential_type": "IdToken",
41-
"secret": "header.eyJvaWQiOiAib2JqZWN0MTIzNCIsICJwcmVmZXJyZWRfdXNlcm5hbWUiOiAiSm9obiBEb2UiLCAic3ViIjogInN1YiJ9.signature",
41+
"secret": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6Imh1Tjk1SXZQZmVocTM0R3pCRFoxR1hHaXJuTSJ9.eyJvaWQiOiAib2JqZWN0MTIzNCIsICJwcmVmZXJyZWRfdXNlcm5hbWUiOiAiSm9obiBEb2UiLCAic3ViIjogInN1YiJ9.signature",
4242
"client_id": "my_client_id",
4343
"home_account_id": "uid.utid"
4444
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
package com.microsoft.aad.msal4j;
5+
6+
import org.testng.Assert;
7+
import org.testng.annotations.Test;
8+
import java.io.IOException;
9+
import java.net.URISyntaxException;
10+
import java.util.Iterator;
11+
import java.util.Map;
12+
13+
public class AccountTest {
14+
15+
@Test
16+
public void testMultiTenantAccount_AccessTenantProfile() throws IOException, URISyntaxException {
17+
18+
ITokenCacheAccessAspect accountCache = new CachePersistenceIT.TokenPersistence(
19+
TestHelper.readResource(this.getClass(),
20+
"/cache_data/multi-tenant-account-cache.json"));
21+
22+
PublicClientApplication app = PublicClientApplication.builder("client_id")
23+
.setTokenCacheAccessAspect(accountCache).build();
24+
25+
Assert.assertEquals(app.getAccounts().join().size(), 3);
26+
Iterator<IAccount> acctIterator = app.getAccounts().join().iterator();
27+
28+
IAccount curAccount;
29+
while (acctIterator.hasNext()) {
30+
curAccount = acctIterator.next();
31+
32+
switch (curAccount.username()) {
33+
case "MultiTenantAccount": {
34+
Assert.assertEquals(curAccount.homeAccountId(), "uid1.utid1");
35+
Map<String, ITenantProfile> tenantProfiles = curAccount.getTenantProfiles();
36+
Assert.assertNotNull(tenantProfiles);
37+
Assert.assertEquals(tenantProfiles.size(), 3);
38+
Assert.assertNotNull(tenantProfiles.get("utid1"));
39+
Assert.assertNotNull(tenantProfiles.get("utid1").getClaims());
40+
Assert.assertNotNull(tenantProfiles.get("utid2"));
41+
Assert.assertNotNull(tenantProfiles.get("utid2").getClaims());
42+
Assert.assertNotNull(tenantProfiles.get("utid3"));
43+
Assert.assertNotNull(tenantProfiles.get("utid3").getClaims());
44+
break;
45+
}
46+
case "SingleTenantAccount": {
47+
Assert.assertEquals(curAccount.homeAccountId(), "uid6.utid5");
48+
Map<String, ITenantProfile> tenantProfiles = curAccount.getTenantProfiles();
49+
Assert.assertNotNull(tenantProfiles);
50+
Assert.assertEquals(tenantProfiles.size(), 1);
51+
Assert.assertNotNull(tenantProfiles.get("utid5"));
52+
Assert.assertNotNull(tenantProfiles.get("utid5").getClaims());
53+
break;
54+
}
55+
case "TenantProfileNoHome": {
56+
Assert.assertEquals(curAccount.homeAccountId(), "uid5.utid4");
57+
Map<String, ITenantProfile> tenantProfiles = curAccount.getTenantProfiles();
58+
Assert.assertNotNull(tenantProfiles);
59+
Assert.assertEquals(tenantProfiles.size(), 1);
60+
Assert.assertNotNull(tenantProfiles.get("utid4"));
61+
Assert.assertNotNull(tenantProfiles.get("utid4").getClaims());
62+
break;
63+
}
64+
}
65+
}
66+
}
67+
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
{
2+
"Account": {
3+
"uid1.utid1-login.windows.net-utid1": {
4+
"username": "MultiTenantAccount",
5+
"local_account_id": "uid1",
6+
"realm": "utid1",
7+
"environment": "login.windows.net",
8+
"home_account_id": "uid1.utid1",
9+
"authority_type": "MSSTS"
10+
},
11+
"uid1.utid1-login.windows.net-utid2": {
12+
"username": "TenantProfile1",
13+
"local_account_id": "uid2",
14+
"realm": "utid2",
15+
"environment": "login.windows.net",
16+
"home_account_id": "uid1.utid1",
17+
"authority_type": "MSSTS"
18+
},
19+
"uid1.utid1-login.windows.net-utid3": {
20+
"username": "TenantProfile2",
21+
"local_account_id": "uid3",
22+
"realm": "utid3",
23+
"environment": "login.windows.net",
24+
"home_account_id": "uid1.utid1",
25+
"authority_type": "MSSTS"
26+
},
27+
"uid5.utid4-login.windows.net-utid4": {
28+
"username": "TenantProfileNoHome",
29+
"local_account_id": "uid4",
30+
"realm": "utid4",
31+
"environment": "login.windows.net",
32+
"home_account_id": "uid5.utid4",
33+
"authority_type": "MSSTS"
34+
},
35+
"uid6.utid5-login.windows.net-utid5": {
36+
"username": "SingleTenantAccount",
37+
"local_account_id": "uid6",
38+
"realm": "utid5",
39+
"environment": "login.windows.net",
40+
"home_account_id": "uid6.utid5",
41+
"authority_type": "MSSTS"
42+
}
43+
},
44+
"IdToken": {
45+
"uid1.utid1-login.windows.net-idtoken-client_id-utid1-": {
46+
"realm": "utid1",
47+
"environment": "login.windows.net",
48+
"credential_type": "IdToken",
49+
"secret": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6Imh1Tjk1SXZQZmVocTM0R3pCRFoxR1hHaXJuTSJ9.eyJvaWQiOiAib2JqZWN0MTIzNCIsICJwcmVmZXJyZWRfdXNlcm5hbWUiOiAiSm9obiBEb2UiLCAic3ViIjogInN1YiJ9.signature",
50+
"client_id": "client_id",
51+
"home_account_id": "uid.utid1"
52+
},
53+
"uid1.utid1-login.windows.net-idtoken-client_id-utid2-": {
54+
"realm": "utid2",
55+
"environment": "login.windows.net",
56+
"credential_type": "IdToken",
57+
"secret": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6Imh1Tjk1SXZQZmVocTM0R3pCRFoxR1hHaXJuTSJ9.eyJvaWQiOiAib2JqZWN0MTIzNCIsICJwcmVmZXJyZWRfdXNlcm5hbWUiOiAiSm9obiBEb2UiLCAic3ViIjogInN1YiJ9.signature",
58+
"client_id": "client_id",
59+
"home_account_id": "uid.utid1"
60+
},
61+
"uid1.utid1-login.windows.net-idtoken-client_id-utid3-": {
62+
"realm": "utid3",
63+
"environment": "login.windows.net",
64+
"credential_type": "IdToken",
65+
"secret": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6Imh1Tjk1SXZQZmVocTM0R3pCRFoxR1hHaXJuTSJ9.eyJvaWQiOiAib2JqZWN0MTIzNCIsICJwcmVmZXJyZWRfdXNlcm5hbWUiOiAiSm9obiBEb2UiLCAic3ViIjogInN1YiJ9.signature",
66+
"client_id": "client_id",
67+
"home_account_id": "uid.utid1"
68+
},
69+
"uid5.utid4-login.windows.net-idtoken-client_id-utid4-": {
70+
"realm": "utid4",
71+
"environment": "login.windows.net",
72+
"credential_type": "IdToken",
73+
"secret": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6Imh1Tjk1SXZQZmVocTM0R3pCRFoxR1hHaXJuTSJ9.eyJvaWQiOiAib2JqZWN0MTIzNCIsICJwcmVmZXJyZWRfdXNlcm5hbWUiOiAiSm9obiBEb2UiLCAic3ViIjogInN1YiJ9.signature",
74+
"client_id": "client_id",
75+
"home_account_id": "uid5.utid4"
76+
},
77+
"uid6.utid5-login.windows.net-idtoken-client_id-utid5-": {
78+
"realm": "utid5",
79+
"environment": "login.windows.net",
80+
"credential_type": "IdToken",
81+
"secret": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6Imh1Tjk1SXZQZmVocTM0R3pCRFoxR1hHaXJuTSJ9.eyJvaWQiOiAib2JqZWN0MTIzNCIsICJwcmVmZXJyZWRfdXNlcm5hbWUiOiAiSm9obiBEb2UiLCAic3ViIjogInN1YiJ9.signature",
82+
"client_id": "client_id",
83+
"home_account_id": "uid6.utid5"
84+
}
85+
}
86+
}

src/test/resources/cache_data/serialized_cache.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@
4141
"realm": "contoso",
4242
"environment": "login.windows.net",
4343
"credential_type": "IdToken",
44-
"secret": "header.eyJvaWQiOiAib2JqZWN0MTIzNCIsICJwcmVmZXJyZWRfdXNlcm5hbWUiOiAiSm9obiBEb2UiLCAic3ViIjogInN1YiJ9.signature",
44+
"secret": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6Imh1Tjk1SXZQZmVocTM0R3pCRFoxR1hHaXJuTSJ9.eyJvaWQiOiAib2JqZWN0MTIzNCIsICJwcmVmZXJyZWRfdXNlcm5hbWUiOiAiSm9obiBEb2UiLCAic3ViIjogInN1YiJ9.signature",
4545
"client_id": "my_client_id",
4646
"home_account_id": "uid.utid"
4747
}

0 commit comments

Comments
 (0)