Skip to content
This repository has been archived by the owner on Dec 11, 2024. It is now read-only.

feat: 認証機構 #23

Merged
merged 23 commits into from
Nov 15, 2023
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,12 @@ MYSQL_ROOT_PASSWORD="test-root-pass"
MYSQL_DATABASE="test-db"
MYSQL_USER="test-taro"
MYSQL_PASSWORD="test-pass"

GITHUB_CLIENT_ID=""
GITHUB_CLIENT_SECRET=""

JWT_SECRET="test-jwt-secret"

ENV="development"
# ENV="prod"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

prod -> production

FRONT_CALLBACK_URL="http://localhost/
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FRONT_CALLBACK_URL="http://localhost/"

5 changes: 5 additions & 0 deletions docker-compose.prod.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,11 @@ services:
- MYSQL_DATABASE=${MYSQL_DATABASE}
- MYSQL_USER=${MYSQL_USER}
- MYSQL_PASSWORD=${MYSQL_PASSWORD}
- GITHUB_CLIENT_ID=${GITHUB_CLIENT_ID}
- GITHUB_CLIENT_SECRET=${GITHUB_CLIENT_SECRET}
- JWT_SECRET=${JWT_SECRET}
- ENV=${ENV}
- FRONT_CALLBACK_URL=${FRONT_CALLBACK_URL}
volumes:
- ./server/public:/app/public
depends_on:
Expand Down
7 changes: 7 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,18 @@ services:
target: dev
volumes:
- ./server:/app
ports:
- 8000:8000
environment:
- ENV=${ENV}
- MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD}
- MYSQL_DATABASE=${MYSQL_DATABASE}
- MYSQL_USER=${MYSQL_USER}
- MYSQL_PASSWORD=${MYSQL_PASSWORD}
- GITHUB_CLIENT_ID=${GITHUB_CLIENT_ID}
- GITHUB_CLIENT_SECRET=${GITHUB_CLIENT_SECRET}
- JWT_SECRET=${JWT_SECRET}
- FRONT_CALLBACK_URL=${FRONT_CALLBACK_URL}
depends_on:
database:
condition: service_healthy
Expand Down
16 changes: 8 additions & 8 deletions etc/nginx/dev/conf.d/meline.conf
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,6 @@ server {
listen 80;
server_name localhost;

location / {
proxy_pass http://client:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
}

location /images {
alias /var/www/public/images;
}
Expand All @@ -21,4 +13,12 @@ server {
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
}

location / {
proxy_pass http://client:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
}
}
3 changes: 1 addition & 2 deletions server/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,13 @@ FROM golang:1.20-alpine AS builder
WORKDIR /app
COPY . .
RUN go mod download
RUN CGO_ENABLED=0 go build -o main .
RUN CGO_ENABLED=0 go build -o main cmd/server/main.go


FROM alpine AS prod
RUN apk --no-cache add ca-certificates
WORKDIR /app
COPY --from=builder /app/main /app/main
COPY --from=builder /app/sql /app/sql

EXPOSE 8000
ENTRYPOINT ["/app/main"]
Expand Down
8 changes: 4 additions & 4 deletions server/cmd/seeder/seeds/user_seeder.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,21 @@ import (
"errors"

"github.com/uptrace/bun"
"github.com/saitamau-maximum/meline/usecase/model"
"github.com/saitamau-maximum/meline/models"
)

