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 25 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
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ public void twoAccountsInCache_SameUserDifferentTenants_RemoveAccountTest() thro
.get();

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

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

Expand Down
7 changes: 7 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,10 @@ class Account implements IAccount {
String environment;

String username;

Map<String, ITenantProfile> tenantProfiles;

public Map<String, ITenantProfile> getTenantProfiles() {
return tenantProfiles;
}
}
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 All @@ -26,7 +27,6 @@ class AccountCacheEntity implements Serializable {
@JsonProperty("environment")
protected String environment;

@EqualsAndHashCode.Exclude
@JsonProperty("realm")
protected String realm;

Expand Down Expand Up @@ -101,6 +101,6 @@ static AccountCacheEntity create(String clientInfoStr, Authority requestAuthorit
}

IAccount toAccount(){
return new Account(homeAccountId, environment, username);
return new Account(homeAccountId, environment, username, null);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -100,4 +100,9 @@ public class AuthenticationErrorCode {
* A JSON processing failure, indicating the JSON provided to MSAL is of invalid format.
*/
public final static String INVALID_JSON = "invalid_json";

/**
* A JWT parsing failure, indicating the JWT provided to MSAL is of invalid format.
*/
public final static String INVALID_JWT = "invalid_jwt";
}
10 changes: 9 additions & 1 deletion src/main/java/com/microsoft/aad/msal4j/IAccount.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

package com.microsoft.aad.msal4j;

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

/**
* Interface representing a single user account. An IAccount is returned in the {@link IAuthenticationResult}
Expand All @@ -26,4 +26,12 @@ public interface IAccount {
* @return account username
*/
String username();

/**
* Map of {@link ITenantProfile} objects related to this account. The keys of the map are tenant ID values, which
* are taken from {@link AccountCacheEntity#realm}
Avery-Dunn marked this conversation as resolved.
Show resolved Hide resolved
*
* @return tenant profiles
Avery-Dunn marked this conversation as resolved.
Show resolved Hide resolved
*/
Map<String, ITenantProfile> getTenantProfiles();
}
36 changes: 36 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,36 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

package com.microsoft.aad.msal4j;

import java.util.Map;

/**
* Interface representing a single tenant profile. ITenantProfiles are made available through the
* {@link IAccount#getTenantProfiles()} method of an Account
*
*/
public interface ITenantProfile {

/**
* This value corresponds to the 'oid' key of an ID token
*
* @return String local OID
*/
String getId();
Avery-Dunn marked this conversation as resolved.
Show resolved Hide resolved

/**
* This value corresponds to the 'realm' key of an ID token
*
* @return String tenant ID
*/
String getTenantId();

/**
* A map of claims taken from an ID token. Keys and values will follow the structure of a JSON Web Token
*
* @return Map claims in id token
*/
Map<String, ?> getClaims();

}
2 changes: 2 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
37 changes: 37 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,37 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

package com.microsoft.aad.msal4j;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import lombok.experimental.Accessors;
import java.util.Map;

/**
* Representation of a single tenant profile
*/
@Accessors(fluent = true)
@Getter
@Setter
@AllArgsConstructor
class TenantProfile implements ITenantProfile {
String id;

String tenantID;

Map<String, ?> idTokenClaims;

public String getId() {
return id;
}

public String getTenantId() {
return tenantID;
}

public Map<String, ?> getClaims() {
return idTokenClaims;
}
}
55 changes: 53 additions & 2 deletions src/main/java/com/microsoft/aad/msal4j/TokenCache.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.nimbusds.jwt.JWTParser;

import java.io.IOException;
import java.text.ParseException;
import java.util.*;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
Expand Down Expand Up @@ -300,16 +302,65 @@ Set<IAccount> getAccounts(String clientId, Set<String> environmentAliases) {
build())) {
try {
lock.readLock().lock();
Map<String, IAccount> rootAccounts = new HashMap<>();

return accounts.values().stream().
Set<AccountCacheEntity> accountsCached = accounts.values().stream().
filter(acc -> environmentAliases.contains(acc.environment())).
collect(Collectors.mapping(AccountCacheEntity::toAccount, Collectors.toSet()));
collect(Collectors.toSet());

for (AccountCacheEntity accCached : accountsCached) {

IdTokenCacheEntity idToken = idTokens.get(getIdTokenKey(
accCached.homeAccountId(),
accCached.environment(),
clientId,
accCached.realm()));

Map<String, ?> idTokenClaims = null;
if (idToken != null) {
idTokenClaims = JWTParser.parse(idToken.secret()).getJWTClaimsSet().getClaims();
}

ITenantProfile profile = new TenantProfile(accCached.localAccountId(),
accCached.realm(),
idTokenClaims);

if (rootAccounts.get(accCached.homeAccountId()) == null) {
IAccount acc = accCached.toAccount();
((Account) acc).tenantProfiles = new HashMap<>();
((Account) acc).tenantProfiles().put(accCached.realm(), profile);

rootAccounts.put(accCached.homeAccountId(), acc);
} else {
((Account)rootAccounts.get(accCached.homeAccountId())).tenantProfiles.put(accCached.realm(), profile);
if (accCached.homeAccountId().contains(accCached.localAccountId())) {
((Account) rootAccounts.get(accCached.homeAccountId())).username(accCached.username());
}
}
}

return new HashSet<>(rootAccounts.values());
} catch (ParseException e) {
throw new MsalClientException("Cached JWT could not be parsed: " + e.getMessage(), AuthenticationErrorCode.INVALID_JWT);
} finally {
lock.readLock().unlock();
}
}
}

