make oauth as a service

This commit is contained in:
Kenta420-Poom 2023-09-21 14:21:14 +07:00
parent da4110a47b
commit dbc741ccf6
5 changed files with 160 additions and 97 deletions

View file

@ -2,14 +2,7 @@
<table class="w-full text-sm text-left text-gray-500">
<caption
class="p-5 text-lg font-semibold text-left text-gray-900 bg-primary"
>
Our products
<p class="mt-1 text-sm font-normal text-gray-200">
Browse a list of Flowbite products designed to help you work and play,
stay organized, get answers, keep in touch, grow your business, and
more.
</p>
</caption>
></caption>
<thead class="text-xs text-gray-700 uppercase bg-secondary">
<tr>
<th scope="col" class="px-6 py-3">Product name</th>

7
server/models/user.go Normal file
View file

@ -0,0 +1,7 @@
package models
type User struct {
Name string `json:"name"`
Email string `json:"email"`
Picture string `json:"picture"`
}

View file

@ -6,44 +6,20 @@ import (
"encoding/json"
"net/http"
"net/url"
"os"
"recipe-manager/config"
"recipe-manager/services/oauth"
"github.com/go-chi/chi/v5"
"golang.org/x/oauth2"
)
type AuthRouter struct {
cfg *config.ServerConfig
gConfig *oauth2.Config
nonce map[string]map[string]string
cfg *config.ServerConfig
oauth oauth.OAuthService
}
func NewAuthRouter(cfg *config.ServerConfig) *AuthRouter {
file, err := os.Open("client_secret.json")
if err != nil {
panic(err)
}
defer file.Close()
var clientSecret map[string]interface{}
json.NewDecoder(file).Decode(&clientSecret)
return &AuthRouter{
cfg: cfg,
gConfig: &oauth2.Config{
ClientID: clientSecret["web"].(map[string]interface{})["client_id"].(string),
ClientSecret: clientSecret["web"].(map[string]interface{})["client_secret"].(string),
RedirectURL: cfg.ServerDomain + "/auth/google/callback",
Scopes: []string{"https://www.googleapis.com/auth/userinfo.email", "https://www.googleapis.com/auth/userinfo.profile"},
Endpoint: oauth2.Endpoint{
AuthURL: clientSecret["web"].(map[string]interface{})["auth_uri"].(string),
TokenURL: clientSecret["web"].(map[string]interface{})["token_uri"].(string),
},
},
nonce: make(map[string]map[string]string),
}
func NewAuthRouter(cfg *config.ServerConfig, oauth oauth.OAuthService) *AuthRouter {
return &AuthRouter{cfg, oauth}
}
func (ar *AuthRouter) Route(r chi.Router) {
@ -61,9 +37,7 @@ func (ar *AuthRouter) Route(r chi.Router) {
stateMap["redirect_to"] = r.URL.Query().Get("redirect_to")
}
ar.nonce[state] = stateMap
url := ar.gConfig.AuthCodeURL(state, oauth2.SetAuthURLParam("hd", "forth.co.th"), oauth2.SetAuthURLParam("include_granted_scopes", "true"), oauth2.AccessTypeOffline)
url := ar.oauth.AuthURL(state, stateMap)
http.Redirect(w, r, url, http.StatusTemporaryRedirect)
})
@ -71,11 +45,12 @@ func (ar *AuthRouter) Route(r chi.Router) {
// check state
var redirect_to string
if r.URL.Query().Get("state") == "" {
state := r.URL.Query().Get("state")
if state == "" {
http.Error(w, "State not found", http.StatusBadRequest)
return
} else {
val, ok := ar.nonce[r.URL.Query().Get("state")]
val, ok := ar.oauth.GetState(state)
if !ok {
http.Error(w, "Invalid state", http.StatusBadRequest)
@ -84,34 +59,30 @@ func (ar *AuthRouter) Route(r chi.Router) {
redirect_to = val["redirect_to"]
// remove state from nonce
delete(ar.nonce, r.URL.Query().Get("state"))
ar.oauth.RemoveState(state)
}
// exchange code for token
token, err := ar.gConfig.Exchange(r.Context(), r.FormValue("code"))
token, err := ar.oauth.Exchange(r.Context(), r.FormValue("code"))
if err != nil {
http.Error(w, "Error exchanging code for token", http.StatusBadRequest)
return
}
// get user info
client := ar.gConfig.Client(r.Context(), token)
resp, err := client.Get("https://www.googleapis.com/oauth2/v3/userinfo")
user, err := ar.oauth.GetUserInfo(r.Context(), token)
if err != nil {
http.Error(w, "Error getting user info", http.StatusBadRequest)
return
}
defer resp.Body.Close()
var userInfo map[string]interface{}
json.NewDecoder(resp.Body).Decode(&userInfo)
value := url.Values{
"name": {user.Name},
"email": {user.Email},
"picture": {user.Picture},
}
value := url.Values{}
value.Add("email", userInfo["email"].(string))
value.Add("name", userInfo["name"].(string))
value.Add("picture", userInfo["picture"].(string))
if redirect_to != "" {
value.Add("redirect_to", redirect_to)
}
@ -132,7 +103,7 @@ func (ar *AuthRouter) Route(r chi.Router) {
}
// exchange refresh token for new token
token, err := ar.gConfig.TokenSource(r.Context(), &oauth2.Token{RefreshToken: refreshToken}).Token()
token, err := ar.oauth.RefreshToken(r.Context(), &oauth2.Token{RefreshToken: refreshToken})
if err != nil {
http.Error(w, "Error exchanging refresh token for token", http.StatusBadRequest)
return
@ -177,35 +148,17 @@ func (ar *AuthRouter) Route(r chi.Router) {
}
// get user info
client := ar.gConfig.Client(r.Context(), token)
resp, err := client.Get("https://www.googleapis.com/oauth2/v3/userinfo")
user, err := ar.oauth.GetUserInfo(r.Context(), token)
if err != nil {
http.Error(w, "Error getting user info", http.StatusBadRequest)
return
}
defer resp.Body.Close()
var userInfo map[string]interface{}
if err = json.NewDecoder(resp.Body).Decode(&userInfo); err != nil {
http.Error(w, "Error getting user info", http.StatusBadRequest)
return
}
if userInfo["error"] != nil {
w.Header().Add("Content-Type", "application/json")
w.WriteHeader(http.StatusUnauthorized)
json.NewEncoder(w).Encode(userInfo)
return
}
// return user info
w.Header().Add("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"user": map[string]string{
"email": userInfo["email"].(string),
"username": userInfo["name"].(string),
"image": userInfo["picture"].(string),
},
"user": user,
})
})

View file

@ -7,18 +7,19 @@ import (
"io"
"log"
"net/http"
"net/url"
"os"
"os/exec"
"path/filepath"
"recipe-manager/config"
"recipe-manager/data"
"recipe-manager/routers"
"recipe-manager/services/oauth"
"strings"
"github.com/go-chi/chi/v5"
"github.com/go-chi/cors"
"github.com/spf13/viper"
"golang.org/x/oauth2"
)
func loadConfig(path string) (*config.ServerConfig, error) {
@ -46,6 +47,7 @@ type Server struct {
server *http.Server
data *data.Data
cfg *config.ServerConfig
oauth oauth.OAuthService
}
func NewServer() *Server {
@ -60,6 +62,7 @@ func NewServer() *Server {
server: &http.Server{Addr: fmt.Sprintf(":%d", serverCfg.ServerPort)},
data: data.NewData(),
cfg: serverCfg,
oauth: oauth.NewOAuthService(serverCfg),
}
}
@ -85,7 +88,7 @@ func (s *Server) createHandler() {
// Auth Router
r.Group(func(r chi.Router) {
ar := routers.NewAuthRouter(s.cfg)
ar := routers.NewAuthRouter(s.cfg, s.oauth)
ar.Route(r)
})
@ -94,31 +97,33 @@ func (s *Server) createHandler() {
r.Use(func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token := &oauth2.Token{}
cookie, err := r.Cookie("access_token")
if err != nil {
w.WriteHeader(http.StatusUnauthorized)
return
if cookie, err := r.Cookie("access_token"); err == nil {
token.AccessToken = cookie.Value
}
// verify token by request to google api
res, err := http.Get("https://www.googleapis.com/oauth2/v3/tokeninfo?access_token=" + url.QueryEscape(cookie.Value))
user, err := s.oauth.GetUserInfo(r.Context(), token)
if err != nil {
w.WriteHeader(http.StatusUnauthorized)
return
// if have refresh token, set refresh token to token
if cookie, err := r.Cookie("refresh_token"); err == nil {
token.RefreshToken = cookie.Value
}
newToken, err := s.oauth.RefreshToken(r.Context(), token)
if err != nil {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
// set new token to cookie
w.Header().Add("set-cookie", fmt.Sprintf("access_token=%s; Path=/; HttpOnly; SameSite=None; Secure; Max-Age=3600", newToken.AccessToken))
}
defer res.Body.Close()
log.Println("res: ", res)
var tokenInfo map[string]interface{}
json.NewDecoder(res.Body).Decode(&tokenInfo)
context := context.WithValue(r.Context(), "user", tokenInfo)
log.Println("decode res: ", tokenInfo["email"])
next.ServeHTTP(w, r.WithContext(context))
ctx := context.WithValue(r.Context(), "user", user)
next.ServeHTTP(w, r.WithContext(ctx))
})
})

View file

@ -0,0 +1,105 @@
package oauth
import (
"context"
"encoding/json"
"errors"
"os"
"recipe-manager/config"
"recipe-manager/models"
"golang.org/x/oauth2"
)
type OAuthService interface {
AuthURL(state string, stateMap map[string]string) string
GetState(state string) (map[string]string, bool)
RemoveState(state string)
Exchange(ctx context.Context, code string) (*oauth2.Token, error)
GetUserInfo(ctx context.Context, token *oauth2.Token) (*models.User, error)
RefreshToken(ctx context.Context, token *oauth2.Token) (*oauth2.Token, error)
}
type oauthService struct {
cfg *config.ServerConfig
gConfig *oauth2.Config
nonce map[string]map[string]string
}
func NewOAuthService(cfg *config.ServerConfig) OAuthService {
file, err := os.Open("client_secret.json")
if err != nil {
panic(err)
}
defer file.Close()
var clientSecret map[string]interface{}
json.NewDecoder(file).Decode(&clientSecret)
return &oauthService{
cfg: cfg,
gConfig: &oauth2.Config{
ClientID: clientSecret["web"].(map[string]interface{})["client_id"].(string),
ClientSecret: clientSecret["web"].(map[string]interface{})["client_secret"].(string),
RedirectURL: cfg.ServerDomain + "/auth/google/callback",
Scopes: []string{"https://www.googleapis.com/auth/userinfo.email", "https://www.googleapis.com/auth/userinfo.profile"},
Endpoint: oauth2.Endpoint{
AuthURL: clientSecret["web"].(map[string]interface{})["auth_uri"].(string),
TokenURL: clientSecret["web"].(map[string]interface{})["token_uri"].(string),
},
},
nonce: make(map[string]map[string]string),
}
}
func (o *oauthService) AuthURL(state string, stateMap map[string]string) string {
if stateMap != nil {
o.nonce[state] = stateMap
} else {
o.nonce[state] = make(map[string]string)
}
return o.gConfig.AuthCodeURL(state, oauth2.SetAuthURLParam("hd", "forth.co.th"), oauth2.SetAuthURLParam("include_granted_scopes", "true"), oauth2.AccessTypeOffline)
}
func (o *oauthService) GetState(state string) (map[string]string, bool) {
val, ok := o.nonce[state]
return val, ok
}
func (o *oauthService) RemoveState(state string) {
delete(o.nonce, state)
}
func (o *oauthService) Exchange(ctx context.Context, code string) (*oauth2.Token, error) {
return o.gConfig.Exchange(ctx, code)
}
func (o *oauthService) GetUserInfo(ctx context.Context, token *oauth2.Token) (*models.User, error) {
client := o.gConfig.Client(ctx, token)
resp, err := client.Get("https://www.googleapis.com/oauth2/v3/userinfo")
if err != nil {
return nil, err
}
defer resp.Body.Close()
var userInfo map[string]interface{}
json.NewDecoder(resp.Body).Decode(&userInfo)
if userInfo["error"] != nil {
return nil, errors.New("Error getting user info")
}
return &models.User{
Name: userInfo["name"].(string),
Email: userInfo["email"].(string),
Picture: userInfo["picture"].(string),
}, nil
}
func (o *oauthService) RefreshToken(ctx context.Context, token *oauth2.Token) (*oauth2.Token, error) {
return o.gConfig.TokenSource(ctx, token).Token()
}