Repository: jkroepke/openvpn-auth-oauth2
Pull request #93 (2023-12-22 10:17:51)

Implement token store
#### What this PR does / why we need it

#### Which issue this PR fixes
*(optional, in `fixes #<issue number>(, fixes #<issue_number>, ...)` format, will close that issue when PR gets merged)*: fixes #

- fixes #
 
#### Special notes for your reviewer

#### Particularly user-facing changes

#### Checklist

Complete these before marking the PR as `ready to review`:

<!-- [Place an '[x]' (no spaces) in all applicable fields.] -->
- [ ] [DCO](https://github.com/prometheus-community/helm-charts/blob/main/CONTRIBUTING.md#sign-off-your-work) signed
- [ ] The PR title has a summary of the changes
- [ ] The PR body has a summary to reflect any significant (and particularly user-facing) changes introduced by this PR



Patch:
diff --git a/cmd/root.go b/cmd/root.go
index cf36c46..314b528 100644
--- a/cmd/root.go
+++ b/cmd/root.go
@@ -15,6 +15,7 @@ import (
 	"github.com/jkroepke/openvpn-auth-oauth2/internal/http"
 	"github.com/jkroepke/openvpn-auth-oauth2/internal/oauth2"
 	"github.com/jkroepke/openvpn-auth-oauth2/internal/openvpn"
+	"github.com/jkroepke/openvpn-auth-oauth2/internal/storage"
 	"github.com/jkroepke/openvpn-auth-oauth2/internal/utils"
 )
 
@@ -58,9 +59,10 @@ func Execute(args []string, logWriter io.Writer, version, commit, date string) i
 		return 1
 	}
 
-	openvpnClient := openvpn.NewClient(logger, conf)
+	storageClient := storage.New(conf.OAuth2.TokenStore.Key.String(), conf.OAuth2.TokenStore.Expires)
+	openvpnClient := openvpn.NewClient(logger, conf, storageClient)
 
