Skip to content

Commit

Permalink
now with session store
Browse files Browse the repository at this point in the history
  • Loading branch information
lsjostro committed Feb 8, 2024
1 parent 3b45f39 commit 6a0db58
Show file tree
Hide file tree
Showing 9 changed files with 549 additions and 72 deletions.
3 changes: 1 addition & 2 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,7 @@ on:

jobs:
build:
# runs-on: shelmangroup-default
runs-on: ubuntu-latest
runs-on: shelmangroup-default
steps:
- uses: actions/checkout@v4
- uses: cachix/install-nix-action@v22
Expand Down
10 changes: 8 additions & 2 deletions .github/workflows/release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,14 @@ on:
- "v*"
jobs:
goreleaser:
# runs-on: shelmangroup-default
runs-on: ubuntu-latest
runs-on: shelmangroup-default
steps:
- uses: actions/checkout@v4
with:
sparse-checkout: |
flake.lock
flake.nix
.envrc
- uses: cachix/install-nix-action@v22
with:
Expand All @@ -17,6 +21,8 @@ jobs:
with:
use_nix_profile: true

- uses: actions/checkout@v4

- name: Log in to the Container registry
uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1
with:
Expand Down
1 change: 0 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ Some of the features it provides:

- Session management

- Server is stateless, all state is stored encrypted in client cookie data
- Refreshes expired tokens automatically

- Open Policy Agent chaining request.
Expand Down
93 changes: 43 additions & 50 deletions authz/authz.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import (

pb "github.com/shelmangroup/envoy-oidc-authserver/internal/gen/session/v1"
"github.com/shelmangroup/envoy-oidc-authserver/session"
"github.com/shelmangroup/envoy-oidc-authserver/store"
)

const ServiceName = "envoy-authz"
Expand All @@ -36,6 +37,7 @@ type Service struct {

cfg *Config
secretKey []byte
store *store.Store
authClient authv3connect.AuthorizationClient
}

