vouch-proxy/handlers/auth.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...)
}