Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implements a Discord provider #528

Open
wants to merge 13 commits into
base: master
Choose a base branch
from
Open
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@

Coming soon! Please document any work in progress here as part of your PR. It will be moved to the next tag when released.

* Implement a Discord provider that uses `Username` as the username to match against in the `whiteList` config
* Or uses `Username#Discriminator` if the Discriminator is present
* Or uses ID if `discord_use_ids` is set

## v0.40.0

- upgrade golang to `v1.22` from `v1.18`
Expand Down
31 changes: 31 additions & 0 deletions config/config.yml_example_discord
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@

# Vouch Proxy configuration
# bare minimum to get Vouch Proxy running with Discord as an OpenID Provider

vouch:
domains:
- yourdomain.com
# whiteList is a list of usernames that will allow a login if allowAllUsers is false
whiteList:
# The default behavior matches the Discord user's username
- loganintech

# If the user still hasn't chosen a new username, the old username#discrimnator format will work
- LoganInTech#1203

# If discord_use_ids is set to true, you must use the user's ID
- 12345678901234567

cookie:
# allow the jwt/cookie to be set into http://yourdomain.com (defaults to true, requiring https://yourdomain.com)
secure: false
# vouch.cookie.domain must be set when enabling allowAllUsers
# domain: yourdomain.com

oauth:
provider: discord
client_id: xxxxxxxxxxxxxxxxxxxxxxxxxxxx
client_secret: xxxxxxxxxxxxxxxxxxxxxxxx
callback_url: http://vouch.yourdomain.com:9090/auth
## Uncomment this to match users based on their Discord ID
# discord_use_ids: true
3 changes: 3 additions & 0 deletions handlers/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"net/http"

"github.com/gorilla/sessions"
"github.com/vouch/vouch-proxy/pkg/providers/discord"
"go.uber.org/zap"
"golang.org/x/oauth2"

Expand Down Expand Up @@ -88,6 +89,8 @@ func getProvider() Provider {
return openid.Provider{}
case cfg.Providers.Alibaba:
return alibaba.Provider{}
case cfg.Providers.Discord:
return discord.Provider{}
default:
// shouldn't ever reach this since cfg checks for a properly configure `oauth.provider`
log.Fatal("oauth.provider appears to be misconfigured, please check your config")
Expand Down
35 changes: 33 additions & 2 deletions pkg/cfg/oauth.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ var (
OpenStax: "openstax",
Nextcloud: "nextcloud",
Alibaba: "alibaba",
Discord: "discord",
}
)

Expand All @@ -59,6 +60,7 @@ type OAuthProviders struct {
OpenStax string
Nextcloud string
Alibaba string
Discord string
}

// oauth config items endoint for access
Expand All @@ -83,6 +85,9 @@ type oauthConfig struct {
PreferredDomain string `mapstructure:"preferredDomain"`
AzureToken string `mapstructure:"azure_token" envconfig:"azure_token"`
CodeChallengeMethod string `mapstructure:"code_challenge_method" envconfig:"code_challenge_method"`
// DiscordUseIDs defaults to false, maintaining the more common username checking behavior
// If set to true, match the Discord user's ID instead of their username
DiscordUseIDs bool `mapstructure:"discord_use_ids" envconfig:"discord_use_ids"`
}

