Skip to content

Conversation

georgweiss
Copy link
Collaborator

@georgweiss georgweiss commented Sep 19, 2025

As per user request...

The credentials manager app now offers a way to sign in to all services (e.g. logbook, save&restore) through a single action.
In case any of the services rejects user's credentials (or is off-line), the dialog stays open (see screen shot) to indicate the problem.

Documentation updated accordingly.

Screenshot 2025-09-19 at 13 19 37

stage.close();
}
} catch (Exception e) {
LOGGER.log(Level.WARNING, "Failed to delete all authentication tokens from key store", e);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this error message correct?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, copy paste mistake

stage.close();
serviceItem.getUsername().get(),
serviceItem.getPassword().get()));
loggedInCount.set(loggedInCount.get() + 1);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reading from and writing to loggedInCount in this way is not thread-safe.

Copy link
Collaborator Author

@georgweiss georgweiss Sep 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Login (via authenticate) and logout APIs are synchronous, so why should this be thread safe?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if two concurrent calls to login() get interleaved? I think that if a computer is slow to respond (maybe it is running out of RAM and/or the CPU is busy), then the UI may become unresponsive and the user may click on "Login" several times, and the calls may be interleaved.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a good point. Will disable login buttons such that new login attempts cannot be invoked until ongoing has completed.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Disabling the button is a good idea, I think, but I think the calls need to be made synchronous also, since state is being modified. setDisable() may also not finish until the button has already been pressed twice.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, that could happen. Will secure concurrency.

} catch (ServiceAuthenticationException exception) {
serviceItem.setLoginResultMessage(Messages.UserNotAuthenticated);
}
serviceItem.setLoginResult(LoginResult.FAILED);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do I understand the code correctly here: is the login-result always set to LoginResult.FAILED here when login() is called?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right, there is a return statement missing.

updateTable();
serviceItem.serviceAuthenticationProvider.logout();
serviceItem.setLoggedOut();
loggedInCount.set(loggedInCount.get() - 1);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This assignment is not thread-safe.

Copy link
Collaborator Author

@georgweiss georgweiss Sep 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Login (via authenticate) and logout APIs are synchronous, so why should this be thread safe?

private final ObjectProperty<LoginResult> loginResult = new SimpleObjectProperty<>(LoginResult.OK);

