Skip to content

Commit

Permalink
Merge pull request #192 from zond/johnpooch/bot-auth
Browse files Browse the repository at this point in the history
Add mechanism to allow bot to authenticate and to get tokens on user's behalf
  • Loading branch information
johnpooch authored Jun 12, 2024
2 parents 80af72e + 6873824 commit 35081c1
Show file tree
Hide file tree
Showing 6 changed files with 242 additions and 37 deletions.
13 changes: 5 additions & 8 deletions Dockerfile → .docker/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -15,24 +15,21 @@ RUN /usr/local/gcloud/google-cloud-sdk/bin/gcloud components install app-engine-
# Add the gcloud command-line tool to your path.
ENV PATH $PATH:/usr/local/gcloud/google-cloud-sdk/bin

# # Run dev_appserver.py . from inside the repo
# RUN dev_appserver.py .

# Set the working directory in the container
WORKDIR /go/src/app

# Copy the current directory contents into the container at /go/src/app
COPY . .

# Remove Dockerfile to prevent issue with App Engine
RUN rm Dockerfile
COPY ../ .

# Expose port 8080 to the outside world
EXPOSE 8080

# Expose port 80 for http traffic
EXPOSE 80

# Expose port 8000 to the outside world
EXPOSE 8000

# Start the process
CMD ["python3", "../../../usr/local/gcloud/google-cloud-sdk/bin/dev_appserver.py", "--host=0.0.0.0", "--admin_host=0.0.0.0", "."]
CMD ["python3", "../../../usr/local/gcloud/google-cloud-sdk/bin/dev_appserver.py", "--host=0.0.0.0", "--admin_host=0.0.0.0", "--enable_host_checking=False", "."]

2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,5 @@ game/game.debug

# Diff tool files
*.orig

.env
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,11 @@ To enable debugging the JSON output in a browser, adding the query parameter `ac

- Download Docker
- Navigate to the root directory of this project
- Run `docker build --tag 'diplicity' .`
- Run `docker run -p 8080:8080 -p 8000:8000 diplicity`
- Run `docker-compose up`
- The API is now available on your machine at `localhost:8080`
- The Admin server is now available on your machine at `localhost:8000`
- **Note** to get the Discord bot auth to work, you need to send a curl request
after the service is initialized: `curl -XPOST http://localhost:8080/_configure -d '{"DiscordBotCredentials": {"Username": "<username>", "Password": "<password>"}}'`

## Running locally

Expand Down
216 changes: 193 additions & 23 deletions auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,15 +33,17 @@ var (
)

const (
LoginRoute = "Login"
LogoutRoute = "Logout"
RedirectRoute = "Redirect"
OAuth2CallbackRoute = "OAuth2Callback"
UnsubscribeRoute = "Unsubscribe"
ApproveRedirectRoute = "ApproveRedirect"
ListRedirectURLsRoute = "ListRedirectURLs"
ReplaceFCMRoute = "ReplaceFCM"
TestUpdateUserRoute = "TestUpdateUser"
LoginRoute = "Login"
LogoutRoute = "Logout"
TokenForDiscordUserRoute = "TokenForDiscordUser"
DiscordBotLoginRoute = "DiscordBotLogin"
RedirectRoute = "Redirect"
OAuth2CallbackRoute = "OAuth2Callback"
UnsubscribeRoute = "Unsubscribe"
ApproveRedirectRoute = "ApproveRedirect"
ListRedirectURLsRoute = "ListRedirectURLs"
ReplaceFCMRoute = "ReplaceFCM"
TestUpdateUserRoute = "TestUpdateUser"
)

