Skip to content

Commit 086fcf1

Browse files
committed
Add persistence of MSAL tokens
1 parent c500bc1 commit 086fcf1

File tree

2 files changed

+139
-49
lines changed

2 files changed

+139
-49
lines changed

sdmx-dl-provider-ri/src/main/java/sdmxdl/provider/ri/authenticators/MsalAuthenticator.java

Lines changed: 57 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
import internal.util.credentials.WinPasswordVault;
55
import lombok.NonNull;
66
import nbbrd.design.DirectImpl;
7+
import nbbrd.design.VisibleForTesting;
8+
import nbbrd.io.sys.OS;
79
import nbbrd.io.text.Formatter;
810
import nbbrd.io.text.Parser;
911
import nbbrd.io.text.Property;
@@ -21,11 +23,15 @@
2123
import java.net.MalformedURLException;
2224
import java.net.PasswordAuthentication;
2325
import java.net.URI;
24-
import java.util.*;
26+
import java.util.Collection;
27+
import java.util.HashSet;
28+
import java.util.List;
29+
import java.util.Set;
2530
import java.util.concurrent.CompletionException;
2631
import java.util.concurrent.ConcurrentHashMap;
2732
import java.util.concurrent.ConcurrentMap;
2833
import java.util.function.BiConsumer;
34+
import java.util.logging.Level;
2935

