2012-12-11 05:59:23 +04:00
package main
import (
"bytes"
2015-04-03 03:57:17 +03:00
"crypto/aes"
"crypto/cipher"
2012-12-11 05:59:23 +04:00
"encoding/base64"
"errors"
"fmt"
2015-03-18 01:06:06 +03:00
"html/template"
2012-12-11 05:59:23 +04:00
"log"
2015-03-19 22:59:48 +03:00
"net"
2012-12-11 05:59:23 +04:00
"net/http"
"net/http/httputil"
"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
2015-03-30 18:42:37 +03:00
"github.com/bitly/google_auth_proxy/api"
2015-03-30 22:48:30 +03:00
"github.com/bitly/google_auth_proxy/providers"
2012-12-11 05:59:23 +04:00
)
2014-10-15 00:22:38 +04:00
const pingPath = "/ping"
2012-12-11 05:59:23 +04:00
const signInPath = "/oauth2/sign_in"
const oauthStartPath = "/oauth2/start"
const oauthCallbackPath = "/oauth2/callback"
type OauthProxy struct {
2015-03-18 06:13:45 +03:00
CookieSeed string
CookieKey string
CookieDomain string
CookieSecure bool
CookieHttpOnly bool
CookieExpire time . Duration
Validator func ( string ) bool
2012-12-11 05:59:23 +04:00
2014-12-09 23:38:57 +03:00
redirectUrl * url . URL // the url to receive requests at
2015-03-30 22:48:30 +03:00
provider providers . Provider
2014-12-09 23:38:57 +03:00
oauthRedemptionUrl * url . URL // endpoint to redeem the code
oauthLoginUrl * url . URL // to redirect the user to
oauthScope string
clientID string
clientSecret string
SignInMessage string
HtpasswdFile * HtpasswdFile
DisplayHtpasswdForm bool
2015-03-19 23:37:16 +03:00
serveMux http . Handler
2014-12-09 23:38:57 +03:00
PassBasicAuth bool
2015-04-03 03:57:17 +03:00
AesCipher cipher . Block
2015-01-12 12:18:41 +03:00
skipAuthRegex [ ] string
compiledRegex [ ] * regexp . Regexp
2015-03-18 01:06:06 +03:00
templates * template . Template
2012-12-11 05:59:23 +04:00
}
2015-03-19 23:37:16 +03:00
type UpstreamProxy struct {
upstream string
handler http . Handler
}
func ( u * UpstreamProxy ) ServeHTTP ( w http . ResponseWriter , r * http . Request ) {
w . Header ( ) . Set ( "GAP-Upstream-Address" , u . upstream )
u . handler . ServeHTTP ( w , r )
}
2014-12-01 04:12:33 +03:00
func NewReverseProxy ( target * url . URL ) ( proxy * httputil . ReverseProxy ) {
2015-03-17 22:15:15 +03:00
return httputil . NewSingleHostReverseProxy ( target )
}
func setProxyUpstreamHostHeader ( proxy * httputil . ReverseProxy , target * url . URL ) {
director := proxy . Director
proxy . Director = func ( req * http . Request ) {
director ( req )
2015-03-18 00:17:40 +03:00
// use RequestURI so that we aren't unescaping encoded slashes in the request path
2015-03-21 22:29:07 +03:00
req . Host = target . Host
req . URL . Opaque = req . RequestURI
2015-03-18 00:17:40 +03:00
req . URL . RawQuery = ""
}
}
func setProxyDirector ( proxy * httputil . ReverseProxy ) {
director := proxy . Director
proxy . Director = func ( req * http . Request ) {
director ( req )
// use RequestURI so that we aren't unescaping encoded slashes in the request path
2015-03-21 22:29:07 +03:00
req . URL . Opaque = req . RequestURI
2015-03-18 00:17:40 +03:00
req . URL . RawQuery = ""
2015-03-17 22:15:15 +03:00
}
2014-12-01 04:12:33 +03:00
}
2014-11-09 22:51:10 +03:00
func NewOauthProxy ( opts * Options , validator func ( string ) bool ) * OauthProxy {
2012-12-11 05:59:23 +04:00
serveMux := http . NewServeMux ( )
2014-11-09 22:51:10 +03:00
for _ , u := range opts . proxyUrls {
2012-12-11 05:59:23 +04:00
path := u . Path
u . Path = ""
2014-11-09 22:51:10 +03:00
log . Printf ( "mapping path %q => upstream %q" , path , u )
2015-03-17 22:15:15 +03:00
proxy := NewReverseProxy ( u )
if ! opts . PassHostHeader {
setProxyUpstreamHostHeader ( proxy , u )
2015-03-18 00:17:40 +03:00
} else {
setProxyDirector ( proxy )
2015-03-17 22:15:15 +03:00
}
2015-03-19 23:37:16 +03:00
serveMux . Handle ( path , & UpstreamProxy { u . Host , proxy } )
2012-12-11 05:59:23 +04:00
}
2015-01-12 12:18:41 +03:00
for _ , u := range opts . CompiledRegex {
log . Printf ( "compiled skip-auth-regex => %q" , u )
}
2014-11-09 22:51:10 +03:00
redirectUrl := opts . redirectUrl
redirectUrl . Path = oauthCallbackPath
2012-12-11 05:59:23 +04:00
2014-11-10 05:07:02 +03:00
log . Printf ( "OauthProxy configured for %s" , opts . ClientID )
2014-11-10 06:21:46 +03:00
domain := opts . CookieDomain
if domain == "" {
domain = "<default>"
}
2015-03-18 06:13:45 +03:00
if ! opts . CookieHttpsOnly {
log . Printf ( "Warning: cookie-https-only setting is deprecated and will be removed in a future version. use cookie-secure" )
opts . CookieSecure = opts . CookieHttpsOnly
}
log . Printf ( "Cookie settings: secure (https):%v httponly:%v expiry:%s domain:%s" , opts . CookieSecure , opts . CookieHttpOnly , opts . CookieExpire , domain )
2015-04-03 03:57:17 +03:00
var aes_cipher cipher . Block
2015-04-05 16:43:40 +03:00
if opts . PassAccessToken {
2015-04-03 03:57:17 +03:00
var err error
aes_cipher , err = aes . NewCipher ( [ ] byte ( opts . CookieSecret ) )
if err != nil {
log . Fatal ( "error creating AES cipher with " +
"pass_access_token == true: %s" , err )
}
}
2014-11-09 22:51:10 +03:00
return & OauthProxy {
2015-03-18 06:13:45 +03:00
CookieKey : "_oauthproxy" ,
CookieSeed : opts . CookieSecret ,
CookieDomain : opts . CookieDomain ,
CookieSecure : opts . CookieSecure ,
CookieHttpOnly : opts . CookieHttpOnly ,
CookieExpire : opts . CookieExpire ,
Validator : validator ,
2014-11-09 22:51:10 +03:00
clientID : opts . ClientID ,
clientSecret : opts . ClientSecret ,
2015-03-30 22:48:30 +03:00
oauthScope : opts . provider . Data ( ) . Scope ,
provider : opts . provider ,
oauthRedemptionUrl : opts . provider . Data ( ) . RedeemUrl ,
oauthLoginUrl : opts . provider . Data ( ) . LoginUrl ,
2012-12-11 05:59:23 +04:00
serveMux : serveMux ,
2014-11-09 22:51:10 +03:00
redirectUrl : redirectUrl ,
2015-01-12 12:18:41 +03:00
skipAuthRegex : opts . SkipAuthRegex ,
2015-01-19 19:10:37 +03:00
compiledRegex : opts . CompiledRegex ,
2014-11-09 22:51:10 +03:00
PassBasicAuth : opts . PassBasicAuth ,
2015-04-03 03:57:17 +03:00
AesCipher : aes_cipher ,
2015-03-18 01:06:06 +03:00
templates : loadTemplates ( opts . CustomTemplatesDir ) ,
2012-12-11 05:59:23 +04:00
}
}
2015-03-17 23:25:19 +03:00
func ( p * OauthProxy ) GetRedirectUrl ( host string ) string {
// default to the request Host if not set
if p . redirectUrl . Host != "" {
return p . redirectUrl . String ( )
}
var u url . URL
u = * p . redirectUrl
if u . Scheme == "" {
2015-03-18 06:13:45 +03:00
if p . CookieSecure {
2015-03-17 23:25:19 +03:00
u . Scheme = "https"
} else {
u . Scheme = "http"
}
}
u . Host = host
return u . String ( )
}
func ( p * OauthProxy ) GetLoginURL ( host , redirect string ) string {
2012-12-11 05:59:23 +04:00
params := url . Values { }
2015-03-17 23:25:19 +03:00
params . Add ( "redirect_uri" , p . GetRedirectUrl ( host ) )
2012-12-11 05:59:23 +04:00
params . Add ( "approval_prompt" , "force" )
params . Add ( "scope" , p . oauthScope )
params . Add ( "client_id" , p . clientID )
params . Add ( "response_type" , "code" )
2015-03-17 23:25:19 +03:00
if strings . HasPrefix ( redirect , "/" ) {
params . Add ( "state" , redirect )
2013-10-22 23:56:29 +04:00
}
2012-12-11 05:59:23 +04:00
return fmt . Sprintf ( "%s?%s" , p . oauthLoginUrl , params . Encode ( ) )
}
2014-12-09 23:38:57 +03:00
func ( p * OauthProxy ) displayCustomLoginForm ( ) bool {
return p . HtpasswdFile != nil && p . DisplayHtpasswdForm
}
2015-03-17 23:25:19 +03:00
func ( p * OauthProxy ) redeemCode ( host , code string ) ( string , string , error ) {
2013-10-22 23:56:29 +04:00
if code == "" {
2014-08-08 00:16:39 +04:00
return "" , "" , errors . New ( "missing code" )
2013-10-22 23:56:29 +04:00
}
2012-12-11 05:59:23 +04:00
params := url . Values { }
2015-03-17 23:25:19 +03:00
params . Add ( "redirect_uri" , p . GetRedirectUrl ( host ) )
2012-12-11 05:59:23 +04:00
params . Add ( "client_id" , p . clientID )
params . Add ( "client_secret" , p . clientSecret )
params . Add ( "code" , code )
params . Add ( "grant_type" , "authorization_code" )
req , err := http . NewRequest ( "POST" , p . oauthRedemptionUrl . String ( ) , bytes . NewBufferString ( params . Encode ( ) ) )
if err != nil {
log . Printf ( "failed building request %s" , err . Error ( ) )
2014-08-08 00:16:39 +04:00
return "" , "" , err
2012-12-11 05:59:23 +04:00
}
req . Header . Set ( "Content-Type" , "application/x-www-form-urlencoded" )
2015-03-30 17:23:30 +03:00
json , err := api . Request ( req )
2012-12-11 05:59:23 +04:00
if err != nil {
2014-08-08 00:16:39 +04:00
log . Printf ( "failed making request %s" , err )
return "" , "" , err
2012-12-11 05:59:23 +04:00
}
access_token , err := json . Get ( "access_token" ) . String ( )
if err != nil {
2014-08-08 00:16:39 +04:00
return "" , "" , err
2012-12-11 05:59:23 +04:00
}
2012-12-17 22:15:23 +04:00
2015-03-30 22:48:30 +03:00
email , err := p . provider . GetEmailAddress ( json , access_token )
2014-08-08 00:16:39 +04:00
if err != nil {
return "" , "" , err
}
return access_token , email , nil
}
2012-12-26 19:35:02 +04:00
func ( p * OauthProxy ) ClearCookie ( rw http . ResponseWriter , req * http . Request ) {
2015-03-19 22:59:48 +03:00
domain := req . Host
if h , _ , err := net . SplitHostPort ( domain ) ; err == nil {
domain = h
}
if p . CookieDomain != "" {
if ! strings . HasSuffix ( domain , p . CookieDomain ) {
log . Printf ( "Warning: request host is %q but using configured cookie domain of %q" , domain , p . CookieDomain )
}
2014-11-09 22:51:10 +03:00
domain = p . CookieDomain
2012-12-11 05:59:23 +04:00
}
cookie := & http . Cookie {
2012-12-26 19:35:02 +04:00
Name : p . CookieKey ,
2012-12-11 05:59:23 +04:00
Value : "" ,
Path : "/" ,
Domain : domain ,
2015-01-19 18:52:18 +03:00
HttpOnly : p . CookieHttpOnly ,
2015-03-19 22:59:48 +03:00
Secure : p . CookieSecure ,
Expires : time . Now ( ) . Add ( time . Duration ( 1 ) * time . Hour * - 1 ) ,
2012-12-11 05:59:23 +04:00
}
http . SetCookie ( rw , cookie )
}
2012-12-26 19:35:02 +04:00
func ( p * OauthProxy ) SetCookie ( rw http . ResponseWriter , req * http . Request , val string ) {
2012-12-26 19:55:41 +04:00
2015-03-19 22:59:48 +03:00
domain := req . Host
if h , _ , err := net . SplitHostPort ( domain ) ; err == nil {
domain = h
}
if p . CookieDomain != "" {
if ! strings . HasSuffix ( domain , p . CookieDomain ) {
log . Printf ( "Warning: request host is %q but using configured cookie domain of %q" , domain , p . CookieDomain )
}
2014-11-09 22:51:10 +03:00
domain = p . CookieDomain
2012-12-26 19:35:02 +04:00
}
cookie := & http . Cookie {
Name : p . CookieKey ,
Value : signedCookieValue ( p . CookieSeed , p . CookieKey , val ) ,
Path : "/" ,
Domain : domain ,
2015-01-19 18:52:18 +03:00
HttpOnly : p . CookieHttpOnly ,
2015-03-18 06:13:45 +03:00
Secure : p . CookieSecure ,
2014-11-09 22:51:10 +03:00
Expires : time . Now ( ) . Add ( p . CookieExpire ) ,
2012-12-26 19:35:02 +04:00
}
http . SetCookie ( rw , cookie )
}
2015-05-08 17:09:47 +03:00
func ( p * OauthProxy ) ProcessCookie ( rw http . ResponseWriter , req * http . Request ) ( email , user , access_token string , ok bool ) {
cookie , err := req . Cookie ( p . CookieKey )
if err == nil {
var value string
value , ok = validateCookie ( cookie , p . CookieSeed )
if ok {
email , user , access_token , err = parseCookieValue (
value , p . AesCipher )
if err != nil {
log . Printf ( err . Error ( ) )
}
}
}
return
}
2014-10-15 00:22:38 +04:00
func ( p * OauthProxy ) PingPage ( rw http . ResponseWriter ) {
rw . WriteHeader ( http . StatusOK )
2014-10-15 01:05:59 +04:00
fmt . Fprintf ( rw , "OK" )
2014-10-15 00:22:38 +04:00
}
2012-12-17 22:15:23 +04:00
func ( p * OauthProxy ) ErrorPage ( rw http . ResponseWriter , code int , title string , message string ) {
log . Printf ( "ErrorPage %d %s %s" , code , title , message )
2012-12-11 05:59:23 +04:00
rw . WriteHeader ( code )
2012-12-17 22:15:23 +04:00
t := struct {
2012-12-17 22:38:33 +04:00
Title string
Message string
2012-12-11 05:59:23 +04:00
} {
2012-12-17 22:38:33 +04:00
Title : fmt . Sprintf ( "%d %s" , code , title ) ,
Message : message ,
2012-12-11 05:59:23 +04:00
}
2015-03-18 01:06:06 +03:00
p . templates . ExecuteTemplate ( rw , "error.html" , t )
2012-12-17 22:15:23 +04:00
}
func ( p * OauthProxy ) SignInPage ( rw http . ResponseWriter , req * http . Request , code int ) {
2012-12-26 19:35:02 +04:00
p . ClearCookie ( rw , req )
2012-12-17 22:15:23 +04:00
rw . WriteHeader ( code )
2012-12-26 19:35:02 +04:00
2015-04-07 05:10:03 +03:00
redirect_url := req . URL . RequestURI ( )
if redirect_url == signInPath {
redirect_url = "/"
}
2012-12-26 19:35:02 +04:00
t := struct {
2015-03-31 19:59:07 +03:00
ProviderName string
2012-12-26 19:55:41 +04:00
SignInMessage string
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
2012-12-26 19:55:41 +04:00
} {
2015-03-31 19:59:07 +03:00
ProviderName : p . provider . Data ( ) . ProviderName ,
2012-12-26 19:35:02 +04:00
SignInMessage : p . SignInMessage ,
2014-12-09 23:38:57 +03:00
CustomLogin : p . displayCustomLoginForm ( ) ,
2015-04-07 05:10:03 +03:00
Redirect : redirect_url ,
2014-11-10 06:01:50 +03:00
Version : VERSION ,
2012-12-26 19:55:41 +04:00
}
2015-03-18 01:06:06 +03:00
p . templates . ExecuteTemplate ( rw , "sign_in.html" , t )
2012-12-11 05:59:23 +04:00
}
2012-12-26 19:35:02 +04:00
func ( p * OauthProxy ) ManualSignIn ( rw http . ResponseWriter , req * http . Request ) ( string , bool ) {
if req . Method != "POST" || p . HtpasswdFile == 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
if p . HtpasswdFile . Validate ( user , passwd ) {
2015-03-19 23:37:16 +03:00
log . Printf ( "authenticated %q via HtpasswdFile" , user )
2012-12-26 19:55:41 +04:00
return user , true
}
return "" , false
}
2013-10-24 19:31:08 +04:00
func ( p * OauthProxy ) GetRedirect ( req * http . Request ) ( string , error ) {
err := req . ParseForm ( )
if err != nil {
return "" , err
}
redirect := req . FormValue ( "rd" )
if redirect == "" {
redirect = "/"
}
return redirect , err
}
2012-12-11 05:59:23 +04:00
func ( p * OauthProxy ) ServeHTTP ( rw http . ResponseWriter , req * http . Request ) {
// check if this is a redirect back at the end of oauth
2014-11-09 22:51:10 +03:00
remoteAddr := req . RemoteAddr
if req . Header . Get ( "X-Real-IP" ) != "" {
remoteAddr += fmt . Sprintf ( " (%q)" , req . Header . Get ( "X-Real-IP" ) )
2012-12-26 19:55:41 +04:00
}
2012-12-26 19:35:02 +04:00
2012-12-26 19:55:41 +04:00
var ok bool
var user string
2014-11-08 10:49:05 +03:00
var email string
2015-04-03 03:57:17 +03:00
var access_token string
2013-10-22 23:56:29 +04:00
2014-10-15 00:22:38 +04:00
if req . URL . Path == pingPath {
p . PingPage ( rw )
return
}
2015-01-12 12:18:41 +03:00
for _ , u := range p . compiledRegex {
match := u . MatchString ( req . URL . Path )
if match {
p . serveMux . ServeHTTP ( rw , req )
return
}
}
2012-12-11 05:59:23 +04:00
if req . URL . Path == signInPath {
2013-10-24 19:31:08 +04:00
redirect , err := p . GetRedirect ( req )
if err != nil {
p . ErrorPage ( rw , 500 , "Internal Error" , err . Error ( ) )
return
}
2012-12-26 19:55:41 +04:00
user , ok = p . ManualSignIn ( rw , req )
if ok {
p . SetCookie ( rw , req , user )
2013-10-22 23:56:29 +04:00
http . Redirect ( rw , req , redirect , 302 )
2012-12-26 19:55:41 +04:00
} else {
p . SignInPage ( rw , req , 200 )
}
2012-12-11 05:59:23 +04:00
return
}
if req . URL . Path == oauthStartPath {
2013-10-24 19:31:08 +04:00
redirect , err := p . GetRedirect ( req )
2013-10-22 23:56:29 +04:00
if err != nil {
p . ErrorPage ( rw , 500 , "Internal Error" , err . Error ( ) )
return
}
2015-03-17 23:25:19 +03:00
http . Redirect ( rw , req , p . GetLoginURL ( req . Host , redirect ) , 302 )
2012-12-11 05:59:23 +04:00
return
}
if req . URL . Path == oauthCallbackPath {
// finish the oauth cycle
2013-10-22 23:56:29 +04:00
err := req . ParseForm ( )
2012-12-11 05:59:23 +04:00
if err != nil {
2012-12-17 22:15:23 +04:00
p . ErrorPage ( rw , 500 , "Internal Error" , err . Error ( ) )
2012-12-11 05:59:23 +04:00
return
}
2013-10-22 23:56:29 +04:00
errorString := req . Form . Get ( "error" )
if errorString != "" {
p . ErrorPage ( rw , 403 , "Permission Denied" , errorString )
2012-12-11 05:59:23 +04:00
return
}
2015-04-03 03:57:17 +03:00
access_token , email , err = p . redeemCode ( req . Host , req . Form . Get ( "code" ) )
2012-12-11 05:59:23 +04:00
if err != nil {
2014-11-09 22:51:10 +03:00
log . Printf ( "%s error redeeming code %s" , remoteAddr , err )
2012-12-17 22:15:23 +04:00
p . ErrorPage ( rw , 500 , "Internal Error" , err . Error ( ) )
2012-12-11 05:59:23 +04:00
return
}
2013-10-22 23:56:29 +04:00
redirect := req . Form . Get ( "state" )
if redirect == "" {
redirect = "/"
}
2012-12-11 05:59:23 +04:00
// set cookie, or deny
if p . Validator ( email ) {
2014-11-09 22:51:10 +03:00
log . Printf ( "%s authenticating %s completed" , remoteAddr , email )
2015-04-05 17:51:17 +03:00
value , err := buildCookieValue (
email , p . AesCipher , access_token )
if err != nil {
log . Printf ( err . Error ( ) )
2015-04-03 03:57:17 +03:00
}
2015-04-05 17:51:17 +03:00
p . SetCookie ( rw , req , value )
2013-10-22 23:56:29 +04:00
http . Redirect ( rw , req , redirect , 302 )
2012-12-11 05:59:23 +04:00
return
} else {
2012-12-17 22:15:23 +04:00
p . ErrorPage ( rw , 403 , "Permission Denied" , "Invalid Account" )
2012-12-11 05:59:23 +04:00
return
}
}
2012-12-17 22:38:33 +04:00
2012-12-26 19:55:41 +04:00
if ! ok {
2015-05-08 17:09:47 +03:00
email , user , access_token , ok = p . ProcessCookie ( rw , req )
2012-12-11 05:59:23 +04:00
}
if ! ok {
user , ok = p . CheckBasicAuth ( req )
}
if ! ok {
2012-12-17 22:15:23 +04:00
p . SignInPage ( rw , req , 403 )
2012-12-11 05:59:23 +04:00
return
}
// At this point, the user is authenticated. proxy normally
2014-11-09 22:51:10 +03:00
if p . PassBasicAuth {
2012-12-11 05:59:23 +04:00
req . SetBasicAuth ( user , "" )
req . Header [ "X-Forwarded-User" ] = [ ] string { user }
2014-11-08 10:49:05 +03:00
req . Header [ "X-Forwarded-Email" ] = [ ] string { email }
2012-12-11 05:59:23 +04:00
}
2015-04-03 03:57:17 +03:00
if access_token != "" {
req . Header [ "X-Forwarded-Access-Token" ] = [ ] string { access_token }
}
2015-03-19 23:37:16 +03:00
if email == "" {
rw . Header ( ) . Set ( "GAP-Auth" , user )
} else {
rw . Header ( ) . Set ( "GAP-Auth" , email )
}
2012-12-11 05:59:23 +04:00
p . serveMux . ServeHTTP ( rw , req )
}
func ( p * OauthProxy ) CheckBasicAuth ( req * http . Request ) ( string , bool ) {
if p . HtpasswdFile == nil {
return "" , false
}
s := strings . SplitN ( req . Header . Get ( "Authorization" ) , " " , 2 )
if len ( s ) != 2 || s [ 0 ] != "Basic" {
return "" , false
}
b , err := base64 . StdEncoding . DecodeString ( s [ 1 ] )
if err != nil {
return "" , false
}
pair := strings . SplitN ( string ( b ) , ":" , 2 )
if len ( pair ) != 2 {
return "" , false
}
if p . HtpasswdFile . Validate ( pair [ 0 ] , pair [ 1 ] ) {
2015-03-19 23:37:16 +03:00
log . Printf ( "authenticated %q via basic auth" , pair [ 0 ] )
2012-12-11 05:59:23 +04:00
return pair [ 0 ] , true
}
return "" , false
}