-
Notifications
You must be signed in to change notification settings - Fork 0
Minimal implementation of logout and user avatar #41
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -16,15 +16,23 @@ | |
| import com.vaadin.flow.router.Layout; | ||
| import com.vaadin.flow.server.menu.MenuConfiguration; | ||
| import com.vaadin.flow.server.menu.MenuEntry; | ||
| import com.vaadin.flow.spring.security.AuthenticationContext; | ||
| import jakarta.annotation.security.PermitAll; | ||
| import org.jspecify.annotations.Nullable; | ||
| import org.springframework.beans.factory.ObjectProvider; | ||
|
|
||
| import java.util.Optional; | ||
|
|
||
| import static com.vaadin.flow.theme.lumo.LumoUtility.*; | ||
|
|
||
| @Layout | ||
| @PermitAll // When security is enabled, allow all authenticated users | ||
| public final class MainLayout extends AppLayout { | ||
|
|
||
| MainLayout() { | ||
| private final @Nullable AuthenticationContext authenticationContext; | ||
|
|
||
| MainLayout(ObjectProvider<AuthenticationContext> authenticationContext) { | ||
| this.authenticationContext = authenticationContext.getIfAvailable(); | ||
| setPrimarySection(Section.DRAWER); | ||
| addToDrawer(createHeader(), new Scroller(createSideNav()), createUserMenu()); | ||
| } | ||
|
|
@@ -58,8 +66,18 @@ private SideNavItem createSideNavItem(MenuEntry menuEntry) { | |
| } | ||
|
|
||
| private Component createUserMenu() { | ||
| // TODO Replace with real user information and actions | ||
| var avatar = new Avatar("John Smith"); | ||
| if (authenticationContext == null || !authenticationContext.isAuthenticated()) { | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This code is also silly:
Since the code of the |
||
| // This happens if the security of your application is not configured correctly. | ||
| // See https://vaadin.com/docs/latest/building-apps/security for details. | ||
|
|
||
| var badge = new Span("Security not configured"); | ||
| badge.getElement().getThemeList().add("badge error"); | ||
| badge.addClassNames(Margin.MEDIUM); | ||
| return badge; | ||
| } | ||
|
|
||
| var fullName = getUserFullName().orElseThrow(); | ||
| var avatar = new Avatar(fullName); | ||
| avatar.addThemeVariants(AvatarVariant.LUMO_XSMALL); | ||
| avatar.addClassNames(Margin.Right.SMALL); | ||
| avatar.setColorIndex(5); | ||
|
|
@@ -69,12 +87,24 @@ private Component createUserMenu() { | |
| userMenu.addClassNames(Margin.MEDIUM); | ||
|
|
||
| var userMenuItem = userMenu.addItem(avatar); | ||
| userMenuItem.add("John Smith"); | ||
| userMenuItem.getSubMenu().addItem("View Profile").setEnabled(false); | ||
| userMenuItem.getSubMenu().addItem("Manage Settings").setEnabled(false); | ||
| userMenuItem.getSubMenu().addItem("Logout").setEnabled(false); | ||
| userMenuItem.add(fullName); | ||
| userMenuItem.getSubMenu().addItem("View Profile").setEnabled(false); // TODO Implement or remove | ||
| userMenuItem.getSubMenu().addItem("Manage Settings").setEnabled(false); // TODO Implement or remove | ||
| userMenuItem.getSubMenu().addItem("Logout", event -> authenticationContext.logout()); | ||
|
|
||
| return userMenu; | ||
| } | ||
|
|
||
| private Optional<String> getUserFullName() { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Copied @peholmst from the flow PR:
If I understand you correctly, you want the skeleton to have a wow effect, be production-grade code and easy to use for beginners and good starting points for novice users. Going with this bullet points as requirement would result Imho in a requirement to always have security included. New people are shielded from the hard part at the beginning and novice user can either use it, remove it or customize it. Now to the question, which security should be integrated. From my experience it's often easier for people to get started with the good old login/password form (even tho there are people like Matti that hate them) - its a good base for people. But on the other hand Oauth2/OIDC is such a wide used concept and requirement that it should not be forgotten. Additionally novice users would love to see Vaadin's approach on it. Speaking from history, I have multiple application developed over the last years were we have both included. We have a common MyAppNameUser class which implements all spring related classes to be interchangeable in both worlds - in a login form and oidc login. This class allows us to swap between both login modules without requiring any code change down the line. To allow easier development we are using a spring profile, for example named "local", "dev" or "demo" which replaced our default security chain that is configured to use Keycloak with a security chain that is using plain old form based login. This allows for rapid local development without external runtime dependencies. This comes with the downside that people have to learn about the usage of spring profiles of they wanna start the app locally with a plain login form - but I would argue that are well spent 5min learning. This exact version can then be 1:1 deployed on CC without the profile enabled and would use OIDC. If you don't like demo code - thanks to different security chains and classes, those or the whole dev package could be removed when creating the production build to ensure nothing leaks
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks, that was a constructive comment. I'm currently trying to summarize everything for myself, to get a better picture of where we are, where we want to go, what options we have, and their pros and cons. I'll get back to this later. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If you have additional questions.. just hit me up or something wasn't understandable.. just saw some typos (typing on mobile and Apple's autocorrect is hard)
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I started playing with a double-configuration prototype, with separate What do you think about this? I've added JavaDocs that explain what each class does. This is not the final version, only a discussion starter. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That looks similar to the way we do it :) Some personal preferences / ideas for you to consider:
public interface AuthenticationService {
MyUser getUser();
}
@Slf4j
@Component
public class AuthenticationServiceImpl implements AuthenticationService {
@Override
public MyUser getUser() {
return getUserFromAuthentication(
SecurityContextHolder.getContext().getAuthentication()
);
}
@Nullable
public static MyUser getUserFromAuthentication(@Nullable Authentication authentication) {
if (null == authentication || null == authentication.getPrincipal()) {
return null;
}
var principal = authentication.getPrincipal();
if (principal instanceof MyOidcUser) {
return ((MyOidcUser) principal).getUser();
}
if (principal instanceof MyUser) {
return (MyUser) principal;
}
if (principal instanceof String && "anonymousUser".equals(principal.toString())) {
return null;
}
log.warn("Could not authenticate User from SecurityContext. Principal was not of type 'MyUser'. Instead it was: '{}' with value: '{}'",
principal.getClass().getName(), principal);
return null;
}
}
public class MyUser implements UserDetails {
// ....
}
public class MyOidcUser extends DefaultOidcUser {
@Getter
private final MyUser user;
public MyOidcUser(MyUser user, OidcUser oidcUser, String nameAttributeKey) {
super(oidcUser.getAuthorities(), oidcUser.getIdToken(), oidcUser.getUserInfo(), nameAttributeKey);
this.user = user;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
// delegate to the correct user so that Spring has access to the correct authorities
return this.user.getAuthorities();
}
}
|
||
| if (authenticationContext == null) { | ||
| return Optional.empty(); | ||
| } | ||
| // If you are using OIDC (e.g., via Vaadin Control Center), this returns the user's full name: | ||
| //return authenticationContext | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If you use OIDC or Control Center and comment this code out, it returns the full name of the user. Without the CC dependency, it fails to compile because At the moment, the CC dependency is in the classpath, which means we could have the code commented out by default. However, that would mean that if the user removes CC, they would have to fix this code. Would that be OK? Also, if the user was to use some other authentication mechanism than OIDC, this code would compile but result in a
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. One way to solve this is to add methods to
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Although I also suggested adding a wrapper user object to Flow, seeing it now feels like it doesn't really simplify anything—if not for the only purpose of the skeleton—with the result that the skeleton magically works but the users are more confused about yet another user class. AFAIK only and you get your Then if you want to deploy with Control Center, you don't need to change your code since it also works with |
||
| // .getAuthenticatedUser(OidcUser.class) | ||
| // .map(OidcUser::getUserInfo) | ||
| // .map(StandardClaimAccessor::getFullName); | ||
|
|
||
| return authenticationContext.getPrincipalName(); // TODO This is typically a username or ID, not the full name. | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the application has security configured, then
AuthenticationContextis always available. Most real applications should have this. The CC dependency also has this configured. However, if that dependency is removed without manually configuring security, theauthenticationContextbecomesnull. The current implementation takes this into account, but it feels silly:AuthenticationContextis always available and all checks and exception cases are unecessary.What action should require code changes from the developer: adding/using CC identity management, or removing/not using CC identity management?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we hide the menu in that case?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can add code for it, but then we're also introducing some extra complexity (although it is quite isolated and has negligible impact on performance). If the application will be secured, having a check for something that is always true is not needed. If the application won't be secured, the entire code block should be deleted.