var (
users = []model.User{
{
GithubID: "sample-1",
ProviderID: "sample-1",
Name: "test-user-1",
},
{
GithubID: "sample-2",
ProviderID: "sample-2",
Name: "test-user-2",
},
{
GithubID: "sample-3",
ProviderID: "sample-3",
Name: "test-user-3",
},
}
Expand Down
26 changes: 21 additions & 5 deletions server/cmd/server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,16 @@ package main
import (
"net/http"

_ "github.com/go-sql-driver/mysql"
"github.com/labstack/echo/v4"
"github.com/uptrace/bun"
"github.com/uptrace/bun/dialect/mysqldialect"
_ "github.com/go-sql-driver/mysql"

infra "github.com/saitamau-maximum/meline/infra/mysql"
"github.com/saitamau-maximum/meline/controller/handler"
"github.com/saitamau-maximum/meline/controller/gateway"
"github.com/saitamau-maximum/meline/infra/github"
"github.com/saitamau-maximum/meline/infra/mysql"
"github.com/saitamau-maximum/meline/usecase"
)

const (
Expand All @@ -18,17 +22,29 @@ const (
func main() {
e := echo.New()

db, err := infra.ConnectDB(HOST)
db, err := mysql.ConnectDB(HOST)
if err != nil {
e.Logger.Error(err)
}

bunDB := bun.NewDB(db, mysqldialect.New())
defer bunDB.Close()

e.GET("/", func(c echo.Context) error {
apiGroup := e.Group("/api")

oAuthConf := github.NewGithubOAuthConf()
oAuthRepository := github.NewOAuthRepository(oAuthConf)
userRepository := mysql.NewUserRepository(bunDB)
authInteractor := usecase.NewGithubOAuthInteractor(oAuthRepository)
userInteractor := usecase.NewUserInteractor(userRepository)
authGateway := gateway.NewAuthGateway(userInteractor)

authGroup := apiGroup.Group("/auth")
handler.NewOAuthHandler(authGroup, authInteractor, userInteractor)

apiGroup.GET("/", authGateway.Auth(func (c echo.Context) error {
return c.String(http.StatusOK, "Hello, World!")
})
}))

e.Start(":8000")
}
Expand Down
72 changes: 72 additions & 0 deletions server/controller/gateway/auth_gateway.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package gateway

import (
"fmt"
"net/http"
"os"
"time"

jwt "github.com/dgrijalva/jwt-go"
"github.com/labstack/echo/v4"
"github.com/saitamau-maximum/meline/usecase"
)

type AuthGateway struct {
userInteractor usecase.IUserInteractor
}

func NewAuthGateway(userInteractor usecase.IUserInteractor) *AuthGateway {
return &AuthGateway{
userInteractor: userInteractor,
}
}

func (h *AuthGateway) Auth(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
ctx := c.Request().Context()

// Get Access Token
cookie, err := c.Cookie("access_token")
if err != nil {
return c.JSON(http.StatusUnauthorized, err)
}

token, err := jwt.Parse(cookie.Value, func(token *jwt.Token) (interface{}, error) {
if token.Method.Alg() != "HS256" {
return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])
}

jwtSecret := os.Getenv("JWT_SECRET")
if jwtSecret == "" {
os.Exit(1)
}

return []byte(jwtSecret), nil
})
if err != nil {
return c.JSON(http.StatusUnauthorized, err)
}

claims, ok := token.Claims.(jwt.MapClaims)
if !ok || !token.Valid {
return c.JSON(http.StatusUnauthorized, err)
}

// Get User
userId := claims["user_id"].(float64)

user, err := h.userInteractor.GetUserByID(ctx, uint64(userId))
if err != nil {
return c.JSON(http.StatusUnauthorized, err)
}

c.Set("user", user)

exp := claims["exp"].(float64)
if int64(exp) < time.Now().Unix() {
return c.JSON(http.StatusForbidden, err)
}

return next(c)
}
}
119 changes: 119 additions & 0 deletions server/controller/handler/oauth_handler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
package handler

import (
"database/sql"
"log"
"net/http"
"os"
"time"

"github.com/labstack/echo/v4"
"github.com/saitamau-maximum/meline/usecase"
)

const (
STATE_LENGTH = 32
)

var (
isDev = os.Getenv("ENV") == "dev"
)

type OAuthHandler struct {
githubOAuthInteractor usecase.IGithubOAuthInteractor
userInteractor usecase.IUserInteractor
}

