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_)
|
* 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
|
||||||
|
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-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")
|
||||||
|
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
@ -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())
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user