mirror of https://github.com/vouch/vouch-proxy
181 lines
5.9 KiB
Go
181 lines
5.9 KiB
Go
/*
|
|
|
|
Copyright 2020 The Vouch Proxy Authors.
|
|
Use of this source code is governed by The MIT License (MIT) that
|
|
can be found in the LICENSE file. Software distributed under The
|
|
MIT License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES
|
|
OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
*/
|
|
|
|
package handlers
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
|
|
"github.com/vouch/vouch-proxy/pkg/cfg"
|
|
"github.com/vouch/vouch-proxy/pkg/cookie"
|
|
"github.com/vouch/vouch-proxy/pkg/domains"
|
|
"github.com/vouch/vouch-proxy/pkg/jwtmanager"
|
|
"github.com/vouch/vouch-proxy/pkg/responses"
|
|
"github.com/vouch/vouch-proxy/pkg/structs"
|
|
|
|
"golang.org/x/oauth2"
|
|
)
|
|
|
|
// CallbackHandler /auth
|
|
// - redirects to /auth/{state}/ with the state coming from the query parameter
|
|
func CallbackHandler(w http.ResponseWriter, r *http.Request) {
|
|
log.Debug("/auth")
|
|
|
|
// did the IdP return an error?
|
|
errorIDP := r.URL.Query().Get("error")
|
|
if errorIDP != "" {
|
|
errorDescription := r.URL.Query().Get("error_description")
|
|
responses.Error401HTTP(w, r, fmt.Errorf("/auth Error from IdP: %s - %s", errorIDP, errorDescription))
|
|
return
|
|
}
|
|
|
|
queryState := r.URL.Query().Get("state")
|
|
if queryState == "" {
|
|
responses.Error400(w, r, fmt.Errorf("/auth: could not find state in query %s", r.URL.RawQuery))
|
|
return
|
|
}
|
|
|
|
// has to have a trailing / in its path, because the path of the session cookie is set to /auth/{state}/.
|
|
// see note in login.go and https://github.com/vouch/vouch-proxy/issues/373
|
|
authStateURL := fmt.Sprintf("%s/auth/%s/?%s", cfg.Cfg.DocumentRoot, queryState, r.URL.RawQuery)
|
|
responses.Redirect302(w, r, authStateURL)
|
|
}
|
|
|
|
// AuthStateHandler /auth/{state}/
|
|
// - validate info from oauth provider (Google, GitHub, OIDC, etc)
|
|
// - issue jwt in the form of a cookie
|
|
func AuthStateHandler(w http.ResponseWriter, r *http.Request) {
|
|
log.Debug("/auth/{state}/")
|
|
// Handle the exchange code to initiate a transport.
|
|
|
|
session, err := sessstore.Get(r, cfg.Cfg.Session.Name)
|
|
if err != nil {
|
|
responses.Error400(w, r, fmt.Errorf("/auth %w: could not find session store %s", err, cfg.Cfg.Session.Name))
|
|
return
|
|
}
|
|
|
|
// is the nonce "state" valid?
|
|
queryState := r.URL.Query().Get("state")
|
|
if session.Values["state"] != queryState {
|
|
responses.Error400(w, r, fmt.Errorf("/auth Invalid session state: stored %s, returned %s", session.Values["state"], queryState))
|
|
return
|
|
}
|
|
|
|
user := structs.User{}
|
|
customClaims := structs.CustomClaims{}
|
|
ptokens := structs.PTokens{}
|
|
|
|
// is code challenge enabled?
|
|
authCodeOptions := []oauth2.AuthCodeOption{}
|
|
|
|
if cfg.GenOAuth.CodeChallengeMethod != "" {
|
|
authCodeOptions = []oauth2.AuthCodeOption{
|
|
oauth2.SetAuthURLParam("code_challenge", session.Values["codeChallenge"].(string)),
|
|
oauth2.SetAuthURLParam("code_verifier", session.Values["codeVerifier"].(string)),
|
|
}
|
|
}
|
|
|
|
if err := getUserInfo(r, &user, &customClaims, &ptokens, authCodeOptions...); err != nil {
|
|
responses.Error400(w, r, fmt.Errorf("/auth Error while retrieving user info after successful login at the OAuth provider: %w", err))
|
|
return
|
|
}
|
|
log.Debugf("/auth/{state}/ Claims from userinfo: %+v", customClaims)
|
|
|
|
// verify / authz the user
|
|
if ok, err := verifyUser(user); !ok {
|
|
responses.Error403(w, r, fmt.Errorf("/auth User is not authorized: %w . Please try again or seek support from your administrator", err))
|
|
return
|
|
}
|
|
|
|
// SUCCESS!! they are authorized
|
|
|
|
// issue the jwt
|
|
|
|
tokenstring, err := jwtmanager.NewVPJWT(user, customClaims, ptokens)
|
|
if err != nil {
|
|
responses.Error500(w, r, fmt.Errorf("/auth Token creation failure: %w . Please seek support from your administrator", err))
|
|
return
|
|
|
|
}
|
|
cookie.SetCookie(w, r, tokenstring)
|
|
|
|
// get the originally requested URL so we can send them on their way
|
|
requestedURL := session.Values["requestedURL"].(string)
|
|
if requestedURL != "" {
|
|
// clear out the session value
|
|
session.Values["requestedURL"] = ""
|
|
session.Values[requestedURL] = 0
|
|
session.Options.MaxAge = -1
|
|
if err = session.Save(r, w); err != nil {
|
|
log.Error(err)
|
|
}
|
|
|
|
responses.Redirect302(w, r, requestedURL)
|
|
return
|
|
}
|
|
|
|
// otherwise serve an error
|
|
responses.RenderIndex(w, "/auth "+tokenstring)
|
|
}
|
|
|
|
// verifyUser validates that the domains match for the user
|
|
func verifyUser(u interface{}) (bool, error) {
|
|
|
|
user := u.(structs.User)
|
|
|
|
switch {
|
|
|
|
// AllowAllUsers
|
|
case cfg.Cfg.AllowAllUsers:
|
|
log.Debugf("verifyUser: Success! skipping verification, cfg.Cfg.AllowAllUsers is %t", cfg.Cfg.AllowAllUsers)
|
|
return true, nil
|
|
|
|
// WhiteList
|
|
case len(cfg.Cfg.WhiteList) != 0:
|
|
for _, wl := range cfg.Cfg.WhiteList {
|
|
if user.Username == wl {
|
|
log.Debugf("verifyUser: Success! found user.Username in WhiteList: %s", user.Username)
|
|
return true, nil
|
|
}
|
|
}
|
|
return false, fmt.Errorf("verifyUser: user.Username not found in WhiteList: %s", user.Username)
|
|
|
|
// TeamWhiteList
|
|
case len(cfg.Cfg.TeamWhiteList) != 0:
|
|
for _, team := range user.TeamMemberships {
|
|
for _, wl := range cfg.Cfg.TeamWhiteList {
|
|
if team == wl {
|
|
log.Debugf("verifyUser: Success! found user.TeamWhiteList in TeamWhiteList: %s for user %s", wl, user.Username)
|
|
return true, nil
|
|
}
|
|
}
|
|
}
|
|
return false, fmt.Errorf("verifyUser: user.TeamMemberships %s not found in TeamWhiteList: %s for user %s", user.TeamMemberships, cfg.Cfg.TeamWhiteList, user.Username)
|
|
|
|
// Domains
|
|
case len(cfg.Cfg.Domains) != 0:
|
|
if domains.IsUnderManagement(user.Email) {
|
|
log.Debugf("verifyUser: Success! Email %s found within a %s managed domain", user.Email, cfg.Branding.FullName)
|
|
return true, nil
|
|
}
|
|
return false, fmt.Errorf("verifyUser: Email %s is not within a %s managed domain", user.Email, cfg.Branding.FullName)
|
|
|
|
// nothing configured, allow everyone through
|
|
default:
|
|
log.Warn("verifyUser: no domains, whitelist, teamWhitelist or AllowAllUsers configured, any successful auth to the IdP authorizes access")
|
|
return true, nil
|
|
}
|
|
}
|
|
|
|
func getUserInfo(r *http.Request, user *structs.User, customClaims *structs.CustomClaims, ptokens *structs.PTokens, opts ...oauth2.AuthCodeOption) error {
|
|
return provider.GetUserInfo(r, user, customClaims, ptokens, opts...)
|
|
}
|