Browse Source

Refactored and added more oAuth stuff

dependabot/npm_and_yarn/web/prismjs-1.21.0
Schmitt, Max 8 years ago
parent
commit
dd338d8138
  1. 3
      README.md
  2. 59
      config/config.go
  3. 139
      handlers/auth.go
  4. 195
      handlers/handlers.go
  5. 73
      handlers/public.go
  6. 23
      handlers/utils.go
  7. 10
      main.go
  8. 8
      static/src/App/App.js
  9. 23
      templates/token.tmpl

3
README.md

@ -141,3 +141,6 @@ Next changes sorted by priority
- [ ] Create Makefile for building everything - [ ] Create Makefile for building everything
- [ ] Test docker-compose installation - [ ] Test docker-compose installation
- [ ] Provide image on the docker hub - [ ] Provide image on the docker hub
https://console.cloud.google.com/

59
config/config.go

@ -25,7 +25,9 @@ type Store struct {
// Handlers contains the needed fields for the Handlers package // Handlers contains the needed fields for the Handlers package
type Handlers struct { type Handlers struct {
ListenAddr string ListenAddr string
BaseURL string
EnableGinDebugMode bool EnableGinDebugMode bool
Secret []byte
OAuth struct { OAuth struct {
Google struct { Google struct {
ClientID string ClientID string
@ -34,20 +36,61 @@ type Handlers struct {
} }
} }
// config holds the temporary loaded data for the
// singelton Get() method
var config *Configuration
var configPath string
// Get returns the configuration from a given file // Get returns the configuration from a given file
func Get() (*Configuration, error) { func Get() *Configuration {
var config *Configuration return config
ex, err := os.Executable() }
// Preload loads the configuration file into the memory for further usage
func Preload() error {
var err error
configPath, err = getConfigPath()
if err != nil { if err != nil {
return nil, errors.Wrap(err, "could not get executable path") return errors.Wrap(err, "could not get configuration path")
} }
file, err := ioutil.ReadFile(filepath.Join(filepath.Dir(ex), "config.json")) err = updateConfig()
if err != nil { if err != nil {
return nil, errors.Wrap(err, "could not read configuration file") return errors.Wrap(err, "could not update config")
}
return nil
}
func updateConfig() error {
file, err := ioutil.ReadFile(configPath)
if err != nil {
return errors.Wrap(err, "could not read configuration file")
} }
err = json.Unmarshal(file, &config) err = json.Unmarshal(file, &config)
if err != nil { if err != nil {
return nil, errors.Wrap(err, "could not unmarshal configuration file") return errors.Wrap(err, "could not unmarshal configuration file")
}
return nil
}
func getConfigPath() (string, error) {
ex, err := os.Executable()
if err != nil {
return "", errors.Wrap(err, "could not get executable path")
}
return filepath.Join(filepath.Dir(ex), "config.json"), nil
}
// Set replaces the current configuration with the given one
func Set(conf *Configuration) error {
data, err := json.MarshalIndent(conf, "", " ")
if err != nil {
return err
}
err = ioutil.WriteFile(configPath, data, 0644)
if err != nil {
return err
} }
return config, nil config = conf
return nil
} }

139
handlers/auth.go

@ -0,0 +1,139 @@
package handlers
import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"time"
jwt "github.com/dgrijalva/jwt-go"
"github.com/gin-gonic/contrib/sessions"
"github.com/gin-gonic/gin"
"golang.org/x/oauth2"
"golang.org/x/oauth2/google"
)
type jwtClaims struct {
jwt.StandardClaims
OAuthProvider string
OAuthID string
}
type oAuthUser struct {
Sub string `json:"sub"`
Name string `json:"name"`
GivenName string `json:"given_name"`
FamilyName string `json:"family_name"`
Profile string `json:"profile"`
Picture string `json:"picture"`
Email string `json:"email"`
EmailVerified bool `json:"email_verified"`
Gender string `json:"gender"`
Hd string `json:"hd"`
}
func (h *Handler) initOAuth() {
store := sessions.NewCookieStore([]byte("secret"))
h.oAuthConf = &oauth2.Config{
ClientID: h.config.OAuth.Google.ClientID,
ClientSecret: h.config.OAuth.Google.ClientSecret,
RedirectURL: h.config.BaseURL + "/api/v1/callback",
Scopes: []string{
"https://www.googleapis.com/auth/userinfo.email",
},
Endpoint: google.Endpoint,
}
h.engine.Use(sessions.Sessions("backend", store))
h.engine.GET("/api/v1/login", h.handleGoogleLogin)
h.engine.GET("/api/v1/callback", h.handleGoogleCallback)
h.engine.POST("/api/v1/check", h.handleGoogleCheck)
}
func (h *Handler) handleGoogleLogin(c *gin.Context) {
state := h.randToken()
session := sessions.Default(c)
session.Set("state", state)
session.Save()
c.Redirect(http.StatusTemporaryRedirect, h.oAuthConf.AuthCodeURL(state))
}
func (h *Handler) handleGoogleCheck(c *gin.Context) {
var data struct {
Token string `binding:"required"`
}
err := c.ShouldBind(&data)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// to the callback, providing flexibility.
token, err := jwt.Parse(data.Token, func(token *jwt.Token) (interface{}, error) {
// Don't forget to validate the alg is what you expect:
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])
}
return h.config.Secret, nil
})
if claims, ok := token.Claims.(jwtClaims); ok && token.Valid {
fmt.Println(claims.OAuthID, claims.OAuthProvider)
c.JSON(http.StatusOK, claims)
} else {
c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
}
}
func (h *Handler) handleGoogleCallback(c *gin.Context) {
session := sessions.Default(c)
retrievedState := session.Get("state")
if retrievedState != c.Query("state") {
c.JSON(http.StatusUnauthorized, gin.H{"error": fmt.Errorf("Invalid session state: %s", retrievedState)})
return
}
oAuthToken, err := h.oAuthConf.Exchange(oauth2.NoContext, c.Query("code"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
client := h.oAuthConf.Client(oauth2.NoContext, oAuthToken)
userinfo, err := client.Get("https://www.googleapis.com/oauth2/v3/userinfo")
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
defer userinfo.Body.Close()
data, err := ioutil.ReadAll(userinfo.Body)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Could not read body: %v", err)})
return
}
var user oAuthUser
err = json.Unmarshal(data, &user)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Decoding userinfo failed: %v", err)})
return
}
// you would like it to contain.
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwtClaims{
jwt.StandardClaims{
ExpiresAt: time.Now().Add(time.Minute * 10).Unix(),
},
"google",
user.Sub,
})
// Sign and get the complete encoded token as a string using the secret
tokenString, err := token.SignedString(h.config.Secret)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("could not sign token: %v", err)})
return
}
c.HTML(http.StatusOK, "token.tmpl", gin.H{
"token": tokenString,
})
}

