2012-12-11 05:59:23 +04:00
package main
import (
2019-01-17 23:49:14 +03:00
"context"
2019-11-08 01:38:36 +03:00
"encoding/json"
2012-12-11 05:59:23 +04:00
"errors"
"fmt"
2015-03-18 01:06:06 +03:00
"html/template"
2015-03-19 22:59:48 +03:00
"net"
2012-12-11 05:59:23 +04:00
"net/http"
"net/url"
2015-01-19 19:10:37 +03:00
"regexp"
2012-12-11 05:59:23 +04:00
"strings"
"time"
2014-08-08 00:16:39 +04:00
2020-07-18 02:42:51 +03:00
"github.com/justinas/alice"
2020-09-29 19:44:42 +03:00
ipapi "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/ip"
middlewareapi "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/middleware"
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
sessionsapi "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
2021-02-06 21:56:31 +03:00
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/app"
2020-09-29 19:44:42 +03:00
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/authentication/basic"
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/cookies"
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/encryption"
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/ip"
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger"
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/middleware"
2021-01-03 00:46:05 +03:00
requestutil "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/requests/util"
2020-09-29 19:44:42 +03:00
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/sessions"
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/upstream"
"github.com/oauth2-proxy/oauth2-proxy/v7/providers"
2012-12-11 05:59:23 +04:00
)
2018-11-29 17:26:41 +03:00
const (
2021-01-03 01:20:48 +03:00
schemeHTTPS = "https"
2019-01-31 18:22:30 +03:00
applicationJSON = "application/json"
2018-11-29 17:26:41 +03:00
)
2019-06-07 06:50:44 +03:00
var (
// ErrNeedsLogin means the user should be redirected to the login page
ErrNeedsLogin = errors . New ( "redirect to login page" )
2020-05-06 14:42:02 +03:00
2020-10-29 04:40:58 +03:00
// ErrAccessDenied means the user should receive a 401 Unauthorized response
ErrAccessDenied = errors . New ( "access denied" )
2020-05-06 14:42:02 +03:00
// Used to check final redirects are not susceptible to open redirects.
// Matches //, /\ and both of these with whitespace in between (eg / / or / \).
2020-06-27 14:07:24 +03:00
invalidRedirectRegex = regexp . MustCompile ( ` [/\\](?:[\s\v]*|\. { 1,2})[/\\] ` )
2019-06-07 06:50:44 +03:00
)
2020-09-23 04:54:32 +03:00
// allowedRoute manages method + path based allowlists
type allowedRoute struct {
method string
pathRegex * regexp . Regexp
}
2018-12-20 12:30:42 +03:00
// OAuthProxy is the main authentication proxy
2015-11-09 02:57:01 +03:00
type OAuthProxy struct {
2015-03-18 06:13:45 +03:00
CookieSeed string
2015-06-08 06:52:28 +03:00
CookieName string
2017-03-28 04:14:38 +03:00
CSRFCookieName string
2020-04-12 14:00:44 +03:00
CookieDomains [ ] string
2019-04-10 00:36:35 +03:00
CookiePath string
2015-03-18 06:13:45 +03:00
CookieSecure bool
2018-11-29 17:26:41 +03:00
CookieHTTPOnly bool
2015-03-18 06:13:45 +03:00
CookieExpire time . Duration
2015-05-08 17:00:57 +03:00
CookieRefresh time . Duration
2019-12-16 21:10:04 +03:00
CookieSameSite string
2015-03-18 06:13:45 +03:00
Validator func ( string ) bool
2012-12-11 05:59:23 +04:00
2015-05-30 01:47:40 +03:00
RobotsPath string
SignInPath string
2017-03-21 19:39:26 +03:00
SignOutPath string
2015-11-09 02:57:01 +03:00
OAuthStartPath string
OAuthCallbackPath string
2015-10-08 16:27:00 +03:00
AuthOnlyPath string
2019-11-08 01:38:36 +03:00
UserInfoPath string
2015-05-30 01:47:40 +03:00
2020-11-16 05:57:48 +03:00
allowedRoutes [ ] allowedRoute
redirectURL * url . URL // the url to receive requests at
whitelistDomains [ ] string
provider providers . Provider
providerNameOverride string
sessionStore sessionsapi . SessionStore
ProxyPrefix string
SignInMessage string
basicAuthValidator basic . Validator
displayHtpasswdForm bool
serveMux http . Handler
SetXAuthRequest bool
PassBasicAuth bool
SetBasicAuth bool
SkipProviderButton bool
PassUserHeaders bool
BasicAuthPassword string
PassAccessToken bool
SetAuthorization bool
PassAuthorization bool
PreferEmailToUser bool
skipAuthPreflight bool
skipJwtBearerTokens bool
templates * template . Template
realClientIPParser ipapi . RealClientIPParser
trustedIPs * ip . NetSet
Banner string
Footer string
2020-07-18 02:42:51 +03:00
sessionChain alice . Chain
2020-07-29 22:10:14 +03:00
headersChain alice . Chain
preAuthChain alice . Chain
2021-02-07 01:05:45 +03:00
errorPage * app . ErrorPage
2012-12-11 05:59:23 +04:00
}
2019-12-20 17:44:59 +03:00
// NewOAuthProxy creates a new instance of OAuthProxy from the options provided
2020-05-25 16:00:49 +03:00
func NewOAuthProxy ( opts * options . Options , validator func ( string ) bool ) ( * OAuthProxy , error ) {
sessionStore , err := sessions . NewSessionStore ( & opts . Session , & opts . Cookie )
if err != nil {
return nil , fmt . Errorf ( "error initialising session store: %v" , err )
}
2021-02-06 21:56:31 +03:00
templates , err := app . LoadTemplates ( opts . Templates . Path )
if err != nil {
return nil , fmt . Errorf ( "error loading templates: %v" , err )
}
2021-02-07 01:17:59 +03:00
errorPage := & app . ErrorPage {
Template : templates . Lookup ( "error.html" ) ,
ProxyPrefix : opts . ProxyPrefix ,
Footer : opts . Templates . Footer ,
Version : VERSION ,
}
upstreamProxy , err := upstream . NewProxy ( opts . UpstreamServers , opts . GetSignatureData ( ) , errorPage . ProxyErrorHandler )
2020-05-26 22:06:27 +03:00
if err != nil {
return nil , fmt . Errorf ( "error initialising upstream proxy: %v" , err )
2015-11-16 06:08:30 +03:00
}
2019-10-04 16:07:31 +03:00
2019-01-17 23:49:14 +03:00
if opts . SkipJwtBearerTokens {
logger . Printf ( "Skipping JWT tokens from configured OIDC issuer: %q" , opts . OIDCIssuerURL )
for _ , issuer := range opts . ExtraJwtIssuers {
logger . Printf ( "Skipping JWT tokens from extra JWT issuer: %q" , issuer )
}
}
2020-04-13 15:50:34 +03:00
redirectURL := opts . GetRedirectURL ( )
2019-03-20 19:25:04 +03:00
if redirectURL . Path == "" {
2019-03-05 17:58:26 +03:00
redirectURL . Path = fmt . Sprintf ( "%s/callback" , opts . ProxyPrefix )
}
2012-12-11 05:59:23 +04:00
2020-04-13 15:50:34 +03:00
logger . Printf ( "OAuthProxy configured for %s Client ID: %s" , opts . GetProvider ( ) . Data ( ) . ProviderName , opts . ClientID )
2015-06-22 22:10:08 +03:00
refresh := "disabled"
2020-04-12 16:00:59 +03:00
if opts . Cookie . Refresh != time . Duration ( 0 ) {
refresh = fmt . Sprintf ( "after %s" , opts . Cookie . Refresh )
2015-06-22 22:10:08 +03:00
}
2015-03-18 06:13:45 +03:00
2020-04-12 16:00:59 +03:00
logger . Printf ( "Cookie settings: name:%s secure(https):%v httponly:%v expiry:%s domains:%s path:%s samesite:%s refresh:%s" , opts . Cookie . Name , opts . Cookie . Secure , opts . Cookie . HTTPOnly , opts . Cookie . Expire , strings . Join ( opts . Cookie . Domains , "," ) , opts . Cookie . Path , opts . Cookie . SameSite , refresh )
2015-03-18 06:13:45 +03:00
2020-07-11 13:10:58 +03:00
trustedIPs := ip . NewNetSet ( )
for _ , ipStr := range opts . TrustedIPs {
if ipNet := ip . ParseIPNet ( ipStr ) ; ipNet != nil {
trustedIPs . AddIPNet ( * ipNet )
} else {
return nil , fmt . Errorf ( "could not parse IP network (%s)" , ipStr )
}
}
2020-07-18 12:18:47 +03:00
var basicAuthValidator basic . Validator
if opts . HtpasswdFile != "" {
logger . Printf ( "using htpasswd file: %s" , opts . HtpasswdFile )
var err error
basicAuthValidator , err = basic . NewHTPasswdValidator ( opts . HtpasswdFile )
if err != nil {
return nil , fmt . Errorf ( "could not load htpasswdfile: %v" , err )
}
}
2020-09-23 04:54:32 +03:00
allowedRoutes , err := buildRoutesAllowlist ( opts )
if err != nil {
return nil , err
}
2020-07-29 22:10:14 +03:00
preAuthChain , err := buildPreAuthChain ( opts )
if err != nil {
return nil , fmt . Errorf ( "could not build pre-auth chain: %v" , err )
}
2020-07-18 02:42:51 +03:00
sessionChain := buildSessionChain ( opts , sessionStore , basicAuthValidator )
2020-07-29 22:10:14 +03:00
headersChain , err := buildHeadersChain ( opts )
if err != nil {
return nil , fmt . Errorf ( "could not build headers chain: %v" , err )
}
2020-07-18 02:42:51 +03:00
2015-11-09 02:57:01 +03:00
return & OAuthProxy {
2020-04-12 16:00:59 +03:00
CookieName : opts . Cookie . Name ,
CSRFCookieName : fmt . Sprintf ( "%v_%v" , opts . Cookie . Name , "csrf" ) ,
CookieSeed : opts . Cookie . Secret ,
CookieDomains : opts . Cookie . Domains ,
CookiePath : opts . Cookie . Path ,
CookieSecure : opts . Cookie . Secure ,
CookieHTTPOnly : opts . Cookie . HTTPOnly ,
CookieExpire : opts . Cookie . Expire ,
CookieRefresh : opts . Cookie . Refresh ,
CookieSameSite : opts . Cookie . SameSite ,
2015-03-18 06:13:45 +03:00
Validator : validator ,
2014-11-09 22:51:10 +03:00
2015-05-30 01:47:40 +03:00
RobotsPath : "/robots.txt" ,
SignInPath : fmt . Sprintf ( "%s/sign_in" , opts . ProxyPrefix ) ,
2017-03-21 19:39:26 +03:00
SignOutPath : fmt . Sprintf ( "%s/sign_out" , opts . ProxyPrefix ) ,
2015-11-09 02:57:01 +03:00
OAuthStartPath : fmt . Sprintf ( "%s/start" , opts . ProxyPrefix ) ,
2019-03-12 19:46:37 +03:00
OAuthCallbackPath : fmt . Sprintf ( "%s/callback" , opts . ProxyPrefix ) ,
2015-10-08 16:27:00 +03:00
AuthOnlyPath : fmt . Sprintf ( "%s/auth" , opts . ProxyPrefix ) ,
2019-11-08 01:38:36 +03:00
UserInfoPath : fmt . Sprintf ( "%s/userinfo" , opts . ProxyPrefix ) ,
2015-05-30 01:47:40 +03:00
2020-11-16 05:57:48 +03:00
ProxyPrefix : opts . ProxyPrefix ,
provider : opts . GetProvider ( ) ,
providerNameOverride : opts . ProviderName ,
sessionStore : sessionStore ,
serveMux : upstreamProxy ,
redirectURL : redirectURL ,
allowedRoutes : allowedRoutes ,
whitelistDomains : opts . WhitelistDomains ,
skipAuthPreflight : opts . SkipAuthPreflight ,
skipJwtBearerTokens : opts . SkipJwtBearerTokens ,
realClientIPParser : opts . GetRealClientIPParser ( ) ,
SkipProviderButton : opts . SkipProviderButton ,
templates : templates ,
trustedIPs : trustedIPs ,
2021-02-06 20:40:51 +03:00
Banner : opts . Templates . Banner ,
Footer : opts . Templates . Footer ,
2020-11-16 05:57:48 +03:00
SignInMessage : buildSignInMessage ( opts ) ,
2020-07-18 12:18:47 +03:00
basicAuthValidator : basicAuthValidator ,
2021-02-06 20:40:51 +03:00
displayHtpasswdForm : basicAuthValidator != nil && opts . Templates . DisplayLoginForm ,
2020-07-18 02:42:51 +03:00
sessionChain : sessionChain ,
2020-07-29 22:10:14 +03:00
headersChain : headersChain ,
preAuthChain : preAuthChain ,
2021-02-07 01:17:59 +03:00
errorPage : errorPage ,
2020-05-25 16:00:49 +03:00
} , nil
2012-12-11 05:59:23 +04:00
}
2020-07-29 22:10:14 +03:00
// buildPreAuthChain constructs a chain that should process every request before
// the OAuth2 Proxy authentication logic kicks in.
// For example forcing HTTPS or health checks.
func buildPreAuthChain ( opts * options . Options ) ( alice . Chain , error ) {
2021-01-03 00:46:05 +03:00
chain := alice . New ( middleware . NewScope ( opts . ReverseProxy ) )
2020-07-18 02:42:51 +03:00
2020-07-29 22:10:14 +03:00
if opts . ForceHTTPS {
_ , httpsPort , err := net . SplitHostPort ( opts . HTTPSAddress )
if err != nil {
return alice . Chain { } , fmt . Errorf ( "invalid HTTPS address %q: %v" , opts . HTTPAddress , err )
}
chain = chain . Append ( middleware . NewRedirectToHTTPS ( httpsPort ) )
}
healthCheckPaths := [ ] string { opts . PingPath }
healthCheckUserAgents := [ ] string { opts . PingUserAgent }
if opts . GCPHealthChecks {
healthCheckPaths = append ( healthCheckPaths , "/liveness_check" , "/readiness_check" )
healthCheckUserAgents = append ( healthCheckUserAgents , "GoogleHC/1.0" )
}
// To silence logging of health checks, register the health check handler before
// the logging handler
if opts . Logging . SilencePing {
chain = chain . Append ( middleware . NewHealthCheck ( healthCheckPaths , healthCheckUserAgents ) , LoggingHandler )
} else {
chain = chain . Append ( LoggingHandler , middleware . NewHealthCheck ( healthCheckPaths , healthCheckUserAgents ) )
}
return chain , nil
}
func buildSessionChain ( opts * options . Options , sessionStore sessionsapi . SessionStore , validator basic . Validator ) alice . Chain {
chain := alice . New ( )
2020-07-18 02:42:51 +03:00
if opts . SkipJwtBearerTokens {
2020-11-16 05:57:48 +03:00
sessionLoaders := [ ] middlewareapi . TokenToSessionFunc {
opts . GetProvider ( ) . CreateSessionFromToken ,
2020-07-18 02:42:51 +03:00
}
for _ , verifier := range opts . GetJWTBearerVerifiers ( ) {
2020-11-16 05:57:48 +03:00
sessionLoaders = append ( sessionLoaders ,
middlewareapi . CreateTokenToSessionFunc ( verifier . Verify ) )
2020-07-18 02:42:51 +03:00
}
chain = chain . Append ( middleware . NewJwtSessionLoader ( sessionLoaders ) )
}
if validator != nil {
chain = chain . Append ( middleware . NewBasicAuthSessionLoader ( validator ) )
}
chain = chain . Append ( middleware . NewStoredSessionLoader ( & middleware . StoredSessionLoaderOptions {
SessionStore : sessionStore ,
RefreshPeriod : opts . Cookie . Refresh ,
RefreshSessionIfNeeded : opts . GetProvider ( ) . RefreshSessionIfNeeded ,
2020-10-24 08:06:50 +03:00
ValidateSessionState : opts . GetProvider ( ) . ValidateSession ,
2020-07-18 02:42:51 +03:00
} ) )
return chain
}
2020-07-29 22:10:14 +03:00
func buildHeadersChain ( opts * options . Options ) ( alice . Chain , error ) {
requestInjector , err := middleware . NewRequestHeaderInjector ( opts . InjectRequestHeaders )
if err != nil {
return alice . Chain { } , fmt . Errorf ( "error constructing request header injector: %v" , err )
}
responseInjector , err := middleware . NewResponseHeaderInjector ( opts . InjectResponseHeaders )
if err != nil {
return alice . Chain { } , fmt . Errorf ( "error constructing request header injector: %v" , err )
}
return alice . New ( requestInjector , responseInjector ) , nil
}
2020-07-21 07:01:54 +03:00
func buildSignInMessage ( opts * options . Options ) string {
var msg string
2021-02-06 20:40:51 +03:00
if len ( opts . Templates . Banner ) >= 1 {
if opts . Templates . Banner == "-" {
2020-07-21 07:01:54 +03:00
msg = ""
} else {
2021-02-06 20:40:51 +03:00
msg = opts . Templates . Banner
2020-07-21 07:01:54 +03:00
}
} else if len ( opts . EmailDomains ) != 0 && opts . AuthenticatedEmailsFile == "" {
if len ( opts . EmailDomains ) > 1 {
msg = fmt . Sprintf ( "Authenticate using one of the following domains: %v" , strings . Join ( opts . EmailDomains , ", " ) )
} else if opts . EmailDomains [ 0 ] != "*" {
msg = fmt . Sprintf ( "Authenticate using %v" , opts . EmailDomains [ 0 ] )
}
}
return msg
}
2020-09-23 04:54:32 +03:00
// buildRoutesAllowlist builds an []allowedRoute list from either the legacy
// SkipAuthRegex option (paths only support) or newer SkipAuthRoutes option
// (method=path support)
2020-10-05 22:39:44 +03:00
func buildRoutesAllowlist ( opts * options . Options ) ( [ ] allowedRoute , error ) {
routes := make ( [ ] allowedRoute , 0 , len ( opts . SkipAuthRegex ) + len ( opts . SkipAuthRoutes ) )
2020-09-23 04:54:32 +03:00
for _ , path := range opts . SkipAuthRegex {
compiledRegex , err := regexp . Compile ( path )
if err != nil {
return nil , err
}
2020-09-26 22:38:01 +03:00
logger . Printf ( "Skipping auth - Method: ALL | Path: %s" , path )
2020-10-05 22:39:44 +03:00
routes = append ( routes , allowedRoute {
2020-09-23 04:54:32 +03:00
method : "" ,
pathRegex : compiledRegex ,
} )
}
for _ , methodPath := range opts . SkipAuthRoutes {
var (
method string
path string
)
2020-10-05 22:39:44 +03:00
parts := strings . SplitN ( methodPath , "=" , 2 )
2020-09-23 04:54:32 +03:00
if len ( parts ) == 1 {
method = ""
path = parts [ 0 ]
} else {
method = strings . ToUpper ( parts [ 0 ] )
2020-10-05 22:39:44 +03:00
path = parts [ 1 ]
2020-09-23 04:54:32 +03:00
}
compiledRegex , err := regexp . Compile ( path )
if err != nil {
return nil , err
}
2020-09-26 22:38:01 +03:00
logger . Printf ( "Skipping auth - Method: %s | Path: %s" , method , path )
2020-10-05 22:39:44 +03:00
routes = append ( routes , allowedRoute {
2020-09-23 04:54:32 +03:00
method : method ,
pathRegex : compiledRegex ,
} )
}
return routes , nil
}
2018-12-20 12:30:42 +03:00
// MakeCSRFCookie creates a cookie for CSRF
2017-03-28 04:14:38 +03:00
func ( p * OAuthProxy ) MakeCSRFCookie ( req * http . Request , value string , expiration time . Duration , now time . Time ) * http . Cookie {
return p . makeCookie ( req , p . CSRFCookieName , value , expiration , now )
}
func ( p * OAuthProxy ) makeCookie ( req * http . Request , name string , value string , expiration time . Duration , now time . Time ) * http . Cookie {
2020-04-12 14:00:44 +03:00
cookieDomain := cookies . GetCookieDomain ( req , p . CookieDomains )
if cookieDomain != "" {
2021-01-03 00:46:05 +03:00
domain := requestutil . GetRequestHost ( req )
2017-04-19 06:33:50 +03:00
if h , _ , err := net . SplitHostPort ( domain ) ; err == nil {
domain = h
}
2020-04-12 14:00:44 +03:00
if ! strings . HasSuffix ( domain , cookieDomain ) {
2020-08-10 13:44:08 +03:00
logger . Errorf ( "Warning: request host is %q but using configured cookie domain of %q" , domain , cookieDomain )
2015-03-19 22:59:48 +03:00
}
2012-12-11 05:59:23 +04:00
}
2015-05-08 18:51:11 +03:00
return & http . Cookie {
2017-03-28 04:14:38 +03:00
Name : name ,
2015-05-08 18:51:11 +03:00
Value : value ,
2019-04-10 00:36:35 +03:00
Path : p . CookiePath ,
2020-04-12 14:00:44 +03:00
Domain : cookieDomain ,
2018-11-29 17:26:41 +03:00
HttpOnly : p . CookieHTTPOnly ,
2015-03-19 22:59:48 +03:00
Secure : p . CookieSecure ,
2015-06-22 22:10:08 +03:00
Expires : now . Add ( expiration ) ,
2019-12-16 21:10:04 +03:00
SameSite : cookies . ParseSameSite ( p . CookieSameSite ) ,
2012-12-11 05:59:23 +04:00
}
}
2018-12-20 12:30:42 +03:00
// ClearCSRFCookie creates a cookie to unset the CSRF cookie stored in the user's
// session
2017-03-28 04:14:38 +03:00
func ( p * OAuthProxy ) ClearCSRFCookie ( rw http . ResponseWriter , req * http . Request ) {
http . SetCookie ( rw , p . MakeCSRFCookie ( req , "" , time . Hour * - 1 , time . Now ( ) ) )
}
2018-12-20 12:30:42 +03:00
// SetCSRFCookie adds a CSRF cookie to the response
2017-03-28 04:14:38 +03:00
func ( p * OAuthProxy ) SetCSRFCookie ( rw http . ResponseWriter , req * http . Request , val string ) {
http . SetCookie ( rw , p . MakeCSRFCookie ( req , val , p . CookieExpire , time . Now ( ) ) )
2012-12-11 05:59:23 +04:00
}
2018-12-20 12:30:42 +03:00
// ClearSessionCookie creates a cookie to unset the user's authentication cookie
// stored in the user's session
2019-05-07 18:13:55 +03:00
func ( p * OAuthProxy ) ClearSessionCookie ( rw http . ResponseWriter , req * http . Request ) error {
return p . sessionStore . Clear ( rw , req )
2017-03-28 04:14:38 +03:00
}
2018-12-20 12:30:42 +03:00
// LoadCookiedSession reads the user's authentication details from the request
2019-05-07 18:13:55 +03:00
func ( p * OAuthProxy ) LoadCookiedSession ( req * http . Request ) ( * sessionsapi . SessionState , error ) {
return p . sessionStore . Load ( req )
2015-06-23 14:23:39 +03:00
}
2018-12-20 12:30:42 +03:00
// SaveSession creates a new session cookie value and sets this on the response
2019-05-07 16:27:09 +03:00
func ( p * OAuthProxy ) SaveSession ( rw http . ResponseWriter , req * http . Request , s * sessionsapi . SessionState ) error {
2019-05-07 18:13:55 +03:00
return p . sessionStore . Save ( rw , req , s )
2012-12-26 19:35:02 +04:00
}
2021-01-03 01:20:48 +03:00
// IsValidRedirect checks whether the redirect URL is whitelisted
func ( p * OAuthProxy ) IsValidRedirect ( redirect string ) bool {
switch {
case redirect == "" :
// The user didn't specify a redirect, should fallback to `/`
return false
case strings . HasPrefix ( redirect , "/" ) && ! strings . HasPrefix ( redirect , "//" ) && ! invalidRedirectRegex . MatchString ( redirect ) :
return true
case strings . HasPrefix ( redirect , "http://" ) || strings . HasPrefix ( redirect , "https://" ) :
redirectURL , err := url . Parse ( redirect )
if err != nil {
logger . Printf ( "Rejecting invalid redirect %q: scheme unsupported or missing" , redirect )
return false
}
redirectHostname := redirectURL . Hostname ( )
2021-02-01 21:04:33 +03:00
for _ , allowedDomain := range p . whitelistDomains {
allowedHost , allowedPort := splitHostPort ( allowedDomain )
if allowedHost == "" {
2021-01-03 01:20:48 +03:00
continue
}
2021-02-01 21:04:33 +03:00
if redirectHostname == strings . TrimPrefix ( allowedHost , "." ) ||
( strings . HasPrefix ( allowedHost , "." ) &&
strings . HasSuffix ( redirectHostname , allowedHost ) ) {
2021-01-03 01:20:48 +03:00
// the domain names match, now validate the ports
// if the whitelisted domain's port is '*', allow all ports
// if the whitelisted domain contains a specific port, only allow that port
// if the whitelisted domain doesn't contain a port at all, only allow empty redirect ports ie http and https
redirectPort := redirectURL . Port ( )
2021-02-01 21:04:33 +03:00
if allowedPort == "*" ||
allowedPort == redirectPort ||
( allowedPort == "" && redirectPort == "" ) {
2021-01-03 01:20:48 +03:00
return true
}
}
}
logger . Printf ( "Rejecting invalid redirect %q: domain / port not in whitelist" , redirect )
return false
default :
logger . Printf ( "Rejecting invalid redirect %q: not an absolute or relative URL" , redirect )
return false
}
}
func ( p * OAuthProxy ) ServeHTTP ( rw http . ResponseWriter , req * http . Request ) {
p . preAuthChain . Then ( http . HandlerFunc ( p . serveHTTP ) ) . ServeHTTP ( rw , req )
}
func ( p * OAuthProxy ) serveHTTP ( rw http . ResponseWriter , req * http . Request ) {
if req . URL . Path != p . AuthOnlyPath && strings . HasPrefix ( req . URL . Path , p . ProxyPrefix ) {
prepareNoCache ( rw )
}
switch path := req . URL . Path ; {
case path == p . RobotsPath :
2021-02-06 20:20:30 +03:00
p . RobotsTxt ( rw , req )
2021-01-03 01:20:48 +03:00
case p . IsAllowedRequest ( req ) :
p . SkipAuthProxy ( rw , req )
case path == p . SignInPath :
p . SignIn ( rw , req )
case path == p . SignOutPath :
p . SignOut ( rw , req )
case path == p . OAuthStartPath :
p . OAuthStart ( rw , req )
case path == p . OAuthCallbackPath :
p . OAuthCallback ( rw , req )
case path == p . AuthOnlyPath :
p . AuthOnly ( rw , req )
case path == p . UserInfoPath :
p . UserInfo ( rw , req )
default :
p . Proxy ( rw , req )
}
}
2018-12-20 12:30:42 +03:00
// RobotsTxt disallows scraping pages from the OAuthProxy
2021-02-06 20:20:30 +03:00
func ( p * OAuthProxy ) RobotsTxt ( rw http . ResponseWriter , req * http . Request ) {
2020-07-20 08:24:18 +03:00
_ , err := fmt . Fprintf ( rw , "User-agent: *\nDisallow: /" )
if err != nil {
2020-07-21 04:34:37 +03:00
logger . Printf ( "Error writing robots.txt: %v" , err )
2021-02-07 01:05:45 +03:00
p . ErrorPage ( rw , req , http . StatusInternalServerError , err . Error ( ) )
2020-07-21 04:34:37 +03:00
return
2020-07-20 08:24:18 +03:00
}
2020-07-21 04:34:37 +03:00
rw . WriteHeader ( http . StatusOK )
2015-05-10 22:15:52 +03:00
}
2018-12-20 12:30:42 +03:00
// ErrorPage writes an error response
2021-02-07 01:05:45 +03:00
func ( p * OAuthProxy ) ErrorPage ( rw http . ResponseWriter , req * http . Request , code int , appError string ) {
2021-02-06 20:20:30 +03:00
redirectURL , err := p . getAppRedirect ( req )
if err != nil {
logger . Errorf ( "Error obtaining redirect: %v" , err )
}
if redirectURL == p . SignInPath || redirectURL == "" {
redirectURL = "/"
}
2021-02-07 01:05:45 +03:00
p . errorPage . Render ( rw , code , redirectURL , appError )
2012-12-17 22:15:23 +04:00
}
2021-01-03 01:20:48 +03:00
// IsAllowedRequest is used to check if auth should be skipped for this request
func ( p * OAuthProxy ) IsAllowedRequest ( req * http . Request ) bool {
isPreflightRequestAllowed := p . skipAuthPreflight && req . Method == "OPTIONS"
return isPreflightRequestAllowed || p . isAllowedRoute ( req ) || p . isTrustedIP ( req )
}
// IsAllowedRoute is used to check if the request method & path is allowed without auth
func ( p * OAuthProxy ) isAllowedRoute ( req * http . Request ) bool {
for _ , route := range p . allowedRoutes {
if ( route . method == "" || req . Method == route . method ) && route . pathRegex . MatchString ( req . URL . Path ) {
return true
}
}
return false
}
// isTrustedIP is used to check if a request comes from a trusted client IP address.
func ( p * OAuthProxy ) isTrustedIP ( req * http . Request ) bool {
if p . trustedIPs == nil {
return false
}
remoteAddr , err := ip . GetClientIP ( p . realClientIPParser , req )
if err != nil {
logger . Errorf ( "Error obtaining real IP for trusted IP list: %v" , err )
// Possibly spoofed X-Real-IP header
return false
}
if remoteAddr == nil {
return false
}
return p . trustedIPs . Has ( remoteAddr )
}
2018-12-20 12:30:42 +03:00
// SignInPage writes the sing in template to the response
2015-11-09 02:57:01 +03:00
func ( p * OAuthProxy ) SignInPage ( rw http . ResponseWriter , req * http . Request , code int ) {
2020-04-09 17:39:07 +03:00
prepareNoCache ( rw )
2020-07-20 08:24:18 +03:00
err := p . ClearSessionCookie ( rw , req )
if err != nil {
2020-07-21 04:34:37 +03:00
logger . Printf ( "Error clearing session cookie: %v" , err )
2021-02-07 01:05:45 +03:00
p . ErrorPage ( rw , req , http . StatusInternalServerError , err . Error ( ) )
2020-07-20 08:24:18 +03:00
return
}
2012-12-17 22:15:23 +04:00
rw . WriteHeader ( code )
2012-12-26 19:35:02 +04:00
2021-01-03 01:20:48 +03:00
redirectURL , err := p . getAppRedirect ( req )
2020-02-28 12:59:27 +03:00
if err != nil {
2020-08-10 13:44:08 +03:00
logger . Errorf ( "Error obtaining redirect: %v" , err )
2021-02-07 01:05:45 +03:00
p . ErrorPage ( rw , req , http . StatusInternalServerError , err . Error ( ) )
2020-02-28 12:59:27 +03:00
return
2016-11-16 09:36:18 +03:00
}
2020-02-28 12:59:27 +03:00
if redirectURL == p . SignInPath {
redirectURL = "/"
2015-04-07 05:10:03 +03:00
}
2020-07-21 04:49:45 +03:00
// We allow unescaped template.HTML since it is user configured options
/* #nosec G203 */
2012-12-26 19:35:02 +04:00
t := struct {
2015-03-31 19:59:07 +03:00
ProviderName string
2020-04-04 17:01:11 +03:00
SignInMessage template . HTML
2014-12-09 23:38:57 +03:00
CustomLogin bool
2013-10-22 23:56:29 +04:00
Redirect string
2014-11-10 06:01:50 +03:00
Version string
2015-05-30 01:47:40 +03:00
ProxyPrefix string
2016-06-19 06:53:42 +03:00
Footer template . HTML
2012-12-26 19:55:41 +04:00
} {
2015-03-31 19:59:07 +03:00
ProviderName : p . provider . Data ( ) . ProviderName ,
2020-04-04 17:01:11 +03:00
SignInMessage : template . HTML ( p . SignInMessage ) ,
2020-07-18 12:18:47 +03:00
CustomLogin : p . displayHtpasswdForm ,
2020-02-28 12:59:27 +03:00
Redirect : redirectURL ,
2014-11-10 06:01:50 +03:00
Version : VERSION ,
2015-05-30 01:47:40 +03:00
ProxyPrefix : p . ProxyPrefix ,
2016-06-19 06:53:42 +03:00
Footer : template . HTML ( p . Footer ) ,
2012-12-26 19:55:41 +04:00
}
2019-11-25 20:20:37 +03:00
if p . providerNameOverride != "" {
t . ProviderName = p . providerNameOverride
}
2020-07-20 08:24:18 +03:00
err = p . templates . ExecuteTemplate ( rw , "sign_in.html" , t )
if err != nil {
2020-07-21 04:34:37 +03:00
logger . Printf ( "Error rendering sign_in.html template: %v" , err )
2021-02-07 01:05:45 +03:00
p . ErrorPage ( rw , req , http . StatusInternalServerError , err . Error ( ) )
2020-07-20 08:24:18 +03:00
}
2012-12-11 05:59:23 +04:00
}
2018-12-20 12:30:42 +03:00
// ManualSignIn handles basic auth logins to the proxy
2020-07-20 08:24:18 +03:00
func ( p * OAuthProxy ) ManualSignIn ( req * http . Request ) ( string , bool ) {
2020-07-18 12:18:47 +03:00
if req . Method != "POST" || p . basicAuthValidator == nil {
2012-12-26 19:55:41 +04:00
return "" , false
}
user := req . FormValue ( "username" )
passwd := req . FormValue ( "password" )
if user == "" {
return "" , false
}
// check auth
2020-07-18 12:18:47 +03:00
if p . basicAuthValidator . Validate ( user , passwd ) {
2019-02-10 20:01:13 +03:00
logger . PrintAuthf ( user , req , logger . AuthSuccess , "Authenticated via HtpasswdFile" )
2012-12-26 19:55:41 +04:00
return user , true
}
2019-02-10 20:01:13 +03:00
logger . PrintAuthf ( user , req , logger . AuthFailure , "Invalid authentication via HtpasswdFile" )
2012-12-26 19:55:41 +04:00
return "" , false
}
2021-01-03 01:20:48 +03:00
// SignIn serves a page prompting users to sign in
func ( p * OAuthProxy ) SignIn ( rw http . ResponseWriter , req * http . Request ) {
redirect , err := p . getAppRedirect ( req )
2013-10-24 19:31:08 +04:00
if err != nil {
2021-01-03 01:20:48 +03:00
logger . Errorf ( "Error obtaining redirect: %v" , err )
2021-02-07 01:05:45 +03:00
p . ErrorPage ( rw , req , http . StatusInternalServerError , err . Error ( ) )
2021-01-03 01:20:48 +03:00
return
2021-01-03 00:46:05 +03:00
}
2021-01-03 01:20:48 +03:00
user , ok := p . ManualSignIn ( req )
if ok {
session := & sessionsapi . SessionState { User : user }
err = p . SaveSession ( rw , req , session )
if err != nil {
logger . Printf ( "Error saving session: %v" , err )
2021-02-07 01:05:45 +03:00
p . ErrorPage ( rw , req , http . StatusInternalServerError , err . Error ( ) )
2021-01-03 01:20:48 +03:00
return
}
http . Redirect ( rw , req , redirect , http . StatusFound )
} else {
if p . SkipProviderButton {
p . OAuthStart ( rw , req )
} else {
p . SignInPage ( rw , req , http . StatusOK )
2021-01-03 00:46:05 +03:00
}
2013-10-24 19:31:08 +04:00
}
2021-01-03 00:46:05 +03:00
}
2021-01-02 02:23:11 +03:00
2021-01-03 01:20:48 +03:00
//UserInfo endpoint outputs session email and preferred username in JSON format
func ( p * OAuthProxy ) UserInfo ( rw http . ResponseWriter , req * http . Request ) {
2021-01-03 00:46:05 +03:00
2021-01-03 01:20:48 +03:00
session , err := p . getAuthenticatedSession ( rw , req )
if err != nil {
http . Error ( rw , http . StatusText ( http . StatusUnauthorized ) , http . StatusUnauthorized )
return
}
2021-01-03 00:46:05 +03:00
2021-01-03 01:20:48 +03:00
userInfo := struct {
User string ` json:"user" `
Email string ` json:"email" `
Groups [ ] string ` json:"groups,omitempty" `
PreferredUsername string ` json:"preferredUsername,omitempty" `
} {
User : session . User ,
Email : session . Email ,
Groups : session . Groups ,
PreferredUsername : session . PreferredUsername ,
2013-10-24 19:31:08 +04:00
}
2021-01-03 01:20:48 +03:00
rw . Header ( ) . Set ( "Content-Type" , "application/json" )
rw . WriteHeader ( http . StatusOK )
err = json . NewEncoder ( rw ) . Encode ( userInfo )
if err != nil {
logger . Printf ( "Error encoding user info: %v" , err )
2021-02-07 01:05:45 +03:00
p . ErrorPage ( rw , req , http . StatusInternalServerError , err . Error ( ) )
2021-01-03 00:46:05 +03:00
}
2013-10-24 19:31:08 +04:00
}
2021-01-03 01:20:48 +03:00
// SignOut sends a response to clear the authentication cookie
func ( p * OAuthProxy ) SignOut ( rw http . ResponseWriter , req * http . Request ) {
redirect , err := p . getAppRedirect ( req )
if err != nil {
logger . Errorf ( "Error obtaining redirect: %v" , err )
2021-02-07 01:05:45 +03:00
p . ErrorPage ( rw , req , http . StatusInternalServerError , err . Error ( ) )
2021-01-03 01:20:48 +03:00
return
2021-01-03 00:46:05 +03:00
}
2021-01-03 01:20:48 +03:00
err = p . ClearSessionCookie ( rw , req )
if err != nil {
logger . Errorf ( "Error clearing session cookie: %v" , err )
2021-02-07 01:05:45 +03:00
p . ErrorPage ( rw , req , http . StatusInternalServerError , err . Error ( ) )
2021-01-03 01:20:48 +03:00
return
2021-01-02 02:23:11 +03:00
}
2021-01-03 01:20:48 +03:00
http . Redirect ( rw , req , redirect , http . StatusFound )
}
2017-03-21 19:39:26 +03:00
2018-12-20 12:30:42 +03:00
// OAuthStart starts the OAuth2 authentication flow
2015-11-09 02:57:01 +03:00
func ( p * OAuthProxy ) OAuthStart ( rw http . ResponseWriter , req * http . Request ) {
2020-04-09 17:39:07 +03:00
prepareNoCache ( rw )
2019-05-24 19:06:48 +03:00
nonce , err := encryption . Nonce ( )
2017-03-28 04:14:38 +03:00
if err != nil {
2020-08-10 13:44:08 +03:00
logger . Errorf ( "Error obtaining nonce: %v" , err )
2021-02-07 01:05:45 +03:00
p . ErrorPage ( rw , req , http . StatusInternalServerError , err . Error ( ) )
2017-03-28 04:14:38 +03:00
return
}
p . SetCSRFCookie ( rw , req , nonce )
2021-01-03 01:20:48 +03:00
redirect , err := p . getAppRedirect ( req )
2015-06-23 14:23:39 +03:00
if err != nil {
2020-08-10 13:44:08 +03:00
logger . Errorf ( "Error obtaining redirect: %v" , err )
2021-02-07 01:05:45 +03:00
p . ErrorPage ( rw , req , http . StatusInternalServerError , err . Error ( ) )
2015-06-23 14:23:39 +03:00
return
2015-01-12 12:18:41 +03:00
}
2021-01-03 01:20:48 +03:00
redirectURI := p . getOAuthRedirectURI ( req )
2020-04-14 11:36:44 +03:00
http . Redirect ( rw , req , p . provider . GetLoginURL ( redirectURI , fmt . Sprintf ( "%v:%v" , nonce , redirect ) ) , http . StatusFound )
2015-06-23 14:23:39 +03:00
}
2015-01-12 12:18:41 +03:00
2018-12-20 12:30:42 +03:00
// OAuthCallback is the OAuth2 authentication flow callback that finishes the
// OAuth2 authentication flow
2015-11-09 02:57:01 +03:00
func ( p * OAuthProxy ) OAuthCallback ( rw http . ResponseWriter , req * http . Request ) {
2020-05-23 17:17:41 +03:00
remoteAddr := ip . GetClientString ( p . realClientIPParser , req , true )
2013-10-24 19:31:08 +04:00
2015-06-23 14:23:39 +03:00
// finish the oauth cycle
err := req . ParseForm ( )
if err != nil {
2020-08-10 13:44:08 +03:00
logger . Errorf ( "Error while parsing OAuth2 callback: %v" , err )
2021-02-07 01:05:45 +03:00
p . ErrorPage ( rw , req , http . StatusInternalServerError , err . Error ( ) )
2012-12-11 05:59:23 +04:00
return
}
2015-06-23 14:23:39 +03:00
errorString := req . Form . Get ( "error" )
if errorString != "" {
2020-08-10 13:44:08 +03:00
logger . Errorf ( "Error while parsing OAuth2 callback: %s" , errorString )
2021-02-07 01:05:45 +03:00
p . ErrorPage ( rw , req , http . StatusForbidden , errorString )
2012-12-11 05:59:23 +04:00
return
}
2021-01-03 01:20:48 +03:00
session , err := p . redeemCode ( req )
2015-06-23 14:23:39 +03:00
if err != nil {
2020-08-10 13:44:08 +03:00
logger . Errorf ( "Error redeeming code during OAuth2 callback: %v" , err )
2021-02-07 01:05:45 +03:00
p . ErrorPage ( rw , req , http . StatusInternalServerError , "Internal Error" )
2015-06-23 14:23:39 +03:00
return
}
2020-09-27 21:46:29 +03:00
err = p . enrichSessionState ( req . Context ( ) , session )
2020-09-26 23:19:08 +03:00
if err != nil {
logger . Errorf ( "Error creating session during OAuth2 callback: %v" , err )
2021-02-07 01:05:45 +03:00
p . ErrorPage ( rw , req , http . StatusInternalServerError , "Internal Error" )
2020-09-26 23:19:08 +03:00
return
}
state := strings . SplitN ( req . Form . Get ( "state" ) , ":" , 2 )
if len ( state ) != 2 {
2020-08-10 13:44:08 +03:00
logger . Error ( "Error while parsing OAuth2 state: invalid length" )
2021-02-07 01:05:45 +03:00
p . ErrorPage ( rw , req , http . StatusInternalServerError , "Invalid State" )
2017-03-28 04:14:38 +03:00
return
}
2020-09-26 23:19:08 +03:00
nonce := state [ 0 ]
redirect := state [ 1 ]
2017-03-28 04:14:38 +03:00
c , err := req . Cookie ( p . CSRFCookieName )
if err != nil {
2020-07-21 04:34:37 +03:00
logger . PrintAuthf ( session . Email , req , logger . AuthFailure , "Invalid authentication via OAuth2: unable to obtain CSRF cookie" )
2021-02-07 01:05:45 +03:00
p . ErrorPage ( rw , req , http . StatusForbidden , err . Error ( ) )
2017-03-28 04:14:38 +03:00
return
}
p . ClearCSRFCookie ( rw , req )
if c . Value != nonce {
2020-07-21 04:34:37 +03:00
logger . PrintAuthf ( session . Email , req , logger . AuthFailure , "Invalid authentication via OAuth2: CSRF token mismatch, potential attack" )
2021-02-07 01:05:45 +03:00
p . ErrorPage ( rw , req , http . StatusForbidden , "CSRF Failed" )
2017-03-28 04:14:38 +03:00
return
}
2017-09-29 18:55:50 +03:00
if ! p . IsValidRedirect ( redirect ) {
2015-06-23 14:23:39 +03:00
redirect = "/"
}
// set cookie, or deny
2020-10-24 06:53:38 +03:00
authorized , err := p . provider . Authorize ( req . Context ( ) , session )
if err != nil {
logger . Errorf ( "Error with authorization: %v" , err )
}
if p . Validator ( session . Email ) && authorized {
2019-04-23 19:36:18 +03:00
logger . PrintAuthf ( session . Email , req , logger . AuthSuccess , "Authenticated via OAuth2: %s" , session )
2015-06-23 14:23:39 +03:00
err := p . SaveSession ( rw , req , session )
2012-12-11 05:59:23 +04:00
if err != nil {
2020-10-24 06:53:38 +03:00
logger . Errorf ( "Error saving session state for %s: %v" , remoteAddr , err )
2021-02-07 01:05:45 +03:00
p . ErrorPage ( rw , req , http . StatusInternalServerError , err . Error ( ) )
2012-12-11 05:59:23 +04:00
return
}
2020-04-14 11:36:44 +03:00
http . Redirect ( rw , req , redirect , http . StatusFound )
2015-06-23 14:23:39 +03:00
} else {
2019-06-20 23:40:04 +03:00
logger . PrintAuthf ( session . Email , req , logger . AuthFailure , "Invalid authentication via OAuth2: unauthorized" )
2021-02-07 01:05:45 +03:00
p . ErrorPage ( rw , req , http . StatusForbidden , "Invalid Account" )
2015-06-23 14:23:39 +03:00
}
}
2012-12-11 05:59:23 +04:00
2021-01-03 01:20:48 +03:00
func ( p * OAuthProxy ) redeemCode ( req * http . Request ) ( * sessionsapi . SessionState , error ) {
code := req . Form . Get ( "code" )
if code == "" {
return nil , providers . ErrMissingCode
}
redirectURI := p . getOAuthRedirectURI ( req )
s , err := p . provider . Redeem ( req . Context ( ) , redirectURI , code )
if err != nil {
return nil , err
}
return s , nil
}
func ( p * OAuthProxy ) enrichSessionState ( ctx context . Context , s * sessionsapi . SessionState ) error {
var err error
if s . Email == "" {
s . Email , err = p . provider . GetEmailAddress ( ctx , s )
if err != nil && ! errors . Is ( err , providers . ErrNotImplemented ) {
return err
}
}
return p . provider . EnrichSession ( ctx , s )
}
2020-10-19 04:14:32 +03:00
// AuthOnly checks whether the user is currently logged in (both authentication
2020-11-20 07:27:28 +03:00
// and optional authorization).
2020-10-19 04:14:32 +03:00
func ( p * OAuthProxy ) AuthOnly ( rw http . ResponseWriter , req * http . Request ) {
2019-06-07 07:25:12 +03:00
session , err := p . getAuthenticatedSession ( rw , req )
2019-06-15 11:48:27 +03:00
if err != nil {
2020-11-18 06:03:41 +03:00
http . Error ( rw , http . StatusText ( http . StatusUnauthorized ) , http . StatusUnauthorized )
2019-06-15 11:48:27 +03:00
return
2015-10-08 16:27:00 +03:00
}
2019-06-15 11:48:27 +03:00
2020-11-20 07:27:28 +03:00
// Unauthorized cases need to return 403 to prevent infinite redirects with
// subrequest architectures
if ! authOnlyAuthorize ( req , session ) {
2020-11-18 06:03:41 +03:00
http . Error ( rw , http . StatusText ( http . StatusForbidden ) , http . StatusForbidden )
2020-10-19 04:14:32 +03:00
return
}
2019-06-15 11:48:27 +03:00
// we are authenticated
2021-01-03 01:20:48 +03:00
p . addHeadersForProxying ( rw , session )
2020-07-29 22:10:14 +03:00
p . headersChain . Then ( http . HandlerFunc ( func ( rw http . ResponseWriter , req * http . Request ) {
rw . WriteHeader ( http . StatusAccepted )
} ) ) . ServeHTTP ( rw , req )
2015-10-08 16:27:00 +03:00
}
2020-09-23 04:54:32 +03:00
// SkipAuthProxy proxies allowlisted requests and skips authentication
2020-07-15 01:46:44 +03:00
func ( p * OAuthProxy ) SkipAuthProxy ( rw http . ResponseWriter , req * http . Request ) {
2020-07-29 22:10:14 +03:00
p . headersChain . Then ( p . serveMux ) . ServeHTTP ( rw , req )
2020-07-15 01:46:44 +03:00
}
2018-12-20 12:30:42 +03:00
// Proxy proxies the user request if the user is authenticated else it prompts
// them to authenticate
2015-11-09 02:57:01 +03:00
func ( p * OAuthProxy ) Proxy ( rw http . ResponseWriter , req * http . Request ) {
2019-06-07 06:50:44 +03:00
session , err := p . getAuthenticatedSession ( rw , req )
switch err {
case nil :
// we are authenticated
2021-01-03 01:20:48 +03:00
p . addHeadersForProxying ( rw , session )
2020-07-29 22:10:14 +03:00
p . headersChain . Then ( p . serveMux ) . ServeHTTP ( rw , req )
2019-06-07 06:50:44 +03:00
case ErrNeedsLogin :
// we need to send the user to a login screen
if isAjax ( req ) {
// no point redirecting an AJAX request
2021-01-03 01:20:48 +03:00
p . errorJSON ( rw , http . StatusUnauthorized )
2019-06-07 06:50:44 +03:00
return
}
2015-11-11 03:42:35 +03:00
if p . SkipProviderButton {
p . OAuthStart ( rw , req )
} else {
p . SignInPage ( rw , req , http . StatusForbidden )
}
2019-06-07 06:50:44 +03:00
2020-10-29 04:40:58 +03:00
case ErrAccessDenied :
2021-02-07 01:05:45 +03:00
p . ErrorPage ( rw , req , http . StatusUnauthorized , "Unauthorized" )
2020-10-29 04:40:58 +03:00
2019-06-07 06:50:44 +03:00
default :
// unknown error
2020-08-10 13:44:08 +03:00
logger . Errorf ( "Unexpected internal error: %v" , err )
2021-02-07 01:05:45 +03:00
p . ErrorPage ( rw , req , http . StatusInternalServerError , "Internal Error" )
2015-10-08 21:10:28 +03:00
}
2021-01-03 01:20:48 +03:00
}
// See https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/http-caching?hl=en
var noCacheHeaders = map [ string ] string {
"Expires" : time . Unix ( 0 , 0 ) . Format ( time . RFC1123 ) ,
"Cache-Control" : "no-cache, no-store, must-revalidate, max-age=0" ,
"X-Accel-Expires" : "0" , // https://www.nginx.com/resources/wiki/start/topics/examples/x-accel/
}
// prepareNoCache prepares headers for preventing browser caching.
func prepareNoCache ( w http . ResponseWriter ) {
// Set NoCache headers
for k , v := range noCacheHeaders {
w . Header ( ) . Set ( k , v )
}
}
// getOAuthRedirectURI returns the redirectURL that the upstream OAuth Provider will
// redirect clients to once authenticated.
// This is usually the OAuthProxy callback URL.
func ( p * OAuthProxy ) getOAuthRedirectURI ( req * http . Request ) string {
// if `p.redirectURL` already has a host, return it
if p . redirectURL . Host != "" {
return p . redirectURL . String ( )
}
// Otherwise figure out the scheme + host from the request
rd := * p . redirectURL
rd . Host = requestutil . GetRequestHost ( req )
rd . Scheme = requestutil . GetRequestProto ( req )
// If CookieSecure is true, return `https` no matter what
// Not all reverse proxies set X-Forwarded-Proto
if p . CookieSecure {
rd . Scheme = schemeHTTPS
}
return rd . String ( )
}
2019-06-07 06:50:44 +03:00
2021-01-03 01:20:48 +03:00
// getAppRedirect determines the full URL or URI path to redirect clients to
// once authenticated with the OAuthProxy
// Strategy priority (first legal result is used):
// - `rd` querysting parameter
// - `X-Auth-Request-Redirect` header
// - `X-Forwarded-(Proto|Host|Uri)` headers (when ReverseProxy mode is enabled)
// - `X-Forwarded-(Proto|Host)` if `Uri` has the ProxyPath (i.e. /oauth2/*)
// - `X-Forwarded-Uri` direct URI path (when ReverseProxy mode is enabled)
// - `req.URL.RequestURI` if not under the ProxyPath (i.e. /oauth2/*)
// - `/`
func ( p * OAuthProxy ) getAppRedirect ( req * http . Request ) ( string , error ) {
err := req . ParseForm ( )
if err != nil {
return "" , err
}
// These redirect getter functions are strategies ordered by priority
// for figuring out the redirect URL.
type redirectGetter func ( req * http . Request ) string
for _ , rdGetter := range [ ] redirectGetter {
p . getRdQuerystringRedirect ,
p . getXAuthRequestRedirect ,
p . getXForwardedHeadersRedirect ,
p . getURIRedirect ,
} {
2021-01-09 22:45:26 +03:00
redirect := rdGetter ( req )
// Call `p.IsValidRedirect` again here a final time to be safe
if redirect != "" && p . IsValidRedirect ( redirect ) {
2021-01-03 01:20:48 +03:00
return redirect , nil
}
}
return "/" , nil
}
func isForwardedRequest ( req * http . Request ) bool {
return requestutil . IsProxied ( req ) &&
req . Host != requestutil . GetRequestHost ( req )
}
func ( p * OAuthProxy ) hasProxyPrefix ( path string ) bool {
return strings . HasPrefix ( path , fmt . Sprintf ( "%s/" , p . ProxyPrefix ) )
}
2021-01-09 22:45:26 +03:00
func ( p * OAuthProxy ) validateRedirect ( redirect string , errorFormat string ) string {
2021-01-03 01:20:48 +03:00
if p . IsValidRedirect ( redirect ) {
return redirect
}
2021-01-09 22:45:26 +03:00
if redirect != "" {
logger . Errorf ( errorFormat , redirect )
}
2021-01-03 01:20:48 +03:00
return ""
}
2021-01-09 22:45:26 +03:00
// getRdQuerystringRedirect handles this getAppRedirect strategy:
// - `rd` querysting parameter
func ( p * OAuthProxy ) getRdQuerystringRedirect ( req * http . Request ) string {
return p . validateRedirect (
req . Form . Get ( "rd" ) ,
"Invalid redirect provided in rd querystring parameter: %s" ,
)
}
2021-01-03 01:20:48 +03:00
// getXAuthRequestRedirect handles this getAppRedirect strategy:
// - `X-Auth-Request-Redirect` Header
func ( p * OAuthProxy ) getXAuthRequestRedirect ( req * http . Request ) string {
2021-01-09 22:45:26 +03:00
return p . validateRedirect (
req . Header . Get ( "X-Auth-Request-Redirect" ) ,
"Invalid redirect provided in X-Auth-Request-Redirect header: %s" ,
)
2021-01-03 01:20:48 +03:00
}
// getXForwardedHeadersRedirect handles these getAppRedirect strategies:
// - `X-Forwarded-(Proto|Host|Uri)` headers (when ReverseProxy mode is enabled)
// - `X-Forwarded-(Proto|Host)` if `Uri` has the ProxyPath (i.e. /oauth2/*)
func ( p * OAuthProxy ) getXForwardedHeadersRedirect ( req * http . Request ) string {
if ! isForwardedRequest ( req ) {
return ""
}
uri := requestutil . GetRequestURI ( req )
if p . hasProxyPrefix ( uri ) {
uri = "/"
}
redirect := fmt . Sprintf (
"%s://%s%s" ,
requestutil . GetRequestProto ( req ) ,
requestutil . GetRequestHost ( req ) ,
uri ,
)
2021-01-09 22:45:26 +03:00
return p . validateRedirect ( redirect ,
"Invalid redirect generated from X-Forwarded-* headers: %s" )
2021-01-03 01:20:48 +03:00
}
// getURIRedirect handles these getAppRedirect strategies:
// - `X-Forwarded-Uri` direct URI path (when ReverseProxy mode is enabled)
// - `req.URL.RequestURI` if not under the ProxyPath (i.e. /oauth2/*)
// - `/`
func ( p * OAuthProxy ) getURIRedirect ( req * http . Request ) string {
2021-01-09 22:45:26 +03:00
redirect := p . validateRedirect (
requestutil . GetRequestURI ( req ) ,
"Invalid redirect generated from X-Forwarded-Uri header: %s" ,
)
if redirect == "" {
2021-01-03 01:20:48 +03:00
redirect = req . URL . RequestURI ( )
}
if p . hasProxyPrefix ( redirect ) {
return "/"
}
return redirect
}
// splitHostPort separates host and port. If the port is not valid, it returns
// the entire input as host, and it doesn't check the validity of the host.
// Unlike net.SplitHostPort, but per RFC 3986, it requires ports to be numeric.
// *** taken from net/url, modified validOptionalPort() to accept ":*"
func splitHostPort ( hostport string ) ( host , port string ) {
host = hostport
colon := strings . LastIndexByte ( host , ':' )
if colon != - 1 && validOptionalPort ( host [ colon : ] ) {
host , port = host [ : colon ] , host [ colon + 1 : ]
}
if strings . HasPrefix ( host , "[" ) && strings . HasSuffix ( host , "]" ) {
host = host [ 1 : len ( host ) - 1 ]
}
return
}
// validOptionalPort reports whether port is either an empty string
// or matches /^:\d*$/
// *** taken from net/url, modified to accept ":*"
func validOptionalPort ( port string ) bool {
if port == "" || port == ":*" {
return true
}
if port [ 0 ] != ':' {
return false
}
for _ , b := range port [ 1 : ] {
if b < '0' || b > '9' {
return false
}
}
return true
2015-10-08 21:10:28 +03:00
}
2019-06-07 06:50:44 +03:00
// getAuthenticatedSession checks whether a user is authenticated and returns a session object and nil error if so
2020-10-29 04:40:58 +03:00
// Returns:
// - `nil, ErrNeedsLogin` if user needs to login.
// - `nil, ErrAccessDenied` if the authenticated user is not authorized
2019-06-07 06:50:44 +03:00
// Set-Cookie headers may be set on the response as a side-effect of calling this method.
func ( p * OAuthProxy ) getAuthenticatedSession ( rw http . ResponseWriter , req * http . Request ) ( * sessionsapi . SessionState , error ) {
2019-01-17 23:49:14 +03:00
var session * sessionsapi . SessionState
2015-06-23 14:23:39 +03:00
2020-07-18 02:42:51 +03:00
getSession := p . sessionChain . Then ( http . HandlerFunc ( func ( rw http . ResponseWriter , req * http . Request ) {
2021-01-03 00:46:05 +03:00
session = middlewareapi . GetRequestScope ( req ) . Session
2020-07-18 02:42:51 +03:00
} ) )
getSession . ServeHTTP ( rw , req )
2015-06-23 14:23:39 +03:00
2019-01-17 23:49:14 +03:00
if session == nil {
2020-07-18 02:42:51 +03:00
return nil , ErrNeedsLogin
2015-06-23 14:23:39 +03:00
}
2013-10-22 23:56:29 +04:00
2020-09-27 03:29:34 +03:00
invalidEmail := session . Email != "" && ! p . Validator ( session . Email )
authorized , err := p . provider . Authorize ( req . Context ( ) , session )
if err != nil {
logger . Errorf ( "Error with authorization: %v" , err )
}
2020-07-28 21:42:09 +03:00
2020-09-27 05:00:44 +03:00
if invalidEmail || ! authorized {
2020-10-29 04:40:58 +03:00
logger . PrintAuthf ( session . Email , req , logger . AuthFailure , "Invalid authorization via session: removing session %s" , session )
2020-07-18 02:42:51 +03:00
// Invalid session, clear it
2020-07-20 08:24:18 +03:00
err := p . ClearSessionCookie ( rw , req )
if err != nil {
2020-09-27 03:29:34 +03:00
logger . Errorf ( "Error clearing session cookie: %v" , err )
2020-07-20 08:24:18 +03:00
}
2020-10-29 04:40:58 +03:00
return nil , ErrAccessDenied
2012-12-11 05:59:23 +04:00
}
2019-06-07 06:50:44 +03:00
return session , nil
}
2020-11-20 07:27:28 +03:00
// authOnlyAuthorize handles special authorization logic that is only done
// on the AuthOnly endpoint for use with Nginx subrequest architectures.
2020-11-27 21:45:55 +03:00
//
// TODO (@NickMeves): This method is a placeholder to be extended but currently
// fails the linter. Remove the nolint when functionality expands.
//
//nolint:S1008
2020-11-20 07:27:28 +03:00
func authOnlyAuthorize ( req * http . Request , s * sessionsapi . SessionState ) bool {
2020-11-27 20:07:21 +03:00
// Allow secondary group restrictions based on the `allowed_groups`
// querystring parameter
2020-11-20 07:27:28 +03:00
if ! checkAllowedGroups ( req , s ) {
return false
}
return true
}
func checkAllowedGroups ( req * http . Request , s * sessionsapi . SessionState ) bool {
2020-10-19 04:14:32 +03:00
allowedGroups := extractAllowedGroups ( req )
if len ( allowedGroups ) == 0 {
return true
}
2020-11-20 07:27:28 +03:00
for _ , group := range s . Groups {
2020-10-19 04:14:32 +03:00
if _ , ok := allowedGroups [ group ] ; ok {
return true
}
}
return false
}
func extractAllowedGroups ( req * http . Request ) map [ string ] struct { } {
groups := map [ string ] struct { } { }
2020-11-27 20:07:21 +03:00
query := req . URL . Query ( )
for _ , allowedGroups := range query [ "allowed_groups" ] {
for _ , group := range strings . Split ( allowedGroups , "," ) {
if group != "" {
groups [ group ] = struct { } { }
}
2020-10-19 04:14:32 +03:00
}
}
return groups
}
2019-06-07 06:50:44 +03:00
// addHeadersForProxying adds the appropriate headers the request / response for proxying
2021-01-03 01:20:48 +03:00
func ( p * OAuthProxy ) addHeadersForProxying ( rw http . ResponseWriter , session * sessionsapi . SessionState ) {
2015-06-23 14:23:39 +03:00
if session . Email == "" {
rw . Header ( ) . Set ( "GAP-Auth" , session . User )
2015-03-19 23:37:16 +03:00
} else {
2015-06-23 14:23:39 +03:00
rw . Header ( ) . Set ( "GAP-Auth" , session . Email )
2015-03-19 23:37:16 +03:00
}
2012-12-11 05:59:23 +04:00
}
2019-01-30 13:13:12 +03:00
// isAjax checks if a request is an ajax request
2019-06-07 06:50:44 +03:00
func isAjax ( req * http . Request ) bool {
2020-04-14 11:36:44 +03:00
acceptValues := req . Header . Values ( "Accept" )
2019-01-31 18:22:30 +03:00
const ajaxReq = applicationJSON
2021-01-12 18:40:14 +03:00
// Iterate over multiple Accept headers, i.e.
// Accept: application/json
// Accept: text/plain
for _ , mimeTypes := range acceptValues {
// Iterate over multiple mimetypes in a single header, i.e.
// Accept: application/json, text/plain, */*
for _ , mimeType := range strings . Split ( mimeTypes , "," ) {
mimeType = strings . TrimSpace ( mimeType )
if mimeType == ajaxReq {
return true
}
2019-01-30 13:13:12 +03:00
}
}
return false
}
2021-01-03 01:20:48 +03:00
// errorJSON returns the error code with an application/json mime type
func ( p * OAuthProxy ) errorJSON ( rw http . ResponseWriter , code int ) {
2019-01-31 18:22:30 +03:00
rw . Header ( ) . Set ( "Content-Type" , applicationJSON )
2019-01-30 13:13:12 +03:00
rw . WriteHeader ( code )
}