diff --git a/articles/building-apps/react/security.adoc b/articles/building-apps/react/security.adoc index 2eea60117f..3373231802 100644 --- a/articles/building-apps/react/security.adoc +++ b/articles/building-apps/react/security.adoc @@ -22,10 +22,12 @@ The real security boundary is on the server, around the *browser-callable servic In practice you do both: protect views so users see only what's relevant to them, and protect services so the data is actually safe. +For how Vaadin checks the access to views and services, see the <<{articles}/flow/security/react-views#,React Views & Browser-Callable Services>> reference. + == Adding a Login View -Vaadin includes a client-side security extension that integrates with Spring Security. Setting it up takes three steps: a user-information endpoint, a client-side authentication context, and wrapping the application in that context. +Vaadin includes a client-side security extension that integrates with Spring Security. Setting it up takes three steps: a user-information endpoint, a client-side authentication context, and wrapping the application in that context. For the functions and their options, see <<{articles}/flow/security/react-views/authentication#,Authentication>>. === Create a User-Information Endpoint @@ -202,7 +204,7 @@ A React view is public unless it, or a layout around it, sets `loginRequired`. A == Restricting Access by Role -To grant access based on roles, first expose the current user's roles to the authentication context, then declare the required roles on each view. +To grant access based on roles, first expose the current user's roles to the authentication context, then declare the required roles on each view. For where Vaadin checks the roles, see <<{articles}/flow/security/react-views/view-access-control#,View Access Control>>. === Expose Roles to the Context @@ -295,7 +297,7 @@ A relative logout URL must include the application's context path. If the applic == Protecting Browser-Callable Services -This is the real security boundary. All browser-callable services are *inaccessible by default* and require an explicit annotation to grant access. +This is the real security boundary. All browser-callable services are *inaccessible by default* and require an explicit annotation to grant access. For the precedence of the annotations and the responses to denied calls, see <<{articles}/flow/security/react-views/service-access-control#,Service Access Control>>. .Services Don't Use Spring Method Security [CAUTION] diff --git a/articles/flow/react-views/browser-callable-services/client.adoc b/articles/flow/react-views/browser-callable-services/client.adoc index f30bf78246..3009dbfc30 100644 --- a/articles/flow/react-views/browser-callable-services/client.adoc +++ b/articles/flow/react-views/browser-callable-services/client.adoc @@ -127,4 +127,4 @@ export async function stubMiddleware(context: MiddlewareContext, next: Middlewar } ---- -The `@vaadin/hilla-frontend` package also has an `InvalidSessionMiddleware`, which handles calls that fail because the session of the user has expired. See <<{articles}/hilla/guides/security/session-expiration#,Handling Session Expiration>>. +The `@vaadin/hilla-frontend` package also has an `InvalidSessionMiddleware`, which handles calls that fail because the session of the user has expired. See <<{articles}/flow/security/react-views/authentication#expired-sessions,Expired Sessions>>. diff --git a/articles/flow/react-views/browser-callable-services/index.adoc b/articles/flow/react-views/browser-callable-services/index.adoc index 172b917e21..3735a0bf32 100644 --- a/articles/flow/react-views/browser-callable-services/index.adoc +++ b/articles/flow/react-views/browser-callable-services/index.adoc @@ -29,7 +29,7 @@ Annotate the service class with [annotationname]`@BrowserCallable`. The annotati include::{root}/src/main/java/com/vaadin/demo/fusion/accessingbackend/CounterService.java[tags=snippet] ---- -Access to services is denied by default. Give the class or each method an access annotation, such as [annotationname]`@AnonymousAllowed`, [annotationname]`@PermitAll`, or [annotationname]`@RolesAllowed`. See <<{articles}/building-apps/react/security#,Secure React Views>>. +Access to services is denied by default. Give the class or each method an access annotation, such as [annotationname]`@AnonymousAllowed`, [annotationname]`@PermitAll`, or [annotationname]`@RolesAllowed`. See <<{articles}/flow/security/react-views/service-access-control#,Service Access Control>>. The [annotationname]`@Endpoint` annotation is an alias for [annotationname]`@BrowserCallable`. Its optional `value` sets the name of the service, which is otherwise the simple name of the class. The generated module and the requests use that name. diff --git a/articles/flow/react-views/routing.adoc b/articles/flow/react-views/routing.adoc index 64b84a214b..9167e67f3c 100644 --- a/articles/flow/react-views/routing.adoc +++ b/articles/flow/react-views/routing.adoc @@ -165,7 +165,7 @@ If `true`, the browser loads the code of the view only when the view is first sh `detail`:: Any further data about the view, for example for rendering the menu. Its type is the type parameter of `ViewConfig`. -The `loginRequired` and `rolesAllowed` options control navigation in the browser, and which views the menu shows. They don't protect any data. Protect the browser-callable services that the view calls on the server. See <<{articles}/building-apps/react/security#,Secure React Views>>. +The `loginRequired` and `rolesAllowed` options control which views the user can open, and which views the menu shows. They don't protect any data. Protect the browser-callable services that the view calls on the server. See <<{articles}/flow/security/react-views/view-access-control#,View Access Control>>. === Reading the Configuration @@ -267,7 +267,7 @@ Adds a route for the paths that no other route matches, in every branch of the r Wraps the views that have the `flowLayout` option in the given component, such as `Flow`. Only the first call has an effect. `protect(redirectPath)`:: -Checks the `loginRequired` and `rolesAllowed` options in the browser. When the user can't open a view, the router goes to `redirectPath` instead, or to `/login` without an argument. It gets the user from the authentication context. See <<{articles}/building-apps/react/security#,Secure React Views>>. +Checks the `loginRequired` and `rolesAllowed` options in the browser. When the user can't open a view, the router goes to `redirectPath` instead, or to `/login` without an argument. It gets the user from the authentication context. See <<{articles}/flow/security/react-views/view-access-control#browser,View Access Control>>. `build(options)`:: Returns an object with the `router` and the `routes`. The optional `options` are passed to the `createBrowserRouter()` function of React Router. diff --git a/articles/flow/security/protections/vulnerabilities.adoc b/articles/flow/security/protections/vulnerabilities.adoc index 0359bbe2cf..54da3c43d5 100644 --- a/articles/flow/security/protections/vulnerabilities.adoc +++ b/articles/flow/security/protections/vulnerabilities.adoc @@ -106,10 +106,14 @@ Upload and heartbeat requests aren't UIDL messages, so they don't carry `Vaadin- Requests that React views send to browser-callable services aren't UIDL requests, so they're protected by Spring Security's CSRF protection instead. Vaadin adds Spring Security's CSRF token to the page, and the client sends it in the HTTP header that Spring Security expects, by default `X-CSRF-TOKEN`. This token is a different one from the `Vaadin-Security-Key`. +An application without Spring Security's CSRF protection uses the link:https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#alternative-using-a-double-submit-cookie-pattern[Double-Submit Cookie Pattern] instead. When the browser loads the page, the server sets a random token in the `csrfToken` cookie, which the browser code can read. The client sends the token of the cookie in the `X-CSRF-Token` header of every call, and the server checks that the header and the cookie match. A site on another domain can't read the cookie, so it can't send the header. + +The server rejects a call with a missing or wrong token with `401 Unauthorized`. + === Disabling the Check -The CSRF token check for UIDL requests can be disabled with the `disable-xsrf-protection` configuration parameter. See <<{articles}/flow/configuration/properties#,Configuration Properties>>. You might do this, for example, to enable repeatable load test scripts that use Gatling or similar tools. This is strongly discouraged, though, when running in production. +The CSRF token check for UIDL requests can be disabled with the `disable-xsrf-protection` configuration parameter. The parameter also disables the check of the `csrfToken` cookie for browser-callable services, but not Spring Security's CSRF protection. See <<{articles}/flow/configuration/properties#,Configuration Properties>>. You might do this, for example, to enable repeatable load test scripts that use Gatling or similar tools. This is strongly discouraged, though, when running in production. [[xss]] diff --git a/articles/flow/security/react-views/authentication.adoc b/articles/flow/security/react-views/authentication.adoc new file mode 100644 index 0000000000..62a86e5d3d --- /dev/null +++ b/articles/flow/security/react-views/authentication.adoc @@ -0,0 +1,150 @@ +--- +title: Authentication +page-title: Authentication and the current user in React views | Vaadin +description: The login view of React views, the user information that the browser gets, the authentication context, and expired sessions. +meta-description: Reference for authentication in Vaadin React views - the login view, the user information service, the authentication context, logging in and out, and expiry. +order: 20 +--- + + += Authentication +:toclevels: 2 + +Spring Security authenticates the users of React views on the server, with the same configuration as for Flow views. The `@vaadin/hilla-react-auth` package tells the browser which user is logged in, and logs users in and out. + +section_outline::[] + + +[[login-view]] +== Login View + +The [methodname]`loginView()` method of [classname]`VaadinSecurityConfigurer` takes the path of a React login view: + +[source,java] +---- +http.with(VaadinSecurityConfigurer.vaadin(), + configurer -> configurer.loginView("/login")); +---- + +Spring Security sends the users who aren't logged in to that path, and expects the login requests at the same path. After logout, the browser goes to `/`, or to the URL that the optional second argument gives. See <<{articles}/flow/security/spring/security-configuration#login-view-configuration,Login View Configuration>>. + + +[[user-information]] +== User Information + +The browser gets the logged-in user from a browser-callable service method of the application. The method returns information about the user, or `null` when nobody is logged in. Vaadin has no type for the information, so the application defines one with the properties that its views need: + +[source,java] +---- +include::{root}/src/main/java/com/vaadin/demo/fusion/security/authentication/UserInfoService.java[tags=snippet,indent=0] +---- + +[source,java] +---- +include::{root}/src/main/java/com/vaadin/demo/fusion/security/authentication/UserInfo.java[tags=snippet,indent=0] +---- + +The `roles` property contains the granted authorities as they are, such as `ROLE_ADMIN`. See <> for why. + +A method that allows only logged-in users works, too: the browser counts its `401 Unauthorized` response as no user. That response also starts the <>, though, for every user who isn't logged in. + + +[[configure-auth]] +== Authentication Context + +The [methodname]`configureAuth()` function of `@vaadin/hilla-react-auth` takes the generated function of the user information method: + +[source,typescript] +.[filename]`auth.ts` +---- +include::{root}/frontend/demo/auth/auth.ts[] +---- + +It returns two items: + +`AuthProvider`:: +A React component that provides the authentication context. It must be around every component that uses the context, so render it around the `RouterProvider` of the application, in an [filename]`index.tsx` file of your own. When it's first rendered, it calls the user information method. + +`useAuth`:: +The hook that returns the authentication context. Its type comes from the return type of the user information method. + +The optional second argument of [methodname]`configureAuth()` is an object with a `getRoles` function. The function gets the user information, and returns the roles of the user as an array of strings. The access checks in the browser compare these roles with the `rolesAllowed` option of the views. By default, the roles are the `roles` property of the user information, when it's an array. + + +[[use-auth]] +== Authentication State + +The object that [methodname]`useAuth()` returns has these properties: + +`state`:: +The authentication state. Its `user` property is the user information, or `undefined` when nobody is logged in. Until the user information has loaded, its `initializing` or `loading` property is `true`. + +`login(username, password, options)`:: +Logs the user in, as described in <>, and then loads the user information again. + +`logout()`:: +Logs the user out, as described in <>. + +`hasAccess(options)`:: +Returns whether the user meets the `loginRequired` and `rolesAllowed` options of the argument. See <>. + + +[[login]] +=== Logging In + +The [methodname]`login()` function sends the user name and the password to the login URL. When the login succeeds, it reloads the page at the URL that the user tried to open before Spring Security sent them to the login view. Without such a URL, it uses the default success URL of Spring Security, which is `/` by default. + +The function returns a promise of a [classname]`LoginResult`. When the login fails, the `error` property of the result is `true`, and the `errorTitle` and `errorMessage` properties describe the error for the user: + +[source,tsx] +.[filename]`login.tsx` +---- +include::{root}/frontend/demo/auth/LoginView.tsx[] +---- + +The third argument of [methodname]`login()` is an optional object with these properties: + +`loginProcessingUrl`:: +The URL of the login request. Defaults to `login`, relative to the base URL of the application. + +`onSuccess`:: +A function that's called after a successful login, before the navigation. + +`navigate`:: +A function that's called with the path to go to after a successful login. By default, the page reloads at that path. + + +[[logout]] +=== Logging Out + +The [methodname]`logout()` function submits a logout request to `logout`, relative to the base URL of the application. The browser then goes to the URL that the server responds with, which is the logout success URL. + +Data that the application keeps in the browser stays there after the logout. See <>. + + +[[expired-sessions]] +== Expired Sessions + +When the session of the user expires, the calls to services fail with `401 Unauthorized`. With the default configuration of Spring Security, this happens also to the calls to methods that allow anonymous access. The token that protects the page from cross-site request forgery belonged to the expired session, so it's no longer valid. + +The [classname]`InvalidSessionMiddleware` of `@vaadin/hilla-frontend` handles these calls. Add it to the `ConnectClient` of the application, as described in <<{articles}/flow/react-views/browser-callable-services/client#middleware,Middleware>>: + +[source,typescript] +.[filename]`connect-client.ts` +---- +include::{root}/frontend/demo/auth/session-expiration/connect-client.ts[tags=snippet,indent=0] +---- + +For each call that fails with `401 Unauthorized`, the middleware calls the function that its constructor got, and waits for the [classname]`LoginResult` that the function returns. When the result has a `token`, the middleware sends the call again with that token. Otherwise, the call fails with the original response. The [methodname]`login()` function doesn't return a token, so the failed calls aren't sent again after it. + +This example asks the user to log in again, and then reloads the page: + +[source,tsx] +.[filename]`SessionExpiredLoginOverlay.tsx` +---- +include::{root}/frontend/demo/auth/session-expiration/SessionExpiredLoginOverlay.tsx[tags=snippet,indent=0] +---- + +Render `SessionExpiredLoginOverlay` once in the main layout, so that it's present in every view. Nothing renders it automatically. If no component renders it, the overlay never opens, and the failed calls wait without any feedback. + +The `navigate` option reloads the page at its current URL. Without it, [methodname]`login()` goes to the default success URL, because Spring Security doesn't save the URL of a service call as a URL to return to. diff --git a/articles/flow/security/react-views/index.adoc b/articles/flow/security/react-views/index.adoc new file mode 100644 index 0000000000..90e1840e93 --- /dev/null +++ b/articles/flow/security/react-views/index.adoc @@ -0,0 +1,58 @@ +--- +title: React Views & Browser-Callable Services +page-title: Securing React views and browser-callable services | Vaadin +description: How Vaadin protects React views and the browser-callable services that they call, and how their defaults differ from Flow views. +meta-description: Reference for the security of Vaadin React views and browser-callable services - the security model, default access rules, and Spring Security settings. +order: 25 +--- + + += React Views & Browser-Callable Services + +A React view runs in the browser. The user can read its code, and change what it does, with the developer tools of the browser. The data of the view comes from the browser-callable services that it calls, and the server checks every call to them. These checks are what protect the data. + +Vaadin also checks access to React views, but only to decide what the user sees: which views the router opens, and which views the menu shows. These checks don't protect anything that the browser has already loaded. + + +[[defaults]] +== Default Access + +Browser-callable services, React views, and Flow views have different defaults: + +[cols="1,2"] +|=== +| |Without an access rule + +|Browser-callable service +|Denied to everyone. See <>. + +|React view +|Open to everyone. See <>. + +|Flow view +|Denied to everyone. See <<{articles}/flow/security/access-control/annotations#,Annotation-Based Access Control>>. +|=== + +A React view that you add to an application whose Flow views all require login is therefore open to everyone, until it or a layout around it requires login. + + +[[spring-security]] +== Spring Security Configuration + +React views and Flow views share the Spring Security configuration that [classname]`VaadinSecurityConfigurer` applies, as described in <<{articles}/flow/security/spring/security-configuration#,Security Configuration>>. For React views and browser-callable services, the configurer does more: + +- It permits the requests for React views that don't require login, and for service methods that allow anonymous access. +- It requires a logged-in user for the other calls to services, and responds to them with `401 Unauthorized` when nobody is logged in. +- It checks the user when the browser loads the page of a React view that requires login, as described in <>. +- It configures the protection of service calls against cross-site request forgery, as described in <<{articles}/flow/security/protections/vulnerabilities#react-views,Cross-Site Request Forgery>>. + + +[[cached-data]] +== Data in the Browser + +A React view can keep the data that it gets from services in the browser, for example in local storage, so that the view also works offline. Whoever uses the browser later can read that data. Decide whether the data may stay in the browser, and remove the data that may not when the user logs out. + + +== Topics + +section_outline::[] diff --git a/articles/flow/security/react-views/service-access-control.adoc b/articles/flow/security/react-views/service-access-control.adoc new file mode 100644 index 0000000000..176dfae968 --- /dev/null +++ b/articles/flow/security/react-views/service-access-control.adoc @@ -0,0 +1,125 @@ +--- +title: Service Access Control +page-title: Access control for Vaadin browser-callable services +description: The access annotations of browser-callable services, how Vaadin checks them, and how to audit the calls that it denies. +meta-description: Reference for access control of Vaadin browser-callable services - access annotations, their precedence, role checks, error responses, and audit events. +order: 10 +--- + + += Service Access Control +:toclevels: 2 + +Vaadin checks every call to a browser-callable service against the access annotations of the service, before it reads the arguments of the call. Access is denied by default: nobody can call a method that has no access annotation, in a class that has none either. + +Vaadin makes these checks itself, without Spring method security, and only for the calls that come from the browser. Java code that calls the service bean directly isn't checked. + +section_outline::[] + + +[[annotations]] +== Access Annotations + +These annotations control the access to a service: + +[annotationname]`@AnonymousAllowed`:: +Allows everyone, including users who aren't logged in. The annotation is in the package `com.vaadin.flow.server.auth`. + +[annotationname]`@PermitAll`:: +Allows the users who are logged in. + +[annotationname]`@RolesAllowed`:: +Allows the users who have at least one of the given roles. + +[annotationname]`@DenyAll`:: +Denies everyone. + +The last three are the Jakarta annotations in the package `jakarta.annotation.security`. + +An annotation on the class applies to the public methods of the class that have no access annotation of their own. When a method has an access annotation, the annotations of the class don't apply to it: + +[source,java] +---- +include::{root}/src/main/java/com/vaadin/demo/fusion/security/access/ProjectService.java[tags=snippet,indent=0] +---- + +When a class or a method has more than one access annotation, the first of them in this list applies: + +. [annotationname]`@DenyAll` +. [annotationname]`@AnonymousAllowed` +. [annotationname]`@RolesAllowed` +. [annotationname]`@PermitAll` + +The access annotations of the service class also apply to the methods that the service inherits, as described in <<{articles}/flow/react-views/browser-callable-services#inherited-methods,Inherited Methods>>. + + +[[roles]] +== Role Checks + +Vaadin checks the roles of [annotationname]`@RolesAllowed` with the [methodname]`isUserInRole()` method of the HTTP request. With Spring Security, this method adds the role prefix `ROLE_` to a role name that doesn't start with it. Both `@RolesAllowed("ADMIN")` and `@RolesAllowed("ROLE_ADMIN")` therefore match the granted authority `ROLE_ADMIN`. See <<{articles}/flow/security/spring/roles-and-authorities#,Roles & Authorities>>. + + +[[responses]] +== Denied Calls + +A denied call fails with the HTTP status `401 Unauthorized` when nobody is logged in, and with `403 Forbidden` when a user is. In the browser, the call rejects with an `UnauthorizedResponseError` or a `ForbiddenResponseError`, as described in <<{articles}/flow/react-views/browser-callable-services#errors,Errors>>. In development mode, the error message lists the annotations that allow access. + +With [classname]`VaadinSecurityConfigurer`, Spring Security already rejects a call to a method that doesn't allow anonymous access when nobody is logged in. Vaadin's check doesn't run for such a call. The response is the same `401 Unauthorized`. + + +[[current-user]] +== The Current User + +A service method can get the user who called it from [classname]`AuthenticationContext`, as described in <<{articles}/flow/security/spring/authentication-context#,Authentication Context>>. The methods of the class that return the user and the roles work in services. Its [methodname]`logout()` method works only in Flow views. + + +[role="since:com.vaadin:vaadin@V25.3"] +[[auditing]] +== Auditing Denied Calls + +When Vaadin's check denies a call, Vaadin publishes a Spring Security [classname]`AuthorizationDeniedEvent`, the same event that Spring method security publishes for a denied call. An application that audits denied access sees the denied service calls in the same audit trail. + +Vaadin publishes the events only when the application has an [classname]`AuthorizationEventPublisher` bean. Spring Security uses the same bean to opt in to these events: + +[source,java] +---- +@Configuration +public class AuditConfig { + + @Bean + AuthorizationEventPublisher authorizationEventPublisher( + ApplicationEventPublisher publisher) { + return new SpringAuthorizationEventPublisher(publisher); + } +} +---- + +The object of the event is an [classname]`EndpointInvocation`, which names the service and the method. It implements [interfacename]`MethodInvocation`, so a listener written for method security accepts it as it is: + +[source,java] +---- +@Component +public class DeniedCallAuditor { + + @EventListener + public void onAccessDenied(AuthorizationDeniedEvent event) { + if (event.getObject() instanceof EndpointInvocation invocation) { + LoggerFactory.getLogger(getClass()).warn( + "Denied call: user={} service={} method={}", + event.getAuthentication().get().getName(), + invocation.getEndpointName(), + invocation.getMethodName()); + } + } +} +---- + +The [methodname]`getThis()` method returns the service bean, which is useful for routing an alert to the owner of the service. The [methodname]`getArguments()` method always returns an empty array, and [methodname]`proceed()` throws an exception. Vaadin checks the access before it reads the arguments, so the arguments of a denied call can't end up in the audit log. + +A call that Spring Security rejects before Vaadin's check, as described in <<#responses,Denied Calls>>, doesn't publish an [classname]`EndpointInvocation`. Spring Security publishes its own [classname]`AuthorizationDeniedEvent` for it, with the HTTP request as the object. + + +[[csrf]] +== Cross-Site Request Forgery + +Every call to a service carries a token that protects it from cross-site request forgery. A call without a valid token fails with `401 Unauthorized`, even when the method allows anonymous access. See <<{articles}/flow/security/protections/vulnerabilities#react-views,Cross-Site Request Forgery>>. diff --git a/articles/flow/security/react-views/stateless-authentication.adoc b/articles/flow/security/react-views/stateless-authentication.adoc new file mode 100644 index 0000000000..c7c15c7aab --- /dev/null +++ b/articles/flow/security/react-views/stateless-authentication.adoc @@ -0,0 +1,105 @@ +--- +title: Stateless Authentication +page-title: Stateless authentication for React views with JWT | Vaadin +description: Keeping the authentication of the user in a signed JSON Web Token in the browser, instead of in the HTTP session, in applications with React views. +meta-description: Reference for stateless authentication of Vaadin React views - the stateless configurer, the JSON Web Token cookies, their lifetime, and the principal. +order: 40 +--- + + += Stateless Authentication + +By default, Spring Security keeps the authentication of the user in the HTTP session. With stateless authentication, the browser keeps it instead, in a signed JSON Web Token (JWT). The server doesn't need the session to know who the user is. A restart of the server therefore doesn't log the users out, and several servers recognize the same users without sharing their sessions. + +.Only for React Views +[IMPORTANT] +Stateless authentication is intended for applications whose views are all React views. Its behavior with Flow views is undefined. Flow views keep their UI state in the HTTP session, so they depend on the session in any case. + +section_outline::[] + + +[[dependencies]] +== Dependencies + +Stateless authentication needs these dependencies. Spring Boot manages their versions: + +[source,xml] +---- + + org.springframework.security + spring-security-oauth2-jose + + + org.springframework.security + spring-security-oauth2-resource-server + +---- + + +[[configuration]] +== Configuration + +Apply [classname]`VaadinStatelessSecurityConfigurer` after [classname]`VaadinSecurityConfigurer`, and turn off the sessions of Spring Security: + +[source,java] +---- + +include::{root}/src/main/java/com/vaadin/demo/fusion/security/stateless/SecurityConfigurer.java[tags="stateless-configure"] +---- +<1> The secret key that signs and verifies the JWT, from the Base64-encoded property `my.app.auth.secret`. +<2> The signature algorithm. The key must be long enough for the algorithm. The algorithm HS256 needs a key of at least 32 bytes. +<3> The issuer of the JWT, a string or a URL that identifies the application. + +.Keep the Secret Key Secret +[CAUTION] +==== +Anyone who has the key can create a JWT that the application accepts. Give each application and each environment a key of its own, and don't commit it to the source repository. See <<{articles}/flow/security/protections/external-configuration#,Storing Sensitive Data>>. +==== + +[classname]`VaadinStatelessSecurityConfigurer` has these configuration methods: + +[methodname]`withSecretKey()`:: +Configures the secret key. It returns a configurer with the methods [methodname]`secretKey()`, which sets the key, [methodname]`algorithm()`, which sets the algorithm, and [methodname]`and()`, which returns to [classname]`VaadinStatelessSecurityConfigurer`. When the algorithm isn't set, it comes from the key. An overload of [methodname]`withSecretKey()` takes a [interfacename]`Customizer` of that configurer instead. + +[methodname]`issuer()`:: +Sets the issuer of the JWT. + +[methodname]`expiresIn()`:: +Sets the lifetime of the JWT in seconds. The default is 1800 seconds, which is 30 minutes. + + +[[tokens]] +== Tokens & Cookies + +After the login, the server sends the JWT in two cookies: + +`jwt.headerAndPayload`:: +The header and the payload of the JWT. The browser code can read this cookie. + +`jwt.signature`:: +The signature of the JWT, in an HTTP-only cookie that the browser code can't read. + +The server renews the JWT and the cookies in its responses, so they expire after the lifetime of the JWT has passed without requests. For example, this sets the lifetime to one hour: + +[source,java] +---- +http.with(new VaadinStatelessSecurityConfigurer<>(), cfg -> cfg + .withSecretKey().secretKey(secretKey).and() + .issuer("com.example.application") + .expiresIn(3600)); +---- + +The payload has the name of the user as its subject, the issuer, and the times when the JWT was issued and when it expires. Its `roles` claim lists the role authorities of the user, without the role prefix. When the server reads the JWT, it adds the prefix again. The other granted authorities of the user aren't in the JWT. + +The CSRF token is also in a cookie, `XSRF-TOKEN`, instead of the session. + + +[[principal]] +== The Principal + +With stateless authentication, the principal of the authentication is a [classname]`Jwt` instead of the object that the authentication provider created, such as a [interfacename]`UserDetails`. To get the full user information, load it with the name of the user: + +[source,java] +---- +include::{root}/src/main/java/com/vaadin/demo/fusion/security/stateless/SecurityUtils.java[tags="user-details"] +---- diff --git a/articles/flow/security/react-views/view-access-control.adoc b/articles/flow/security/react-views/view-access-control.adoc new file mode 100644 index 0000000000..780c66f8bf --- /dev/null +++ b/articles/flow/security/react-views/view-access-control.adoc @@ -0,0 +1,112 @@ +--- +title: View Access Control +page-title: Access control for React views in Vaadin applications +description: How Vaadin checks the access options of React views in the browser, in the menu, and on page loads. +meta-description: Reference for access control of Vaadin React views - the access options of views, the checks in the router, in the menu, and on page loads, and role names. +order: 30 +--- + + += View Access Control +:toclevels: 2 + +The `loginRequired` and `rolesAllowed` options of a view control who can open it. They are part of the view configuration, described in <<{articles}/flow/react-views/routing#view-configuration,View Configuration>>. A view without them is open to everyone. A view with `rolesAllowed` needs a logged-in user, even without `loginRequired`. + +Vaadin checks these options in three places: + +- In the browser, when the router opens a view. +- On the server, when it sends the menu items. +- In Spring Security, when the browser loads a page. + +None of these checks protects data. Protect the services that the views call, as described in <>. + +section_outline::[] + + +[[browser]] +== In the Browser + +The [methodname]`protect()` method of [classname]`RouterConfigurationBuilder` makes the router check the options. The [filename]`routes.tsx` file that Vaadin generates calls it. See <<{articles}/flow/react-views/routing#routes-file,The Route Configuration File>>. + +When the router opens a view or a layout that has one of the options, it checks them with the <> function of the authentication context. When the check fails, the router goes to the path that [methodname]`protect()` got, or to `/login` without an argument. This happens also to a logged-in user who doesn't have the role. While the user information is loading, the router renders an empty element instead of the view. + +The options of a layout apply to all the views inside it. + +The check needs the `AuthProvider` of the authentication context. See <>. + + +[[menu]] +== In the Menu + +When the browser loads a page, the server sends the list of views that the menu can show. It leaves out the views that the user can't open, and the views inside a layout that the user can't open. It checks the roles with the [methodname]`isUserInRole()` method of the HTTP request. With Spring Security, this method adds the role prefix to a role name without it. See <<{articles}/flow/react-views/routing#menu,Creating Menu Items>>. + + +[[page-loads]] +== Page Loads + +When the browser loads the page of a view that has `loginRequired: true`, Spring Security checks the request: + +- When nobody is logged in, the browser goes to the login view. After the login, it comes back to the view. +- When the view also has `rolesAllowed`, the user needs a granted authority with the same name as one of the roles. Otherwise, the server responds with `403 Forbidden`. + +The page of a view without `loginRequired: true` loads for everyone. This includes a view that has only `rolesAllowed`, and a view inside a layout that requires login. The check in the browser then decides whether the view opens. + + +[[role-names]] +== Role Names + +The three checks compare the names in `rolesAllowed` with different things: + +[cols="1,2"] +|=== +|Check |Compares `rolesAllowed` with + +|In the browser +|The roles that the `getRoles` function of the authentication context returns, as they are + +|In the menu +|The granted authorities, after adding the role prefix `ROLE_` to the names that don't have it + +|On page loads +|The granted authorities, as they are +|=== + +For example, `rolesAllowed: ['ADMIN']` matches the granted authority `ROLE_ADMIN` in the menu, but not on a page load. To get the same result from all three checks, write the names of the granted authorities in `rolesAllowed`, such as `ROLE_ADMIN`. Then return the granted authorities as they are from `getRoles`, as the example in <> does. + + +[[has-access]] +== Checking Access in a View + +The [methodname]`hasAccess()` function of the authentication context makes the same check as the router. It takes an object with `loginRequired` and `rolesAllowed` properties, and returns `true` when the current user meets them: + +[source,tsx] +---- +const { hasAccess } = useAuth(); +const canDelete = hasAccess({ rolesAllowed: ['ROLE_ADMIN'] }); +---- + +Without either property, it returns `true`. An explicit `loginRequired: false` turns the check off, even together with `rolesAllowed`. The router treats the options of a view the same way. + + +[[route-objects]] +== Route Objects + +For routes that you define as React Router route objects, the [methodname]`protectRoutes()` function of `@vaadin/hilla-react-auth` makes the same check. It takes the routes, and reads the `loginRequired` and `rolesAllowed` options from the `handle` of each route: + +[source,tsx] +---- +import { protectRoutes } from '@vaadin/hilla-react-auth'; + +export const routes = protectRoutes([ + { path: '/admin', element: , handle: { rolesAllowed: ['ROLE_ADMIN'] } }, + { path: '/login', element: }, +], '/login'); +---- + +The second argument is the path to go to when the check fails, and defaults to `/login`. + + +[[flow-views]] +== Flow and React Views Together + +Flow views are protected by their access annotations, as described in <<{articles}/flow/security/access-control/annotations#,Annotation-Based Access Control>>. When the login view is a React view, navigation access control sends the users who aren't logged in to it, also from Flow views. The menu items that <<{articles}/flow/react-views/routing#menu,`createMenuItems()`>> returns include the Flow views that have the [annotationname]`@Menu` annotation and that the user can open. See <<{articles}/flow/react-views/flow-interop#menu,Flow Views in the Menu>>. diff --git a/articles/flow/security/spring/security-configuration.adoc b/articles/flow/security/spring/security-configuration.adoc index dc1dcaa99d..3fd285140c 100644 --- a/articles/flow/security/spring/security-configuration.adoc +++ b/articles/flow/security/spring/security-configuration.adoc @@ -78,7 +78,7 @@ Configures the login view for use in a Flow application and the logout success U public VaadinSecurityConfigurer loginView(String loginView) ---- -Configures a React login view, available at the given path. Use this when the login view of your application is a React view. See <<{articles}/building-apps/react/security#,Secure React Views>>. +Configures a React login view, available at the given path. Use this when the login view of your application is a React view. See <<{articles}/flow/security/react-views/authentication#login-view,Login View of React Views>>. [source,java] ---- @@ -255,6 +255,34 @@ public RequestMatcher defaultPermitMatcher() Creates and returns a composite `RequestMatcher` for identifying requests that should be permitted without authentication within a Vaadin application. This matcher combines multiple specific matchers, including those for framework internal requests, anonymous browser-callable services, allowed Hilla views, anonymous routes, custom web icons, and default security configurations. +[source,java] +---- +public static RequestMatcher getDefaultHttpSecurityPermitMatcher() +public static RequestMatcher getDefaultHttpSecurityPermitMatcher(String urlMapping) +---- + +Returns the matcher for the internal requests of the framework that need the security context. The configurer permits them in [methodname]`defaultPermitMatcher()`. Without an argument, the matcher assumes that the Vaadin servlet is mapped to the root path. Pass the URL mapping of the servlet otherwise. + +[source,java] +---- +public static RequestMatcher getDefaultWebSecurityIgnoreMatcher() +public static RequestMatcher getDefaultWebSecurityIgnoreMatcher(String urlMapping) +---- + +Returns the matcher for the static resources of Vaadin, such as the PWA manifest, the service worker, the default icon, and the themes. The configurer also permits them in [methodname]`defaultPermitMatcher()`. Without an argument, the matcher assumes that the Vaadin servlet is mapped to the root path. + +Use these static methods in a security configuration that doesn't apply the configurer, for example to allow the same requests in a filter chain of your own: + +[source,java] +---- +http.authorizeHttpRequests(auth -> auth + .requestMatchers( + VaadinSecurityConfigurer.getDefaultHttpSecurityPermitMatcher(), + VaadinSecurityConfigurer.getDefaultWebSecurityIgnoreMatcher()) + .permitAll() + .anyRequest().authenticated()); +---- + == Examples === Multiple Filter Chains diff --git a/articles/hilla/_shared/guides/security/_index.adoc b/articles/hilla/_shared/guides/security/_index.adoc deleted file mode 100644 index 58dea547cc..0000000000 --- a/articles/hilla/_shared/guides/security/_index.adoc +++ /dev/null @@ -1,15 +0,0 @@ -// tag::content[] - -// TODO try to move content from this section to places where they are connected, like "securing services" should be in the browser-callable services article, and authorizing view access should be in routing and navigation - -= Security - -Hilla is a combined client and server programming model. -As an application developer, you make a decision about how much of the application state is stored on the server and how much is stored in the user's browser. -The following sections describe the best practices for securing such applications. - -== Topics - -section_outline::[] - -// end::content[] diff --git a/articles/hilla/_shared/guides/security/_session-expiration.adoc b/articles/hilla/_shared/guides/security/_session-expiration.adoc deleted file mode 100644 index dc832d03f5..0000000000 --- a/articles/hilla/_shared/guides/security/_session-expiration.adoc +++ /dev/null @@ -1,49 +0,0 @@ -// tag::content[] - -= Handling Session Expiration - -You can use the built-in <<{articles}/flow/react-views/browser-callable-services/client#middleware, middleware>> [classname]`InvalidSessionMiddleWare` to detect when a user session expires. - -This middleware requires a function as a constructor parameter (type of `OnInvalidSessionCallback`). -The function should return a promise of [classname]`LoginResult`, containing the metadata of a login result, including: - -`error`:: -Indicates whether the login attempt has failed. - -`token`:: -In the event of a successful login, this is the cross-site request forgery (CSRF) prevention token, which can be extracted from the [filename]`index.html` page. -See <<{articles}/hilla/guides/security/configuring#csrf, CSRF protection of browser-callable services>> for more information. - -`errorTitle`:: -A short text describing a login error. - -`errorMessage`:: -A more detailed explanation of the login error. - - - -== Example - -As an example, you can use the `InvalidSessionMiddleware` to show a login view to the user. - -ifdef::hilla-react[] -Configure the [classname]`ConnectClient` with the middleware, and let the callback open a login overlay: - -.`frontend/connect-client.ts` -[source,typescript] ----- -include::{root}{root-fix}/frontend/demo/auth/session-expiration/connect-client.ts[tags=snippet] ----- - -The overlay reuses the `login` function from the <>, and resolves the promise the middleware is waiting on once the user has signed in again: - -.`frontend/auth/SessionExpiredLoginOverlay.tsx` -[source,tsx] ----- -include::{root}{root-fix}/frontend/demo/auth/session-expiration/SessionExpiredLoginOverlay.tsx[tags=snippet] ----- - -Render [classname]`SessionExpiredLoginOverlay` once in your main layout, so that it's mounted for every view. It stays closed until the middleware reports an expired session, and the service call that triggered it proceeds after a successful login. -endif::hilla-react[] - -// end::content[] diff --git a/articles/hilla/_shared/guides/security/_spring-login.adoc b/articles/hilla/_shared/guides/security/_spring-login.adoc deleted file mode 100644 index 5b73ef917c..0000000000 --- a/articles/hilla/_shared/guides/security/_spring-login.adoc +++ /dev/null @@ -1,437 +0,0 @@ -// tag::content[] - - -= Authentication with Spring Security - -Authentication may be configured to use Spring Security. Since the downloaded application is a link:https://spring.io/projects/spring-boot[Spring Boot] project, the easiest way to enable authentication is by adding link:https://spring.io/projects/spring-security[Spring Security]. - - -== Dependencies - -Using Spring Security requires some dependencies. Add the following to your project Maven file: - -.`pom.xml` -[source,xml] ----- - - org.springframework.boot - spring-boot-starter-security - ----- - -After doing this, the application is protected with a default Spring login view. By default, it has a single user (i.e., 'user') and a random password. When you add `logging.level.org.springframework.security = DEBUG` to the [filename]`application.properties` file, the username and password are shown in the console when the application starts. - - -== Server Configuration - -To implement your own security configuration, create a new configuration class that uses the [classname]`VaadinSecurityConfigurer` class. Then annotate it to enable security. - -[classname]`VaadinSecurityConfigurer` is a helper which provides default bean implementations for link:https://docs.spring.io/spring-security/site/docs/current/api/org/springframework/security/web/SecurityFilterChain.html[SecurityFilterChain] and link:https://docs.spring.io/spring-security/site/docs/current/api/org/springframework/security/config/annotation/web/configuration/WebSecurityCustomizer.html[WebSecurityCustomizer]. It takes care of the basic configuration for requests, so that you can concentrate on your application-specific configuration. - -.`SecurityConfig.java` -[source,java] ----- -@EnableWebSecurity -@Configuration -public class SecurityConfig { - - @Bean - SecurityFilterChain securityFilterChain(HttpSecurity http, RouteUtil routeUtil) throws Exception { - http.with(VaadinSecurityConfigurer.vaadin(), configurer -> { - // use a custom login view and redirect to root on logout - configurer.loginView("/login", "/"); - }); - return http.build(); - } - - @Bean - public UserDetailsManager userDetailsService() { - // Configure users and roles in memory - return new InMemoryUserDetailsManager( - // the {noop} prefix tells Spring that the password is not encoded - User.withUsername("user").password("{noop}user").roles("USER").build(), - User.withUsername("admin").password("{noop}admin").roles("ADMIN", "USER").build() - ); - } -} ----- - -.Never Hard-Coded Credentials -[WARNING] -You should never hard-code credentials in an application. The <<{articles}/hilla/guides/security/spring-login#appendix-production-data-sources,Security>> documentation has examples of setting up LDAP or SQL-based user management. - - -=== Public Views & Resources - -Public views need to be added to the configuration in [methodname]`securityFilterChain`. Here's an example of this: - -.`SecurityConfig.java` -[source,java] ----- - @Bean - SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { - http.authorizeHttpRequests(registry -> { - registry.requestMatchers("/public-view").permitAll(); // custom matcher - }); - http.with(VaadinSecurityConfigurer.vaadin(), configurer -> { ... }); - return http.build(); - } ----- - -Public resources can be added to the configuration in [methodname]`securityFilterChain` like so: - -.`SecurityConfig.java` -[source,java] ----- -include::{root}{root-fix}/src/main/java/com/vaadin/demo/fusion/security/authentication/SecurityConfigDemo.java[tags=public-resources] ----- - -ifdef::hilla-react[] - - -== Implement Client-Side Security [[client-side-security]] - -The implementation of client-side security in React is greatly simplified by the `react-auth` package. It can use any `@BrowserCallable` service that provides user authentication information, like the one in this example: - -[#_user-info-service] -[.example] --- -[source,java] ----- -include::{root}{root-fix}/src/main/java/com/vaadin/demo/fusion/security/authentication/UserInfoService.java[tags=snippet] ----- - -[source,java] ----- -include::{root}{root-fix}/src/main/java/com/vaadin/demo/fusion/security/authentication/UserInfo.java[tags=snippet] ----- --- - -You can instruct Hilla to use this service as the source for authentication information. The service is expected to return no user if not authenticated, or an object providing information about the current user. - -The easiest way to configure authentication to use this service is to create an [filename]`auth.ts` file: - -.`frontend/auth.ts` -[source,typescript] ----- -include::{root}{root-fix}/frontend/demo/auth/auth.ts[] ----- - -The exported `AuthProvider` must wrap the root component of your application to be able to apply security to it: - -`frontend/index.tsx` -[source,tsx] ----- -include::{root}{root-fix}/frontend/demo/auth/index.tsx[tags=snippet] ----- - -The `useAuth` hook provides four items: `state`, which contains the authentication state that allows access to the user object and other information like authentication status (i.e., loading, errors); `login` and `logout`, which are functions used to perform the corresponding actions; and `hasAccess`, which is another function used to verify if the current user has access to a path. - - -=== Login View - -Use the `` component to create the following login view, so that the autocomplete and password features of the browser are used: - -.`frontend/views/LoginView.tsx` -[source,typescript] ----- -include::{root}{root-fix}/frontend/demo/auth/LoginView.tsx[tags=snippet] ----- - - -=== Protect Hilla Views - -After the login view is defined, you should define a route for it in the [filename]`routes.tsx` file. You can wrap the routes definition with the `protectRoutes` function that will filter out views having authentication requirements not fulfilled by the current authentication state. - -.`frontend/routes.tsx` -[source,typescript] ----- -include::{root}{root-fix}/frontend/demo/auth/routes.tsx[tags=snippet] ----- - - -=== Role-based Protection - -Hilla supports role-based security by default, provided that the user object returned by your service has a `roles` property. This returns an array or collection of strings. In that case, you can configure a route to allow access to one or more specific roles: - -.`frontend/routes.tsx` -[source,typescript] ----- -{ - path: '/', - element: , - handle: { title: 'Administration Page', rolesAllowed: ['ADMIN'] }, -} ----- - -If roles are exposed differently in your user object, you can still tell Hilla how to find them by amending your authentication configuration on the client. In the <<#_user-info-service,[classname]`UserInfo` example>> provided before, the roles are returned by [methodname]`getAuthorities`, so the [filename]`auth.ts` file should be modified as such: - -.`frontend/auth.ts` -[source,typescript] ----- -include::{root}{root-fix}/frontend/demo/auth/authRoles.ts[tags=snippet] ----- - -The `getRoles` function is still expected to return an array of strings. As a result, you might need to `map` your roles to strings. - -endif::hilla-react[] - - -=== Configuration Helper Alternatives - -[methodname]`VaadinSecurityConfigurer` configures HTTP security to bypass framework internal resources. If you prefer to make your own configuration, instead of using the helper, the matcher for these resources can be retrieved with [methodname]`VaadinSecurityConfigurer.getDefaultHttpSecurityPermitMatcher()`. - -For example, if you want to allow public access to certain views and require authentication for all other requests, you can configure it as follows: - -.`SecurityConfig.java` -[source,java] ----- -@Bean -SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { - http.authorizeHttpRequests(registry -> - registry.requestMatchers( - VaadinSecurityConfigurer.getDefaultHttpSecurityPermitMatcher() - ).permitAll() - .requestMatchers("/public-view").permitAll() // custom matcher - .anyRequest().authenticated() - ); - ... - return http.build(); -} ----- - -Similarly, the matcher for static resources to be ignored is available as [methodname]`VaadinSecurityConfigurer.getDefaultWebSecurityIgnoreMatcher()`: - -.`SecurityConfig.java` -[source,java] ----- -@Bean -SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { - http.authorizeHttpRequests(registry -> - registry.requestMatchers( - VaadinSecurityConfigurer.getDefaultWebSecurityIgnoreMatcher() - ).permitAll() - .requestMatchers("static/**").permitAll() // custom matcher - .anyRequest().authenticated() - ); - ... - return http.build(); -} ----- - - -== Implement Stateful Authentication - -Vaadin applications that have both Hilla and Flow views, can be configured to use stateful authentication. This requires some basic <> and <<{articles}/flow/security/spring/setup#, steps for Flow>>. -An example project that demonstrates the stateful authentication for the hybrid case can be found in link:https://github.com/vaadin/flow-hilla-hybrid-example/tree/v24.4[GitHub]. - -For this example, you'd add <> and then set up <>. - -ifdef::hilla-react[] -Next, you'd implement <>, including the user information service, `useAuth` and `AuthProvider` objects and their use. Also, implement <>. -endif::[] - -The browser page needs to be reloaded after login and, if you want to exclude the `LoginView` from the automatically generated menu, you need to set: - -.`LoginView.tsx` -[source,typescript] ----- -export const config: ViewConfig = { - menu: { exclude: true} -} ----- - -The next step is to protect the views with login and roles. Add the annotations to the server-side views, as described in <<{articles}/flow/security/access-control/annotations#, Annotation-Based Access Control>>. Add the `ViewConfig` object to the client-side views, as shown below: - -.`HillaView.tsx` -[source,typescript] ----- -export const config: ViewConfig = { - loginRequired: true, - rolesAllowed: ['ROLE_USER'], -}; ----- - -Use `createMenuItems` function to create a main layout, that filters out protected views and shows the only allowed views for an authenticated user. - -.`frontend/views/@layout.tsx` -[source,typescript] ----- -import { createMenuItems } from '@vaadin/hilla-file-router/runtime.js'; -import { AppLayout, SideNav } from '@vaadin/react-components'; -import { Outlet, useLocation, useNavigate } from 'react-router'; - -// inside layout component: -const navigate = useNavigate(); -const location = useLocation(); -// ... - - // ... - // SideNav Vaadin component inside - navigate(path!)} - location={location}> - { - createMenuItems().map(({ to, title }) => ( - {title} - )) - } - - ----- - -As an alternative, add the menu items manually and specify the access options: - -.`frontend/MainLayout.tsx` -[source,typescript] ----- -include::{root}{root-fix}/frontend/demo/auth/MainLayout.tsx[] ----- - -Then you can add a custom configuration for routes -- this is optional. Routes configuration is usually present in [filename]`routes.tsx` file, which is generated by Vaadin. This should be enough for common cases: - -.`Frontend/generated/routes.tsx` -[source,typescript] ----- -import { RouterConfigurationBuilder } from '@vaadin/hilla-file-router/runtime.js'; -import Flow from 'Frontend/generated/flow/Flow'; -import fileRoutes from 'Frontend/generated/file-routes.js'; - -export const { router, routes } = new RouterConfigurationBuilder() - .withFileRoutes(fileRoutes) - .withFallback(Flow) - .protect() - .build(); ----- - -Note that the client-side views are protected by default with a `protect()` function. If a custom routing is desired, the generated file [filename]`Frontend/generated/routes.tsx` should be copied to [filename]`Frontend/routes.tsx` and modified. - -For example, you may want to change the login URL: - -.`Frontend/generated/routes.tsx` -[source,typescript] ----- -new RouterConfigurationBuilder().protect('/custom-login-url') ----- - -Add specific React route objects with `withReactRoutes` function: - -.`Frontend/generated/routes.tsx` -[source,typescript] ----- -new RouterConfigurationBuilder().withReactRoutes( - [ - { - element: , - handle: { title: 'Main' }, - children: [ - { path: '/hilla', element: , handle: { title: 'Hilla' } } - ], - }, - { path: '/login', element: , handle: { title: 'Login' } } - ] -) ----- - -Disable server-side views or add a fallback component with a `withFallback` function. For example, 404 page that will be shown if no client-side view is found for a given URL. - -.`Frontend/generated/routes.tsx` -[source,typescript] ----- - new RouterConfigurationBuilder().withFallback(PageNotFoundReactComponent) ----- - - -[[appendix-production-data-sources]] -== Appendix: Production Data Sources - -The example given here of managing users in memory is valid for test applications. However, Spring Security offers other implementations for production scenarios. - - -=== SQL Authentication - -The following example demonstrates how to access an SQL database with tables for users and authorities. - - -.`SecurityConfig.java` -[source,java] ----- - -@EnableWebSecurity -@Configuration -public class SecurityConfig { - //... - - @Bean - UserDetailsService userDetailsService(DataSource dataSource) { - // Configure users and roles in a JDBC database - JdbcUserDetailsManager jdbcUserDetailsManager = new JdbcUserDetailsManager(dataSource); - jdbcUserDetailsManager.setUsersByUsernameQuery( - "SELECT username, password, enabled FROM users WHERE username=?"); - jdbcUserDetailsManager.setAuthoritiesByUsernameQuery( - "SELECT username, authority FROM authorities WHERE username=?"); - return jdbcUserDetailsManager; - } - - @Bean - PasswordEncoder passwordEncoder() { - return new BCryptPasswordEncoder(); - } -} ----- - -=== LDAP Authentication - -This next example shows how to configure authentication by using an LDAP repository: - -.`SecurityConfig.java` -[source,java] ----- - -@EnableWebSecurity -@Configuration -public class SecurityConfig { - //... - - @Bean - public EmbeddedLdapServerContextSourceFactoryBean contextSourceFactoryBean() { // <1> - return EmbeddedLdapServerContextSourceFactoryBean.fromEmbeddedLdapServer(); - } - - @Bean - AuthenticationManager authenticationManager(BaseLdapPathContextSource contextSource, LdapAuthoritiesPopulator authorities) { - LdapPasswordComparisonAuthenticationManagerFactory factory = new LdapPasswordComparisonAuthenticationManagerFactory( - contextSource, NoOpPasswordEncoder.getInstance()); - factory.setUserDnPatterns("uid={0},ou=people"); - factory.setUserSearchBase("ou=people"); - factory.setPasswordAttribute("userPassword"); - factory.setLdapAuthoritiesPopulator(authorities); - return factory.createAuthenticationManager(); - } - - @Bean - LdapAuthoritiesPopulator authorities(BaseLdapPathContextSource contextSource) { - String groupSearchBase = "ou=groups"; - DefaultLdapAuthoritiesPopulator authorities = - new DefaultLdapAuthoritiesPopulator(contextSource, groupSearchBase); - authorities.setGroupSearchFilter("member={0}"); - return authorities; - } -} ----- - -<1> `VaadinSecurityConfigurer` example here configure https://docs.spring.io/spring-security/reference/servlet/authentication/passwords/ldap.html#servlet-authentication-ldap-embedded[embedded UnboundID LDAP server]. You can also configure https://docs.spring.io/spring-security/reference/servlet/authentication/passwords/ldap.html#servlet-authentication-ldap-contextsource[LDAP ContextSource] to connect to other LDAP server. - -Remember to add the corresponding LDAP client dependency to the project: - -.`pom.xml` -[source,xml] ----- - - org.springframework.security - spring-security-ldap - 5.2.0.RELEASE - ----- - -// end::content[] diff --git a/articles/hilla/guides/full-stack-signals.adoc b/articles/hilla/guides/full-stack-signals.adoc index 1c9e41bf1c..f211ab865a 100644 --- a/articles/hilla/guides/full-stack-signals.adoc +++ b/articles/hilla/guides/full-stack-signals.adoc @@ -593,7 +593,7 @@ Security with full-Stack signals can be enabled in two separate levels. They're Full-stack signals are exposed by the services that are annotated with [classname]`@BrowserCallable` -- or the synonym, [classname]`@Endpoint`. This means the services that expose the signals are secured by the same security rules as any other service using the [classname]`@AnonymousAllowed`, [classname]`@PermitAll`, [classname]`@RolesAllowed`, or [classname]`@DenyAll` on the class or the individual methods. -For more information on how to secure the services, see the <<./security/intro#, security documentation>>. +For more information on how to secure the services, see <<{articles}/flow/security/react-views/service-access-control#,Service Access Control>>. [[fine-grained-access-control]] diff --git a/articles/hilla/guides/security/authentication.adoc b/articles/hilla/guides/security/authentication.adoc deleted file mode 100644 index 02d0ce04b1..0000000000 --- a/articles/hilla/guides/security/authentication.adoc +++ /dev/null @@ -1,139 +0,0 @@ ---- -title: Accessing Auth Data -page-title: How to configure authentication in Hilla | Vaadin -description: Accessing authentication data such as username and roles on the server side, as well as transferring the data to the client. -meta-description: Learn how to access authentication data such as username and roles on the server side, as well as transferring the data to the client in Hilla applications. -order: 95 ---- -= Accessing Authentication Data - -Although authorization is defined at service level (as described in the <>), you may need to know specific authentication parameters, either in the Java service code or in client-side code. - -Accessing authentication data such as username and roles on the server side, as well as transferring the data to the client, is explained here. - -== Server Side Access - -=== Security Principal - -Hilla authenticates each server request and, if authentication is successful, associates the request with a Java security principal. You can get the authenticated user as a [classname]`UserPrincipal` from the current request. You can retrieve the current request using [methodname]`VaadinRequest.getCurrent()`. Calling [methodname]`getUserPrincipal()` for the request returns the authenticated user, or null if the request isn't authenticated. - -[.example] --- - -.[filename]`EchoService.java` -[source,java] ----- -@BrowserCallable -public class EchoService { - @PermitAll - public String saySomething(String message) { - return VaadinRequest.getCurrent().getUserPrincipal().getName() + " says: " + message; - } -} ----- - -.[filename]`frontend/index.ts` -[source,typescript] ----- -import { EchoService } from 'Frontend/generated/EchoService'; - -EchoService - .saySomething("It's snowing in Turku") - .then(response => console.log(response)); ----- - --- - -ifndef::react[] -== With Spring Security - -The easiest way to configure authentication is by using <>, so use its API to check the user in your services. In the next example, the username is checked in the Java code: - -[source,java] ----- -@BrowserCallable -public class DrawService { - - @PermitAll - public String checkWinner() { - Authentication auth = - SecurityContextHolder.getContext().getAuthentication(); - if (auth != null && "peter".equals(auth.getName())) { - return "Congrats! you are the winner."; - } - return "Sorry, keep looking"; - } -} ----- -endif::[] - -== Client Side Authentication - -// If you need to know on the client side whether a user is authenticated, please read <>. - -=== Checking the Username - -In TypeScript, there is no direct way to check whether the user is authenticated. However, you can expose a server-side service method that checks user privileges and returns the status. - -The next example returns the username if the user is logged in; otherwise it returns the word `anonymousUser`: - -[source,java] ----- -import org.springframework.security.core.Authentication; -import org.springframework.security.core.context.SecurityContextHolder; - -@BrowserCallable -public class MyAppService { - - @AnonymousAllowed - public String checkUser() { - Authentication auth = - SecurityContextHolder.getContext().getAuthentication(); - return auth == null ? null : auth.getName(); - } -} ----- - -[source,typescript] ----- -import { MyAppService } from 'Frontend/generated/MyAppService'; - -const username = await MyAppService.checkUser(); - -if ('anonymousUser' === username) { - console.log('You are an anonymous user'); -} else { - console.log('Your username is: ' + username); -} ----- - -=== Checking Roles - -A developer might want to check whether the user can access certain services, so that the appropriate options are enabled in the application menu. - -The following example exposes a method that checks whether a user is an admin user. - -[source,java] ----- -@BrowserCallable -public class MyAppService { - - @RolesAllowed("ROLE_ADMIN") - public boolean isAdmin() { - return true; - } -} ----- - -[source,typescript] ----- -import { MyAppService } from 'Frontend/generated/MyAppService'; - -const isAdmin = await MyAppService.isAdmin().catch(() => false); - -if (isAdmin) { - console.log('You are an admin user'); -} else { - console.log('Sorry, you are not an admin user'); -} ----- diff --git a/articles/hilla/guides/security/best-practices.adoc b/articles/hilla/guides/security/best-practices.adoc deleted file mode 100644 index f4a86936ad..0000000000 --- a/articles/hilla/guides/security/best-practices.adoc +++ /dev/null @@ -1,59 +0,0 @@ ---- -title: Best Practices -page-title: How to implement security best practices in Hilla apps -description: Best practices in authentication and authorization, data validation, and SSL and HTTPS. -meta-description: Follow best practices to secure Hilla applications and protect sensitive data. -order: 151 ---- -= Security Best Practices - -== Authentication & Authorization - -Hilla lets you choose which authentication and authorization framework you want to use, instead of bundling any specific one. -Hilla is fully compatible with the most-used security solutions in the Java ecosystem, including, but not limited to, Spring Security, JAAS and Apache Shiro. -The Hilla Spring add-on has helpers for developers to integrate into the security mechanisms of these frameworks. - -Since Hilla is a server-side framework, credential-processing always happens on the server, away from any possible attack surface. -Credentials are never transmitted to the client, unless this is explicitly done by the developer. - -Generally, it's recommended that the developer double-check user identity and access rights for each call from the client. -This can be automated with, for example, Spring Security and view-based authentication using roles. -What typically cannot be automated by these frameworks is data-based access rights, such as limiting access to specific entities. - -As an example, if the server receives the ID of a `User` object to be displayed in, say, a URL request parameter (`{yourapp.com}/users/4/edit`), the ID in question can be freely changed by an attacker. -The application needs to be aware of this and check that the currently logged-in user has access rights to this entity. -This is something that's common to all UI frameworks, and not specific to Hilla. - -ifndef::react[] -See <> for examples of integrating Spring Security. -endif::[] - -== Data Validation - -In a Hilla application, the data-binding API supports data validation on the server, which cannot be bypassed via client-side attacks. -Vaadin components do support client-side validation to increase the responsiveness of the application. -However, the developer should be aware that these should be used purely for convenience, since they are easily circumvented in the browser. - -As with other web applications, data coming from the client should always be validated once it reaches the server. -It's not safe to rely only on client-side validation. -Hilla provides a set of pre-created server-side validators for this purpose. -In addition, the developer is free to use any Java API to validate the data, including connecting to external services. -Hilla also has a built-in integration with Java's Bean Validation (JSR-303) standard. - -Data coming from a data store (such as a database) and inserted as HTML into DOM elements (for example, by setting `innerHTML` for elements or using HTML mode in component captions) should also be escaped. -// TODO no such chapter I believe -// See the chapter on XSS for more information. - - -== Browser-Callable Services - -By default, a browser-callable service requires requests to be authenticated. -It's recommended to use stricter access control, such as `@RolesAllowed()`. -Less strict access control, such as `@AnonymousAllowed`, should be used with caution. -See <<./configuring#, Configuring Security>> for more information. - -== SSL & HTTPS - -Vaadin always recommends developers to set up TLS on the server and run all communication exclusively under HTTPS. -Hilla works out of the box with HTTPS, and there is nothing for the developer to configure in their application code. -Refer to the documentation of your servlet container for details on how to set up HTTPS on your server. diff --git a/articles/hilla/guides/security/configuring.adoc b/articles/hilla/guides/security/configuring.adoc deleted file mode 100644 index 3a60805a19..0000000000 --- a/articles/hilla/guides/security/configuring.adoc +++ /dev/null @@ -1,191 +0,0 @@ ---- -title: Controlling Service Access -page-title: How to configure security in Hilla | Vaadin -description: How to specify the role-based access control rules as annotations for the browser-callable service class or its individual methods. -meta-description: Set up and customize security configurations in Hilla applications. -order: 80 ---- - -= Controlling Service Access - -When developing server-side views, access control is implemented using regular Java approaches: servlet-container-based security, third-party libraries, or session-based solutions. - -This article describes all the pieces needed for securing client-centric applications. - -== Securing Browser-Callable Services - -The first step is to configure authorization of each service that the application exposes. - -Consider the simple browser-callable service defined in the following class: - -.`CounterService.java` -[source,java] ----- -@BrowserCallable -@PermitAll -public class CounterService { - public int addOne(int number) { - return number + 1; - } -} ----- - -Hilla access-control features are enabled by default for any service method. -If not specified in Java code explicitly, a [classname]`Principal` object must be present in the request before invoking a service method. -The [methodname]`HttpServletRequest.getUserPrincipal()` Java API is used for the check. - -At this point, the servlet container or the application needs to be configured appropriately to handle user authentication. -This is described in <>. - -[[security-options]] -== Security Options - -By default, all anonymous and authenticated requests are denied. -To change the default behavior, place a security Java annotation on either the service class or the service method. -The following annotations are available: - -* [classname]`@PermitAll` -Allows any authenticated user to call a method via the request. -* [classname]`@RolesAllowed` -Grants access to users having the roles specified in the annotation value. Roles are covered in the <>. -* [classname]`@DenyAll` -Disallows anyone from calling the method via the request. The default. -* [classname]`@AnonymousAllowed` -Permits anyone to call the method via the request without authorization. - -A security annotation that's placed on a class is applied to any public method of this class that has no security annotations. -If the method has security annotations, any class-level annotation is discarded and only method-level ones are applied. - -If there are multiple annotations specified on some entity, the following rules are applied: - -- `DenyAll` overrides other annotations -- `AnonymousAllowed` overrides `RolesAllowed` and `PermitAll` -- `RolesAllowed` overrides `PermitAll` - -Example: - -.`MyService.java` -[source,java] ----- -@BrowserCallable -@PermitAll -public class MyService { - - @DenyAll - public void deniedMethod() { - // Not possible to call by any request. Since there - // is a `@PermitAll` annotation on the - // class, we specify this one on a method to override - // the class one. - } - - @AnonymousAllowed - public void anonymousMethod() { - // Possible to call by any request (even without - // authorization) since method level annotation - // overrides class-level one - } - - public void permittedToAllMethod() { - // Permitted to all authenticated users as per the - // class annotation. - } - - @RolesAllowed("ROLE_ADMIN") - public void permittedToRoleMethod() { - // Permitted to all authenticated users having the - // role ROLE_ADMIN. - } -} ----- - -[[permissions]] -== Defining User Permissions - -As mentioned in the previous section, every user can have roles and this may affect their ability to access some service methods. -This section explains how to specify these for each user. - -Browser-callable services check the existence of roles by using the [methodname]`HttpServletRequest.isUserInRole(String)` Java API. - -After the servlet container has been configured to handle user authentication, when the server receives a request for the secured service, the user and its roles are checked. -If everything is OK, the method is executed. - -== Accessing User Information in a Service Method - -When access to the [classname]`UserPrincipal` is required in a service, call the [methodname]`VaadinRequest.getCurrent()` method to access the `HttpServletRequest` API. - -.`EchoService.java` -[source,java] ----- -@BrowserCallable -public class EchoService { - public String saySomething(String message) { - return VaadinRequest.getCurrent().getUserPrincipal().getName() + " says: " + message; - } -} ----- - -.`frontend/index.ts` -[source,typescript] ----- -import { EchoService } from 'Frontend/generated/EchoService'; - -EchoService - .saySomething("It's snowing in Turku") - .then(response => console.log(response)); ----- - -[role="since:com.vaadin:vaadin@V25.3"] -== Auditing Denied Calls - -When an access check rejects a call to a browser-callable service, Vaadin publishes a Spring Security authorization denied event -- the same event that method security publishes for a rejected call to a `@PreAuthorize` bean. An application that audits denied access therefore sees rejected service calls alongside the rest of its audit trail. - -The events are published only when the application provides an [classname]`AuthorizationEventPublisher` bean, which is the opt-in Spring Security itself uses for these events: - -[source,java] ----- -@Configuration -public class AuditConfig { - - @Bean - AuthorizationEventPublisher authorizationEventPublisher( - ApplicationEventPublisher publisher) { - return new SpringAuthorizationEventPublisher(publisher); - } -} ----- - -A listener then receives an [classname]`AuthorizationDeniedEvent` whose subject is an [classname]`EndpointInvocation`. It implements [interfacename]`MethodInvocation`, so a listener written for method security accepts it unchanged, and it names the service and the method that was denied: - -[source,java] ----- -@Component -public class DeniedCallAuditor { - - @EventListener - public void onAccessDenied(AuthorizationDeniedEvent event) { - if (event.getObject() instanceof EndpointInvocation invocation) { - LoggerFactory.getLogger(getClass()).warn( - "Denied call: user={} service={} method={}", - event.getAuthentication().get().getName(), - invocation.getEndpointName(), - invocation.getMethodName()); - } - } -} ----- - -The service bean the call was addressed to is available from [methodname]`getThis()`, which is useful for routing an alert to whoever owns that service. [methodname]`getArguments()` is always empty and [methodname]`proceed()` throws, because the access check runs before the request payload is deserialized: the call never happened, and its payload can't leak into the audit log. - -The authentication the event carries is the caller as Spring Security resolves it, so an anonymous caller arrives as an [classname]`AnonymousAuthenticationToken` rather than as `null`. When there's no security context at all, reading the authentication throws [classname]`AuthenticationCredentialsNotFoundException`. - -== Client Side Authentication - -If it's required to know, on the client side, whether a user is authenticated, please read <<{articles}/hilla/guides/security/authentication#, Checking Authentication>>. - -[[csrf]] -== CSRF Protection of Browser-Callable Services - -Browser-callable services are protected from Cross-Site Request Forgery (CSRF) attacks by Spring Security's CSRF protection, using the token that Vaadin adds to the page. -See <<{articles}/flow/security/protections/vulnerabilities#cross-site-request-forgery-csrf, Cross-Site Request Forgery>> in the Vaadin Flow security guide for more details. - diff --git a/articles/hilla/guides/security/index.adoc b/articles/hilla/guides/security/index.adoc deleted file mode 100644 index 7f7b0d3b0e..0000000000 --- a/articles/hilla/guides/security/index.adoc +++ /dev/null @@ -1,9 +0,0 @@ ---- -title: Security -page-title: How to secure your Hilla application | Vaadin -description: "The best practices for securing applications, configuring security, authentication and role-based access control for views." -meta-description: Explore comprehensive security practices and tools to safeguard your Hilla applications. -order: 100 ---- -:hilla-react: -include::{root}/articles/hilla/_shared/guides/security/_index.adoc[tag=content] diff --git a/articles/hilla/guides/security/intro.adoc b/articles/hilla/guides/security/intro.adoc deleted file mode 100644 index 13ffecc17e..0000000000 --- a/articles/hilla/guides/security/intro.adoc +++ /dev/null @@ -1,99 +0,0 @@ ---- -title: Introduction -page-title: Introduction to security in Hilla | Vaadin -description: An introduction to the Hilla security architecture and how it works in practice. -meta-description: Learn the basics of securing Vaadin Hilla applications with proven strategies. -order: 10 ---- -= Security in Hilla applications - -== Architecture - -Hilla enables applications' security by orchestrating server- and client-side security. -Although the technical details are taken care of by the framework, it's important to understand how to design the application so that the server responds to client requests in a secure way. - -=== Authentication - -Most applications need to ensure that only eligible users can access sensitive data stored on the server. -The client side authenticates by first obtaining an access token identifying the server session. -The token contains, among other attributes, the principal (current user). -This token is then included in subsequent service calls. -In Hilla, the token is a `JSESSIONID` cookie obtained via either the Spring-provided login form or an authentication helper method. - -ifndef::react[] -See <> for more information on configuring authentication. -These sections also describe how to configure Spring Security to use different authentication providers, such as a SQL database or LDAP. -endif::[] - -=== Authorization - -Apart from authenticating users, applications often need to enforce restrictions on who can view what data. -When the server receives an HTTP request targeting a browser-callable service, Hilla first checks whether the accessing user is authorized to invoke that service. -It does this based on the standard role-based access control scheme. -The application developer can specify role-based access rules as annotations for the service class or its individual methods. -For example: - -.[filename]`MyService.java` -[source,java] ----- -@BrowserCallable -public class MyService { - @PermitAll - public void permittedToAllMethod() { - // Any authenticated user can access - } - - @RolesAllowed("ROLE_ADMIN") - public void permittedToRoleMethod() { - // Only users with admin role can access - } -} ----- - -In the above example, all methods in the [classname]`MyService` class are denied by default. -The annotations `@PermitAll` and `@RolesAllowed` relax this restriction on the two methods [methodname]`permittedToAllMethod()` and [methodname]`permittedToRoleMethod()`, respectively. -Services can also be exposed for unauthenticated requests using the `@AnonymousAllowed` annotation. -See <> for more details and code examples on service security. - - -== Security Considerations - -=== Application Security is Service Security - -Once the client side of an application has been delivered to the browser, the server cannot trust that it hasn't been tampered with. -This means that validation and sanitation of data received by browser-callable services should always occur on the server, regardless of the client-side logic. - -=== Cross-Site Request Forgery (CSRF) Protection - -A CSRF token is delivered to the browser when the Hilla application is first loaded, as part of the initial bootstrap HTML. -Its purpose is to protect the session from requests that don't originate from the client code that was served to that browser. -The CSRF token is included in each service call and validated as matching by the server. - -=== Cached Data - -Hilla-based PWAs may choose to cache data retrieved from the server on the client side, using, for example, browser local storage, in order for a data-centric application to work offline. -The developer needs to make an informed decision on whether the data is safe to store, and implement the clean-up of such stored local data if necessary – for example, at log-out. - -== Vaadin Security Practices - -=== Releasing Security Patches - -Security fixes are implemented as fast as possible and released for all supported versions. -The fix is mentioned in the release notes, and Vaadin also sends a separate security notification email to all registered users, explaining the issue and how to fix it. -This is typically achieved by updating to a new maintenance version. - -=== Users Reporting Security Issues - -If a developer or user finds a potential security issue, they can report it directly to `security@vaadin.com`. -The issue is reviewed and fixed internally, before publishing to GitHub. - -If the issue is minor and public discussion is acceptable and appropriate, then issues can reported directly in GitHub. - -=== Internal Security Practices - -All code goes through an internal code review before it's merged. -Each change is also run against an existing battery of tens of thousands of unit, integration and behavior tests that have to be passed for the merge to be accepted. - -Developers are also encouraged to actively think about security issues while developing the framework and its parts. -Vaadin takes security extremely seriously. -Anyone can escalate an issue that they think might relate to security, and investigating it's given priority over other tasks. diff --git a/articles/hilla/guides/security/offline-auth.adoc b/articles/hilla/guides/security/offline-auth.adoc deleted file mode 100644 index eaebb9a7f8..0000000000 --- a/articles/hilla/guides/security/offline-auth.adoc +++ /dev/null @@ -1,114 +0,0 @@ ---- -title: Offline Authentication -page-title: How to implement offline authentication in Hilla apps | Vaadin -description: Storing the authentication data in the browser for offline applications. -meta-description: Learn how to implement offline authentication for Hilla applications to enhance user experience. -order: 110 ---- -= Offline Authentication - -When building a Hilla application with offline support, take care when storing authentication in the browser to enable offline access-checking on the client side. - -The `localStorage` API is an easy way to store data in the browser that also works offline. -This article describes how to use it to store authentication. - -== Storing Authentication for Offline - -It's good practice to expire authentication after a time limit. -However, the data in `localStorage` doesn't expire automatically; you need to have a timestamp in the authentication data object itself. -Add the timestamp property to the TypeScript definition for the authentication object, as follows: - -.`auth.ts` -[source,typescript] ----- -include::{root}/frontend/demo/fusion/authentication/auth.ts[tags=basic;!userinfo] ----- - -Also, define the string key for `localStorage`, as well as the maximum age constant. -The examples in this article use a 30-day limit. - -.`auth.ts` -[source,typescript] ----- -include::{root}/frontend/demo/fusion/authentication/auth.ts[tags=storage-defs] ----- - -In your login method, if the login is successful, create the object with the timestamp, and save it using [methodname]`localStorage.setItem()`: - -.`auth.ts` -[source,typescript] ----- -include::{root}/frontend/demo/fusion/authentication/auth.ts[tags=login;!userinfo] ----- - -The saved authentication can now be reloaded when starting the application offline. - -== Restoring Authentication on Load - -Use the [methodname]`localStorage.getItem()` method to restore the authentication when the application starts. - -.`auth.ts` -[source,typescript] ----- -include::{root}/frontend/demo/fusion/authentication/auth.ts[tags=restore;!userinfo] ----- - -Remember to check the timestamp before using the loaded authentication. - -== Removing an Expired Authentication - -If the authentication is expired, or when the user logs out, remove the stored authentication from `localStorage`, as follows: - -.`auth.ts` -[source,typescript] ----- -include::{root}/frontend/demo/fusion/authentication/auth.ts[tags=logout] ----- - -== Offline Support Example - -The following code example contains the complete client-side authentication implementation with offline support: - -.`auth.ts` -[source,typescript] ----- -include::{root}/frontend/demo/fusion/authentication/auth.ts[tags=impl;basic;login;logout;isLoggedIn;offline;!userinfo] ----- - - - - -== Security Considerations - -=== Authenticity - -Do you need to protect the local user's data from any other person who may have access to the same device? -If so, you need to add a `passcode` in addition to the regular online log-in (or use fingerprint/face ID authentication through the WebAuthn API on devices that support these). -This is what many mobile bank apps do to encrypt any user data that's cached locally for offline use. -If you do go down that route, most likely you end up having a helper library that you can call to check if the user is logged in. - -Arguably, not many applications would need to securely verify user authenticity offline. Most can check if an online log-in happened in the not-so-distant past. - - - For this, the simplest approach would be setting a `localStorage` key after a user has logged in online, and clear it when the user logs out. -During offline navigation, the user authentication would then be checked from `localStorage`. -// The <<../security/authentication-offline#, Offline Support for Authentication>> article describes how to do this. -It's worth classifying this as a UX improvement, not as a security feature, because it's easy to bypass through the browser developer tools. - -- And if you need to add a more reliable expiration mechanism, you could use signed tokens (for example, JWT), so that the client can strongly verify the timestamp of the last log-in. -Nevertheless, this is still not bulletproof, because the user can change the date on the device. - -=== Authorization - -Authorization (checking roles) is really only feasible on the backend. -The reason is simple: the backend is the only place that can reliably guard the data the user isn't authorized to view/edit. -Once some data has reached the client, you have to assume it may be tampered with. -When it comes to, for example, checking whether a user has an appropriate role to access a certain view, the client-side implementation is mostly about the UX. -You would need a way to check whether the user has the permissions to navigate to a view, but it doesn't need to be a secure way. -If the user tampers with the permission on their device, they may end up navigating to that view anyway, but they cannot see any sensitive data, because this would be securely guarded by the backend. - -What you may end up doing if you want to keep more than a plain `isLoggedIn` flag is keeping a list of user roles in plain text in the local storage, and checking against that list in offline navigation. -If the user changed their roles through the developer tools, they could see views they aren't allowed to see (if the view templates are stored in the browser cache), but they couldn't see any data there. - -Using secure tokens, such as JWT, makes the list of roles secure. -It's cryptographically signed and the client application can verify that it hasn't been tampered with. -Although such tokens are useful when accessing the backend, because they allow the backend to be stateless, there are no particular benefits for client-side offline authentication. diff --git a/articles/hilla/guides/security/session-expiration.adoc b/articles/hilla/guides/security/session-expiration.adoc deleted file mode 100644 index 46e109aded..0000000000 --- a/articles/hilla/guides/security/session-expiration.adoc +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: Session Expiration -page-title: Handle session expiration in Hilla applications | Vaadin -description: How to detect session expiration, for example to show a login view to the user. -meta-description: Learn how to manage and handle user session expiration securely in Hilla applications. -order: 141 ---- -:hilla-react: -// Workaround for https://github.com/vaadin/docs-app/issues/411 -:root-fix: /../ -include::{root}/articles/hilla/_shared/guides/security/_session-expiration.adoc[tag=content] diff --git a/articles/hilla/guides/security/spring-login.adoc b/articles/hilla/guides/security/spring-login.adoc deleted file mode 100644 index 68ec3b2932..0000000000 --- a/articles/hilla/guides/security/spring-login.adoc +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: Authentication with Spring Security -page-title: How to set up spring login in Hilla apps | Vaadin -description: How to configure authentication with Spring Security. -meta-description: Explore how to configure and implement secure login in Hilla applications using Spring Security. -order: 90 ---- -:hilla-react: -// Workaround for https://github.com/vaadin/docs-app/issues/411 -:root-fix: /../ -include::{root}/articles/hilla/_shared/guides/security/_spring-login.adoc[tag=content] diff --git a/articles/hilla/guides/security/spring-stateless.adoc b/articles/hilla/guides/security/spring-stateless.adoc deleted file mode 100644 index f65dc0acf6..0000000000 --- a/articles/hilla/guides/security/spring-stateless.adoc +++ /dev/null @@ -1,132 +0,0 @@ ---- -title: Stateless Authentication -page-title: How to implement Spring stateless security in Hilla | Vaadin -description: Using stateless authentication to persist authentication on the client side between requests. -meta-description: How to use stateless authentication to persist authentication on the client side between requests in your Hilla application. -order: 100 ---- -= Stateless Authentication with Spring Security - -After a user logs in, the web application has to persist the authentication data over server requests, so that the user isn't asked to log in again for every single action. Stateless authentication presents a way to persist the authentication data on the client side between requests. - -Unlike server-side authentication storage solutions -- which commonly rely on sessions -- stateless authentication doesn't require you to track sessions on the server. - -Using stateless authentication brings benefits in the following use cases: - -**Horizontal Scaling of the Backend**:: -This helps to avoid the complexity of managing shared or sticky sessions among multiple backend servers. - -**Seamless Deployment**:: -Backend servers can be restarted without logging out users, and without the need for session persistence. - -**Offline Logout for Client-Side Applications**:: -Users can log off and have their authentication data destroyed on the client without requesting a logout from the server. - -Hilla provides stateless authentication support in applications using Spring Security. It uses a signed JSON Web Token (JWT) stored in a cookie pair -- the token content in the JS-accessible cookie, and the signature in the HTTP-only cookie. - - -== Enabling Stateless Authentication - -The following examples illustrate the steps to enable stateless authentication in a Hilla application that uses Spring Security. They involve adding dependencies, configuring Spring Security, the JWT authentication principal, and verification. - - -=== Dependencies - -Add the following dependencies to the project's [filename]`pom.xml` file: - -.`pom.xml` -[source,xml] ----- - - org.springframework.security - spring-security-oauth2-jose - - - org.springframework.security - spring-security-oauth2-resource-server - ----- - - -=== Configure Spring Security - -Modify the Spring Security configuration and use the [methodname]`VaadinStatelessSecurityConfigurer` to set up stateless authentication, as follows: - -.`SecurityConfigurer.java` -[source,java] ----- - -include::{root}/src/main/java/com/vaadin/demo/fusion/security/stateless/SecurityConfigurer.java[tags="stateless-configure"] ----- - -<1> Sets the secret key that's used for signing and verifying the JWT. -<2> Sets the JWT signature algorithm. The key length should match the algorithm chosen. For example, the HS256 algorithm used here requires a 32-byte secret key. -<3> Sets the issuer JWT claim – a string or a URL that identifies your application. - -[CAUTION] -.Secret-Key Considerations -==== -The secret key must be unique to your application. Use different keys in the development, staging, and production environments. Don't commit the secret key into the repository. -==== - -The security configuration given here gets the secret key from the Base64-encoded, `my.app.auth.secret` string property. You should configure the property value in your environment, accordingly. - -To avoid hard-coding the value and committing it into the repository, you can create a separate [filename]`application.properties` file in the `config/local/` subdirectory and instruct Git to ignore the directory. Here's how you might do that: - -[source,terminal] ----- -mkdir -p config/local/ - -echo " -# Contains secrets that shouldn't go into the repository -config/local/" >> .gitignore - -echo "my.app.auth.secret=$(openssl rand -base64 32)" > config/local/application.properties ----- - -Spring Boot supports many ways of configuring properties. See the link:https://docs.spring.io/spring-boot/docs/current/reference/html/features.html#features.external-config[Externalized Configuration] feature section in the Spring Boot Reference manual. - - -=== Handle JWT Authentication Principal - -When using stateless authentication, the [methodname]`SecurityContext.getAuthentication().getPrincipal()` call returns a `Jwt` instance that contains only the username and roles. In your application, you need to verify that the reference returned by [methodname]`Authentication.getPrincipal()` is a `Jwt` instance. - -In applications that use username-password authentication, you may need to access the full [classname]`UserDetails` instance for the current user. You can use the [classname]`UserDetailsService` to load the user details via the username from the JWT: - -.`SecurityUtils.java` -[source,java] ----- -include::{root}/src/main/java/com/vaadin/demo/fusion/security/stateless/SecurityUtils.java[tags="user-details"] ----- - - -==== Verification - -After completing the previous steps, your application should be using stateless authentication. To verify this, start the development server. From there, open your application and log in. Then restart the development server. You should remain logged in after the restart finishes. - - -== JWT Expiration - -By default, the JWT and cookies expire thirty minutes after the last server request. You can customize the expiration period by using an additional duration argument for the configuration method like so: - -.`SecurityConfig.java` -[source,java] ----- - -@EnableWebSecurity -@Configuration -public class SecurityConfig { - @Bean - SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { - ... - http.with(new VaadinStatelessSecurityConfigurer<>(), - cfg -> cfg.withSecretKey().secretKey( - new SecretKeySpec(Base64.getDecoder().decode("..."), - JwsAlgorithms.HS256) - ).and().issuer("com.example.application") - .expiresIn(3600) // The JWT lifetime in seconds - ); - return http.build(); - } -} ----- diff --git a/articles/hilla/guides/security/vulnerabilities.adoc b/articles/hilla/guides/security/vulnerabilities.adoc deleted file mode 100644 index 675a681c69..0000000000 --- a/articles/hilla/guides/security/vulnerabilities.adoc +++ /dev/null @@ -1,91 +0,0 @@ ---- -title: Common Vulnerabilities -page-title: How to address security vulnerabilities in Hilla | Vaadin -description: Dealing with vulnerabilities, such as SQL injection, cross-site request forgery, and Java serialization. -meta-description: Explore best practices to address and mitigate security vulnerabilities in Hilla applications. -order: 150 ---- - -= Common Vulnerabilities - -== SQL Injections - -Since Hilla is a backend-agnostic UI framework, it doesn't deal directly with backend access. -Instead, the choice of backend framework (for example, Spring Data) is left to the developer. -Hilla doesn't provide mitigation for SQL injections. -This is left to the backend provider and developer. - -However, SQL injections can be completely blocked in Hilla applications by validating and escaping data as described in <>, as well as by following standard secure database access practices. - -Most providers have their own ways of dealing with injections out of the box, and following those guides is recommended. -However, if the developer uses pure JDBC, they have to deal with injection risks themselves. -Here is an example of pure JDBC, demonstrating SQL-injection mitigation using the value from a Text Field in a prepared statement: - -[.example] --- -.`name-view.tsx` -[source,tsx] ----- -import { TextField, type TextFieldValueChangedEvent } from '@vaadin/react-components'; -import { NameService } from 'Frontend/generated/endpoints'; - -export default function NameView() { - return ( - - NameService.updateName(event.detail.value) - } - /> - ); -} ----- -.`NameService.java` -[source,java] ----- -@BrowserCallable -public class NameService { - public void updateName(String value) { - // This is the correct way - String sql = "UPDATE app_users WHERE id=? SET name=?"; - - try { - // Use prepared statement to safely call the DB - PreparedStatement ps = dbConnection.prepareStatement(sql); - - ps.setLong(1, user.getId()); - ps.setString(2, value); - ps.executeUpdate(); - } catch (SQLException e) { - throw new RuntimeException(e); - } - - // This is the INCORRECT way, DON'T USE! - // sql = "UPDATE app_users WHERE id="+ user.getId() +" SET name=\"" + value + - // "\""; - } -} ----- --- - -== Cross-Site Request Forgery (CSRF / XSRF) - -All requests between the client and the server have a user-session-specific CSRF token included. -All communication between the server and the client is handled by Vaadin, so you don't need to remember to include and verify the CSRF tokens manually. -See <> for more information. - -The CSRF token mechanism can be overridden on the server, for example to enable repeatable load test scripts using Gatling or similar tools. -This is strongly discouraged when running in production. - -== Java Serialization Vulnerability - -A general security issue has been identified in programming language mechanics where the language allows execution of code that comes from serialized objects. -The Java language isn't immune to this; as a non-exhaustive list, at least the Java Serialization framework, RMI, JMX and JMS features are vulnerable to the issue. - -If the application is set up to deserialize Java objects (for example, using the libraries above), an attacker can feed the system a malicious payload that gets deserialized into Java objects. -The attacker can then execute arbitrary code using specific language features (such as reflection). - -Vaadin has published a link:https://vaadin.com/security/2018-11-13[security alert] for this vulnerability. - -The vulnerability cannot be fixed in Vaadin. -Developers need to mitigate the risk using methods described in the alert appendices. diff --git a/dspublisher/config/default.json b/dspublisher/config/default.json index 52c607e3cc..44a1c22f67 100644 --- a/dspublisher/config/default.json +++ b/dspublisher/config/default.json @@ -69,6 +69,16 @@ "/hilla/guides/reactive-endpoints": "/flow/react-views/browser-callable-services/reactive-services", "/hilla/guides/reactive-services": "/flow/react-views/browser-callable-services/reactive-services", "/hilla/guides/routing": "/flow/react-views/routing", + "/hilla/guides/security": "/flow/security/react-views", + "/hilla/guides/security/authentication": "/flow/security/react-views/authentication", + "/hilla/guides/security/best-practices": "/flow/security/protections", + "/hilla/guides/security/configuring": "/flow/security/react-views/service-access-control", + "/hilla/guides/security/intro": "/flow/security/react-views", + "/hilla/guides/security/offline-auth": "/flow/security/react-views", + "/hilla/guides/security/session-expiration": "/flow/security/react-views/authentication#expired-sessions", + "/hilla/guides/security/spring-login": "/building-apps/react/security", + "/hilla/guides/security/spring-stateless": "/flow/security/react-views/stateless-authentication", + "/hilla/guides/security/vulnerabilities": "/flow/security/protections/vulnerabilities", "/hilla/guides/upgrading": "/upgrading", "/hilla/lit": "/upgrading#vaadin-router-removal", "/hilla/lit/components": "/components", @@ -108,17 +118,17 @@ "/hilla/lit/guides/reactive-endpoints": "/flow/react-views/browser-callable-services/reactive-services", "/hilla/lit/guides/reactive-services": "/flow/react-views/browser-callable-services/reactive-services", "/hilla/lit/guides/routing": "/flow/react-views/routing", - "/hilla/lit/guides/security": "/hilla/guides/security", - "/hilla/lit/guides/security/authentication": "/hilla/guides/security/authentication", - "/hilla/lit/guides/security/best-practices": "/hilla/guides/security/best-practices", - "/hilla/lit/guides/security/configuring": "/hilla/guides/security/configuring", - "/hilla/lit/guides/security/intro": "/hilla/guides/security/intro", - "/hilla/lit/guides/security/offline-auth": "/hilla/guides/security/offline-auth", + "/hilla/lit/guides/security": "/flow/security/react-views", + "/hilla/lit/guides/security/authentication": "/flow/security/react-views/authentication", + "/hilla/lit/guides/security/best-practices": "/flow/security/protections", + "/hilla/lit/guides/security/configuring": "/flow/security/react-views/service-access-control", + "/hilla/lit/guides/security/intro": "/flow/security/react-views", + "/hilla/lit/guides/security/offline-auth": "/flow/security/react-views", "/hilla/lit/guides/security/role-based-access": "/building-apps/react/security", - "/hilla/lit/guides/security/session-expiration": "/hilla/guides/security/session-expiration", + "/hilla/lit/guides/security/session-expiration": "/flow/security/react-views/authentication#expired-sessions", "/hilla/lit/guides/security/spring-login": "/building-apps/react/security", - "/hilla/lit/guides/security/spring-stateless": "/hilla/guides/security/spring-stateless", - "/hilla/lit/guides/security/vulnerabilities": "/hilla/guides/security/vulnerabilities", + "/hilla/lit/guides/security/spring-stateless": "/flow/security/react-views/stateless-authentication", + "/hilla/lit/guides/security/vulnerabilities": "/flow/security/protections/vulnerabilities", "/hilla/lit/guides/state-management": "/upgrading#vaadin-router-removal", "/hilla/lit/guides/styling": "/styling/stylesheets", "/hilla/lit/guides/upgrading": "/upgrading", diff --git a/frontend/demo/auth/AboutView.tsx b/frontend/demo/auth/AboutView.tsx deleted file mode 100644 index 088c927925..0000000000 --- a/frontend/demo/auth/AboutView.tsx +++ /dev/null @@ -1,9 +0,0 @@ -export default function AboutView() { - return ( -
- -