195
handlers/handlers.go

@ -3,18 +3,12 @@ package handlers
import ( import (
"crypto/rand" "crypto/rand"
"encoding/base64"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"github.com/gin-gonic/contrib/sessions"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/maxibanki/golang-url-shortener/config" "github.com/maxibanki/golang-url-shortener/config"
"github.com/maxibanki/golang-url-shortener/store" "github.com/maxibanki/golang-url-shortener/store"
"github.com/pkg/errors"
"golang.org/x/oauth2" "golang.org/x/oauth2"
"golang.org/x/oauth2/google"
) )
// Handler holds the funcs and attributes for the // Handler holds the funcs and attributes for the
@ -26,189 +20,48 @@ type Handler struct {
oAuthConf *oauth2.Config oAuthConf *oauth2.Config
} }
// URLUtil is used to help in- and outgoing requests for json
// un- and marshalling
type URLUtil struct {
URL string `binding:"required"`
}
type oAuthUser struct {
Sub string `json:"sub"`
Name string `json:"name"`
GivenName string `json:"given_name"`
FamilyName string `json:"family_name"`
Profile string `json:"profile"`
Picture string `json:"picture"`
Email string `json:"email"`
EmailVerified bool `json:"email_verified"`
Gender string `json:"gender"`
Hd string `json:"hd"`
}
// New initializes the http handlers // New initializes the http handlers
func New(handlerConfig config.Handlers, store store.Store) *Handler { func New(handlerConfig config.Handlers, store store.Store) (*Handler, error) {
h := &Handler{ h := &Handler{
config: handlerConfig, config: handlerConfig,
store: store, store: store,
engine: gin.Default(), engine: gin.Default(),
} }
h.setHandlers() h.setHandlers()
h.initOAuth() err := h.checkIfSecretExist()
return h
}
func (h *Handler) setHandlers() {
if !h.config.EnableGinDebugMode {
gin.SetMode(gin.ReleaseMode)
}
h.engine.POST("/api/v1/create", h.handleCreate)
h.engine.POST("/api/v1/info", h.handleInfo)
// h.engine.Static("/static", "static/src")
h.engine.NoRoute(h.handleAccess)
}
func (h *Handler) initOAuth() {
store := sessions.NewCookieStore([]byte("secret"))
h.oAuthConf = &oauth2.Config{
ClientID: h.config.OAuth.Google.ClientID,
ClientSecret: h.config.OAuth.Google.ClientSecret,
RedirectURL: "http://127.0.0.1:3000/api/v1/auth/",
Scopes: []string{
"https://www.googleapis.com/auth/userinfo.email",
},
Endpoint: google.Endpoint,
}
h.engine.Use(sessions.Sessions("goquestsession", store))
h.engine.GET("/api/v1/login", h.handleGoogleLogin)
private := h.engine.Group("/api/v1/auth")
private.Use(h.handleGoogleAuth)
private.GET("/", h.handleGoogleCallback)
private.GET("/api", func(c *gin.Context) {
c.JSON(200, gin.H{"message": "Hello from private for groups"})
})
}
func (h *Handler) randToken() string {
b := make([]byte, 32)
rand.Read(b)
return base64.StdEncoding.EncodeToString(b)
}
func (h *Handler) handleGoogleAuth(c *gin.Context) {
session := sessions.Default(c)
retrievedState := session.Get("state")
if retrievedState != c.Query("state") {
c.JSON(http.StatusUnauthorized, gin.H{"error": fmt.Errorf("Invalid session state: %s", retrievedState)})
return
}
token, err := h.oAuthConf.Exchange(oauth2.NoContext, c.Query("code"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
client := h.oAuthConf.Client(oauth2.NoContext, token)
userinfo, err := client.Get("https://www.googleapis.com/oauth2/v3/userinfo")
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
defer userinfo.Body.Close()
data, err := ioutil.ReadAll(userinfo.Body)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Could not read body: %v", err)})
return
}
var user oAuthUser
err = json.Unmarshal(data, &user)
if err != nil { if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Decoding userinfo failed: %v", err)}) return nil, errors.Wrap(err, "could not check if secret exist")
return
} }
c.Set("user", user) h.initOAuth()
} return h, nil
func (h *Handler) handleGoogleLogin(c *gin.Context) {
state := h.randToken()
session := sessions.Default(c)
session.Set("state", state)
session.Save()
c.Redirect(http.StatusTemporaryRedirect, h.oAuthConf.AuthCodeURL(state))
}
func (h *Handler) handleGoogleCallback(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"Hello": "from private", "user": c.MustGet("user").(oAuthUser)})
} }
// handleCreate handles requests to create an entry func (h *Handler) checkIfSecretExist() error {
func (h *Handler) handleCreate(c *gin.Context) { conf := config.Get()
var data URLUtil if conf.Handlers.Secret == nil {
err := c.ShouldBind(&data) b := make([]byte, 128)
if err != nil { _, err := rand.Read(b)
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
id, err := h.store.CreateEntry(data.URL, c.ClientIP())
if err != nil { if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return err
return
}
data.URL = h.getSchemaAndHost(c) + "/" + id
c.JSON(http.StatusOK, data)
}
func (h *Handler) getSchemaAndHost(c *gin.Context) string {
protocol := "http"
if c.Request.TLS != nil {
protocol = "https"
}
return fmt.Sprintf("%s://%s", protocol, c.Request.Host)
}
// handleInfo is the http handler for getting the infos
func (h *Handler) handleInfo(c *gin.Context) {
var data struct {
ID string `binding:"required"`
} }
err := c.ShouldBind(&data) conf.Handlers.Secret = b
err = config.Set(conf)
if err != nil { if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return err
return
} }
entry, err := h.store.GetEntryByID(data.ID)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
return
} }
entry.RemoteAddr = "" return nil
c.JSON(http.StatusOK, entry)
} }
// handleAccess handles the access for incoming requests func (h *Handler) setHandlers() {
func (h *Handler) handleAccess(c *gin.Context) { if !h.config.EnableGinDebugMode {
var id string gin.SetMode(gin.ReleaseMode)
if len(c.Request.URL.Path) > 1 {
id = c.Request.URL.Path[1:]
}
entry, err := h.store.GetEntryByID(id)
if err == store.ErrIDIsEmpty || err == store.ErrNoEntryFound {
return // return normal 404 error if such an error occurs
} else if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
return
}
err = h.store.IncreaseVisitCounter(id)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
} }
c.Redirect(http.StatusTemporaryRedirect, entry.URL) h.engine.POST("/api/v1/create", h.handleCreate)
h.engine.POST("/api/v1/info", h.handleInfo)
// h.engine.Static("/static", "static/src")
h.engine.NoRoute(h.handleAccess)
h.engine.LoadHTMLGlob("templates/*")
} }
// Listen starts the http server // Listen starts the http server