3036
import static java.util.Collections.emptyList;
3137
import static nbbrd.io.function.IOFunction.unchecked;
@@ -38,6 +44,10 @@
3844
@ServiceProvider
3945
public final class MsalAuthenticator implements Authenticator {
4046

47+
@PropertyDefinition
48+
public static final Property<String> UID_PROPERTY =
49+
Property.of(AUTHENTICATOR_PROPERTY_PREFIX + ".uid", null, Parser.onString(), Formatter.onString());
50+
4151
@PropertyDefinition
4252
public static final Property<String> CLIENT_ID_PROPERTY =
4353
Property.of(AUTHENTICATOR_PROPERTY_PREFIX + ".clientId", null, Parser.onString(), Formatter.onString());
@@ -54,7 +64,7 @@ public final class MsalAuthenticator implements Authenticator {
5464
public static final Property<URI> REDIRECT_URI_PROPERTY =
5565
Property.of(AUTHENTICATOR_PROPERTY_PREFIX + ".redirectUri", URI.create("http://localhost"), Parser.onURI(), Formatter.onURI());
5666

57-
private static final ConcurrentMap<String, IPublicClientApplication> cache = new ConcurrentHashMap<>();
67+
private final ConcurrentMap<String, IPublicClientApplication> cache = new ConcurrentHashMap<>();
5868

5969
@Override
6070
public @NonNull String getAuthenticatorId() {
@@ -70,7 +80,7 @@ public boolean isAuthenticatorAvailable() {
7080
public @Nullable PasswordAuthentication getPasswordAuthenticationOrNull(@NonNull WebSource source) throws IOException {
7181
MsalConfig config = MsalConfig.parse(source);
7282
if (config != null) {
73-
IPublicClientApplication app = getClientApplication(source, config);
83+
IPublicClientApplication app = getClientApplication(config);
7484
return newToken(acquireToken(app, config.getScopes(), config.getRedirectUri()).accessToken());
7585
}
7686
return null;
@@ -80,31 +90,45 @@ public boolean isAuthenticatorAvailable() {
8090
public void invalidateAuthentication(@NonNull WebSource source) throws IOException {
8191
MsalConfig config = MsalConfig.parse(source);
8292
if (config != null) {
83-
cache.remove(TypedId.getUniqueID(source));
93+
cache.remove(config.getUid());
8494
}
8595
}
8696

8797
@Override
8898
public @NonNull Collection<String> getAuthenticatorProperties() {
8999
return keysOf(
100+
UID_PROPERTY,
90101
CLIENT_ID_PROPERTY,
91102
AUTHORITY_PROPERTY,
92103
SCOPES_PROPERTY,
93104
REDIRECT_URI_PROPERTY
94105
);
95106
}
96107

108+
@VisibleForTesting
97109
@lombok.Value
98-
public static class MsalConfig {
110+
static class MsalConfig {
111+
112+
@NonNull
113+
String uid;
99114

115+
@NonNull
100116
String clientId;
117+
118+
@NonNull
101119
String authority;
120+
121+
@NonNull
102122
Set<String> scopes;
123+
124+
@NonNull
103125
URI redirectUri;
104126

105-
public static MsalConfig parse(WebSource source) throws IOException {
127+
public static @Nullable MsalConfig parse(@NonNull WebSource source) throws IOException {
106128
if (AuthSchemes.MSAL_AUTH_SCHEME.equals(DriverProperties.AUTH_SCHEME_PROPERTY.get(source.getProperties()))) {
129+
String uid = UID_PROPERTY.get(source.getProperties());
107130
return new MsalConfig(
131+
uid != null && !uid.isEmpty() ? uid : TypedId.getUniqueID(source),
108132
getNotNull(CLIENT_ID_PROPERTY, source),
109133
getNotNull(AUTHORITY_PROPERTY, source),
110134
new HashSet<>(getNotNull(SCOPES_PROPERTY, source)),
@@ -115,50 +139,47 @@ public static MsalConfig parse(WebSource source) throws IOException {
115139
}
116140
}
117141

118-
private static IPublicClientApplication getClientApplication(WebSource source, MsalConfig config) throws IOException {
142+
private IPublicClientApplication getClientApplication(MsalConfig config) throws IOException {
119143
try {
120-
return cache.computeIfAbsent(TypedId.getUniqueID(source), unchecked(key -> newClientApplication(source, config, key)));
144+
return cache.computeIfAbsent(config.getUid(), unchecked(uid -> newClientApplication(config)));
121145
} catch (UncheckedIOException ex) {
122146
throw ex.getCause();
123147
}
124148
}
125149

126-
private static IPublicClientApplication newClientApplication(WebSource source, MsalConfig config, String key) throws MalformedURLException {
127-
return newClientApplication(config.getClientId(), config.getAuthority(), newTokenPersistence(key, key));
128-
}
129-
130-
private static IPublicClientApplication newClientApplication(String clientId, String authority, ITokenCacheAccessAspect tokenPersistence) throws MalformedURLException {
150+
private static IPublicClientApplication newClientApplication(MsalConfig config) throws MalformedURLException {
131151
return PublicClientApplication
132-
.builder(clientId)
133-
.authority(authority)
134-
.setTokenCacheAccessAspect(tokenPersistence)
152+
.builder(config.getClientId())
153+
.authority(config.getAuthority())
154+
.setTokenCacheAccessAspect(newTokenPersistence(config.getUid(), config.getUid()))
135155
.build();
136156
}
137157

138158
private static ITokenCacheAccessAspect newTokenPersistence(String resource, String userName) {
139-
// return OS.NAME.equals(OS.Name.WINDOWS)
140-
// ? new VaultTokenPersistence(resource, userName, (msg, e) -> log.log(Level.SEVERE, msg, e))
141-
// : NoOpTokenPersistence.INSTANCE;
142-
return NoOpTokenPersistence.INSTANCE;
159+
return OS.NAME.equals(OS.Name.WINDOWS)
160+
? new VaultTokenPersistence(resource, userName, (msg, e) -> log.log(Level.SEVERE, msg, e))
161+
: NoOpTokenPersistence.INSTANCE;
143162
}
144163

145164
private static IAuthenticationResult acquireToken(IPublicClientApplication app, Set<String> scopes, URI redirectUri) throws IOException {
146-
try {
147-
return app.acquireTokenSilently(SilentParameters
148-
.builder(scopes)
149-
.account(app.getAccounts().join().stream().findFirst().orElse(null))
150-
.build())
151-
.join();
152-
} catch (CompletionException ex) {
153-
if (ex.getCause() instanceof MsalException) {
154-
return app.acquireToken(InteractiveRequestParameters
155-
.builder(redirectUri)
156-
.scopes(scopes)
157-
.prompt(Prompt.SELECT_ACCOUNT)
165+
synchronized (app) {
166+
try {
167+
return app.acquireTokenSilently(SilentParameters
168+
.builder(scopes)
169+
.account(app.getAccounts().join().stream().findFirst().orElse(null))
158170
.build())
159171
.join();
160-
} else {
161-
throw new IOException(ex.getCause());
172+
} catch (CompletionException ex) {
173+
if (ex.getCause() instanceof MsalException) {
174+
return app.acquireToken(InteractiveRequestParameters
175+
.builder(redirectUri)
176+
.scopes(scopes)
177+
.prompt(Prompt.SELECT_ACCOUNT)
178+
.build())
179+
.join();
180+
} else {
181+
throw new IOException(ex.getCause());
182+
}
162183
}
163184
}
164185
}
@@ -198,7 +219,7 @@ public void beforeCacheAccess(ITokenCacheAccessContext context) {
198219
try (WinPasswordVault vault = WinPasswordVault.open()) {
199220
WinPasswordVault.PasswordCredential credential = vault.get(resource);
200221
if (credential != null && credential.getUserName().equals(userName)) {
201-
context.tokenCache().deserialize(Arrays.toString(credential.getPassword()));
222+
context.tokenCache().deserialize(String.valueOf(credential.getPassword()));
202223
}
203224
} catch (IOException e) {
204225
onError.accept("Failed to access token cache from Windows Password Vault", e);
@@ -209,6 +230,7 @@ public void beforeCacheAccess(ITokenCacheAccessContext context) {
209230
public void afterCacheAccess(ITokenCacheAccessContext context) {
210231
if (context.hasCacheChanged()) {
211232
try (WinPasswordVault vault = WinPasswordVault.open()) {
233+
vault.invalidate(resource);
212234
vault.add(new WinPasswordVault.PasswordCredential(resource, userName, context.tokenCache().serialize().toCharArray()));
213235
} catch (IOException e) {
214236
onError.accept("Failed to update token cache in Windows Password Vault", e);

sdmx-dl-provider-ri/src/test/java/sdmxdl/provider/ri/authenticators/MsalAuthenticatorTest.java

Lines changed: 82 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,13 @@
44
import sdmxdl.web.WebSource;
55
import tests.sdmxdl.web.spi.AuthenticatorAssert;
66

7+
import java.io.IOException;
8+
import java.net.URI;
9+
10+
import static java.util.Collections.emptySet;
11+
import static java.util.Collections.singleton;
12+
import static org.assertj.core.api.Assertions.assertThat;
13+
import static org.assertj.core.api.Assertions.assertThatIOException;
714
import static sdmxdl.provider.ri.authenticators.MsalAuthenticator.*;
815
import static sdmxdl.provider.ri.drivers.AuthSchemes.MSAL_AUTH_SCHEME;
916
import static sdmxdl.provider.web.DriverProperties.AUTH_SCHEME_PROPERTY;
@@ -13,27 +20,88 @@ public class MsalAuthenticatorTest {
1320

1421
@Test
1522
public void testCompliance() {
16-
WebSource ignoring = WebSource
17-
.builder()
18-
.id("valid")
19-
.driver("driver")
20-
.endpointOf("http://localhost")
21-
.build();
2223

2324
assertCompliance(
2425
new MsalAuthenticator(),
2526
AuthenticatorAssert.Sample
2627
.builder()
2728
.ignoring(ignoring)
28-
.invalid(ignoring.toBuilder().propertyOf(AUTH_SCHEME_PROPERTY, MSAL_AUTH_SCHEME).build())
29-
// .valid(ignoring
30-
// .toBuilder()
31-
// .propertyOf(AUTH_SCHEME_PROPERTY, MSAL_AUTH_SCHEME)
32-
// .propertyOf(CLIENT_ID_PROPERTY, "client-id")
33-
// .propertyOf(AUTHORITY_PROPERTY, "https://localhost")
34-
// .propertyOf(SCOPE_PROPERTY, "scope")
35-
// .build())
29+
.invalid(invalid)
30+
// .valid(valid)
3631
.build()
3732
);
3833
}
34+
35+
@SuppressWarnings("DataFlowIssue")
36+
@Test
37+
public void testConfig() throws IOException {
38+
WebSource ignoring = WebSource
39+
.builder()
40+
.id("valid")
41+
.driver("driver")
42+
.endpointOf("http://localhost")
43+
.build();
44+
45+
assertThat(MsalAuthenticator.MsalConfig.parse(ignoring)).isNull();
46+
47+
assertThatIOException().isThrownBy(() ->
48+
MsalAuthenticator.MsalConfig.parse(invalid)
49+
);
50+
51+
assertThatIOException().isThrownBy(() ->
52+
MsalAuthenticator.MsalConfig.parse(invalid
53+
.toBuilder()
54+
.propertyOf(CLIENT_ID_PROPERTY, "client-id")
55+
.build())
56+
);
57+
58+
assertThat(MsalAuthenticator.MsalConfig.parse(valid1))
59+
.returns("valid_c47f08b", MsalAuthenticator.MsalConfig::getUid)
60+
.returns("client-id", MsalAuthenticator.MsalConfig::getClientId)
61+
.returns("https://localhost", MsalAuthenticator.MsalConfig::getAuthority)
62+
.returns(emptySet(), MsalAuthenticator.MsalConfig::getScopes)
63+
.returns(URI.create("http://localhost"), MsalAuthenticator.MsalConfig::getRedirectUri);
64+
65+
assertThat(MsalAuthenticator.MsalConfig.parse(valid2))
66+
.returns("valid_0efdcc7", MsalAuthenticator.MsalConfig::getUid)
67+
.returns("client-id", MsalAuthenticator.MsalConfig::getClientId)
68+
.returns("https://localhost", MsalAuthenticator.MsalConfig::getAuthority)
69+
.returns(singleton("scope"), MsalAuthenticator.MsalConfig::getScopes)
70+
.returns(URI.create("http://localhost"), MsalAuthenticator.MsalConfig::getRedirectUri);
71+
72+
assertThat(MsalAuthenticator.MsalConfig.parse(valid3))
73+
.returns("abc", MsalAuthenticator.MsalConfig::getUid)
74+
.returns("client-id", MsalAuthenticator.MsalConfig::getClientId)
75+
.returns("https://localhost", MsalAuthenticator.MsalConfig::getAuthority)
76+
.returns(singleton("scope"), MsalAuthenticator.MsalConfig::getScopes)
77+
.returns(URI.create("http://localhost"), MsalAuthenticator.MsalConfig::getRedirectUri);
78+
}
79+
80+
private final WebSource ignoring = WebSource
81+
.builder()
82+
.id("valid")
83+
.driver("driver")
84+
.endpointOf("http://localhost")
85+
.build();
86+
87+
private final WebSource invalid = ignoring
88+
.toBuilder()
89+
.propertyOf(AUTH_SCHEME_PROPERTY, MSAL_AUTH_SCHEME)
90+
.build();
91+
92+
private final WebSource valid1 = invalid
93+
.toBuilder()
94+
.propertyOf(CLIENT_ID_PROPERTY, "client-id")
95+
.propertyOf(AUTHORITY_PROPERTY, "https://localhost")
96+
.build();
97+
98+
private final WebSource valid2 = valid1
99+
.toBuilder()
100+
.propertyOf(SCOPES_PROPERTY, "scope")
101+
.build();
102+
103+
private final WebSource valid3 = valid2
104+
.toBuilder()
105+
.propertyOf(UID_PROPERTY, "abc")
106+
.build();
39107
}

0 commit comments

Comments
 (0)