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
8 changes: 8 additions & 0 deletions config/config.yml_example
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,14 @@ vouch:
jwt: X-Vouch-Token
querystring: access_token
redirect: X-Vouch-Requested-URI
# If idToken is defined, all valid requests to /validate will receive a header (defined in the value) containing the id_token from the OpenID Provider
# If idToken is empty or undefined, no header will be added
# This will make the response largerwich in turn can affect the response time
# idToken: x-vouch-idtoken
# If accessToken is defined, all valid requests to /validate will receive a header (defined in the value) containing the access_token from the OpenID Provider
# If accessToken is empty or undefined, no header will be added
# This will make the response larger wich in turn can affect the response time
# accessToken: x-vouch-accesstoken

db:
file: data/vouch_bolt.db
Expand Down
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.Cfg.Headers.IDToken != "" {
w.Header().Add(cfg.Cfg.Headers.IDToken, claims.IDToken)
}
if cfg.Cfg.Headers.AccessToken != "" {
w.Header().Add(cfg.Cfg.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
22 changes: 20 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 @@ -236,8 +240,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 +356,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 +414,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 +442,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
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