Route Security – 3

Agar route secure, kita harus lakukan pengecekan pada backend server. Untuk melakukan cek, kita perlu buat sebagai middleware.

Saat ini kita menggunakan middleware enableCors untuk semua route. KIta perlu menambahkan middleware untuk cek token pada route tertentu saja, dalam hal ini adalah edit dan delete movie.

Agar kita dapat menggunakan multiple middleware, kita perlu install library pendukung.

go get github.com/justinas/alice

Buka file cmd/api/middleware.go, lalu lakukan modifikasi

  • modifikasi penambahahan header pada fungsi enableCors.
  • tambahkan fungsi checkToken.

Berikut hasil akhir file cmd/api/middleware.go

package main

import (
	"errors"
	"log"
	"net/http"
	"strconv"
	"strings"
	"time"

	"github.com/pascaldekloe/jwt"
)

func (app *application) enableCORS(next http.Handler) http.Handler {
	return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
		rw.Header().Set("Access-Control-Allow-Origin", "*")
		rw.Header().Set("Access-Control-Allow-Headers", "Content-Type,Authorization")
		next.ServeHTTP(rw, r)
	})
}

func (app *application) checkToken(next http.Handler) http.Handler {
	return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
		rw.Header().Add("Vary", "Authorization")

		authHeader := r.Header.Get("Authorization")

		// //dapat digunakan untuk anonymous user
		// if authHeader == "" {

		// }

		headerParts := strings.Split(authHeader, " ")
		if len(headerParts) != 2 {
			app.errorJSON(rw, errors.New("invalid auth header"))
			return
		}

		if headerParts[0] != "Bearer" {
			app.errorJSON(rw, errors.New("invalid auth header - Bearer"))
			return
		}

		token := headerParts[1]

		claims, err := jwt.HMACCheck([]byte(token), []byte(app.config.jwt.secret))
		if err != nil {
			app.errorJSON(rw, errors.New("unauthorized - HMAC Check"))
			return
		}

		if !claims.Valid(time.Now()) {
			app.errorJSON(rw, errors.New("unauthorized - token expired"))
			return
		}

		if !claims.AcceptAudience("mydomain.com") {
			app.errorJSON(rw, errors.New("unauthorized - invalid audience"))
			return
		}

		if claims.Issuer != "mydomain.com" {
			app.errorJSON(rw, errors.New("unauthorized - invalid issuer"))
			return
		}

		userID, err := strconv.ParseInt(claims.Subject, 10, 64)
		if err != nil {
			app.errorJSON(rw, errors.New("unauthorized"))
			return
		}

		log.Println(userID)

		next.ServeHTTP(rw, r)
	})
}

Pada file cmd/api/routes.go, lakukan modifikasi

  • tambahkan fungsi untuk wrap fungsi yang memerlukan token checking.
  • ubah cara inisialisasi route pada route yang memerlukan token checking.

Berikut hasil akhir file cmd/api/routes.go

package main

import (
	"context"
	"net/http"

	"github.com/julienschmidt/httprouter"
	"github.com/justinas/alice"
)

func (app *application) wrap(next http.Handler) httprouter.Handle {
	return func(rw http.ResponseWriter, r *http.Request, ps httprouter.Params) {
		ctx := context.WithValue(r.Context(), "params", ps)
		next.ServeHTTP(rw, r.WithContext(ctx))
	}
}

func (app *application) routes() http.Handler {
	router := httprouter.New()
	secure := alice.New(app.checkToken)

	router.HandlerFunc(http.MethodGet, "/status", app.statusHandler)

	router.HandlerFunc(http.MethodPost, "/v1/signin", app.Signin)

	router.HandlerFunc(http.MethodGet, "/v1/anime/:id", app.getAnime)
	router.HandlerFunc(http.MethodGet, "/v1/animes", app.getAnimes)
	router.HandlerFunc(http.MethodGet, "/v1/genres", app.getAllGenres)
	router.HandlerFunc(http.MethodGet, "/v1/animes/:genre_id", app.getAnimeByGenre)

	router.POST("/v1/admin/addeditanime", app.wrap(secure.ThenFunc(app.addEditAnime)))

	router.HandlerFunc(http.MethodGet, "/v1/admin/deleteanime/:id", app.deleteAnime)

	return app.enableCORS(router)
}

Selanjutnya kita lakukan modifikasi pada frontend. Pertama kita passing state untuk informasi jwt ke component yang dimaksud, dalam hal ini adalah AddEditAnime.js.

Buka file src/App.js, lalu modifikasi element Route untuk passing state.

<Route path="/admin/anime/:id" component={(props) => <AddEditAnime {...props} jwt={this.state.jwt}/>}/>

Selanjutnya lakukan modifikasi pada component AddEditAnime.js. dengan menambahkan header baru pada request object untuk mengirim kembali jwt ke server.

const myheader = new Headers();
myheader.append("Content-Type", "application/json");
myheader.append("Authorization", "Bearer " + this.props.jwt);

const requestOpt = {
    method: 'POST',
    body: JSON.stringify(payload),
    headers : myheader,
}

Sampai disini, kita telah menambahkan pengecekan authorization pada frontend. Jadi bila belum login, kita tidak bisa mengubah data.

Selanjutnya, kita harus memeriksa jika user langsung memasukan route untuk melakukan editAnime. JIka user unauthorized, kita akan redirect ke halaman login.

Tambahkan code berikut pada fungsi componentDidMount().

if(this.props.jwt===""){
    this.props.history.push({
        pathname: "/login",
    });
    return
}

Berikut isi lengkap file src/components/AddEditAnime.js

