mirror of
https://github.com/openziti/zrok.git
synced 2026-09-24 07:35:05 -05:00
295 lines
10 KiB
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
|
|
}
|