Skip to content
This repository has been archived by the owner on Jan 24, 2019. It is now read-only.

Commit

Permalink
Merge pull request #88 from 18F/auto-refresh
Browse files Browse the repository at this point in the history
Auto refresh auth token
  • Loading branch information
jehiah committed May 12, 2015
2 parents 5c03fe3 + 2808ba7 commit 9047920
Show file tree
Hide file tree
Showing 13 changed files with 406 additions and 50 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ Usage of google_auth_proxy:
-cookie-expire=168h0m0s: expire timeframe for cookie
-cookie-httponly=true: set HttpOnly cookie flag
-cookie-https-only=true: set secure (HTTPS) cookies (deprecated. use --cookie-secure setting)
-cookie-refresh=0: refresh the cookie when less than this much time remains before expiration; 0 to disable
-cookie-secret="": the seed string for secure cookies
-cookie-secure=true: set secure (HTTPS) cookie flag
-custom-templates-dir="": path to custom html templates
Expand All @@ -96,6 +97,7 @@ Usage of google_auth_proxy:
-scope="": Oauth scope specification
-skip-auth-regex=: bypass authentication for requests path's that match (may be given multiple times)
-upstream=: the http url(s) of the upstream endpoint. If multiple, routing is based on path
-validate-url="": Access token validation endpoint
-version=false: print version string
```

Expand Down
9 changes: 7 additions & 2 deletions contrib/google_auth_proxy.cfg.example
Original file line number Diff line number Diff line change
Expand Up @@ -46,12 +46,17 @@


## Cookie Settings
## Secret - the seed string for secure cookies
## Secret - the seed string for secure cookies; should be 16, 24, or 32 bytes
## for use with an AES cipher when cookie_refresh or pass_access_code
## is set
## Domain - optional cookie domain to force cookies to (ie: .yourcompany.com)
## Expire - expire timeframe for cookie
## Refresh - refresh the cookie when less than this much time remains before
## expiration; should be less than cookie_expire; set to 0 to disable
# cookie_secret = ""
# cookie_domain = ""
# cookie_expire = "168h"
# cookie_refresh = "144h"
# cookie_secure = true
# cookie_httponly = true

# pass_access_code = true
8 changes: 4 additions & 4 deletions cookies.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,11 @@ import (
"time"
)

func validateCookie(cookie *http.Cookie, seed string) (string, bool) {
func validateCookie(cookie *http.Cookie, seed string) (string, time.Time, bool) {
// value, timestamp, sig
parts := strings.Split(cookie.Value, "|")
if len(parts) != 3 {
return "", false
return "", time.Unix(0, 0), false
}
sig := cookieSignature(seed, cookie.Name, parts[0], parts[1])
if checkHmac(parts[2], sig) {
Expand All @@ -28,11 +28,11 @@ func validateCookie(cookie *http.Cookie, seed string) (string, bool) {
// it's a valid cookie. now get the contents
rawValue, err := base64.URLEncoding.DecodeString(parts[0])
if err == nil {
return string(rawValue), true
return string(rawValue), time.Unix(int64(ts), 0), true
}
}
}
return "", false
return "", time.Unix(0, 0), false
}

func signedCookieValue(seed string, key string, value string) string {
Expand Down
2 changes: 2 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ func main() {
flagSet.String("cookie-secret", "", "the seed string for secure cookies")
flagSet.String("cookie-domain", "", "an optional cookie domain to force cookies to (ie: .yourcompany.com)*")
flagSet.Duration("cookie-expire", time.Duration(168)*time.Hour, "expire timeframe for cookie")
flagSet.Duration("cookie-refresh", time.Duration(0)*time.Hour, "refresh the cookie when less than this much time remains before expiration; 0 to disable")
flagSet.Bool("cookie-https-only", true, "set secure (HTTPS) cookies (deprecated. use --cookie-secure setting)")
flagSet.Bool("cookie-secure", true, "set secure (HTTPS) cookie flag")
flagSet.Bool("cookie-httponly", true, "set HttpOnly cookie flag")
Expand All @@ -55,6 +56,7 @@ func main() {
flagSet.String("login-url", "", "Authentication endpoint")
flagSet.String("redeem-url", "", "Token redemption endpoint")
flagSet.String("profile-url", "", "Profile access endpoint")
flagSet.String("validate-url", "", "Access token validation endpoint")
flagSet.String("scope", "", "Oauth scope specification")

flagSet.Parse(os.Args[1:])
Expand Down
103 changes: 67 additions & 36 deletions oauthproxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,14 @@ type OauthProxy struct {
CookieSecure bool
CookieHttpOnly bool
CookieExpire time.Duration
CookieRefresh time.Duration
Validator func(string) bool

redirectUrl *url.URL // the url to receive requests at
provider providers.Provider
oauthRedemptionUrl *url.URL // endpoint to redeem the code
oauthLoginUrl *url.URL // to redirect the user to
oauthValidateUrl *url.URL // to validate the access token
oauthScope string
clientID string
clientSecret string
Expand All @@ -48,6 +50,7 @@ type OauthProxy struct {
DisplayHtpasswdForm bool
serveMux http.Handler
PassBasicAuth bool
PassAccessToken bool
AesCipher cipher.Block
skipAuthRegex []string
compiledRegex []*regexp.Regexp
Expand Down Expand Up @@ -121,12 +124,12 @@ func NewOauthProxy(opts *Options, validator func(string) bool) *OauthProxy {
log.Printf("Cookie settings: secure (https):%v httponly:%v expiry:%s domain:%s", opts.CookieSecure, opts.CookieHttpOnly, opts.CookieExpire, domain)

var aes_cipher cipher.Block
if opts.PassAccessToken {
if opts.PassAccessToken || (opts.CookieRefresh != time.Duration(0)) {
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)
"cookie-secret ", opts.CookieSecret, ": ", err)
}
}

Expand All @@ -137,6 +140,7 @@ func NewOauthProxy(opts *Options, validator func(string) bool) *OauthProxy {
CookieSecure: opts.CookieSecure,
CookieHttpOnly: opts.CookieHttpOnly,
CookieExpire: opts.CookieExpire,
CookieRefresh: opts.CookieRefresh,
Validator: validator,

clientID: opts.ClientID,
Expand All @@ -145,11 +149,13 @@ func NewOauthProxy(opts *Options, validator func(string) bool) *OauthProxy {
provider: opts.provider,
oauthRedemptionUrl: opts.provider.Data().RedeemUrl,
oauthLoginUrl: opts.provider.Data().LoginUrl,
oauthValidateUrl: opts.provider.Data().ValidateUrl,
serveMux: serveMux,
redirectUrl: redirectUrl,
skipAuthRegex: opts.SkipAuthRegex,
compiledRegex: opts.CompiledRegex,
PassBasicAuth: opts.PassBasicAuth,
PassAccessToken: opts.PassAccessToken,
AesCipher: aes_cipher,
templates: loadTemplates(opts.CustomTemplatesDir),
}
Expand Down Expand Up @@ -224,7 +230,7 @@ func (p *OauthProxy) redeemCode(host, code string) (string, string, error) {
return access_token, email, nil
}

func (p *OauthProxy) ClearCookie(rw http.ResponseWriter, req *http.Request) {
func (p *OauthProxy) MakeCookie(req *http.Request, value string, expiration time.Duration) *http.Cookie {
domain := req.Host
if h, _, err := net.SplitHostPort(domain); err == nil {
domain = h
Expand All @@ -235,40 +241,76 @@ func (p *OauthProxy) ClearCookie(rw http.ResponseWriter, req *http.Request) {
}
domain = p.CookieDomain
}
cookie := &http.Cookie{

if value != "" {
value = signedCookieValue(p.CookieSeed, p.CookieKey, value)
}

return &http.Cookie{
Name: p.CookieKey,
Value: "",
Value: value,
Path: "/",
Domain: domain,
HttpOnly: p.CookieHttpOnly,
Secure: p.CookieSecure,
Expires: time.Now().Add(time.Duration(1) * time.Hour * -1),
Expires: time.Now().Add(expiration),
}
http.SetCookie(rw, cookie)
}

func (p *OauthProxy) ClearCookie(rw http.ResponseWriter, req *http.Request) {
http.SetCookie(rw, p.MakeCookie(req, "", time.Duration(1)*time.Hour*-1))
}

func (p *OauthProxy) SetCookie(rw http.ResponseWriter, req *http.Request, val string) {
http.SetCookie(rw, p.MakeCookie(req, val, p.CookieExpire))
}

domain := req.Host
if h, _, err := net.SplitHostPort(domain); err == nil {
domain = h
func (p *OauthProxy) ValidateToken(access_token string) bool {
if access_token == "" || p.oauthValidateUrl == nil {
return false
}
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)

req, err := http.NewRequest("GET",
p.oauthValidateUrl.String()+"?access_token="+access_token, nil)
if err != nil {
log.Printf("failed building token validation request: %s", err)
return false
}

httpclient := &http.Client{}
resp, err := httpclient.Do(req)
if err != nil {
log.Printf("token validation request failed: %s", err)
return false
}
return resp.StatusCode == 200
}

func (p *OauthProxy) ProcessCookie(rw http.ResponseWriter, req *http.Request) (email, user, access_token string, ok bool) {
var value string
var timestamp time.Time
cookie, err := req.Cookie(p.CookieKey)
if err == nil {
value, timestamp, ok = validateCookie(cookie, p.CookieSeed)
if ok {
email, user, access_token, err = parseCookieValue(
value, p.AesCipher)
}
domain = p.CookieDomain
}
cookie := &http.Cookie{
Name: p.CookieKey,
Value: signedCookieValue(p.CookieSeed, p.CookieKey, val),
Path: "/",
Domain: domain,
HttpOnly: p.CookieHttpOnly,
Secure: p.CookieSecure,
Expires: time.Now().Add(p.CookieExpire),
if err != nil {
log.Printf(err.Error())
ok = false
} else if p.CookieRefresh != time.Duration(0) {
expires := timestamp.Add(p.CookieExpire)
refresh_threshold := time.Now().Add(p.CookieRefresh)
if refresh_threshold.Unix() > expires.Unix() {
ok = p.Validator(email) && p.ValidateToken(access_token)
if ok {
p.SetCookie(rw, req, value)
}
}
}
http.SetCookie(rw, cookie)
return
}

func (p *OauthProxy) RobotsTxt(rw http.ResponseWriter) {
Expand Down Expand Up @@ -451,18 +493,7 @@ func (p *OauthProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
}

if !ok {
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())
}
}
}
email, user, access_token, ok = p.ProcessCookie(rw, req)
}

if !ok {
Expand All @@ -480,7 +511,7 @@ func (p *OauthProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
req.Header["X-Forwarded-User"] = []string{user}
req.Header["X-Forwarded-Email"] = []string{email}
}
if access_token != "" {
if p.PassAccessToken {
req.Header["X-Forwarded-Access-Token"] = []string{access_token}
}
if email == "" {
Expand Down
Loading

0 comments on commit 9047920

Please sign in to comment.