Skip to content

Commit

Permalink
add oauth integration with discord
Browse files Browse the repository at this point in the history
  • Loading branch information
mycrEEpy committed Nov 22, 2020
1 parent 0153b02 commit 560655f
Show file tree
Hide file tree
Showing 5 changed files with 608 additions and 19 deletions.
139 changes: 139 additions & 0 deletions auth.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
package main

import (
"errors"
"fmt"
"net/http"
"os"

"github.com/dgrijalva/jwt-go"
"github.com/gin-gonic/gin"
"github.com/markbates/goth"
"github.com/markbates/goth/gothic"
"github.com/markbates/goth/providers/discord"
)

const (
WebTokenCookieName = "mnbcontrol_webtoken"
)

func AuthSetup(callbackURL string) {
goth.UseProviders(
discord.New(
os.Getenv("DISCORD_KEY"),
os.Getenv("DISCORD_SECRET"),
callbackURL,
discord.ScopeIdentify,
),
)
}

func AuthLogin(ctx *gin.Context) {
// try to get the user without re-authenticating
if user, err := gothic.CompleteUserAuth(ctx.Writer, ctx.Request); err == nil {
ctx.JSON(http.StatusOK, user)
return
}
gothic.BeginAuthHandler(ctx.Writer, ctx.Request)
}

func AuthCallback(ctx *gin.Context) {
user, err := gothic.CompleteUserAuth(ctx.Writer, ctx.Request)
if err != nil {
ctx.AbortWithStatusJSON(http.StatusInternalServerError, APIError{
fmt.Errorf("auth callback failed: %s", err).Error(),
})
return
}

token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"userID": user.UserID,
})
tokenString, err := token.SignedString([]byte(os.Getenv("JWT_SIGNING_KEY")))
if err != nil {
ctx.AbortWithStatusJSON(http.StatusInternalServerError, APIError{
fmt.Errorf("failed to sign token: %s", err).Error(),
})
return
}
ctx.SetCookie(WebTokenCookieName, tokenString, 86400, "/", "", false, true)
ctx.Status(http.StatusOK)
}

func AuthLogout(ctx *gin.Context) {
err := gothic.Logout(ctx.Writer, ctx.Request)
if err != nil {
ctx.AbortWithStatusJSON(http.StatusInternalServerError, APIError{
fmt.Errorf("failed to logout: %s", err).Error(),
})
return
}
ctx.Redirect(http.StatusTemporaryRedirect, "/")
}

