Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 20 additions & 8 deletions authentik/policies/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,28 +68,40 @@ def resolve_provider_application(self):
is not caught, and will return directly"""
raise NotImplementedError

def supports_cors_preflight_requests(self) -> bool:
"""If true, OPTIONS requests will be answered without authentication or permission checks.
This is useful for CORS preflight requests."""
return hasattr(self, "options") and callable(self.options)

def dispatch(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse:
# Validate request before doing anything else
try:
self.pre_permission_check()
except RequestValidationError as exc:
if exc.response:
return exc.response
return self.handle_no_permission()

try:
self.resolve_provider_application()
except (Application.DoesNotExist, Provider.DoesNotExist) as exc:
LOGGER.warning("failed to resolve application", exc=exc)
return self.handle_no_permission_authenticated(
PolicyResult(False, _("Failed to resolve application"))
)
# Check if user is unauthenticated, so we pass the application
# for the identification stage
if not request.user.is_authenticated:
return self.handle_no_permission()
# Check permissions
result = self.user_has_access()
if not result.passing:
return self.handle_no_permission_authenticated(result)

# CORS preflight requests should not require authentication
if request.method != "OPTIONS" or not self.supports_cors_preflight_requests():
# Check if user is unauthenticated, so we pass the application
# for the identification stage
if not request.user.is_authenticated:
return self.handle_no_permission()

# Check permissions
result = self.user_has_access()
if not result.passing:
return self.handle_no_permission_authenticated(result)

return super().dispatch(request, *args, **kwargs)

def handle_no_permission(self) -> HttpResponse:
Expand Down
25 changes: 23 additions & 2 deletions authentik/providers/oauth2/views/authorize.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@
ResponseTypes,
ScopeMapping,
)
from authentik.providers.oauth2.utils import HttpResponseRedirectScheme
from authentik.providers.oauth2.utils import HttpResponseRedirectScheme, TokenResponse, cors_allow
from authentik.providers.oauth2.views.userinfo import UserInfoView
from authentik.stages.consent.models import ConsentMode, ConsentStage
from authentik.stages.consent.stage import (
Expand Down Expand Up @@ -347,6 +347,10 @@ class AuthorizationFlowInitView(BufferedPolicyAccessView):
def pre_permission_check(self):
"""Check prompt parameter before checking permission/authentication,
see https://openid.net/specs/openid-connect-core-1_0.html#rfc.section.3.1.2.6"""
# Allow unauthenticated CORS preflight requests
if self.request.method == "OPTIONS":
return

# Quick sanity check at the beginning to prevent event spamming
if len(self.request.GET) < 1:
raise Http404
Expand Down Expand Up @@ -422,14 +426,31 @@ def dispatch_with_language(self, request: HttpRequest, *args, **kwargs) -> HttpR
response["Location"] = urlunparse(
parsed_url._replace(query=urlencode(args, quote_via=quote, doseq=True))
)

# Add CORS headers based on the provider's redirect URIs, if a provider exists and has
# redirect URIs configured
allowed_origins = []
if hasattr(self, "provider") and self.provider and hasattr(self.provider, "redirect_uris"):
allowed_origins = [x.url for x in self.provider.redirect_uris]
cors_allow(self.request, response, *allowed_origins)

# Override Access-Control-Allow-Methods to only allow GET and OPTIONS
# POST is not defined for this endpoint
if "Access-Control-Allow-Methods" in response:
response["Access-Control-Allow-Methods"] = "GET, OPTIONS"

return response

def dispatch(self, request: HttpRequest, *args, **kwargs):
# Activate language before parsing params (error messages should be localised)
return self.dispatch_with_language(request, *args, **kwargs)

