【Go】go-blueprintを使ってOAuth2を爆速で実装する
はじめに
GoでOAuth2認証の実装を行います。
今回は例としてgo-blueprintを使用し、Google認証を行う流れを紹介します。
ライブラリについて
go-blueprint
DBの接続設定、GoプロジェクトのDocker設定、GithubActionsを使用したCI/CDワークフローのセットアップなどを自動的に行なってくれます。
goth
gothは、Go言語でOAuth 認証を簡単に実装するためのライブラリです。Google、Facebook、Apple、Githubなどの主要なソーシャルログインプロバイダに対応しており、ユーザー認証フローを簡素化することができます。
事前に
Google Cloudで、あらかじめ作成しておいたプロジェクトを開きます。
その後、クライアントID、クライアントシークレットを発行してください。
また、リダイレクトするコールバックURIも許可するために登録しておきます。
サーバ側の実装
まず下記のコマンドを打ってインストールとプロジェクト作成を行います。
go install github.com/melkeydev/go-blueprint@latest
go-blueprint create
任意のプロジェクト名で今回はフレームワークはGinを選択します。
プロジェクトのテンプレートがされるはずです。
その後、各パッケージをインストールします。
go get github.com/markbates/goth
go get github.com/gorilla/sessions
次にプロジェクト内にinternal/auth/auth.go
を作成します。
auth.goではGoogle OAuth 2.0
認証を実装するためのセットアップを行う関数を定義しています。この関数を呼び出すことで、アプリケーションが Google認証を通じたユーザー認証を提供できるようになります。
今回はGoogle認証を行うのでgoth.UseProviders(google.New(...))
により、Google認証用のプロバイダを設定します。このプロバイダは、Googleの認証エンドポイントとコールバックURLを指定します。
package auth
import (
"log"
"os"
"github.com/gorilla/sessions"
"github.com/joho/godotenv"
"github.com/markbates/goth"
"github.com/markbates/goth/gothic"
"github.com/markbates/goth/providers/google"
)
const (
MaxAge = 86400 * 30
IsProd = false
)
func NewAuth() {
err := godotenv.Load()
if err != nil {
log.Fatal("Error loading .env file")
}
googleClientId := os.Getenv("GOOGLE_CLIENT_ID")
googleClientSecret := os.Getenv("GOOGLE_CLIENT_SECRET")
sessionSecret := os.Getenv("SESSION_SECRET")
if sessionSecret == "" {
log.Fatal("SESSION_SECRET is not set")
}
store := sessions.NewCookieStore([]byte(sessionSecret))
store.MaxAge(MaxAge)
store.Options.Path = "/"
store.Options.HttpOnly = true
store.Options.Secure = IsProd
gothic.Store = store
goth.UseProviders(
google.New(googleClientId, googleClientSecret, "http://localhost:3000/auth/google/callback"),
)
}
auth.NewAuth() を呼び出すことで、Google認証の設定と、セッションの管理に必要な準備が整います。これにより、後でユーザーがGoogleアカウントを使って認証を行い、セッションにその情報を保存できるようになります。
package main
import (
"context"
"fmt"
"log"
"net/http"
"os/signal"
"syscall"
"time"
"go-oauth/internal/auth"
"go-oauth/internal/server"
)
func gracefulShutdown(apiServer *http.Server, done chan bool) {
// Create context that listens for the interrupt signal from the OS.
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer stop()
// Listen for the interrupt signal.
<-ctx.Done()
log.Println("shutting down gracefully, press Ctrl+C again to force")
// The context is used to inform the server it has 5 seconds to finish
// the request it is currently handling
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := apiServer.Shutdown(ctx); err != nil {
log.Printf("Server forced to shutdown with error: %v", err)
}
log.Println("Server exiting")
// Notify the main goroutine that the shutdown is complete
done <- true
}
func main() {
auth.NewAuth()
server := server.NewServer()
// Create a done channel to signal when the shutdown is complete
done := make(chan bool, 1)
// Run graceful shutdown in a separate goroutine
go gracefulShutdown(server, done)
err := server.ListenAndServe()
if err != nil && err != http.ErrServerClosed {
panic(fmt.Sprintf("http server error: %s", err))
}
// Wait for the graceful shutdown to complete
<-done
log.Println("Graceful shutdown complete.")
}
getAuthCallbackFunction
は、ユーザーが認証後に呼び出されるOAuthのコールバック処理です。
今回の実装する使用はGothicを用いてユーザー認証を完了し、認証済みユーザー情報をセッションに保存します。正常に処理が終わったら、フロントエンドのユーザ画面(http://localhost:5173/user
)にリダイレクトするという仕様になります。
getAuthCallbackFunction
は認証情報を解析し、それをもとにユーザ情報をレスポンスで返すシンプルなAPIです。
package server
import (
"net/http"
"os"
"github.com/gin-contrib/cors"
"github.com/gin-gonic/gin"
"github.com/gorilla/sessions"
"github.com/markbates/goth"
"github.com/markbates/goth/gothic"
)
var sessionSecret = os.Getenv("SESSION_SECRET")
var store = gothic.Store.(*sessions.CookieStore)
func (s *Server) RegisterRoutes() http.Handler {
r := gin.Default()
r.Use(cors.New(cors.Config{
// クライアントのURL
AllowOrigins: []string{"http://localhost:5173"},
AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
AllowHeaders: []string{"Origin", "Content-Type", "Accept"},
ExposeHeaders: []string{"Content-Length"},
AllowCredentials: true,
}))
r.GET("/auth/:provider/callback", s.getAuthCallbackFunction)
r.GET("/logout/:provider", func(c *gin.Context) {
gothic.Logout(c.Writer, c.Request)
c.Redirect(http.StatusTemporaryRedirect, "/")
})
r.GET("/", s.HelloWorldHandler)
r.GET("/auth/:provider", func(c *gin.Context) {
q := c.Request.URL.Query()
q.Add("provider", c.Param("provider"))
c.Request.URL.RawQuery = q.Encode()
gothic.BeginAuthHandler(c.Writer, c.Request)
})
r.GET("/getUser", func(c *gin.Context) {
s.getUserAfterAuthorization(c)
})
return r
}
func (s *Server) HelloWorldHandler(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"message": "Hello World"})
}
func (s *Server) getAuthCallbackFunction(c *gin.Context) {
user, err := gothic.CompleteUserAuth(c.Writer, c.Request)
if err != nil {
c.String(http.StatusInternalServerError, err.Error())
return
}
var store = gothic.Store.(*sessions.CookieStore)
session, err := store.Get(c.Request, sessionSecret)
if err != nil {
c.String(http.StatusInternalServerError, err.Error())
return
}
session.Values["user_data"] = user
if err := session.Save(c.Request, c.Writer); err != nil {
c.String(http.StatusInternalServerError, err.Error())
return
}
c.Redirect(http.StatusFound, "http://localhost:5173/user")
}
func (s *Server) getUserAfterAuthorization(c *gin.Context) {
session, err := store.Get(c.Request, sessionSecret)
if err != nil {
c.String(http.StatusInternalServerError, "Failed to get session")
return
}
userData, ok := session.Values["user_data"].(goth.User)
if !ok {
c.String(http.StatusUnauthorized, "User not authenticated")
return
}
response := map[string]string{
"email": userData.Email,
"name": userData.Name,
"access_token": userData.AccessToken,
}
c.JSON(http.StatusOK, response)
}
クライアント側の実装(おまけ)
テストする上で、Reactでクライアント側も作成しました。
※Goの記事を書くのが目的だったため、このへんはざっくりテキトーになりますがお許しください
import '../App.css'
const Home: React.FC = () => {
const handleLogin = () => {
window.location.href = "http://localhost:3000/auth/google";
}
return (
<>
<div className="card">
<button onClick={handleLogin}>
Googleでログイン
</button>
</div>
</>
)
}
export default Home
認証成功後にユーザ画面にリダイレクトされます。
クッキー情報をバックエンドに送信します。
バックエンド側で認証情報を解析してユーザ情報をレスポンスで返してくれるので、それを画面に表示します。
今回はサンプルでメールアドレスと名前を表示しています。
import React, { useEffect, useState } from 'react';
type UserData = {
email: string;
name: string;
accessToken: string;
// 必要に応じて他のフィールドも定義
};
const UserPage: React.FC = () => {
const [userData, setUserData] = useState<UserData>();
useEffect(() => {
const fetchUserData = async () => {
const response = await fetch('http://localhost:3000/getUser', {
credentials: 'include', // クッキーを含めてセッション情報を取得
});
if (!response.ok) {
throw new Error('Failed to fetch user data');
}
const data = await response.json();
setUserData(data);
};
fetchUserData();
}, []);
if (!userData) {
return <div>Loading...</div>;
}
return (
<div>
<h1>Email: {userData.email}</h1>
<h1>Name: {userData.name}</h1>
</div>
);
};
export default UserPage;
感想
OAuth2認証を学ぶ上でgo-blueprintを活用しGoogle認証を実装してみました。
今回は自分で実装したものは、NewAuth
、getAuthCallbackFunction
、getUserAfterAuthorization
ぐらいなので面倒な初期設定は省くことができ、非常に便利でした。
今回は最初の認証部分を実装しましたが、今後はリフレッシュトークンを活用し、アクセストークンの再取得するロジックにも挑戦していきたいです。
Discussion