-	provider, err := oauth2.NewProvider(logger, conf, openvpnClient)
+	provider, err := oauth2.NewProvider(logger, conf, storageClient, openvpnClient)
 	if err != nil {
 		logger.Error(err.Error())
 
diff --git a/docs/Providers.md b/docs/Providers.md
index 7f64fda..9ea1c26 100644
--- a/docs/Providers.md
+++ b/docs/Providers.md
@@ -31,6 +31,27 @@ References:
 - https://docs.microsoft.com/en-us/azure/active-directory/develop/quickstart-register-app
 - https://learn.microsoft.com/en-us/azure/active-directory/develop/active-directory-optional-claims
 
+## Google Cloud / Google Workspace
+
+### Register an app on google cloud console
+
+1. Login as admin into your google console from here https://console.cloud.google.com/
+2. click on Create a new project or select an existing project
+3. then "API & Services", then "Credentials" (left column)
+4. click "create credentials" (button at the top-middle) then type "OAuth Client ID" (in the dropdown)
+5. Choose a name for your app like "openvpn connection"
+6. in the "Authorized Redirect URIs" add one and set it to "https://yourdomain/oauth2/callback" (yourdomain would be the domain where a proxy is redirecting to your openvpn-auth-oauth2 daemon, typically on port 9000) 
+7. you'll get your client id and client secret from google, copy them somewhere safe
+8. use those in the config as shown below
+
+### Configuration
+
+Set the following variables in your openvpn-auth-oauth2 configuration file: 
+
+- `CONFIG_OAUTH2_ISSUER=https://accounts.google.com/.well-known/openid-configuration`
+- `CONFIG_OAUTH2_CLIENT_ID=162738495-xxxxx.apps.googleusercontent.com`
+- `CONFIG_OAUTH2_CLIENT_SECRET=GOCSPX-xxxxxxxx`
+
 ## GitHub
 ### Caveats
 A user must explicitly [request](https://help.github.com/articles/requesting-organization-approval-for-oauth-apps/) an
diff --git a/internal/config/config.go b/internal/config/config.go
index 32819f6..ac4e10b 100644
--- a/internal/config/config.go
+++ b/internal/config/config.go
@@ -102,9 +102,9 @@ func FlagSet(name string) *flag.FlagSet {
 		Defaults.OpenVpn.AuthTokenUser,
 		"Define auth-token-user for all sessions",
 	)
-	flagSet.String(
+	flagSet.Duration(
 		"openvpn.auth-pending-timeout",
-		Defaults.OpenVpn.AuthPendingTimeout.String(),
+		Defaults.OpenVpn.AuthPendingTimeout,
 		"How long OpenVPN server wait until user is authenticated",
 	)
 	flagSet.TextVar(new(StringSlice),
@@ -188,6 +188,16 @@ func FlagSet(name string) *flag.FlagSet {
 		Defaults.OAuth2.Scopes,
 		"oauth2 token scopes. Defaults depends on oauth2.provider",
 	)
+	flagSet.Duration(
+		"oauth2.tokenstore.expires",
+		Defaults.OAuth2.TokenStore.Expires,
+		"TTL of stored oauth2 token.",
+	)
+	flagSet.TextVar(new(Secret),
+		"oauth2.tokenstore.key",
+		Defaults.OAuth2.TokenStore.Key,
+		"encryption key to encrypt oauth2 token in store",
+	)
 	flagSet.Bool(
 		"version",
 		false,
diff --git a/internal/config/defaults.go b/internal/config/defaults.go
index da893aa..55c2769 100644
--- a/internal/config/defaults.go
+++ b/internal/config/defaults.go
@@ -54,5 +54,8 @@ var Defaults = Config{
 			Issuer: true,
 		},
 		Scopes: []string{},
+		TokenStore: OAuth2TokenStore{
+			Expires: time.Hour * 8,
+		},
 	},
 }
diff --git a/internal/config/types.go b/internal/config/types.go
index ae40990..b82a813 100644
--- a/internal/config/types.go
+++ b/internal/config/types.go
@@ -56,14 +56,15 @@ type OpenVPNCommonName struct {
 }
 
 type OAuth2 struct {
-	Issuer          *url.URL        `koanf:"issuer"`
-	Provider        string          `koanf:"provider"`
-	AuthorizeParams string          `koanf:"authorize-params"`
-	Endpoints       OAuth2Endpoints `koanf:"endpoint"`
-	Client          OAuth2Client    `koanf:"client"`
-	Scopes          StringSlice     `koanf:"scopes"`
-	Pkce            bool            `koanf:"pkce"`
-	Validate        OAuth2Validate  `koanf:"validate"`
+	Issuer          *url.URL         `koanf:"issuer"`
+	Provider        string           `koanf:"provider"`
+	AuthorizeParams string           `koanf:"authorize-params"`
+	Endpoints       OAuth2Endpoints  `koanf:"endpoint"`
+	Client          OAuth2Client     `koanf:"client"`
+	Scopes          StringSlice      `koanf:"scopes"`
+	Pkce            bool             `koanf:"pkce"`
+	Validate        OAuth2Validate   `koanf:"validate"`
+	TokenStore      OAuth2TokenStore `koanf:"tokenstore"`
 }
 
 type OAuth2Client struct {
@@ -85,6 +86,11 @@ type OAuth2Validate struct {
 	CommonName string      `koanf:"common_name"`
 }
 
+type OAuth2TokenStore struct {
+	Expires time.Duration `koanf:"expires"`
+	Key     Secret        `koanf:"salt"`
+}
+
 type OpenVPNCommonNameMode int
 
 const (
diff --git a/internal/oauth2/handler.go b/internal/oauth2/handler.go
index 3107f91..0442cea 100644
--- a/internal/oauth2/handler.go
+++ b/internal/oauth2/handler.go
@@ -183,6 +183,12 @@ func (provider Provider) oauth2Callback() http.Handler {
 			provider.openvpn.AcceptClient(logger, session.Client)
 		}
 
+		if tokens.RefreshToken != "" {
+			if err = provider.storage.Set(session.Client.Cid, tokens.RefreshToken); err != nil {
+				logger.Warn(err.Error())
+			}
+		}
+
 		writeSuccess(w, provider.conf, logger)
 	}, provider.RelyingParty)
 }
diff --git a/internal/oauth2/provider.go b/internal/oauth2/provider.go
index 4d54d8d..7d46ab2 100644
--- a/internal/oauth2/provider.go
+++ b/internal/oauth2/provider.go
@@ -13,6 +13,7 @@ import (
 	"github.com/jkroepke/openvpn-auth-oauth2/internal/oauth2/providers/generic"
 	"github.com/jkroepke/openvpn-auth-oauth2/internal/oauth2/providers/github"
 	"github.com/jkroepke/openvpn-auth-oauth2/internal/state"
+	"github.com/jkroepke/openvpn-auth-oauth2/internal/storage"
 	"github.com/jkroepke/openvpn-auth-oauth2/internal/utils"
 	"github.com/zitadel/logging"
 	"github.com/zitadel/oidc/v3/pkg/client/rp"
@@ -21,7 +22,7 @@ import (
 )
 
 // NewProvider returns a [Provider] instance.
-func NewProvider(logger *slog.Logger, conf config.Config, openvpnCallback OpenVPN) (Provider, error) {
+func NewProvider(logger *slog.Logger, conf config.Config, storageClient *storage.Storage, openvpnClient OpenVPN) (Provider, error) {
 	provider, err := newOidcProvider(conf)
 	if err != nil {
 		return Provider{}, err
@@ -55,10 +56,10 @@ func NewProvider(logger *slog.Logger, conf config.Config, openvpnCallback OpenVP
 		rp.WithVerifierOpts(rp.WithIssuedAtOffset(5 * time.Second)),
 		rp.WithHTTPClient(&http.Client{Timeout: time.Second * 30, Transport: utils.NewUserAgentTransport(nil)}),
 		rp.WithErrorHandler(func(w http.ResponseWriter, r *http.Request, errorType string, errorDesc string, encryptedSession string) {
-			errorHandler(w, conf, logger, openvpnCallback, http.StatusInternalServerError, errorType, errorDesc, encryptedSession)
+			errorHandler(w, conf, logger, openvpnClient, http.StatusInternalServerError, errorType, errorDesc, encryptedSession)
 		}),
 		rp.WithUnauthorizedHandler(func(w http.ResponseWriter, r *http.Request, desc string, encryptedSession string) {
-			errorHandler(w, conf, logger, openvpnCallback, http.StatusUnauthorized, "Unauthorized", desc, encryptedSession)
+			errorHandler(w, conf, logger, openvpnClient, http.StatusUnauthorized, "Unauthorized", desc, encryptedSession)
 		}),
 	}
 
@@ -126,7 +127,8 @@ func NewProvider(logger *slog.Logger, conf config.Config, openvpnCallback OpenVP
 	return Provider{
 		RelyingParty:    relyingParty,
 		OIDC:            provider,
-		openvpn:         openvpnCallback,
+		openvpn:         openvpnClient,
+		storage:         storageClient,
 		conf:            conf,
 		logger:          logger,
 		authorizeParams: authorizeParams,
diff --git a/internal/oauth2/types.go b/internal/oauth2/types.go
index b0675bb..bd23866 100644
--- a/internal/oauth2/types.go
+++ b/internal/oauth2/types.go
@@ -7,6 +7,7 @@ import (
 	"github.com/jkroepke/openvpn-auth-oauth2/internal/config"
 	"github.com/jkroepke/openvpn-auth-oauth2/internal/oauth2/idtoken"
 	"github.com/jkroepke/openvpn-auth-oauth2/internal/state"
+	"github.com/jkroepke/openvpn-auth-oauth2/internal/storage"
 	"github.com/jkroepke/openvpn-auth-oauth2/internal/types"
 	"github.com/zitadel/oidc/v3/pkg/client/rp"
 	"github.com/zitadel/oidc/v3/pkg/oidc"
@@ -14,8 +15,9 @@ import (
 )
 
 type Provider struct {
-	conf   config.Config
-	logger *slog.Logger
+	conf    config.Config
+	logger  *slog.Logger
+	storage *storage.Storage
 
 	rp.RelyingParty
 	OIDC            oidcProvider
diff --git a/internal/openvpn/main.go b/internal/openvpn/main.go
index 6589b62..4b6dc72 100644
--- a/internal/openvpn/main.go
+++ b/internal/openvpn/main.go
@@ -16,13 +16,15 @@ import (
 	"github.com/jkroepke/openvpn-auth-oauth2/internal/config"
 	"github.com/jkroepke/openvpn-auth-oauth2/internal/openvpn/connection"
 	"github.com/jkroepke/openvpn-auth-oauth2/internal/state"
+	"github.com/jkroepke/openvpn-auth-oauth2/internal/storage"
 	"github.com/jkroepke/openvpn-auth-oauth2/internal/utils"
 )
 
-func NewClient(logger *slog.Logger, conf config.Config) *Client {
+func NewClient(logger *slog.Logger, conf config.Config, stor *storage.Storage) *Client {
 	return &Client{
-		conf:   conf,
-		logger: logger,
+		conf:    conf,
+		storage: stor,
+		logger:  logger,
 
 		closed: false,
 		mu:     sync.Mutex{},
diff --git a/internal/openvpn/types.go b/internal/openvpn/types.go
index b981f09..681f27c 100644
--- a/internal/openvpn/types.go
+++ b/internal/openvpn/types.go
@@ -8,6 +8,7 @@ import (
 
 	"github.com/jkroepke/openvpn-auth-oauth2/internal/config"
 	"github.com/jkroepke/openvpn-auth-oauth2/internal/openvpn/connection"
+	"github.com/jkroepke/openvpn-auth-oauth2/internal/storage"
 )
 
 type Client struct {
@@ -15,6 +16,7 @@ type Client struct {
 	conn    net.Conn
 	scanner *bufio.Scanner
 	logger  *slog.Logger
+	storage *storage.Storage
 
 	mu     sync.Mutex
 	closed bool
diff --git a/internal/storage/errors.go b/internal/storage/errors.go
new file mode 100644
index 0000000..7730b7d
--- /dev/null
+++ b/internal/storage/errors.go
@@ -0,0 +1,5 @@
+package storage
+
+import "errors"
+
+var ErrNotExists = errors.New("value does not exist")
diff --git a/internal/storage/storage.go b/internal/storage/storage.go
new file mode 100644
index 0000000..bf39956
--- /dev/null
+++ b/internal/storage/storage.go
@@ -0,0 +1,80 @@
+package storage
+
+import (
+	"fmt"
+	"sync"
+	"time"
+
+	"github.com/zitadel/oidc/v3/pkg/crypto"
+)
+
+type Storage struct {
+	encryptionKey string
+
+	expires time.Duration
+	data    sync.Map
+}
+
+type item struct {
+	token   string
+	expires time.Time
+}
+
+func New(encryptionKey string, expires time.Duration) *Storage {
+	storage := &Storage{
+		encryptionKey,
+		expires,
+		sync.Map{},
+	}
+	go storage.collect()
+
+	return storage
+}
+
+func (storage *Storage) collect() {
+	for {
+		storage.data.Range(func(client, data any) bool {
+			entry, ok := data.(item)
+			if !ok {
+				panic(data)
+			}
+
+			if entry.expires.Compare(time.Now()) == -1 {
+				storage.data.Delete(client)
+			}
+
+			return true
+		})
+
+		time.Sleep(time.Minute * 5)
+	}
+}
+
+func (storage *Storage) Set(client uint64, token string) error {
+	encryptedToken, err := crypto.EncryptAES(token, storage.encryptionKey)
+	if err != nil {
+		return fmt.Errorf("encrypt error: %w", err)
+	}
+
+	storage.data.Store(client, item{encryptedToken, time.Now().Add(storage.expires)})
+
+	return nil
+}
+
+func (storage *Storage) Get(client uint64) (string, error) {
+	data, ok := storage.data.Load(client)
+	if !ok {
+		return "", ErrNotExists
+	}
+
+	token, err := crypto.DecryptAES(data.(item).token, storage.encryptionKey)
+	if err != nil {
+		return "", fmt.Errorf("decrypt error: %w", err)
+	}
+
+	return token, nil
+}
+
+func (storage *Storage) Delete(client uint64) {
+	storage.data.Delete(client)
+}
diff --git a/pkg/plugin/plugin.go b/pkg/plugin/plugin.go
index 142e6dd..8e02c79 100644
--- a/pkg/plugin/plugin.go
+++ b/pkg/plugin/plugin.go
@@ -20,6 +20,7 @@ import (
 	"github.com/jkroepke/openvpn-auth-oauth2/internal/http"
 	"github.com/jkroepke/openvpn-auth-oauth2/internal/oauth2"
 	"github.com/jkroepke/openvpn-auth-oauth2/internal/state"
+	"github.com/jkroepke/openvpn-auth-oauth2/internal/storage"
 	"github.com/jkroepke/openvpn-auth-oauth2/internal/utils"
 )
 
@@ -74,7 +75,8 @@ func openvpn_plugin_open_v3_go(v3structver C.int, args *C.struct_openvpn_plugin_
 		conf:   conf,
 	}
 
-	provider, err := oauth2.NewProvider(logger, conf, handle)
+	storageClient := storage.New(conf.OAuth2.TokenStore.Key.String(), conf.OAuth2.TokenStore.Expires)
+	provider, err := oauth2.NewProvider(logger, conf, storageClient, handle)
 	if err != nil {
 		logger.Error(err.Error())
 


Test patch:
diff --git a/internal/oauth2/handler_test.go b/internal/oauth2/handler_test.go
index d5fcc45..50deeba 100644
--- a/internal/oauth2/handler_test.go
+++ b/internal/oauth2/handler_test.go
@@ -20,6 +20,7 @@ import (
 	"github.com/jkroepke/openvpn-auth-oauth2/internal/oauth2"
 	"github.com/jkroepke/openvpn-auth-oauth2/internal/openvpn"
 	"github.com/jkroepke/openvpn-auth-oauth2/internal/state"
+	"github.com/jkroepke/openvpn-auth-oauth2/internal/storage"
 	"github.com/jkroepke/openvpn-auth-oauth2/pkg/testutils"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
@@ -317,10 +318,12 @@ func TestHandler(t *testing.T) {
 			tt.conf.HTTP.BaseURL = &url.URL{Scheme: "http", Host: clientListener.Addr().String()}
 			tt.conf.OpenVpn.Addr = &url.URL{Scheme: managementInterface.Addr().Network(), Host: managementInterface.Addr().String()}
 
-			client := openvpn.NewClient(logger, tt.conf)
+			storageClient := storage.New("0123456789101112", time.Hour)
+
+			client := openvpn.NewClient(logger, tt.conf, storageClient)
 			defer client.Shutdown()
 
-			provider, err := oauth2.NewProvider(logger, tt.conf, client)
+			provider, err := oauth2.NewProvider(logger, tt.conf, storageClient, client)
 			require.NoError(t, err)
 
 			httpClientListener := httptest.NewUnstartedServer(provider.Handler())
diff --git a/internal/oauth2/provider_test.go b/internal/oauth2/provider_test.go
index bbca164..4ba1c3d 100644
--- a/internal/oauth2/provider_test.go
+++ b/internal/oauth2/provider_test.go
@@ -7,15 +7,17 @@ import (
 	"net/url"
 	"strings"
 	"testing"
+	"time"
 
 	"github.com/jkroepke/openvpn-auth-oauth2/internal/config"
 	"github.com/jkroepke/openvpn-auth-oauth2/internal/oauth2"
 	"github.com/jkroepke/openvpn-auth-oauth2/internal/oauth2/providers/generic"
 	"github.com/jkroepke/openvpn-auth-oauth2/internal/openvpn"
+	"github.com/jkroepke/openvpn-auth-oauth2/internal/storage"
 	"github.com/jkroepke/openvpn-auth-oauth2/pkg/testutils"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
-	"github.com/zitadel/oidc/v3/example/server/storage"
+	serverStorage "github.com/zitadel/oidc/v3/example/server/storage"
 	"github.com/zitadel/oidc/v3/pkg/op"
 	"golang.org/x/text/language"
 )
@@ -23,7 +25,7 @@ import (
 func TestNewProvider(t *testing.T) {
 	t.Parallel()
 
-	opStorage := storage.NewStorageWithClients(storage.NewUserStore("http://localhost"), map[string]*storage.Client{})
+	opStorage := serverStorage.NewStorageWithClients(serverStorage.NewUserStore("http://localhost"), map[string]*serverStorage.Client{})
 	opConfig := &op.Config{
 		CryptoKey:                sha256.Sum256([]byte("test")),
 		DefaultLogoutRedirectURI: "/",
@@ -155,10 +157,12 @@ func TestNewProvider(t *testing.T) {
 			defer managementInterface.Close()
 			tt.conf.OpenVpn.Addr = &url.URL{Scheme: managementInterface.Addr().Network(), Host: managementInterface.Addr().String()}
 
-			client := openvpn.NewClient(logger, tt.conf)
+			storageClient := storage.New("0123456789101112", time.Hour)
+
+			client := openvpn.NewClient(logger, tt.conf, storageClient)
 			defer client.Shutdown()
 
-			provider, err := oauth2.NewProvider(logger, tt.conf, client)
+			provider, err := oauth2.NewProvider(logger, tt.conf, storageClient, client)
 			if tt.err != "" {
 				require.Error(t, err)
 				assert.Equal(t, strings.TrimSpace(err.Error()), tt.err)
diff --git a/internal/openvpn/main_test.go b/internal/openvpn/main_test.go
index 70fd88d..3f9c2be 100644
--- a/internal/openvpn/main_test.go
+++ b/internal/openvpn/main_test.go
@@ -9,10 +9,12 @@ import (
 	"strings"
 	"sync"
 	"testing"
+	"time"
 
 	"github.com/jkroepke/openvpn-auth-oauth2/internal/config"
 	"github.com/jkroepke/openvpn-auth-oauth2/internal/openvpn"
 	"github.com/jkroepke/openvpn-auth-oauth2/internal/state"
+	"github.com/jkroepke/openvpn-auth-oauth2/internal/storage"
 	"github.com/jkroepke/openvpn-auth-oauth2/pkg/testutils"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
@@ -32,7 +34,8 @@ func TestClientInvalidServer(t *testing.T) {
 			Bypass: config.OpenVpnBypass{CommonNames: make([]string, 0)},
 		},
 	}
-	client := openvpn.NewClient(logger, conf)
+	storageClient := storage.New("0123456789101112", time.Hour)
+	client := openvpn.NewClient(logger, conf, storageClient)
 	err := client.Connect()
 	require.Error(t, err)
 	assert.Equal(t, "unable to connect to openvpn management interface tcp://0.0.0.0:1: dial tcp 0.0.0.0:1: connect: connection refused", err.Error())
@@ -190,7 +193,9 @@ func TestClientFull(t *testing.T) {
 
 			tt.conf.OpenVpn.Addr = &url.URL{Scheme: l.Addr().Network(), Host: l.Addr().String()}
 
-			client := openvpn.NewClient(logger, tt.conf)
+			storageClient := storage.New("0123456789101112", time.Hour)
+			client := openvpn.NewClient(logger, tt.conf, storageClient)
+
 			wg := sync.WaitGroup{}
 			wg.Add(1)
 
@@ -289,7 +294,8 @@ func TestClientInvalidPassword(t *testing.T) {
 		},
 	}
 
-	client := openvpn.NewClient(logger, conf)
+	storageClient := storage.New("0123456789101112", time.Hour)
+	client := openvpn.NewClient(logger, conf, storageClient)
 
 	go func() {
 		conn, err := l.Accept()
@@ -360,7 +366,8 @@ func TestClientInvalidVersion(t *testing.T) {
 
 			conf.OpenVpn.Addr = &url.URL{Scheme: l.Addr().Network(), Host: l.Addr().String()}
 
-			client := openvpn.NewClient(logger, conf)
+			storageClient := storage.New("0123456789101112", time.Hour)
+			client := openvpn.NewClient(logger, conf, storageClient)
 
 			go func() {
 				conn, err := l.Accept()