-
Notifications
You must be signed in to change notification settings - Fork 329
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
17 changed files
with
1,027 additions
and
380 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,112 @@ | ||
package adfs | ||
|
||
import ( | ||
"encoding/base64" | ||
"encoding/json" | ||
"github.com/vouch/vouch-proxy/handlers/common" | ||
"github.com/vouch/vouch-proxy/pkg/cfg" | ||
"github.com/vouch/vouch-proxy/pkg/structs" | ||
"io/ioutil" | ||
"net/http" | ||
"net/url" | ||
"regexp" | ||
"strconv" | ||
"strings" | ||
) | ||
|
||
type Handler struct{} | ||
|
||
type adfsTokenRes struct { | ||
AccessToken string `json:"access_token"` | ||
TokenType string `json:"token_type"` | ||
IDToken string `json:"id_token"` | ||
ExpiresIn int64 `json:"expires_in"` // relative seconds from now | ||
} | ||
|
||
var ( | ||
log = cfg.Cfg.Logger | ||
) | ||
|
||
// More info: https://docs.microsoft.com/en-us/windows-server/identity/ad-fs/overview/ad-fs-scenarios-for-developers#supported-scenarios | ||
func (Handler) GetUserInfo(r *http.Request, user *structs.User, customClaims *structs.CustomClaims, ptokens *structs.PTokens) (rerr error) { | ||
code := r.URL.Query().Get("code") | ||
log.Debugf("code: %s", code) | ||
|
||
formData := url.Values{} | ||
formData.Set("code", code) | ||
formData.Set("grant_type", "authorization_code") | ||
formData.Set("resource", cfg.GenOAuth.RedirectURL) | ||
formData.Set("client_id", cfg.GenOAuth.ClientID) | ||
formData.Set("redirect_uri", cfg.GenOAuth.RedirectURL) | ||
if cfg.GenOAuth.ClientSecret != "" { | ||
formData.Set("client_secret", cfg.GenOAuth.ClientSecret) | ||
} | ||
req, err := http.NewRequest("POST", cfg.GenOAuth.TokenURL, strings.NewReader(formData.Encode())) | ||
if err != nil { | ||
return err | ||
} | ||
req.Header.Add("Content-Type", "application/x-www-form-urlencoded") | ||
req.Header.Add("Content-Length", strconv.Itoa(len(formData.Encode()))) | ||
req.Header.Set("Accept", "application/json") | ||
|
||
client := &http.Client{} | ||
userinfo, err := client.Do(req) | ||
|
||
if err != nil { | ||
return err | ||
} | ||
defer func() { | ||
if err := userinfo.Body.Close(); err != nil { | ||
rerr = err | ||
} | ||
}() | ||
|
||
data, _ := ioutil.ReadAll(userinfo.Body) | ||
tokenRes := adfsTokenRes{} | ||
|
||
if err := json.Unmarshal(data, &tokenRes); err != nil { | ||
log.Errorf("oauth2: cannot fetch token: %v", err) | ||
return nil | ||
} | ||
|
||
ptokens.PAccessToken = string(tokenRes.AccessToken) | ||
ptokens.PIdToken = string(tokenRes.IDToken) | ||
|
||
s := strings.Split(tokenRes.IDToken, ".") | ||
if len(s) < 2 { | ||
log.Error("jws: invalid token received") | ||
return nil | ||
} | ||
|
||
idToken, err := base64.RawURLEncoding.DecodeString(s[1]) | ||
if err != nil { | ||
log.Error(err) | ||
return nil | ||
} | ||
log.Debugf("idToken: %+v", string(idToken)) | ||
|
||
adfsUser := structs.ADFSUser{} | ||
json.Unmarshal([]byte(idToken), &adfsUser) | ||
log.Infof("adfs adfsUser: %+v", adfsUser) | ||
// data contains an access token, refresh token, and id token | ||
// Please note that in order for custom claims to work you MUST set allatclaims in ADFS to be passed | ||
// https://oktotechnologies.ca/2018/08/26/adfs-openidconnect-configuration/ | ||
if err = common.MapClaims([]byte(idToken), customClaims); err != nil { | ||
log.Error(err) | ||
return err | ||
} | ||
adfsUser.PrepareUserData() | ||
var rxEmail = regexp.MustCompile("^[a-zA-Z0-9.!#$%&'*+\\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$") | ||
|
||
if len(adfsUser.Email) == 0 { | ||
// If the email is blank, we will try to determine if the UPN is an email. | ||
if rxEmail.MatchString(adfsUser.UPN) { | ||
// Set the email from UPN if there is a valid email present. | ||
adfsUser.Email = adfsUser.UPN | ||
} | ||
} | ||
user.Username = adfsUser.Username | ||
user.Email = adfsUser.Email | ||
log.Debugf("User Obj: %+v", user) | ||
return nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
package common | ||
|
||
import ( | ||
"context" | ||
"encoding/json" | ||
"github.com/vouch/vouch-proxy/pkg/cfg" | ||
"github.com/vouch/vouch-proxy/pkg/structs" | ||
"golang.org/x/oauth2" | ||
"net/http" | ||
) | ||
|
||
var ( | ||
log = cfg.Cfg.Logger | ||
) | ||
|
||
func PrepareTokensAndClient(r *http.Request, ptokens *structs.PTokens, setpid bool) (error, *http.Client, *oauth2.Token) { | ||
providerToken, err := cfg.OAuthClient.Exchange(context.TODO(), r.URL.Query().Get("code")) | ||
if err != nil { | ||
return err, nil, nil | ||
} | ||
ptokens.PAccessToken = providerToken.AccessToken | ||
|
||
if setpid { | ||
if providerToken.Extra("id_token") != nil { | ||
// Certain providers (eg. gitea) don't provide an id_token | ||
// and it's not neccessary for the authentication phase | ||
ptokens.PIdToken = providerToken.Extra("id_token").(string) | ||
} else { | ||
log.Debugf("id_token missing - may not be supported by this provider") | ||
} | ||
} | ||
|
||
log.Debugf("ptokens: %+v", ptokens) | ||
|
||
client := cfg.OAuthClient.Client(context.TODO(), providerToken) | ||
return err, client, providerToken | ||
} | ||
|
||
func MapClaims(claims []byte, customClaims *structs.CustomClaims) error { | ||
// Create a struct that contains the claims that we want to store from the config. | ||
var f interface{} | ||
err := json.Unmarshal(claims, &f) | ||
if err != nil { | ||
log.Error("Error unmarshaling claims") | ||
return err | ||
} | ||
m := f.(map[string]interface{}) | ||
for k := range m { | ||
var found = false | ||
for _, e := range cfg.Cfg.Headers.Claims { | ||
if k == e { | ||
found = true | ||
} | ||
} | ||
if found == false { | ||
delete(m, k) | ||
} | ||
} | ||
customClaims.Claims = m | ||
return nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,168 @@ | ||
package github | ||
|
||
import ( | ||
"encoding/json" | ||
"errors" | ||
"github.com/vouch/vouch-proxy/handlers/common" | ||
"github.com/vouch/vouch-proxy/pkg/cfg" | ||
"github.com/vouch/vouch-proxy/pkg/structs" | ||
"golang.org/x/oauth2" | ||
"io/ioutil" | ||
"net/http" | ||
"strings" | ||
) | ||
|
||
type Handler struct { | ||
PrepareTokensAndClient func(*http.Request, *structs.PTokens, bool) (error, *http.Client, *oauth2.Token) | ||
} | ||
|
||
var ( | ||
log = cfg.Cfg.Logger | ||
) | ||
|
||
// github | ||
// https://developer.github.com/apps/building-integrations/setting-up-and-registering-oauth-apps/about-authorization-options-for-oauth-apps/ | ||
func (me Handler) GetUserInfo(r *http.Request, user *structs.User, customClaims *structs.CustomClaims, ptokens *structs.PTokens) (rerr error) { | ||
err, client, ptoken := me.PrepareTokensAndClient(r, ptokens, true) | ||
if err != nil { | ||
// http.Error(w, err.Error(), http.StatusBadRequest) | ||
return err | ||
} | ||
log.Errorf("ptoken.AccessToken: %s", ptoken.AccessToken) | ||
userinfo, err := client.Get(cfg.GenOAuth.UserInfoURL + ptoken.AccessToken) | ||
if err != nil { | ||
// http.Error(w, err.Error(), http.StatusBadRequest) | ||
return err | ||
} | ||
defer func() { | ||
if err := userinfo.Body.Close(); err != nil { | ||
rerr = err | ||
} | ||
}() | ||
data, _ := ioutil.ReadAll(userinfo.Body) | ||
log.Infof("github userinfo body: %s", string(data)) | ||
if err = common.MapClaims(data, customClaims); err != nil { | ||
log.Error(err) | ||
return err | ||
} | ||
ghUser := structs.GitHubUser{} | ||
if err = json.Unmarshal(data, &ghUser); err != nil { | ||
log.Error(err) | ||
return err | ||
} | ||
log.Debug("getUserInfoFromGitHub ghUser") | ||
log.Debug(ghUser) | ||
log.Debug("getUserInfoFromGitHub user") | ||
log.Debug(user) | ||
|
||
ghUser.PrepareUserData() | ||
user.Email = ghUser.Email | ||
user.Name = ghUser.Name | ||
user.Username = ghUser.Username | ||
user.ID = ghUser.ID | ||
|
||
// user = &ghUser.User | ||
|
||
toOrgAndTeam := func(orgAndTeam string) (string, string) { | ||
split := strings.Split(orgAndTeam, "/") | ||
if len(split) == 1 { | ||
// only organization given | ||
return orgAndTeam, "" | ||
} else if len(split) == 2 { | ||
return split[0], split[1] | ||
} else { | ||
return "", "" | ||
} | ||
} | ||
|
||
if len(cfg.Cfg.TeamWhiteList) != 0 { | ||
for _, orgAndTeam := range cfg.Cfg.TeamWhiteList { | ||
org, team := toOrgAndTeam(orgAndTeam) | ||
if org != "" { | ||
log.Info(org) | ||
var ( | ||
e error | ||
isMember bool | ||
) | ||
if team != "" { | ||
e, isMember = getTeamMembershipStateFromGitHub(client, user, org, team, ptoken) | ||
} else { | ||
e, isMember = getOrgMembershipStateFromGitHub(client, user, org, ptoken) | ||
} | ||
if e != nil { | ||
return e | ||
} else { | ||
if isMember { | ||
user.TeamMemberships = append(user.TeamMemberships, orgAndTeam) | ||
} | ||
} | ||
} else { | ||
log.Warnf("Invalid org/team format in %s: must be written as <orgId>/<teamSlug>", orgAndTeam) | ||
} | ||
} | ||
} | ||
|
||
log.Debug("getUserInfoFromGitHub") | ||
log.Debug(user) | ||
return nil | ||
} | ||
|
||
func getOrgMembershipStateFromGitHub(client *http.Client, user *structs.User, orgId string, ptoken *oauth2.Token) (rerr error, isMember bool) { | ||
replacements := strings.NewReplacer(":org_id", orgId, ":username", user.Username) | ||
orgMembershipResp, err := client.Get(replacements.Replace(cfg.GenOAuth.UserOrgURL) + ptoken.AccessToken) | ||
if err != nil { | ||
log.Error(err) | ||
return err, false | ||
} | ||
|
||
if orgMembershipResp.StatusCode == 302 { | ||
log.Debug("Need to check public membership") | ||
location := orgMembershipResp.Header.Get("Location") | ||
if location != "" { | ||
orgMembershipResp, err = client.Get(location) | ||
} | ||
} | ||
|
||
if orgMembershipResp.StatusCode == 204 { | ||
log.Debug("getOrgMembershipStateFromGitHub isMember: true") | ||
return nil, true | ||
} else if orgMembershipResp.StatusCode == 404 { | ||
log.Debug("getOrgMembershipStateFromGitHub isMember: false") | ||
return nil, false | ||
} else { | ||
log.Errorf("getOrgMembershipStateFromGitHub: unexpected status code %d", orgMembershipResp.StatusCode) | ||
return errors.New("Unexpected response status " + orgMembershipResp.Status), false | ||
} | ||
} | ||
|
||
func getTeamMembershipStateFromGitHub(client *http.Client, user *structs.User, orgId string, team string, ptoken *oauth2.Token) (rerr error, isMember bool) { | ||
replacements := strings.NewReplacer(":org_id", orgId, ":team_slug", team, ":username", user.Username) | ||
membershipStateResp, err := client.Get(replacements.Replace(cfg.GenOAuth.UserTeamURL) + ptoken.AccessToken) | ||
if err != nil { | ||
log.Error(err) | ||
return err, false | ||
} | ||
defer func() { | ||
if err := membershipStateResp.Body.Close(); err != nil { | ||
rerr = err | ||
} | ||
}() | ||
if membershipStateResp.StatusCode == 200 { | ||
data, _ := ioutil.ReadAll(membershipStateResp.Body) | ||
log.Infof("github team membership body: ", string(data)) | ||
ghTeamState := structs.GitHubTeamMembershipState{} | ||
if err = json.Unmarshal(data, &ghTeamState); err != nil { | ||
log.Error(err) | ||
return err, false | ||
} | ||
log.Debug("getTeamMembershipStateFromGitHub ghTeamState") | ||
log.Debug(ghTeamState) | ||
return nil, ghTeamState.State == "active" | ||
} else if membershipStateResp.StatusCode == 404 { | ||
log.Debug("getTeamMembershipStateFromGitHub isMember: false") | ||
return nil, false | ||
} else { | ||
log.Errorf("getTeamMembershipStateFromGitHub: unexpected status code %d", membershipStateResp.StatusCode) | ||
return errors.New("Unexpected response status " + membershipStateResp.Status), false | ||
} | ||
} |
Oops, something went wrong.