mirror of
https://github.com/pomerium/pomerium.git
synced 2025-05-02 11:56:02 +02:00
When user signin to 2 sites "a.example.com" and "b.example.com", we're using the same session for user when accessing those sites. When user singout from "a.example.com", that session is marked as deleted, thus user now can not access "b.example.com" nor re-signin to get new access. User must wait the cookie is expired, or delete the cookie manually to re-signin to "b.example.com". This is also affected if user signout from authenticate service dashboard page directly. To fix this, we will clear the session state if the session was deleted, authorize service will return unauthorized, so the user will be redirected to re-authenticate. Updates #1014 Updates #858
323 lines
9.4 KiB
Go
323 lines
9.4 KiB
Go
package authorize
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"io/ioutil"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
|
|
"github.com/golang/protobuf/ptypes"
|
|
"github.com/rs/zerolog"
|
|
|
|
"github.com/pomerium/pomerium/authorize/evaluator"
|
|
"github.com/pomerium/pomerium/internal/httputil"
|
|
"github.com/pomerium/pomerium/internal/log"
|
|
"github.com/pomerium/pomerium/internal/sessions"
|
|
"github.com/pomerium/pomerium/internal/telemetry/requestid"
|
|
"github.com/pomerium/pomerium/internal/telemetry/trace"
|
|
"github.com/pomerium/pomerium/internal/urlutil"
|
|
"github.com/pomerium/pomerium/pkg/grpc/databroker"
|
|
"github.com/pomerium/pomerium/pkg/grpc/session"
|
|
"github.com/pomerium/pomerium/pkg/grpc/user"
|
|
|
|
envoy_api_v2_core "github.com/envoyproxy/go-control-plane/envoy/api/v2/core"
|
|
envoy_service_auth_v2 "github.com/envoyproxy/go-control-plane/envoy/service/auth/v2"
|
|
)
|
|
|
|
var sessionTypeURL, userTypeURL string
|
|
|
|
func init() {
|
|
any, _ := ptypes.MarshalAny(new(session.Session))
|
|
sessionTypeURL = any.GetTypeUrl()
|
|
|
|
any, _ = ptypes.MarshalAny(new(user.User))
|
|
userTypeURL = any.GetTypeUrl()
|
|
}
|
|
|
|
// Check implements the envoy auth server gRPC endpoint.
|
|
func (a *Authorize) Check(ctx context.Context, in *envoy_service_auth_v2.CheckRequest) (*envoy_service_auth_v2.CheckResponse, error) {
|
|
ctx, span := trace.StartSpan(ctx, "authorize.grpc.Check")
|
|
defer span.End()
|
|
|
|
// maybe rewrite http request for forward auth
|
|
isForwardAuth := a.handleForwardAuth(in)
|
|
hreq := getHTTPRequestFromCheckRequest(in)
|
|
rawJWT, _ := loadRawSession(hreq, a.currentOptions.Load(), a.currentEncoder.Load())
|
|
sessionState, _ := loadSession(a.currentEncoder.Load(), rawJWT)
|
|
|
|
// only accept sessions whose databroker server versions match
|
|
if sessionState != nil {
|
|
a.dataBrokerDataLock.RLock()
|
|
if a.dataBrokerSessionServerVersion != sessionState.Version.String() {
|
|
log.Warn().
|
|
Str("server_version", a.dataBrokerSessionServerVersion).
|
|
Str("session_version", sessionState.Version.String()).
|
|
Msg("clearing session due to invalid version")
|
|
sessionState = nil
|
|
}
|
|
a.dataBrokerDataLock.RUnlock()
|
|
}
|
|
|
|
if err := a.forceSync(ctx, sessionState); err != nil {
|
|
log.Warn().Err(err).Msg("clearing session due to force sync failed")
|
|
sessionState = nil
|
|
}
|
|
|
|
a.dataBrokerDataLock.RLock()
|
|
defer a.dataBrokerDataLock.RUnlock()
|
|
|
|
req := a.getEvaluatorRequestFromCheckRequest(in, sessionState)
|
|
reply, err := a.pe.Evaluate(ctx, req)
|
|
if err != nil {
|
|
log.Error().Err(err).Msg("error during OPA evaluation")
|
|
return nil, err
|
|
}
|
|
logAuthorizeCheck(ctx, in, reply)
|
|
|
|
switch {
|
|
case reply.Status == http.StatusOK:
|
|
return a.okResponse(reply), nil
|
|
case reply.Status == http.StatusUnauthorized:
|
|
if isForwardAuth {
|
|
return a.deniedResponse(in, http.StatusUnauthorized, "Unauthenticated", nil), nil
|
|
}
|
|
return a.redirectResponse(in), nil
|
|
}
|
|
return a.deniedResponse(in, int32(reply.Status), reply.Message, nil), nil
|
|
}
|
|
|
|
func (a *Authorize) forceSync(ctx context.Context, ss *sessions.State) error {
|
|
ctx, span := trace.StartSpan(ctx, "authorize.forceSync")
|
|
defer span.End()
|
|
if ss == nil {
|
|
return nil
|
|
}
|
|
s := a.forceSyncSession(ctx, ss.ID)
|
|
if s == nil {
|
|
return nil
|
|
}
|
|
if s.DeletedAt != nil {
|
|
return errors.New("session was deleted")
|
|
}
|
|
a.forceSyncUser(ctx, s.GetUserId())
|
|
return nil
|
|
}
|
|
|
|
func (a *Authorize) forceSyncSession(ctx context.Context, sessionID string) *session.Session {
|
|
ctx, span := trace.StartSpan(ctx, "authorize.forceSyncSession")
|
|
defer span.End()
|
|
|
|
a.dataBrokerDataLock.RLock()
|
|
s, ok := a.dataBrokerData.Get(sessionTypeURL, sessionID).(*session.Session)
|
|
a.dataBrokerDataLock.RUnlock()
|
|
if ok {
|
|
return s
|
|
}
|
|
|
|
res, err := a.dataBrokerClient.Get(ctx, &databroker.GetRequest{
|
|
Type: sessionTypeURL,
|
|
Id: sessionID,
|
|
})
|
|
if err != nil {
|
|
log.Warn().Err(err).Msg("failed to get session from databroker")
|
|
return nil
|
|
}
|
|
|
|
a.dataBrokerDataLock.Lock()
|
|
if current := a.dataBrokerData.Get(sessionTypeURL, sessionID); current == nil {
|
|
a.dataBrokerData.Update(res.GetRecord())
|
|
}
|
|
s, _ = a.dataBrokerData.Get(sessionTypeURL, sessionID).(*session.Session)
|
|
a.dataBrokerDataLock.Unlock()
|
|
|
|
return s
|
|
}
|
|
|
|
func (a *Authorize) forceSyncUser(ctx context.Context, userID string) *user.User {
|
|
ctx, span := trace.StartSpan(ctx, "authorize.forceSyncUser")
|
|
defer span.End()
|
|
|
|
a.dataBrokerDataLock.RLock()
|
|
s, ok := a.dataBrokerData.Get(userTypeURL, userID).(*user.User)
|
|
a.dataBrokerDataLock.RUnlock()
|
|
if ok {
|
|
return s
|
|
}
|
|
|
|
res, err := a.dataBrokerClient.Get(ctx, &databroker.GetRequest{
|
|
Type: userTypeURL,
|
|
Id: userID,
|
|
})
|
|
if err != nil {
|
|
log.Warn().Err(err).Msg("failed to get user from databroker")
|
|
return nil
|
|
}
|
|
|
|
a.dataBrokerDataLock.Lock()
|
|
if current := a.dataBrokerData.Get(userTypeURL, userID); current == nil {
|
|
a.dataBrokerData.Update(res.GetRecord())
|
|
}
|
|
s, _ = a.dataBrokerData.Get(userTypeURL, userID).(*user.User)
|
|
a.dataBrokerDataLock.Unlock()
|
|
|
|
return s
|
|
}
|
|
|
|
func (a *Authorize) getEnvoyRequestHeaders(signedJWT string) ([]*envoy_api_v2_core.HeaderValueOption, error) {
|
|
var hvos []*envoy_api_v2_core.HeaderValueOption
|
|
|
|
hdrs, err := a.getJWTClaimHeaders(a.currentOptions.Load(), signedJWT)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for k, v := range hdrs {
|
|
hvos = append(hvos, mkHeader(k, v))
|
|
}
|
|
|
|
return hvos, nil
|
|
}
|
|
|
|
func (a *Authorize) handleForwardAuth(req *envoy_service_auth_v2.CheckRequest) bool {
|
|
opts := a.currentOptions.Load()
|
|
|
|
if opts.ForwardAuthURL == nil {
|
|
return false
|
|
}
|
|
|
|
checkURL := getCheckRequestURL(req)
|
|
if urlutil.StripPort(checkURL.Host) != urlutil.StripPort(opts.GetForwardAuthURL().Host) {
|
|
return false
|
|
}
|
|
|
|
uriQuery := checkURL.Query().Get("uri")
|
|
if (checkURL.Path != "/" && checkURL.Path != "/verify") || uriQuery == "" {
|
|
return false
|
|
}
|
|
verifyURL, err := url.Parse(uriQuery)
|
|
if err != nil {
|
|
log.Warn().Str("uri", checkURL.Query().Get("uri")).Err(err).Msg("failed to parse uri for forward authentication")
|
|
return false
|
|
}
|
|
|
|
req.Attributes.Request.Http.Scheme = verifyURL.Scheme
|
|
req.Attributes.Request.Http.Host = verifyURL.Host
|
|
req.Attributes.Request.Http.Path = verifyURL.Path
|
|
if headers := req.GetAttributes().GetRequest().GetHttp().GetHeaders(); headers != nil {
|
|
if xfu := headers[http.CanonicalHeaderKey(httputil.HeaderForwardedURI)]; xfu != "" {
|
|
req.Attributes.Request.Http.Path += xfu
|
|
}
|
|
}
|
|
// envoy sends the query string as part of the path
|
|
if verifyURL.RawQuery != "" {
|
|
req.Attributes.Request.Http.Path += "?" + verifyURL.RawQuery
|
|
}
|
|
return true
|
|
}
|
|
|
|
func (a *Authorize) getEvaluatorRequestFromCheckRequest(in *envoy_service_auth_v2.CheckRequest, sessionState *sessions.State) *evaluator.Request {
|
|
requestURL := getCheckRequestURL(in)
|
|
req := &evaluator.Request{
|
|
DataBrokerData: a.dataBrokerData,
|
|
HTTP: evaluator.RequestHTTP{
|
|
Method: in.GetAttributes().GetRequest().GetHttp().GetMethod(),
|
|
URL: requestURL.String(),
|
|
Headers: getCheckRequestHeaders(in),
|
|
ClientCertificate: getPeerCertificate(in),
|
|
},
|
|
}
|
|
if sessionState != nil {
|
|
req.Session = evaluator.RequestSession{
|
|
ID: sessionState.ID,
|
|
ImpersonateEmail: sessionState.ImpersonateEmail,
|
|
ImpersonateGroups: sessionState.ImpersonateGroups,
|
|
}
|
|
}
|
|
return req
|
|
}
|
|
|
|
func getHTTPRequestFromCheckRequest(req *envoy_service_auth_v2.CheckRequest) *http.Request {
|
|
hattrs := req.GetAttributes().GetRequest().GetHttp()
|
|
hreq := &http.Request{
|
|
Method: hattrs.GetMethod(),
|
|
URL: getCheckRequestURL(req),
|
|
Header: make(http.Header),
|
|
Body: ioutil.NopCloser(strings.NewReader(hattrs.GetBody())),
|
|
Host: hattrs.GetHost(),
|
|
RequestURI: hattrs.GetPath(),
|
|
}
|
|
for k, v := range getCheckRequestHeaders(req) {
|
|
hreq.Header.Set(k, v)
|
|
}
|
|
return hreq
|
|
}
|
|
|
|
func getCheckRequestHeaders(req *envoy_service_auth_v2.CheckRequest) map[string]string {
|
|
hdrs := make(map[string]string)
|
|
ch := req.GetAttributes().GetRequest().GetHttp().GetHeaders()
|
|
for k, v := range ch {
|
|
hdrs[http.CanonicalHeaderKey(k)] = v
|
|
}
|
|
return hdrs
|
|
}
|
|
|
|
func getCheckRequestURL(req *envoy_service_auth_v2.CheckRequest) *url.URL {
|
|
h := req.GetAttributes().GetRequest().GetHttp()
|
|
u := &url.URL{
|
|
Scheme: h.GetScheme(),
|
|
Host: h.GetHost(),
|
|
}
|
|
|
|
// envoy sends the query string as part of the path
|
|
path := h.GetPath()
|
|
if idx := strings.Index(path, "?"); idx != -1 {
|
|
u.Path, u.RawQuery = path[:idx], path[idx+1:]
|
|
} else {
|
|
u.Path = path
|
|
}
|
|
|
|
if h.GetHeaders() != nil {
|
|
if fwdProto, ok := h.GetHeaders()["x-forwarded-proto"]; ok {
|
|
u.Scheme = fwdProto
|
|
}
|
|
}
|
|
return u
|
|
}
|
|
|
|
// getPeerCertificate gets the PEM-encoded peer certificate from the check request
|
|
func getPeerCertificate(in *envoy_service_auth_v2.CheckRequest) string {
|
|
// ignore the error as we will just return the empty string in that case
|
|
cert, _ := url.QueryUnescape(in.GetAttributes().GetSource().GetCertificate())
|
|
return cert
|
|
}
|
|
|
|
func logAuthorizeCheck(
|
|
ctx context.Context,
|
|
in *envoy_service_auth_v2.CheckRequest,
|
|
reply *evaluator.Result,
|
|
) {
|
|
hdrs := getCheckRequestHeaders(in)
|
|
hattrs := in.GetAttributes().GetRequest().GetHttp()
|
|
evt := log.Info().Str("service", "authorize")
|
|
// request
|
|
evt = evt.Str("request-id", requestid.FromContext(ctx))
|
|
evt = evt.Str("check-request-id", hdrs["X-Request-Id"])
|
|
evt = evt.Str("method", hattrs.GetMethod())
|
|
evt = evt.Str("path", hattrs.GetPath())
|
|
evt = evt.Str("host", hattrs.GetHost())
|
|
evt = evt.Str("query", hattrs.GetQuery())
|
|
// reply
|
|
if reply != nil {
|
|
evt = evt.Bool("allow", reply.Status == http.StatusOK)
|
|
evt = evt.Int("status", reply.Status)
|
|
evt = evt.Str("message", reply.Message)
|
|
}
|
|
|
|
// potentially sensitive, only log if debug mode
|
|
if zerolog.GlobalLevel() <= zerolog.DebugLevel {
|
|
evt = evt.Interface("headers", hdrs)
|
|
}
|
|
|
|
evt.Msg("authorize check")
|
|
}
|