diff --git a/cmd/tokenserver/.gitignore b/cmd/tokenserver/.gitignore new file mode 100644 index 0000000..aece56d --- /dev/null +++ b/cmd/tokenserver/.gitignore @@ -0,0 +1,2 @@ +tokenserver +settings.json diff --git a/cmd/tokenserver/tokenserver.go b/cmd/tokenserver/tokenserver.go new file mode 100644 index 0000000..0290d8f --- /dev/null +++ b/cmd/tokenserver/tokenserver.go @@ -0,0 +1,187 @@ +package main + +import ( + "context" + "encoding/base64" + "encoding/json" + "fmt" + "html/template" + "log" + "net/http" + "net/url" + "os" + "strings" + + "github.com/Jille/convreq" + "github.com/Jille/convreq/respond" + "github.com/Jille/rufs/security" + "github.com/coreos/go-oidc/v3/oidc" + "golang.org/x/oauth2" +) + +var ( + ca *security.CAKeyPair + + oidcProvider *oidc.Provider + oauthConfig oauth2.Config +) + +type Config struct { + Certdir string `json:"certdir"` + OIDCProvider string `json:"oidc_provider"` + ClientID string `json:"client_id"` + ClientSecret string `json:"client_secret"` + ReturnURL string `json:"return_url"` + Port int `json:"port"` +} + +func main() { + if len(os.Args) != 2 { + log.Fatalf("Usage: %s ", os.Args[0]) + } + c, err := os.ReadFile(os.Args[1]) + if err != nil { + log.Fatalf("Failed to read config file %q: %v", os.Args[1], err) + } + var cfg Config + if err := json.Unmarshal(c, &cfg); err != nil { + log.Fatalf("Failed to read config file %q: %v", os.Args[1], err) + } + + if cfg.Certdir == "" { + log.Fatalf("Configuration setting \"certdir\" is empty") + } + if cfg.OIDCProvider == "" { + log.Fatalf("Configuration setting \"oidc_provider\" is empty") + } + if cfg.ClientID == "" { + log.Fatalf("Configuration setting \"client_id\" is empty") + } + if cfg.ClientSecret == "" { + log.Fatalf("Configuration setting \"client_secret\" is empty") + } + if cfg.ReturnURL == "" { + log.Fatalf("Configuration setting \"return_url\" is empty") + } + if cfg.Port == 0 { + log.Fatalf("Configuration setting \"port\" is not set") + } + + ca, err = security.LoadCAKeyPair(cfg.Certdir) + if err != nil { + log.Fatalf("Failed to load CA key pair: %v", err) + } + + oidcProvider, err = oidc.NewProvider(context.Background(), cfg.OIDCProvider) + if err != nil { + log.Fatalf("Failed to discover openid connect provider: %v", err) + } + oauthConfig = oauth2.Config{ + ClientID: cfg.ClientID, + ClientSecret: cfg.ClientSecret, + Endpoint: oidcProvider.Endpoint(), + RedirectURL: cfg.ReturnURL, + Scopes: []string{"openid"}, + } + + http.Handle("/oauth2_return", convreq.Wrap(oauth2Return)) + http.Handle("/request_token", convreq.Wrap(requestToken)) + http.Handle("/", convreq.Wrap(mainPage)) + log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", cfg.Port), nil)) +} + +type requestTokenGet struct { + Hostname string `schema:"hostname"` + ReturnURL string `schema:"return_url"` +} + +func requestToken(get requestTokenGet) convreq.HttpResponse { + s := base64.URLEncoding.EncodeToString([]byte(get.Hostname)) + ":" + base64.URLEncoding.EncodeToString([]byte(get.ReturnURL)) + u := oauthConfig.AuthCodeURL(s) + return respond.Redirect(302, u) +} + +type oauth2ReturnGet struct { + Code string `schema:"code"` + State string `schema:"state"` +} + +func oauth2Return(ctx context.Context, get oauth2ReturnGet) convreq.HttpResponse { + t, err := oauthConfig.Exchange(ctx, get.Code) + if err != nil { + return respond.BadRequest("Oauth failed: " + err.Error()) + } + userInfo, err := oidcProvider.UserInfo(ctx, oauth2.StaticTokenSource(t)) + if err != nil { + return respond.Error(err) + } + var ui UserInfo + if err := userInfo.Claims(&ui); err != nil { + return respond.Error(err) + } + + sp := strings.Split(get.State, ":") + hostname, err := base64.URLEncoding.DecodeString(sp[0]) + if err != nil { + return respond.BadRequest("bad state") + } + returnURL, err := base64.URLEncoding.DecodeString(sp[1]) + if err != nil { + return respond.BadRequest("bad state") + } + + username := strings.Split(ui.PreferredUsername, "@")[0] + username = strings.ReplaceAll(username, "-", "") + username += "-" + string(hostname) + + token := ca.CreateToken(username) + + u, err := url.Parse(string(returnURL)) + if err != nil { + return respond.BadRequest("Invalid return_url: " + err.Error()) + } + q := u.Query() + q.Set("username", username) + q.Set("token", token) + q.Set("circle", ca.Name()) + q.Set("fingerprint", ca.Fingerprint()) + + if len(returnURL) == 0 { + var ret string + for k, vs := range q { + ret += k + ":" + vs[0] + "\n" + } + return respond.String(ret) + } + + u.RawQuery = q.Encode() + return respond.Redirect(302, u.String()) +} + +type UserInfo struct { + Name string `json:"name"` + PreferredUsername string `json:"preferred_username"` +} + +type mainPageGet struct { + Hostname string `schema:"hostname"` + ReturnURL string `schema:"return_url"` +} + +var mainPageTpl = template.Must(template.New("").Parse(` + + + + +
+ Hostname: + + +
+ + + `)) + +func mainPage(get mainPageGet) convreq.HttpResponse { + return respond.RenderTemplate(mainPageTpl, get) +} diff --git a/go.mod b/go.mod index 579d335..6bd8917 100644 --- a/go.mod +++ b/go.mod @@ -15,6 +15,7 @@ require ( github.com/Jille/rpcz v0.3.0 github.com/billziss-gh/cgofuse v1.5.0 github.com/cenkalti/backoff/v4 v4.3.0 + github.com/coreos/go-oidc/v3 v3.11.0 github.com/getlantern/systray v1.2.2 github.com/go-git/go-billy/v5 v5.5.0 github.com/golang/protobuf v1.5.4 @@ -27,6 +28,7 @@ require ( github.com/prometheus/client_golang v1.12.2 github.com/stoewer/go-strcase v1.3.0 github.com/yookoala/realpath v1.0.0 + golang.org/x/oauth2 v0.21.0 golang.org/x/sync v0.8.0 google.golang.org/grpc v1.65.0 google.golang.org/protobuf v1.34.2 @@ -44,6 +46,7 @@ require ( github.com/getlantern/hex v0.0.0-20220104173244-ad7e4b9194dc // indirect github.com/getlantern/hidden v0.0.0-20220104173330-f221c5a24770 // indirect github.com/getlantern/ops v0.0.0-20231025133620-f368ab734534 // indirect + github.com/go-jose/go-jose/v4 v4.0.2 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-stack/stack v1.8.1 // indirect @@ -60,6 +63,7 @@ require ( go.opentelemetry.io/otel/trace v1.28.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect + golang.org/x/crypto v0.26.0 // indirect golang.org/x/net v0.28.0 // indirect golang.org/x/sys v0.24.0 // indirect golang.org/x/text v0.17.0 // indirect diff --git a/go.sum b/go.sum index 2fc9b7e..17881b9 100644 --- a/go.sum +++ b/go.sum @@ -82,6 +82,8 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/coreos/go-oidc/v3 v3.11.0 h1:Ia3MxdwpSw702YW0xgfmP1GVCMA9aEFWu12XUZ3/OtI= +github.com/coreos/go-oidc/v3 v3.11.0/go.mod h1:gE3LgjOgFoHi9a4ce4/tJczr0Ai2/BoDhf0r5lltWI0= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/cyphar/filepath-securejoin v0.3.1 h1:1V7cHiaW+C+39wEfpH6XlLBQo3j/PciWFrgfCLS8XrE= github.com/cyphar/filepath-securejoin v0.3.1/go.mod h1:F7i41x/9cBF7lzCrVsYs9fuzwRZm4NQsGTBdpp6mETc= @@ -125,6 +127,8 @@ github.com/go-git/go-billy/v5 v5.5.0/go.mod h1:hmexnoNsr2SJU1Ju67OaNz5ASJY3+sHgF github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-jose/go-jose/v4 v4.0.2 h1:R3l3kkBds16bO7ZFAEEcofK0MkrAJt3jlJznWZG0nvk= +github.com/go-jose/go-jose/v4 v4.0.2/go.mod h1:WVf9LFMHh/QVrmqrOfqun0C45tMe3RoiKJMPvgWwLfY= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= @@ -353,6 +357,8 @@ golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= +golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -425,6 +431,8 @@ golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4Iltr golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= +golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs= +golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=