Merge pull request #116 from jehiah/google_refresh_token_116

Google - use offline access token
This commit is contained in:
Jehiah Czebotar 2015-06-23 14:01:35 -04:00
commit b9ae5dc8d7
8 changed files with 124 additions and 17 deletions

View File

@ -50,6 +50,8 @@ For Google, the registration steps are:
* Fill in the necessary fields and Save (this is _required_) * Fill in the necessary fields and Save (this is _required_)
5. Take note of the **Client ID** and **Client Secret** 5. Take note of the **Client ID** and **Client Secret**
It's recommended to refresh sessions on a short interval (1h) with `cookie-refresh` setting which validates that the account is still authorized.
### GitHub Auth Provider ### GitHub Auth Provider
1. Create a new project: https://github.com/settings/developers 1. Create a new project: https://github.com/settings/developers
@ -100,7 +102,7 @@ Usage of oauth2_proxy:
-cookie-expire=168h0m0s: expire timeframe for cookie -cookie-expire=168h0m0s: expire timeframe for cookie
-cookie-httponly=true: set HttpOnly cookie flag -cookie-httponly=true: set HttpOnly cookie flag
-cookie-key="_oauth2_proxy": the name of the cookie that the oauth_proxy creates -cookie-key="_oauth2_proxy": the name of the cookie that the oauth_proxy creates
-cookie-refresh=0: refresh the cookie when less than this much time remains before expiration; 0 to disable -cookie-refresh=0: refresh the cookie after this duration; 0 to disable
-cookie-secret="": the seed string for secure cookies -cookie-secret="": the seed string for secure cookies
-cookie-secure=true: set secure (HTTPS) cookie flag -cookie-secure=true: set secure (HTTPS) cookie flag
-custom-templates-dir="": path to custom html templates -custom-templates-dir="": path to custom html templates

View File

