2012-12-11 05:59:23 +04:00
package main
import (
"bytes"
"encoding/base64"
"errors"
"fmt"
"io/ioutil"
"log"
"net/http"
"net/http/httputil"
"net/url"
"strings"
"time"
2014-08-08 00:16:39 +04:00
"github.com/bitly/go-simplejson"
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 {
2014-11-09 22:51:10 +03:00
CookieSeed string
CookieKey string
CookieDomain string
CookieHttpsOnly bool
CookieExpire time . Duration
Validator func ( string ) bool
2012-12-11 05:59:23 +04:00
redirectUrl * url . URL // the url to receive requests at
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
serveMux * http . ServeMux
2014-11-09 22:51:10 +03:00
PassBasicAuth bool
2012-12-11 05:59:23 +04: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
login , _ := url . Parse ( "https://accounts.google.com/o/oauth2/auth" )
redeem , _ := url . Parse ( "https://accounts.google.com/o/oauth2/token" )
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 )
2012-12-11 05:59:23 +04:00
serveMux . Handle ( path , httputil . NewSingleHostReverseProxy ( 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>"
}
log . Printf ( "Cookie settings: https_only: %v expiry: %s domain:%s" , opts . CookieHttpsOnly , opts . CookieExpire , domain )
2014-11-09 22:51:10 +03:00
return & OauthProxy {
CookieKey : "_oauthproxy" ,
CookieSeed : opts . CookieSecret ,
CookieDomain : opts . CookieDomain ,
CookieHttpsOnly : opts . CookieHttpsOnly ,
CookieExpire : opts . CookieExpire ,
Validator : validator ,
clientID : opts . ClientID ,
clientSecret : opts . ClientSecret ,
2014-08-08 00:16:39 +04:00
oauthScope : "profile email" ,
2012-12-11 05:59:23 +04:00
oauthRedemptionUrl : redeem ,
oauthLoginUrl : login ,
serveMux : serveMux ,
2014-11-09 22:51:10 +03:00
redirectUrl : redirectUrl ,
PassBasicAuth : opts . PassBasicAuth ,
2012-12-11 05:59:23 +04:00
}
}
2013-10-22 23:56:29 +04:00
func ( p * OauthProxy ) GetLoginURL ( redirectUrl string ) string {
2012-12-11 05:59:23 +04:00
params := url . Values { }
params . Add ( "redirect_uri" , p . redirectUrl . String ( ) )
params . Add ( "approval_prompt" , "force" )
params . Add ( "scope" , p . oauthScope )
params . Add ( "client_id" , p . clientID )
params . Add ( "response_type" , "code" )
2013-10-22 23:56:29 +04:00
if strings . HasPrefix ( redirectUrl , "/" ) {
params . Add ( "state" , redirectUrl )
}
2012-12-11 05:59:23 +04:00
return fmt . Sprintf ( "%s?%s" , p . oauthLoginUrl , params . Encode ( ) )
}
func apiRequest ( req * http . Request ) ( * simplejson . Json , error ) {
httpclient := & http . Client { }
resp , err := httpclient . Do ( req )
if err != nil {
return nil , err
}
body , err := ioutil . ReadAll ( resp . Body )
resp . Body . Close ( )
if err != nil {
return nil , err
}
if resp . StatusCode != 200 {
log . Printf ( "got response code %d - %s" , resp . StatusCode , body )
2014-07-08 16:07:05 +04:00
return nil , errors . New ( "api request returned non 200 status code" )
2012-12-11 05:59:23 +04:00
}
data , err := simplejson . NewJson ( body )
if err != nil {
return nil , err
}
return data , nil
}
2014-08-08 00:16:39 +04:00
func ( p * OauthProxy ) redeemCode ( 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 { }
params . Add ( "redirect_uri" , p . redirectUrl . String ( ) )
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" )
json , err := apiRequest ( req )
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
2014-08-08 00:16:39 +04:00
idToken , err := json . Get ( "id_token" ) . String ( )
2012-12-11 05:59:23 +04:00
if err != nil {
2014-08-08 00:16:39 +04:00
return "" , "" , err
2012-12-11 05:59:23 +04:00
}
2014-08-08 00:16:39 +04:00
// id_token is a base64 encode ID token payload
// https://developers.google.com/accounts/docs/OAuth2Login#obtainuserinfo
jwt := strings . Split ( idToken , "." )
b , err := jwtDecodeSegment ( jwt [ 1 ] )
2012-12-11 05:59:23 +04:00
if err != nil {
2014-08-08 00:16:39 +04:00
return "" , "" , err
2012-12-11 05:59:23 +04:00
}
2014-08-08 00:16:39 +04:00
data , err := simplejson . NewJson ( b )
2012-12-11 05:59:23 +04:00
if err != nil {
2014-08-08 00:16:39 +04:00
return "" , "" , err
}
email , err := data . Get ( "email" ) . String ( )
if err != nil {
return "" , "" , err
}
return access_token , email , nil
}
func jwtDecodeSegment ( seg string ) ( [ ] byte , error ) {
if l := len ( seg ) % 4 ; l > 0 {
seg += strings . Repeat ( "=" , 4 - l )
2012-12-11 05:59:23 +04:00
}
2014-08-08 00:16:39 +04:00
return base64 . URLEncoding . DecodeString ( seg )
2012-12-11 05:59:23 +04:00
}
2012-12-26 19:35:02 +04:00
func ( p * OauthProxy ) ClearCookie ( rw http . ResponseWriter , req * http . Request ) {
2012-12-11 05:59:23 +04:00
domain := strings . Split ( req . Host , ":" ) [ 0 ]
2014-11-09 22:51:10 +03:00
if p . CookieDomain != "" && strings . HasSuffix ( domain , p . CookieDomain ) {
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 ,
Expires : time . Now ( ) . Add ( time . Duration ( 1 ) * time . Hour * - 1 ) ,
HttpOnly : true ,
}
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
2012-12-26 19:35:02 +04:00
domain := strings . Split ( req . Host , ":" ) [ 0 ] // strip the port (if any)
2014-11-09 22:51:10 +03:00
if p . CookieDomain != "" && strings . HasSuffix ( domain , p . CookieDomain ) {
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 ,
2014-11-08 21:26:55 +03:00
HttpOnly : true ,
2014-11-09 22:51:10 +03:00
Secure : p . CookieHttpsOnly ,
Expires : time . Now ( ) . Add ( p . CookieExpire ) ,
2012-12-26 19:35:02 +04:00
}
http . SetCookie ( rw , cookie )
}
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
templates := getTemplates ( )
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
}
2012-12-17 22:15:23 +04:00
templates . ExecuteTemplate ( rw , "error.html" , t )
}
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 )
templates := getTemplates ( )
2012-12-26 19:35:02 +04:00
t := struct {
2012-12-26 19:55:41 +04:00
SignInMessage string
2012-12-26 19:35:02 +04:00
Htpasswd 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
} {
2012-12-26 19:35:02 +04:00
SignInMessage : p . SignInMessage ,
Htpasswd : p . HtpasswdFile != nil ,
2013-10-22 23:56:29 +04:00
Redirect : req . URL . RequestURI ( ) ,
2014-11-10 06:01:50 +03:00
Version : VERSION ,
2012-12-26 19:55:41 +04:00
}
2012-12-17 22:15:23 +04:00
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 ) {
log . Printf ( "authenticated %s via manual sign in" , user )
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
}
2014-11-09 22:51:10 +03:00
log . Printf ( "%s %s %s" , remoteAddr , req . Method , req . URL . RequestURI ( ) )
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
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
}
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
}
http . Redirect ( rw , req , p . GetLoginURL ( 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
}
2014-08-08 00:16:39 +04:00
_ , email , err := p . redeemCode ( 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 )
2012-12-26 19:35:02 +04:00
p . SetCookie ( rw , req , email )
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 {
cookie , err := req . Cookie ( p . CookieKey )
if err == nil {
email , ok = validateCookie ( cookie , p . CookieSeed )
user = strings . Split ( email , "@" ) [ 0 ]
}
2012-12-11 05:59:23 +04:00
}
if ! ok {
user , ok = p . CheckBasicAuth ( req )
2012-12-26 19:55:41 +04:00
// if we want to promote basic auth requests to cookie'd requests, we could do that here
// not sure that would be ideal in all circumstances though
// if ok {
// p.SetCookie(rw, req, user)
// }
2012-12-11 05:59:23 +04:00
}
if ! ok {
2014-11-09 22:51:10 +03:00
log . Printf ( "%s - invalid cookie session" , remoteAddr )
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
}
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 ] ) {
2012-12-26 19:55:41 +04:00
log . Printf ( "authenticated %s via basic auth" , pair [ 0 ] )
2012-12-11 05:59:23 +04:00
return pair [ 0 ] , true
}
return "" , false
}