Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add doc for typescript auth helpers #94

Merged
merged 8 commits into from
Jan 20, 2021
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/styles/Vaadin/Abbr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ exceptions:
- JAR
- JPA
- JSON
- JWT
- LDAP
- NPM
- PATH
- PDF
Expand Down
1 change: 1 addition & 0 deletions .github/styles/Vocab/Docs/accept.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
app
async
[bB]oolean
[cC]acheable
classpath
configurator
Ctrl
Expand Down
39 changes: 39 additions & 0 deletions articles/fusion/pwa/offline-authentication-check.asciidoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
---
title: Offline Authentication Checks
order: 8
layout: page
---

= Security Aspects of Offline Authentication Checks

Take the following security aspects into account when you do authentication checks while the user is offline

== Authenticity
Do you need to protect the local user's data on the device from whoever else may have access to the same device? If that is a requirement, then you will end up adding a `passcode` in addition to the regular online login (can be replaced with a fingerprint/face id auth through the WebAuthn API on the devices that support those). This is what many mobile bank apps do.
It is to encrypt any user data that is cached locally for offline use.
If you do go that route, most likely you will end up having a helper library that you can call to check if the user is logged in or not.

Arguably, not many apps would need to securely verify the user authenticity offline, and most can check if an online login happened in the not-so-distant past.

- For that the simplest approach would be setting a local storage flag after a user has successfully logged in online (and clear it when the user logs out).
During offline navigation, the user auth then would be checked from the local storage.
The <<../security/custom-spring-login#, Adding a Custom Login Form with Spring Security>> article describes how to do that.
It is worth to classify that this as a UX improvement, not as a security feature, because it is 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 login.
Though, it is still not bulletproof because the user may change the date on the device.

== Authorization
When it comes to authorization (checking roles), in reality, it is only feasible on the backend.
The reason is simple: the backend is the only place that can reliably guard the data the user is not 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 a role to access a certain view, the client-side implementation is mostly about the UX.
You would need a way to check if the user has the permissions to navigate to a view, but this way does not need to be a secure one.
In case if the user tampers with the permission on their device, they may end up navigating to that view anyway, but they can not see any sensitive data because that would be securely guarded by the backend.

What you may end up doing in case when 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 changes their roles through the developer tools, they could see the views they are not allowed to see (if the view templates are stored in the browser cache), but they can not see any data there.

Using secure tokens, like JWT, makes the list of roles secure.
It is cryptographically signed and the client application may verify that it has not been tampered with.
While 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.
131 changes: 131 additions & 0 deletions articles/fusion/security/custom-spring-login.asciidoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
---
title: Adding a Custom Login Form with Spring Security
order: 91
layout: page
---

= Adding a Custom Login Form with Spring Security

Instead of using the <<fusion-security-spring-login#, default Spring login form>>, you may want to use your own customized form.
This article describes how to do that.

== Dependencies and Server Configuration

The dependencies and server configuration are the same as described in <<fusion-security-spring-login#, Adding a Login Form with Spring Security>>, with a difference that you need to specify the URL of the custom login view.

.`SecurityConfig.java`
[source, java]
----
@EnableWebSecurity
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

@Override
protected void configure(HttpSecurity http) throws Exception {
// Ignore the login processing url and vaadin endpoint calls
http.csrf().ignoringAntMatchers("/login", "/connect/**");

// specify the URL of the login view, the value of the parameter
// is the defined route for the login view component.
http.formLogin().loginPage("login");
}
}
----

== Custom Login View

The easiest way to make a login view is to use the `<vaadin-login-overlay>` component.
Vaadin provides a `login` helper method for Spring Security based authentication that you can use for the login action.

.`frontend/login-view.ts`
[source, typescript]
----
include::{root}/frontend/demo/fusion/authentication/login-view.ts[group=TypeScript]
----
After the login view is defined, you should also define a route for the login view component, for example in the `index.ts` file.
Note, the `path` for the login view component should match the one defined in `SecurityConfig` for `http.formLogin().loginPage()`.

