Route Security – 1

Untuk mengamankan route pada aplikasi web, kita harus mengerjakan pada sisi frontend dan backend.

Frontend

Pada frontend kita buat class component Login. Buat file baru src/components/Login.js, dengan code seperti berikut:

import React, {Component, Fragment} from "react";

export default class Login extends Component{
    constructor(props){
        super(props);

        this.state={
            email : "",
            pswrd : "",
            error: null,
            errors: [],
            alert:{
                type: "d-none",
                msg: "",
            }
        }

        this.handleChange = this.handleChange.bind(this);
        this.handleSubmit = this.handleSubmit.bind(this);
    }

    handleChange = (evt) =>{
        let value = evt.target.value;
        let name = evt.target.name;

        this.setState((prevState)=>({
            ...prevState,
            [name] : value
        }))
    }

    handleSubmit = (evt) =>{
        evt.preventDefault();
    }

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

    render(){
        return(
            <Fragment>
                <h2>Login</h2>
                <div className={`alert ${this.state.alert.type}`} role="alert">{this.state.alert.msg}</div>
                <form className="pt-3" onSubmit={this.handleSubmit}>
                    <div className="mb-3">                    
                        <label htmlFor="email" className="form-label">Email</label>
                        {/* <input type="email" className={this.hasError("email")?"form-control is-invalid":"form-control"} id="email" name="email" onChange={this.handleChg}/> */}
                        <input type="email" className={`form-control ${this.hasError("email")?"is-invalid":""}`} id="email" name="email" onChange={this.handleChg}/>
                        <div className={this.hasError("email")?"text-danger":"d-none"}>Please enter email...</div>
                    </div>                    
                    <div className="mb-3">
                        <label htmlFor="pswrd" className="form-label">Password</label>
                        <input type="password" className={`form-control ${this.hasError("pswrd")?"is-invalid":""}`} id="pswrd" name="pswrd" onChange={this.handleChg}/>
                        <div className={this.hasError("pswrd")?"text-danger":"d-none"}>Please enter password...</div>                        
                    </div>
                    <hr/>
                    <button className="btn btn-primary">Login</button>
                </form>
            </Fragment>
        );
    }
}

Pada app.js akan kita lakukan perubahan, dengan skenario, Jika user belum login, maka menu add anime dan manage anime tidak ditampilkan dan pada top menu tampilkan link login.

Dan jika user sudah login dan memiliki akses, maka pada top menu ditampilkan link login dan menu add dan manage anime ditampilkan.

Karena app.js akan menangani state, maka perlu diubah dari fungsi component menjadi class component.

Berikut modifikasi pada file src/App.js.

import React, { Component, Fragment } from 'react';
import {BrowserRouter as Router, Switch, Route, Link} from 'react-router-dom';
import Home from './components/Home';
import Animes from './components/Animes';
import Admin from './components/Admin';
import Genres from './components/Genres';
import Anime from './components/Anime';
import Genre from './components/Genre';
import AddEditAnime from './components/AddEditAnime';
import Login from './components/Login';

export default class App extends Component {
  constructor(props){
    super(props);
    this.state={
      jwt : "",
    }
    this.handleJWT(this.handleJWT.bind(this));
  }

  handleJWT = (jwt) =>{
    this.setState({jwt:jwt});
  }

  logout = () =>{
    this.setState({jwt:""});
  }

