Skip to content

Commit

Permalink
add new provider Bitbucket
Browse files Browse the repository at this point in the history
Signed-off-by: Elliot Murphy <elliot@elliotmurphy.com>
  • Loading branch information
statik authored and ploxiln committed Feb 2, 2020
1 parent 7316837 commit aeada85
Show file tree
Hide file tree
Showing 6 changed files with 293 additions and 0 deletions.
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ Valid providers are :
* [GitLab](#gitlab-auth-provider)
* [LinkedIn](#linkedin-auth-provider)
* [Discord](#discord-auth-provider)
* [Bitbucket](#bitbucket-auth-provider)

The provider can be selected using the `provider` configuration value.

Expand Down Expand Up @@ -237,6 +238,17 @@ In this case, you can set the `-skip-oidc-discovery` option, and supply those re
1. Create a new Discord Application from <https://discordapp.com/developers/applications/>
2. Under OAuth2, Add Redirect to `https://internal.yourcompany.com/oauth2/callback`

### Bitbucket Auth Provider

The [Bitbucket](https://bitbucket.org) provider.

For Bitbucket, follow the [registration steps to create an OAuth client](https://confluence.atlassian.com/bitbucket/oauth-on-bitbucket-cloud-238027431.html#OAuthonBitbucketCloud-Createaconsumer).

The Bitbucket auth provider supports one additional parameter to restrict
authentication to members of a given Bitbucket team. Restricting by team is
normally accompanied with `--email-domain=*`

-bitbucket-team="": restrict logins to members of this team

## Email Authentication

Expand Down
1 change: 1 addition & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ func mainFlagSet() *flag.FlagSet {
flagSet.Var(&emailDomains, "email-domain", "authenticate emails with the specified domain (may be given multiple times). Use * to authenticate any email")
flagSet.Var(&whitelistDomains, "whitelist-domain", "allowed domain for redirection after authentication, leading '.' allows subdomains (may be given multiple times)")
flagSet.String("azure-tenant", "common", "go to a tenant-specific or common (tenant-independent) endpoint.")
flagSet.String("bitbucket-team", "", "restrict logins to members of this team")
flagSet.String("github-org", "", "restrict logins to members of this organisation")
flagSet.String("github-team", "", "restrict logins to members of this team (slug) (may be given multiple times)")
flagSet.Var(&gitlabGroups, "gitlab-group", "restrict logins to members of this group (full path) (may be given multiple times)")
Expand Down
3 changes: 3 additions & 0 deletions options.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ type Options struct {

AuthenticatedEmailsFile string `flag:"authenticated-emails-file" cfg:"authenticated_emails_file"`
AzureTenant string `flag:"azure-tenant" cfg:"azure_tenant"`
BitbucketTeam string `flag:"bitbucket-team" cfg:"bitbucket_team"`
EmailDomains []string `flag:"email-domain" cfg:"email_domains"`
WhitelistDomains []string `flag:"whitelist-domain" cfg:"whitelist_domains" env:"OAUTH2_PROXY_WHITELIST_DOMAINS"`
GitHubOrg string `flag:"github-org" cfg:"github_org"`
Expand Down Expand Up @@ -243,6 +244,8 @@ func parseProviderInfo(o *Options, msgs []string) []string {
switch p := o.provider.(type) {
case *providers.AzureProvider:
p.Configure(o.AzureTenant)
case *providers.BitbucketProvider:
p.SetTeam(o.BitbucketTeam)
case *providers.GitHubProvider:
p.SetOrgTeam(o.GitHubOrg, o.GitHubTeam)
case *providers.GitLabProvider:
Expand Down
123 changes: 123 additions & 0 deletions providers/bitbucket.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
package providers

import (
"fmt"
"log"
"net/http"
"net/http/httputil"
"net/url"

"github.com/bitly/oauth2_proxy/api"
)

type BitbucketProvider struct {
*ProviderData
Team string
}

func NewBitbucketProvider(p *ProviderData) *BitbucketProvider {
p.ProviderName = "Bitbucket"
if p.LoginURL == nil || p.LoginURL.String() == "" {
p.LoginURL = &url.URL{
Scheme: "https",
Host: "bitbucket.org",
Path: "/site/oauth2/authorize",
}
}
if p.RedeemURL == nil || p.RedeemURL.String() == "" {
p.RedeemURL = &url.URL{
Scheme: "https",
Host: "bitbucket.org",
Path: "/site/oauth2/access_token",
}
}
if p.ValidateURL == nil || p.ValidateURL.String() == "" {
p.ValidateURL = &url.URL{
Scheme: "https",
Host: "api.bitbucket.org",
Path: "/2.0/user/emails",
}
}
if p.Scope == "" {
p.Scope = "account team"
}
return &BitbucketProvider{ProviderData: p}
}

func (p *BitbucketProvider) SetTeam(team string) {
p.Team = team
}

func debug(data []byte, err error) {
if err == nil {
fmt.Printf("%s\n\n", data)
} else {
log.Fatalf("%s\n\n", err)
}
}

func (p *BitbucketProvider) GetEmailAddress(s *SessionState) (string, error) {

var emails struct {
Values []struct {
Email string `json:"email"`
Primary bool `json:"is_primary"`
}
}
var teams struct {
Values []struct {
Name string `json:"username"`
}
}
req, err := http.NewRequest("GET",
p.ValidateURL.String()+"?access_token="+s.AccessToken, nil)
if err != nil {
log.Printf("failed building request %s", err)
return "", err
}
err = api.RequestJson(req, &emails)
if err != nil {
log.Printf("failed making request %s", err)
debug(httputil.DumpRequestOut(req, true))
return "", err
}

if p.Team != "" {
log.Printf("Filtering against membership in team %s\n", p.Team)
teamURL := &url.URL{}
*teamURL = *p.ValidateURL
teamURL.Path = "/2.0/teams"
req, err = http.NewRequest("GET",
teamURL.String()+"?role=member&access_token="+s.AccessToken, nil)
if err != nil {
log.Printf("failed building request %s", err)
return "", err
}
err = api.RequestJson(req, &teams)
if err != nil {
log.Printf("failed requesting teams membership %s", err)
debug(httputil.DumpRequestOut(req, true))
return "", err
}
var found = false
log.Printf("%+v\n", teams)
for _, team := range teams.Values {
if p.Team == team.Name {
found = true
break
}
}
if found != true {
log.Printf("team membership test failed, access denied")
return "", nil
}
}

for _, email := range emails.Values {
if email.Primary {
return email.Email, nil
}
}

return "", nil
}
152 changes: 152 additions & 0 deletions providers/bitbucket_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
package providers

import (
"log"
"net/http"
"net/http/httptest"
"net/url"
"testing"

"github.com/bmizerany/assert"
)

func testBitbucketProvider(hostname, team string) *BitbucketProvider {
p := NewBitbucketProvider(
&ProviderData{
ProviderName: "",
LoginURL: &url.URL{},
RedeemURL: &url.URL{},
ProfileURL: &url.URL{},
ValidateURL: &url.URL{},
Scope: ""})

if team != "" {
p.SetTeam(team)
}

if hostname != "" {
updateURL(p.Data().LoginURL, hostname)
updateURL(p.Data().RedeemURL, hostname)
updateURL(p.Data().ProfileURL, hostname)
updateURL(p.Data().ValidateURL, hostname)
}
return p
}

func testBitbucketBackend(payload string) *httptest.Server {
paths := map[string]bool{
"/2.0/user/emails": true,
"/2.0/teams": true,
}

return httptest.NewServer(http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) {
url := r.URL
if !paths[url.Path] {
log.Printf("%s not in %+v\n", url.Path, paths)
w.WriteHeader(404)
} else if r.URL.Query().Get("access_token") != "imaginary_access_token" {
w.WriteHeader(403)
} else {
w.WriteHeader(200)
w.Write([]byte(payload))
}
}))
}

func TestBitbucketProviderDefaults(t *testing.T) {
p := testBitbucketProvider("", "")
assert.NotEqual(t, nil, p)
assert.Equal(t, "Bitbucket", p.Data().ProviderName)
assert.Equal(t, "https://bitbucket.org/site/oauth2/authorize",
p.Data().LoginURL.String())
assert.Equal(t, "https://bitbucket.org/site/oauth2/access_token",
p.Data().RedeemURL.String())
assert.Equal(t, "https://api.bitbucket.org/2.0/user/emails",
p.Data().ValidateURL.String())
assert.Equal(t, "account team", p.Data().Scope)
}

func TestBitbucketProviderOverrides(t *testing.T) {
p := NewBitbucketProvider(
&ProviderData{
LoginURL: &url.URL{
Scheme: "https",
Host: "example.com",
Path: "/oauth/auth"},
RedeemURL: &url.URL{
Scheme: "https",
Host: "example.com",
Path: "/oauth/token"},
ValidateURL: &url.URL{
Scheme: "https",
Host: "example.com",
Path: "/api/v3/user"},
Scope: "profile"})
assert.NotEqual(t, nil, p)
assert.Equal(t, "Bitbucket", p.Data().ProviderName)
assert.Equal(t, "https://example.com/oauth/auth",
p.Data().LoginURL.String())
assert.Equal(t, "https://example.com/oauth/token",
p.Data().RedeemURL.String())
assert.Equal(t, "https://example.com/api/v3/user",
p.Data().ValidateURL.String())
assert.Equal(t, "profile", p.Data().Scope)
}

func TestBitbucketProviderGetEmailAddress(t *testing.T) {
b := testBitbucketBackend("{\"values\": [ { \"email\": \"michael.bland@gsa.gov\", \"is_primary\": true } ] }")
defer b.Close()

b_url, _ := url.Parse(b.URL)
p := testBitbucketProvider(b_url.Host, "")

session := &SessionState{AccessToken: "imaginary_access_token"}
email, err := p.GetEmailAddress(session)
assert.Equal(t, nil, err)
assert.Equal(t, "michael.bland@gsa.gov", email)
}

func TestBitbucketProviderGetEmailAddressAndGroup(t *testing.T) {
b := testBitbucketBackend("{\"values\": [ { \"email\": \"michael.bland@gsa.gov\", \"is_primary\": true, \"username\": \"bioinformatics\" } ] }")
defer b.Close()

b_url, _ := url.Parse(b.URL)
p := testBitbucketProvider(b_url.Host, "bioinformatics")

session := &SessionState{AccessToken: "imaginary_access_token"}
email, err := p.GetEmailAddress(session)
assert.Equal(t, nil, err)
assert.Equal(t, "michael.bland@gsa.gov", email)
}

// Note that trying to trigger the "failed building request" case is not
// practical, since the only way it can fail is if the URL fails to parse.
func TestBitbucketProviderGetEmailAddressFailedRequest(t *testing.T) {
b := testBitbucketBackend("unused payload")
defer b.Close()

b_url, _ := url.Parse(b.URL)
p := testBitbucketProvider(b_url.Host, "")

// We'll trigger a request failure by using an unexpected access
// token. Alternatively, we could allow the parsing of the payload as
// JSON to fail.
session := &SessionState{AccessToken: "unexpected_access_token"}
email, err := p.GetEmailAddress(session)
assert.NotEqual(t, nil, err)
assert.Equal(t, "", email)
}

func TestBitbucketProviderGetEmailAddressEmailNotPresentInPayload(t *testing.T) {
b := testBitbucketBackend("{\"foo\": \"bar\"}")
defer b.Close()

b_url, _ := url.Parse(b.URL)
p := testBitbucketProvider(b_url.Host, "")

session := &SessionState{AccessToken: "imaginary_access_token"}
email, err := p.GetEmailAddress(session)
assert.Equal(t, "", email)
assert.Equal(t, nil, err)
}
2 changes: 2 additions & 0 deletions providers/providers.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ func New(provider string, p *ProviderData) Provider {
return NewOIDCProvider(p)
case "discord":
return NewDiscordProvider(p)
case "bitbucket":
return NewBitbucketProvider(p)
default:
return NewGoogleProvider(p)
}
Expand Down

0 comments on commit aeada85

Please sign in to comment.