mirror of
https://github.com/pomerium/pomerium.git
synced 2025-05-02 20:06:03 +02:00
- Fixed a bug where Lifetime TTL was set to a minute. - Remove nested mux in authenticate handlers. - Remove extra ping endpoint in authenticate and proxy. - Simplified sign in flow with multi-catch case statement. - Removed debugging logging. - Broke out cmd/pomerium options into own file. - Renamed msicreant cipher to just cipher. Closes #23
308 lines
9.1 KiB
Go
308 lines
9.1 KiB
Go
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
|
|
}
|