1
0
mirror of https://github.com/oauth2-proxy/oauth2-proxy.git synced 2025-06-15 00:15:00 +02:00

Merge branch 'master' into refactor

This commit is contained in:
Joel Speed
2019-07-15 11:30:43 +01:00
committed by GitHub
18 changed files with 689 additions and 184 deletions

View File

@ -1,6 +1,7 @@
package main
import (
"context"
b64 "encoding/base64"
"errors"
"fmt"
@ -13,6 +14,7 @@ import (
"strings"
"time"
"github.com/coreos/go-oidc"
"github.com/mbland/hmacauth"
sessionsapi "github.com/pusher/oauth2_proxy/pkg/apis/sessions"
"github.com/pusher/oauth2_proxy/pkg/encryption"
@ -47,6 +49,11 @@ var SignatureHeaders = []string{
"Gap-Auth",
}
var (
// ErrNeedsLogin means the user should be redirected to the login page
ErrNeedsLogin = errors.New("redirect to login page")
)
// OAuthProxy is the main authentication proxy
type OAuthProxy struct {
CookieSeed string
@ -87,8 +94,11 @@ type OAuthProxy struct {
PassAuthorization bool
skipAuthRegex []string
skipAuthPreflight bool
skipJwtBearerTokens bool
jwtBearerVerifiers []*oidc.IDTokenVerifier
compiledRegex []*regexp.Regexp
templates *template.Template
Banner string
Footer string
}
@ -151,7 +161,7 @@ func NewFileServer(path string, filesystemPath string) (proxy http.Handler) {
}
// NewWebSocketOrRestReverseProxy creates a reverse proxy for REST or websocket based on url
func NewWebSocketOrRestReverseProxy(u *url.URL, opts *Options, auth hmacauth.HmacAuth) (restProxy http.Handler) {
func NewWebSocketOrRestReverseProxy(u *url.URL, opts *Options, auth hmacauth.HmacAuth) http.Handler {
u.Path = ""
proxy := NewReverseProxy(u, opts.FlushInterval)
if !opts.PassHostHeader {
@ -167,7 +177,12 @@ func NewWebSocketOrRestReverseProxy(u *url.URL, opts *Options, auth hmacauth.Hma
wsURL := &url.URL{Scheme: wsScheme, Host: u.Host}
wsProxy = wsutil.NewSingleHostReverseProxy(wsURL)
}
return &UpstreamProxy{u.Host, proxy, wsProxy, auth}
return &UpstreamProxy{
upstream: u.Host,
handler: proxy,
wsHandler: wsProxy,
auth: auth,
}
}
// NewOAuthProxy creates a new instance of OOuthProxy from the options provided
@ -192,7 +207,13 @@ func NewOAuthProxy(opts *Options, validator func(string) bool) *OAuthProxy {
}
logger.Printf("mapping path %q => file system %q", path, u.Path)
proxy := NewFileServer(path, u.Path)
serveMux.Handle(path, &UpstreamProxy{path, proxy, nil, nil})
uProxy := UpstreamProxy{
upstream: path,
handler: proxy,
wsHandler: nil,
auth: nil,
}
serveMux.Handle(path, &uProxy)
default:
panic(fmt.Sprintf("unknown upstream protocol %s", u.Scheme))
}
@ -201,6 +222,12 @@ func NewOAuthProxy(opts *Options, validator func(string) bool) *OAuthProxy {
logger.Printf("compiled skip-auth-regex => %q", u)
}
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)
}
}
redirectURL := opts.redirectURL
if redirectURL.Path == "" {
redirectURL.Path = fmt.Sprintf("%s/callback", opts.ProxyPrefix)
@ -234,25 +261,28 @@ func NewOAuthProxy(opts *Options, validator func(string) bool) *OAuthProxy {
OAuthCallbackPath: fmt.Sprintf("%s/callback", opts.ProxyPrefix),
AuthOnlyPath: fmt.Sprintf("%s/auth", opts.ProxyPrefix),
ProxyPrefix: opts.ProxyPrefix,
provider: opts.provider,
sessionStore: opts.sessionStore,
serveMux: serveMux,
redirectURL: redirectURL,
whitelistDomains: opts.WhitelistDomains,
skipAuthRegex: opts.SkipAuthRegex,
skipAuthPreflight: opts.SkipAuthPreflight,
compiledRegex: opts.CompiledRegex,
SetXAuthRequest: opts.SetXAuthRequest,
PassBasicAuth: opts.PassBasicAuth,
PassUserHeaders: opts.PassUserHeaders,
BasicAuthPassword: opts.BasicAuthPassword,
PassAccessToken: opts.PassAccessToken,
SetAuthorization: opts.SetAuthorization,
PassAuthorization: opts.PassAuthorization,
SkipProviderButton: opts.SkipProviderButton,
templates: loadTemplates(opts.CustomTemplatesDir),
Footer: opts.Footer,
ProxyPrefix: opts.ProxyPrefix,
provider: opts.provider,
sessionStore: opts.sessionStore,
serveMux: serveMux,
redirectURL: redirectURL,
whitelistDomains: opts.WhitelistDomains,
skipAuthRegex: opts.SkipAuthRegex,
skipAuthPreflight: opts.SkipAuthPreflight,
skipJwtBearerTokens: opts.SkipJwtBearerTokens,
jwtBearerVerifiers: opts.jwtBearerVerifiers,
compiledRegex: opts.CompiledRegex,
SetXAuthRequest: opts.SetXAuthRequest,
PassBasicAuth: opts.PassBasicAuth,
PassUserHeaders: opts.PassUserHeaders,
BasicAuthPassword: opts.BasicAuthPassword,
PassAccessToken: opts.PassAccessToken,
SetAuthorization: opts.SetAuthorization,
PassAuthorization: opts.PassAuthorization,
SkipProviderButton: opts.SkipProviderButton,
templates: loadTemplates(opts.CustomTemplatesDir),
Banner: opts.Banner,
Footer: opts.Footer,
}
}
@ -477,20 +507,19 @@ func (p *OAuthProxy) IsValidRedirect(redirect string) bool {
}
// IsWhitelistedRequest is used to check if auth should be skipped for this request
func (p *OAuthProxy) IsWhitelistedRequest(req *http.Request) (ok bool) {
func (p *OAuthProxy) IsWhitelistedRequest(req *http.Request) bool {
isPreflightRequestAllowed := p.skipAuthPreflight && req.Method == "OPTIONS"
return isPreflightRequestAllowed || p.IsWhitelistedPath(req.URL.Path)
}
// IsWhitelistedPath is used to check if the request path is allowed without auth
func (p *OAuthProxy) IsWhitelistedPath(path string) (ok bool) {
func (p *OAuthProxy) IsWhitelistedPath(path string) bool {
for _, u := range p.compiledRegex {
ok = u.MatchString(path)
if ok {
return
if u.MatchString(path) {
return true
}
}
return
return false
}
func getRemoteAddr(req *http.Request) (s string) {
@ -634,63 +663,97 @@ func (p *OAuthProxy) OAuthCallback(rw http.ResponseWriter, req *http.Request) {
}
http.Redirect(rw, req, redirect, 302)
} else {
logger.PrintAuthf(session.Email, req, logger.AuthSuccess, "Invalid authentication via OAuth2: unauthorized")
logger.PrintAuthf(session.Email, req, logger.AuthFailure, "Invalid authentication via OAuth2: unauthorized")
p.ErrorPage(rw, 403, "Permission Denied", "Invalid Account")
}
}
// AuthenticateOnly checks whether the user is currently logged in
func (p *OAuthProxy) AuthenticateOnly(rw http.ResponseWriter, req *http.Request) {
status := p.Authenticate(rw, req)
if status == http.StatusAccepted {
rw.WriteHeader(http.StatusAccepted)
} else {
session, err := p.getAuthenticatedSession(rw, req)
if err != nil {
http.Error(rw, "unauthorized request", http.StatusUnauthorized)
return
}
// we are authenticated
p.addHeadersForProxying(rw, req, session)
rw.WriteHeader(http.StatusAccepted)
}
// Proxy proxies the user request if the user is authenticated else it prompts
// them to authenticate
func (p *OAuthProxy) Proxy(rw http.ResponseWriter, req *http.Request) {
status := p.Authenticate(rw, req)
if status == http.StatusInternalServerError {
p.ErrorPage(rw, http.StatusInternalServerError,
"Internal Error", "Internal Error")
} else if status == http.StatusForbidden {
session, err := p.getAuthenticatedSession(rw, req)
switch err {
case nil:
// we are authenticated
p.addHeadersForProxying(rw, req, session)
p.serveMux.ServeHTTP(rw, req)
case ErrNeedsLogin:
// we need to send the user to a login screen
if isAjax(req) {
// no point redirecting an AJAX request
p.ErrorJSON(rw, http.StatusUnauthorized)
return
}
if p.SkipProviderButton {
p.OAuthStart(rw, req)
} else {
p.SignInPage(rw, req, http.StatusForbidden)
}
} else if status == http.StatusUnauthorized {
p.ErrorJSON(rw, status)
} else {
p.serveMux.ServeHTTP(rw, req)
default:
// unknown error
logger.Printf("Unexpected internal error: %s", err)
p.ErrorPage(rw, http.StatusInternalServerError,
"Internal Error", "Internal Error")
}
}
// Authenticate checks whether a user is authenticated
func (p *OAuthProxy) Authenticate(rw http.ResponseWriter, req *http.Request) int {
// getAuthenticatedSession checks whether a user is authenticated and returns a session object and nil error if so
// Returns nil, ErrNeedsLogin if user needs to login.
// 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) {
var session *sessionsapi.SessionState
var err error
var saveSession, clearSession, revalidated bool
if p.skipJwtBearerTokens && req.Header.Get("Authorization") != "" {
session, err = p.GetJwtSession(req)
if err != nil {
logger.Printf("Error retrieving session from token in Authorization header: %s", err)
}
if session != nil {
saveSession = false
}
}
remoteAddr := getRemoteAddr(req)
if session == nil {
session, err = p.LoadCookiedSession(req)
if err != nil {
logger.Printf("Error loading cookied session: %s", err)
}
session, err := p.LoadCookiedSession(req)
if err != nil {
logger.Printf("Error loading cookied session: %s", err)
}
if session != nil && session.Age() > p.CookieRefresh && p.CookieRefresh != time.Duration(0) {
logger.Printf("Refreshing %s old session cookie for %s (refresh after %s)", session.Age(), session, p.CookieRefresh)
saveSession = true
}
if session != nil {
if session.Age() > p.CookieRefresh && p.CookieRefresh != time.Duration(0) {
logger.Printf("Refreshing %s old session cookie for %s (refresh after %s)", session.Age(), session, p.CookieRefresh)
saveSession = true
}
var ok bool
if ok, err = p.provider.RefreshSessionIfNeeded(session); err != nil {
logger.Printf("%s removing session. error refreshing access token %s %s", remoteAddr, err, session)
clearSession = true
session = nil
} else if ok {
saveSession = true
revalidated = true
if ok, err := p.provider.RefreshSessionIfNeeded(session); err != nil {
logger.Printf("%s removing session. error refreshing access token %s %s", remoteAddr, err, session)
clearSession = true
session = nil
} else if ok {
saveSession = true
revalidated = true
}
}
}
if session != nil && session.IsExpired() {
@ -709,18 +772,20 @@ func (p *OAuthProxy) Authenticate(rw http.ResponseWriter, req *http.Request) int
}
}
if session != nil && session.Email != "" && !p.Validator(session.Email) {
logger.Printf(session.Email, req, logger.AuthFailure, "Invalid authentication via session: removing session %s", session)
session = nil
saveSession = false
clearSession = true
if session != nil && session.Email != "" {
if !p.Validator(session.Email) || !p.provider.ValidateGroup(session.Email) {
logger.Printf(session.Email, req, logger.AuthFailure, "Invalid authentication via session: removing session %s", session)
session = nil
saveSession = false
clearSession = true
}
}
if saveSession && session != nil {
err = p.SaveSession(rw, req, session)
if err != nil {
logger.PrintAuthf(session.Email, req, logger.AuthError, "Save session error %s", err)
return http.StatusInternalServerError
return nil, err
}
}
@ -736,15 +801,14 @@ func (p *OAuthProxy) Authenticate(rw http.ResponseWriter, req *http.Request) int
}
if session == nil {
// Check if is an ajax request and return unauthorized to avoid a redirect
// to the login page
if p.isAjax(req) {
return http.StatusUnauthorized
}
return http.StatusForbidden
return nil, ErrNeedsLogin
}
// At this point, the user is authenticated. proxy normally
return session, nil
}
// addHeadersForProxying adds the appropriate headers the request / response for proxying
func (p *OAuthProxy) addHeadersForProxying(rw http.ResponseWriter, req *http.Request, session *sessionsapi.SessionState) {
if p.PassBasicAuth {
req.SetBasicAuth(session.User, p.BasicAuthPassword)
req.Header["X-Forwarded-User"] = []string{session.User}
@ -781,7 +845,6 @@ func (p *OAuthProxy) Authenticate(rw http.ResponseWriter, req *http.Request) int
} else {
rw.Header().Set("GAP-Auth", session.Email)
}
return http.StatusAccepted
}
// CheckBasicAuth checks the requests Authorization header for basic auth
@ -815,7 +878,7 @@ func (p *OAuthProxy) CheckBasicAuth(req *http.Request) (*sessionsapi.SessionStat
}
// isAjax checks if a request is an ajax request
func (p *OAuthProxy) isAjax(req *http.Request) bool {
func isAjax(req *http.Request) bool {
acceptValues, ok := req.Header["accept"]
if !ok {
acceptValues = req.Header["Accept"]
@ -834,3 +897,92 @@ func (p *OAuthProxy) ErrorJSON(rw http.ResponseWriter, code int) {
rw.Header().Set("Content-Type", applicationJSON)
rw.WriteHeader(code)
}
// GetJwtSession loads a session based on a JWT token in the authorization header.
func (p *OAuthProxy) GetJwtSession(req *http.Request) (*sessionsapi.SessionState, error) {
rawBearerToken, err := p.findBearerToken(req)
if err != nil {
return nil, err
}
ctx := context.Background()
var session *sessionsapi.SessionState
for _, verifier := range p.jwtBearerVerifiers {
bearerToken, err := verifier.Verify(ctx, rawBearerToken)
if err != nil {
logger.Printf("failed to verify bearer token: %v", err)
continue
}
var claims struct {
Subject string `json:"sub"`
Email string `json:"email"`
Verified *bool `json:"email_verified"`
}
if err := bearerToken.Claims(&claims); err != nil {
return nil, fmt.Errorf("failed to parse bearer token claims: %v", err)
}
if claims.Email == "" {
claims.Email = claims.Subject
}
if claims.Verified != nil && !*claims.Verified {
return nil, fmt.Errorf("email in id_token (%s) isn't verified", claims.Email)
}
session = &sessionsapi.SessionState{
AccessToken: rawBearerToken,
IDToken: rawBearerToken,
RefreshToken: "",
ExpiresOn: bearerToken.Expiry,
Email: claims.Email,
User: claims.Email,
}
return session, nil
}
return nil, fmt.Errorf("unable to verify jwt token %s", req.Header.Get("Authorization"))
}
// findBearerToken finds a valid JWT token from the Authorization header of a given request.
func (p *OAuthProxy) findBearerToken(req *http.Request) (string, error) {
auth := req.Header.Get("Authorization")
s := strings.SplitN(auth, " ", 2)
if len(s) != 2 {
return "", fmt.Errorf("invalid authorization header %s", auth)
}
jwtRegex := regexp.MustCompile(`^eyJ[a-zA-Z0-9_-]*\.eyJ[a-zA-Z0-9_-]*\.[a-zA-Z0-9_-]+$`)
var rawBearerToken string
if s[0] == "Bearer" && jwtRegex.MatchString(s[1]) {
rawBearerToken = s[1]
} else if s[0] == "Basic" {
// Check if we have a Bearer token masquerading in Basic
b, err := b64.StdEncoding.DecodeString(s[1])
if err != nil {
return "", err
}
pair := strings.SplitN(string(b), ":", 2)
if len(pair) != 2 {
return "", fmt.Errorf("invalid format %s", b)
}
user, password := pair[0], pair[1]
// check user, user+password, or just password for a token
if jwtRegex.MatchString(user) {
// Support blank passwords or magic `x-oauth-basic` passwords - nothing else
if password == "" || password == "x-oauth-basic" {
rawBearerToken = user
}
} else if jwtRegex.MatchString(password) {
// support passwords and ignore user
rawBearerToken = password
}
}
if rawBearerToken == "" {
return "", fmt.Errorf("no valid bearer token found in authorization header")
}
return rawBearerToken, nil
}