public ServiceItem(ServiceAuthenticationProvider serviceAuthenticationProvider, String username, String password) {
this.serviceAuthenticationProvider = serviceAuthenticationProvider;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it may be a good idea to change the type of serviceAuthenticationProvider from ServiceAuthenticationProvider to Optional<ServiceAuthenticationProvider> since serviceAuthenticationProvider may be null. (See lines 287-290: https://github.com/ControlSystemStudio/phoebus/pull/3554/files#diff-8682c52d79413fbe3e61310d72e1daa306a698af4c7efe00f97ffa2e16952350R287-R290.) This way, the fact that the field may be empty is reflected in its type. See also line 358 [1] and line 364 [2].

[1] https://github.com/ControlSystemStudio/phoebus/pull/3554/files#diff-8682c52d79413fbe3e61310d72e1daa306a698af4c7efe00f97ffa2e16952350R358
[2] https://github.com/ControlSystemStudio/phoebus/pull/3554/files#diff-8682c52d79413fbe3e61310d72e1daa306a698af4c7efe00f97ffa2e16952350R364

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In practice the ServiceAuthenticationProvier should never be null as saved tokens are associated only with existing ServiceAuthenticationProviers. I will update the callee code to handle potential nulls.

}
catch (Exception e) {
Logger.getLogger(OlogServiceAuthenticationProvider.class.getName())
.log(Level.WARNING, "Failed to authenticate user " + username + " with logbook service", e);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can the stack trace of e contain sensitive information? The exception originates from the call to OlogHttpClient.builder().build().authenticate(username, password) which contains username and password. I think it may be safer to not include the exception e in the log, and also not to re-throw it (https://github.com/ControlSystemStudio/phoebus/pull/3554/files#diff-1cc8e66eabcdb21075575c28e20439ca032c7b00a5770c4044c369028a6d2eadR44).

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No. But I'll remove the exception anyway.

} catch (ConnectException e) {
throw e;
} catch (Exception e) {
throw new RuntimeException(e);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above: is it possible that the exception e contains sensitive information, since it originates in a call that contains userName and password? I think it may be safer if e is not re-thrown (both on this line and on line 592).

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No. But I'll remove the exception anyway.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exception not logged.

SecureStore secureStore = new SecureStore();
secureStore.deleteScopedAuthenticationToken(getAuthenticationScope());
} catch (Exception e) {
throw new RuntimeException(e);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's safer not to re-throw an exception here, since it originates from a call to secureStore.

Copy link
Collaborator Author

@georgweiss georgweiss Sep 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The case where exceptions could be thrown is when calling the Java keystore APIs. It would be quite surprising to see those APIs leak sensitive information.

password = get(scope.getName() + "." + PASSWORD_TAG);
scope = findAuthenticationScopeFromProviders(tokens[0]);
if(scope == null){
throw new IllegalArgumentException("No authentication provider found matching scope \"" + tokens[0] + "\"");
Copy link
Collaborator

@abrahamwolk abrahamwolk Sep 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's not safe to throw exceptions from within a class called SecureStore, since exceptions may contain sensitive information.

Could a failure be handled in a type-safe way in the code by a suitable return-type? Perhaps the function could have the return type Optional<List<ScopedAuthenticationToken>> and return Optional.empty() in case it is not successful.

Copy link
Collaborator Author

@georgweiss georgweiss Sep 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See previous comment.

* @param password Password
*/
void authenticate(String username, String password);
void authenticate(String username, String password) throws ConnectException;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest not to throw exceptions in authenticate(), but instead to change its return-type to something that can represent failure. Perhaps boolean would work: true indicates success and false indicates failure to login. The motivation is to avoid accidental printing of sensitive information in stack traces or similar.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The idea is to catch different type of exceptions and differentiate messages to user in the UI, e.g. "User not authenticated" vs "Service offline".

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Decided to introduce an AuthenticationResult enum to differentiate between bad credentials and failure to connect. Will update PR shortly.

@georgweiss
Copy link
Collaborator Author

Updates based on peer feed-back. Also some refactoring in the name of simplification and readability.

throw e;
} catch (Exception e) {
throw new RuntimeException(e);
public void authenticate(String userName, String password) throws Exception {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The declared exception that may be thrown is now more general (Exception instead of ConnectException) than before (i.e., there is less static information about it): is this correct?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is correct. The change is about intercepting a true authentication response and turn that into an ServiceAuthenticationException. The SaveAndRestoreAuthenticationProvider then analyses exceptions to return the appropriate AuthenticationStatus

btn.setOnAction((ActionEvent event) -> {
ServiceItem serviceItem = getTableRow().getItem();
if (serviceItem != null && serviceItem.userLoggedIn.get()) {
if (serviceItem != null && (serviceItem.authenticationStatus.get().equals(AuthenticationStatus.AUTHENTICATED) ||
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the action of this button also needs to be synchronized.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The login and logout methods are synchronized.

private final StringProperty buttonTextProperty = new SimpleStringProperty();
private final StringProperty loginResultMessage = new SimpleStringProperty();
private final ObjectProperty<LoginResult> loginResult = new SimpleObjectProperty<>(LoginResult.OK);
private final ObjectProperty<AuthenticationStatus> authenticationStatus = new SimpleObjectProperty<>();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should the SimpleObjectProperty be initialized with an initial value?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do not see the need.

username.get(),
password.get()));
} catch (Exception e) {
throw new RuntimeException(e);
Copy link
Collaborator

@abrahamwolk abrahamwolk Sep 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's better not to re-throw an exception here (both username and password are arguments to the call in the try-block). (Also Exception e can maybe be generalized to Throwable t, if the clause is intended to catch all errors.)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will update to just log the problem. But I fail to see how credentials could be leaked.

catch(ConnectException e){
throw e;
return AuthenticationStatus.AUTHENTICATED;
} catch (ConnectException e) {
Copy link
Collaborator

@abrahamwolk abrahamwolk Sep 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the catch-clause for ConnectException still relevant after the changes in this commit? (The catch-all clause Exception e should remain, of course).

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See previous comment on how exceptions are handled.

public void authenticate(String username, String password) throws ConnectException{
public AuthenticationStatus authenticate(String username, String password) {
try {
OlogHttpClient.builder().build().authenticate(username, password);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could the function authenticate() return the authentication status, instead of throwing exceptions when failing?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is what it does.

* @param password Password, must not be <code>null</code>.
* @throws Exception if the login fails, e.g. bad credentials or service off-line.
*/
public void authenticate(String userName, String password) throws Exception {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest to return the authentication status as a value instead of throwing exceptions when there's an error. This makes it much easier to follow the control flow.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You have a point. Will move the exception handling to the http client classes.

logger.log(Level.WARNING, "User " + username + " not authenticated");
return AuthenticationStatus.BAD_CREDENTIALS;
}
catch (Exception e) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This catch-all clause can be improved by changing it from Exception e to Throwable t.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could still include the exception in the log message to show the reason and stack trace:

logger.log(Level.WARNING, "User " + username + " not authenticated", e);

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants