Skip to content

Commit 72bf2aa

Browse files
committed
Add a tokenserver that hands out (invitation-) tokens based on OIDC
closes #115
1 parent 4be4bac commit 72bf2aa

File tree

4 files changed

+201
-0
lines changed

4 files changed

+201
-0
lines changed

cmd/tokenserver/.gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
tokenserver
2+
settings.json

cmd/tokenserver/tokenserver.go

+187
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"encoding/base64"
6+
"encoding/json"
7+
"fmt"
8+
"html/template"
9+
"log"
10+
"net/http"
11+
"net/url"
12+
"os"
13+
"strings"
14+
15+
"github.com/Jille/convreq"
16+
"github.com/Jille/convreq/respond"
17+
"github.com/Jille/rufs/security"
18+
"github.com/coreos/go-oidc/v3/oidc"
19+
"golang.org/x/oauth2"
20+
)
21+
22+
var (
23+
ca *security.CAKeyPair
24+
25+
oidcProvider *oidc.Provider
26+
oauthConfig oauth2.Config
27+
)
28+
29+
type Config struct {
30+
Certdir string `json:"certdir"`
31+
OIDCProvider string `json:"oidc_provider"`
32+
ClientID string `json:"client_id"`
33+
ClientSecret string `json:"client_secret"`
34+
ReturnURL string `json:"return_url"`
35+
Port int `json:"port"`
36+
}
37+
38+
func main() {
39+
if len(os.Args) != 2 {
40+
log.Fatalf("Usage: %s <configfile>", os.Args[0])
41+
}
42+
c, err := os.ReadFile(os.Args[1])
43+
if err != nil {
44+
log.Fatalf("Failed to read config file %q: %v", os.Args[1], err)
45+
}
46+
var cfg Config
47+
if err := json.Unmarshal(c, &cfg); err != nil {
48+
log.Fatalf("Failed to read config file %q: %v", os.Args[1], err)
49+
}
50+
51+
if cfg.Certdir == "" {
52+
log.Fatalf("Configuration setting \"certdir\" is empty")
53+
}
54+
if cfg.OIDCProvider == "" {
55+
log.Fatalf("Configuration setting \"oidc_provider\" is empty")
56+
}
57+
if cfg.ClientID == "" {
58+
log.Fatalf("Configuration setting \"client_id\" is empty")
59+
}
60+
if cfg.ClientSecret == "" {
61+
log.Fatalf("Configuration setting \"client_secret\" is empty")
62+
}
63+
if cfg.ReturnURL == "" {
64+
log.Fatalf("Configuration setting \"return_url\" is empty")
65+
}
66+
if cfg.Port == 0 {
67+
log.Fatalf("Configuration setting \"port\" is not set")
68+
}
69+
70+
ca, err = security.LoadCAKeyPair(cfg.Certdir)
71+
if err != nil {
72+
log.Fatalf("Failed to load CA key pair: %v", err)
73+
}
74+
75+
oidcProvider, err = oidc.NewProvider(context.Background(), cfg.OIDCProvider)
76+
if err != nil {
77+
log.Fatalf("Failed to discover openid connect provider: %v", err)
78+
}
79+
oauthConfig = oauth2.Config{
80+
ClientID: cfg.ClientID,
81+
ClientSecret: cfg.ClientSecret,
82+
Endpoint: oidcProvider.Endpoint(),
83+
RedirectURL: cfg.ReturnURL,
84+
Scopes: []string{"openid"},
85+
}
86+
87+
http.Handle("/oauth2_return", convreq.Wrap(oauth2Return))
88+
http.Handle("/request_token", convreq.Wrap(requestToken))
89+
http.Handle("/", convreq.Wrap(mainPage))
90+
log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", cfg.Port), nil))
91+
}
92+
93+
type requestTokenGet struct {
94+
Hostname string `schema:"hostname"`
95+
ReturnURL string `schema:"return_url"`
96+
}
97+
98+
func requestToken(get requestTokenGet) convreq.HttpResponse {
99+
s := base64.URLEncoding.EncodeToString([]byte(get.Hostname)) + ":" + base64.URLEncoding.EncodeToString([]byte(get.ReturnURL))
100+
u := oauthConfig.AuthCodeURL(s)
101+
return respond.Redirect(302, u)
102+
}
103+
104+
type oauth2ReturnGet struct {
105+
Code string `schema:"code"`
106+
State string `schema:"state"`
107+
}
108+
109+
func oauth2Return(ctx context.Context, get oauth2ReturnGet) convreq.HttpResponse {
110+
t, err := oauthConfig.Exchange(ctx, get.Code)
111+
if err != nil {
112+
return respond.BadRequest("Oauth failed: " + err.Error())
113+
}
114+
userInfo, err := oidcProvider.UserInfo(ctx, oauth2.StaticTokenSource(t))
115+
if err != nil {
116+
return respond.Error(err)
117+
}
118+
var ui UserInfo
119+
if err := userInfo.Claims(&ui); err != nil {
120+
return respond.Error(err)
121+
}
122+
123+
sp := strings.Split(get.State, ":")
124+
hostname, err := base64.URLEncoding.DecodeString(sp[0])
125+
if err != nil {
126+
return respond.BadRequest("bad state")
127+
}
128+
returnURL, err := base64.URLEncoding.DecodeString(sp[1])
129+
if err != nil {
130+
return respond.BadRequest("bad state")
131+
}
132+
133+
username := strings.Split(ui.PreferredUsername, "@")[0]
134+
username = strings.ReplaceAll(username, "-", "")
135+
username += "-" + string(hostname)
136+
137+
token := ca.CreateToken(username)
138+
139+
u, err := url.Parse(string(returnURL))
140+
if err != nil {
141+
return respond.BadRequest("Invalid return_url: " + err.Error())
142+
}
143+
q := u.Query()
144+
q.Set("username", username)
145+
q.Set("token", token)
146+
q.Set("circle", ca.Name())
147+
q.Set("fingerprint", ca.Fingerprint())
148+
149+
if len(returnURL) == 0 {
150+
var ret string
151+
for k, vs := range q {
152+
ret += k + ":" + vs[0] + "\n"
153+
}
154+
return respond.String(ret)
155+
}
156+
157+
u.RawQuery = q.Encode()
158+
return respond.Redirect(302, u.String())
159+
}
160+
161+
type UserInfo struct {
162+
Name string `json:"name"`
163+
PreferredUsername string `json:"preferred_username"`
164+
}
165+
166+
type mainPageGet struct {
167+
Hostname string `schema:"hostname"`
168+
ReturnURL string `schema:"return_url"`
169+
}
170+
171+
var mainPageTpl = template.Must(template.New("").Parse(`
172+
<html>
173+
<head>
174+
</head>
175+
<body>
176+
<form method="GET" action="/request_token">
177+
Hostname: <input name="hostname" value="{{.Hostname}}" />
178+
<input type="hidden" name="return_url" value="{{.ReturnURL}}" />
179+
<input type="submit" value="Continue" />
180+
</form>
181+
</body>
182+
</html>
183+
`))
184+
185+
func mainPage(get mainPageGet) convreq.HttpResponse {
186+
return respond.RenderTemplate(mainPageTpl, get)
187+
}

