Skip to content

Commit

Permalink
feat: adding allowed orgs
Browse files Browse the repository at this point in the history
  • Loading branch information
katallaxie authored Oct 2, 2024
1 parent 6c1c434 commit 922c815
Show file tree
Hide file tree
Showing 5 changed files with 110 additions and 79 deletions.
3 changes: 2 additions & 1 deletion examples/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"log"
"os"
"sort"
"strings"

goth "github.com/zeiss/fiber-goth"
gorm_adapter "github.com/zeiss/fiber-goth/adapters/gorm"
Expand Down Expand Up @@ -90,7 +91,7 @@ func run(_ context.Context) error {

ga := gorm_adapter.New(conn)

providers.RegisterProvider(github.New(os.Getenv("GITHUB_KEY"), os.Getenv("GITHUB_SECRET"), "http://localhost:3000/auth/github/callback"))
providers.RegisterProvider(github.New(os.Getenv("GITHUB_KEY"), os.Getenv("GITHUB_SECRET"), "http://localhost:3000/auth/github/callback", github.WithAllowedOrgs(strings.Split(os.Getenv("GITHUB_ALLOWED_ORGS"), ",")...)))
providers.RegisterProvider(entraid.New(os.Getenv("ENTRAID_CLIENT_ID"), os.Getenv("ENTRAID_CLIENT_SECRET"), "http://localhost:3000/auth/entraid/callback", entraid.TenantType(os.Getenv("ENTRAID_TENANT_ID"))))

m := map[string]string{
Expand Down
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ require (
github.com/gofiber/fiber/v2 v2.52.5
github.com/golang/mock v1.7.0-rc.1
github.com/golangci/golangci-lint v1.61.0
github.com/google/go-github/v56 v56.0.0
github.com/google/uuid v1.6.0
github.com/katallaxie/pkg v0.6.6
github.com/spf13/cobra v1.8.1
Expand Down Expand Up @@ -85,6 +86,7 @@ require (
github.com/golangci/revgrep v0.5.3 // indirect
github.com/golangci/unconvert v0.0.0-20240309020433-c5143eacb3ed // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/gordonklaus/ineffassign v0.1.0 // indirect
github.com/gostaticanalysis/analysisutil v0.7.1 // indirect
github.com/gostaticanalysis/comment v1.4.2 // indirect
Expand Down
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,10 @@ github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-github/v56 v56.0.0 h1:TysL7dMa/r7wsQi44BjqlwaHvwlFlqkK8CtBWCX3gb4=
github.com/google/go-github/v56 v56.0.0/go.mod h1:D8cdcX98YWJvi7TLo7zM4/h8ZTx6u6fwGEkCdisopo0=
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
github.com/google/pprof v0.0.0-20240827171923-fa2c70bbbfe5 h1:5iH8iuqE5apketRbSFBy+X1V0o+l+8NF1avt4HWl7cA=
github.com/google/pprof v0.0.0-20240827171923-fa2c70bbbfe5/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
Expand Down
180 changes: 102 additions & 78 deletions providers/github/github.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,28 @@ package github

import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"strconv"
"strings"

"github.com/zeiss/fiber-goth/adapters"
"github.com/zeiss/fiber-goth/providers"

"github.com/google/go-github/v56/github"
"github.com/zeiss/pkg/cast"
"github.com/zeiss/pkg/slices"
"github.com/zeiss/pkg/utilx"
"golang.org/x/oauth2"
"golang.org/x/oauth2/endpoints"
)

var ErrNoVerifiedPrimaryEmail = errors.New("goth: no verified primary email found")
var (
ErrNoVerifiedPrimaryEmail = errors.New("goth: no verified primary email found")
ErrFailedFetchUser = errors.New("goth: no failed to fetch user")
ErrNotAllowedOrg = errors.New("goth: user not in allowed org")
ErrNoName = errors.New("goth: user has no display name set")
)

const NoopEmail = ""

Expand All @@ -36,36 +40,64 @@ var (
var DefaultScopes = []string{"user:email", "read:user"}

type githubProvider struct {
id string
name string
clientKey string
secret string
callbackURL string
userURL string
emailURL string
authURL string
providerType providers.ProviderType
client *http.Client
config *oauth2.Config
id string
name string
clientKey string
secret string
callbackURL string
userURL string
emailURL string
authURL string
enterpriseURL string
allowedOrgs []string
providerType providers.ProviderType
client *http.Client
config *oauth2.Config
scopes []string

providers.UnimplementedProvider
}

// Opt is a function that configures the GitHub provider.
type Opt func(*githubProvider)

// WithScopes sets the scopes for the GitHub provider.
func WithScopes(scopes ...string) Opt {
return func(p *githubProvider) {
p.config.Scopes = scopes
}
}

// WithAllowedOrgs sets the allowed organizations for the GitHub provider.
func WithAllowedOrgs(orgs ...string) Opt {
return func(p *githubProvider) {
p.allowedOrgs = orgs
}
}

// New creates a new GitHub provider.
func New(clientKey, secret, callbackURL string, scopes ...string) *githubProvider {
func New(clientKey, secret, callbackURL string, opts ...Opt) *githubProvider {
p := &githubProvider{
id: "github",
name: "GitHub",
clientKey: clientKey,
secret: secret,
callbackURL: callbackURL,
userURL: UserURL,
emailURL: EmailURL,
authURL: AuthURL,
providerType: providers.ProviderTypeOAuth2,
client: providers.DefaultClient,
}
p.config = newConfig(p, scopes...)
id: "github",
name: "GitHub",
clientKey: clientKey,
secret: secret,
callbackURL: callbackURL,
userURL: UserURL,
emailURL: EmailURL,
authURL: AuthURL,
enterpriseURL: "",
providerType: providers.ProviderTypeOAuth2,
client: providers.DefaultClient,
allowedOrgs: []string{},
scopes: DefaultScopes,
}

for _, opt := range opts {
opt(p)
}

p.config = newConfig(p, p.scopes...)

return p
}
Expand Down Expand Up @@ -131,28 +163,17 @@ func (g *githubProvider) CompleteAuth(ctx context.Context, adapter adapters.Adap
return adapters.GothUser{}, err
}

req, err := http.NewRequestWithContext(ctx, "GET", g.userURL, nil)
if err != nil {
return adapters.GothUser{}, err
}
req.Header.Add("Authorization", "Bearer "+token.AccessToken)

resp, err := g.client.Do(req)
if err != nil {
return adapters.GothUser{}, err
}
defer io.Copy(io.Discard, resp.Body) // equivalent to `cp body /dev/null`
defer resp.Body.Close()
gc := github.NewClient(g.config.Client(ctx, token))

err = json.NewDecoder(resp.Body).Decode(&u)
gu, _, err := gc.Users.Get(ctx, "")
if err != nil {
return adapters.GothUser{}, err
}

user := adapters.GothUser{
Name: u.Name,
Email: u.Email,
Image: cast.Ptr(u.Picture),
Name: gu.GetName(),
Email: gu.GetEmail(),
Image: cast.Ptr(gu.GetAvatarURL()),
Accounts: []adapters.GothAccount{
{
Type: adapters.AccountTypeOAuth2,
Expand All @@ -167,16 +188,35 @@ func (g *githubProvider) CompleteAuth(ctx context.Context, adapter adapters.Adap
}

if utilx.Empty(user.Email) && slices.Any(checkScope, g.config.Scopes...) {
user.Email, err = getPrivateMail(ctx, g, token)
if err != nil {
return user, err
opt := &github.ListOptions{}

for {
emails, resp, err := gc.Users.ListEmails(ctx, opt)
if err != nil {
return adapters.GothUser{}, err
}

user.Email, err = checkEmail(emails...)
if err != nil {
return adapters.GothUser{}, err
}

if resp.NextPage == 0 {
break
}

opt.Page = resp.NextPage
}
}

if utilx.Empty(user.Email) {
return user, ErrNoVerifiedPrimaryEmail
}

if len(g.allowedOrgs) > 0 && !slices.Any(checkOrg(ctx, gc, gu.GetLogin()), g.allowedOrgs...) {
return adapters.GothUser{}, ErrNotAllowedOrg
}

user, err = adapter.CreateUser(ctx, user)
if err != nil {
return adapters.GothUser{}, err
Expand All @@ -202,43 +242,27 @@ func newConfig(p *githubProvider, scopes ...string) *oauth2.Config {
return c
}

func getPrivateMail(ctx context.Context, p *githubProvider, token *oauth2.Token) (string, error) {
req, err := http.NewRequestWithContext(ctx, "GET", p.emailURL, nil)
if err != nil {
return NoopEmail, err
}
req.Header.Add("Authorization", "Bearer "+token.AccessToken)

res, err := p.client.Do(req)
if err != nil {
return NoopEmail, err
}
defer res.Body.Close()

if res.StatusCode != http.StatusOK {
return NoopEmail, fmt.Errorf("goth: GitHub API responded with a %d trying to fetch user email", res.StatusCode)
}
func checkScope(scope string) bool {
return strings.TrimSpace(scope) == "user" || strings.TrimSpace(scope) == "user:email"
}

var mailList []struct {
Email string `json:"email"`
Primary bool `json:"primary"`
Verified bool `json:"verified"`
}
func checkOrg(ctx context.Context, c *github.Client, user string) func(string) bool {
return func(org string) bool {
m, _, err := c.Organizations.IsMember(ctx, org, user)
if err != nil {
return false
}

err = json.NewDecoder(res.Body).Decode(&mailList)
if err != nil {
return NoopEmail, err
return m
}
}

for _, v := range mailList {
if v.Primary && v.Verified {
return v.Email, nil
func checkEmail(emails ...*github.UserEmail) (string, error) {
for _, e := range emails {
if e.GetPrimary() && e.GetVerified() {
return cast.Value(e.Email), nil
}
}

return NoopEmail, ErrNoVerifiedPrimaryEmail
}

func checkScope(scope string) bool {
return strings.TrimSpace(scope) == "user" || strings.TrimSpace(scope) == "user:email"
}
Binary file added tmp/main
Binary file not shown.

0 comments on commit 922c815

Please sign in to comment.