const (
Expand All @@ -51,30 +53,72 @@ const (
)

const (
UserKind = "User"
naClKind = "NaCl"
oAuthKind = "OAuth"
redirectURLKind = "RedirectURL"
superusersKind = "Superusers"
prodKey = "prod"
UserKind = "User"
naClKind = "NaCl"
oAuthKind = "OAuth"
redirectURLKind = "RedirectURL"
superusersKind = "Superusers"
discordBotCredentialsKind = "DiscordBotCredentials"
prodKey = "prod"
)

const (
defaultTokenDuration = time.Hour * 20
)

var (
prodOAuth *OAuth
prodOAuthLock = sync.RWMutex{}
prodNaCl *naCl
prodNaClLock = sync.RWMutex{}
prodSuperusers *Superusers
prodSuperusersLock = sync.RWMutex{}
router *mux.Router
prodOAuth *OAuth
prodOAuthLock = sync.RWMutex{}
prodNaCl *naCl
prodNaClLock = sync.RWMutex{}
prodSuperusers *Superusers
prodSuperusersLock = sync.RWMutex{}
prodDiscordBotCredentials *DiscordBotCredentials
prodDiscordBotCredentialsLock = sync.RWMutex{}
router *mux.Router

RedirectURLResource *Resource
)

type DiscordBotCredentials struct {
Username string
Password string
}

func getDiscordBotCredentialsKey(ctx context.Context) *datastore.Key {
return datastore.NewKey(ctx, discordBotCredentialsKind, prodKey, 0, nil)
}

func SetDiscordBotCredentials(ctx context.Context, discordBotCredentials *DiscordBotCredentials) error {
return datastore.RunInTransaction(ctx, func(ctx context.Context) error {
currentDiscordBotCredentials := &DiscordBotCredentials{}
if err := datastore.Get(ctx, getDiscordBotCredentialsKey(ctx), currentDiscordBotCredentials); err == nil {
return HTTPErr{"DiscordBotCredentials already configured", http.StatusBadRequest}
}
if _, err := datastore.Put(ctx, getDiscordBotCredentialsKey(ctx), discordBotCredentials); err != nil {
return err
}
return nil
}, &datastore.TransactionOptions{XG: false})
}

func getDiscordBotCredentials(ctx context.Context) (*DiscordBotCredentials, error) {
prodDiscordBotCredentialsLock.RLock()
if prodDiscordBotCredentials != nil {
defer prodDiscordBotCredentialsLock.RUnlock()
return prodDiscordBotCredentials, nil
}
prodDiscordBotCredentialsLock.RUnlock()
prodDiscordBotCredentialsLock.Lock()
defer prodDiscordBotCredentialsLock.Unlock()
foundDiscordBotCredentials := &DiscordBotCredentials{}
if err := datastore.Get(ctx, getDiscordBotCredentialsKey(ctx), foundDiscordBotCredentials); err != nil {
return nil, err
}
prodDiscordBotCredentials = foundDiscordBotCredentials
return prodDiscordBotCredentials, nil
}

func init() {
RedirectURLResource = &Resource{
Delete: deleteRedirectURL,
Expand Down Expand Up @@ -417,6 +461,88 @@ func getOAuth2Config(ctx context.Context, r *http.Request) (*oauth2.Config, erro
}, nil
}

func handleGetTokenForDiscordUser(w ResponseWriter, r Request) error {
ctx := appengine.NewContext(r.Req())

user, ok := r.Values()["user"].(*User)
if !ok {
return HTTPErr{
Body: "Unauthenticated",
Status: http.StatusUnauthorized,
}
}

if !appengine.IsDevAppServer() {

superusers, err := GetSuperusers(ctx)
if err != nil {
return HTTPErr{
Body: "Unable to load superusers",
Status: http.StatusInternalServerError,
}
}

if !superusers.Includes(user.Id) {
return HTTPErr{
Body: "Unauthorized",
Status: http.StatusForbidden,
}
}
}

discordUserId := r.Vars()["user_id"]
if discordUserId == "" {
return HTTPErr{
Body: "Must provide discord user id",
Status: http.StatusBadRequest,
}
}

discordUser := createUserFromDiscordUserId(discordUserId)

if _, err := datastore.Put(ctx, UserID(ctx, discordUser.Id), discordUser); err != nil {
return HTTPErr{
Body: "Unable to store user",
Status: http.StatusInternalServerError,
}
}

token, err := encodeUserToToken(ctx, discordUser)
if err != nil {
return HTTPErr{
Body: "Unable to encode user to token",
Status: http.StatusInternalServerError,
}
}

w.SetContent(NewItem(token).SetName("token"))
return nil
}

func createUserFromDiscordUserId(discordUserId string) *User {
return &User{
Email: "[email protected]",
FamilyName: "Discord User",
GivenName: "Discord User",
Id: discordUserId,
Name: "Discord User",
VerifiedEmail: true,
ValidUntil: time.Now().Add(time.Hour * 24 * 365 * 10),
}
}

func createDiscordBotUser() *User {
return &User{
Email: "[email protected]",
FamilyName: "Discord Bot",
GivenName: "Discord Bot",
Id: "discord-bot-user-id",
Name: "Discord Bot",
VerifiedEmail: true,
ValidUntil: time.Now().Add(time.Hour * 24 * 365 * 10),
}
}

func handleLogin(w ResponseWriter, r Request) error {
ctx := appengine.NewContext(r.Req())

Expand Down Expand Up @@ -448,6 +574,48 @@ func handleLogin(w ResponseWriter, r Request) error {
return nil
}

func handleDiscordBotLogin(w ResponseWriter, r Request) error {
ctx := appengine.NewContext(r.Req())

discordBotCredentials, err := getDiscordBotCredentials(ctx)
if err != nil {
return HTTPErr{"Unable to load discord bot credentials", http.StatusInternalServerError}
}

authHeader := r.Req().Header.Get("Authorization")
if !strings.HasPrefix(authHeader, "Basic ") {
return HTTPErr{"Authorization header must be Basic", http.StatusBadRequest}
}

decoded, err := base64.StdEncoding.DecodeString(authHeader[6:])
if err != nil {
return HTTPErr{"Unable to decode authorization header", http.StatusBadRequest}
}

parts := strings.Split(string(decoded), ":")
if len(parts) != 2 {
return HTTPErr{"Authorization header format not username:password", http.StatusBadRequest}
}

if parts[0] != discordBotCredentials.Username || parts[1] != discordBotCredentials.Password {
return HTTPErr{"Unauthorized", http.StatusUnauthorized}
}

discordBotUser := createDiscordBotUser()

if _, err := datastore.Put(ctx, UserID(ctx, discordBotUser.Id), discordBotUser); err != nil {
return HTTPErr{"Unable to store user", http.StatusInternalServerError}
}

token, err := encodeUserToToken(ctx, discordBotUser)
if err != nil {
return HTTPErr{"Unable to encode user to token", http.StatusInternalServerError}
}

w.SetContent(NewItem(token).SetName("token"))
return nil
}

func EncodeString(ctx context.Context, s string) (string, error) {
b, err := EncodeBytes(ctx, []byte(s))
if err != nil {
Expand Down Expand Up @@ -790,7 +958,7 @@ func tokenFilter(w ResponseWriter, r Request) (bool, error) {
token := r.Req().URL.Query().Get("token")
if token == "" {
queryToken = false
if authHeader := r.Req().Header.Get("Authorization"); authHeader != "" {
if authHeader := r.Req().Header.Get("Authorization"); authHeader != "" && !strings.HasPrefix(authHeader, "Basic") {
parts := strings.Split(authHeader, " ")
if len(parts) != 2 {
return false, HTTPErr{"Authorization header not two parts joined by space", http.StatusBadRequest}
Expand Down Expand Up @@ -1090,6 +1258,8 @@ func SetupRouter(r *mux.Router) {
HandleResource(router, RedirectURLResource)
Handle(router, "/_test_update_user", []string{"PUT"}, TestUpdateUserRoute, handleTestUpdateUser)
Handle(router, "/Auth/Login", []string{"GET"}, LoginRoute, handleLogin)
Handle(router, "/Auth/DiscordBotLogin", []string{"GET"}, DiscordBotLoginRoute, handleDiscordBotLogin)
Handle(router, "/Auth/{user_id}/TokenForDiscordUser", []string{"GET"}, TokenForDiscordUserRoute, handleGetTokenForDiscordUser)
Handle(router, "/Auth/Logout", []string{"GET"}, LogoutRoute, handleLogout)
// Don't use `Handle` here, because we don't want CORS support for this particular route.
router.Path("/Auth/OAuth2Callback").Methods("GET").Name(OAuth2CallbackRoute).HandlerFunc(handleOAuth2Callback)
Expand Down
29 changes: 29 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
version: "3.9"
services:
diplicity-application:
build:
context: .
dockerfile: .docker/Dockerfile
container_name: diplicity-application
entrypoint:
[
"python3",
"../../../usr/local/gcloud/google-cloud-sdk/bin/dev_appserver.py",
"--host=0.0.0.0",
"--admin_host=0.0.0.0",
"--enable_host_checking=False",
".",
]
volumes:
- .:/go/src/app
networks:
- diplicity-net
env_file:
- .env
ports:
- "8080:8080"
- "8000:8000"
networks:
diplicity-net:
name: diplicity-net
driver: bridge
14 changes: 10 additions & 4 deletions game/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -655,10 +655,11 @@ func createAllocation(w ResponseWriter, r Request) (*Allocation, error) {
}

type configuration struct {
OAuth *auth.OAuth
FCMConf *FCMConf
SendGrid *auth.SendGrid
Superusers *auth.Superusers
OAuth *auth.OAuth
FCMConf *FCMConf
SendGrid *auth.SendGrid
Superusers *auth.Superusers
DiscordBotCredentials *auth.DiscordBotCredentials
}

func handleConfigure(w ResponseWriter, r Request) error {
Expand Down Expand Up @@ -688,6 +689,11 @@ func handleConfigure(w ResponseWriter, r Request) error {
return err
}
}
if conf.DiscordBotCredentials != nil {
if err := auth.SetDiscordBotCredentials(ctx, conf.DiscordBotCredentials); err != nil {
return err
}
}
return nil
}

Expand Down

0 comments on commit 35081c1

Please sign in to comment.