Skip to content
This repository has been archived by the owner on Jan 24, 2019. It is now read-only.

Roles header with refresh configuration #277

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,7 @@ Usage of oauth2_proxy:
-pass-access-token=false: pass OAuth access_token to upstream via X-Forwarded-Access-Token header
-pass-basic-auth=true: pass HTTP Basic Auth, X-Forwarded-User and X-Forwarded-Email information to upstream
-pass-host-header=true: pass the request Host Header to upstream
-pass-roles-header=false: pass user's roles upstream via X-Forwarded-Roles header
-profile-url="": Profile access endpoint
-provider="google": OAuth provider
-proxy-prefix="/oauth2": the url root path that this proxy should be nested under (e.g. /<oauth2>/sign_in)
Expand Down
3 changes: 3 additions & 0 deletions contrib/oauth2_proxy.cfg.example
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@
## Pass OAuth Access token to upstream via "X-Forwarded-Access-Token"
# pass_access_token = false

## Pass roles the user has via X-Forwarded-Roles
# pass_roles_header = false

## Authenticated Email Addresses File (one email per line)
# authenticated_emails_file = ""

Expand Down
1 change: 1 addition & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ func main() {
flagSet.String("basic-auth-password", "", "the password to set when passing the HTTP Basic Auth header")
flagSet.Bool("pass-access-token", false, "pass OAuth access_token to upstream via X-Forwarded-Access-Token header")
flagSet.Bool("pass-host-header", true, "pass the request Host Header to upstream")
flagSet.Bool("pass-roles-header", false, "pass user's teams upstream via X-Forwarded-Roles header")
flagSet.Var(&skipAuthRegex, "skip-auth-regex", "bypass authentication for requests path's that match (may be given multiple times)")
flagSet.Bool("skip-provider-button", false, "will skip sign-in-page to directly reach the next step: oauth/start")

Expand Down
28 changes: 28 additions & 0 deletions oauthproxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ var SignatureHeaders []string = []string{
"X-Forwarded-User",
"X-Forwarded-Email",
"X-Forwarded-Access-Token",
"X-Forwarded-Roles",
"Cookie",
"Gap-Auth",
}
Expand Down Expand Up @@ -62,6 +63,7 @@ type OAuthProxy struct {
SkipProviderButton bool
BasicAuthPassword string
PassAccessToken bool
PassRolesHeader bool
CookieCipher *cookie.Cipher
skipAuthRegex []string
compiledRegex []*regexp.Regexp
Expand Down Expand Up @@ -196,6 +198,7 @@ func NewOAuthProxy(opts *Options, validator func(string) bool) *OAuthProxy {
PassBasicAuth: opts.PassBasicAuth,
BasicAuthPassword: opts.BasicAuthPassword,
PassAccessToken: opts.PassAccessToken,
PassRolesHeader: opts.PassRolesHeader,
SkipProviderButton: opts.SkipProviderButton,
CookieCipher: cipher,
templates: loadTemplates(opts.CustomTemplatesDir),
Expand Down Expand Up @@ -536,6 +539,9 @@ func (p *OAuthProxy) Authenticate(rw http.ResponseWriter, req *http.Request) int
}
if session != nil && sessionAge > p.CookieRefresh && p.CookieRefresh != time.Duration(0) {
log.Printf("%s refreshing %s old session cookie for %s (refresh after %s)", remoteAddr, sessionAge, session, p.CookieRefresh)
log.Printf("Refreshing role permissions for user")
rp := p.provider.(providers.RoleProvider)
rp.SetUserRoles(session.AccessToken)
saveSession = true
}

Expand Down Expand Up @@ -605,6 +611,28 @@ func (p *OAuthProxy) Authenticate(rw http.ResponseWriter, req *http.Request) int
if p.PassAccessToken && session.AccessToken != "" {
req.Header["X-Forwarded-Access-Token"] = []string{session.AccessToken}
}

if p.PassRolesHeader {
rp := p.provider.(providers.RoleProvider)
roles := rp.GetUserRoles()

// Upon restarting the proxy, if there is an existing cookie, we need to re-fetch roles from provider
// Project preference is to avoid cookie bloat, so we aren't storing roles in the cookie
// https://github.com/bitly/oauth2_proxy/issues/174#issuecomment-1578273584
var i = 0
if len(roles) < 1 && i < 1 {
i++
rp.SetUserRoles(session.AccessToken)
refreshedRoles := rp.GetUserRoles()
req.Header["X-Forwarded-Roles"] = []string{refreshedRoles}
log.Printf("Refreshed user role data - %v", refreshedRoles)

} else {
req.Header["X-Forwarded-Roles"] = []string{roles}
log.Printf("User role data - %v", roles)
}
}

if session.Email == "" {
rw.Header().Set("GAP-Auth", session.User)
} else {
Expand Down
9 changes: 9 additions & 0 deletions options.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ type Options struct {
BasicAuthPassword string `flag:"basic-auth-password" cfg:"basic_auth_password"`
PassAccessToken bool `flag:"pass-access-token" cfg:"pass_access_token"`
PassHostHeader bool `flag:"pass-host-header" cfg:"pass_host_header"`
PassRolesHeader bool `flag:"pass-roles-header" cfg:"pass_roles_header"`
SkipProviderButton bool `flag:"skip-provider-button" cfg:"skip_provider_button"`

// These options allow for other providers besides Google, with
Expand Down Expand Up @@ -96,6 +97,7 @@ func NewOptions() *Options {
PassBasicAuth: true,
PassAccessToken: false,
PassHostHeader: true,
PassRolesHeader: false,
SkipProviderButton: false,
ApprovalPrompt: "force",
RequestLogging: true,
Expand Down Expand Up @@ -187,6 +189,13 @@ func (o *Options) Validate() error {
o.CookieExpire.String()))
}

// Confirm the provider type supports sending user roles
if o.PassRolesHeader {
if _, ok := o.provider.(providers.RoleProvider); !ok {
msgs = append(msgs, "Provider '"+o.provider.Data().ProviderName+"' does not support sending a roles header.")
}
}

if len(o.GoogleGroups) > 0 || o.GoogleAdminEmail != "" || o.GoogleServiceAccountJSON != "" {
if len(o.GoogleGroups) < 1 {
msgs = append(msgs, "missing setting: google-group")
Expand Down
62 changes: 48 additions & 14 deletions providers/github.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,15 @@ import (

type GitHubProvider struct {
*ProviderData
Org string
Team string
Org string
Team string
userRoles []struct {
Name string `json:"name"`
Slug string `json:"slug"`
Org struct {
Login string `json:"login"`
} `json:"organization"`
}
}

func NewGitHubProvider(p *ProviderData) *GitHubProvider {
Expand Down Expand Up @@ -46,6 +53,7 @@ func NewGitHubProvider(p *ProviderData) *GitHubProvider {
}
return &GitHubProvider{ProviderData: p}
}

func (p *GitHubProvider) SetOrgTeam(org, team string) {
p.Org = org
p.Team = team
Expand Down Expand Up @@ -73,6 +81,7 @@ func (p *GitHubProvider) hasOrg(accessToken string) (bool, error) {
RawQuery: params.Encode(),
}
req, _ := http.NewRequest("GET", endpoint.String(), nil)

req.Header.Set("Accept", "application/vnd.github.v3+json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
Expand Down Expand Up @@ -105,17 +114,9 @@ func (p *GitHubProvider) hasOrg(accessToken string) (bool, error) {
return false, nil
}

func (p *GitHubProvider) hasOrgAndTeam(accessToken string) (bool, error) {
// https://developer.github.com/v3/orgs/teams/#list-user-teams

var teams []struct {
Name string `json:"name"`
Slug string `json:"slug"`
Org struct {
Login string `json:"login"`
} `json:"organization"`
}
func (p *GitHubProvider) SetUserRoles(accessToken string) (bool, error) {

// https://developer.github.com/v3/orgs/teams/#list-user-teams
params := url.Values{
"access_token": {accessToken},
"limit": {"100"},
Expand Down Expand Up @@ -143,14 +144,21 @@ func (p *GitHubProvider) hasOrgAndTeam(accessToken string) (bool, error) {
return false, fmt.Errorf("got %d from %q %s", resp.StatusCode, endpoint, body)
}

if err := json.Unmarshal(body, &teams); err != nil {
if err := json.Unmarshal(body, &p.userRoles); err != nil {
return false, fmt.Errorf("%s unmarshaling %s", err, body)
}

log.Printf("Returned roles - %v", p.userRoles)

return true, nil
}

func (p *GitHubProvider) hasOrgAndTeam(accessToken string) (bool, error) {

var hasOrg bool
presentOrgs := make(map[string]bool)
var presentTeams []string
for _, team := range teams {
for _, team := range p.userRoles {
presentOrgs[team.Org.Login] = true
if p.Org == team.Org.Login {
hasOrg = true
Expand Down Expand Up @@ -183,6 +191,10 @@ func (p *GitHubProvider) GetEmailAddress(s *SessionState) (string, error) {
Primary bool `json:"primary"`
}

if ok, err := p.SetUserRoles(s.AccessToken); err != nil || !ok {
return "", err
}

// if we require an Org or Team, check that first
if p.Org != "" {
if p.Team != "" {
Expand Down Expand Up @@ -234,3 +246,25 @@ func (p *GitHubProvider) GetEmailAddress(s *SessionState) (string, error) {

return "", nil
}

// Return a filtered list of all teams assigned to a user by the organization defined in the configuration
func (p *GitHubProvider) GetUserRoles() string {

// Todo - could abstract this filtering and refactor hasOrgAndTeam()
presentOrgs := make(map[string]bool)
var presentRoles []string
for _, team := range p.userRoles {
presentOrgs[team.Org.Login] = true
if p.Org == team.Org.Login {
ts := strings.Split(p.Team, ",")
for _, t := range ts {
if t == team.Slug {
log.Printf("Found Github Organization:%q Team:%q (Name:%q)", team.Org.Login, team.Slug, team.Name)
}
}
presentRoles = append(presentRoles, team.Slug)
}
}

return strings.Join(presentRoles, ",")
}
3 changes: 1 addition & 2 deletions providers/provider_default.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import (
"net/http"
"net/url"
"strings"

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

Expand Down Expand Up @@ -122,4 +121,4 @@ func (p *ProviderData) ValidateSessionState(s *SessionState) bool {
// RefreshSessionIfNeeded
func (p *ProviderData) RefreshSessionIfNeeded(s *SessionState) (bool, error) {
return false, nil
}
}
11 changes: 11 additions & 0 deletions providers/providers.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import (
"github.com/bitly/oauth2_proxy/cookie"
)

// Provider is the primary interface for an authentication provider
// all provider
type Provider interface {
Data() *ProviderData
GetEmailAddress(*SessionState) (string, error)
Expand All @@ -16,6 +18,15 @@ type Provider interface {
CookieForSession(*SessionState, *cookie.Cipher) (string, error)
}

// RoleProvider is an optional interface that exposes a list of roles
// for a user. For Providers like GitHub this would be the teams the user
// is a member of.
type RoleProvider interface {
GetUserRoles() string
SetUserRoles(string) (bool, error)
}

// New gives you an instance of the given provider
func New(provider string, p *ProviderData) Provider {
switch provider {
case "myusa":
Expand Down