import React, {Component, Fragment} from "react";
import { Link } from "react-router-dom";
import { confirmAlert } from 'react-confirm-alert'; // Import
import 'react-confirm-alert/src/react-confirm-alert.css'; // Import css

export default class AddEditAnime extends Component{
    constructor(props){
        super(props);
        this.state ={
            anime:{
                id:0,
                title: "",
                desc: "",
                year: "", 
            },
            isLoaded : false,
            error : null,
            errors : [],
            alert:{
                type: "d-none",
                msg :"",
            }
        }
        this.handleChg = this.handleChg.bind(this)
        this.handleSubm = this.handleSubm.bind(this)
    }

    handleSubm = (evt) =>{
        evt.preventDefault();
        // validasi form
        let errors = [];

        if (this.state.anime.title === ''){
            errors.push("title");
        }
        if (this.state.anime.desc === ''){
            errors.push("desc");
        }
        if (this.state.anime.year === ''){
            errors.push("year");
        }        

        this.setState({errors:errors});
        if (errors.length>0){
            return false;
        }

        const data = new FormData(evt.target);
        const payload = Object.fromEntries(data.entries());
        const myheader = new Headers();
        myheader.append("Content-Type", "application/json");
        myheader.append("Authorization", "Bearer " + this.props.jwt);

        const requestOpt = {
            method: 'POST',
            body: JSON.stringify(payload),
            headers : myheader,
        }

        fetch('http://localhost:4000/v1/admin/addeditanime', requestOpt)
        .then(response => response.json())
        .then(data => {
            if(data.error){
                this.setState({
                    alert: {type: "alert-danger", msg: data.error.msg}
                });
            }else{
                this.props.history.push({
                    pathname:"/admin",
                });
            }
        });
    };

    handleChg = (evt)=>{
        let value = evt.target.value;
        let name = evt.target.name;
        this.setState((prevstate)=>({
            anime:{
                ...prevstate.anime,
                [name] : value,
            }
        }));
    }

    hasError(key){
        return this.state.errors.indexOf(key) !== -1;
    }

    componentDidMount(){        
        if(this.props.jwt===""){
            this.props.history.push({
                pathname: "/login",
            });
            return
        }
        const id = this.props.match.params.id;
        if (id > 0){
            fetch("http://localhost:4000/v1/anime/" +id)
            .then((response)=>{
                if(response !== "200"){
                    let err = Error;
                    err.Message = "Invalid response code: " + response.status;
                    this.setState({error : err});
                }
                return response.json();
            })
            .then((json) => {
                this.setState({
                    anime:{
                        id: id,
                        title: json.anime.title,
                        year: json.anime.year,
                        desc: json.anime.desc,
                    },
                    isLoaded : true,
                },
                (error) => {
                    this.setState({
                        isLoaded : true,
                        error,
                    })
                }
                )
            })
        }else{
            this.setState({isLoaded:true});
        }
    }

    confirmDelete = (e)=>{
        confirmAlert({
            title: 'Confirm Delete',
            message: 'Are you sure to do this?',
            buttons: [
              {
                label: 'Yes',
                onClick: () => {
                    fetch("http://localhost:4000/v1/admin/deleteanime/" + this.state.anime.id, {method:"GET"})
                    .then(response => response.json)
                    .then(data => {
                        if(data.error){
                            this.setState({
                                alert:{type:"alert-danger", msg: data.error.message}
                            })
                        }else{                           
                            this.props.history.push({
                                pathname:"/admin",
                            })
                        }
                    })
                }

              },
              {
                label: 'No',
                onClick: () => {}}
            ]
          });
    }

    render(){
        let {anime, isLoaded, error} = this.state;
        if (error){
            return <div>Error : {error.Message}</div>
        }else if (!isLoaded){
            return <p>Loading...</p>
        }else{
            return(
                <Fragment>
                    <h2>Add/Edit Anime</h2>
                    <div className={`alert ${this.state.alert.type}`} role="alert">{this.state.alert.msg}</div>
                    <hr/>
                    <form onSubmit={this.handleSubm}>
                        <input type="hidden" name="id" id="id" value={anime.id} onChange={this.handleChg}/>
                        <div className="mb-3">
                            <label htmlFor="title" className="form-label">Title</label>
                            <input type="text" className={this.hasError("title")?"form-control is-invalid":"form-control"} id="title" name="title" value={anime.title} onChange={this.handleChg}/>
                            <div className={this.hasError("title")?"text-danger":"d-none"}>Please enter title...</div>
                        </div>    
                        <div className="mb-3">
                            <label htmlFor="des" className="form-label">Description</label>
                            <textarea type="text" className={this.hasError("desc")?"form-control is-invalid":"form-control"} id="desc" name="desc" rows="3" onChange={this.handleChg} value={anime.desc}/>
                            <div className={this.hasError("desc")?"text-danger":"d-none"}>Please enter description...</div>
                        </div>                    
                        <div className="mb-3">
                            <label htmlFor="year" className="form-label">Year</label>
                            <input type="text" className={this.hasError("year")?"form-control is-invalid":"form-control"} id="year" name="year" value={anime.year} onChange={this.handleChg}/>
                            <div className={this.hasError("year")?"text-danger":"d-none"}>Please enter year...</div>
                        </div>
                        <hr/>
                        <button className="btn btn-primary">Save</button>
                        <Link to="/admin" className="btn btn-warning ms-1">Cancel</Link>
                        {anime.id >0 && (
                           <a href="#!" onClick={()=>this.confirmDelete()} className="btn btn-danger ms-1">Delete </a>
                        )}
                    </form>
                </Fragment>
            );
        }


    }
}

Sharing is caring:

Leave a Comment