Repository navigation
Expand file tree
/
Copy pathDefaultLoginService.java
More file actions
310 lines (277 loc) · 14.4 KB
/
Copy pathDefaultLoginService.java
File metadata and controls
310 lines (277 loc) · 14.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package software.aws.toolkits.eclipse.amazonq.lsp.auth;
import java.time.Duration;
import java.time.Instant;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.atomic.AtomicReference;
import software.aws.toolkits.eclipse.amazonq.configuration.PluginStore;
import software.aws.toolkits.eclipse.amazonq.configuration.customization.CustomizationUtil;
import software.aws.toolkits.eclipse.amazonq.configuration.profiles.QDeveloperProfileUtil;
import software.aws.toolkits.eclipse.amazonq.exception.AmazonQPluginException;
import software.aws.toolkits.eclipse.amazonq.lsp.auth.model.AuthState;
import software.aws.toolkits.eclipse.amazonq.lsp.auth.model.InvalidateSsoTokenParams;
import software.aws.toolkits.eclipse.amazonq.lsp.auth.model.LoginParams;
import software.aws.toolkits.eclipse.amazonq.lsp.auth.model.LoginType;
import software.aws.toolkits.eclipse.amazonq.lsp.model.UpdateCredentialsPayload;
import software.aws.toolkits.eclipse.amazonq.plugin.Activator;
import software.aws.toolkits.eclipse.amazonq.providers.lsp.LspProvider;
import software.aws.toolkits.eclipse.amazonq.telemetry.AwsTelemetryProvider;
import software.aws.toolkits.eclipse.amazonq.telemetry.AwsTelemetryProvider.BrowserLoginParams;
import software.aws.toolkits.eclipse.amazonq.util.AuthUtil;
import software.aws.toolkits.telemetry.TelemetryDefinitions.CredentialType;
import software.aws.toolkits.telemetry.TelemetryDefinitions.Result;
/**
* Core authentication service for the Amazon Q Eclipse plugin that manages
* user authentication flows and token lifecycle management.
*
* Key responsibilities:
* - Manages authentication workflows (login/logout/expire/re-authenticate)
* - Coordinates browser-based authentication flows
* - Maintains authentication session state
*
* Important: Maintaining synchronized credentials with the Amazon Q LSP server is critical for proper operation.
* Communication to the external Amazon Q server is handled by the Amazon Q LSP server acting as a proxy. Outdated
* credentials may cause inconsistent behavior and failed requests.
*
* @see AuthStateManager For authentication state management and persistent storage updates
* @see AuthTokenService Handles operations related to SSO token
* @see AuthCredentialsService Handles operations related to LSP server credentials
*/
public final class DefaultLoginService implements LoginService {
private AuthStateManager authStateManager;
private AuthTokenService authTokenService;
private AuthCredentialsService authCredentialsService;
private AuthPluginStore authPluginStore;
private DefaultLoginService(final Builder builder) {
this.authStateManager = Objects.requireNonNull(builder.authStateManager, "authStateManager cannot be null");
this.authTokenService = Objects.requireNonNull(builder.authTokenService, "authTokenService cannot be null");
this.authCredentialsService = Objects.requireNonNull(builder.authCredentialsService, "authCredentialsService cannot be null");
this.authPluginStore = new AuthPluginStore(Objects.requireNonNull(builder.pluginStore, "pluginStore cannot be null"));
if (builder.initializeOnStartUp) {
AuthState authState = authStateManager.getAuthState();
if (!authState.isLoggedOut()) {
boolean loginOnInvalidToken = false;
reAuthenticate(loginOnInvalidToken).thenRun(() -> {
if (authStateManager.getAuthState().loginType().equals(LoginType.IAM_IDENTITY_CENTER)) {
QDeveloperProfileUtil.getInstance().initialize();
}
});
}
}
}
public static Builder builder() {
return new Builder();
}
@Override
public CompletableFuture<Void> login(final LoginType loginType, final LoginParams loginParams) {
if (authStateManager.getAuthState().isLoggedIn()) {
Activator.getLogger().warn("Attempted to log in while already in a logged in state");
return CompletableFuture.completedFuture(null);
}
Activator.getLogger().info("Attempting to login...");
return processLogin(loginType, loginParams, true, false)
.exceptionally(throwable -> {
Activator.getLogger().error("Failed to log in", throwable);
logout();
return null;
});
}
@Override
public CompletableFuture<Void> logout() {
AuthState authState = getAuthState();
if (authState.isLoggedOut()) {
Activator.getLogger().warn("Attempted to log out while already in a logged out state");
return CompletableFuture.completedFuture(null);
}
if (authState.ssoTokenId() == null || authState.ssoTokenId().isBlank()) {
authStateManager.toLoggedOut();
Activator.getLogger().warn("Attempted to log out with no ssoTokenId saved in auth state");
return CompletableFuture.completedFuture(null);
}
Activator.getLogger().info("Attempting to log out...");
InvalidateSsoTokenParams params = new InvalidateSsoTokenParams(authState.ssoTokenId());
QDeveloperProfileUtil.getInstance().clearSelectedProfile();
return authTokenService.invalidateSsoToken(params)
.thenRun(() -> {
authCredentialsService.deleteTokenCredentials();
})
.thenRun(() -> {
authStateManager.toLoggedOut();
Activator.getLogger().info("Successfully logged out");
})
.exceptionally(throwable -> {
Activator.getLogger().error("Failed to log out", throwable);
return null;
});
}
@Override
public CompletableFuture<Void> expire() {
Activator.getLogger().info("Attempting to expire credentials...");
return authCredentialsService.updateTokenCredentials(new UpdateCredentialsPayload(null, null, false))
.thenRun(() -> {
authStateManager.toExpired();
Activator.getLogger().info("Successfully expired credentials");
})
.exceptionally(throwable -> {
Activator.getLogger().error("Failed to expire credentials", throwable);
return null;
});
}
@Override
public CompletableFuture<Void> reAuthenticate(final boolean loginOnInvalidToken) {
AuthState authState = authStateManager.getAuthState();
if (authState.isLoggedOut()) {
Activator.getLogger().warn("Attempted to re-authenticate while user is in a logged out state");
return CompletableFuture.completedFuture(null);
}
Activator.getLogger().info("Attempting to re-authenticate...");
return processLogin(authState.loginType(), authState.loginParams(), loginOnInvalidToken, true)
.exceptionally(throwable -> {
Activator.getLogger().error("Failed to re-authenticate", throwable);
logout();
return null;
});
}
@Override
public AuthState getAuthState() {
return authStateManager.getAuthState();
}
CompletableFuture<Void> processLogin(final LoginType loginType, final LoginParams loginParams, final boolean loginOnInvalidToken,
final boolean isReAuth) {
AuthUtil.validateLoginParameters(loginType, loginParams);
final AtomicReference<String> ssoTokenId = new AtomicReference<>(); // Saved for logout
return authTokenService.getSsoToken(loginType, loginParams, loginOnInvalidToken)
.thenApply(ssoToken -> {
ssoTokenId.set(ssoToken.ssoToken().id());
return ssoToken;
})
.thenAccept(ssoToken -> {
authCredentialsService.updateTokenCredentials(loginType == LoginType.IAM_IDENTITY_CENTER
? ssoToken.getUpdateCredentialsPayloadHydratedWithStartUrl(
loginParams.getLoginIdcParams().getUrl())
: ssoToken.updateCredentialsParams());
})
.thenRun(() -> {
authStateManager.toLoggedIn(loginType, loginParams, ssoTokenId.get());
if (loginOnInvalidToken) {
emitBrowserLoginMetric(loginType, loginParams, isReAuth, Result.SUCCEEDED, null);
}
Activator.getLogger().info("Successfully logged in");
})
/*
* Reports the outcome of the login itself. The steps that follow are not part of the login,
* so they are wired after this stage to keep them out of the metric.
*
* Only logins that were allowed to open the browser are reported. The re-authentication
* performed on start up passes loginOnInvalidToken=false, it refreshes the cached token
* silently and would otherwise report a browser login (and a session duration) on every
* start of the IDE.
*/
.whenComplete((unused, throwable) -> {
if (throwable != null && loginOnInvalidToken) {
emitBrowserLoginMetric(loginType, loginParams, isReAuth, Result.FAILED, getReasonCode(throwable));
}
}).thenRun(() -> {
CustomizationUtil.triggerChangeConfigurationNotification();
}).exceptionally(throwable -> {
throw new AmazonQPluginException("Failed to process log in", throwable);
});
}
/**
* Emits the browser login metric, reporting how long the previous authentication session for the
* same start url lived for.
*
* The session duration is only known once a login has been recorded for that start url, so it is
* left out of the first login and of the first login that follows a sign out, which clears the
* recorded login. A successful login becomes the new reference point for the next one.
*
* @param loginType the type of connection being authenticated
* @param loginParams the parameters of the connection being authenticated
* @param isReAuth whether the login renews an existing connection
* @param result whether the login succeeded
* @param reason a short reason code when the login failed, null otherwise
*/
private void emitBrowserLoginMetric(final LoginType loginType, final LoginParams loginParams, final boolean isReAuth,
final Result result, final String reason) {
String credentialStartUrl = AuthUtil.getIssuerUrl(loginType, loginParams);
// The start url identifies the authentication session, a metric without it carries no signal.
if (credentialStartUrl == null || credentialStartUrl.isBlank()) {
return;
}
Long sessionDuration = null;
if (result == Result.SUCCEEDED) {
Instant loginInstant = Instant.now();
sessionDuration = authPluginStore.getLoginTimestamp(credentialStartUrl)
.map(previousLogin -> Duration.between(previousLogin, loginInstant).toMillis())
.filter(duration -> duration >= 0) // guards against a recorded login dated in the future
.orElse(null);
authPluginStore.setLoginTimestamp(credentialStartUrl, loginInstant);
}
AwsTelemetryProvider.emitLoginWithBrowserEvent(new BrowserLoginParams(credentialStartUrl,
CredentialType.BEARER_TOKEN, isReAuth, result, reason, sessionDuration));
}
private static String getReasonCode(final Throwable throwable) {
Throwable cause = throwable instanceof CompletionException && throwable.getCause() != null
? throwable.getCause()
: throwable;
return cause.getClass().getSimpleName();
}
public static class Builder {
private LspProvider lspProvider;
private PluginStore pluginStore;
private AuthStateManager authStateManager;
private AuthCredentialsService authCredentialsService;
private AuthTokenService authTokenService;
private boolean initializeOnStartUp;
public final Builder withLspProvider(final LspProvider lspProvider) {
this.lspProvider = lspProvider;
return this;
}
public final Builder withPluginStore(final PluginStore pluginStore) {
this.pluginStore = pluginStore;
return this;
}
public final Builder withAuthStateManager(final AuthStateManager authStateManager) {
this.authStateManager = authStateManager;
return this;
}
public final Builder withAuthCredentialsService(final AuthCredentialsService qCredentialService) {
this.authCredentialsService = qCredentialService;
return this;
}
public final Builder withAuthTokenService(final AuthTokenService authTokenService) {
this.authTokenService = authTokenService;
return this;
}
public final Builder initializeOnStartUp() {
this.initializeOnStartUp = true;
return this;
}
public final DefaultLoginService build() {
if (lspProvider == null) {
lspProvider = Activator.getLspProvider();
}
if (pluginStore == null) {
pluginStore = Activator.getPluginStore();
}
if (authStateManager == null) {
authStateManager = new DefaultAuthStateManager(pluginStore);
}
if (authCredentialsService == null) {
authCredentialsService = DefaultAuthCredentialsService.builder()
.withLspProvider(lspProvider)
.build();
}
if (authTokenService == null) {
authTokenService = DefaultAuthTokenService.builder()
.withLspProvider(lspProvider)
.build();
}
DefaultLoginService instance = new DefaultLoginService(this);
return instance;
}
}
}