-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #6 from kstm-su/feat/oauth-server
OAuth2.0 サーバーの実装
- Loading branch information
Showing
31 changed files
with
1,913 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 }} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 GitHub Actions / member-portal:backendThe 'as' keyword should match the case of the 'from' keyword
|
||
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"] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
``` | ||
|
||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
Oops, something went wrong.