73
handlers/public.go

@ -0,0 +1,73 @@
package handlers
import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/maxibanki/golang-url-shortener/store"
)
// URLUtil is used to help in- and outgoing requests for json
// un- and marshalling
type URLUtil struct {
URL string `binding:"required"`
}
// handleInfo is the http handler for getting the infos
func (h *Handler) handleInfo(c *gin.Context) {
var data struct {
ID string `binding:"required"`
}
err := c.ShouldBind(&data)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
entry, err := h.store.GetEntryByID(data.ID)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
return
}
entry.RemoteAddr = ""
c.JSON(http.StatusOK, entry)
}
// handleAccess handles the access for incoming requests
func (h *Handler) handleAccess(c *gin.Context) {
var id string
if len(c.Request.URL.Path) > 1 {
id = c.Request.URL.Path[1:]
}
entry, err := h.store.GetEntryByID(id)
if err == store.ErrIDIsEmpty || err == store.ErrNoEntryFound {
return // return normal 404 error if such an error occurs
} else if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
return
}
err = h.store.IncreaseVisitCounter(id)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.Redirect(http.StatusTemporaryRedirect, entry.URL)
}
// handleCreate handles requests to create an entry
func (h *Handler) handleCreate(c *gin.Context) {
var data URLUtil
err := c.ShouldBind(&data)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
id, err := h.store.CreateEntry(data.URL, c.ClientIP())
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
data.URL = h.getSchemaAndHost(c) + "/" + id
c.JSON(http.StatusOK, data)
}

