Products

Authentication

Overview

Intrasheets Server uses an event-based architecture, allowing developers to implement custom authentication methods tailored to their application needs.

Authentication Events

On the client side, developers define an authentication object using the auth property available during all server events.

Among other events, the server provides the onbefore events specifically for handling authentication logic. These events allow developers to validate properties such as JWT tokens or apply other custom mechanisms. They return a true or false flag to the server, determining whether the user can proceed with their request.

Description
onbeforeconnect(auth: object) => boolean
Determines whether to allow or block a user attempting to establish a connection.
onbeforeload(auth: object) => boolean
Checks whether the connected user can load a specific spreadsheet based on its GUID.
onbeforechange(auth: object) => boolean
Provides developers with information about changes the user tries to make in the spreadsheet. The event should return true or false, but it can also be used to overwrite changes sent by the client.

Examples

Owner Only

The most basic example checks if the user trying to access a document is the owner.

Server Implementation

/**
 * Method to check if the user has permission to proceed
 * @param {string} method Purpose of the request (e.g., load, change)
 * @param {string} guid The document's unique identifier
 * @param {object} auth Information about the user's authentication
 * @returns {boolean}
 */
const Authentication = async function(method, guid, auth) {
    if (guid) {
        // Validate signature and get the jwt information
        let info = await jwt.verify(auth.token, process.env.JWT_SECRET);
        if (info) {
            // Get the document information from the database
            let document = await adapter.get(guid);
            if (document && info.sub === document.user_id) {
                // The user is the owner of the document
                return true;
            }
        }
    }
    return false;
};

// Server setup
server({
    port: 3000,
    beforeLoad: async function(guid, auth) {
        // Return false to block access if the user is not the owner
        return await Authentication('load', guid, auth);
    },
    load: async function(guid, auth, cachedConfiguration) {
        return await adapter.load(guid, auth, cachedConfiguration);
    },
    change: async function(guid, changes, auth, onerror) {
        return await adapter.change(guid, changes, auth, onerror);
    },
    create: async function(guid, config, auth) {
        return await adapter.create(guid, config, auth);
    },
    destroy: async function(guid, auth) {
        return await adapter.destroy(guid, auth);
    }
});

Client implementation

On the client side, the authentication token contains the sub (subject) field, passed to the server in the auth object.


let token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c';

intrasheets({
    bar: {
        suggestions: true
    },
    client: {
        url: 'http://localhost:3009',
        auth: {
            token: token
        },
    }
})

Visibility of the Spreadsheet

In this example, we check the visibility of a spreadsheet. Any user can view a spreadsheet flagged as public. The developer can set and persist this flag using the available server events.

/**
 * Method to check if the user has permission to proceed
 * @param {string} method Purpose of the request (e.g., load, change)
 * @param {string} guid The document's unique identifier
 * @param {object} auth Information about the user's authentication
 * @returns {boolean}
 */
const Authentication = async function(method, guid, auth) {
    if (guid) {
        // Retrieve the document information from the database
        let document = await adapter.get(guid);
        if (document && document.state === 0) {
            // The document is public
            return true;
        }
    }
    // The document is either private or does not exist
    return false;
};

// Server setup
server({
    port: 3000,

    beforeLoad: async function(guid, auth) {
        // Allow or block access based on the document's visibility
        return await Authentication('load', guid, auth);
    },

    load: async function(guid, auth, cachedConfiguration) {
        return await adapter.load(guid, auth, cachedConfiguration);
    },

    change: async function(guid, changes, auth, onerror) {
        return await adapter.change(guid, changes, auth, onerror);
    },

    create: async function(guid, config, auth) {
        return await adapter.create(guid, config, auth);
    },

    destroy: async function(guid, auth) {
        return await adapter.destroy(guid, auth);
    }
});

Intrasheets

As mentioned earlier, developers can use events to store documents and apply custom flags to meet any application requirements. However, Intrasheets' base implementation suggests three access levels: owner, editor, and viewer.

To implement those suggestions, the developers need to store and validate the properties.

  • Ownership: Validate if the user is the owner of the spreadsheet.
  • Visibility: Determine if the spreadsheet is public or private.
  • Invitations: When a spreadsheet is shared, verify if the user has been granted editor or viewer access.

By convention, Intrasheets assigns access levels 2 for owners, 1 for editors, and 0 for viewers. The following example checks the user_id, privacy, and users properties stored in the database to determine the correct permission level based on the authentication object provided from the front end.

Basic Example

The following methods illustrate a potential approach for implementing the appropriate validations in an Intrasheets application.

Keep in mind that you are responsible for the security and data, so it is necessary, on top of the following suggestion, to validate the JWT authenticity and implement rules to control connection access, as the example currently allows all users to connect and not check the JWT signature.

/**
 * This method retrieves the user's access level for the specified document.
 * @param {string} guid The document's unique identifier
 * @param {object} auth The authentication object containing token and invitation information
 * @returns {number|boolean} Returns the user's access level (0 for read-only, 1 for editor, 2 for owner) or false if no access
 */
const getUserLevel = async function(guid, auth) {
    // Validate signature and get the jwt information
    let info = await jwt.verify(auth.token, process.env.JWT_SECRET);
    // Retrieve the document information from the database
    let document = await adapter.get(guid);
    if (document) {
        // Check if the user is the owner (user_id matches the subject in the JWT token)
        if (info && info.sub === document.user_id) {
            return 2; // Owner level
        }

        // Check if the spreadsheet is public (privacy flag is false)
        if (!document.spreadsheet.privacy) {
            return 1; // Editor privileges
        }

        // Check if the user has an invitation to access the document
        if (auth.invitation && document.users && document.users.length) {
            for (let i = 0; i < document.users.length; i++) {
                // Match the invitation code with the invited users
                if (document.users[i].hash === auth.invitation) {
                    // Return the access level based on the invitation (0: read-only, 1: editor, 2: owner)
                    return document.users[i].level;
                }
            }
        }
    }

    // No access granted
    return false;
}

/**
 * Method to check if the user has permission to proceed
 * @param {string} method The request's purpose (e.g., 'load', 'change')
 * @param {string|null} guid The document's unique identifier (or null if connecting to the server)
 * @param {object} auth The authentication object containing token and invitation information
 * @return {Promise<boolean>} Returns true if the user is authorized, false otherwise
 */
const Authentication = async function(method, guid, auth) {
    // If a document is being accessed
    if (guid) {
        // Get the user's access level for the specified document
        let level = await getUserLevel(guid, auth);

        if (level !== false) {
            // Any invited user can connect or load the document. Only editors (1) or owners (2) can make changes.
            if (level === 0) {
                // Read-only users cannot change the spreadsheet
                if (method !== 'change') {
                    return true;
                }
            } else {
                // Editors and owners have full access
                return true;
            }
        }
    } else {
        // Would you like to allow everyone to connect?
        return true;
    }

    // If none of the conditions are met, deny access
    return false;
}