React UI kit authentication

For single-sign-on (SSO) and seamless authentication between your app and Weavy, the React UI kit should act on behalf of your authenticated user. This is known as user-to-server communication and requires an access_token for API calls to the Weavy backend.

Token factory

When adding the <WeavyProvider> component, you need to specify a tokenFactory. The token factory should be an async function which accepts a single boolean argument and is expected to return an access_token for your authenticated user:

async function getAccessToken(refresh) {
  // typically implemented as an API call to *your* backend
  // which returns an access_token for the authenticated user
}

When the <WeavyProvider> is first initialized, your function will be called with refresh=false. The <WeavyProvider> will use the access_token you return as long as it is valid, but if the token expires or is revoked your function will be called again with refresh=true. This allows your code to clear the invalid token from your application’s storage and request a new token from the Weavy backend.

Implementing the token factory

You will typically implement the tokenFactory function with an API call to your backend as in the following example:

import React, { useCallback, useEffect, useState } from 'react';
import { WeavyClient, WeavyProvider } from '@weavy/uikit-react';

function App() {
   const [weavyClient, setWeavyClient] = useState(null);
   const [user, setUser] = useState(null);
   const [loading, setLoading] = useState(true);

   const getToken = useCallback(async (refresh) => {
       // fetch access_token for the authenticated user with an api call to your application backend
       let response = await fetch(`/token?&refresh=${refresh}`);
       let json = await response.json();
       return json.access_token

   }, []);

   useEffect(() => {
       if(user){
           let client = new WeavyClient({ url: '{WEAVY_BACKEND_URL}', tokenFactory: getToken });
           setWeavyClient(client);
       }        
   }, [getToken, user]);

   useEffect(() => {
       async function fetchUser() {
           // get signed in user from server if available
           let response = await fetch('/auth');
           let user = await response.text();
           setUser(user);
           setLoading(false);
       }
       fetchUser();
   }, []);

   const handleLogin = (user) => {
       setUser(user)
   }

   const handleLogout = async () => {
       // call /logout on server to clear session cookie
       await fetch('/logout');

       // destroy the Weavy client instance
       weavyClient.destroy();

       // Reset state variables. When client property on WeavyProvider is set to null, the internal query cache is also reset. Make sure to do this when you sign out a user.
       setWeavyClient(null);
       setUser(null);
   }

   return (
       <div className="App">
           {!user &&
               <!-- A login component which calls /login on the server with username and password. Check server implementation below -->
               <Login onAuthenticated={handleLogin} />
           }
           
           {user && 
               <div>
                   <button className='btn btn-primary' onClick={handleLogout}>Log out</button>
                   <WeavyProvider client={weavyClient}>
                       <!-- Weavy components goes here -->
                   </WeavyProvider>
               </div>
           }           
       </div>
   )
}

export default App;

Your backend then requests an access_token from the Weavy backend with a server-to-server request as explained in the API authentication article. In the example below we show what an implementation of the /token endpoint could look like if you have a Node backend. As seen in the example, we recommend you store and reuse tokens. This avoid unnecessary roundtrips and reduces your application's attack surface.

import express from 'express';
import fetch from 'node-fetch'
import config from 'config';
import cookieSession from 'cookie-session';

const app = express();
let _tokens = [];
const apiKey= config.get("apiKey");

app.use(express.json());
app.use(cookieSession({
    name: 'session',
    secret: 'secret_key'
}));

app.post('/login', (req, res) => {
    let username = req.body.username;
    let password = req.body.password;

    // validate user here...
    let validUser = true; // validate user...
    let message = "OK";
    if(validUser){
        req.session.user = `${username}`;        
    } else{
        message = "ERROR"
    }
    
    res.end(message)

});

// get authenticated user if available
app.get('/auth', (req, res) => {    
    res.end(req.session.user);
});

// sign out user
app.get('/logout', (req, res) => {    
    req.session = null;
    res.end("Logged out...");
});

// get token from Weavy server
app.get("/token", async (req, res) => {
     let username = req.session.user; // get user from session
    
    if((!req.query.refresh || req.query.refresh === "false") && _tokens.find((t) => t.username === username)){        
        res.json({ access_token: _tokens.find((t) => t.username === username).access_token});        
    } else{
        let response = await fetch(`${config.get("backend")}/api/users/${username}/tokens`, {
            method: 'POST',
            headers: {
                'content-type': 'application/json',
                'Authorization': `Bearer ${apiKey}`
            },
            body: JSON.stringify({ name: username })
        });
        
        if(response.ok){
            let data = await response.json();                
            _tokens = [..._tokens.filter((t) => t.username !== username), { username: username, access_token: data.access_token}];    
            res.json(data);            
        } else{
            res.json({ message: "Could not get access token from server!" })            
        }
    }    
});

app.listen(PORT, () => {
    console.log(`Server listening on ${PORT}`);
});

In the /token endpoint above, we first check the value of the refresh parameter. If it's false and the token is available in the internal token cache, the token is returned. If the token is not available, or the refresh parameter is true, we perform an api request to the Weavy backend's /api/users/{username}/tokens endpoint to get a new access token. Make sure to provide the server apiKey as the Bearer token. We can provide several additional properties for the user in the body, but here we only supply the username that we want to set for the user stored in Weavy. When successful, we store the user in the internal cache, which is just an array in the example, and then we return the data to the client application whcih is used by the WeavyClient.

Weavy Docs