Hantera säkerhet i React med JWT

Under de senaste åren har det skett en stor förändring kring sättet vi använder programvara. Fokus har skiftats från stationära applikationer till lättdrivna applikationer i molnet (SaaS). Välkända företag som Facebook och Google har varit ledande i utvecklingen kring denna teknik.

Google har som exempel introducerat Angular, medan Facebook ligger bakom React, båda JavaScript bibliotek som används för att bygga webbgränssnitt.

På Visionmate har vi valt den senare alltså React för att bygga våra applikationer. I det här inlägget tänkte jag prata mer om hur vi hanterar själva säkerheten i applikationsutvecklingen.

JSON Web Token (JWT) är ett populärt sätt för att säkra upp och skydda dina processer (finns detaljerat i RFC 7519-standard).

Den stora fördelen med att använda en JWT-token (säkerhetsnyckel) är att du kan skjuta in extra information som bara kan avkodas av din back-end-kod.

Vi använder vi Grails för back-end och använder Spring Security REST-plugin för att generera och hantera JWT-tokens. Jag kommer inte gå in så mycket på just det idag, men jag eller någon annan i teamet planerar att skriva mer om ämnet framöver.

I det här exemplet använder jag Redux som state container inkluderat autentiseringsdata och nyckelinformation.

Inloggningsfunktionen skickar inte bara uppgifter om själva säkerhetsnyckeln utan även tilldelade användarroller. Anropet talar om alltså vilken typ av rättigheter användaren har – till exempel om de kan redigera vissa dokument eller få tillgång till adminpanelen.

API-inloggningsfunktionen (alla anrop görs med axios):

function login(user) {
        const config = {
            headers: {
                'Accept': 'application/json',
                'Content-Type': 'application/json'
            }
        };
        return axios.post(`${SERVER_URL}/api/login`, user, config)       
    }

Login action:

export function login(username, password) {
    const user = {
        username,
        password
    };
    return (dispatch) => {
        ApiConnector.login(user)
            .then(response => {
                const data = response.data
                const user = {
                    access_token: data.access_token,
                    refresh_token: data.refresh_token,
                    roles: data.roles,
                    username: data.username,
                };
                sessionStorage.setItem('user', JSON.stringify(user));
                dispatch({
                    type: LOGIN,
                    user
                });
            })
            .catch((error) => {
                console.log(error)
            })
    }
}

Eftersom vi har en asynkron kommunikation så används redux-thunk middleware för att hantera den. Som du kan se innehåller svaret inte bara åtkomst- och uppdateringstoken, men även användarroller som senare kan användas för att till exempel göra ytterligare paneler på ett dashboard.

Nyckeln lagras sedan i antingen en session- eller lokal lagring beroende på applikationskraven, det här gör att användaren inte behöver logga in varje gång en webbläsare uppdateras.

Autentiseringsdatan lagras i en reducer:

export default function (state = {loggedIn: false}, action) {
    switch(action.type) {        
        case LOGIN:
            return {...state,
                user: action.user,                 
                loggedIn: true                
            };
        case LOGOUT:
            return {...state,
                user: {},
                loggedIn: false
            };
        default:
            return state
    }
}

En JWT-nyckel inkluderas i varje anrop. Därför kan den lagras i en separat JavaScript-fil som enkelt hämtas senare.

I det här fallet lagras den i headers.js:

export default () => {
    return {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${sessionStorage.getItem('user') ? JSON.parse(sessionStorage.getItem('user')).access_token : null}`
    }
}

Funktionen adderas sedan till varje API-anrop:

import headers from "../security/headers";

 function get(target, params) {
        const config = {
            headers: headers(),
            params
        }
        return axios.get(`${SERVER_URL}/api/${target}`, config)
    }

När säkerhetsnyckeln löper ut och blir ogiltigt så behöver användaren inte logga in igen – det är tillräckligt att använda uppdateringen för att få en ny giltig access.

Uppdatera token function:

function refreshToken() {
        const config = {
            headers: {
                'Accept': 'application/json',
                'Content-Type': 'application/x-www-form-urlencoded'
            }
        };
        const body = {
            grant_type: "refresh_token",
            refresh_token: JSON.parse(sessionStorage.user).refresh_token
        };
        axios.post(`${SERVER_URL}/oauth/access_token`, qs.stringify(body), config)
            .then(response => {
                const data = response.data
                const user = {
                    access_token: data.access_token,
                    refresh_token: data.refresh_token,
                    roles: data.roles,
                    username: data.username                    
                };
                sessionStorage.setItem('user', JSON.stringify(user));
                store.dispatch({
                    type: REFRESH_TOKEN,
                    user
                })
            })
    }

För att logga ut användaren, radera helt enkelt nyckeln från session/lokal lagring som visas i åtgärden nedan.

Logout action:

export function logout() {
    sessionStorage.removeItem('user');
    return {
        type: LOGOUT
    }
}

Summering

Som jag försökt visa i det här inlägget så måste vissa steg följas för att kunna använda JWT med din applikation. Du kan också använda exemplet som en mall för att lägga till mer funktionalitet och på så sätt utveckla din applikation.

/Radek

 

Bibliotek som används: redux, redux-thunk och axios.

Skicka gärna frågor och kommentarer till vår mail:

info(@)visionmate.se

 

Alla artiklar