package application import ( "context" "crypto/tls" "encoding/gob" "fmt" "net/http" "net/url" "regexp" "strings" "time" "github.com/coreos/go-oidc" "github.com/getsentry/sentry-go" sentryhttp "github.com/getsentry/sentry-go/http" "github.com/gorilla/mux" "github.com/gorilla/sessions" "github.com/pkg/errors" "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 endpint OIDCEndpoint oauthConfig oauth2.Config tokenVerifier *oidc.IDTokenVerifier outpostName string sessions sessions.Store proxyConfig api.ProxyOutpostConfig httpClient *http.Client log *log.Entry mux *mux.Router } func NewApplication(p api.ProxyOutpostConfig, c *http.Client, cs *ak.CryptoStore, ak *ak.APIController) (*Application, error) { gob.Register(Claims{}) muxLogger := log.WithField("logger", "authentik.outpost.proxyv2.application").WithField("name", p.Name) externalHost, err := url.Parse(p.ExternalHost) if err != nil { return nil, fmt.Errorf("failed to parse URL, skipping provider") } var ks oidc.KeySet if contains(p.OidcConfiguration.IdTokenSigningAlgValuesSupported, "HS256") { ks = hs256.NewKeySet(*p.ClientSecret) } else { ctx := context.WithValue(context.Background(), oauth2.HTTPClient, c) ks = oidc.NewRemoteKeySet(ctx, p.OidcConfiguration.JwksUri) } var verifier = oidc.NewVerifier(p.OidcConfiguration.Issuer, ks, &oidc.Config{ ClientID: *p.ClientId, SupportedSigningAlgs: []string{"RS256", "HS256"}, }) // Configure an OpenID Connect aware OAuth2 client. endpoint := GetOIDCEndpoint(p, ak.Outpost.Config["authentik_host"].(string)) oauth2Config := oauth2.Config{ ClientID: *p.ClientId, ClientSecret: *p.ClientSecret, RedirectURL: urlJoin(p.ExternalHost, "/akprox/callback"), Endpoint: endpoint.Endpoint, Scopes: p.ScopesToRequest, } mux := mux.NewRouter() a := &Application{ Host: externalHost.Host, log: log.WithField("logger", "authentik.outpost.proxy.bundle").WithField("provider", p.Name), outpostName: ak.Outpost.Name, endpint: endpoint, oauthConfig: oauth2Config, tokenVerifier: verifier, proxyConfig: p, httpClient: c, mux: mux, } a.sessions = a.getStore(p) 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.PreferredUsername) })) 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.PreferredUsername hub := sentry.GetHubFromContext(r.Context()) if hub == nil { hub = sentry.CurrentHub() } hub.Scope().SetUser(sentry.User{ Username: user, ID: c.Sub, IPAddress: r.RemoteAddr, }) } before := time.Now() inner.ServeHTTP(rw, r) after := time.Since(before) metrics.Requests.With(prometheus.Labels{ "outpost_name": a.outpostName, "type": "app", "scheme": r.URL.Scheme, "method": r.Method, "path": r.URL.Path, "host": web.GetHost(r), "user": user, }).Observe(float64(after)) }) }) mux.Use(sentryhttp.New(sentryhttp.Options{}).Handle) // 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 { return nil, errors.Wrap(err, "failed to configure application mode") } if kp := p.Certificate.Get(); kp != nil { err := cs.AddKeypair(*kp) if err != nil { return nil, errors.Wrap(err, "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 SkipPathRegex") continue } else { a.UnauthenticatedRegex = append(a.UnauthenticatedRegex, re) } } } return a, nil } func (a *Application) IsAllowlisted(r *http.Request) bool { for _, u := range a.UnauthenticatedRegex { a.log.WithField("regex", u.String()).WithField("url", r.URL.Path).Trace("Matching URL against allow list") 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.endpint.EndSessionEndpoint, http.StatusFound) return } s.Options.MaxAge = -1 err = s.Save(r, rw) if err != nil { http.Redirect(rw, r, a.endpint.EndSessionEndpoint, http.StatusFound) return } http.Redirect(rw, r, a.endpint.EndSessionEndpoint, http.StatusFound) }