def options(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
# Return an empty response. The dispatch method will add the CORS headers.
return TokenResponse({})

def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
"""Start FlowPLanner, return to flow executor shell"""
"""Start FlowPlanner, return to flow executor shell"""
# Require a login event to be set, otherwise make the user re-login
login_event = get_login_event(request)
if not login_event:
Expand Down
102 changes: 102 additions & 0 deletions internal/outpost/proxyv2/application/application.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ type Application struct {
outpostName string
sessionName string

corsOrigin string

sessions sessions.Store
proxyConfig api.ProxyOutpostConfig
httpClient *http.Client
Expand Down Expand Up @@ -79,6 +81,18 @@ func NewApplication(p api.ProxyOutpostConfig, c *http.Client, server Server, old
return nil, fmt.Errorf("failed to parse URL, skipping provider")
}

// Build a URL with just the scheme, host, and port for CORS use
corsURL := &url.URL{
Scheme: externalHost.Scheme,
Host: externalHost.Host,
}

if err := setUrlPort(corsURL); err != nil {
return nil, fmt.Errorf("failed to set CORS origin URL port: %w", err)
}

corsOrigin := strings.ToLower(corsURL.String())

var ks oidc.KeySet
if contains(p.OidcConfiguration.IdTokenSigningAlgValuesSupported, "HS256") {
ks = hs256.NewKeySet(*p.ClientSecret)
Expand Down Expand Up @@ -139,6 +153,7 @@ func NewApplication(p api.ProxyOutpostConfig, c *http.Client, server Server, old
endpoint: endpoint,
oauthConfig: oauth2Config,
tokenVerifier: verifier,
corsOrigin: corsOrigin,
proxyConfig: p,
httpClient: c,
publicHostHTTPClient: publicHTTPClient,
Expand Down Expand Up @@ -317,3 +332,90 @@ func (a *Application) handleSignOut(rw http.ResponseWriter, r *http.Request) {
}
http.Redirect(rw, r, redirect, http.StatusFound)
}

func (a *Application) sendCORSPreflightResponse(rw http.ResponseWriter, r *http.Request, options ...string) {
a.addCORSHeaders(rw, r)

// Allow all headers that the client requested
requestedHeaders := r.Header.Get("Access-Control-Request-Headers")
if requestedHeaders != "" {
rw.Header().Set("Access-Control-Allow-Headers", requestedHeaders)
}

// Allow whatever method the client requested
requestedMethod := r.Header.Get("Access-Control-Request-Method")
if requestedMethod != "" {
rw.Header().Set("Access-Control-Allow-Methods", requestedMethod)
}
}

// addCORSHeaders adds headers to the response to allow CORS requests to work properly. Without this, redirect responses
// to CORS requests will fail in the browser.
// These should be set both on preflight (OPTIONS) requests and actual authenticated requests.
func (a *Application) addCORSHeaders(rw http.ResponseWriter, r *http.Request) {
origin := r.Header.Get("Origin")
if origin != "" {
rw.Header().Set("Access-Control-Allow-Origin", origin)
}

rw.Header().Set("Vary", "Origin")
rw.Header().Set("Access-Control-Allow-Credentials", "true")

a.log.WithField("origin", origin).Trace("added CORS headers to response")
}

// isCORSPreflightRequest returns true if the request is a CORS preflight request, false otherwise.
// CORS preflight requests should be allowed to pass through unauthenticated.
func (a *Application) isCORSPreflightRequest(r *http.Request) bool {
if r.Method != http.MethodOptions {
a.log.WithField("method", r.Method).Trace("not an OPTIONS request, skipping CORS preflight check")
return false
}

if !a.isOriginAllowed(r) {
a.log.Trace("request origin is not allowed, skipping CORS preflight check")
return false
}

return true
}

// isOriginAllowed checks if the request's Origin header matches the application's configured CORS origin.
// If the Origin header is not present or is malformed, it returns false.
func (a *Application) isOriginAllowed(r *http.Request) bool {
origin := r.Header.Get("Origin")
if origin == "" {
a.log.Trace("no origin header present, skipping CORS preflight check")
return false
}

// The origin header can be a literal "null" when the request is coming from the same origin, e.g. from
// https://app.example.com to https://app.example.com/outpost.goauthentik.io/callback. Because the forward
// auth setup requires that the '/outpost.goauthentik.io' path is under the same origin as the application,
// and because endpoints under that path are called in a CORS "redirect" chain, same origin requests will
// occur. In this case, allow "null" origins to pass through.
originString := "null"
if origin != "null" {
parsedOrigin, err := url.Parse(origin)
if err != nil {
a.log.WithError(err).WithField("origin", origin).Trace("failed to parse origin header")
return false
}

// Ensure that the port is set for comparison
if err := setUrlPort(parsedOrigin); err != nil {
// This really shouldn't ever happen unless the client uses an unknown protocol
a.log.WithError(err).WithField("origin", origin).Trace("failed to set port for origin header")
return false
}

originString = strings.ToLower(parsedOrigin.String())
if a.corsOrigin != originString {
a.log.WithField("origin", origin).WithField("external_host", originString).Trace("origin does not match external host")
return false
}
}

a.log.WithField("origin", origin).WithField("external_host", originString).Trace("origin matches external host")
return true
}
48 changes: 44 additions & 4 deletions internal/outpost/proxyv2/application/mode_forward.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,14 +48,24 @@ func (a *Application) forwardHandleTraefik(rw http.ResponseWriter, r *http.Reque
a.handleSignOut(rw, r)
return
}

// Check for CORS preflight request and allow it to pass through unauthenticated
if a.isCORSPreflightRequest(r) {
rw.Header().Set("User-Agent", r.Header.Get("User-Agent"))
a.log.Trace("allowing CORS preflight request to pass through unauthenticated")
return
}

// Check if we're authenticated, or the request path is on the allowlist
claims, err := a.checkAuth(rw, r)
if claims != nil && err == nil {
a.addHeaders(rw.Header(), claims)
rw.Header().Set("User-Agent", r.Header.Get("User-Agent"))
a.log.WithField("headers", rw.Header()).Trace("headers written to forward_auth")
return
} else if claims == nil && a.IsAllowlisted(fwd) {
}

if claims == nil && a.IsAllowlisted(fwd) {
a.log.Trace("path can be accessed without authentication")
return
}
Expand Down Expand Up @@ -91,14 +101,24 @@ func (a *Application) forwardHandleCaddy(rw http.ResponseWriter, r *http.Request
a.handleSignOut(rw, r)
return
}

// Check for CORS preflight request and allow it to pass through unauthenticated
if a.isCORSPreflightRequest(r) {
rw.Header().Set("User-Agent", r.Header.Get("User-Agent"))
a.log.Trace("allowing CORS preflight request to pass through unauthenticated")
return
}

// Check if we're authenticated, or the request path is on the allowlist
claims, err := a.checkAuth(rw, r)
if claims != nil && err == nil {
a.addHeaders(rw.Header(), claims)
rw.Header().Set("User-Agent", r.Header.Get("User-Agent"))
a.log.WithField("headers", rw.Header()).Trace("headers written to forward_auth")
return
} else if claims == nil && a.IsAllowlisted(fwd) {
}

if claims == nil && a.IsAllowlisted(fwd) {
a.log.Trace("path can be accessed without authentication")
return
}
Expand All @@ -123,14 +143,23 @@ func (a *Application) forwardHandleNginx(rw http.ResponseWriter, r *http.Request
return
}

// Check for CORS preflight request and allow it to pass through unauthenticated
if a.isCORSPreflightRequest(r) {
rw.Header().Set("User-Agent", r.Header.Get("User-Agent"))
a.log.Trace("allowing CORS preflight request to pass through unauthenticated")
return
}

claims, err := a.checkAuth(rw, r)
if claims != nil && err == nil {
a.addHeaders(rw.Header(), claims)
rw.Header().Set("User-Agent", r.Header.Get("User-Agent"))
rw.WriteHeader(200)
a.log.WithField("headers", rw.Header()).Trace("headers written to forward_auth")
return
} else if claims == nil && a.IsAllowlisted(fwd) {
}

if claims == nil && a.IsAllowlisted(fwd) {
a.log.Trace("path can be accessed without authentication")
return
}
Expand Down Expand Up @@ -158,17 +187,28 @@ func (a *Application) forwardHandleEnvoy(rw http.ResponseWriter, r *http.Request
r.URL.Path = strings.TrimPrefix(r.URL.Path, envoyPrefix)
r.URL.Host = r.Host
fwd := r.URL

// Check for CORS preflight request and allow it to pass through unauthenticated
if a.isCORSPreflightRequest(r) {
rw.Header().Set("User-Agent", r.Header.Get("User-Agent"))
a.log.Trace("allowing CORS preflight request to pass through unauthenticated")
return
}

// Check if we're authenticated, or the request path is on the allowlist
claims, err := a.checkAuth(rw, r)
if claims != nil && err == nil {
a.addHeaders(rw.Header(), claims)
rw.Header().Set("User-Agent", r.Header.Get("User-Agent"))
a.log.WithField("headers", rw.Header()).Trace("headers written to forward_auth")
return
} else if claims == nil && a.IsAllowlisted(fwd) {
}

if claims == nil && a.IsAllowlisted(fwd) {
a.log.Trace("path can be accessed without authentication")
return
}

// set the redirect 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
Expand Down
7 changes: 7 additions & 0 deletions internal/outpost/proxyv2/application/oauth.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,13 @@ func (a *Application) handleAuthStart(rw http.ResponseWriter, r *http.Request, f
if err != nil {
a.log.WithError(err).Warning("failed to save session")
}

// Add CORS headers if origin is allowed
if a.isOriginAllowed(r) {
a.addCORSHeaders(rw, r)
return
}

http.Redirect(rw, r, a.oauthConfig.AuthCodeURL(state), http.StatusFound)
}

Expand Down
7 changes: 7 additions & 0 deletions internal/outpost/proxyv2/application/oauth_callback.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,13 @@ import (
)

func (a *Application) handleAuthCallback(rw http.ResponseWriter, r *http.Request) {
// Check for CORS preflight request and allow it to pass through unauthenticated
if a.isCORSPreflightRequest(r) {
a.sendCORSPreflightResponse(rw, r)
a.log.Trace("responding to CORS preflight request on callback endpoint")
return
}

state := a.stateFromRequest(r)
if state == nil {
a.log.Warning("invalid state")
Expand Down
Loading