type oauthClaimsConfig struct {
Expand Down Expand Up @@ -122,7 +127,8 @@ func oauthBasicTest() error {
GenOAuth.Provider != Providers.OIDC &&
GenOAuth.Provider != Providers.OpenStax &&
GenOAuth.Provider != Providers.Nextcloud &&
GenOAuth.Provider != Providers.Alibaba {
GenOAuth.Provider != Providers.Alibaba &&
GenOAuth.Provider != Providers.Discord {
return errors.New("configuration error: Unknown oauth provider: " + GenOAuth.Provider)
}
// OAuthconfig Checks
Expand Down Expand Up @@ -188,6 +194,9 @@ func setProviderDefaults() {
} else if GenOAuth.Provider == Providers.IndieAuth {
GenOAuth.CodeChallengeMethod = "S256"
configureOAuthClient()
} else if GenOAuth.Provider == Providers.Discord {
setDefaultsDiscord()
configureOAuthClient()
} else {
// OIDC, OpenStax, Nextcloud
configureOAuthClient()
Expand Down Expand Up @@ -270,6 +279,25 @@ func setDefaultsGitHub() {
GenOAuth.CodeChallengeMethod = "S256"
}

func setDefaultsDiscord() {
// log.Info("configuring GitHub OAuth")
if GenOAuth.AuthURL == "" {
GenOAuth.AuthURL = "https://discord.com/oauth2/authorize"
}
if GenOAuth.TokenURL == "" {
GenOAuth.TokenURL = "https://discord.com/api/oauth2/token"
}
if GenOAuth.UserInfoURL == "" {
GenOAuth.UserInfoURL = "https://discord.com/api/users/@me"
}
if len(GenOAuth.Scopes) == 0 {
//Required for UserInfo URL
//https://discord.com/developers/docs/resources/user#get-current-user
GenOAuth.Scopes = []string{"identify", "email"}
}
GenOAuth.CodeChallengeMethod = "S256"
}

func configureOAuthClient() {
log.Infof("configuring %s OAuth with Endpoint %s", GenOAuth.Provider, GenOAuth.AuthURL)
OAuthClient = &oauth2.Config{
Expand Down Expand Up @@ -297,7 +325,10 @@ func checkCallbackConfig(url string) error {
}
}
if !found {
return fmt.Errorf("configuration error: oauth.callback_url (%s) must be within a configured domains where the cookie will be set: either `vouch.domains` %s or `vouch.cookie.domain` %s", url, Cfg.Domains, Cfg.Cookie.Domain)
return fmt.Errorf("configuration error: oauth.callback_url (%s) must be within a configured domains where the cookie will be set: either `vouch.domains` %s or `vouch.cookie.domain` %s",
url,
Cfg.Domains,
Cfg.Cookie.Domain)
}

return nil
Expand Down
70 changes: 70 additions & 0 deletions pkg/providers/discord/discord.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/*

Copyright 2020 The Vouch Proxy Authors.
Use of this source code is governed by The MIT License (MIT) that
can be found in the LICENSE file. Software distributed under The
MIT License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES
OR CONDITIONS OF ANY KIND, either express or implied.

*/

package discord

import (
"encoding/json"
"io"
"net/http"

"golang.org/x/oauth2"

"go.uber.org/zap"

"github.com/vouch/vouch-proxy/pkg/cfg"
"github.com/vouch/vouch-proxy/pkg/providers/common"
"github.com/vouch/vouch-proxy/pkg/structs"
)

// Provider provider specific functions
type Provider struct{}

var log *zap.SugaredLogger

// Configure see main.go configure()
func (Provider) Configure() {
log = cfg.Logging.Logger
}

// GetUserInfo provider specific call to get userinfomation
func (Provider) GetUserInfo(r *http.Request, user *structs.User, customClaims *structs.CustomClaims, ptokens *structs.PTokens, opts ...oauth2.AuthCodeOption) (rerr error) {
client, _, err := common.PrepareTokensAndClient(r, ptokens, true, opts...)
if err != nil {
return err
}
userinfo, err := client.Get(cfg.GenOAuth.UserInfoURL)
if err != nil {
return err
}
defer func() {
if err := userinfo.Body.Close(); err != nil {
rerr = err
}
}()
data, err := io.ReadAll(userinfo.Body)
if err != nil {
return err
}
log.Infof("Discord userinfo body: %s", string(data))
if err = common.MapClaims(data, customClaims); err != nil {
log.Error(err)
return err
}
discordUser := structs.DiscordUser{}
if err = json.Unmarshal(data, &discordUser); err != nil {
log.Error(err)
return err
}
discordUser.PrepareUserData()
user.Username = discordUser.PreparedUsername
user.Email = discordUser.Email
return nil
}
39 changes: 37 additions & 2 deletions pkg/structs/structs.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,12 @@ OR CONDITIONS OF ANY KIND, either express or implied.

package structs

import "strconv"
import (
"fmt"
"strconv"

"github.com/vouch/vouch-proxy/pkg/cfg"
)

// CustomClaims Temporary struct storing custom claims until JWT creation.
type CustomClaims struct {
Expand Down Expand Up @@ -148,7 +153,7 @@ type Contact struct {
Verified bool `json:"is_verified"`
}

//OpenStaxUser is a retrieved and authenticated user from OpenStax Accounts
// OpenStaxUser is a retrieved and authenticated user from OpenStax Accounts
type OpenStaxUser struct {
User
Contacts []Contact `json:"contact_infos"`
Expand Down Expand Up @@ -240,3 +245,33 @@ type PTokens struct {
PAccessToken string
PIdToken string
}

// DiscordUser deserializes values from the Discord User Object: https://discord.com/developers/docs/resources/user#user-object-user-structure
type DiscordUser struct {
Id string `json:"id"`
Username string `json:"username"`
Discriminator string `json:"discriminator"`
GlobalName string `json:"global_name"`
Email string `json:"email"`
Verified bool `json:"verified"`

PreparedUsername string
}

// PrepareUserData copies the Username to PreparedUsername.
// If the provider is configured to use IDs, the ID is copied to PreparedUsername.
// If the Discriminator is present that is appended to the Username in the format "Username#Discriminator"
// to match the old format of Discord usernames
// Previous format which is being phased out: https://support.discord.com/hc/en-us/articles/4407571667351-Law-Enforcement-Guidelines Subheading "How to find usernames and discriminators"
// Details about the new username requirements: https://support.discord.com/hc/en-us/articles/12620128861463
func (u *DiscordUser) PrepareUserData() {
if cfg.GenOAuth.DiscordUseIDs {
u.PreparedUsername = u.Id
return
}

u.PreparedUsername = u.Username
if u.Discriminator != "0" {
u.PreparedUsername = fmt.Sprintf("%s#%s", u.Username, u.Discriminator)
}
}