  render(){
    let loginLink;
    if(this.state.jwt===""){
      loginLink = <Link to="/login">Login</Link>
    }else{
      loginLink = <Link to="/logout" onClick={this.logout}>Logout</Link>
    }
    return (
    <Router>
      <div className="container">
        <div className="row">
            <div className="col mt-3">
              <h1 className="mt-3">Anime Collection</h1>
            </div>
            <div className="col mt-3 text-end">
              {loginLink}
            </div>            
            <hr className="mb-3"/>
        </div>
        <div className="row">
            <div className="col-md-2">
                <nav>
                    <ul className="list-group">
                        <li className="list-group-item"><Link to="/">Home</Link></li>
                        <li className="list-group-item"><Link to="/animes">Anime</Link></li>
                        <li className="list-group-item"><Link to="/genres">Genre</Link></li>
                        {this.state.jwt !== "" &&
                        <Fragment>
                          <li className="list-group-item"><Link to="/admin/anime/0">Add Anime</Link></li>
                          <li className="list-group-item"><Link to="/admin">Manage Anime</Link></li>
                        </Fragment>
                        }
                    </ul>
                </nav>
            </div>
            <div className="col-md-10">
              <Switch>
                <Route path="/animes/:id" component={Anime} />
                <Route path="/animes"><Animes /></Route>
                <Route path="/genre/:id" component={Genre} />
                <Route exact path="/login" component={(props) => <Login {...props} handleJWT={this.handleJWT}/>}/>
                <Route exact path="/genres"><Genres /></Route>
                <Route path="/admin/anime/:id" component={AddEditAnime} />
                <Route path="/admin"><Admin /></Route>
                <Route path="/"><Home /></Route>
              </Switch>          
            </div>
        </div>
      </div>    
    </Router>
    );
  }
}

Backend

Library yang perlu diinstall adalah bcrypt dan jwt.

go get github.com/pascaldekloe/jwt
go get golang.org/x/crypto/bcrypt

Pada file main.go dilakukan modifikasi penambahan config untuk menangani jwt.

Perhatian untuk menyederhanakan tutorial, JWT secret jadi masih di hardcode. Digenerate manual di https://play.golang.org/p/s8KlqJIOWej

Buka file main.go, ubah bagian config struct dan config inisialisasi.

type config struct {
	port int
	env  string
	db   struct {
		dsn string
	}
	jwt struct {
		secret string
	}
}
flag.StringVar(&cfg.jwt.secret, "jwt", "2dce505d96a53c5768052ee90f3df2055657518dad489160df9913f66042e160", "secret")

Pada file routes.go tambahkan routing baru untuk menangani user signin.

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

Buat model untuk user, buka file models/models.go

type User struct {
	ID    int
	Email string
	Pswrd string
}

Buat file baru cmd/api/tokens.go

Perhatian data user di hardcode, agar tutorial lebih sederhana. Pada prakteknya akan digunakan database untuk menyimpan informasi user.

Password adalah password yang di hash. Lihat dilink berikut https://play.golang.org/p/uKMMCzJWGsW

package main

import (
	"backend/models"
	"encoding/json"
	"errors"
	"fmt"
	"net/http"
	"time"

	"github.com/pascaldekloe/jwt"
	"golang.org/x/crypto/bcrypt"
)

var validUser = models.User{
	ID:    1,
	Email: "test@test.com",
	Pswrd: "$2a$12$J797QGR8wqvROqXBl3LFc.axk/kfrV/YjqbBC19.UtzqS9RGsfJSW",
}

type Credentials struct {
	UsrNm string `json:"email"`
	Pswrd string `json:"pswrd"`
}

func (app *application) Signin(rw http.ResponseWriter, r *http.Request) {
	var creds Credentials

	err := json.NewDecoder(r.Body).Decode(&creds)
	if err != nil {
		app.errorJSON(rw, errors.New("unauthorized"))
		return
	}

	hashedPswrd := validUser.Pswrd

	err = bcrypt.CompareHashAndPassword([]byte(hashedPswrd), []byte(creds.Pswrd))
	if err != nil {
		app.errorJSON(rw, errors.New("unauthorized"))
		return
	}

	var claims jwt.Claims
	claims.Subject = fmt.Sprint(validUser.ID)
	claims.Issued = jwt.NewNumericTime(time.Now())
	claims.NotBefore = jwt.NewNumericTime(time.Now())
	claims.Expires = jwt.NewNumericTime(time.Now().Add(24 * time.Hour))
	claims.Issuer = "mydomain.com"
	claims.Audiences = []string{"mydomain.com"}

	jwtBytes, err := claims.HMACSign(jwt.HS256, []byte(app.config.jwt.secret))
	if err != nil {
		app.errorJSON(rw, errors.New("error signing"))
		return
	}

	app.writeJSON(rw, http.StatusOK, string(jwtBytes), "response")
}

Pada tahap awal, tampilan web app akan seperti berikut

Pada modul selanjutnya kita akan hubungkan tombol submit dengan API.

Sharing is caring:

Leave a Comment