mirror of
https://github.com/pomerium/pomerium.git
synced 2025-06-03 11:22:45 +02:00
authenticate: use gRPC for service endpoints (#39)
* authenticate: set cookie secure as default. * authenticate: remove single flight provider. * authenticate/providers: Rename “ProviderData” to “IdentityProvider” * authenticate/providers: Fixed an issue where scopes were not being overwritten * proxy/authenticate : http client code removed. * proxy: standardized session variable names between services. * docs: change basic docker-config to be an “all-in-one” example with no nginx load. * docs: nginx balanced docker compose example with intra-ingress settings. * license: attribution for adaptation of goji’s middleware pattern.
This commit is contained in:
parent
9ca3ff4fa2
commit
c886b924e7
54 changed files with 2184 additions and 1463 deletions
|
@ -1,308 +0,0 @@
|
|||
package authenticator // import "github.com/pomerium/pomerium/proxy/authenticator"
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/pomerium/pomerium/internal/log"
|
||||
"github.com/pomerium/pomerium/internal/sessions"
|
||||
"github.com/pomerium/pomerium/internal/version"
|
||||
)
|
||||
|
||||
var defaultHTTPClient = &http.Client{
|
||||
Timeout: time.Second * 5,
|
||||
Transport: &http.Transport{
|
||||
Dial: (&net.Dialer{
|
||||
Timeout: 2 * time.Second,
|
||||
}).Dial,
|
||||
TLSHandshakeTimeout: 2 * time.Second,
|
||||
},
|
||||
}
|
||||
|
||||
// Errors
|
||||
var (
|
||||
ErrMissingRefreshToken = errors.New("missing refresh token")
|
||||
ErrAuthProviderUnavailable = errors.New("auth provider unavailable")
|
||||
)
|
||||
|
||||
// AuthenticateClient holds the data associated with the AuthenticateClients
|
||||
// necessary to implement a AuthenticateClient interface.
|
||||
type AuthenticateClient struct {
|
||||
AuthenticateServiceURL *url.URL
|
||||
|
||||
SharedKey string
|
||||
|
||||
SignInURL *url.URL
|
||||
SignOutURL *url.URL
|
||||
RedeemURL *url.URL
|
||||
RefreshURL *url.URL
|
||||
ProfileURL *url.URL
|
||||
ValidateURL *url.URL
|
||||
|
||||
SessionValidTTL time.Duration
|
||||
SessionLifetimeTTL time.Duration
|
||||
GracePeriodTTL time.Duration
|
||||
}
|
||||
|
||||
// NewClient instantiates a new AuthenticateClient with provider data
|
||||
func NewClient(uri *url.URL, sharedKey string, sessionValid, sessionLifetime, gracePeriod time.Duration) *AuthenticateClient {
|
||||
return &AuthenticateClient{
|
||||
AuthenticateServiceURL: uri,
|
||||
|
||||
SharedKey: sharedKey,
|
||||
|
||||
SignInURL: uri.ResolveReference(&url.URL{Path: "/sign_in"}),
|
||||
SignOutURL: uri.ResolveReference(&url.URL{Path: "/sign_out"}),
|
||||
RedeemURL: uri.ResolveReference(&url.URL{Path: "/redeem"}),
|
||||
RefreshURL: uri.ResolveReference(&url.URL{Path: "/refresh"}),
|
||||
ValidateURL: uri.ResolveReference(&url.URL{Path: "/validate"}),
|
||||
ProfileURL: uri.ResolveReference(&url.URL{Path: "/profile"}),
|
||||
|
||||
SessionValidTTL: sessionValid,
|
||||
SessionLifetimeTTL: sessionLifetime,
|
||||
GracePeriodTTL: gracePeriod,
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (p *AuthenticateClient) newRequest(method, url string, body io.Reader) (*http.Request, error) {
|
||||
req, err := http.NewRequest(method, url, body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("User-Agent", version.UserAgent())
|
||||
req.Header.Set("Accept", "application/json")
|
||||
req.Host = p.AuthenticateServiceURL.Host
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func isProviderUnavailable(statusCode int) bool {
|
||||
return statusCode == http.StatusTooManyRequests || statusCode == http.StatusServiceUnavailable
|
||||
}
|
||||
|
||||
func extendDeadline(ttl time.Duration) time.Time {
|
||||
return time.Now().Add(ttl).Truncate(time.Second)
|
||||
}
|
||||
|
||||
func (p *AuthenticateClient) withinGracePeriod(s *sessions.SessionState) bool {
|
||||
if s.GracePeriodStart.IsZero() {
|
||||
s.GracePeriodStart = time.Now()
|
||||
}
|
||||
return s.GracePeriodStart.Add(p.GracePeriodTTL).After(time.Now())
|
||||
}
|
||||
|
||||
// Redeem takes a redirectURL and code and redeems the SessionState
|
||||
func (p *AuthenticateClient) Redeem(redirectURL, code string) (*sessions.SessionState, error) {
|
||||
if code == "" {
|
||||
return nil, errors.New("missing code")
|
||||
}
|
||||
|
||||
params := url.Values{}
|
||||
params.Add("shared_secret", p.SharedKey)
|
||||
params.Add("code", code)
|
||||
|
||||
req, err := p.newRequest("POST", p.RedeemURL.String(), bytes.NewBufferString(params.Encode()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
resp, err := defaultHTTPClient.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 {
|
||||
if isProviderUnavailable(resp.StatusCode) {
|
||||
return nil, ErrAuthProviderUnavailable
|
||||
}
|
||||
return nil, fmt.Errorf("got %d from %q %s", resp.StatusCode, p.RedeemURL.String(), body)
|
||||
}
|
||||
|
||||
var jsonResponse struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
IDToken string `json:"id_token"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
ExpiresIn int64 `json:"expires_in"`
|
||||
Email string `json:"email"`
|
||||
}
|
||||
err = json.Unmarshal(body, &jsonResponse)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
user := strings.Split(jsonResponse.Email, "@")[0]
|
||||
return &sessions.SessionState{
|
||||
AccessToken: jsonResponse.AccessToken,
|
||||
RefreshToken: jsonResponse.RefreshToken,
|
||||
IDToken: jsonResponse.IDToken,
|
||||
|
||||
RefreshDeadline: extendDeadline(time.Duration(jsonResponse.ExpiresIn) * time.Second),
|
||||
LifetimeDeadline: extendDeadline(p.SessionLifetimeTTL),
|
||||
ValidDeadline: extendDeadline(p.SessionValidTTL),
|
||||
|
||||
Email: jsonResponse.Email,
|
||||
User: user,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// RefreshSession refreshes the current session
|
||||
func (p *AuthenticateClient) RefreshSession(s *sessions.SessionState) (bool, error) {
|
||||
|
||||
if s.RefreshToken == "" {
|
||||
return false, ErrMissingRefreshToken
|
||||
}
|
||||
|
||||
newToken, duration, err := p.redeemRefreshToken(s.RefreshToken)
|
||||
if err != nil {
|
||||
// When we detect that the auth provider is not explicitly denying
|
||||
// authentication, and is merely unavailable, we refresh and continue
|
||||
// as normal during the "grace period"
|
||||
if err == ErrAuthProviderUnavailable && p.withinGracePeriod(s) {
|
||||
s.RefreshDeadline = extendDeadline(p.SessionValidTTL)
|
||||
return true, nil
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
|
||||
s.AccessToken = newToken
|
||||
s.RefreshDeadline = extendDeadline(duration)
|
||||
s.GracePeriodStart = time.Time{}
|
||||
log.Info().Str("user", s.Email).Msg("proxy/authenticator.RefreshSession")
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (p *AuthenticateClient) redeemRefreshToken(refreshToken string) (token string, expires time.Duration, err error) {
|
||||
params := url.Values{}
|
||||
params.Add("shared_secret", p.SharedKey)
|
||||
params.Add("refresh_token", refreshToken)
|
||||
var req *http.Request
|
||||
req, err = p.newRequest("POST", p.RefreshURL.String(), bytes.NewBufferString(params.Encode()))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
resp, err := defaultHTTPClient.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 != http.StatusCreated {
|
||||
if isProviderUnavailable(resp.StatusCode) {
|
||||
err = ErrAuthProviderUnavailable
|
||||
} else {
|
||||
err = fmt.Errorf("got %d from %q %s", resp.StatusCode, p.RefreshURL.String(), body)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
var data struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
ExpiresIn int64 `json:"expires_in"`
|
||||
}
|
||||
err = json.Unmarshal(body, &data)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
token = data.AccessToken
|
||||
expires = time.Duration(data.ExpiresIn) * time.Second
|
||||
return
|
||||
}
|
||||
|
||||
// ValidateSessionState validates the current sessions state
|
||||
func (p *AuthenticateClient) ValidateSessionState(s *sessions.SessionState) bool {
|
||||
// we validate the user's access token is valid
|
||||
params := url.Values{}
|
||||
params.Add("shared_secret", p.SharedKey)
|
||||
req, err := p.newRequest("GET", fmt.Sprintf("%s?%s", p.ValidateURL.String(), params.Encode()), nil)
|
||||
if err != nil {
|
||||
log.Info().Err(err).Str("user", s.Email).Msg("proxy/authenticator: error validating session state")
|
||||
return false
|
||||
}
|
||||
req.Header.Set("X-Client-Secret", p.SharedKey)
|
||||
req.Header.Set("X-Access-Token", s.AccessToken)
|
||||
req.Header.Set("X-Id-Token", s.IDToken)
|
||||
|
||||
resp, err := defaultHTTPClient.Do(req)
|
||||
if err != nil {
|
||||
log.Info().Err(err).Str("user", s.Email).Msg("proxy/authenticator: error validating access token")
|
||||
return false
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
// When we detect that the auth provider is not explicitly denying
|
||||
// authentication, and is merely unavailable, we validate and continue
|
||||
// as normal during the "grace period"
|
||||
if isProviderUnavailable(resp.StatusCode) && p.withinGracePeriod(s) {
|
||||
s.ValidDeadline = extendDeadline(p.SessionValidTTL)
|
||||
return true
|
||||
}
|
||||
log.Info().Str("user", s.Email).Int("status-code", resp.StatusCode).Msg("proxy/authenticator: bad status code")
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
s.ValidDeadline = extendDeadline(p.SessionValidTTL)
|
||||
s.GracePeriodStart = time.Time{}
|
||||
return true
|
||||
}
|
||||
|
||||
// signRedirectURL signs the redirect url string, given a timestamp, and returns it
|
||||
func (p *AuthenticateClient) signRedirectURL(rawRedirect string, timestamp time.Time) string {
|
||||
h := hmac.New(sha256.New, []byte(p.SharedKey))
|
||||
h.Write([]byte(rawRedirect))
|
||||
h.Write([]byte(fmt.Sprint(timestamp.Unix())))
|
||||
return base64.URLEncoding.EncodeToString(h.Sum(nil))
|
||||
}
|
||||
|
||||
// GetSignInURL with typical oauth parameters
|
||||
func (p *AuthenticateClient) GetSignInURL(redirectURL *url.URL, state string) *url.URL {
|
||||
a := *p.SignInURL
|
||||
now := time.Now()
|
||||
rawRedirect := redirectURL.String()
|
||||
params, _ := url.ParseQuery(a.RawQuery)
|
||||
params.Set("redirect_uri", rawRedirect)
|
||||
params.Set("shared_secret", p.SharedKey)
|
||||
params.Set("response_type", "code")
|
||||
params.Add("state", state)
|
||||
params.Set("ts", fmt.Sprint(now.Unix()))
|
||||
params.Set("sig", p.signRedirectURL(rawRedirect, now))
|
||||
a.RawQuery = params.Encode()
|
||||
return &a
|
||||
}
|
||||
|
||||
// GetSignOutURL creates and returns the sign out URL, given a redirectURL
|
||||
func (p *AuthenticateClient) GetSignOutURL(redirectURL *url.URL) *url.URL {
|
||||
a := *p.SignOutURL
|
||||
now := time.Now()
|
||||
rawRedirect := redirectURL.String()
|
||||
params, _ := url.ParseQuery(a.RawQuery)
|
||||
params.Add("redirect_uri", rawRedirect)
|
||||
params.Set("ts", fmt.Sprint(now.Unix()))
|
||||
params.Set("sig", p.signRedirectURL(rawRedirect, now))
|
||||
a.RawQuery = params.Encode()
|
||||
return &a
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue