mirror of
https://github.com/openziti/zrok.git
synced 2024-11-07 16:54:23 +01:00
177 lines
5.1 KiB
Go
177 lines
5.1 KiB
Go
package publicProxy
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/golang-jwt/jwt/v5"
|
|
"github.com/google/uuid"
|
|
"github.com/sirupsen/logrus"
|
|
"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"
|
|
githubOAuth "golang.org/x/oauth2/github"
|
|
)
|
|
|
|
func configureGithubOauth(cfg *OauthConfig, tls bool) error {
|
|
scheme := "http"
|
|
if tls {
|
|
scheme = "https"
|
|
}
|
|
|
|
providerCfg := cfg.GetProvider("github")
|
|
if providerCfg == nil {
|
|
logrus.Info("unable to find provider config for github. Skipping.")
|
|
return nil
|
|
}
|
|
clientID := providerCfg.ClientId
|
|
callbackPath := "/github/oauth"
|
|
port := cfg.Port
|
|
redirectUrl := fmt.Sprintf("%s://%s", scheme, cfg.RedirectUrl)
|
|
rpConfig := &oauth2.Config{
|
|
ClientID: clientID,
|
|
ClientSecret: providerCfg.ClientSecret,
|
|
RedirectURL: fmt.Sprintf("%v:%v%v", redirectUrl, port, callbackPath),
|
|
Scopes: []string{"user:email"},
|
|
Endpoint: githubOAuth.Endpoint,
|
|
}
|
|
|
|
key := []byte(cfg.HashKeyRaw)
|
|
|
|
u, err := url.Parse(redirectUrl)
|
|
if err != nil {
|
|
logrus.Errorf("unable to parse redirect url: %v", err)
|
|
return err
|
|
}
|
|
parts := strings.Split(u.Hostname(), ".")
|
|
domain := parts[len(parts)-2] + "." + parts[len(parts)-1]
|
|
|
|
cookieHandler := zhttp.NewCookieHandler(key, key, zhttp.WithUnsecure(), zhttp.WithDomain(domain))
|
|
|
|
options := []rp.Option{
|
|
rp.WithCookieHandler(cookieHandler),
|
|
rp.WithVerifierOpts(rp.WithIssuedAtOffset(5 * time.Second)),
|
|
//rp.WithPKCE(cookieHandler), //Github currently doesn't support pkce. Update when that changes.
|
|
}
|
|
|
|
relyingParty, err := rp.NewRelyingPartyOAuth(rpConfig, options...)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
type IntermediateJWT struct {
|
|
State string `json:"state"`
|
|
Host string `json:"host"`
|
|
AuthorizationCheckInterval string `json:"authorizationCheckInterval"`
|
|
jwt.RegisteredClaims
|
|
}
|
|
|
|
type githubUserResp struct {
|
|
Email string
|
|
Primary bool
|
|
Verified bool
|
|
Visibility string
|
|
}
|
|
|
|
authHandlerWithQueryState := func(party rp.RelyingParty) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
host, err := url.QueryUnescape(r.URL.Query().Get("targethost"))
|
|
if err != nil {
|
|
logrus.Errorf("Unable to unescape target host: %v", err)
|
|
}
|
|
rp.AuthURLHandler(func() string {
|
|
id := uuid.New().String()
|
|
t := jwt.NewWithClaims(jwt.SigningMethodHS256, IntermediateJWT{
|
|
id,
|
|
host,
|
|
r.URL.Query().Get("checkInterval"),
|
|
jwt.RegisteredClaims{
|
|
ExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * time.Hour)),
|
|
IssuedAt: jwt.NewNumericDate(time.Now()),
|
|
NotBefore: jwt.NewNumericDate(time.Now()),
|
|
Issuer: "zrok",
|
|
Subject: "intermediate_token",
|
|
ID: id,
|
|
},
|
|
})
|
|
s, err := t.SignedString(key)
|
|
if err != nil {
|
|
logrus.Errorf("Unable to sign intermediate JWT: %v", err)
|
|
}
|
|
return s
|
|
}, party, rp.WithURLParam("access_type", "offline"))(w, r)
|
|
}
|
|
}
|
|
|
|
http.Handle("/github/login", authHandlerWithQueryState(relyingParty))
|
|
getEmail := func(w http.ResponseWriter, r *http.Request, tokens *oidc.Tokens[*oidc.IDTokenClaims], state string, rp rp.RelyingParty) {
|
|
parsedUrl, err := url.Parse("https://api.github.com/user/emails")
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
req := &http.Request{
|
|
Method: http.MethodGet,
|
|
URL: parsedUrl,
|
|
Header: make(http.Header),
|
|
}
|
|
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", tokens.AccessToken))
|
|
resp, err := http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
logrus.Error("Error getting user info from github: " + err.Error() + "\n")
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
defer resp.Body.Close()
|
|
response, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
logrus.Errorf("Error reading response body: %v", err)
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
rDat := []githubUserResp{}
|
|
err = json.Unmarshal(response, &rDat)
|
|
if err != nil {
|
|
logrus.Errorf("Error unmarshalling google oauth response: %v", err)
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
primaryEmail := ""
|
|
for _, email := range rDat {
|
|
if email.Primary {
|
|
primaryEmail = email.Email
|
|
break
|
|
}
|
|
}
|
|
|
|
token, err := jwt.ParseWithClaims(state, &IntermediateJWT{}, func(t *jwt.Token) (interface{}, error) {
|
|
return key, nil
|
|
})
|
|
if err != nil {
|
|
http.Error(w, fmt.Sprintf("After intermediate token parse: %v", err.Error()), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
authCheckInterval := 3 * time.Hour
|
|
i, err := time.ParseDuration(token.Claims.(*IntermediateJWT).AuthorizationCheckInterval)
|
|
if err != nil {
|
|
logrus.Errorf("unable to parse authorization check interval: %v. Defaulting to 3 hours", err)
|
|
} else {
|
|
authCheckInterval = i
|
|
}
|
|
|
|
SetZrokCookie(w, domain, primaryEmail, tokens.AccessToken, "github", authCheckInterval, key)
|
|
http.Redirect(w, r, fmt.Sprintf("%s://%s", scheme, token.Claims.(*IntermediateJWT).Host), http.StatusFound)
|
|
}
|
|
|
|
http.Handle(callbackPath, rp.CodeExchangeHandler(getEmail, relyingParty))
|
|
return nil
|
|
}
|