mirror of
https://github.com/roleypoly/roleypoly.git
synced 2025-04-24 19:39:11 +00:00
add basic oauth bounces
This commit is contained in:
parent
bebfc862e8
commit
a23184efd2
13 changed files with 262 additions and 1 deletions
2
go.mod
2
go.mod
|
@ -9,6 +9,8 @@ require (
|
|||
github.com/gorilla/websocket v1.4.2 // indirect
|
||||
github.com/joho/godotenv v1.3.0
|
||||
github.com/lampjaw/discordclient v0.0.0-20200923011548-6558fc9e89df
|
||||
github.com/onsi/gomega v1.8.1
|
||||
github.com/segmentio/ksuid v1.0.3
|
||||
golang.org/x/crypto v0.0.0-20201117144127-c1f2f97bffc9 // indirect
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 // indirect
|
||||
k8s.io/klog v1.0.0
|
||||
|
|
2
go.sum
2
go.sum
|
@ -150,6 +150,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
|
|||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/segmentio/ksuid v1.0.3 h1:FoResxvleQwYiPAVKe1tMUlEirodZqlqglIuFsdDntY=
|
||||
github.com/segmentio/ksuid v1.0.3/go.mod h1:/XUiZBD3kVx5SmUOl55voK5yeAbBNNIed+2O73XgrPE=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
|
||||
|
|
3
hack/functions-local/README.md
Normal file
3
hack/functions-local/README.md
Normal file
|
@ -0,0 +1,3 @@
|
|||
# FaaS local dev shim
|
||||
|
||||
This tool provides a thin layer to emulate Google Cloud Functions for Roleypoly backend components, as well as a weak UI to understand the app layout. This is not used in production.
|
|
@ -8,6 +8,9 @@ import (
|
|||
"os"
|
||||
|
||||
"github.com/GoogleCloudPlatform/functions-framework-go/funcframework"
|
||||
_ "github.com/joho/godotenv/autoload"
|
||||
|
||||
botjoin "github.com/roleypoly/roleypoly/src/functions/bot-join"
|
||||
sessiondata "github.com/roleypoly/roleypoly/src/functions/session-data"
|
||||
sessionprewarm "github.com/roleypoly/roleypoly/src/functions/session-prewarm"
|
||||
)
|
||||
|
@ -15,6 +18,7 @@ import (
|
|||
var mappings map[string]http.HandlerFunc = map[string]http.HandlerFunc{
|
||||
"/session-prewarm": sessionprewarm.SessionPrewarm,
|
||||
"/session-data": sessiondata.SessionData,
|
||||
"/bot-join": botjoin.BotJoin,
|
||||
}
|
||||
|
||||
var port string
|
|
@ -9,6 +9,7 @@ import (
|
|||
|
||||
// GetenvValue is a holder type for Getenv to translate any Getenv strings to real types
|
||||
type GetenvValue struct {
|
||||
key string
|
||||
value string
|
||||
}
|
||||
|
||||
|
@ -25,6 +26,7 @@ func Getenv(key string, defaultValue ...string) GetenvValue {
|
|||
|
||||
return GetenvValue{
|
||||
value: strings.TrimSpace(value),
|
||||
key: key,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -63,3 +65,11 @@ func (g GetenvValue) Number() int {
|
|||
func (g GetenvValue) JSON(target interface{}) error {
|
||||
return json.Unmarshal([]byte(g.value), target)
|
||||
}
|
||||
|
||||
func (g GetenvValue) OrFatal() GetenvValue {
|
||||
if g.value == "" {
|
||||
panic("Getenv value was empty and shouldn't be. key: " + g.key)
|
||||
}
|
||||
|
||||
return g
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ import (
|
|||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/onsi/gomega"
|
||||
"github.com/roleypoly/roleypoly/src/common"
|
||||
)
|
||||
|
||||
|
@ -121,3 +122,17 @@ func TestEnvconfigJSON(t *testing.T) {
|
|||
t.FailNow()
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnvconfigFatal(t *testing.T) {
|
||||
O := gomega.NewWithT(t)
|
||||
O.Expect(func() {
|
||||
_ = common.Getenv("test__thing_that_doesnt_exist").OrFatal().String()
|
||||
}).Should(gomega.Panic(), "Getenv without a value should panic")
|
||||
}
|
||||
|
||||
func TestEnvconfigNotFatal(t *testing.T) {
|
||||
O := gomega.NewWithT(t)
|
||||
O.Expect(func() {
|
||||
_ = common.Getenv("test__string").OrFatal().String()
|
||||
}).ShouldNot(gomega.Panic(), "Getenv with a value should not panic")
|
||||
}
|
||||
|
|
|
@ -28,7 +28,7 @@ func assertAuthLevel(err error, requiredAuthLevel AuthLevel, assertedAuthLevel A
|
|||
|
||||
// AuthMustMatch will assert the current session's authorization group/level; only can match for Guest, User, and Super.
|
||||
func AuthMustMatch(request *http.Request, authLevel AuthLevel) error {
|
||||
authCookie, err := request.Cookie("Authorization")
|
||||
_, err := request.Cookie("Authorization")
|
||||
if errors.Is(err, http.ErrNoCookie) {
|
||||
// No cookie is present, assert guest.
|
||||
return assertAuthLevel(ErrNotAuthorized, authLevel, AuthGuest)
|
||||
|
@ -36,4 +36,5 @@ func AuthMustMatch(request *http.Request, authLevel AuthLevel) error {
|
|||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
66
src/common/faas/bounce.go
Normal file
66
src/common/faas/bounce.go
Normal file
|
@ -0,0 +1,66 @@
|
|||
package faas
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
var bounceHTML = template.Must(template.New("bounceHTML").Parse(
|
||||
`<!doctype html>
|
||||
<meta charset="utf8">
|
||||
<title>Redirecting...</title>
|
||||
<meta http-equiv="refresh" content="0;URL='{{.Location}}'">
|
||||
<style>
|
||||
body {
|
||||
background-color: #453E3D;
|
||||
color: #AB9B9A;
|
||||
}
|
||||
a {
|
||||
color: #AB9B9A;
|
||||
}
|
||||
</style>
|
||||
<p>
|
||||
Redirecting you to <a href="{{.Location}}">{{.Location}}</a>
|
||||
</p>
|
||||
`,
|
||||
))
|
||||
|
||||
type bounceData struct {
|
||||
Location string
|
||||
}
|
||||
|
||||
// Bounce will do a 303 See Other response with url.
|
||||
func Bounce(rw http.ResponseWriter, url string) {
|
||||
rw.Header().Add("location", url)
|
||||
rw.WriteHeader(303)
|
||||
bounceHTML.Execute(rw, bounceData{Location: url})
|
||||
}
|
||||
|
||||
// Stash will save the specified URL for later use in Unstash(), e.g. after an OAuth bounce
|
||||
func Stash(rw http.ResponseWriter, url string) {
|
||||
if url == "" {
|
||||
return
|
||||
}
|
||||
|
||||
cookie := http.Cookie{
|
||||
Name: "rp_stashed_url",
|
||||
Value: url,
|
||||
HttpOnly: true,
|
||||
Expires: time.Now().Add(5 * time.Minute),
|
||||
}
|
||||
|
||||
rw.Header().Add("set-cookie", cookie.String())
|
||||
}
|
||||
|
||||
// Unstash will redirect/Bounce() to a previously stashed URL or the defaultURL, whichever is available.
|
||||
func Unstash(rw http.ResponseWriter, req *http.Request, defaultURL string) {
|
||||
redirectURL := defaultURL
|
||||
cookie, _ := req.Cookie("rp_stashed_url")
|
||||
|
||||
if cookie != nil && cookie.Expires.After(time.Now()) && cookie.Value != "" {
|
||||
redirectURL = cookie.Value
|
||||
}
|
||||
|
||||
Bounce(rw, redirectURL)
|
||||
}
|
10
src/functions/bot-join/README.md
Normal file
10
src/functions/bot-join/README.md
Normal file
|
@ -0,0 +1,10 @@
|
|||
# Bot Join Bounce Service
|
||||
|
||||
This function sends a user to the necessary Discord.com Bot OAuth flow.
|
||||
|
||||
Two cases may be present:
|
||||
|
||||
- General: The user will be sent to allow any of their relevant servers to join
|
||||
- This flow would be triggered from a generalized button
|
||||
- Specific: The user will be sent to join one of their servers.
|
||||
- This flow would be triggered from server picker
|
45
src/functions/bot-join/botjoin.go
Normal file
45
src/functions/bot-join/botjoin.go
Normal file
|
@ -0,0 +1,45 @@
|
|||
package botjoin
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"text/template"
|
||||
|
||||
"github.com/roleypoly/roleypoly/src/common"
|
||||
"github.com/roleypoly/roleypoly/src/common/faas"
|
||||
)
|
||||
|
||||
var (
|
||||
validGuildID = regexp.MustCompile(`^[0-9]+$`)
|
||||
redirectPathTemplate = template.Must(
|
||||
template.New("redirect").Parse(
|
||||
`https://discord.com/api/oauth2/authorize?client_id={{.ClientID}}&scope=bot&permissions={{.Permissions}}{{if .GuildID}}&guild_id={{.GuildID}}&disable_guild_select=true{{end}}`,
|
||||
),
|
||||
)
|
||||
clientID = common.Getenv("BOT_CLIENT_ID").String()
|
||||
)
|
||||
|
||||
type redirectPathData struct {
|
||||
ClientID string
|
||||
Permissions int
|
||||
GuildID string
|
||||
}
|
||||
|
||||
func BotJoin(rw http.ResponseWriter, r *http.Request) {
|
||||
guildID := r.URL.Query().Get("guild")
|
||||
if !validGuildID.MatchString(guildID) {
|
||||
guildID = ""
|
||||
}
|
||||
|
||||
pathData := redirectPathData{
|
||||
ClientID: clientID,
|
||||
Permissions: 268435456, // MANAGE_ROLES
|
||||
GuildID: guildID,
|
||||
}
|
||||
|
||||
pathBuffer := bytes.Buffer{}
|
||||
redirectPathTemplate.Execute(&pathBuffer, pathData)
|
||||
|
||||
faas.Bounce(rw, pathBuffer.String())
|
||||
}
|
36
src/functions/bot-join/botjoin_test.go
Normal file
36
src/functions/bot-join/botjoin_test.go
Normal file
|
@ -0,0 +1,36 @@
|
|||
package botjoin_test
|
||||
|
||||
import (
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/onsi/gomega"
|
||||
botjoin "github.com/roleypoly/roleypoly/src/functions/bot-join"
|
||||
)
|
||||
|
||||
func TestGeneral(t *testing.T) {
|
||||
O := gomega.NewWithT(t)
|
||||
|
||||
req := httptest.NewRequest("GET", "/bot-join", nil)
|
||||
resp := httptest.NewRecorder()
|
||||
|
||||
botjoin.BotJoin(resp, req)
|
||||
|
||||
result := resp.Result()
|
||||
O.Expect(result.StatusCode).Should(gomega.BeIdenticalTo(303))
|
||||
O.Expect(result.Header.Get("location")).ShouldNot(gomega.ContainSubstring("guild_id"))
|
||||
|
||||
}
|
||||
|
||||
func TestGeneralSpecific(t *testing.T) {
|
||||
O := gomega.NewWithT(t)
|
||||
|
||||
req := httptest.NewRequest("GET", "/bot-join?guild=386659935687147521", nil)
|
||||
resp := httptest.NewRecorder()
|
||||
|
||||
botjoin.BotJoin(resp, req)
|
||||
|
||||
result := resp.Result()
|
||||
O.Expect(result.StatusCode).Should(gomega.BeIdenticalTo(303))
|
||||
O.Expect(result.Header.Get("location")).Should(gomega.ContainSubstring("guild_id=386659935687147521"))
|
||||
}
|
43
src/functions/login-bounce/loginbounce.go
Normal file
43
src/functions/login-bounce/loginbounce.go
Normal file
|
@ -0,0 +1,43 @@
|
|||
package loginbounce
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"net/http"
|
||||
"text/template"
|
||||
|
||||
"github.com/segmentio/ksuid"
|
||||
|
||||
"github.com/roleypoly/roleypoly/src/common"
|
||||
"github.com/roleypoly/roleypoly/src/common/faas"
|
||||
)
|
||||
|
||||
var (
|
||||
redirectPathTemplate = template.Must(
|
||||
template.New("redirect").Parse(
|
||||
`https://discord.com/api/oauth2/authorize?client_id={{.ClientID}}&scope=identify,guilds&redirect_uri={{.RedirectURI}}&state={{.State}}`,
|
||||
),
|
||||
)
|
||||
clientID = common.Getenv("BOT_CLIENT_ID").String()
|
||||
redirectURI = common.Getenv("OAUTH_REDIRECT_URI").String()
|
||||
)
|
||||
|
||||
type redirectPathData struct {
|
||||
ClientID string
|
||||
RedirectURI string
|
||||
State string
|
||||
}
|
||||
|
||||
func LoginBounce(rw http.ResponseWriter, r *http.Request) {
|
||||
faas.Stash(rw, r.URL.Query().Get("redirect_url"))
|
||||
|
||||
pathData := redirectPathData{
|
||||
ClientID: clientID,
|
||||
RedirectURI: redirectURI,
|
||||
State: ksuid.New().String(),
|
||||
}
|
||||
|
||||
pathBuffer := bytes.Buffer{}
|
||||
redirectPathTemplate.Execute(&pathBuffer, pathData)
|
||||
|
||||
faas.Bounce(rw, pathBuffer.String())
|
||||
}
|
24
src/functions/login-bounce/loginbounce_test.go
Normal file
24
src/functions/login-bounce/loginbounce_test.go
Normal file
|
@ -0,0 +1,24 @@
|
|||
package loginbounce_test
|
||||
|
||||
import (
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/onsi/gomega"
|
||||
loginbounce "github.com/roleypoly/roleypoly/src/functions/login-bounce"
|
||||
)
|
||||
|
||||
func TestBounce(t *testing.T) {
|
||||
O := gomega.NewWithT(t)
|
||||
|
||||
req := httptest.NewRequest("GET", "/login-bounce?redirect_url=https://localhost:6600/test", nil)
|
||||
rw := httptest.NewRecorder()
|
||||
|
||||
loginbounce.LoginBounce(rw, req)
|
||||
|
||||
resp := rw.Result()
|
||||
|
||||
O.Expect(resp.StatusCode).Should(gomega.BeIdenticalTo(303))
|
||||
O.Expect(resp.Header.Get("location")).Should(gomega.ContainSubstring("identify,guild"))
|
||||
O.Expect(resp.Header.Get("set-cookie")).Should(gomega.ContainSubstring("https://localhost:6600/test"))
|
||||
}
|
Loading…
Add table
Reference in a new issue