func (control *Control) Authorize() gin.HandlerFunc {
return func(ctx *gin.Context) {
authCookie, err := ctx.Request.Cookie(WebTokenCookieName)
if err != nil {
ctx.AbortWithStatusJSON(http.StatusUnauthorized, APIError{
errors.New("unauthorized: missing auth").Error(),
})
return
}

token, err := jwt.Parse(authCookie.Value, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
return []byte(os.Getenv("JWT_SIGNING_KEY")), nil
})
if err != nil {
ctx.AbortWithStatusJSON(http.StatusUnauthorized, APIError{
errors.New("unauthorized: failed to parse token").Error(),
})
return
}
if !token.Valid {
ctx.AbortWithStatusJSON(http.StatusUnauthorized, APIError{
errors.New("unauthorized: token is invalid").Error(),
})
return
}
if err = token.Claims.Valid(); err != nil {
ctx.AbortWithStatusJSON(http.StatusUnauthorized, APIError{
errors.New("unauthorized: token claims are invalid").Error(),
})
return
}
claims, ok := token.Claims.(jwt.MapClaims)
if !ok {
ctx.AbortWithStatusJSON(http.StatusUnauthorized, APIError{
errors.New("unauthorized: unexpected token claims").Error(),
})
return
}

member, err := control.discordSession.GuildMember(*discordGuildID, fmt.Sprint(claims["userID"]))
if err != nil {
ctx.AbortWithStatusJSON(http.StatusForbidden, APIError{
fmt.Errorf("forbidden: no guild membership: %s", err).Error(),
})
return
}
var hasAccess bool
for _, role := range member.Roles {
if role == *discordRoleID {
hasAccess = true
break
}
}
if !hasAccess {
ctx.AbortWithStatusJSON(http.StatusForbidden, APIError{
fmt.Errorf("forbidden: permission check failed: %s", err).Error(),
})
return
}

ctx.Next()
}
}
71 changes: 62 additions & 9 deletions control.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,13 @@ import (
"fmt"
"net/http"
"os"
"os/signal"
"strconv"
"sync"
"syscall"
"time"

"github.com/bwmarrin/discordgo"
"github.com/gin-gonic/gin"
"github.com/hetznercloud/hcloud-go/hcloud"
log "github.com/sirupsen/logrus"
Expand All @@ -24,9 +28,10 @@ const (
)

type Control struct {
Config *ControlConfig
api *http.Server
hclient *hcloud.Client
Config *ControlConfig
api *http.Server
hclient *hcloud.Client
discordSession *discordgo.Session
}

type ControlConfig struct {
Expand All @@ -37,7 +42,7 @@ type ControlConfig struct {
}

type APIError struct {
Error string
Error string `json:"error"`
}

type CreateNewServerRequest struct {
Expand All @@ -63,6 +68,12 @@ func NewControl(config *ControlConfig) (*Control, error) {
}
control.hclient = hcloud.NewClient(hcloud.WithToken(token), hcloud.WithPollInterval(1*time.Second))

var err error
control.discordSession, err = discordgo.New("Bot " + os.Getenv("DISCORD_BOT_TOKEN"))
if err != nil {
return nil, fmt.Errorf("failed to create discord session: %s", err)
}

gin.SetMode(gin.ReleaseMode)
engine := gin.New()
engine.Use(gin.Recovery(), gin.Logger())
Expand All @@ -71,17 +82,59 @@ func NewControl(config *ControlConfig) (*Control, error) {
Handler: engine,
}

engine.GET("/server", control.ListServers)
engine.POST("/server", control.NewServer)
engine.POST("/server/:name/_start", control.StartServer)
engine.DELETE("/server/:name", control.TerminateServer)
apiV1 := engine.Group("/api/v1")
apiV1.Use(control.Authorize())

apiServer := apiV1.Group("/server")
apiServer.GET("/", control.ListServers)
apiServer.POST("/", control.NewServer)
apiServer.POST("/:name/_start", control.StartServer)
apiServer.DELETE("/:name", control.TerminateServer)

auth := engine.Group("/auth")
auth.GET("/", AuthLogin)
auth.GET("/callback", AuthCallback)
auth.GET("/logout", AuthLogout)

//engine.Static("/", "./web")

return control, nil
}

func (control *Control) Run() error {
log.Info("control is warming up")
err := control.discordSession.Open()
if err != nil {
return fmt.Errorf("failed to open discord session: %s", err)
}

shutdownChan := make(chan os.Signal)
signal.Notify(shutdownChan, syscall.SIGINT, syscall.SIGTERM)
shutdownWG := &sync.WaitGroup{}
shutdownWG.Add(1)
go control.waitForShutdown(shutdownChan, shutdownWG)

log.Infof("control api listening on %s", control.api.Addr)
return control.api.ListenAndServe()
if err = control.api.ListenAndServe(); err != http.ErrServerClosed {
return fmt.Errorf("control api server failed: %s", err)
}
shutdownWG.Wait()
return nil
}

func (control *Control) waitForShutdown(shutdownChan <-chan os.Signal, shutdownWG *sync.WaitGroup) {
<-shutdownChan
err := control.discordSession.Close()
if err != nil {
log.Errorf("failed to close discord session: %s", err)
}
ctx, _ := context.WithTimeout(context.Background(), 10*time.Second)
err = control.api.Shutdown(ctx)
if err != nil {
log.Errorf("failed to shutdown api server: %s", err)
}
log.Info("control shutdown complete, see you next time!")
shutdownWG.Done()
}

func (control *Control) ListServers(ctx *gin.Context) {
Expand Down
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ module github.com/mycreepy/mnbcontrol
go 1.15

require (
github.com/bwmarrin/discordgo v0.22.0
github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/gin-gonic/gin v1.6.3
github.com/hetznercloud/hcloud-go v1.23.1
github.com/markbates/goth v1.66.0
github.com/sirupsen/logrus v1.7.0
)
Loading

0 comments on commit 560655f

Please sign in to comment.