@ -50,7 +50,7 @@ func main() {
flagSet.String("cookie-secret", "", "the seed string for secure cookies") 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.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-expire", time.Duration(168)*time.Hour, "expire timeframe for cookie")
flagSet.Duration("cookie-refresh", time.Duration(0), "refresh the cookie when less than this much time remains before expiration; 0 to disable") flagSet.Duration("cookie-refresh", time.Duration(0), "refresh the cookie after this duration; 0 to disable")
flagSet.Bool("cookie-secure", true, "set secure (HTTPS) cookie flag") flagSet.Bool("cookie-secure", true, "set secure (HTTPS) cookie flag")
flagSet.Bool("cookie-httponly", true, "set HttpOnly cookie flag") flagSet.Bool("cookie-httponly", true, "set HttpOnly cookie flag")

View File

@ -246,7 +246,13 @@ func (p *OauthProxy) ProcessCookie(rw http.ResponseWriter, req *http.Request) (e
} else if ok && p.CookieRefresh != time.Duration(0) { } else if ok && p.CookieRefresh != time.Duration(0) {
refresh := timestamp.Add(p.CookieRefresh) refresh := timestamp.Add(p.CookieRefresh)
if refresh.Before(time.Now()) { if refresh.Before(time.Now()) {
ok = p.Validator(email) && p.provider.ValidateToken(access_token) log.Printf("refreshing %s old session for %s (refresh after %s)", time.Now().Sub(timestamp), email, p.CookieRefresh)
ok = p.Validator(email)
log.Printf("re-validating %s valid:%v", email, ok)
if ok {
ok = p.provider.ValidateToken(access_token)
log.Printf("re-validating access token. valid:%v", ok)
}
if ok { if ok {
p.SetCookie(rw, req, value) p.SetCookie(rw, req, value)
} }
@ -432,6 +438,7 @@ func (p *OauthProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
http.Redirect(rw, req, redirect, 302) http.Redirect(rw, req, redirect, 302)
return return
} else { } else {
log.Printf("validating: %s is unauthorized")
p.ErrorPage(rw, 403, "Permission Denied", "Invalid Account") p.ErrorPage(rw, 403, "Permission Denied", "Invalid Account")
return return
} }

View File

@ -96,9 +96,9 @@ func TestDefaultProviderApiSettings(t *testing.T) {
o := testOptions() o := testOptions()
assert.Equal(t, nil, o.Validate()) assert.Equal(t, nil, o.Validate())
p := o.provider.Data() p := o.provider.Data()
assert.Equal(t, "https://accounts.google.com/o/oauth2/auth", assert.Equal(t, "https://accounts.google.com/o/oauth2/auth?access_type=offline",
p.LoginUrl.String()) p.LoginUrl.String())
assert.Equal(t, "https://accounts.google.com/o/oauth2/token", assert.Equal(t, "https://www.googleapis.com/oauth2/v3/token",
p.RedeemUrl.String()) p.RedeemUrl.String())
assert.Equal(t, "", p.ProfileUrl.String()) assert.Equal(t, "", p.ProfileUrl.String())
assert.Equal(t, "profile email", p.Scope) assert.Equal(t, "profile email", p.Scope)

View File

@ -1,15 +1,20 @@
package providers package providers
import ( import (
"bytes"
"encoding/base64" "encoding/base64"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt"
"io/ioutil"
"net/http"
"net/url" "net/url"
"strings" "strings"
) )
type GoogleProvider struct { type GoogleProvider struct {
*ProviderData *ProviderData
RedeemRefreshUrl *url.URL
} }
func NewGoogleProvider(p *ProviderData) *GoogleProvider { func NewGoogleProvider(p *ProviderData) *GoogleProvider {
@ -17,12 +22,15 @@ func NewGoogleProvider(p *ProviderData) *GoogleProvider {
if p.LoginUrl.String() == "" { if p.LoginUrl.String() == "" {
p.LoginUrl = &url.URL{Scheme: "https", p.LoginUrl = &url.URL{Scheme: "https",
Host: "accounts.google.com", Host: "accounts.google.com",
Path: "/o/oauth2/auth"} Path: "/o/oauth2/auth",
// to get a refresh token. see https://developers.google.com/identity/protocols/OAuth2WebServer#offline
RawQuery: "access_type=offline",
}
} }
if p.RedeemUrl.String() == "" { if p.RedeemUrl.String() == "" {
p.RedeemUrl = &url.URL{Scheme: "https", p.RedeemUrl = &url.URL{Scheme: "https",
Host: "accounts.google.com", Host: "www.googleapis.com",
Path: "/o/oauth2/token"} Path: "/oauth2/v3/token"}
} }
if p.ValidateUrl.String() == "" { if p.ValidateUrl.String() == "" {
p.ValidateUrl = &url.URL{Scheme: "https", p.ValidateUrl = &url.URL{Scheme: "https",
@ -76,3 +84,90 @@ func jwtDecodeSegment(seg string) ([]byte, error) {
func (p *GoogleProvider) ValidateToken(access_token string) bool { func (p *GoogleProvider) ValidateToken(access_token string) bool {
return validateToken(p, access_token, nil) return validateToken(p, access_token, nil)
} }
func (p *GoogleProvider) Redeem(redirectUrl, code string) (body []byte, token string, err error) {
if code == "" {
err = errors.New("missing code")
return
}
params := url.Values{}
params.Add("redirect_uri", redirectUrl)
params.Add("client_id", p.ClientID)
params.Add("client_secret", p.ClientSecret)
params.Add("code", code)
params.Add("grant_type", "authorization_code")
var req *http.Request
req, err = http.NewRequest("POST", p.RedeemUrl.String(), bytes.NewBufferString(params.Encode()))
if err != nil {
return
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
resp, err := http.DefaultClient.Do(req)
if err != nil {
return
}
body, err = ioutil.ReadAll(resp.Body)
resp.Body.Close()
if err != nil {
return
}
if resp.StatusCode != 200 {
err = fmt.Errorf("got %d from %q %s", resp.StatusCode, p.RedeemUrl.String(), body)
return
}
var jsonResponse struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
}
err = json.Unmarshal(body, &jsonResponse)
if err != nil {
return
}
token, err = p.redeemRefreshToken(jsonResponse.RefreshToken)
return
}
func (p *GoogleProvider) redeemRefreshToken(refreshToken string) (token string, err error) {
// https://developers.google.com/identity/protocols/OAuth2WebServer#refresh
params := url.Values{}
params.Add("client_id", p.ClientID)
params.Add("client_secret", p.ClientSecret)
params.Add("refresh_token", refreshToken)
params.Add("grant_type", "refresh_token")
var req *http.Request
req, err = http.NewRequest("POST", p.RedeemUrl.String(), bytes.NewBufferString(params.Encode()))
if err != nil {
return
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
resp, err := http.DefaultClient.Do(req)
if err != nil {
return
}
var body []byte
body, err = ioutil.ReadAll(resp.Body)
resp.Body.Close()
if err != nil {
return
}
if resp.StatusCode != 200 {
err = fmt.Errorf("got %d from %q %s", resp.StatusCode, p.RedeemUrl.String(), body)
return
}
var jsonResponse struct {
AccessToken string `json:"access_token"`
}
err = json.Unmarshal(body, &jsonResponse)
if err != nil {
return
}
return jsonResponse.AccessToken, nil
}

View File

@ -23,9 +23,9 @@ func TestGoogleProviderDefaults(t *testing.T) {
p := newGoogleProvider() p := newGoogleProvider()
assert.NotEqual(t, nil, p) assert.NotEqual(t, nil, p)
assert.Equal(t, "Google", p.Data().ProviderName) assert.Equal(t, "Google", p.Data().ProviderName)
assert.Equal(t, "https://accounts.google.com/o/oauth2/auth", assert.Equal(t, "https://accounts.google.com/o/oauth2/auth?access_type=offline",
p.Data().LoginUrl.String()) p.Data().LoginUrl.String())
assert.Equal(t, "https://accounts.google.com/o/oauth2/token", assert.Equal(t, "https://www.googleapis.com/oauth2/v3/token",
p.Data().RedeemUrl.String()) p.Data().RedeemUrl.String())
assert.Equal(t, "https://www.googleapis.com/oauth2/v1/tokeninfo", assert.Equal(t, "https://www.googleapis.com/oauth2/v1/tokeninfo",
p.Data().ValidateUrl.String()) p.Data().ValidateUrl.String())

View File

@ -56,15 +56,19 @@ func (p *ProviderData) Redeem(redirectUrl, code string) (body []byte, token stri
return body, v.Get("access_token"), err return body, v.Get("access_token"), err
} }
// GetLoginURL with typical oauth parameters
func (p *ProviderData) GetLoginURL(redirectURI, finalRedirect string) string { func (p *ProviderData) GetLoginURL(redirectURI, finalRedirect string) string {
params := url.Values{} var a url.URL
params.Add("redirect_uri", redirectURI) a = *p.LoginUrl
params.Add("approval_prompt", "force") params, _ := url.ParseQuery(a.RawQuery)
params.Set("redirect_uri", redirectURI)
params.Set("approval_prompt", "force")
params.Add("scope", p.Scope) params.Add("scope", p.Scope)
params.Add("client_id", p.ClientID) params.Set("client_id", p.ClientID)
params.Add("response_type", "code") params.Set("response_type", "code")
if strings.HasPrefix(finalRedirect, "/") { if strings.HasPrefix(finalRedirect, "/") {
params.Add("state", finalRedirect) params.Add("state", finalRedirect)
} }
return fmt.Sprintf("%s?%s", p.LoginUrl, params.Encode()) a.RawQuery = params.Encode()
return a.String()
} }

View File

@ -83,7 +83,6 @@ func newValidatorImpl(domains []string, usersFile string,
if allowAll { if allowAll {
valid = true valid = true
} }
log.Printf("validating: is %s valid? %v", email, valid)
return valid return valid
} }
return validator return validator