Skip to content

Commit

Permalink
Merge pull request #6 from kstm-su/feat/oauth-server
Browse files Browse the repository at this point in the history
OAuth2.0 サーバーの実装
  • Loading branch information
Cyndaquil1999 authored Sep 4, 2024
2 parents 7b972ee + fa77f21 commit e75dc69
Show file tree
Hide file tree
Showing 31 changed files with 1,913 additions and 0 deletions.
42 changes: 42 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
name: Build docker image

on:
workflow_dispatch:
push:
branches:
- main

jobs:
push:
name: "member-portal:${{ matrix.tag }}"
runs-on: ubuntu-latest
strategy:
fail-fast: true
matrix:
tag:
- backend
- frontend
steps:
- uses: actions/checkout@v4
- uses: docker/setup-qemu-action@v3
- uses: docker/setup-buildx-action@v3
with:
buildkitd-flags: --debug
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name : Add short sha
run: echo "GITHUB_SHA_SHORT=$(echo $GITHUB_SHA | head -c7)" >> $GITHUB_ENV
- uses: docker/build-push-action@v6
with:
context: ./member-portal-${{ matrix.tag }}
file: ./member-portal-${{ matrix.tag }}/Dockerfile
platforms: linux/amd64
push: true
tags: |
ghcr.io/kstm-su/member-portal/${{ matrix.tag }}:latest
ghcr.io/kstm-su/member-portal/${{ matrix.tag }}:${{ env.GITHUB_SHA_SHORT }}
cache-from: type=gha,scope=member-portal-${{ matrix.tag }}
cache-to: type=gha,mode=max,scope=member-portal-${{ matrix.tag }}
35 changes: 35 additions & 0 deletions member-portal-backend/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
FROM --platform=$BUILDPLATFORM golang:1.22.4 as build

Check warning on line 1 in member-portal-backend/Dockerfile

View workflow job for this annotation

GitHub Actions / member-portal:backend

The 'as' keyword should match the case of the 'from' keyword

FromAsCasing: 'as' and 'FROM' keywords' casing do not match More info: https://docs.docker.com/go/dockerfile/rule/from-as-casing/
RUN mkdir /storage

WORKDIR /go/src/github.com/kstm-su/Member-Portal/backend/

COPY ./go.* ./

RUN --mount=type=cache,target=/go/pkg/mod go mod download

ENV GOCACHE=/tmp/go/cache
ENV CGO_ENABLED=1

ARG TARGETOS
ARG TARGETARCH
ENV GOOS=$TARGETOS
ENV GOARCH=$TARGETARCH

RUN apt update && apt install -y gcc

COPY . .
RUN --mount=type=cache,target=/go/pkg/mod --mount=type=cache,target=/tmp/go/cache \
go build -ldflags '-extldflags "-static"' -o /Member-Portal

FROM gcr.io/distroless/static-debian11
WORKDIR /app
EXPOSE 8080

COPY --from=build /storage/ /app/storage/
VOLUME /app/storage

COPY --from=build /go/src/github.com/kstm-su/Member-Portal/backend/public /app/public

COPY --from=build /Member-Portal ./

CMD ["./Member-Portal"]
32 changes: 32 additions & 0 deletions member-portal-backend/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# kstm-member-portal backend side

