Skip to content

Commit

Permalink
Improvements (#15)
Browse files Browse the repository at this point in the history
  • Loading branch information
jkroepke authored Sep 17, 2023
1 parent e627e7a commit 6b81ed1
Show file tree
Hide file tree
Showing 11 changed files with 289 additions and 137 deletions.
2 changes: 1 addition & 1 deletion .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@ insert_final_newline = true
charset = utf-8
trim_trailing_whitespace = true

[*.py]
[*.go]
max_line_length = 120
18 changes: 13 additions & 5 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,14 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [1.0.0] - 2023-09-16
## [1.2.0] - 2023-09-16

### Added

- Option to define custom OIDC discovery, auth and token url
- Option to define custom callback HTML template which is parsed by go template engine

## [1.1.0] - 2023-09-16

### Added

Expand All @@ -20,7 +27,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

- First release

[Unreleased]: https://github.com/jkroepke/openvpn-auth-azure-ad/compare/v1.1.0...HEAD
[1.1.0]: https://github.com/jkroepke/openvpn-auth-azure-ad/releases/tag/v1.1.0
[1.0.0]: https://github.com/jkroepke/openvpn-auth-azure-ad/releases/tag/v1.0.0
[0.1.0]: https://github.com/jkroepke/openvpn-auth-azure-ad/releases/tag/v0.1.0
[Unreleased]: https://github.com/jkroepke/openvpn-auth-oauth2/compare/v1.2.0...HEAD
[1.2.0]: https://github.com/jkroepke/openvpn-auth-oauth2/releases/tag/v1.2.0
[1.1.0]: https://github.com/jkroepke/openvpn-auth-oauth2/releases/tag/v1.1.0
[1.0.0]: https://github.com/jkroepke/openvpn-auth-oauth2/releases/tag/v1.0.0
[0.1.0]: https://github.com/jkroepke/openvpn-auth-oauth2/releases/tag/v0.1.0
42 changes: 26 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,22 +85,32 @@ None

```
Usage of openvpn-auth-oauth2:
--configfile string path to one .yaml config files. (env: CONFIG_CONFIGFILE)
--http.baseurl string listen addr for client listener. (env: CONFIG_HTTP_BASEURL) (default "http://localhost:9000")
--http.cert string Path to tls server certificate. (env: CONFIG_HTTP_CERT)
--http.key string Path to tls server key. (env: CONFIG_HTTP_KEY)
--http.listen string listen addr for client listener. (env: CONFIG_HTTP_LISTEN) (default ":9000")
--http.sessionsecret string Secret crypt session tokens. (env: CONFIG_HTTP_SESSIONSECRET)
--http.tls enable TLS listener. (env: CONFIG_HTTP_TLS)
--oauth2.client.id string oauth2 client id. (env: CONFIG_OAUTH2_CLIENT_ID)
--oauth2.client.secret string oauth2 client secret. (env: CONFIG_OAUTH2_CLIENT_SECRET)
--oauth2.issuer string oauth2 issuer. (env: CONFIG_OAUTH2_ISSUER)
--oauth2.scopes strings oauth2 token scopes. (env: CONFIG_OAUTH2_SCOPES) (default [openid,offline_access])
--oauth2.validate.groups strings oauth2 required user groups. (env: CONFIG_OAUTH2_VALIDATE_GROUPS)
--oauth2.validate.ipaddr validate client ipaddr between VPN and OIDC token. (env: CONFIG_OAUTH2_VALIDATE_IPADDR)
--oauth2.validate.roles strings oauth2 required user roles. (env: CONFIG_OAUTH2_VALIDATE_ROLES)
--openvpn.addr string openvpn management interface addr. (env: CONFIG_OPENVPN_ADDR) (default "127.0.0.1:54321")
--openvpn.password string openvpn management interface password. (env: CONFIG_OPENVPN_PASSWORD)
--configfile string path to one .yaml config files. (env: CONFIG_CONFIGFILE)
--http.baseUrl string listen addr for client listener. (env: CONFIG_HTTP_BASEURL) (default "http://localhost:9000")
--http.callbackTemplatePath string Path to a HTML file which is displayed at the end of the screen. (env: CONFIG_HTTP_CALLBACKTEMPLATEPATH)
--http.cert string Path to tls server certificate. (env: CONFIG_HTTP_CERT)
--http.key string Path to tls server key. (env: CONFIG_HTTP_KEY)
--http.listen string listen addr for client listener. (env: CONFIG_HTTP_LISTEN) (default ":9000")
--http.secret string Cookie secret. (env: CONFIG_HTTP_SECRET)
--http.tls enable TLS listener. (env: CONFIG_HTTP_TLS)
--log.format string log format. json or console (env: CONFIG_LOG_FORMAT) (default "json")
--log.level string log level. (env: CONFIG_LOG_LEVEL) (default "info")
--oauth2.bypass.cn strings bypass oauth authentication for CNs. (env: CONFIG_OAUTH2_BYPASS_CN)
--oauth2.client.id string oauth2 client id. (env: CONFIG_OAUTH2_CLIENT_ID)
--oauth2.client.secret string oauth2 client secret. (env: CONFIG_OAUTH2_CLIENT_SECRET)
--oauth2.discoveryUrl string custom oauth2 discovery url. (env: CONFIG_OAUTH2_DISCOVERY)
--oauth2.endpoint.authUrl string custom oauth2 auth endpoint. (env: CONFIG_OAUTH2_ENDPOINT_AUTH_URL)
--oauth2.endpoint.discovery string custom oauth2 discovery url. (env: CONFIG_OAUTH2_ENDPOINT_DISCOVERY)
--oauth2.endpoint.tokenUrl string custom oauth2 token endpoint. (env: CONFIG_OAUTH2_ENDPOINT_TOKEN_URL)
--oauth2.issuer string oauth2 issuer. (env: CONFIG_OAUTH2_ISSUER)
--oauth2.scopes strings oauth2 token scopes. (env: CONFIG_OAUTH2_SCOPES) (default [openid,profile])
--oauth2.validate.common_name string validate common_name from OpenVPN with IDToken claim. (env: CONFIG_OAUTH2_VALIDATE_COMMON_NAME)
--oauth2.validate.groups strings oauth2 required user groups. (env: CONFIG_OAUTH2_VALIDATE_GROUPS)
--oauth2.validate.ipaddr validate client ipaddr between VPN and OIDC token. (env: CONFIG_OAUTH2_VALIDATE_IPADDR)
--oauth2.validate.issuer validate issuer from oidc discovery. (env: CONFIG_OAUTH2_VALIDATE_ISSUER) (default true)
--oauth2.validate.roles strings oauth2 required user roles. (env: CONFIG_OAUTH2_VALIDATE_ROLES)
--openvpn.addr string openvpn management interface addr. (env: CONFIG_OPENVPN_ADDR) (default "tcp://127.0.0.1:54321")
--openvpn.password string openvpn management interface password. (env: CONFIG_OPENVPN_PASSWORD)
```

# Related projects
Expand Down
21 changes: 14 additions & 7 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"log/slog"
"net/http"
"os"
"runtime"
"strings"

"github.com/jkroepke/openvpn-auth-oauth2/internal/config"
Expand All @@ -21,7 +22,7 @@ import (

var k = koanf.New(".")

func Execute() {
func Execute(version, commit, date string) {
logger, _ := zap.NewProduction()
defer logger.Sync() //nolint:errcheck

Expand All @@ -30,6 +31,11 @@ func Execute() {
logger.Fatal(fmt.Sprintf("error parsing cli args: %v", err))
}

if versionFlag, _ := f.GetBool("version"); versionFlag {
fmt.Printf("version: %s\ncommit: %s\ndate: %s\ngo: %s\n", version, commit, date, runtime.Version())
os.Exit(0)
}

configFile, _ := f.GetString("configfile")
if configFile != "" {
if err := k.Load(file.Provider(configFile), yaml.Parser()); err != nil {
Expand All @@ -56,14 +62,10 @@ func Execute() {
}

var conf config.Config
if err := k.UnmarshalWithConf("", &conf, koanf.UnmarshalConf{}); err != nil {
if err := k.Unmarshal("", &conf); err != nil {
logger.Fatal(fmt.Sprintf("error loading config: %v", err))
}

if err := config.Validate(&conf); err != nil {
logger.Fatal(fmt.Sprintf("error validating config: %v", err))
}

logger, err := configureLogger(&conf)
if err != nil {
logger.Fatal(fmt.Sprintf("error configure logger: %v", err))
Expand All @@ -72,7 +74,12 @@ func Execute() {

sl := slog.New(zapslog.NewHandler(logger.Core(), nil))

oidcClient, err := oauth2.Configure(&conf)
if err := config.Validate(&conf); err != nil {
sl.Error(fmt.Sprintf("error validating config: %v", err))
os.Exit(1)
}

oidcClient, err := oauth2.NewProvider(sl, &conf)
if err != nil {
sl.Error(err.Error())
os.Exit(1)
Expand Down
110 changes: 84 additions & 26 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package config
import (
"errors"
"fmt"
"html/template"
"net/url"
"os"
"slices"

Expand All @@ -18,12 +20,14 @@ type Config struct {
}

type Http struct {
Listen string `koanf:"listen"`
CertFile string `koanf:"cert"`
KeyFile string `koanf:"key"`
Tls bool `koanf:"tls"`
BaseUrl string `koanf:"baseurl"`
SessionSecret string `koanf:"sessionsecret"`
Listen string `koanf:"listen"`
CertFile string `koanf:"cert"`
KeyFile string `koanf:"key"`
Tls bool `koanf:"tls"`
BaseUrl string `koanf:"baseUrl"`
Secret string `koanf:"secret"`
CallbackTemplate *template.Template `koanf:"callbackTemplate"`
CallbackTemplatePath string `koanf:"callbackTemplatePath"`
}

type Log struct {
Expand All @@ -42,30 +46,37 @@ type OpenVpnBypass struct {
}

type OAuth2 struct {
Issuer string `koanf:"issuer"`
Client OAuth2Client `koanf:"client"`
Scopes []string `koanf:"scopes"`
Pkce bool `koanf:"pkce"`
Validate OAuth2Validate `koanf:"validate"`
Issuer string `koanf:"issuer"`
Endpoints OAuth2Endpoints `koanf:"endpoint"`
Client OAuth2Client `koanf:"client"`
Scopes []string `koanf:"scopes"`
Pkce bool `koanf:"pkce"`
Validate OAuth2Validate `koanf:"validate"`
}

type OAuth2Client struct {
Id string `koanf:"id"`
Secret string `koanf:"secret"`
}

type OAuth2Endpoints struct {
DiscoveryUrl string `koanf:"discoveryUrl"`
AuthUrl string `koanf:"authUrl"`
TokenUrl string `koanf:"tokenUrl"`
}

type OAuth2Validate struct {
Groups []string `koanf:"groups"`
Roles []string `koanf:"roles"`
IpAddr bool `koanf:"ipaddr"`
Issuer bool `koanf:"issuer"`
CommonName string `koanf:"commonname"`
CommonName string `koanf:"common_name"`
}

func FlagSet() *flag.FlagSet {
f := flag.NewFlagSet("openvpn-auth-oauth2", flag.ContinueOnError)
f.Usage = func() {
fmt.Fprintln(os.Stderr, "Usage of openvpn-auth-oauth2:")
_, _ = fmt.Fprintln(os.Stderr, "Usage of openvpn-auth-oauth2:")
f.PrintDefaults()
os.Exit(0)
}
Expand All @@ -75,41 +86,88 @@ func FlagSet() *flag.FlagSet {
f.String("log.level", "info", "log level. (env: CONFIG_LOG_LEVEL)")
f.String("http.listen", ":9000", "listen addr for client listener. (env: CONFIG_HTTP_LISTEN)")
f.Bool("http.tls", false, "enable TLS listener. (env: CONFIG_HTTP_TLS)")
f.String("http.baseurl", "http://localhost:9000", "listen addr for client listener. (env: CONFIG_HTTP_BASEURL)")
f.String("http.sessionsecret", "", "Secret crypt session tokens. (env: CONFIG_HTTP_SESSIONSECRET)")
f.String("http.baseUrl", "http://localhost:9000", "listen addr for client listener. (env: CONFIG_HTTP_BASEURL)")
f.String("http.secret", "", "Cookie secret. (env: CONFIG_HTTP_SECRET)")
f.String("http.key", "", "Path to tls server key. (env: CONFIG_HTTP_KEY)")
f.String("http.cert", "", "Path to tls server certificate. (env: CONFIG_HTTP_CERT)")
f.String("openvpn.addr", "127.0.0.1:54321", "openvpn management interface addr. (env: CONFIG_OPENVPN_ADDR)")
f.String("http.callbackTemplatePath", "", "Path to a HTML file which is displayed at the end of the screen. (env: CONFIG_HTTP_CALLBACKTEMPLATEPATH)")
f.String("openvpn.addr", "tcp://127.0.0.1:54321", "openvpn management interface addr. (env: CONFIG_OPENVPN_ADDR)")
f.String("openvpn.password", "", "openvpn management interface password. (env: CONFIG_OPENVPN_PASSWORD)")
f.StringSlice("oauth2.bypass.cn", []string{}, "bypass oauth authentication for CNs. (env: CONFIG_OAUTH2_BYPASS_CN)")
f.String("oauth2.issuer", "", "oauth2 issuer. (env: CONFIG_OAUTH2_ISSUER)")
f.String("oauth2.endpoint.discovery", "", "custom oauth2 discovery url. (env: CONFIG_OAUTH2_ENDPOINT_DISCOVERY)")
f.String("oauth2.endpoint.authUrl", "", "custom oauth2 auth endpoint. (env: CONFIG_OAUTH2_ENDPOINT_AUTH_URL)")
f.String("oauth2.endpoint.tokenUrl", "", "custom oauth2 token endpoint. (env: CONFIG_OAUTH2_ENDPOINT_TOKEN_URL)")
f.String("oauth2.discoveryUrl", "", "custom oauth2 discovery url. (env: CONFIG_OAUTH2_DISCOVERY)")
f.String("oauth2.client.id", "", "oauth2 client id. (env: CONFIG_OAUTH2_CLIENT_ID)")
f.String("oauth2.client.secret", "", "oauth2 client secret. (env: CONFIG_OAUTH2_CLIENT_SECRET)")
f.StringSlice("oauth2.validate.groups", []string{}, "oauth2 required user groups. (env: CONFIG_OAUTH2_VALIDATE_GROUPS)")
f.StringSlice("oauth2.validate.roles", []string{}, "oauth2 required user roles. (env: CONFIG_OAUTH2_VALIDATE_ROLES)")
f.Bool("oauth2.validate.ipaddr", false, "validate client ipaddr between VPN and OIDC token. (env: CONFIG_OAUTH2_VALIDATE_IPADDR)")
f.Bool("oauth2.validate.issuer", true, "validate issuer from oidc discovery. (env: CONFIG_OAUTH2_VALIDATE_ISSUER)")
f.String("oauth2.validate.commonname", "", "validate common_name from OpenVPN with IDToken claim. (env: CONFIG_OAUTH2_VALIDATE_COMMONNAME)")
f.StringSlice("oauth2.scopes", []string{"openid", "profile", "offline_access"}, "oauth2 token scopes. (env: CONFIG_OAUTH2_SCOPES)")
f.String("oauth2.validate.common_name", "", "validate common_name from OpenVPN with IDToken claim. (env: CONFIG_OAUTH2_VALIDATE_COMMON_NAME)")
f.StringSlice("oauth2.scopes", []string{"openid", "profile"}, "oauth2 token scopes. (env: CONFIG_OAUTH2_SCOPES)")
f.Bool("version", false, "shows versions")

return f
}

func Validate(conf *Config) error {
if conf.Http.SessionSecret == "" {
return errors.New("http.sessionsecret is required")
for key, value := range map[string]string{
"http.baseUrl": conf.Http.BaseUrl,
"http.secret": conf.Http.Secret,
"oauth2.issuer": conf.Oauth2.Issuer,
"oauth2.client.id": conf.Oauth2.Client.Id,
} {
if value == "" {
return fmt.Errorf("%s is required", key)
}
}

if !slices.Contains([]int{16, 24, 32}, len(conf.Http.Secret)) {
return errors.New("http.secret requires a length of 16, 24 or 32")
}

if !slices.Contains([]int{16, 24, 32}, len(conf.Http.SessionSecret)) {
return errors.New("http.sessionsecret requires a length of 16, 24 or 32")
if uri, err := url.Parse(conf.OpenVpn.Addr); err != nil {
return fmt.Errorf("openvpn.addr: invalid URL. error: %s", err)
} else if !slices.Contains([]string{"tcp", "unix"}, uri.Scheme) {
return errors.New("openvpn.addr: invalid URL. only tcp://addr or unix://addr scheme supported")
}

if conf.Oauth2.Issuer == "" {
return errors.New("oauth2.issuer is required")
for key, value := range map[string]string{
"http.baseUrl": conf.Http.BaseUrl,
"oauth2.issuer": conf.Oauth2.Issuer,
"oauth2.endpoint.discoveryUrl": conf.Oauth2.Endpoints.DiscoveryUrl,
"oauth2.endpoint.tokenUrl": conf.Oauth2.Endpoints.TokenUrl,
"oauth2.endpoint.authUrl": conf.Oauth2.Endpoints.AuthUrl,
} {
if value == "" {
continue
}

uri, err := url.Parse(value)

if err != nil {
return fmt.Errorf("%s: invalid URL. error: %s", key, err)
}

if !slices.Contains([]string{"http", "https"}, uri.Scheme) {
return fmt.Errorf("%s: invalid URL. only http:// or https:// scheme supported", key)
}
}

if conf.Oauth2.Client.Id == "" {
return errors.New("oauth2.client.id is required")
if (conf.Oauth2.Endpoints.TokenUrl != "" && conf.Oauth2.Endpoints.AuthUrl == "") ||
(conf.Oauth2.Endpoints.TokenUrl == "" && conf.Oauth2.Endpoints.AuthUrl != "") {
return errors.New("both oauth2.endpoints.tokenUrl and oauth2.endpoints.authUrl are required")
}

if conf.Http.CallbackTemplatePath != "" {
tmpl, err := template.New("callback").ParseFiles(conf.Http.CallbackTemplatePath)
if err != nil {
return fmt.Errorf("http.callbackTemplatePath: invalid template: %s", err)
}

conf.Http.CallbackTemplate = tmpl
}

return nil
Expand Down
21 changes: 16 additions & 5 deletions internal/oauth2/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import (
"fmt"
"log/slog"
"net/http"
"net/url"
"strings"

"github.com/jkroepke/openvpn-auth-oauth2/internal/config"
"github.com/jkroepke/openvpn-auth-oauth2/internal/openvpn"
Expand All @@ -13,9 +15,12 @@ import (
)

func Handler(logger *slog.Logger, oidcClient *rp.RelyingParty, conf *config.Config, openvpnClient *openvpn.Client) *http.ServeMux {
baseUrl, _ := url.Parse(conf.Http.BaseUrl)

mux := http.NewServeMux()
mux.Handle("/oauth2/start", oauth2Start(logger, oidcClient, conf))
mux.Handle("/oauth2/callback", oauth2Callback(logger, oidcClient, conf, openvpnClient))
mux.Handle("/", http.NotFoundHandler())
mux.Handle(strings.TrimSuffix(baseUrl.Path, "/")+"/oauth2/start", oauth2Start(logger, oidcClient, conf))
mux.Handle(strings.TrimSuffix(baseUrl.Path, "/")+"/oauth2/callback", oauth2Callback(logger, oidcClient, conf, openvpnClient))

return mux
}
Expand All @@ -29,7 +34,7 @@ func oauth2Start(logger *slog.Logger, oidcClient *rp.RelyingParty, conf *config.
}

session := state.NewEncoded(sessionState)
if err := session.Decode(conf.Http.SessionSecret); err != nil {
if err := session.Decode(conf.Http.Secret); err != nil {
logger.Warn(fmt.Sprintf("invalid state: %s", sessionState))
w.WriteHeader(http.StatusBadRequest)
return
Expand All @@ -51,7 +56,7 @@ func oauth2Callback(logger *slog.Logger, oidcClient *rp.RelyingParty, conf *conf

return rp.CodeExchangeHandler(func(w http.ResponseWriter, r *http.Request, tokens *oidc.Tokens[*oidc.IDTokenClaims], encryptedState string, rp rp.RelyingParty) {
session := state.NewEncoded(encryptedState)
if err := session.Decode(conf.Http.SessionSecret); err != nil {
if err := session.Decode(conf.Http.Secret); err != nil {
logger.Warn(err.Error(),
"subject", tokens.IDTokenClaims.Subject,
"preferred_username", tokens.IDTokenClaims.PreferredUsername,
Expand Down Expand Up @@ -105,6 +110,12 @@ func oauth2Callback(logger *slog.Logger, oidcClient *rp.RelyingParty, conf *conf
openvpnClient.SendCommand("client-auth %s %s\npush \"auth-token-user %s\"\npush \"auth-token %s\"\nEND", ids[0], ids[1], username, tokens.RefreshToken)
}
*/
_, _ = w.Write([]byte(callbackHtml))

if conf.Http.CallbackTemplate == nil {
_, _ = w.Write([]byte(callbackHtml))
} else if err := conf.Http.CallbackTemplate.Execute(w, map[string]string{}); err != nil {
logger.Error("executing template:", err)
w.WriteHeader(http.StatusInternalServerError)
}
}, *oidcClient)
}
Loading

0 comments on commit 6b81ed1

Please sign in to comment.