/**
* Returns a String representing a key of a cached ID token, formatted in the same way as {@link IdTokenCacheEntity#getKey}
*
* @return String representing a possible key of a cached ID token
*/
private String getIdTokenKey(String homeAccountId, String environment, String clientId, String realm) {
return String.join(Constants.CACHE_KEY_SEPARATOR,
Arrays.asList(homeAccountId,
environment,
"idtoken", clientId,
realm, "")).toLowerCase();
}

/**
* @return familyId status of application
*/
Expand Down
2 changes: 1 addition & 1 deletion src/samples/cache/sample_cache.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
"realm": "contoso",
"environment": "login.windows.net",
"credential_type": "IdToken",
"secret": "header.eyJvaWQiOiAib2JqZWN0MTIzNCIsICJwcmVmZXJyZWRfdXNlcm5hbWUiOiAiSm9obiBEb2UiLCAic3ViIjogInN1YiJ9.signature",
"secret": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6Imh1Tjk1SXZQZmVocTM0R3pCRFoxR1hHaXJuTSJ9.eyJvaWQiOiAib2JqZWN0MTIzNCIsICJwcmVmZXJyZWRfdXNlcm5hbWUiOiAiSm9obiBEb2UiLCAic3ViIjogInN1YiJ9.signature",
"client_id": "my_client_id",
"home_account_id": "uid.utid"
}
Expand Down
67 changes: 67 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,67 @@
// 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_AccessTenantProfile() throws IOException, URISyntaxException {

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

PublicClientApplication app = PublicClientApplication.builder("client_id")
.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();

switch (curAccount.username()) {
case "MultiTenantAccount": {
Assert.assertEquals(curAccount.homeAccountId(), "uid1.utid1");
Map<String, ITenantProfile> tenantProfiles = curAccount.getTenantProfiles();
Assert.assertNotNull(tenantProfiles);
Assert.assertEquals(tenantProfiles.size(), 3);
Assert.assertNotNull(tenantProfiles.get("utid1"));
Assert.assertNotNull(tenantProfiles.get("utid1").getClaims());
Assert.assertNotNull(tenantProfiles.get("utid2"));
Assert.assertNotNull(tenantProfiles.get("utid2").getClaims());
Assert.assertNotNull(tenantProfiles.get("utid3"));
Assert.assertNotNull(tenantProfiles.get("utid3").getClaims());
break;
}
case "SingleTenantAccount": {
Assert.assertEquals(curAccount.homeAccountId(), "uid6.utid5");
Map<String, ITenantProfile> tenantProfiles = curAccount.getTenantProfiles();
Assert.assertNotNull(tenantProfiles);
Assert.assertEquals(tenantProfiles.size(), 1);
Assert.assertNotNull(tenantProfiles.get("utid5"));
Assert.assertNotNull(tenantProfiles.get("utid5").getClaims());
break;
}
case "TenantProfileNoHome": {
Assert.assertEquals(curAccount.homeAccountId(), "uid5.utid4");
Map<String, ITenantProfile> tenantProfiles = curAccount.getTenantProfiles();
Assert.assertNotNull(tenantProfiles);
Assert.assertEquals(tenantProfiles.size(), 1);
Assert.assertNotNull(tenantProfiles.get("utid4"));
Assert.assertNotNull(tenantProfiles.get("utid4").getClaims());
break;
}
}
}
}
}
Loading