Go: JWT の実装

こちらの記事の実装を Go で行ってみました。
[認証におけるJWTの利用](https://qiita.com/Naoto9282/items/8427918564400968bd2b#%E8%AA%8D%E8%A8%BC%E3%81%AB%E3%81%8A%E3%81%91%E3%82%8Bjwt%E3%81%AE%E5%88%A9%E7%94%A8)

## 作成する API

| Method | URL | Parameter | Response | 説明 |
| --- | --- | --- | --- | --- |
| POST | /register | name,password,firstname,lastname | Tokenを返す | ユーザ登録を行う |
| POST | /login | name,password | Tokenを返す | ログインを行う |
| GET | /user | Authorization | ユーザ名を返す | ユーザ情報を返す |

## 準備

```bash
mkdir jwt_work && cd jwt_work
go mod init jwt_work
go get -u github.com/gin-gonic/gin
```

次のフォルダー構造にします。

```text
$ tree
.
├── go.mod
├── go.sum
├── login
│   └── login.go
├── main.go
├── register
│   ├── generate_token.go
│   └── register.go
├── test_dir
│   ├── go_login.sh
│   ├── go_register.sh
│   └── go_user.sh
└── user
    └── user.go
```

```go:main.go
// ---------------------------------------------------------------
//
//	main.go
//
//					Feb/04/2021
// ---------------------------------------------------------------
package main

import (
	"os"
	"fmt"
	"strings"
)

import "github.com/gin-gonic/gin"

import "jwt_work/register"
import "jwt_work/login"
import "jwt_work/user"
// ---------------------------------------------------------------
func main() {
	fmt.Fprintf (os.Stderr,"*** 開始 ***\n")

	rr := gin.Default()
	rr.GET("/ping", func(cc *gin.Context) {
		cc.JSON(200, gin.H{
			"message": "pong",
		})
	})

	rr.GET("/user", func(cc *gin.Context) {
		str_aa := cc.Request.Header["Authorization"]
		fmt.Fprintf (os.Stderr, str_aa[0] + "\n")
		slice := strings.Split(str_aa[0], " ")
		fmt.Fprintf (os.Stderr, slice[1] + "\n")
		unit_aa := user.User_proc(slice[1]) 
		cc.JSON(200, gin.H{
			"name": unit_aa["name"],
			"firstname": unit_aa["firstname"],
			"lastname": unit_aa["lastname"],
		})
	})

	rr.POST("/register", func(cc *gin.Context) {
		name := cc.PostForm("name")
		password := cc.PostForm("password")
		firstname := cc.PostForm("firstname")
		lastname := cc.PostForm("lastname")
		str_out := register.Register_proc(name,password,firstname,lastname) 
		cc.JSON(200, gin.H{
			"token": str_out,
		})
	})

	rr.POST("/login", func(cc *gin.Context) {
		name := cc.PostForm("name")
		password := cc.PostForm("password")
		str_out := login.Login_proc(name,password) 
		cc.JSON(200, gin.H{
			"token": str_out,
		})
	})

	rr.Run()
}

// ---------------------------------------------------------------
```

```go:login/login.go
// ---------------------------------------------------------------
//
//	login/login.go
//
//					Feb/04/2021
// ---------------------------------------------------------------
package login

import (
	"os"
	"fmt"
	"time"
)

import "jwt_work/register"

// ---------------------------------------------------------------
func Login_proc(name string,password string) string {
	fmt.Fprintf (os.Stderr,"*** Login_proc ***\n")

	now := time.Now ()
	str_out,_ := register.Generate_token_proc(name, now)

	return str_out
}

// ---------------------------------------------------------------
```

```go:register/generate_token.go
// ---------------------------------------------------------------
//
//	register/generate_token.go
//
//					Feb/04/2021
// ---------------------------------------------------------------
package register

import (
	"os"
	"fmt"
	"time"
//	"errors"

//	"encoding/json"

	"github.com/dgrijalva/jwt-go"
)

const (
    // secret は openssl rand -base64 40 コマンドで作成した。
    secret = "2FMd5FNSqS/nW2wWJy5S3ppjSHhUnLt8HuwBkTD6HqfPfBBDlykwLA=="

    // userIDKey はユーザーの ID を表す。
    userIDKey = "user_id"

    // iat と exp は登録済みクレーム名。それぞれの意味は https://tools.ietf.org/html/rfc7519#section-4.1 を参照。{
    iatKey = "iat"
    expKey = "exp"
    // }

    // lifetime は jwt の発行から失効までの期間を表す。
    lifetime = 30 * time.Minute
)

type Auth struct {
    UserID    string
    Iat       int64
}

// ---------------------------------------------------------------
func Generate_token_proc(userID string, now time.Time) (string, error) {
	fmt.Fprintf (os.Stderr,"*** Generate_token_proc ***\n")

    token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
        userIDKey: userID,
        iatKey:    now.Unix(),
        expKey:    now.Add(lifetime).Unix(),
    })

//	secret := "2FMd5FNSqS/nW2wWJy5S3ppjSHhUnLt8HuwBkTD6HqfPfBBDlykwLA=="

    return token.SignedString([]byte(secret))
}

// ---------------------------------------------------------------
func Parse_proc(signedString string) (*Auth, error) {
    token, err := jwt.Parse(signedString, func(token *jwt.Token) (interface{}, error) {
        if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
            return "", fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
//            return "", err.Errorf("unexpected signing method: %v", token.Header["alg"])
        }
        return []byte(secret), nil
    })

    if err != nil {
        if ve, ok := err.(*jwt.ValidationError); ok {
            if ve.Errors&jwt.ValidationErrorExpired != 0 {
//                return nil, err.Wrapf(err, "%s is expired", signedString)
                return nil, fmt.Errorf("%s is expired", signedString,err)
            } else {
                return nil, fmt.Errorf("%s is invalid", signedString, err)
//                return nil, err.Wrapf(err, "%s is invalid", signedString)
            }
        } else {
            return nil, fmt.Errorf("%s is invalid", signedString,err)
//            return nil, err.Wrapf(err, "%s is invalid", signedString)
        }
    }

    if token == nil {
        return nil, fmt.Errorf("not found token in %s:", signedString)
//        return nil, err.Errorf("not found token in %s:", signedString)
    }

    claims, ok := token.Claims.(jwt.MapClaims)
    if !ok {
        return nil, fmt.Errorf("not found claims in %s", signedString)
//        return nil, err.Errorf("not found claims in %s", signedString)
    }
    userID, ok := claims[userIDKey].(string)
    if !ok {
        return nil, fmt.Errorf("not found %s in %s", userIDKey, signedString)
//        return nil, err.Errorf("not found %s in %s", userIDKey, signedString)
    }
    iat, ok := claims[iatKey].(float64)
    if !ok {
        return nil, fmt.Errorf("not found %s in %s", iatKey, signedString)
//        return nil, err.Errorf("not found %s in %s", iatKey, signedString)
    }

    return &Auth{
        UserID:    userID,
        Iat:       int64(iat),
    }, nil
}

// ---------------------------------------------------------------
```

```go:register/register.go
// ---------------------------------------------------------------
//
//	register/register.go
//
//					Feb/04/2021
// ---------------------------------------------------------------
package register

import (
	"os"
	"fmt"
	"time"
	"github.com/jinzhu/gorm"
	_ "github.com/go-sql-driver/mysql"
)

// ---------------------------------------------------------------
func Register_proc(name string,password string,firstname string,lastname string) string {
	fmt.Fprintf (os.Stderr,"*** Register_proc ***\n")

	now := time.Now ()
	str_out,_ := Generate_token_proc(name, now)

	db, _ := gorm.Open ("mysql","scott:tiger123@/test_db")
	defer db.Close ()

sql_str :="insert into users (name,password,firstname,lastname,created) values (?,?,?,?,?)"
        db.Exec (sql_str,name,password,firstname,lastname,now)

	return str_out
}

// ---------------------------------------------------------------
```

```go:user/user.go
// ---------------------------------------------------------------
//
//	user/user.go
//
//					Feb/04/2021
// ---------------------------------------------------------------
package user

import (
	"os"
	"fmt"

	"github.com/jinzhu/gorm"
	_ "github.com/go-sql-driver/mysql"
)

import "jwt_work/register"

// ---------------------------------------------------------------
func User_proc(token string) map[string]interface{}{
	fmt.Fprintf (os.Stderr,"*** User_proc ***\n")
	fmt.Fprintf (os.Stderr,"token = " + token + "\n")

	auth,_ := register.Parse_proc(token)

	fmt.Fprintf (os.Stderr,"*** User_proc *** bbb ***\n")
	fmt.Fprintf (os.Stderr,  auth.UserID)
	fmt.Fprintf (os.Stderr,"*** User_proc *** ccc ***\n")

	name := auth.UserID
	db, _ := gorm.Open ("mysql","scott:tiger123@/test_db")
	defer db.Close ()

	firstname := ""
	lastname := ""
	row := db.Table("users").Where("name = ?", name).Select("firstname, lastname").Row() // (*sql.Row)
	row.Scan(&firstname, &lastname)
	fmt.Fprintf (os.Stderr,"firstname = " + firstname + "\n")
	fmt.Fprintf (os.Stderr,"lastname = " + lastname + "\n")

	unit_aa := make (map[string]interface{})
	unit_aa["name"] = name
	unit_aa["firstname"] = firstname
	unit_aa["lastname"] = lastname

	return unit_aa
}

// ---------------------------------------------------------------
```

## MariaDB の用意
>User: scott
>Password: tiger123
>Database: test_db

作成方法

```text
$  sudo mariadb -uroot

MariaDB [(none)]> create schema test_db;

MariaDB [(none)]>  grant all on test_db.* to  'scott'@'localhost';
```


```sql:create_table.sql
drop table if exists users;
create table users (name varchar(16) primary key, password text, firstname text, lastname text, created date);
show columns from users;
quit
```

テーブルの作成

```bash
mysql -u scott -ptiger123 test_db < create_table.sql
```

## サーバーの起動

```bash
go run main.go
```

起動時に出るメッセージ

```text
$ go run main.go 
*** 開始 ***
[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.

[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
 - using env:	export GIN_MODE=release
 - using code:	gin.SetMode(gin.ReleaseMode)

[GIN-debug] GET    /ping                     --> main.main.func1 (3 handlers)
[GIN-debug] GET    /user                     --> main.main.func2 (3 handlers)
[GIN-debug] POST   /register                 --> main.main.func3 (3 handlers)
[GIN-debug] POST   /login                    --> main.main.func4 (3 handlers)
[GIN-debug] Environment variable PORT is undefined. Using port :8080 by default
[GIN-debug] Listening and serving HTTP on :8080
```

## テストスクリプト

Register データの登録

```bash:go_register.sh
#
URL=http://localhost:8080/register
#
curl -X POST -d name=lion -d password=tiger123 \
	-d firstname=Scott -d lastname=White \
	$URL
#
echo
#
curl -X POST -d name=panda -d password=tiger123 \
	-d firstname=John -d lastname=Clinton \
	$URL
#
echo
#
curl -X POST -d name=coala -d password=tiger123 \
	-d firstname=Mary -d lastname=Smith \
	$URL
#
echo
#
```

Login　トークンの取得

```bash:go_login.sh
#
URL="http://localhost:8080/login"
#
curl -X POST -d name=lion -d password=tiger123 $URL > lion.json
#
jq . lion.json
echo
#
curl -X POST -d name=panda -d password=tiger123 $URL > panda.json
#
jq . panda.json
echo
#
curl -X POST -d name=coala -d password=tiger123 $URL > coala.json
#
jq . coala.json
echo
#
```

User　トークンを渡して、ユーザー情報を得る

```bash:go_user.sh
#
URL="localhost:8080/user"
#
token=`jq .token lion.json | sed 's/"//g'`
curl -X GET $URL \
	-H "Authorization:Bearer ${token}" | jq .
#
token=`jq .token panda.json | sed 's/"//g'`
curl -X GET $URL \
	-H "Authorization:Bearer ${token}" | jq .
#
token=`jq .token coala.json | sed 's/"//g'`
curl -X GET $URL \
	-H "Authorization:Bearer ${token}" | jq .
#
```

実行結果

```text
$ ./go_user.sh 

{
  "firstname": "Scott",
  "lastname": "White",
  "name": "lion"
}

{
  "firstname": "John",
  "lastname": "Clinton",
  "name": "panda"
}

{
  "firstname": "Mary",
  "lastname": "Smith",
  "name": "coala"
}
```

Httpie を使ったスクリプト

```bash:http_user.sh
#
URL="localhost:8080/user"
#
token=`jq .token lion.json | sed 's/"//g'`
http $URL "Authorization:Bearer ${token}"
#
token=`jq .token panda.json | sed 's/"//g'`
http $URL "Authorization:Bearer ${token}"
#
token=`jq .token coala.json | sed 's/"//g'`
http $URL "Authorization:Bearer ${token}"
#
```


## 確認したバージョン

```text
$ go version
go version go1.24.0 linux/amd64
```