go.mod

+4
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ require (
1515
github.com/Jille/rpcz v0.3.0
1616
github.com/billziss-gh/cgofuse v1.5.0
1717
github.com/cenkalti/backoff/v4 v4.3.0
18+
github.com/coreos/go-oidc/v3 v3.11.0
1819
github.com/getlantern/systray v1.2.2
1920
github.com/go-git/go-billy/v5 v5.5.0
2021
github.com/golang/protobuf v1.5.4
@@ -27,6 +28,7 @@ require (
2728
github.com/prometheus/client_golang v1.12.2
2829
github.com/stoewer/go-strcase v1.3.0
2930
github.com/yookoala/realpath v1.0.0
31+
golang.org/x/oauth2 v0.21.0
3032
golang.org/x/sync v0.8.0
3133
google.golang.org/grpc v1.65.0
3234
google.golang.org/protobuf v1.34.2
@@ -44,6 +46,7 @@ require (
4446
github.com/getlantern/hex v0.0.0-20220104173244-ad7e4b9194dc // indirect
4547
github.com/getlantern/hidden v0.0.0-20220104173330-f221c5a24770 // indirect
4648
github.com/getlantern/ops v0.0.0-20231025133620-f368ab734534 // indirect
49+
github.com/go-jose/go-jose/v4 v4.0.2 // indirect
4750
github.com/go-logr/logr v1.4.2 // indirect
4851
github.com/go-logr/stdr v1.2.2 // indirect
4952
github.com/go-stack/stack v1.8.1 // indirect
@@ -60,6 +63,7 @@ require (
6063
go.opentelemetry.io/otel/trace v1.28.0 // indirect
6164
go.uber.org/multierr v1.11.0 // indirect
6265
go.uber.org/zap v1.27.0 // indirect
66+
golang.org/x/crypto v0.26.0 // indirect
6367
golang.org/x/net v0.28.0 // indirect
6468
golang.org/x/sys v0.24.0 // indirect
6569
golang.org/x/text v0.17.0 // indirect

go.sum

+8
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,8 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk
8282
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
8383
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
8484
github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
85+
github.com/coreos/go-oidc/v3 v3.11.0 h1:Ia3MxdwpSw702YW0xgfmP1GVCMA9aEFWu12XUZ3/OtI=
86+
github.com/coreos/go-oidc/v3 v3.11.0/go.mod h1:gE3LgjOgFoHi9a4ce4/tJczr0Ai2/BoDhf0r5lltWI0=
8587
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
8688
github.com/cyphar/filepath-securejoin v0.3.1 h1:1V7cHiaW+C+39wEfpH6XlLBQo3j/PciWFrgfCLS8XrE=
8789
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
125127
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
126128
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
127129
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
130+
github.com/go-jose/go-jose/v4 v4.0.2 h1:R3l3kkBds16bO7ZFAEEcofK0MkrAJt3jlJznWZG0nvk=
131+
github.com/go-jose/go-jose/v4 v4.0.2/go.mod h1:WVf9LFMHh/QVrmqrOfqun0C45tMe3RoiKJMPvgWwLfY=
128132
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
129133
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
130134
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
353357
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
354358
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
355359
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
360+
golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw=
361+
golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=
356362
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
357363
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
358364
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
425431
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
426432
golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
427433
golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
434+
golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs=
435+
golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
428436
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
429437
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
430438
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=

0 commit comments

Comments
 (0)