23
handlers/utils.go

@ -0,0 +1,23 @@
package handlers
import (
"crypto/rand"
"encoding/base64"
"fmt"
"github.com/gin-gonic/gin"
)
func (h *Handler) getSchemaAndHost(c *gin.Context) string {
protocol := "http"
if c.Request.TLS != nil {
protocol = "https"
}
return fmt.Sprintf("%s://%s", protocol, c.Request.Host)
}
func (h *Handler) randToken() string {
b := make([]byte, 32)
rand.Read(b)
return base64.StdEncoding.EncodeToString(b)
}

10
main.go

@ -24,15 +24,19 @@ func main() {
} }
func initShortener() (func(), error) { func initShortener() (func(), error) {
config, err := config.Get() err := config.Preload()
if err != nil { if err != nil {
return nil, errors.Wrap(err, "could not get config") return nil, errors.Wrap(err, "could not get config")
} }
store, err := store.New(config.Store) conf := config.Get()
store, err := store.New(conf.Store)
if err != nil { if err != nil {
return nil, errors.Wrap(err, "could not create store") return nil, errors.Wrap(err, "could not create store")
} }
handler := handlers.New(config.Handlers, *store) handler, err := handlers.New(conf.Handlers, *store)
if err != nil {
return nil, errors.Wrap(err, "could not create handlers")
}
go func() { go func() {
err := handler.Listen() err := handler.Listen()
if err != nil { if err != nil {

8
static/src/App/App.js

@ -25,13 +25,19 @@ class ContainerExampleContainer extends Component {
this.setState({ open: true }) this.setState({ open: true })
} }
onAuthCallback = data => {
window.removeEventListener('onAuthCallback', this.onAuthCallback);
var token = data.detail.token;
}
onAuthClick = () => { onAuthClick = () => {
console.log("onAuthClick") console.log("onAuthClick")
window.addEventListener('onAuthCallback', this.onAuthCallback, false);
var wwidth = 400, var wwidth = 400,
wHeight = 500; wHeight = 500;
var wLeft = (window.screen.width / 2) - (wwidth / 2); var wLeft = (window.screen.width / 2) - (wwidth / 2);
var wTop = (window.screen.height / 2) - (wHeight / 2); var wTop = (window.screen.height / 2) - (wHeight / 2);
window.open("/api/v1/login", "", `width=${wwidth}, height=${wHeight}, top=${wTop}, left=${wLeft}`) window.open("/api/v1/login", "", `width=${wwidth}, height=${wHeight}, top=${wTop}, left=${wLeft}, menubar=0, toolbar=0`)
} }
render() { render() {

23
templates/token.tmpl

@ -0,0 +1,23 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>You will be redirected</title>
<script>
window.opener.dispatchEvent(new CustomEvent('onAuthCallback', {
detail: {
token: {{ .token }}
}
}));
window.close();
</script>
</head>
<body>
</body>
</html>
Loading…
Cancel
Save