シェルスクリプトマガジン

香川大学SLPからお届け!(Vol.65掲載)

筆者:重松 亜夢

 はじめまして!香川大学の重松亜夢です。2019年秋にSLPの所長を引き継ぎま
した。SLPの最近の主な活動はチーム開発です。2019年12月には部員が最近の活動をブログに投稿し、Advent Calendarを作成しました。また、2020年1月には餅つきで親交を深めました。
 今回は、Webアプリケーションに認証機能を実装します。具体的には、米Google社の「Google Cloud Platform」のAPIを使って「Googleでログイン」を実装します。

シェルスクリプトマガジン Vol.65は以下のリンク先でご購入できます。

図9 「.env」ファイルに記述する内容

SAMPLE_HOST="localhost:1323"
GOOGLE_CLIENT_ID="クライアントID"
GOOGLE_CLIENT_SECRET="クライアントシークレット"

図10 「main.go」ファイルに記述する内容

package main
import (
  route "example.com/user_name/sample-app/route"
)
func main() {
  route.Echo.Logger.Fatal(route.Echo.Start(":1323"))
}

図11 「router.go」ファイルに記述する内容

package route
import (
  "fmt"
  "log"
  "os"
  "example.com/user_name/sample-app/handler"
  "github.com/joho/godotenv"
  "github.com/labstack/echo/v4"
  "github.com/labstack/echo/v4/middleware"
  "github.com/stretchr/gomniauth"
  "github.com/stretchr/gomniauth/providers/google"
  "github.com/stretchr/signature"
)
var Echo *echo.Echo
func init() {
  e := echo.New()
  err := setupOAuth()
  if err != nil {
    log.Fatal("Error loading .env file")
  }
  e.Use(middleware.Logger())
  e.GET("/auth/login/:provider", handler.LoginHandler)
  e.GET("/auth/callback/:provider", handler.CallbackHandler)
  Echo = e
}

図12 「router.go」ファイルに追記する内容

func setupOAuth() error {
  err := godotenv.Load()
  if err != nil {
    return err
  }
  host := os.Getenv("SAMPLE_HOST")
  googleCallbackURL := fmt.Sprintf("http://%s/auth/callback/google", host)
  gomniauth.SetSecurityKey(signature.RandomKey(64))
  gomniauth.WithProviders(
    google.New(
      os.Getenv("GOOGLE_CLIENT_ID"),
      os.Getenv("GOOGLE_CLIENT_SECRET"),
      googleCallbackURL,
    ),
  )
  return nil
}

図13 「sesseion.go」ファイルに記述する内容

package handler
import (
  "net/http"
  "github.com/labstack/echo/v4"
  "github.com/stretchr/gomniauth"
  "github.com/stretchr/objx"
)
func LoginHandler(c echo.Context) error {
  provider, err := gomniauth.Provider(c.Param("provider"))
  if err != nil {
    return err
  }
  authURL, err := provider.GetBeginAuthURL(nil, nil)
  if err != nil {
    return err
  }
  return c.Redirect(http.StatusTemporaryRedirect, authURL)
}

図13 「sesseion.go」ファイルに記述する内容

package handler
import (
  "net/http"
  "github.com/labstack/echo/v4"
  "github.com/stretchr/gomniauth"
  "github.com/stretchr/objx"
)
func LoginHandler(c echo.Context) error {
  provider, err := gomniauth.Provider(c.Param("provider"))
  if err != nil {
    return err
  }
  authURL, err := provider.GetBeginAuthURL(nil, nil)
  if err != nil {
    return err
  }
  return c.Redirect(http.StatusTemporaryRedirect, authURL)
}

図14 「sesseion.go」ファイルに追記する内容

func CallbackHandler(c echo.Context) error {
  provider, err := gomniauth.Provider(c.Param("provider"))
  if err != nil {
    return err
  }
  omap, err := objx.FromURLQuery(c.QueryString())
  if err != nil {
    return err
  }
  _, err = provider.CompleteAuth(omap)
  if err != nil {
    return err
  }
  return c.String(http.StatusOK, "Login Success!")
}

図16 「sesseion.go」ファイルのCallbackHandler関数の変更コード

  creds, err := provider.CompleteAuth(omap)
  if err != nil {
    return err
  }
  user, err := provider.GetUser(creds)
  if err != nil {
    return err
  }
  authCookieValue := objx.New(map[string]interface{}{
    "name":      user.Name(),
    "email":     user.Email(),
    "avatarURL": user.AvatarURL(),
  }).MustBase64()
  cookie := &http.Cookie{
    Name:    "auth",
    Value:   authCookieValue,
    Path:    "/",
    Expires: time.Now().Add(24 * time.Hour),
  }
  c.SetCookie(cookie)
  return c.Redirect(http.StatusTemporaryRedirect, "/")

図17 「router.go」ファイルに追記する内容

type TemplateRenderer struct {
  templates *template.Template
}
func (t *TemplateRenderer) Render(w io.Writer, 
                                  name string, data interface{},
                                  c echo.Context) error {
  return t.templates.ExecuteTemplate(w, name, data)
}

図18 「router.go」ファイルのinit関数に挿入する内容

  renderer := &TemplateRenderer{
    templates: template.Must(template.ParseGlob("templates/*.html")),
  }
  e.Renderer = renderer
  e.GET("/", handler.MainPageHandler)

図19 「handler.go」ファイルに記述する内容

package handler
import (
  "net/http"
  "github.com/labstack/echo/v4"
  "github.com/stretchr/objx"
)
func MainPageHandler(c echo.Context) error {
  auth, err := c.Cookie("auth")
  if err != nil {
    return c.Render(http.StatusOK, 
                    "welcome", map[string]interface{}{
      "title": "Welcome",
    })
  }
  userData := objx.MustFromBase64(auth.Value)
  return c.Render(http.StatusOK, "top", map[string]interface{}{
    "name":      userData["name"],
    "email":     userData["email"],
    "avatarURL": userData["avatarURL"],
    "title":     "TopPage",
  })
}

図20 「index.html」ファイルに記述する内容

{{define "top"}}
  {{template "head" .}}
  <img src={{.avatarURL}} width="10%">
  <h2>Hello, {{.name}}</h2>
  Your Email : {{.email}}
{{end}}
{{define "welcome"}}
  {{template "head" .}}
  <h1>ようこそ</h1>
  <ul>
    <li>
      <a href="/auth/login/google">Googleでログイン</a>
    </li>
  </ul>
{{end}}

図21 「base.html」ファイルに記述する内容

{{define "head"}}
  <title>{{ .title }} / Sample-App</title>
{{end}}

図23 「router.go」ファイルのinit関数定義部分に挿入する内容

e.Pre(middleware.RemoveTrailingSlash())
e.Group("", authCheckMiddleware())

図24 「router.go」ファイルの末尾に追記する内容

func authCheckMiddleware() echo.MiddlewareFunc {
  return func(next echo.HandlerFunc) echo.HandlerFunc {
    return func(c echo.Context) error {
      _, err := c.Cookie("auth")
      if err != nil {
        return c.Redirect(http.StatusTemporaryRedirect, "/")
      }
      return next(c)
    }
  }
}

図25 認証後だけアクセスできるルートを設定する例

authCheck := e.Group("", authCheckMiddleware())
authCheck.GET("/fuga", handler.SamplePage)