.`frontend/index.ts`
[source, typescript]
----
const routes = [
{
path: '/login',
component: 'login-view'
},
// more routes
}
----
== Redirect the User to the Login View
To protect a view from unauthenticated users, that is, redirect unauthenticated users to the login view, you can use route action.

.`frontend/index.ts`
[source, typescript]
----
const routes = [
...
{
path: '/my-view',
action: (_: Router.Context, commands: Router.Commands) => {
if (!isLoggedIn()) {
return commands.redirect('/login');
}
return undefined;
},
component: 'my-view'
}
...
}
----
You can also add the route action to the parent layout, so that all the child views are protected.

.`frontend/index.ts`
[source, typescript]
----
const routes = [
...
{
path: '/',
action: (_: Router.Context, commands: Router.Commands) => {
if (!isLoggedIn()) {
return commands.redirect('/login');
}
return undefined;
},
component: 'main-layout',
children: [
...
]
}
...
}
----
The `isLoggedIn()` method in the above code examples uses a `lastLoginTimestamp` variable stored in the
https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage[localStorage] to check if the user is logged in. The `lastLoginTimestamp` variable needs to be reset when logging out.

The authentication helper methods in the code examples are grouped in a separate TypeScript file as shown below.

.`frontend/auth.ts`
[source, typescript]
----
include::{root}/frontend/demo/fusion/authentication/auth.ts[group=TypeScript]
----

Using localStorage allows navigating to sub views without having to check authentication from the backend on every navigation so that the authentication check could also work offline.


== Logout

To avoid a full page reload the application needs to have a `/logout` route like the one below.
It can be triggered with a link like `<a href="/logout">Log out</a>`.

.`frontend/index.ts`
[source, typescript]
----
path: '/logout',
action: async (_: Context, commands: Commands) => {
// use the logout helper method.
await logout();
return commands.redirect('/');
}
----
33 changes: 33 additions & 0 deletions articles/fusion/security/handle-session-expiration.asciidoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
---
title: Handling Session Expiration
order: 141
layout: page
---

= Handling Session Expiration

Use the built-in <<../advanced/fusion-advanced-client-middleware#, middleware>> `InvalidSessionMiddleWare` to detect session expiration, for example, to show a login view to the user.

== How to Use the InvalidSessionMiddleWare?

The `InvalidSessionMiddleWare` requires a `OnInvalidSessionCallback` as a constructor parameter.
The `OnInvalidSessionCallback` is a function that takes no parameters and should return a promise of `LoginResult`.
`LoginResult` contains the metadata of a login result, including:
- error: indicating if the login attempt has failed or not.
- token: in case of success login, it is the CSRF token, which can be extracted from the `index.html` page.
- errorTitle: a short text of the login error.
- errorMessage: a more detailed explanation of the login error.


.Example using `InvalidSessionMiddleWare`
[.example]
--
[source, typescript]
----
include::{root}/frontend/demo/fusion/authentication/handle-session-expiration/connect-client.ts[]
----
[source, typescript]
----
include::{root}/frontend/demo/fusion/authentication/handle-session-expiration/login-overlay.ts[]
----
--
12 changes: 7 additions & 5 deletions articles/fusion/security/spring-login.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -52,14 +52,16 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter {

@Override
protected void configure(HttpSecurity http) throws Exception {
// Ignore the login processing url and vaadin endpoint calls
http.csrf().ignoringAntMatchers("/login", "/connect/**");
// Use default spring login form
http.formLogin();
http.csrf().disable();
}
}
----

[NOTE]
If CSRF is still needed for other endpoints in the application, it is possibe to disable CSRF just for Vaadin endpoints: `http.csrf().ignoringAntMatchers("/connect/**")`
If CSRF is still needed for other endpoints in the application, it is possible to disable CSRF just for Vaadin endpoints: `http.csrf().ignoringAntMatchers("/connect/**")`