This is the backend part of kstm-member-portal. <br>
It is implemented by golang and [echo](https://github.com/gofiber/fiber).

## Getting Started
### Go
```bash
go run main.go
```

or

```bash
go build -o kstm-member-portal
./kstm-member-portal
```

### Docker
```bash
docker build -t kstm-member-portal .
docker run -p 8080:8080 kstm-member-portal
```

## Configuration
引数で設定ファイルを指定することができます。デフォルトは`/app/config.yaml`です。
```bash
./kstm-member-portal --config /path/to/config.yaml
```



45 changes: 45 additions & 0 deletions member-portal-backend/cmd/root.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package cmd

import (
"github.com/kstm-su/Member-Portal/backend/config"
"github.com/kstm-su/Member-Portal/backend/crypto"
"github.com/kstm-su/Member-Portal/backend/database"
"github.com/kstm-su/Member-Portal/backend/router"
"github.com/spf13/cobra"
)

var configFile string

const name = "member-portal"

var rootCmd = &cobra.Command{
Use: name,
Short: "Backend server for the OAuth2 server",
}

func init() {
// コマンドフラグの設定
flags := rootCmd.PersistentFlags()
// 設定ファイルのパスを指定するフラグ --config, -c
flags.StringVarP(&configFile, "config", "c", "/app/config.yaml", "config file path (default is /app/config.yaml)")
}

func Execute() error {
// コマンドの実行
err := rootCmd.Execute()
if err != nil {
return err
}
c, err := config.Load(configFile)
if err != nil {
print(err.Error())
return err
}
//キーペアの生成
crypto.Init(*c)
// データベースの初期化
database.InitDatabase(c)
// ルーターの実行
router.Execute(c)
return nil
}
126 changes: 126 additions & 0 deletions member-portal-backend/config/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
package config

import (
"crypto/rand"
"fmt"
"github.com/spf13/viper"
"log/slog"
"math/big"
"os"
)

// 設定ファイルの構造体
type Config struct {
Server struct {
Port int `yaml:"port"`
Host string `yaml:"host"`
} `yaml:"server"`

File struct {
Base string `yaml:"base"`
} `yaml:"file"`

Database struct {
Type string `yaml:"type"`
SQLite struct {
Path string `yaml:"path"`
} `yaml:"sqlite"`
Postgres struct {
Host string `yaml:"host"`
Port int `yaml:"port"`
User string `yaml:"user"`
Password string `yaml:"password"`
} `yaml:"postgres"`
} `yaml:"database"`

JWT struct {
Key struct {
PrivateKey string `yaml:"private_key"`
PublicKey string `yaml:"public_key"`
} `yaml:"key"`
Issuer string `yaml:"issuer"`
Realm string `yaml:"realm"`
KeyId string `yaml:"key_id"`
} `yaml:"jwt"`

Password struct {
Pepper string `yaml:"pepper"`
Algorithm string `yaml:"algorithm"`
} `yaml:"password"`
}

func init() {
//デフォルト設定ファイルの設定
viper.SetDefault("server.port", 8080)
viper.SetDefault("server.host", "localhost")

viper.SetDefault("file.base", "/app")

viper.SetDefault("database.type", "sqlite")

viper.SetDefault("database.sqlite.path", "/app/db.sqlite3")

viper.SetDefault("jwt.key.private_key", "private.pem")
viper.SetDefault("jwt.key.public_key", "public.pem")
viper.SetDefault("jwt.issuer", "localhost")
viper.SetDefault("jwt.realm", "localhost")
viper.SetDefault("jwt.key_id", "key")

//pepperについては一時的にconfigに保存するが、実環境ではHashiCorp Vaultなどのシークレット管理ツールを使用することを検討
//Databaseに保存はしない
viper.SetDefault("password.pepper", generateRandomString(30))
viper.SetDefault("password.algorithm", "argon2")
}

var Cfg Config

func Load(configFile string) (*Config, error) {
//設定ファイの初期化
viper.SetConfigType("yaml")
viper.SetConfigFile(configFile)

//設定ファイルの存在チェック ない場合はデフォルト設定ファイルを作成
if _, err := os.Stat(configFile); os.IsNotExist(err) {
err = viper.WriteConfig()
if err != nil {
return nil, fmt.Errorf("failed to write default config: %s \n", err)
}
slog.Info("設定ファイルが存在しないため、デフォルト設定ファイルを作成しました。")
}

//設定ファイルの読み込み
err := viper.ReadInConfig()
if err != nil {
return nil, fmt.Errorf("設定ファイル読み込みエラー: %s \n", err)
}
slog.Info("設定ファイルを読み込みました。")

//設定ファイルを構造体に変換
err = viper.Unmarshal(&Cfg)
if err != nil {
return nil, fmt.Errorf("unmarshal error: %s \n", err)
}
slog.Info("設定ファイルを構造体に変換しました。")

baseDir := Cfg.File.Base
err = os.MkdirAll(baseDir, 0700)
if err != nil {
return nil, fmt.Errorf("baseディレクトリを作成するのに失敗しました: %s \n", err)
}

return &Cfg, nil
}

func generateRandomString(n int) string {
const letters = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-"
ret := make([]byte, n)
for i := 0; i < n; i++ {
num, err := rand.Int(rand.Reader, big.NewInt(int64(len(letters))))
if err != nil {
return ""
}
ret[i] = letters[num.Int64()]
}

return string(ret)
}
105 changes: 105 additions & 0 deletions member-portal-backend/crypto/crypto.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package crypto

import (
"bytes"
"crypto/rand"
"encoding/base64"
"fmt"
"github.com/kstm-su/Member-Portal/backend/config"
"golang.org/x/crypto/argon2"
_ "golang.org/x/crypto/argon2"
"math/big"
"strings"
)

func PasswordEncrypt(password string, config *config.Config) string {
salt := []byte(GenerateRandomString(30))

pepper := config.Password.Pepper

memory := 64 * 1024
iterations := 4
threads := 1
keyLen := 32

encoded := passwordEncryptWithParams(password, string(salt), pepper, memory, threads, iterations, keyLen)

return encoded
}

func passwordEncryptWithParams(password string, salt string, pepper string, memory int, threads int, iterations int, keyLen int) string {
// パスワードとpepperを連結
withPepper := password + pepper

// ハッシュ値を生成
hash := argon2.IDKey([]byte(withPepper), []byte(salt), uint32(iterations), uint32(memory), uint8(threads), uint32(keyLen))

// ハッシュ値とsaltをbase64エンコード
b64Salt := base64.RawStdEncoding.EncodeToString([]byte(salt))
b64Hash := base64.RawStdEncoding.EncodeToString(hash)

// データベースに保存するための文字列を生成
encodedHash := fmt.Sprintf("$argon2id$v=%d$m=%d,t=%d,p=%d$%s$%s",
argon2.Version, memory, iterations, threads, b64Salt, b64Hash)

return encodedHash
}

func VerifyPassword(hash string, password string, config config.Config) bool {
// 例: $argon2id$v=19$m=65536,t=4,p=1$c2FsdC1zYWx0$6JNmlGvpjNKYpQNSJdGNfAJQ7+upIXwebdDMWcJf30g
// このような形式の文字列をパースする
parts := strings.Split(hash, "$")
if len(parts) != 6 {
return false
}

// パースした文字列から必要な情報を取り出す
// saltがbase64エンコードされているのでデコードする
salt, err := base64.RawStdEncoding.DecodeString(parts[4])
if err != nil {
return false
}

// ハッシュ値もbase64エンコードされているのでデコードする
decodedHash, err := base64.RawStdEncoding.DecodeString(parts[5])
if err != nil {
return false
}

// ハッシュ値を生成する際に使用したパラメータを取り出す
// m=65536,t=4,p=1
var iterator int
var memory uint32
var threads uint8
_, err = fmt.Sscanf(parts[3], "m=%d,t=%d,p=%d", &memory, &iterator, &threads)

// ハッシュ値の長さを取得
keyLen := len(decodedHash)

// パスワードとpepperを連結してハッシュ値を生成
withPepper := password + config.Password.Pepper

// ハッシュ値を生成
genHash := argon2.IDKey([]byte(withPepper), salt, uint32(iterator), memory, threads, uint32(keyLen))

// 生成したハッシュ値とデータベースに保存されているハッシュ値を比較
if bytes.Equal(decodedHash, genHash) {
return true
}

return false
}

func GenerateRandomString(n int) string {
const letters = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-"
ret := make([]byte, n)
for i := 0; i < n; i++ {
num, err := rand.Int(rand.Reader, big.NewInt(int64(len(letters))))
if err != nil {
return ""
}
ret[i] = letters[num.Int64()]
}

return string(ret)
}
Loading

0 comments on commit e75dc69

Please sign in to comment.