This place intentionally left empty

-

It’s a place where you can grow your own UI 🤗

-
- ); -} diff --git a/frontend/demo/auth/App.tsx b/frontend/demo/auth/App.tsx deleted file mode 100644 index 8e87a6424b..0000000000 --- a/frontend/demo/auth/App.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { RouterProvider } from 'react-router'; -import { AuthProvider } from './auth'; -import router from './routes'; - -// tag::snippet[] -export default function App() { - return ( - - - - ); -} -// end::snippet[] diff --git a/frontend/demo/auth/LoginView.tsx b/frontend/demo/auth/LoginView.tsx index d2ff13424a..af162503cd 100644 --- a/frontend/demo/auth/LoginView.tsx +++ b/frontend/demo/auth/LoginView.tsx @@ -1,33 +1,26 @@ -import React from 'react'; -import { useSignals } from '@preact/signals-react/runtime'; // hidden-source-line +import { useState } from 'react'; import type { ViewConfig } from '@vaadin/hilla-file-router/types.js'; -import { useSignal } from '@vaadin/hilla-react-signals'; -import { LoginOverlay } from '@vaadin/react-components/LoginOverlay.js'; +import { LoginForm } from '@vaadin/react-components/LoginForm.js'; import { useAuth } from './auth'; export const config: ViewConfig = { + skipLayouts: true, menu: { exclude: true }, }; -// tag::snippet[] export default function LoginView() { - useSignals(); // hidden-source-line const { login } = useAuth(); - const hasError = useSignal(false); + const [hasError, setHasError] = useState(false); return ( - { - hasError.value = event.detail.value; - }} + onErrorChanged={(event) => setHasError(event.detail.value)} onLogin={async ({ detail: { username, password } }) => { const { error } = await login(username, password); - hasError.value = Boolean(error); + setHasError(error); }} /> ); } -// end::snippet[] diff --git a/frontend/demo/auth/MainLayout.tsx b/frontend/demo/auth/MainLayout.tsx deleted file mode 100644 index 583eb34609..0000000000 --- a/frontend/demo/auth/MainLayout.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import { Suspense } from 'react'; -import { NavLink, Outlet } from 'react-router'; -import { AppLayout } from '@vaadin/react-components/AppLayout.js'; -import { Button } from '@vaadin/react-components/Button.js'; -import { DrawerToggle } from '@vaadin/react-components/DrawerToggle.js'; -import { useAuth } from './auth'; -import { useRouteMetadata } from './routing'; - -const navLinkClasses = ({ isActive }: any) => - `block rounded-m p-s ${isActive ? 'bg-primary-10 text-primary' : 'text-body'}`; - -export default function MainLayout() { - const currentTitle = useRouteMetadata()?.title ?? 'My App'; - const { state, logout } = useAuth(); - - return ( - -
-
-