Expand Down Expand Up @@ -66,6 +68,7 @@ func NewService(cfg *Config, opaURL, secretKey string) *Service {
return &Service{
cfg: cfg,
authClient: c,
store: store.NewStore(),
secretKey: []byte(secretKey),
}
}
Expand Down Expand Up @@ -157,7 +160,7 @@ func (s *Service) authProcess(ctx context.Context, req *auth.AttributeContext_Ht

// If the request is for the callback URI, then we need to exchange the code for tokens
if strings.HasPrefix(requestedURL, provider.CallbackURI+"?") && sessionData.AccessToken == "" {
headers, err := s.retriveTokens(ctx, provider, sessionData, requestedURL, sessionCookieName, sessionId)
err := s.retriveTokens(ctx, provider, sessionData, requestedURL, sessionCookieName, sessionId)
if err != nil {
return nil, err
}
Expand All @@ -167,7 +170,7 @@ func (s *Service) authProcess(ctx context.Context, req *auth.AttributeContext_Ht
return s.authResponse(false, envoy_type.StatusCode_Found, headers, nil, "redirect to requested url"), nil
}

respHeaders, err := s.validateTokens(ctx, provider, sessionData, sessionCookieName, sessionId)
err := s.validateTokens(ctx, provider, sessionData, sessionCookieName, sessionId)
if err != nil {
slog.Warn("couldn't validating tokens", slog.String("err", err.Error()))
headers, err := s.newSession(ctx, sourceIP, requestedURL, sessionCookieName, provider)
Expand All @@ -179,19 +182,17 @@ func (s *Service) authProcess(ctx context.Context, req *auth.AttributeContext_Ht

slog.Debug("setting authorization header to upstream request", slog.String("session_id", sessionId))
headers = append(headers, s.setAuthorizationHeader(sessionData.IdToken))
return s.authResponse(true, envoy_type.StatusCode_OK, headers, respHeaders, "success"), nil
return s.authResponse(true, envoy_type.StatusCode_OK, headers, nil, "success"), nil
}

func (s *Service) retriveTokens(ctx context.Context, provider *OIDCProvider, sessionData *pb.SessionData, requestedURL, sessionCookieName, sessionId string) ([]*core.HeaderValueOption, error) {
headers := []*core.HeaderValueOption{}

func (s *Service) retriveTokens(ctx context.Context, provider *OIDCProvider, sessionData *pb.SessionData, requestedURL, sessionCookieName, sessionId string) error {
code, err := s.getCodeQueryParam(requestedURL)
if err != nil {
return nil, err
return err
}
tokens, err := provider.p.RetriveTokens(ctx, code, sessionData.CodeVerifier)
if err != nil {
return nil, err
return err
}

// Copy the tokens into the session data
Expand All @@ -200,45 +201,39 @@ func (s *Service) retriveTokens(ctx context.Context, provider *OIDCProvider, ses
sessionData.IdToken = tokens.IDToken
sessionData.Expiry = timestamppb.New(tokens.Expiry)

slog.Debug("successfully acquried tokens, now storing it to session cookie", slog.String("expire", tokens.Expiry.String()))
// slog.Debug("successfully acquried tokens, now storing it to session cookie", slog.Any("sessionData", sessionData))

enc, err := session.EncodeToken(ctx, [32]byte(s.secretKey), sessionData)
if err != nil {
slog.Error("error encrypting session data", slog.String("err", err.Error()))
return nil, err
return err
}
slog.Debug("Encrypted SessionData", slog.Int("byte_len", len(enc)))

cookie := &http.Cookie{
Name: sessionCookieName,
Value: sessionId + "." + enc,
Path: "/",
HttpOnly: true,
Secure: provider.SecureCookie,
SameSite: http.SameSiteLaxMode,
// store session data in cache
if err := s.store.Set(ctx, sessionId, enc); err != nil {
return err
}
headers = append(headers, s.setCookie(cookie)...)
return headers, nil

return nil
}

// Validates and poteintially refreshes the token
func (s *Service) validateTokens(ctx context.Context, provider *OIDCProvider, d *pb.SessionData, sessionCookieName, sessionId string) ([]*core.HeaderValueOption, error) {
headers := []*core.HeaderValueOption{}

func (s *Service) validateTokens(ctx context.Context, provider *OIDCProvider, d *pb.SessionData, sessionCookieName, sessionId string) error {
expired, err := provider.p.VerifyTokens(ctx, d.AccessToken, d.IdToken)
if err != nil {
return nil, err
return err
}
if !expired {
return nil, nil
return nil
}

if expired && d.RefreshToken == "" {
return nil, errors.New("token expired and no refresh token found, add scope=offline_access to the auth request to get a refresh token")
return errors.New("token expired and no refresh token found, add scope=offline_access to the auth request to get a refresh token")
}
t, err := provider.p.RefreshTokens(ctx, d.RefreshToken, d.AccessToken)
if err != nil {
return nil, err
return err
}

d.RefreshToken = t.RefreshToken
Expand All @@ -250,20 +245,14 @@ func (s *Service) validateTokens(ctx context.Context, provider *OIDCProvider, d
enc, err := session.EncodeToken(ctx, [32]byte(s.secretKey), d)
if err != nil {
slog.Error("error encrypting session data", slog.String("err", err.Error()))
return nil, err
return err
}

cookie := &http.Cookie{
Name: sessionCookieName,
Value: sessionId + "." + enc,
Path: "/",
HttpOnly: true,
Secure: provider.SecureCookie,
SameSite: http.SameSiteLaxMode,
if err := s.store.Set(ctx, sessionId, enc); err != nil {
return err
}
headers = append(headers, s.setCookie(cookie)...)

return headers, nil
return nil
}

func (s *Service) newSession(ctx context.Context, sourceIP, requestedURL, sessionCookieName string, provider *OIDCProvider) ([]*core.HeaderValueOption, error) {
Expand All @@ -283,14 +272,18 @@ func (s *Service) newSession(ctx context.Context, sourceIP, requestedURL, sessio
if err != nil {
return nil, err
}
slog.Debug("NewSession Encrypted SessionData", slog.Int("byte_len", len(enc)), slog.String("encrypted", enc))

// store session data in cache
if err := s.store.Set(ctx, sessionCookieToken, enc); err != nil {
return nil, err
}

idpAuthURL := provider.p.IdpAuthURL(sessionData.CodeChallenge)
headers = append(headers, s.setRedirectHeader(idpAuthURL))
// set cookie with session id and redirect to Idp
cookie := &http.Cookie{
Name: sessionCookieName,
Value: sessionCookieToken + "." + enc,
Value: sessionCookieToken,
Path: "/",
HttpOnly: true,
Secure: provider.SecureCookie,
Expand Down Expand Up @@ -318,28 +311,28 @@ func (s *Service) getSessionCookieData(ctx context.Context, req *auth.AttributeC
return nil, ""
}

// Split cookie value with . delimeter
cookieValues := strings.Split(cookie.Value, ".")
if len(cookieValues) != 2 {
slog.Error("cookie values != 2", slog.Int("values_len", len(cookieValues)))
sessionId := cookie.Value
slog.Debug("client source ip", slog.String("session_id", sessionId), slog.String("ip", sourceIP))

d, err := s.store.Cache.Get(ctx, sessionId)
if err != nil {
slog.Error("error getting session data from cache", slog.String("err", err.Error()))
return nil, ""
}

slog.Debug("client source ip", slog.String("session_id", cookieValues[0]), slog.String("ip", sourceIP))

sessionData, err := session.DecodeToken(ctx, [32]byte(s.secretKey), cookieValues[1])
sessionData, err = session.DecodeToken(ctx, [32]byte(s.secretKey), d)
if err != nil {
slog.Error("error decrypt session data", slog.String("err", err.Error()))
return nil, ""
}
slog.Debug("getting session data from session cookie", slog.String("session_id", cookieValues[0]), slog.String("session_data_expiry", sessionData.Expiry.AsTime().String()))
slog.Debug("getting session data from session cookie", slog.String("session_id", sessionId), slog.String("session_data_expiry", sessionData.Expiry.AsTime().String()))

if sessionData.SourceIp != sourceIP {
slog.Warn("source ip missmatch, re-auth needed!", slog.String("session_id", cookieValues[0]), slog.String("session_ip", sessionData.SourceIp), slog.String("req_ip", sourceIP))
slog.Warn("source ip missmatch, re-auth needed!", slog.String("session_id", sessionId), slog.String("session_ip", sessionData.SourceIp), slog.String("req_ip", sourceIP))
return nil, ""
}

return sessionData, cookieValues[0]
return sessionData, sessionId
}

// parse cookie header string into []*http.Cookie struct
Expand Down Expand Up @@ -439,11 +432,11 @@ func (s *Service) authResponse(success bool, httpStatusCode envoy_type.StatusCod
func realIP(headers map[string]string) string {
var ip string

var trueClientIP = "true-client-ip"
var envoyExternalAddress = "x-envoy-external-address"
var xForwardedFor = "x-forwarded-for"
var xRealIP = "x-real-ip"

if tcip, ok := headers[trueClientIP]; ok {
if tcip, ok := headers[envoyExternalAddress]; ok {
ip = tcip
} else if xrip, ok := headers[xRealIP]; ok {
ip = xrip
Expand Down
8 changes: 4 additions & 4 deletions authz/authz_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
envoy_type "buf.build/gen/go/envoyproxy/envoy/protocolbuffers/go/envoy/type/v3"

"github.com/shelmangroup/envoy-oidc-authserver/oidc"
"github.com/shelmangroup/envoy-oidc-authserver/store"
)

func initializeMock(cfg *Config) (*Config, error) {
Expand Down Expand Up @@ -53,7 +54,7 @@ func TestCheckServiceAuthFlow(t *testing.T) {
require.NoError(t, err, "init cfg should not have failed")

secretKey := []byte("G_TdvPJ9T8C4p&A?Wr3YAUYW$*9vn4?t")
authz := Service{cfg: testCfg, secretKey: secretKey}
authz := Service{cfg: testCfg, store: store.NewStore(), secretKey: secretKey}

//1. Check Authorization response without callback and no cookie req.
initialRequestedURL := "http://foo.bar/"
Expand Down Expand Up @@ -102,10 +103,9 @@ func TestCheckServiceAuthFlow(t *testing.T) {
resp, err = authz.Check(context.TODO(), cookieReq)
require.NoError(t, err, "check with callback should not have failed")
assert.Equal(t, int32(rpc.PERMISSION_DENIED), resp.Msg.Status.Code)
assert.Equal(t, initialRequestedURL, resp.Msg.GetDeniedResponse().GetHeaders()[4].GetHeader().GetValue())
assert.Equal(t, initialRequestedURL, resp.Msg.GetDeniedResponse().GetHeaders()[0].GetHeader().GetValue())

//3. Success with Auth header set
authCookie := resp.Msg.GetDeniedResponse().GetHeaders()[3].GetHeader().GetValue()
successReq := connect.NewRequest(
&auth.CheckRequest{
Attributes: &auth.AttributeContext{
Expand All @@ -116,7 +116,7 @@ func TestCheckServiceAuthFlow(t *testing.T) {
Path: "/",
Headers: map[string]string{
":authority": "foo.bar",
"cookie": authCookie,
"cookie": cookie,
},
},
},
Expand Down
16 changes: 15 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@ require (
connectrpc.com/grpchealth v1.3.0
connectrpc.com/grpcreflect v1.2.0
connectrpc.com/otelconnect v0.6.0
github.com/eko/gocache/lib/v4 v4.1.5
github.com/gogo/googleapis v1.4.1
github.com/google/uuid v1.5.0
github.com/grokify/go-pkce v0.2.3
github.com/peterbourgon/ff/v3 v3.4.0
github.com/stretchr/testify v1.8.4
github.com/zitadel/oidc/v3 v3.8.1
Expand All @@ -26,19 +28,31 @@ require (
google.golang.org/grpc v1.60.1
)

require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/golang/mock v1.6.0 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
github.com/prometheus/client_golang v1.14.0 // indirect
github.com/prometheus/client_model v0.3.0 // indirect
github.com/prometheus/common v0.37.0 // indirect
github.com/prometheus/procfs v0.8.0 // indirect
)

require (
buf.build/gen/go/cncf/xds/protocolbuffers/go v1.32.0-20231212190141-23263dcfaa96.1 // indirect
buf.build/gen/go/envoyproxy/protoc-gen-validate/protocolbuffers/go v1.32.0-20231130202533-71881f09a0c5.1 // indirect
github.com/allegro/bigcache v1.2.1
github.com/cenkalti/backoff/v4 v4.2.1 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/eko/gocache/store/bigcache/v4 v4.2.1
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/go-jose/go-jose/v3 v3.0.1 // indirect
github.com/go-logr/logr v1.3.0 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/gorilla/securecookie v1.1.2 // indirect
github.com/grokify/go-pkce v0.2.3
github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 // indirect
github.com/lmittmann/tint v1.0.3
github.com/mattn/go-isatty v0.0.20
Expand Down
Loading

0 comments on commit 6a0db58

Please sign in to comment.