→ *Step 3* - Allow public access to Vaadin static resources.
Expand Down Expand Up @@ -114,10 +116,10 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter {

@Override
protected void configure(HttpSecurity http) throws Exception {
// Ignore the login processing url and vaadin endpoint calls
http.csrf().ignoringAntMatchers("/login", "/connect/**");
// Use default spring login form
http.formLogin();
// Vaadin already handles csrf.
http.csrf().disable();
}

@Override
Expand All @@ -137,7 +139,7 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter {
}
----

== Client configuration
== Client Configuration

Add links in the main layout for login and logout.

Expand Down
5 changes: 3 additions & 2 deletions articles/theming/using-themes.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ layout: page
ifdef::web[]
endif::web[]

The built-in themes are all customisable and provide a great starting point for an application. Completely custom themes can also be built from scratch.
The built-in themes are all customizable and provide a great starting point for an application.
Completely custom themes can also be built from scratch.

ifdef::web[]
endif::web[]
Expand Down Expand Up @@ -45,7 +46,7 @@ include::src/main/js/ImportStyleSheets.js[tags=client-side-theme, indent=0, grou

== Custom Themes

There are two ways to customize themes:
Themes can be customized in two ways:

1. Customize existing themes
2. Create a completely new theme
Expand Down
46 changes: 46 additions & 0 deletions frontend/demo/fusion/authentication/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// Uses the Vaadin provided login an logout helper methods
import { login as loginImpl, logout as logoutImpl } from '@vaadin/flow-frontend';
import type { LoginResult } from '@vaadin/flow-frontend';

// check if user is logged in or not by checking if there
// is an login event in the past 30 days
const LAST_LOGIN_TIMESTAMP = 'lastLoginTimestamp';
const THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1000;
const lastLoginTimestamp = localStorage.getItem(LAST_LOGIN_TIMESTAMP);
const hasRecentLoginTimestamp = (lastLoginTimestamp &&
(new Date().getTime() - new Date(+lastLoginTimestamp).getTime()) < THIRTY_DAYS_MS) || false;

let _isLoggedIn = hasRecentLoginTimestamp;

export async function login(username: string, password: string): Promise<LoginResult> {
if (_isLoggedIn) {
return { error: false } as LoginResult;
} else {
// Use the Vaadin provided login helper method to
// obtain the login result
const result = await loginImpl(username, password);
if (!result.error) {
_isLoggedIn = true;
// update the last login timestamp in the local storage
localStorage.setItem(LAST_LOGIN_TIMESTAMP, new Date().getTime() + '')
}
return result;
}
}

export async function logout() {
_isLoggedIn = false;
// clear the last login timestamp from the local storage
// when logging out.
localStorage.removeItem(LAST_LOGIN_TIMESTAMP);
return await logoutImpl();
}

export function isLoggedIn() {
return _isLoggedIn;
}

export function setSessionExpired() {
_isLoggedIn = false;
localStorage.removeItem(LAST_LOGIN_TIMESTAMP);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { ConnectClient, InvalidSessionMiddleware } from '@vaadin/flow-frontend';
import {setSessionExpired} from '../auth';
const client = new ConnectClient({
prefix: 'connect',
middlewares: [
new InvalidSessionMiddleware(async () => {
setSessionExpired();
const { LoginView } = await import('./login-overlay');
return LoginView.showOverlay();
})
]
});
export default client;
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { customElement, LitElement } from 'lit-element';
import { Router } from '@vaadin/router';
import { LoginResult } from '@vaadin/flow-frontend';

@customElement('login-view')
export class LoginView extends LitElement {

private returnUrl = '/';

// @ts-ignore
private onSuccess = (_: LoginResult) => { Router.go(this.returnUrl) };

private static overlayResult?: Promise<LoginResult>;

// Show the login view as an overlay, when the session has
// expired, and a user tries to invoke an endpoint call.
// Close the login overly once the login attempt has succeeded.
static async showOverlay(): Promise<LoginResult> {
if (this.overlayResult) {
return this.overlayResult;
}
const overlay = new this();
return this.overlayResult = new Promise(resolve => {
overlay.onSuccess = result => {
this.overlayResult = undefined;
overlay.remove();
resolve(result);
}
document.body.append(overlay);
});
}
}
Loading