func NewOAuthHandler(authGroup *echo.Group, githubOAuthInteractor usecase.IGithubOAuthInteractor, userInteractor usecase.IUserInteractor) {
oAuthHandler := &OAuthHandler{
githubOAuthInteractor: githubOAuthInteractor,
userInteractor: userInteractor,
}

authGroup.GET("/login", oAuthHandler.Login)
authGroup.GET("/callback", oAuthHandler.CallBack)
}

func (h *OAuthHandler) Login(c echo.Context) error {
ctx := c.Request().Context()

state := h.githubOAuthInteractor.GenerateState(STATE_LENGTH)

cookie := new(http.Cookie)
cookie.Name = "state"
cookie.Value = state
cookie.Path = "/"
cookie.HttpOnly = true
cookie.SameSite = http.SameSiteLaxMode
cookie.Secure = !isDev

c.SetCookie(cookie)

url := h.githubOAuthInteractor.GetGithubOAuthURL(ctx, state)

return c.Redirect(http.StatusTemporaryRedirect, url)
}

func (h *OAuthHandler) CallBack(c echo.Context) error {
ctx := c.Request().Context()

// Check State
state := c.QueryParam("state")
cookie, err := c.Cookie("state")
if err != nil {
log.Default().Println(err)
return c.JSON(http.StatusUnauthorized, err)
}

if state != cookie.Value {
log.Default().Println(err)
return c.JSON(http.StatusUnauthorized, err)
}

code := c.QueryParam("code")
gitToken, err := h.githubOAuthInteractor.GetGithubOAuthToken(ctx, code)
if err != nil {
log.Default().Println(err)
return c.JSON(http.StatusUnauthorized, err)
}

userRes, err := h.githubOAuthInteractor.GetGithubUser(ctx, gitToken)
if err != nil {
log.Default().Println(err)
return c.JSON(http.StatusUnauthorized, err)
}

user, err := h.userInteractor.GetUserByGithubID(ctx, userRes.OAuthUserID)
if err != nil {
if (err == sql.ErrNoRows) {
user, err = h.userInteractor.CreateUser(ctx, userRes.OAuthUserID, userRes.Name, userRes.ImageURL)
if err != nil {
log.Default().Println(err)
return c.JSON(http.StatusInternalServerError, err)
}
} else {
log.Default().Println(err)
return c.JSON(http.StatusInternalServerError, err)
}
}

// Set Access Token
token, err := h.githubOAuthInteractor.CreateAccessToken(ctx, user)
if err != nil {
log.Default().Println(err)
return c.JSON(http.StatusInternalServerError, err)
}

newCookie := new(http.Cookie)
newCookie.Name = "access_token"
newCookie.Value = token
newCookie.Path = "/"
newCookie.HttpOnly = true
newCookie.SameSite = http.SameSiteLaxMode
newCookie.Secure = !isDev
newCookie.Expires = time.Now().Add(3 * time.Hour)

c.SetCookie(newCookie)

return c.Redirect(http.StatusTemporaryRedirect, os.Getenv("FRONT_CALLBACK_URL"))
}
7 changes: 7 additions & 0 deletions server/domain/entity/oauth_user_response.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package entity

type OAuthUserResponse struct {
OAuthUserID string `json:"user_id"`
Name string `json:"name"`
ImageURL string `json:"image_url"`
}
19 changes: 10 additions & 9 deletions server/domain/entity/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,22 @@ import (
)

type User struct {
ID uint64
GithubID string
Name string
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt time.Time
ID uint64 `json:"id"`
ProviderID string `json:"provider_id"`
Name string `json:"name"`
ImageURL string `json:"image_url"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt time.Time `json:"deleted_at"`
}

func NewUserEntity(id uint64, githubID, name string, createdAt, updatedAt, deletedAt time.Time) *User {
func NewUserEntity(id uint64, providerID, name string, createdAt, updatedAt, deletedAt time.Time) *User {
return &User{
ID: id,
GithubID: githubID,
ProviderID: providerID,
Name: name,
CreatedAt: createdAt,
UpdatedAt: updatedAt,
DeletedAt: deletedAt,
}
}
}
Loading