outposts/proxyv2 (#1365)
* outposts/proxyv2: initial commit Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> add rs256 Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> more stuff Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> add forward auth an sign_out Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> match cookie name Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> re-add support for rs256 for backwards compat Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> add error handler Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> ensure unique user-agent is used Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> set cookie duration based on id_token expiry Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> build proxy v2 Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> add ssl Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> add basic auth and custom header support Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> add application cert loading Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> implement whitelist Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> add redis Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> migrate embedded outpost to v2 Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> remove old proxy Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> providers/proxy: make token expiration configurable Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> add metrics Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> fix tests Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * providers/proxy: only allow one redirect URI Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * fix docker build for proxy Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * remove default port offset Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * add AUTHENTIK_HOST_BROWSER Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * tests: fix e2e/integration tests not using proper tags Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * remove references of old port Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * fix user_attributes not being loaded correctly Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * cleanup dependencies Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org> * cleanup Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
This commit is contained in:
208
internal/outpost/proxyv2/application/application.go
Normal file
208
internal/outpost/proxyv2/application/application.go
Normal file
@ -0,0 +1,208 @@
|
||||
package application
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/go-oidc"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/gorilla/sessions"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"goauthentik.io/api"
|
||||
"goauthentik.io/internal/outpost/ak"
|
||||
"goauthentik.io/internal/outpost/proxyv2/constants"
|
||||
"goauthentik.io/internal/outpost/proxyv2/hs256"
|
||||
"goauthentik.io/internal/outpost/proxyv2/metrics"
|
||||
"goauthentik.io/internal/utils/web"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
type Application struct {
|
||||
Host string
|
||||
Cert *tls.Certificate
|
||||
UnauthenticatedRegex []*regexp.Regexp
|
||||
|
||||
oauthConfig oauth2.Config
|
||||
tokenVerifier *oidc.IDTokenVerifier
|
||||
|
||||
sessions sessions.Store
|
||||
proxyConfig api.ProxyOutpostConfig
|
||||
httpClient *http.Client
|
||||
|
||||
log *log.Entry
|
||||
mux *mux.Router
|
||||
}
|
||||
|
||||
func akProviderToEndpoint(p api.ProxyOutpostConfig) oauth2.Endpoint {
|
||||
authUrl := p.OidcConfiguration.AuthorizationEndpoint
|
||||
if browserHost, found := os.LookupEnv("AUTHENTIK_HOST_BROWSER"); found {
|
||||
host := os.Getenv("AUTHENTIK_HOST")
|
||||
authUrl = strings.ReplaceAll(authUrl, host, browserHost)
|
||||
}
|
||||
return oauth2.Endpoint{
|
||||
AuthURL: authUrl,
|
||||
TokenURL: p.OidcConfiguration.TokenEndpoint,
|
||||
AuthStyle: oauth2.AuthStyleInParams,
|
||||
}
|
||||
}
|
||||
|
||||
func NewApplication(p api.ProxyOutpostConfig, c *http.Client, cs *ak.CryptoStore) *Application {
|
||||
gob.Register(Claims{})
|
||||
|
||||
externalHost, err := url.Parse(p.ExternalHost)
|
||||
if err != nil {
|
||||
log.WithError(err).Warning("Failed to parse URL, skipping provider")
|
||||
}
|
||||
|
||||
// Support for RS256, new proxy providers will use HS256 but old ones
|
||||
// might not, and this makes testing easier
|
||||
var ks oidc.KeySet
|
||||
if contains(p.OidcConfiguration.IdTokenSigningAlgValuesSupported, "HS256") {
|
||||
ks = hs256.NewKeySet(*p.ClientSecret)
|
||||
} else {
|
||||
ctx := context.WithValue(context.Background(), oauth2.HTTPClient, c)
|
||||
oidc.NewRemoteKeySet(ctx, p.OidcConfiguration.JwksUri)
|
||||
}
|
||||
|
||||
var verifier = oidc.NewVerifier(p.OidcConfiguration.Issuer, ks, &oidc.Config{
|
||||
ClientID: *p.ClientId,
|
||||
SupportedSigningAlgs: []string{"HS256"},
|
||||
})
|
||||
|
||||
// Configure an OpenID Connect aware OAuth2 client.
|
||||
oauth2Config := oauth2.Config{
|
||||
ClientID: *p.ClientId,
|
||||
ClientSecret: *p.ClientSecret,
|
||||
RedirectURL: fmt.Sprintf("%s/akprox/callback", p.ExternalHost),
|
||||
Endpoint: akProviderToEndpoint(p),
|
||||
Scopes: []string{oidc.ScopeOpenID, "profile", "email", "ak_proxy"},
|
||||
}
|
||||
mux := mux.NewRouter()
|
||||
a := &Application{
|
||||
Host: externalHost.Host,
|
||||
log: log.WithField("logger", "authentik.outpost.proxy.bundle").WithField("provider", p.Name),
|
||||
oauthConfig: oauth2Config,
|
||||
tokenVerifier: verifier,
|
||||
sessions: GetStore(p),
|
||||
proxyConfig: p,
|
||||
httpClient: c,
|
||||
mux: mux,
|
||||
}
|
||||
muxLogger := log.WithField("logger", "authentik.outpost.proxyv2.application").WithField("name", p.Name)
|
||||
mux.Use(web.NewLoggingHandler(muxLogger, func(l *log.Entry, r *http.Request) *log.Entry {
|
||||
s, err := a.sessions.Get(r, constants.SeesionName)
|
||||
if err != nil {
|
||||
return l
|
||||
}
|
||||
claims, ok := s.Values[constants.SessionClaims]
|
||||
if claims == nil || !ok {
|
||||
return l
|
||||
}
|
||||
c, ok := claims.(Claims)
|
||||
if !ok {
|
||||
return l
|
||||
}
|
||||
return l.WithField("request_username", c.Email)
|
||||
}))
|
||||
mux.Use(func(inner http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||
c, _ := a.getClaims(r)
|
||||
user := ""
|
||||
if c != nil {
|
||||
user = c.Email
|
||||
}
|
||||
before := time.Now()
|
||||
inner.ServeHTTP(rw, r)
|
||||
after := time.Since(before)
|
||||
metrics.Requests.With(prometheus.Labels{
|
||||
"type": "app",
|
||||
"scheme": r.URL.Scheme,
|
||||
"method": r.Method,
|
||||
"path": r.URL.Path,
|
||||
"host": web.GetHost(r),
|
||||
"user": user,
|
||||
}).Observe(float64(after))
|
||||
})
|
||||
})
|
||||
|
||||
// Support /start and /sign_in for backwards compatibility
|
||||
mux.HandleFunc("/akprox/start", a.handleRedirect)
|
||||
mux.HandleFunc("/akprox/sign_in", a.handleRedirect)
|
||||
mux.HandleFunc("/akprox/callback", a.handleCallback)
|
||||
mux.HandleFunc("/akprox/sign_out", a.handleSignOut)
|
||||
switch *p.Mode {
|
||||
case api.PROXYMODE_PROXY:
|
||||
err = a.configureProxy()
|
||||
case api.PROXYMODE_FORWARD_SINGLE:
|
||||
fallthrough
|
||||
case api.PROXYMODE_FORWARD_DOMAIN:
|
||||
err = a.configureForward()
|
||||
}
|
||||
if err != nil {
|
||||
a.log.WithError(err).Warning("failed to configure mode")
|
||||
}
|
||||
|
||||
if kp := p.Certificate.Get(); kp != nil {
|
||||
err := cs.AddKeypair(*kp)
|
||||
if err != nil {
|
||||
a.log.WithError(err).Warning("Failed to initially fetch certificate")
|
||||
}
|
||||
a.Cert = cs.Get(*kp)
|
||||
}
|
||||
|
||||
if *p.SkipPathRegex != "" {
|
||||
a.UnauthenticatedRegex = make([]*regexp.Regexp, 0)
|
||||
for _, regex := range strings.Split(*p.SkipPathRegex, "\n") {
|
||||
re, err := regexp.Compile(regex)
|
||||
if err != nil {
|
||||
// TODO: maybe create event for this?
|
||||
a.log.WithError(err).Warning("failed to compile regex")
|
||||
} else {
|
||||
a.UnauthenticatedRegex = append(a.UnauthenticatedRegex, re)
|
||||
}
|
||||
}
|
||||
}
|
||||
return a
|
||||
}
|
||||
|
||||
func (a *Application) IsAllowlisted(r *http.Request) bool {
|
||||
for _, u := range a.UnauthenticatedRegex {
|
||||
if u.MatchString(r.URL.Path) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (a *Application) Mode() api.ProxyMode {
|
||||
return *a.proxyConfig.Mode
|
||||
}
|
||||
|
||||
func (a *Application) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
|
||||
a.mux.ServeHTTP(rw, r)
|
||||
}
|
||||
|
||||
func (a *Application) handleSignOut(rw http.ResponseWriter, r *http.Request) {
|
||||
// TODO: Token revocation
|
||||
s, err := a.sessions.Get(r, constants.SeesionName)
|
||||
if err != nil {
|
||||
http.Redirect(rw, r, a.proxyConfig.OidcConfiguration.EndSessionEndpoint, http.StatusFound)
|
||||
return
|
||||
}
|
||||
s.Options.MaxAge = -1
|
||||
err = s.Save(r, rw)
|
||||
if err != nil {
|
||||
http.Redirect(rw, r, a.proxyConfig.OidcConfiguration.EndSessionEndpoint, http.StatusFound)
|
||||
return
|
||||
}
|
||||
http.Redirect(rw, r, a.proxyConfig.OidcConfiguration.EndSessionEndpoint, http.StatusFound)
|
||||
}
|
||||
16
internal/outpost/proxyv2/application/claims.go
Normal file
16
internal/outpost/proxyv2/application/claims.go
Normal file
@ -0,0 +1,16 @@
|
||||
package application
|
||||
|
||||
type ProxyClaims struct {
|
||||
UserAttributes map[string]interface{} `json:"user_attributes"`
|
||||
}
|
||||
|
||||
type Claims struct {
|
||||
Sub string `json:"sub"`
|
||||
Exp int `json:"exp"`
|
||||
Email string `json:"email"`
|
||||
Verified bool `json:"email_verified"`
|
||||
Proxy ProxyClaims `json:"ak_proxy"`
|
||||
Name string `json:"name"`
|
||||
PreferredUsername string `json:"preferred_username"`
|
||||
Groups []string `json:"groups"`
|
||||
}
|
||||
29
internal/outpost/proxyv2/application/error.go
Normal file
29
internal/outpost/proxyv2/application/error.go
Normal file
@ -0,0 +1,29 @@
|
||||
package application
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"net/http"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// NewProxyErrorHandler creates a ProxyErrorHandler using the template given.
|
||||
func NewProxyErrorHandler(errorTemplate *template.Template) func(http.ResponseWriter, *http.Request, error) {
|
||||
return func(rw http.ResponseWriter, req *http.Request, proxyErr error) {
|
||||
log.Errorf("Error proxying to upstream server: %v", proxyErr)
|
||||
rw.WriteHeader(http.StatusBadGateway)
|
||||
data := struct {
|
||||
Title string
|
||||
Message string
|
||||
ProxyPrefix string
|
||||
}{
|
||||
Title: "Bad Gateway",
|
||||
Message: "Error proxying to upstream server",
|
||||
ProxyPrefix: "/akprox",
|
||||
}
|
||||
err := errorTemplate.Execute(rw, data)
|
||||
if err != nil {
|
||||
http.Error(rw, "Internal Server Error", http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
}
|
||||
53
internal/outpost/proxyv2/application/mode_common.go
Normal file
53
internal/outpost/proxyv2/application/mode_common.go
Normal file
@ -0,0 +1,53 @@
|
||||
package application
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func (a *Application) addHeaders(r *http.Request, c *Claims) {
|
||||
// https://goauthentik.io/docs/providers/proxy/proxy
|
||||
r.Header.Set("X-Auth-Username", c.PreferredUsername)
|
||||
r.Header.Set("X-Auth-Groups", strings.Join(c.Groups, "|"))
|
||||
r.Header.Set("X-Forwarded-Email", c.Email)
|
||||
r.Header.Set("X-Forwarded-Preferred-Username", c.PreferredUsername)
|
||||
r.Header.Set("X-Forwarded-User", c.Sub)
|
||||
|
||||
userAttributes := c.Proxy.UserAttributes
|
||||
// Attempt to set basic auth based on user's attributes
|
||||
if *a.proxyConfig.BasicAuthEnabled {
|
||||
var ok bool
|
||||
var password string
|
||||
if password, ok = userAttributes[*a.proxyConfig.BasicAuthPasswordAttribute].(string); !ok {
|
||||
password = ""
|
||||
}
|
||||
// Check if we should use email or a custom attribute as username
|
||||
var username string
|
||||
if username, ok = userAttributes[*a.proxyConfig.BasicAuthUserAttribute].(string); !ok {
|
||||
username = c.Email
|
||||
}
|
||||
authVal := base64.StdEncoding.EncodeToString([]byte(username + ":" + password))
|
||||
a.log.WithField("username", username).Trace("setting http basic auth")
|
||||
r.Header["Authorization"] = []string{fmt.Sprintf("Basic %s", authVal)}
|
||||
}
|
||||
// Check if user has additional headers set that we should sent
|
||||
if additionalHeaders, ok := userAttributes["additionalHeaders"].(map[string]interface{}); ok {
|
||||
a.log.WithField("headers", additionalHeaders).Trace("setting additional headers")
|
||||
if additionalHeaders == nil {
|
||||
return
|
||||
}
|
||||
for key, value := range additionalHeaders {
|
||||
r.Header.Set(key, toString(value))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func copyHeadersToResponse(rw http.ResponseWriter, r *http.Request) {
|
||||
for headerKey, headers := range r.Header {
|
||||
for _, value := range headers {
|
||||
rw.Header().Set(headerKey, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
77
internal/outpost/proxyv2/application/mode_forward.go
Normal file
77
internal/outpost/proxyv2/application/mode_forward.go
Normal file
@ -0,0 +1,77 @@
|
||||
package application
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"goauthentik.io/api"
|
||||
"goauthentik.io/internal/utils/web"
|
||||
)
|
||||
|
||||
func (a *Application) configureForward() error {
|
||||
a.mux.HandleFunc("/akprox/auth", func(rw http.ResponseWriter, r *http.Request) {
|
||||
if _, ok := r.URL.Query()["traefik"]; ok {
|
||||
a.forwardHandleTraefik(rw, r)
|
||||
return
|
||||
}
|
||||
a.forwardHandleNginx(rw, r)
|
||||
})
|
||||
a.mux.HandleFunc("/akprox/auth/traefik", a.forwardHandleTraefik)
|
||||
a.mux.HandleFunc("/akprox/auth/nginx", a.forwardHandleNginx)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *Application) forwardHandleTraefik(rw http.ResponseWriter, r *http.Request) {
|
||||
claims, err := a.getClaims(r)
|
||||
if claims != nil && err == nil {
|
||||
a.addHeaders(r, claims)
|
||||
copyHeadersToResponse(rw, r)
|
||||
return
|
||||
} else if claims == nil && a.IsAllowlisted(r) {
|
||||
a.log.Trace("path can be accessed without authentication")
|
||||
return
|
||||
}
|
||||
host := ""
|
||||
// Optional suffix, which is appended to the URL
|
||||
suffix := ""
|
||||
if *a.proxyConfig.Mode == api.PROXYMODE_FORWARD_SINGLE {
|
||||
host = web.GetHost(r)
|
||||
} else if *a.proxyConfig.Mode == api.PROXYMODE_FORWARD_DOMAIN {
|
||||
host = a.proxyConfig.ExternalHost
|
||||
// set the ?rd flag to the current URL we have, since we redirect
|
||||
// to a (possibly) different domain, but we want to be redirected back
|
||||
// to the application
|
||||
v := url.Values{
|
||||
// see https://doc.traefik.io/traefik/middlewares/forwardauth/
|
||||
// X-Forwarded-Uri is only the path, so we need to build the entire URL
|
||||
"rd": []string{fmt.Sprintf(
|
||||
"%s://%s%s",
|
||||
r.Header.Get("X-Forwarded-Proto"),
|
||||
r.Header.Get("X-Forwarded-Host"),
|
||||
r.Header.Get("X-Forwarded-Uri"),
|
||||
)},
|
||||
}
|
||||
suffix = fmt.Sprintf("?%s", v.Encode())
|
||||
}
|
||||
proto := r.Header.Get("X-Forwarded-Proto")
|
||||
if proto != "" {
|
||||
proto = proto + ":"
|
||||
}
|
||||
rdFinal := fmt.Sprintf("%s//%s%s%s", proto, host, "/akprox/start", suffix)
|
||||
a.log.WithField("url", rdFinal).Debug("Redirecting to login")
|
||||
http.Redirect(rw, r, rdFinal, http.StatusTemporaryRedirect)
|
||||
}
|
||||
|
||||
func (a *Application) forwardHandleNginx(rw http.ResponseWriter, r *http.Request) {
|
||||
claims, err := a.getClaims(r)
|
||||
if claims != nil && err == nil {
|
||||
a.addHeaders(r, claims)
|
||||
copyHeadersToResponse(rw, r)
|
||||
return
|
||||
} else if claims == nil && a.IsAllowlisted(r) {
|
||||
a.log.Trace("path can be accessed without authentication")
|
||||
return
|
||||
}
|
||||
http.Error(rw, "unauthorized request", http.StatusUnauthorized)
|
||||
}
|
||||
63
internal/outpost/proxyv2/application/mode_proxy.go
Normal file
63
internal/outpost/proxyv2/application/mode_proxy.go
Normal file
@ -0,0 +1,63 @@
|
||||
package application
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"goauthentik.io/internal/outpost/proxyv2/metrics"
|
||||
"goauthentik.io/internal/outpost/proxyv2/templates"
|
||||
"goauthentik.io/internal/utils/web"
|
||||
)
|
||||
|
||||
func (a *Application) configureProxy() error {
|
||||
// Reverse proxy to the application server
|
||||
u, err := url.Parse(*a.proxyConfig.InternalHost)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rp := &httputil.ReverseProxy{Director: a.proxyModifyRequest(u)}
|
||||
rp.ErrorHandler = NewProxyErrorHandler(templates.GetTemplates())
|
||||
rp.ModifyResponse = a.proxyModifyResponse
|
||||
a.mux.PathPrefix("/").HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||
claims, err := a.getClaims(r)
|
||||
if claims == nil && a.IsAllowlisted(r) {
|
||||
a.log.Trace("path can be accessed without authentication")
|
||||
} else if claims == nil && err != nil {
|
||||
a.redirectToStart(rw, r)
|
||||
return
|
||||
} else {
|
||||
a.addHeaders(r, claims)
|
||||
}
|
||||
before := time.Now()
|
||||
rp.ServeHTTP(rw, r)
|
||||
after := time.Since(before)
|
||||
|
||||
user := ""
|
||||
if claims != nil {
|
||||
user = claims.Email
|
||||
}
|
||||
metrics.UpstreamTiming.With(prometheus.Labels{
|
||||
"upstream_host": u.String(),
|
||||
"scheme": r.URL.Scheme,
|
||||
"method": r.Method,
|
||||
"path": r.URL.Path,
|
||||
"host": web.GetHost(r),
|
||||
"user": user,
|
||||
}).Observe(float64(after))
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *Application) proxyModifyRequest(u *url.URL) func(req *http.Request) {
|
||||
return func(req *http.Request) {
|
||||
req.URL.Scheme = u.Scheme
|
||||
req.URL.Host = u.Host
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Application) proxyModifyResponse(res *http.Response) error {
|
||||
return nil
|
||||
}
|
||||
54
internal/outpost/proxyv2/application/oauth.go
Normal file
54
internal/outpost/proxyv2/application/oauth.go
Normal file
@ -0,0 +1,54 @@
|
||||
package application
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/securecookie"
|
||||
"goauthentik.io/internal/outpost/proxyv2/constants"
|
||||
)
|
||||
|
||||
func (a *Application) handleRedirect(rw http.ResponseWriter, r *http.Request) {
|
||||
state := base64.RawStdEncoding.EncodeToString(securecookie.GenerateRandomKey(32))
|
||||
s, _ := a.sessions.Get(r, constants.SeesionName)
|
||||
s.Values[constants.SessionOAuthState] = state
|
||||
err := s.Save(r, rw)
|
||||
if err != nil {
|
||||
a.log.WithError(err).Warning("failed to save session")
|
||||
}
|
||||
http.Redirect(rw, r, a.oauthConfig.AuthCodeURL(state), http.StatusFound)
|
||||
}
|
||||
|
||||
func (a *Application) handleCallback(rw http.ResponseWriter, r *http.Request) {
|
||||
s, _ := a.sessions.Get(r, constants.SeesionName)
|
||||
state, ok := s.Values[constants.SessionOAuthState]
|
||||
if !ok {
|
||||
a.log.Warning("No state saved in session")
|
||||
http.Redirect(rw, r, a.proxyConfig.ExternalHost, http.StatusFound)
|
||||
return
|
||||
}
|
||||
claims, err := a.redeemCallback(r, state.(string))
|
||||
if err != nil {
|
||||
a.log.WithError(err).Warning("failed to redeem code")
|
||||
rw.WriteHeader(400)
|
||||
// To prevent the user from just refreshing and cause more errors, delete
|
||||
// the state from the session
|
||||
delete(s.Values, constants.SessionOAuthState)
|
||||
err := s.Save(r, rw)
|
||||
if err != nil {
|
||||
a.log.WithError(err).Warning("failed to save session")
|
||||
rw.WriteHeader(400)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
s.Options.MaxAge = claims.Exp / 1000
|
||||
s.Values[constants.SessionClaims] = &claims
|
||||
err = s.Save(r, rw)
|
||||
if err != nil {
|
||||
a.log.WithError(err).Warning("failed to save session")
|
||||
rw.WriteHeader(400)
|
||||
return
|
||||
}
|
||||
http.Redirect(rw, r, a.proxyConfig.ExternalHost, http.StatusFound)
|
||||
}
|
||||
49
internal/outpost/proxyv2/application/oauth_callback.go
Normal file
49
internal/outpost/proxyv2/application/oauth_callback.go
Normal file
@ -0,0 +1,49 @@
|
||||
package application
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
func (a *Application) redeemCallback(r *http.Request, shouldState string) (*Claims, error) {
|
||||
state := r.URL.Query().Get("state")
|
||||
if state == "" || state != shouldState {
|
||||
return nil, fmt.Errorf("blank/invalid state")
|
||||
}
|
||||
|
||||
code := r.URL.Query().Get("code")
|
||||
if code == "" {
|
||||
return nil, fmt.Errorf("blank code")
|
||||
}
|
||||
|
||||
ctx := context.WithValue(r.Context(), oauth2.HTTPClient, a.httpClient)
|
||||
// Verify state and errors.
|
||||
oauth2Token, err := a.oauthConfig.Exchange(ctx, code)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Extract the ID Token from OAuth2 token.
|
||||
rawIDToken, ok := oauth2Token.Extra("id_token").(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("missing id_token")
|
||||
}
|
||||
|
||||
a.log.WithField("id_token", rawIDToken).Trace("id_token")
|
||||
|
||||
// Parse and verify ID Token payload.
|
||||
idToken, err := a.tokenVerifier.Verify(ctx, rawIDToken)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Extract custom claims
|
||||
var claims *Claims
|
||||
if err := idToken.Claims(&claims); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return claims, nil
|
||||
}
|
||||
28
internal/outpost/proxyv2/application/session.go
Normal file
28
internal/outpost/proxyv2/application/session.go
Normal file
@ -0,0 +1,28 @@
|
||||
package application
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/gorilla/sessions"
|
||||
"goauthentik.io/api"
|
||||
"goauthentik.io/internal/config"
|
||||
"gopkg.in/boj/redistore.v1"
|
||||
)
|
||||
|
||||
func GetStore(p api.ProxyOutpostConfig) sessions.Store {
|
||||
var store sessions.Store
|
||||
if config.G.Redis.Host != "" {
|
||||
rs, err := redistore.NewRediStoreWithDB(10, "tcp", fmt.Sprintf("%s:%d", config.G.Redis.Host, config.G.Redis.Port), config.G.Redis.Password, strconv.Itoa(config.G.Redis.OutpostSessionDB))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
rs.Options.Domain = *p.CookieDomain
|
||||
store = rs
|
||||
} else {
|
||||
cs := sessions.NewCookieStore([]byte(*p.CookieSecret))
|
||||
cs.Options.Domain = *p.CookieDomain
|
||||
store = cs
|
||||
}
|
||||
return store
|
||||
}
|
||||
56
internal/outpost/proxyv2/application/utils.go
Normal file
56
internal/outpost/proxyv2/application/utils.go
Normal file
@ -0,0 +1,56 @@
|
||||
package application
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"goauthentik.io/internal/outpost/proxyv2/constants"
|
||||
)
|
||||
|
||||
func (a *Application) redirectToStart(rw http.ResponseWriter, r *http.Request) {
|
||||
authUrl := fmt.Sprintf("%s/akprox/start", a.proxyConfig.ExternalHost)
|
||||
http.Redirect(rw, r, authUrl, http.StatusFound)
|
||||
}
|
||||
|
||||
// getClaims Get claims which are currently in session
|
||||
// Returns an error if the session can't be loaded or the claims can't be parsed/type-cast
|
||||
func (a *Application) getClaims(r *http.Request) (*Claims, error) {
|
||||
s, err := a.sessions.Get(r, constants.SeesionName)
|
||||
if err != nil {
|
||||
// err == user has no session/session is not valid, reject
|
||||
return nil, fmt.Errorf("invalid session")
|
||||
}
|
||||
claims, ok := s.Values[constants.SessionClaims]
|
||||
if claims == nil || !ok {
|
||||
// no claims saved, reject
|
||||
return nil, fmt.Errorf("invalid session")
|
||||
}
|
||||
c, ok := claims.(Claims)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid session")
|
||||
}
|
||||
return &c, nil
|
||||
}
|
||||
|
||||
func contains(s []string, e string) bool {
|
||||
for _, a := range s {
|
||||
if a == e {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// toString Generic to string function, currently supports actual strings and integers
|
||||
func toString(in interface{}) string {
|
||||
switch v := in.(type) {
|
||||
case string:
|
||||
return v
|
||||
case *string:
|
||||
return *v
|
||||
case int:
|
||||
return strconv.Itoa(v)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
Reference in New Issue
Block a user