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

Add idtoken response #71

Closed
85 changes: 84 additions & 1 deletion handlers/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import (
"io/ioutil"
"mime/multipart"
"net/http"
"net/url"
"strconv"
"strings"

log "github.com/Sirupsen/logrus"
Expand Down Expand Up @@ -48,7 +50,7 @@ var (
func randString() string {
b := make([]byte, 32)
rand.Read(b)
return base64.StdEncoding.EncodeToString(b)
return base64.URLEncoding.EncodeToString(b)
}

func loginURL(r *http.Request, state string) string {
Expand All @@ -73,6 +75,8 @@ func loginURL(r *http.Request, state string) string {
}
} else if cfg.GenOAuth.Provider == cfg.Providers.IndieAuth {
url = cfg.OAuthClient.AuthCodeURL(state, oauth2.SetAuthURLParam("response_type", "id"))
} else if cfg.GenOAuth.Provider == cfg.Providers.ADFS {
url = cfg.OAuthClient.AuthCodeURL(state, cfg.OAuthopts)
} else {
url = cfg.OAuthClient.AuthCodeURL(state)
}
Expand Down Expand Up @@ -188,6 +192,12 @@ func ValidateRequestHandler(w http.ResponseWriter, r *http.Request) {
}

w.Header().Add(cfg.Cfg.Headers.User, claims.Username)
if cfg.Get("Headers.IDToken") != "" {
w.Header().Add(cfg.Get("Headers.IDToken"), claims.IDToken)
}
if cfg.Get("Headers.AccessToken") != "" {
w.Header().Add(cfg.Get("Headers.AccessToken"), claims.AccessToken)
}
w.Header().Add(cfg.Cfg.Headers.Success, "true")
log.WithFields(log.Fields{cfg.Cfg.Headers.User: w.Header().Get(cfg.Cfg.Headers.User)}).Debug("response header")

Expand Down Expand Up @@ -424,6 +434,8 @@ func getUserInfo(r *http.Request, user *structs.User) error {
// indieauth sends the "me" setting in json back to the callback, so just pluck it from the callback
if cfg.GenOAuth.Provider == cfg.Providers.IndieAuth {
return getUserInfoFromIndieAuth(r, user)
} else if cfg.GenOAuth.Provider == cfg.Providers.ADFS {
return getUserInfoFromADFS(r, user)
}

providerToken, err := cfg.OAuthClient.Exchange(oauth2.NoContext, r.URL.Query().Get("code"))
Expand Down Expand Up @@ -569,6 +581,77 @@ func getUserInfoFromIndieAuth(r *http.Request, user *structs.User) error {
return nil
}

// More info: https://docs.microsoft.com/en-us/windows-server/identity/ad-fs/overview/ad-fs-scenarios-for-developers#supported-scenarios
func getUserInfoFromADFS(r *http.Request, user *structs.User) error {
code := r.URL.Query().Get("code")
log.Errorf("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)
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")

// v := url.Values{}
// userinfo, err := client.PostForm(cfg.GenOAuth.UserInfoURL, v)

client := &http.Client{}
userinfo, err := client.Do(req)

if err != nil {
// http.Error(w, err.Error(), http.StatusBadRequest)
return err
}
defer userinfo.Body.Close()

body, _ := ioutil.ReadAll(userinfo.Body)

var tokenRes 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
}

if err := json.Unmarshal(body, &tokenRes); err != nil {
log.Errorf("oauth2: cannot fetch token: %v", err)
return nil
}

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
}

adfsUser := structs.ADFSUser{}
json.Unmarshal([]byte(idToken), &adfsUser)
log.Println("adfs adfsUser: ", adfsUser)

adfsUser.PrepareUserData()
user.Username = adfsUser.UPN
user.IDToken = string(tokenRes.IDToken)
user.AccessToken = string(tokenRes.AccessToken)
log.Debug(user)
return nil
}

// the standard error
// this is captured by nginx, which converts the 401 into 302 to the login page
func error401(w http.ResponseWriter, r *http.Request, ae AuthError) {
Expand Down
32 changes: 30 additions & 2 deletions pkg/cfg/cfg.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ type config struct {
}
Headers struct {
JWT string `mapstructure:"jwt"`
IDToken string `mapstructure:"idToken"`
AccessToken string `mapstructure:"accessToken"`
User string `mapstructure:"user"`
QueryString string `mapstructure:"querystring"`
Redirect string `mapstructure:"redirect"`
Expand Down Expand Up @@ -79,6 +81,7 @@ type OAuthProviders struct {
Google string
GitHub string
IndieAuth string
ADFS string
OIDC string
}

Expand Down Expand Up @@ -113,6 +116,7 @@ var (
Google: "google",
GitHub: "github",
IndieAuth: "indieauth",
ADFS: "adfs",
OIDC: "oidc",
}

Expand Down Expand Up @@ -213,6 +217,16 @@ func Get(key string) string {
return viper.GetString(key)
}

// GetInt int value for key
func GetInt(key string) int {
return viper.GetInt(key)
}

// GetBool bool value for key
func GetBool(key string) bool {
return viper.GetBool(key)
}

// BasicTest just a quick sanity check to see if the config is sound
func BasicTest() error {
for _, opt := range RequiredOptions {
Expand All @@ -236,8 +250,8 @@ func BasicTest() error {
case GenOAuth.Provider != Providers.Google && GenOAuth.AuthURL == "":
// everyone except IndieAuth and Google has an authURL
return errors.New("configuration error: oauth.auth_url not found")
case GenOAuth.Provider != Providers.Google && GenOAuth.Provider != Providers.IndieAuth && GenOAuth.UserInfoURL == "":
// everyone except IndieAuth and Google has an userInfoURL
case GenOAuth.Provider != Providers.Google && GenOAuth.Provider != Providers.IndieAuth && GenOAuth.Provider != Providers.ADFS && GenOAuth.UserInfoURL == "":
// everyone except IndieAuth, Google and ADFS has an userInfoURL
return errors.New("configuration error: oauth.user_info_url not found")
}

Expand Down Expand Up @@ -352,6 +366,12 @@ func setDefaults() {
if !viper.IsSet(Branding.LCName + ".headers.jwt") {
Cfg.Headers.JWT = "X-" + Branding.CcName + "-Token"
}
if !viper.IsSet(Branding.LCName + ".headers.idToken") {
Cfg.Headers.IDToken = ""
}
if !viper.IsSet(Branding.LCName + ".headers.accessToken") {
Cfg.Headers.AccessToken = ""
}
if !viper.IsSet(Branding.LCName + ".headers.querystring") {
Cfg.Headers.QueryString = "access_token"
}
Expand Down Expand Up @@ -404,6 +424,9 @@ func setDefaults() {
} else if GenOAuth.Provider == Providers.GitHub {
setDefaultsGitHub()
configureOAuthClient()
} else if GenOAuth.Provider == Providers.ADFS {
setDefaultsADFS()
configureOAuthClient()
} else {
configureOAuthClient()
}
Expand All @@ -429,6 +452,11 @@ func setDefaultsGoogle() {
}
}

func setDefaultsADFS() {
log.Info("configuring ADFS OAuth")
OAuthopts = oauth2.SetAuthURLParam("resource", GenOAuth.RedirectURL) // Needed or all claims won't be included
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suspect this should be in #68 :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it should! I'm going to try and move it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bnfinet I've been able to add it to #68, but I don't think I'll be able to create a PR for idToken without ADFS support (since that's what I'm using).

func setDefaultsGitHub() {
// log.Info("configuring GitHub OAuth")
if GenOAuth.AuthURL == "" {
Expand Down
17 changes: 9 additions & 8 deletions pkg/cookie/cookie.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ import (
"github.com/vouch/vouch-proxy/pkg/domains"
)

var defaultMaxAge = cfg.Cfg.JWT.MaxAge * 60
var defaultMaxAge = cfg.GetInt("JWT.MaxAge") * 60


// SetCookie http
func SetCookie(w http.ResponseWriter, r *http.Request, val string) {
Expand All @@ -24,25 +25,25 @@ func setCookie(w http.ResponseWriter, r *http.Request, val string, maxAge int) {
}
domain := domains.Matches(r.Host)
// Allow overriding the cookie domain in the config file
if cfg.Cfg.Cookie.Domain != "" {
domain = cfg.Cfg.Cookie.Domain
if cfg.Get("Cookie.Domain") != "" {
domain = cfg.Get("Cookie.Domain")
log.Debugf("setting the cookie domain to %v", domain)
}
// log.Debugf("cookie %s expires %d", cfg.Cfg.Cookie.Name, expires)
http.SetCookie(w, &http.Cookie{
Name: cfg.Cfg.Cookie.Name,
Name: cfg.Get("Cookie.Name"),
Value: val,
Path: "/",
Domain: domain,
MaxAge: maxAge,
Secure: cfg.Cfg.Cookie.Secure,
HttpOnly: cfg.Cfg.Cookie.HTTPOnly,
Secure: cfg.GetBool("Cookie.Secure"),
HttpOnly: cfg.GetBool("Cookie.HTTPOnly"),
})
}

// Cookie get the vouch jwt cookie
func Cookie(r *http.Request) (string, error) {
cookie, err := r.Cookie(cfg.Cfg.Cookie.Name)
cookie, err := r.Cookie(cfg.Get("Cookie.Name"))
if err != nil {
return "", err
}
Expand All @@ -51,7 +52,7 @@ func Cookie(r *http.Request) (string, error) {
}

log.WithFields(log.Fields{
"cookieName": cfg.Cfg.Cookie.Name,
"cookieName": cfg.Get("Cookie.Name"),
"cookieValue": cookie.Value,
}).Debug("cookie")
return cookie.Value, err
Expand Down
8 changes: 6 additions & 2 deletions pkg/jwtmanager/jwtmanager.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,10 @@ import (

// VouchClaims jwt Claims specific to vouch
type VouchClaims struct {
Username string `json:"username"`
Sites []string `json:"sites"` // tempting to make this a map but the array is fewer characters in the jwt
Username string `json:"username"`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've been wondering if this should be sub instead of username. Do you have an opinion?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No opinion from my side! :)

Sites []string `json:"sites"` // tempting to make this a map but the array is fewer characters in the jwt
IDToken string `json:"id_token"`
AccessToken string `json:"access_token"`
jwt.StandardClaims
}

Expand Down Expand Up @@ -53,6 +55,8 @@ func CreateUserTokenString(u structs.User) string {
claims := VouchClaims{
u.Username,
Sites,
u.IDToken,
u.AccessToken,
StandardClaims,
}

Expand Down
2 changes: 2 additions & 0 deletions pkg/jwtmanager/jwtmanager_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ func init() {
lc = VouchClaims{
u1.Username,
Sites,
u1.IDToken,
u1.AccessToken,
StandardClaims,
}
}
Expand Down
30 changes: 24 additions & 6 deletions pkg/structs/structs.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,14 @@ type User struct {
// TODO: set Provider here so that we can pass it to db
// populated by db (via mapstructure) or from provider (via json)
// Provider string `json:"provider",mapstructure:"provider"`
Username string `json:"username",mapstructure:"username"`
Name string `json:"name",mapstructure:"name"`
Email string `json:"email",mapstructure:"email"`
CreatedOn int64 `json:"createdon"`
LastUpdate int64 `json:"lastupdate"`
ID int `json:"id",mapstructure:"id"`
Username string `json:"username",mapstructure:"username"`
Name string `json:"name",mapstructure:"name"`
Email string `json:"email",mapstructure:"email"`
CreatedOn int64 `json:"createdon"`
LastUpdate int64 `json:"lastupdate"`
ID int `json:"id",mapstructure:"id"`
IDToken string `json:"id_token",mapstructure:"id_token"`
AccessToken string `json:"access_token,mapstructure:"id_token"`
// jwt.StandardClaims
}

Expand Down Expand Up @@ -47,6 +49,22 @@ func (u *GoogleUser) PrepareUserData() {
u.Username = u.Email
}

type ADFSUser struct {
User
Sub string `json:"sub"`
UPN string `json:"upn"`
// UniqueName string `json:"unique_name"`
// PwdExp string `json:"pwd_exp"`
// SID string `json:"sid"`
// Groups string `json:"groups"`
// jwt.StandardClaims
}

// PrepareUserData implement PersonalData interface
func (u *ADFSUser) PrepareUserData() {
u.Username = u.UPN
}

// GitHubUser is a retrieved and authentiacted user from GitHub.
type GitHubUser struct {
User
Expand Down