My App

- -
-
- {state.user ? ( - <> -
{state.user.name}
- - - ) : ( - Sign in - )} -
-
- - -

- {currentTitle} -

- - - - -
- ); -} diff --git a/frontend/demo/auth/auth.ts b/frontend/demo/auth/auth.ts index 190e61813c..ff61346884 100644 --- a/frontend/demo/auth/auth.ts +++ b/frontend/demo/auth/auth.ts @@ -1,10 +1,7 @@ import { configureAuth } from '@vaadin/hilla-react-auth'; import { UserInfoService } from 'Frontend/generated/endpoints'; -// Configure auth to use `UserInfoService.getUserInfo` const auth = configureAuth(UserInfoService.getUserInfo); -// Export auth provider and useAuth hook, which are automatically -// typed to the result of `UserInfoService.getUserInfo` export const useAuth = auth.useAuth; export const AuthProvider = auth.AuthProvider; diff --git a/frontend/demo/auth/authRoles.ts b/frontend/demo/auth/authRoles.ts deleted file mode 100644 index f3c277e6d3..0000000000 --- a/frontend/demo/auth/authRoles.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { configureAuth } from '@vaadin/hilla-react-auth'; -import { UserInfoService } from 'Frontend/generated/endpoints'; - -// tag::snippet[] -// Configure auth to use `UserInfoService.getUserInfo` and map to custom roles -const auth = configureAuth(UserInfoService.getUserInfo, { - getRoles: (userInfo) => userInfo.authorities.map((v) => v ?? ''), -}); -// end::snippet[] - -// Export auth provider and useAuth hook, which are automatically -// typed to the result of `UserInfoService.getUserInfo` -export const useAuth = auth.useAuth; -export const AuthProvider = auth.AuthProvider; diff --git a/frontend/demo/auth/index.tsx b/frontend/demo/auth/index.tsx deleted file mode 100644 index a425ef6d4e..0000000000 --- a/frontend/demo/auth/index.tsx +++ /dev/null @@ -1,17 +0,0 @@ -// tag::snippet[] -import { createElement } from 'react'; -import { createRoot } from 'react-dom/client'; -import { RouterProvider } from 'react-router'; -import { AuthProvider } from './auth'; -import router from './routes'; - -function App() { - return ( - - - - ); -} - -createRoot(document.getElementById('outlet')!).render(createElement(App)); -// end::snippet[] diff --git a/frontend/demo/auth/routes.tsx b/frontend/demo/auth/routes.tsx deleted file mode 100644 index 4405ce1f7c..0000000000 --- a/frontend/demo/auth/routes.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { createBrowserRouter, type RouteObject } from 'react-router'; -import { protectRoutes } from '@vaadin/hilla-react-auth'; -import AboutView from './AboutView'; -import LoginView from './LoginView'; -import MainLayout from './MainLayout'; - -// tag::snippet[] -export const routes: RouteObject[] = protectRoutes([ - { - element: , - handle: { title: 'Main' }, - children: [ - { path: '/', element: , handle: { title: 'About', requiresLogin: true } }, - ], - }, - { path: '/login', element: }, -]); - -export default createBrowserRouter(routes); -// end::snippet[] diff --git a/frontend/demo/auth/routing.ts b/frontend/demo/auth/routing.ts deleted file mode 100644 index 1301791577..0000000000 --- a/frontend/demo/auth/routing.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { useMatches } from 'react-router'; - -type RouteMetadata = Record; - -/** - * Returns the `handle` object containing the metadata for the current route, - * or undefined if the route does not have defined a handle. - */ -export function useRouteMetadata(): RouteMetadata | undefined { - const matches = useMatches(); - const match = matches[matches.length - 1]; - return match?.handle as RouteMetadata | undefined; -} diff --git a/frontend/demo/auth/session-expiration/SessionExpiredLoginOverlay.tsx b/frontend/demo/auth/session-expiration/SessionExpiredLoginOverlay.tsx index 59f24d5308..608c9ff820 100644 --- a/frontend/demo/auth/session-expiration/SessionExpiredLoginOverlay.tsx +++ b/frontend/demo/auth/session-expiration/SessionExpiredLoginOverlay.tsx @@ -1,4 +1,3 @@ -import React from 'react'; import { useSignals } from '@preact/signals-react/runtime'; // hidden-source-line import type { LoginResult } from '@vaadin/hilla-frontend'; import { signal, useSignal } from '@vaadin/hilla-react-signals'; @@ -7,16 +6,11 @@ import { useAuth } from '../auth'; // tag::snippet[] const sessionExpired = signal(false); -const waiting: Array<(result: LoginResult) => void> = []; -// Called by the InvalidSessionMiddleware once per request that ran into the -// expired session. Opens the overlay, and resolves every waiting caller once -// the user has signed in again, so that all of those calls can be retried. export async function promptLogin(): Promise { sessionExpired.value = true; - return new Promise((resolve) => { - waiting.push(resolve); - }); + // Never resolves: the page reloads after the login + return new Promise(() => undefined); } export default function SessionExpiredLoginOverlay() { @@ -33,12 +27,11 @@ export default function SessionExpiredLoginOverlay() { hasError.value = event.detail.value; }} onLogin={async ({ detail: { username, password } }) => { - const result = await login(username, password); - hasError.value = result.error; - if (!result.error) { - sessionExpired.value = false; - waiting.splice(0).forEach((resolve) => resolve(result)); - } + const { error } = await login(username, password, { + // Reloads the current page instead of going to the default URL + navigate: () => window.location.reload(), + }); + hasError.value = error; }} /> ); diff --git a/frontend/demo/auth/session-expiration/connect-client.ts b/frontend/demo/auth/session-expiration/connect-client.ts index 98c51e2a05..832a43581b 100644 --- a/frontend/demo/auth/session-expiration/connect-client.ts +++ b/frontend/demo/auth/session-expiration/connect-client.ts @@ -5,6 +5,8 @@ const client = new ConnectClient({ prefix: 'connect', middlewares: [ new InvalidSessionMiddleware(async () => { + // Imported here, because the overlay imports the services, + // which import this client const { promptLogin } = await import('./SessionExpiredLoginOverlay'); return promptLogin(); }), diff --git a/frontend/demo/fusion/authentication/auth.ts b/frontend/demo/fusion/authentication/auth.ts deleted file mode 100644 index 3f43a50131..0000000000 --- a/frontend/demo/fusion/authentication/auth.ts +++ /dev/null @@ -1,142 +0,0 @@ -// tag::impl[] -// Uses the Vaadin provided login an logout helper methods -import { - login as loginImpl, - type LoginOptions, - type LoginResult, - logout as logoutImpl, - type LogoutOptions, -} from '@vaadin/hilla-frontend'; -// end::impl[] -// tag::userinfo[] -import type UserInfo from 'Frontend/generated/com/vaadin/demo/fusion/security/authentication/UserInfo'; -import { UserInfoService } from 'Frontend/generated/endpoints'; -// end::userinfo[] -// tag::basic[] - -interface Authentication { - // tag::userinfo[] - user: UserInfo; - // end::userinfo[] - // tag::offline[] - timestamp: number; - // end::offline[] -} - -let authentication: Authentication | undefined; - -// end::basic[] -// tag::storage-defs[] -const AUTHENTICATION_KEY = 'authentication'; -const THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1000; -// end::storage-defs[] -/** - * Forces the session to expire and removes user information stored in - * `localStorage`. - */ -// tag::logout[] -export function setSessionExpired() { - authentication = undefined; - // tag::offline[] - - // Delete the authentication from the local storage - localStorage.removeItem(AUTHENTICATION_KEY); - // end::offline[] -} - -// end::logout[] -// tag::offline[] - -// tag::restore[] -// Get authentication from local storage -const storedAuthenticationJson = localStorage.getItem(AUTHENTICATION_KEY); -if (storedAuthenticationJson !== null) { - const storedAuthentication = JSON.parse(storedAuthenticationJson) as Authentication; - // Check that the stored timestamp is not older than 30 days - const hasRecentAuthenticationTimestamp = - new Date().getTime() - storedAuthentication.timestamp < THIRTY_DAYS_MS; - if (hasRecentAuthenticationTimestamp) { - // Use loaded authentication - authentication = storedAuthentication; - } else { - // Delete expired stored authentication - setSessionExpired(); - } -} - -// end::restore[] -// end::offline[] -/** - * Login wrapper method that retrieves user information. - * - * Uses `localStorage` for offline support. - */ -// tag::login[] -export async function login( - username: string, - password: string, - options: LoginOptions = {} -): Promise { - return await loginImpl(username, password, { - ...options, - async onSuccess() { - // tag::userinfo[] - // Get user info from endpoint - const user = await UserInfoService.getUserInfo(); - // end::userinfo[] - authentication = { - // tag::userinfo[] - user, - // end::userinfo[] - // tag::offline[] - timestamp: new Date().getTime(), - // end::offline[] - }; - // tag::offline[] - - // Save the authentication to local storage - localStorage.setItem(AUTHENTICATION_KEY, JSON.stringify(authentication)); - // end::offline[] - }, - }); -} - -// end::login[] -/** - * Login wrapper method that retrieves user information. - * - * Uses `localStorage` for offline support. - */ -// tag::logout[] -export async function logout(options: LogoutOptions = {}) { - return await logoutImpl({ - ...options, - onSuccess() { - setSessionExpired(); - }, - }); -} - -// end::logout[] -/** - * Checks if the user is logged in. - */ -// tag::isLoggedIn[] -export function isLoggedIn() { - return !!authentication; -} - -// end::isLoggedIn[] -/** - * Checks if the user has the role. - */ -// tag::isUserInRole[] -export function isUserInRole(role: string) { - if (!authentication) { - return false; - } - - return authentication.user.authorities.includes(`ROLE_${role}`); -} - -// end::isUserInRole[] diff --git a/src/main/java/com/vaadin/demo/fusion/security/access/ProjectService.java b/src/main/java/com/vaadin/demo/fusion/security/access/ProjectService.java new file mode 100644 index 0000000000..9c65519aee --- /dev/null +++ b/src/main/java/com/vaadin/demo/fusion/security/access/ProjectService.java @@ -0,0 +1,40 @@ +package com.vaadin.demo.fusion.security.access; + +import java.util.List; + +import jakarta.annotation.security.DenyAll; +import jakarta.annotation.security.PermitAll; +import jakarta.annotation.security.RolesAllowed; + +import com.vaadin.flow.server.auth.AnonymousAllowed; +import com.vaadin.hilla.BrowserCallable; + +// tag::snippet[] +@BrowserCallable +@PermitAll +public class ProjectService { + + // Any logged-in user, from the annotation of the class + public List listProjects() { + return List.of("Apollo", "Gemini"); + } + + // Everyone, also users who aren't logged in + @AnonymousAllowed + public int countProjects() { + return 2; + } + + // Users with the role ADMIN + @RolesAllowed("ADMIN") + public void deleteProject(String name) { + // ... + } + + // Nobody + @DenyAll + public void resetAll() { + // ... + } +} +// end::snippet[] diff --git a/src/main/java/com/vaadin/demo/fusion/security/access/package-info.java b/src/main/java/com/vaadin/demo/fusion/security/access/package-info.java new file mode 100644 index 0000000000..427e2c36a0 --- /dev/null +++ b/src/main/java/com/vaadin/demo/fusion/security/access/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package com.vaadin.demo.fusion.security.access; + +import org.jspecify.annotations.NullMarked; diff --git a/src/main/java/com/vaadin/demo/fusion/security/authentication/SecurityConfigDemo.java b/src/main/java/com/vaadin/demo/fusion/security/authentication/SecurityConfigDemo.java deleted file mode 100644 index e85988d95b..0000000000 --- a/src/main/java/com/vaadin/demo/fusion/security/authentication/SecurityConfigDemo.java +++ /dev/null @@ -1,43 +0,0 @@ -package com.vaadin.demo.fusion.security.authentication; - -import com.vaadin.flow.spring.security.VaadinSecurityConfigurer; - -import org.springframework.context.annotation.Bean; -import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.core.userdetails.User; -import org.springframework.security.provisioning.InMemoryUserDetailsManager; -import org.springframework.security.provisioning.UserDetailsManager; -import org.springframework.security.web.SecurityFilterChain; - -/** - * An example code for demoing the Spring Security configuration, shouldn't - * affect the doc application itself. - */ -// @EnableWebSecurity -// @Configuration -public class SecurityConfigDemo { - - @Bean - SecurityFilterChain securityFilterChain(HttpSecurity http) - throws Exception { - // tag::public-resources[] - http.authorizeHttpRequests( - auth -> auth.requestMatchers("/images/**").permitAll()); - // end::public-resources[] - // tag::login[] - http.with(VaadinSecurityConfigurer.vaadin(), - configurer -> configurer.loginView("/login")); - // end::login[] - return http.build(); - } - - @Bean - public UserDetailsManager userDetailsService() { - // Configure users and roles in memory - return new InMemoryUserDetailsManager( - // the {noop} prefix tells Spring that the password is not - // encoded - User.withUsername("user").password("{noop}password") - .roles("USER").build()); - } -} diff --git a/src/main/java/com/vaadin/demo/fusion/security/authentication/UserInfo.java b/src/main/java/com/vaadin/demo/fusion/security/authentication/UserInfo.java index 559320d1c5..544b37926f 100644 --- a/src/main/java/com/vaadin/demo/fusion/security/authentication/UserInfo.java +++ b/src/main/java/com/vaadin/demo/fusion/security/authentication/UserInfo.java @@ -1,34 +1,8 @@ package com.vaadin.demo.fusion.security.authentication; -import jakarta.annotation.Nonnull; +import java.util.List; -import java.util.Collection; -import java.util.Collections; - -/** - * User information used in client-side authentication and authorization. To be - * saved in browsers’ LocalStorage for offline support. - */ // tag::snippet[] -public class UserInfo { - - @Nonnull - private String name; - @Nonnull - private Collection authorities; - - public UserInfo(String name, Collection authorities) { - this.name = name; - this.authorities = Collections.unmodifiableCollection(authorities); - } - - public String getName() { - return name; - } - - public Collection getAuthorities() { - return authorities; - } - +public record UserInfo(String name, List roles) { } // end::snippet[] diff --git a/src/main/java/com/vaadin/demo/fusion/security/authentication/UserInfoService.java b/src/main/java/com/vaadin/demo/fusion/security/authentication/UserInfoService.java index e3358a90dc..491c42e341 100644 --- a/src/main/java/com/vaadin/demo/fusion/security/authentication/UserInfoService.java +++ b/src/main/java/com/vaadin/demo/fusion/security/authentication/UserInfoService.java @@ -1,32 +1,29 @@ package com.vaadin.demo.fusion.security.authentication; -import com.vaadin.hilla.BrowserCallable; -import jakarta.annotation.Nonnull; -import jakarta.annotation.security.PermitAll; -import org.springframework.security.core.Authentication; +import org.jspecify.annotations.Nullable; import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.core.context.SecurityContextHolder; -import java.util.List; +import com.vaadin.flow.server.auth.AnonymousAllowed; +import com.vaadin.flow.spring.security.AuthenticationContext; +import com.vaadin.hilla.BrowserCallable; -/** - * Provides information about the current user. - */ // tag::snippet[] @BrowserCallable public class UserInfoService { - @PermitAll - @Nonnull - public UserInfo getUserInfo() { - Authentication auth = SecurityContextHolder.getContext() - .getAuthentication(); + private final AuthenticationContext authenticationContext; - final List authorities = auth.getAuthorities().stream() - .map(GrantedAuthority::getAuthority).toList(); - - return new UserInfo(auth.getName(), authorities); + UserInfoService(AuthenticationContext authenticationContext) { + this.authenticationContext = authenticationContext; } + @AnonymousAllowed + public @Nullable UserInfo getUserInfo() { + return authenticationContext.getPrincipalName() + .map(name -> new UserInfo(name, + authenticationContext.getGrantedAuthorities().stream() + .map(GrantedAuthority::getAuthority).toList())) + .orElse(null); // Nobody is logged in + } } // end::snippet[] diff --git a/src/main/java/com/vaadin/demo/fusion/security/authentication/package-info.java b/src/main/java/com/vaadin/demo/fusion/security/authentication/package-info.java new file mode 100644 index 0000000000..fbabef20d3 --- /dev/null +++ b/src/main/java/com/vaadin/demo/fusion/security/authentication/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package com.vaadin.demo.fusion.security.authentication; + +import org.jspecify.annotations.NullMarked; diff --git a/src/main/java/com/vaadin/demo/fusion/security/stateless/SecurityUtils.java b/src/main/java/com/vaadin/demo/fusion/security/stateless/SecurityUtils.java index 06a037b078..51af97f08e 100644 --- a/src/main/java/com/vaadin/demo/fusion/security/stateless/SecurityUtils.java +++ b/src/main/java/com/vaadin/demo/fusion/security/stateless/SecurityUtils.java @@ -1,6 +1,7 @@ package com.vaadin.demo.fusion.security.stateless; -import org.springframework.beans.factory.annotation.Autowired; +import java.util.Optional; + import org.springframework.context.annotation.Profile; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; @@ -9,23 +10,24 @@ import org.springframework.security.oauth2.jwt.Jwt; import org.springframework.stereotype.Component; -import java.util.Optional; - @Profile("this-is-just-a-demo-class") // hidden-source-line // tag::user-details[] @Component public class SecurityUtils { - @Autowired - private UserDetailsService userDetailsService; + private final UserDetailsService userDetailsService; + + SecurityUtils(UserDetailsService userDetailsService) { + this.userDetailsService = userDetailsService; + } public Optional getAuthenticatedUser() { Authentication authentication = SecurityContextHolder.getContext() .getAuthentication(); if (authentication != null && authentication.getPrincipal() instanceof Jwt jwt) { - String username = jwt.getSubject(); - return Optional.of(userDetailsService.loadUserByUsername(username)); + return Optional.of( + userDetailsService.loadUserByUsername(jwt.getSubject())); } // Anonymous or no authentication. return Optional.empty();