Files
zrok/endpoints/dynamicProxy/providerGoogle.go

295 lines
10 KiB
Go

package dynamicProxy
import (
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"time"
"github.com/openziti/zrok/v2/endpoints"
"github.com/golang-jwt/jwt/v5"
"github.com/google/uuid"
"github.com/gorilla/mux"
"github.com/michaelquigley/df/dd"
"github.com/michaelquigley/df/dl"
"github.com/openziti/zrok/v2/endpoints/proxyUi"
"github.com/zitadel/oidc/v2/pkg/client/rp"
zhttp "github.com/zitadel/oidc/v2/pkg/http"
"github.com/zitadel/oidc/v2/pkg/oidc"
"golang.org/x/oauth2"
googleOauth "golang.org/x/oauth2/google"
)
func init() {
registerOauthBinder((&googleConfig{}).Type(), newGoogleConfig)
}
type googleConfig struct {
Name string
ClientId string
ClientSecret string
}
func newGoogleConfig(v map[string]interface{}) (dd.Dynamic, error) {
return dd.New[googleConfig](v)
}
func (c *googleConfig) Type() string { return "google" }
func (c *googleConfig) ToMap() (map[string]any, error) { return nil, nil }
func (c *googleConfig) configure(cfg *oauthConfig, tls bool) error {
// create google provider instance
provider, err := createGoogleProvider(c, cfg, tls)
if err != nil {
return err
}
// register with the oauth router
return registerOAuthProvider(provider)
}
// googleProvider implements the oauthProvider interface for Google OAuth
type googleProvider struct {
config *googleConfig
oauthCfg *oauthConfig
provider rp.RelyingParty
signingKey []byte
encryptionKey []byte
tls bool
}
// googleOauthEmailResp represents the response from Google's userinfo endpoint
type googleOauthEmailResp struct {
Email string `json:"email"`
}
// Name returns the provider name
func (p *googleProvider) Name() string {
return p.config.Name
}
// RegisterRoutes registers the Google OAuth routes with the provided router
func (p *googleProvider) RegisterRoutes(router *mux.Router) error {
// register login route
router.Handle(fmt.Sprintf("/%v/login", p.config.Name), p.authHandler())
// register callback route
router.Handle(fmt.Sprintf("/%v/auth/callback", p.config.Name),
rp.CodeExchangeHandler(p.loginHandler(), p.provider))
// register logout route
router.HandleFunc(fmt.Sprintf("/%v/logout", p.config.Name), p.logoutHandler())
dl.Debugf("registered google provider routes at '/%v'", p.config.Name)
return nil
}
// authHandler creates the authentication handler for initiating OAuth flow
func (p *googleProvider) authHandler() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
targetHost, err := url.QueryUnescape(r.URL.Query().Get("targetHost"))
if err != nil {
dl.Errorf("unable to unescape targetHost: %v", err)
proxyUi.WriteUnauthorized(w, proxyUi.UnauthorizedData().WithError(errors.New("unable to escape targetHost")))
return
}
rp.AuthURLHandler(func() string {
id := uuid.New().String()
t := jwt.NewWithClaims(jwt.SigningMethodHS256, IntermediateJWT{
State: id,
TargetHost: targetHost,
RefreshInterval: r.URL.Query().Get("refreshInterval"),
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(p.oauthCfg.IntermediateLifetime)),
IssuedAt: jwt.NewNumericDate(time.Now()),
NotBefore: jwt.NewNumericDate(time.Now()),
Issuer: "zrok",
Subject: "intermediate_token",
ID: id,
},
})
s, err := t.SignedString(p.signingKey)
if err != nil {
dl.Errorf("unable to sign intermediate JWT: %v", err)
}
return s
}, p.provider, rp.WithURLParam("access_type", "offline"), rp.URLParamOpt(rp.WithPrompt("login")))(w, r)
})
}
// loginHandler creates the login callback handler for processing OAuth responses
func (p *googleProvider) loginHandler() func(w http.ResponseWriter, r *http.Request, tokens *oidc.Tokens[*oidc.IDTokenClaims], state string, rp rp.RelyingParty) {
return func(w http.ResponseWriter, r *http.Request, tokens *oidc.Tokens[*oidc.IDTokenClaims], state string, rp rp.RelyingParty) {
token, err := jwt.ParseWithClaims(state, &IntermediateJWT{}, func(t *jwt.Token) (interface{}, error) {
return p.signingKey, nil
})
if err != nil {
dl.Errorf("error parsing intermediate token: %v", err)
proxyUi.WriteUnauthorized(w, proxyUi.UnauthorizedData().WithError(errors.New("error parsing intermediate token")))
return
}
var refreshInterval time.Duration
if v, err := time.ParseDuration(token.Claims.(*IntermediateJWT).RefreshInterval); err == nil {
refreshInterval = v
} else {
dl.Errorf("unable to parse authorization check interval: %v", err)
proxyUi.WriteUnauthorized(w, proxyUi.UnauthorizedData().WithError(errors.New("unable to parse authorization check interval")))
return
}
// get user info from google
resp, err := http.Get("https://www.googleapis.com/oauth2/v2/userinfo?access_token=" + url.QueryEscape(tokens.AccessToken))
if err != nil {
dl.Errorf("error getting user info from google: %v", err)
proxyUi.WriteUnauthorized(w, proxyUi.UnauthorizedData().WithError(errors.New("error getting user info from google")))
return
}
defer func() {
_ = resp.Body.Close()
}()
response, err := io.ReadAll(resp.Body)
if err != nil {
dl.Errorf("error reading response body: %v", err)
proxyUi.WriteUnauthorized(w, proxyUi.UnauthorizedData().WithError(errors.New("error reading google response body")))
return
}
dl.Debugf("response from google userinfo endpoint: %s", string(response))
data := googleOauthEmailResp{}
err = json.Unmarshal(response, &data)
if err != nil {
dl.Errorf("error unmarshalling google oauth response: %v", err)
proxyUi.WriteUnauthorized(w, proxyUi.UnauthorizedData().WithError(errors.New("error unmarshalling google oauth response")))
return
}
// set session cookie
setSessionCookie(w, sessionCookieRequest{
oauthCfg: p.oauthCfg,
supportsRefresh: false,
email: data.Email,
accessToken: tokens.AccessToken,
provider: p.config.Name,
refreshInterval: refreshInterval,
signingKey: p.signingKey,
encryptionKey: p.encryptionKey,
targetHost: token.Claims.(*IntermediateJWT).TargetHost,
})
scheme := "http"
if p.tls {
scheme = "https"
}
http.Redirect(w, r, fmt.Sprintf("%s://%s", scheme, token.Claims.(*IntermediateJWT).TargetHost), http.StatusFound)
}
}
// logoutHandler creates the logout handler for revoking Google tokens and clearing cookies
func (p *googleProvider) logoutHandler() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
cookie, err := getSessionCookie(r, p.oauthCfg)
if err == nil {
tkn, err := jwt.ParseWithClaims(cookie.Value, &zrokClaims{}, func(t *jwt.Token) (interface{}, error) {
return p.signingKey, nil
})
if err == nil {
claims := tkn.Claims.(*zrokClaims)
if claims.Provider == p.config.Name {
accessToken, err := endpoints.DecryptToken(claims.AccessToken, p.encryptionKey)
if err == nil {
// revoke google token
revokeURL := "https://oauth2.googleapis.com/revoke"
resp, err := http.PostForm(revokeURL, url.Values{
"token": {accessToken},
})
if err == nil {
defer resp.Body.Close()
if resp.StatusCode == http.StatusOK {
dl.Infof("revoked google token for '%v'", claims.Email)
} else {
dl.Errorf("access token revocation failed with status: %v", resp.StatusCode)
proxyUi.WriteUnauthorized(w, proxyUi.UnauthorizedUser(claims.Email).WithError(errors.New("access token revocation failed")))
return
}
} else {
dl.Errorf("unable to revoke access token for '%v': %v", claims.Email, err)
proxyUi.WriteUnauthorized(w, proxyUi.UnauthorizedUser(claims.Email).WithError(errors.New("unable to post access token revocation")))
return
}
} else {
dl.Errorf("unable to decrypt access token for '%v': %v", claims.Email, err)
proxyUi.WriteUnauthorized(w, proxyUi.UnauthorizedUser(claims.Email).WithError(errors.New("unable to decrypt access token")))
return
}
} else {
dl.Errorf("expected provider name '%v' got '%v'", p.config.Name, claims.Provider)
proxyUi.WriteUnauthorized(w, proxyUi.UnauthorizedUser(claims.Email).WithError(errors.New("provider name mismatch")))
return
}
} else {
dl.Errorf("invalid jwt; unable to parse: %v", err)
proxyUi.WriteUnauthorized(w, proxyUi.UnauthorizedData().WithError(errors.New("invalid jwt; unable to parse")))
return
}
} else {
dl.Errorf("error getting cookie '%v': %v", p.oauthCfg.CookieName, err)
proxyUi.WriteUnauthorized(w, proxyUi.UnauthorizedData().WithError(errors.New("error getting cookie")))
return
}
clearSessionCookies(w, r, p.oauthCfg.CookieName, p.oauthCfg)
redirectURL := r.URL.Query().Get("redirect_url")
if redirectURL == "" {
redirectURL = fmt.Sprintf("%s/%s/login", p.oauthCfg.EndpointUrl, p.config.Name)
}
http.Redirect(w, r, redirectURL, http.StatusFound)
}
}
// createGoogleProvider creates a new Google OAuth provider
func createGoogleProvider(config *googleConfig, oauthCfg *oauthConfig, tls bool) (*googleProvider, error) {
signingKey, err := endpoints.DeriveKey(oauthCfg.SigningKey, 32)
if err != nil {
return nil, err
}
encryptionKey, err := endpoints.DeriveKey(oauthCfg.EncryptionKey, 32)
if err != nil {
return nil, err
}
cookieHandler := zhttp.NewCookieHandler(signingKey, encryptionKey, zhttp.WithUnsecure(), zhttp.WithDomain(oauthCfg.CookieDomain))
rpConfig := &oauth2.Config{
ClientID: config.ClientId,
ClientSecret: config.ClientSecret,
RedirectURL: fmt.Sprintf("%v/%v/auth/callback", oauthCfg.EndpointUrl, config.Name),
Scopes: []string{"https://www.googleapis.com/auth/userinfo.email"},
Endpoint: googleOauth.Endpoint,
}
providerOptions := []rp.Option{
rp.WithCookieHandler(cookieHandler),
rp.WithVerifierOpts(rp.WithIssuedAtOffset(5 * time.Second)),
rp.WithPKCE(cookieHandler),
}
provider, err := rp.NewRelyingPartyOAuth(rpConfig, providerOptions...)
if err != nil {
return nil, err
}
return &googleProvider{
config: config,
oauthCfg: oauthCfg,
provider: provider,
signingKey: signingKey,
encryptionKey: encryptionKey,
tls: tls,
}, nil
}