(FEAT): Implemented the first stages of Google OAuth.
All that's left is the UI and repository implementation.
This commit is contained in:
parent
6fb4664478
commit
a9cdc25adf
21
internal/app/handlers/auth_handler.go
Normal file
21
internal/app/handlers/auth_handler.go
Normal file
@ -0,0 +1,21 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/haydenhargreaves/Potion/internal/app/service"
|
||||
)
|
||||
|
||||
func GoogleLogin(ctx *gin.Context) {
|
||||
url := service.GoogleLogin(ctx)
|
||||
ctx.Redirect(http.StatusSeeOther, url)
|
||||
}
|
||||
|
||||
func GoogleCallback(ctx *gin.Context) {
|
||||
if googleUserInfo, err := service.GoogleCallback(ctx); err != nil {
|
||||
ctx.JSON(http.StatusInternalServerError, err)
|
||||
} else {
|
||||
ctx.JSON(http.StatusOK, googleUserInfo)
|
||||
}
|
||||
}
|
||||
@ -2,9 +2,14 @@ package server
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/a-h/templ/examples/integration-gin/gintemplrenderer"
|
||||
"github.com/gin-contrib/cors"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/haydenhargreaves/Potion/internal/app/handlers"
|
||||
"github.com/haydenhargreaves/Potion/internal/infrastructure/auth"
|
||||
"github.com/joho/godotenv"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
@ -25,6 +30,13 @@ func Init(port int) *Server {
|
||||
config: cors.DefaultConfig(),
|
||||
}
|
||||
|
||||
// Some stuff for templ rendering
|
||||
htmlRenderer := server.Router.HTMLRender
|
||||
server.Router.HTMLRender = &gintemplrenderer.HTMLTemplRenderer{FallbackHtmlRenderer: htmlRenderer}
|
||||
|
||||
// Disable proxy warnings
|
||||
server.Router.SetTrustedProxies(nil)
|
||||
|
||||
// Setup the CORS settings and active them
|
||||
server.config.AllowAllOrigins = true
|
||||
server.Router.Use(cors.New(server.config))
|
||||
@ -32,6 +44,28 @@ func Init(port int) *Server {
|
||||
return server
|
||||
}
|
||||
|
||||
func (s *Server) ConfigureAuth() *Server {
|
||||
err := godotenv.Load(".env")
|
||||
if err != nil {
|
||||
panic("Could not load env file")
|
||||
}
|
||||
|
||||
var (
|
||||
redirectUrl string = "http://localhost:3000/v1/api/auth/callback"
|
||||
clientId string = os.Getenv("GOOGLE_CLIENT_ID")
|
||||
clientSecret string = os.Getenv("GOOGLE_CLIENT_SECRET")
|
||||
scope []string = []string{
|
||||
"https://www.googleapis.com/auth/userinfo.email",
|
||||
"https://www.googleapis.com/auth/userinfo.profile",
|
||||
}
|
||||
)
|
||||
|
||||
// Setup Google OAuth
|
||||
auth.NewGoogleConfig(redirectUrl, clientId, clientSecret, scope)
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
// Start starts the server on the port provided when the server was initialized
|
||||
func (s *Server) Start() {
|
||||
s.Router.Run(fmt.Sprintf(":%d", s.port))
|
||||
@ -45,11 +79,18 @@ func (s *Server) Setup() *Server {
|
||||
router_web := router_v1.Group("/web")
|
||||
router_api := router_v1.Group("/api")
|
||||
|
||||
// Static routes
|
||||
router_web.Static("/static", "./web/static")
|
||||
|
||||
// API router endpoints
|
||||
router_api.GET("/", func(ctx *gin.Context) { ctx.JSON(200, gin.H{"message": "Server is active."}) })
|
||||
|
||||
// WEB router endpoints
|
||||
router_web.GET("/", nil)
|
||||
router_web.GET("/login", handlers.LoginPage)
|
||||
|
||||
// Google oauth
|
||||
router_api.GET("/auth/login", handlers.GoogleLogin)
|
||||
router_api.GET("/auth/callback", handlers.GoogleCallback)
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
70
internal/app/service/auth_service.go
Normal file
70
internal/app/service/auth_service.go
Normal file
@ -0,0 +1,70 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
domain "github.com/haydenhargreaves/Potion/internal/domain/user"
|
||||
"github.com/haydenhargreaves/Potion/internal/infrastructure/auth"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
// NOTE: HOW THIS WORKS
|
||||
//
|
||||
// I need to store the refresh token along side the user in the DB.
|
||||
// Create a "session token" (cookie) with an expiration and store that in the DB and the session.
|
||||
// Send this session token along with all the requests (stored in the session) and when
|
||||
// authorization is needed, use it.
|
||||
// Once the expiration of MY token expires, prompt the user to log back in.
|
||||
//
|
||||
// So what is the point of the Google refresh token? Well, its not really useful right now, but if
|
||||
// we need to perform Google actions, we can use it to get more access tokens, which are needed for
|
||||
// the Google actions. Currently, I have no need for it, but it will be stored anyway.
|
||||
//
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
|
||||
// GoogleLogin generates a URL which is used to redirect the user to the Google sign in page. This
|
||||
// URL is in the Google domain and is not coupled with this application. The data from this URL will
|
||||
// be passed into a callback and work to complete the Google OAuth workflow.
|
||||
func GoogleLogin(ctx *gin.Context) string {
|
||||
url := auth.GoogleAuthConfig.AuthCodeURL(
|
||||
"randomstate",
|
||||
oauth2.AccessTypeOffline,
|
||||
oauth2.ApprovalForce,
|
||||
)
|
||||
|
||||
return url
|
||||
}
|
||||
|
||||
// GoogleCallback accepts the data from the Google login endpoint and uses it to fetch the users
|
||||
// data. The data is then used to log the user in or create an account.
|
||||
//
|
||||
// TODO: The repository implementation is not yet done.
|
||||
func GoogleCallback(ctx *gin.Context) (domain.GoogleUserInfo, error) {
|
||||
var (
|
||||
state string = ctx.Query("state")
|
||||
code string = ctx.Query("code")
|
||||
)
|
||||
|
||||
// Ensure the state matches, prevents M.I.T.M. attacks
|
||||
if state != "randomstate" {
|
||||
return domain.GoogleUserInfo{}, fmt.Errorf("States don't match, received %s", state)
|
||||
}
|
||||
|
||||
// Get access token from Google
|
||||
token, err := auth.GoogleAuthConfig.Exchange(context.Background(), code)
|
||||
if err != nil {
|
||||
return domain.GoogleUserInfo{}, fmt.Errorf("Code exchange failed: %s", err.Error())
|
||||
}
|
||||
|
||||
googleUserInfo, err := auth.GetUserData(token.AccessToken)
|
||||
if err != nil {
|
||||
return domain.GoogleUserInfo{}, err
|
||||
}
|
||||
|
||||
// TODO: Need to hit the repository to store the required data in the user table
|
||||
|
||||
// temporary
|
||||
return googleUserInfo, nil
|
||||
}
|
||||
@ -0,0 +1,12 @@
|
||||
package domain
|
||||
|
||||
// GoogleUserInfo is a data type which contains a mapping of the Google User Info API call.
|
||||
type GoogleUserInfo struct {
|
||||
Id string `json:"id"`
|
||||
Email string `json:"email"`
|
||||
Verified bool `json:"verified_email"`
|
||||
Name string `json:"name"`
|
||||
GivenName string `json:"given_name"`
|
||||
FamilyName string `json:"family_name"`
|
||||
Picture string `json:"picture"`
|
||||
}
|
||||
61
internal/infrastructure/auth/google.go
Normal file
61
internal/infrastructure/auth/google.go
Normal file
@ -0,0 +1,61 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
domain "github.com/haydenhargreaves/Potion/internal/domain/user"
|
||||
"golang.org/x/oauth2"
|
||||
"golang.org/x/oauth2/google"
|
||||
)
|
||||
|
||||
// GoogleAuthConfig is a global variable that stores the current state of the Google configuration.
|
||||
// NewGoogleConfig should be called before this value is accessed, or the application will panic
|
||||
// to prevent a null reference.
|
||||
var GoogleAuthConfig oauth2.Config
|
||||
|
||||
// NewGOogleConfig creates a google authentication configuration. The configuration is set as a
|
||||
// global variable which can be used by importing this module. The configuration is used to query
|
||||
// the google OAuth API and collect user data.
|
||||
//
|
||||
// This function should be called before any calls to the Google API are made, otherwise a null
|
||||
// value will be accessed and the call will panic.
|
||||
//
|
||||
// The global variable is returned by this function but is not needed.
|
||||
func NewGoogleConfig(redirectUrl, clientId, clientSecret string, scope []string) oauth2.Config {
|
||||
GoogleAuthConfig = oauth2.Config{
|
||||
RedirectURL: redirectUrl,
|
||||
ClientID: clientId,
|
||||
ClientSecret: clientSecret,
|
||||
Scopes: scope,
|
||||
Endpoint: google.Endpoint,
|
||||
}
|
||||
|
||||
return GoogleAuthConfig
|
||||
}
|
||||
|
||||
// GetUserData accepts a access token and calls the Google OAuth API to receive the respective user
|
||||
// data. Any errors that occur will be wrapped and returned to the caller.
|
||||
func GetUserData(accessToken string) (domain.GoogleUserInfo, error) {
|
||||
googleApiUrl := fmt.Sprintf("https://www.googleapis.com/oauth2/v2/userinfo?access_token=%s", accessToken)
|
||||
|
||||
resp, err := http.Get(googleApiUrl)
|
||||
if err != nil {
|
||||
return domain.GoogleUserInfo{}, fmt.Errorf("Failed to fetch user info from Google: %s", err.Error())
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return domain.GoogleUserInfo{}, fmt.Errorf("Failed to read body from response: %s", err.Error())
|
||||
}
|
||||
|
||||
var googleUserInfo domain.GoogleUserInfo
|
||||
if err := json.Unmarshal(body, &googleUserInfo); err != nil {
|
||||
return domain.GoogleUserInfo{}, fmt.Errorf("Failed to parser response: %s", err.Error())
|
||||
}
|
||||
|
||||
return googleUserInfo, nil
|
||||
}
|
||||
@ -1,5 +1,8 @@
|
||||
module.exports = {
|
||||
content: ["./**/*.html", "./**/*.templ", "./**/*.go",],
|
||||
content: [
|
||||
"./internal/templates/**/*.templ",
|
||||
"./internal/templates/**/*.html",
|
||||
],
|
||||
theme: { extend: {}, },
|
||||
plugins: [],
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user