Pada modul ini kita akan membuat fungsi untuk handler authentication. Kita akan menggunakan JWT, kita perlu install package untuk JWT.
Pada root direktori aplikasi, install package menggunakan perintah berikut:
$ go get github.com/golang-jwt/jwt/v4
Kemudian buat file cmd/api/auth.go
package main
import (
"errors"
"fmt"
"net/http"
"strings"
"time"
"webapp/pkg/data"
"github.com/golang-jwt/jwt/v4"
)
const jwtTokenExpiry = time.Minute * 15
const refreshTokenExpiry = time.Hour * 24
type TokenPairs struct {
Token string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
}
type Claims struct {
UserName string `json:"name"`
jwt.RegisteredClaims
}
func (app *application) getTokenFromHeaderAndVerify(w http.ResponseWriter, r *http.Request) (string, *Claims, error) {
// we expect our authorization header to look like this:
// Bearer <token>
// add a header
w.Header().Add("Vary", "Authorization")
// get the authorization header
authHeader := r.Header.Get("Authorization")
// sanity check
if authHeader == "" {
return "", nil, errors.New("no auth header")
}
// split the header on spaces
headerParts := strings.Split(authHeader, " ")
if len(headerParts) != 2 {
return "", nil, errors.New("invalid auth header")
}
// check to see if we have the word "Bearer"
if headerParts[0] != "Bearer" {
return "", nil, errors.New("unauthorized: no Bearer")
}
token := headerParts[1]
// declare an empty Claims variable
claims := &Claims{}
// parse the token with our claims (we read into claims), using our secret (from the receiver)
_, err := jwt.ParseWithClaims(token, claims, func(token *jwt.Token) (interface{}, error) {
// validate the signing algorithm
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexepected signing method: %v", token.Header["alg"])
}
return []byte(app.JWTSecret), nil
})
// check for an error; note that this catches expired tokens as well.
if err != nil {
if strings.HasPrefix(err.Error(), "token is expired by") {
return "", nil, errors.New("expired token")
}
return "", nil, err
}
// make sure that we issued this token
if claims.Issuer != app.Domain {
return "", nil, errors.New("incorrect issuer")
}
// valid token
return token, claims, nil
}
func (app *application) generateTokenPair(user *data.User) (TokenPairs, error) {
// Create the token.
token := jwt.New(jwt.SigningMethodHS256)
// set claims
claims := token.Claims.(jwt.MapClaims)
claims["name"] = fmt.Sprintf("%s %s", user.FirstName, user.LastName)
claims["sub"] = fmt.Sprint(user.ID)
claims["aud"] = app.Domain
claims["iss"] = app.Domain
if user.IsAdmin == 1 {
claims["admin"] = true
} else {
claims["admin"] = false
}
// set the expiry
claims["exp"] = time.Now().Add(jwtTokenExpiry).Unix()
// create the signed token
signedAccessToken, err := token.SignedString([]byte(app.JWTSecret))
if err != nil {
return TokenPairs{}, err
}
// create the refresh token
refreshToken := jwt.New(jwt.SigningMethodHS256)
refreshTokenClaims := refreshToken.Claims.(jwt.MapClaims)
refreshTokenClaims["sub"] = fmt.Sprint(user.ID)
// set expiry; must be longer than jwt expiry
refreshTokenClaims["exp"] = time.Now().Add(refreshTokenExpiry).Unix()
// create signed refresh token
signedRefreshToken, err := refreshToken.SignedString([]byte(app.JWTSecret))
if err != nil {
return TokenPairs{}, err
}
var tokenPairs = TokenPairs{
Token: signedAccessToken,
RefreshToken: signedRefreshToken,
}
return tokenPairs, nil
}
Kemudian kita buat handler authenticate. Buka file cmd/api/api-handlers.go
package main
import (
"errors"
"net/http"
"golang.org/x/crypto/bcrypt"
)
type Credentials struct {
Username string `json:"email"`
Password string `json:"password"`
}
func (app *application) authenticate(w http.ResponseWriter, r *http.Request) {
var creds Credentials
// read a json payload
err := app.readJSON(w, r, &creds)
if err != nil {
app.errorJSON(w, errors.New("unauthorized"), http.StatusUnauthorized)
return
}
// look up the user by email address
user, err := app.DB.GetUserByEmail(creds.Username)
if err != nil {
app.errorJSON(w, errors.New("unauthorized"), http.StatusUnauthorized)
return
}
// check password
err = bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(creds.Password))
if err != nil {
app.errorJSON(w, errors.New("unauthorized"), http.StatusUnauthorized)
return
}
// generate tokens
tokenPairs, err := app.generateTokenPair(user)
if err != nil {
app.errorJSON(w, errors.New("unauthorized"), http.StatusUnauthorized)
return
}
// send token to user
_ = app.writeJSON(w, http.StatusOK, tokenPairs)
}
func (app *application) refresh(w http.ResponseWriter, r *http.Request) {
}
func (app *application) allUsers(w http.ResponseWriter, r *http.Request) {
}
func (app *application) getUser(w http.ResponseWriter, r *http.Request) {
}
func (app *application) updateUser(w http.ResponseWriter, r *http.Request) {
}
func (app *application) deleteUser(w http.ResponseWriter, r *http.Request) {
}
func (app *application) insertUser(w http.ResponseWriter, r *http.Request) {
}
Selanjutnya mari kita buat fungsi test authentication, buat file cmd/api/api-handlers_test.go, lalu gunakan code test berikut.
package main
import (
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"
)
func Test_app_authenticate(t *testing.T) {
var theTests = []struct {
name string
requestBody string
expectedStatusCode int
}{
{"valid user", `{"email":"admin@example.com","password":"secret"}`, http.StatusOK},
{"not json", `I'm not JSON`, http.StatusUnauthorized},
{"empty json", `{}`, http.StatusUnauthorized},
{"empty email", `{"email":""}`, http.StatusUnauthorized},
{"empty password", `{"email":"admin@example.com"}`, http.StatusUnauthorized},
{"invalid user", `{"email":"admin@someotherdomain.com","password":"secret"}`, http.StatusUnauthorized},
}
for _, e := range theTests {
var reader io.Reader = strings.NewReader(e.requestBody)
req, _ := http.NewRequest("POST", "/auth", reader)
rr := httptest.NewRecorder()
handler := http.HandlerFunc(app.authenticate)
handler.ServeHTTP(rr, req)
if e.expectedStatusCode != rr.Code {
t.Errorf("%s: returned wrong status code; expected %d but got %d", e.name, e.expectedStatusCode, rr.Code)
}
}
}
Buat file cmd/api/setup_test.go, lalu tambahkan code berikut
package main
import (
"os"
"testing"
"webapp/pkg/repository/dbrepo"
)
var app application
func TestMain(m *testing.M) {
app.DB = &dbrepo.TestDBRepo{}
app.Domain = "example.com"
app.JWTSecret = "2dce505d96a53c5768052ee90f3df2055657518dad489160df9913f66042e160"
os.Exit(m.Run())
}
Sampai disini fungsi testing telah siap. Buka command prompt, masuk ke direktori cmd/api, lalu jalankan unit test.
$ go test -v .
go test -v .
=== RUN Test_app_authenticate
--- PASS: Test_app_authenticate (3.24s)
PASS
ok webapp/cmd/api 3.961s
Sesuai ekspektasi, fungsi test berhasil.