Merge pull request #116 from jehiah/google_refresh_token_116
Google - use offline access token
This commit is contained in:
		
						commit
						b9ae5dc8d7
					
				|  | @ -50,6 +50,8 @@ For Google, the registration steps are: | |||
|    * Fill in the necessary fields and Save (this is _required_) | ||||
| 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 | ||||
| 
 | ||||
| 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-httponly=true: set HttpOnly cookie flag | ||||
|   -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-secure=true: set secure (HTTPS) cookie flag | ||||
|   -custom-templates-dir="": path to custom html templates | ||||
|  |  | |||
							
								
								
									
										2
									
								
								main.go
								
								
								
								
							
							
						
						
									
										2
									
								
								main.go
								
								
								
								
							|  | @ -50,7 +50,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), "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-httponly", true, "set HttpOnly cookie flag") | ||||
| 
 | ||||
|  |  | |||
|  | @ -246,7 +246,13 @@ func (p *OauthProxy) ProcessCookie(rw http.ResponseWriter, req *http.Request) (e | |||
| 	} else if ok && p.CookieRefresh != time.Duration(0) { | ||||
| 		refresh := timestamp.Add(p.CookieRefresh) | ||||
| 		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 { | ||||
| 				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) | ||||
| 			return | ||||
| 		} else { | ||||
| 			log.Printf("validating: %s is unauthorized") | ||||
| 			p.ErrorPage(rw, 403, "Permission Denied", "Invalid Account") | ||||
| 			return | ||||
| 		} | ||||
|  |  | |||
|  | @ -96,9 +96,9 @@ func TestDefaultProviderApiSettings(t *testing.T) { | |||
| 	o := testOptions() | ||||
| 	assert.Equal(t, nil, o.Validate()) | ||||
| 	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()) | ||||
| 	assert.Equal(t, "https://accounts.google.com/o/oauth2/token", | ||||
| 	assert.Equal(t, "https://www.googleapis.com/oauth2/v3/token", | ||||
| 		p.RedeemUrl.String()) | ||||
| 	assert.Equal(t, "", p.ProfileUrl.String()) | ||||
| 	assert.Equal(t, "profile email", p.Scope) | ||||
|  |  | |||
|  | @ -1,15 +1,20 @@ | |||
| package providers | ||||
| 
 | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"encoding/base64" | ||||
| 	"encoding/json" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"io/ioutil" | ||||
| 	"net/http" | ||||
| 	"net/url" | ||||
| 	"strings" | ||||
| ) | ||||
| 
 | ||||
| type GoogleProvider struct { | ||||
| 	*ProviderData | ||||
| 	RedeemRefreshUrl *url.URL | ||||
| } | ||||
| 
 | ||||
| func NewGoogleProvider(p *ProviderData) *GoogleProvider { | ||||
|  | @ -17,12 +22,15 @@ func NewGoogleProvider(p *ProviderData) *GoogleProvider { | |||
| 	if p.LoginUrl.String() == "" { | ||||
| 		p.LoginUrl = &url.URL{Scheme: "https", | ||||
| 			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() == "" { | ||||
| 		p.RedeemUrl = &url.URL{Scheme: "https", | ||||
| 			Host: "accounts.google.com", | ||||
| 			Path: "/o/oauth2/token"} | ||||
| 			Host: "www.googleapis.com", | ||||
| 			Path: "/oauth2/v3/token"} | ||||
| 	} | ||||
| 	if p.ValidateUrl.String() == "" { | ||||
| 		p.ValidateUrl = &url.URL{Scheme: "https", | ||||
|  | @ -76,3 +84,90 @@ func jwtDecodeSegment(seg string) ([]byte, error) { | |||
| func (p *GoogleProvider) ValidateToken(access_token string) bool { | ||||
| 	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 | ||||
| } | ||||
|  |  | |||
|  | @ -23,9 +23,9 @@ func TestGoogleProviderDefaults(t *testing.T) { | |||
| 	p := newGoogleProvider() | ||||
| 	assert.NotEqual(t, nil, p) | ||||
| 	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()) | ||||
| 	assert.Equal(t, "https://accounts.google.com/o/oauth2/token", | ||||
| 	assert.Equal(t, "https://www.googleapis.com/oauth2/v3/token", | ||||
| 		p.Data().RedeemUrl.String()) | ||||
| 	assert.Equal(t, "https://www.googleapis.com/oauth2/v1/tokeninfo", | ||||
| 		p.Data().ValidateUrl.String()) | ||||
|  |  | |||
|  | @ -56,15 +56,19 @@ func (p *ProviderData) Redeem(redirectUrl, code string) (body []byte, token stri | |||
| 	return body, v.Get("access_token"), err | ||||
| } | ||||
| 
 | ||||
| // GetLoginURL with typical oauth parameters
 | ||||
| func (p *ProviderData) GetLoginURL(redirectURI, finalRedirect string) string { | ||||
| 	params := url.Values{} | ||||
| 	params.Add("redirect_uri", redirectURI) | ||||
| 	params.Add("approval_prompt", "force") | ||||
| 	var a url.URL | ||||
| 	a = *p.LoginUrl | ||||
| 	params, _ := url.ParseQuery(a.RawQuery) | ||||
| 	params.Set("redirect_uri", redirectURI) | ||||
| 	params.Set("approval_prompt", "force") | ||||
| 	params.Add("scope", p.Scope) | ||||
| 	params.Add("client_id", p.ClientID) | ||||
| 	params.Add("response_type", "code") | ||||
| 	params.Set("client_id", p.ClientID) | ||||
| 	params.Set("response_type", "code") | ||||
| 	if strings.HasPrefix(finalRedirect, "/") { | ||||
| 		params.Add("state", finalRedirect) | ||||
| 	} | ||||
| 	return fmt.Sprintf("%s?%s", p.LoginUrl, params.Encode()) | ||||
| 	a.RawQuery = params.Encode() | ||||
| 	return a.String() | ||||
| } | ||||
|  |  | |||
|  | @ -83,7 +83,6 @@ func newValidatorImpl(domains []string, usersFile string, | |||
| 		if allowAll { | ||||
| 			valid = true | ||||
| 		} | ||||
| 		log.Printf("validating: is %s valid? %v", email, valid) | ||||
| 		return valid | ||||
| 	} | ||||
| 	return validator | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue