mirror of
https://github.com/netbirdio/netbird.git
synced 2025-01-11 16:38:27 +01:00
peer management HTTP API (#81)
* feature: create account for a newly registered user * feature: finalize user auth flow * feature: create protected API with JWT * chore: cleanup http server * feature: add UI assets * chore: update react UI * refactor: move account not exists -> create to AccountManager * chore: update UI * chore: return only peers on peers endpoint * chore: add UI path to the config * chore: remove ui from management * chore: remove unused Docker comamnds * docs: update management config sample * fix: store creation * feature: introduce peer response to the HTTP api * fix: lint errors * feature: add setup-keys HTTP endpoint * fix: return empty json arrays in HTTP API * feature: add new peer response fields
This commit is contained in:
parent
d5af5f1878
commit
3c47a3c408
6
go.mod
6
go.mod
@ -3,14 +3,15 @@ module github.com/wiretrustee/wiretrustee
|
||||
go 1.16
|
||||
|
||||
require (
|
||||
github.com/auth0/go-jwt-middleware v1.0.1
|
||||
github.com/cenkalti/backoff/v4 v4.1.0
|
||||
github.com/codegangsta/negroni v1.0.0
|
||||
github.com/coreos/go-oidc v2.2.1+incompatible
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible
|
||||
github.com/golang/protobuf v1.5.2
|
||||
github.com/google/uuid v1.2.0
|
||||
github.com/gorilla/mux v1.8.0
|
||||
github.com/gorilla/mux v1.8.0 // indirect
|
||||
github.com/gorilla/sessions v1.2.1
|
||||
github.com/joho/godotenv v1.3.0
|
||||
github.com/kardianos/service v1.2.0
|
||||
github.com/onsi/ginkgo v1.16.4
|
||||
github.com/onsi/gomega v1.13.0
|
||||
@ -20,7 +21,6 @@ require (
|
||||
github.com/spf13/cobra v1.1.3
|
||||
github.com/vishvananda/netlink v1.1.0
|
||||
golang.org/x/crypto v0.0.0-20210506145944-38f3c27a63bf
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45
|
||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007
|
||||
golang.zx2c4.com/wireguard v0.0.0-20210604143328-f9b48a961cd2
|
||||
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20210506160403-92e472f520a5
|
||||
|
17
go.sum
17
go.sum
@ -19,6 +19,8 @@ github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRF
|
||||
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
|
||||
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
|
||||
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
|
||||
github.com/auth0/go-jwt-middleware v1.0.1 h1:/fsQ4vRr4zod1wKReUH+0A3ySRjGiT9G34kypO/EKwI=
|
||||
github.com/auth0/go-jwt-middleware v1.0.1/go.mod h1:YSeUX3z6+TF2H+7padiEqNJ73Zy9vXW72U//IgN0BIM=
|
||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
||||
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
|
||||
@ -48,6 +50,8 @@ github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymF
|
||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||
github.com/form3tech-oss/jwt-go v3.2.2+incompatible h1:TcekIExNqud5crz4xD2pavyTgWiPvpYe4Xau31I0PRk=
|
||||
github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
|
||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||
@ -60,6 +64,8 @@ github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/me
|
||||
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
|
||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
@ -97,6 +103,9 @@ github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+
|
||||
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00 h1:l5lAOZEym3oK3SQ2HBHWsJUfbNBiTXJDeW2QDxw9AQ0=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
|
||||
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
|
||||
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
|
||||
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
|
||||
@ -130,8 +139,6 @@ github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/J
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
|
||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||
github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc=
|
||||
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
|
||||
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
|
||||
github.com/josharian/native v0.0.0-20200817173448-b6b71def0850 h1:uhL5Gw7BINiiPAo24A2sxkcDI0Jt/sqp1v5xQCniEFA=
|
||||
github.com/josharian/native v0.0.0-20200817173448-b6b71def0850/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
|
||||
@ -145,6 +152,7 @@ github.com/jsimonetti/rtnetlink v0.0.0-20210212075122-66c871082f2b h1:c3NTyLNozI
|
||||
github.com/jsimonetti/rtnetlink v0.0.0-20210212075122-66c871082f2b/go.mod h1:8w9Rh8m+aHZIG69YPGGem1i5VzoyRC8nw2kA8B+ik5U=
|
||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
|
||||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||
github.com/kardianos/service v1.2.0 h1:bGuZ/epo3vrt8IPC7mnKQolqFeYJb7Cs8Rk4PSOBB/g=
|
||||
@ -254,6 +262,9 @@ github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPx
|
||||
github.com/sirupsen/logrus v1.7.0 h1:ShrD1U9pZB12TX0cVy0DtePoCH97K8EtX+mg7ZARUtM=
|
||||
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||
github.com/smartystreets/assertions v1.1.0 h1:MkTeG1DMwsrdH7QtLXy5W+fUxWq+vmb6cLmyJ7aRtF0=
|
||||
github.com/smartystreets/assertions v1.1.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo=
|
||||
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
|
||||
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
|
||||
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||
@ -276,6 +287,8 @@ github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5Cc
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
|
||||
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
|
||||
github.com/urfave/negroni v1.0.0 h1:kIimOitoypq34K7TG7DUaJ9kq/N4Ofuwi1sjz0KipXc=
|
||||
github.com/urfave/negroni v1.0.0/go.mod h1:Meg73S6kFm/4PpbYdq35yYWoCZ9mS/YSx+lKnmiohz4=
|
||||
github.com/vishvananda/netlink v1.1.0 h1:1iyaYNBLmP6L0220aDnYQpo1QEV4t4hJ+xEEhhJH8j0=
|
||||
github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE=
|
||||
github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df h1:OviZH7qLw/7ZovXvuNyL3XQl8UFofeikI1NW1Gypu7k=
|
||||
|
@ -25,9 +25,9 @@
|
||||
"HttpConfig": {
|
||||
"LetsEncryptDomain": "",
|
||||
"Address": "0.0.0.0:3000",
|
||||
"AuthDomain": "<PASTE YOUR AUTH0 DOMAIN HERE>",
|
||||
"AuthClientId": "<PASTE YOUR AUTH0 CLIENT ID HERE>",
|
||||
"AuthClientSecret": "<PASTE YOUR AUTH0 SECRET>",
|
||||
"AuthCallback": "http://localhost:3000/callback"
|
||||
"AuthIssuer": "<PASTE YOUR AUTH0 ISSUER HERE>",
|
||||
"AuthAudience": "<PASTE YOUR AUTH0 AUDIENCE HERE>",
|
||||
"AuthKeysLocation": "<PASTE YOUR JWT KEY SET location>",
|
||||
"UIFilesLocation": "/var/lib/wiretrustee/ui/"
|
||||
}
|
||||
}
|
@ -57,6 +57,12 @@ var (
|
||||
}
|
||||
}
|
||||
|
||||
store, err := server.NewStore(config.Datadir)
|
||||
if err != nil {
|
||||
log.Fatalf("failed creating a store: %s: %v", config.Datadir, err)
|
||||
}
|
||||
accountManager := server.NewManager(store)
|
||||
|
||||
var opts []grpc.ServerOption
|
||||
|
||||
var httpServer *http.Server
|
||||
@ -65,15 +71,15 @@ var (
|
||||
transportCredentials := credentials.NewTLS(certManager.TLSConfig())
|
||||
opts = append(opts, grpc.Creds(transportCredentials))
|
||||
|
||||
httpServer = http.NewHttpsServer(config.HttpConfig, certManager)
|
||||
httpServer = http.NewHttpsServer(config.HttpConfig, certManager, accountManager)
|
||||
} else {
|
||||
httpServer = http.NewHttpServer(config.HttpConfig)
|
||||
httpServer = http.NewHttpServer(config.HttpConfig, accountManager)
|
||||
}
|
||||
|
||||
opts = append(opts, grpc.KeepaliveEnforcementPolicy(kaep), grpc.KeepaliveParams(kasp))
|
||||
grpcServer := grpc.NewServer(opts...)
|
||||
|
||||
server, err := grpc2.NewServer(config)
|
||||
server, err := grpc2.NewServer(config, accountManager)
|
||||
if err != nil {
|
||||
log.Fatalf("failed creating new server: %v", err)
|
||||
}
|
||||
|
@ -82,6 +82,83 @@ func (manager *AccountManager) GetPeersForAPeer(peerKey string) ([]*Peer, error)
|
||||
return res, nil
|
||||
}
|
||||
|
||||
//GetAccount returns an existing account or error (NotFound) if doesn't exist
|
||||
func (manager *AccountManager) GetAccount(accountId string) (*Account, error) {
|
||||
manager.mux.Lock()
|
||||
defer manager.mux.Unlock()
|
||||
|
||||
account, err := manager.Store.GetAccount(accountId)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "failed retrieving account")
|
||||
}
|
||||
|
||||
return account, nil
|
||||
}
|
||||
|
||||
// GetOrCreateAccount returns an existing account or creates a new one if doesn't exist
|
||||
func (manager *AccountManager) GetOrCreateAccount(accountId string) (*Account, error) {
|
||||
manager.mux.Lock()
|
||||
defer manager.mux.Unlock()
|
||||
|
||||
_, err := manager.Store.GetAccount(accountId)
|
||||
if err != nil {
|
||||
if s, ok := status.FromError(err); ok && s.Code() == codes.NotFound {
|
||||
return manager.createAccount(accountId)
|
||||
} else {
|
||||
// other error
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
account, err := manager.Store.GetAccount(accountId)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "failed retrieving account")
|
||||
}
|
||||
|
||||
return account, nil
|
||||
}
|
||||
|
||||
//AccountExists checks whether account exists (returns true) or not (returns false)
|
||||
func (manager *AccountManager) AccountExists(accountId string) (*bool, error) {
|
||||
manager.mux.Lock()
|
||||
defer manager.mux.Unlock()
|
||||
|
||||
var res bool
|
||||
_, err := manager.Store.GetAccount(accountId)
|
||||
if err != nil {
|
||||
if s, ok := status.FromError(err); ok && s.Code() == codes.NotFound {
|
||||
res = false
|
||||
return &res, nil
|
||||
} else {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
res = true
|
||||
return &res, nil
|
||||
}
|
||||
|
||||
// AddAccount generates a new Account with a provided accountId and saves to the Store
|
||||
func (manager *AccountManager) AddAccount(accountId string) (*Account, error) {
|
||||
|
||||
manager.mux.Lock()
|
||||
defer manager.mux.Unlock()
|
||||
|
||||
return manager.createAccount(accountId)
|
||||
|
||||
}
|
||||
|
||||
func (manager *AccountManager) createAccount(accountId string) (*Account, error) {
|
||||
account, _ := newAccountWithId(accountId)
|
||||
|
||||
err := manager.Store.SaveAccount(account)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "failed creating account")
|
||||
}
|
||||
|
||||
return account, nil
|
||||
}
|
||||
|
||||
// AddPeer adds a new peer to the Store.
|
||||
// Each Account has a list of pre-authorised SetupKey and if no Account has a given key err wit ha code codes.Unauthenticated
|
||||
// will be returned, meaning the key is invalid
|
||||
@ -96,7 +173,7 @@ func (manager *AccountManager) AddPeer(setupKey string, peerKey string) (*Peer,
|
||||
var sk *SetupKey
|
||||
if len(setupKey) == 0 {
|
||||
// Empty setup key, create a new account for it.
|
||||
account, sk = manager.newAccount()
|
||||
account, sk = newAccount()
|
||||
} else {
|
||||
sk = &SetupKey{Key: setupKey}
|
||||
account, err = manager.Store.GetAccountBySetupKey(sk.Key)
|
||||
@ -129,20 +206,28 @@ func (manager *AccountManager) AddPeer(setupKey string, peerKey string) (*Peer,
|
||||
|
||||
}
|
||||
|
||||
// newAccount creates a new Account with a default SetupKey (doesn't store in a Store)
|
||||
func (manager *AccountManager) newAccount() (*Account, *SetupKey) {
|
||||
// newAccountWithId creates a new Account with a default SetupKey (doesn't store in a Store) and provided id
|
||||
func newAccountWithId(accountId string) (*Account, *SetupKey) {
|
||||
|
||||
log.Debugf("creating new account")
|
||||
|
||||
accountId := uuid.New().String()
|
||||
setupKeyId := uuid.New().String()
|
||||
setupKeys := make(map[string]*SetupKey)
|
||||
setupKey := &SetupKey{Key: setupKeyId}
|
||||
setupKeys[setupKeyId] = setupKey
|
||||
network := &Network{Id: uuid.New().String(), Net: net.IPNet{}, Dns: ""}
|
||||
network := &Network{
|
||||
Id: uuid.New().String(),
|
||||
Net: net.IPNet{IP: net.ParseIP("100.64.0.0"), Mask: net.IPMask{255, 192, 0, 0}},
|
||||
Dns: ""}
|
||||
peers := make(map[string]*Peer)
|
||||
|
||||
log.Debugf("created new account %s with setup key %s", accountId, setupKeyId)
|
||||
|
||||
return &Account{Id: accountId, SetupKeys: setupKeys, Network: network, Peers: peers}, setupKey
|
||||
}
|
||||
|
||||
// newAccount creates a new Account with a default SetupKey (doesn't store in a Store)
|
||||
func newAccount() (*Account, *SetupKey) {
|
||||
accountId := uuid.New().String()
|
||||
return newAccountWithId(accountId)
|
||||
}
|
||||
|
@ -25,10 +25,14 @@ type Config struct {
|
||||
type HttpServerConfig struct {
|
||||
LetsEncryptDomain string
|
||||
Address string
|
||||
AuthDomain string
|
||||
AuthClientId string
|
||||
AuthClientSecret string
|
||||
AuthCallback string
|
||||
// AuthAudience identifies the recipients that the JWT is intended for (aud in JWT)
|
||||
AuthAudience string
|
||||
// AuthIssuer identifies principal that issued the JWT.
|
||||
AuthIssuer string
|
||||
// AuthKeysLocation is a location of JWT key set containing the public keys used to verify JWT
|
||||
AuthKeysLocation string
|
||||
// UIFilesLocation is the location of static UI files for management frontend
|
||||
UIFilesLocation string
|
||||
}
|
||||
|
||||
// Host represents a Wiretrustee host (e.g. STUN, TURN, Signal)
|
||||
|
@ -41,9 +41,11 @@ func restore(file string) (*FileStore, error) {
|
||||
if _, err := os.Stat(file); os.IsNotExist(err) {
|
||||
// create a new FileStore if previously didn't exist (e.g. first run)
|
||||
s := &FileStore{
|
||||
Accounts: make(map[string]*Account),
|
||||
mux: sync.Mutex{},
|
||||
storeFile: file,
|
||||
Accounts: make(map[string]*Account),
|
||||
mux: sync.Mutex{},
|
||||
SetupKeyId2AccountId: make(map[string]string),
|
||||
PeerKeyId2AccountId: make(map[string]string),
|
||||
storeFile: file,
|
||||
}
|
||||
|
||||
err = s.persist(file)
|
||||
@ -148,7 +150,7 @@ func (s *FileStore) GetAccount(accountId string) (*Account, error) {
|
||||
|
||||
account, accountFound := s.Accounts[accountId]
|
||||
if !accountFound {
|
||||
return nil, status.Errorf(codes.Internal, "account not found")
|
||||
return nil, status.Errorf(codes.NotFound, "account not found")
|
||||
}
|
||||
|
||||
return account, nil
|
||||
|
@ -34,21 +34,17 @@ type UpdateChannelMessage struct {
|
||||
}
|
||||
|
||||
// NewServer creates a new Management server
|
||||
func NewServer(config *server.Config) (*Server, error) {
|
||||
func NewServer(config *server.Config, accountManager *server.AccountManager) (*Server, error) {
|
||||
key, err := wgtypes.GeneratePrivateKey()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
store, err := server.NewStore(config.Datadir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Server{
|
||||
wgKey: key,
|
||||
// peerKey -> event channel
|
||||
peerChannels: make(map[string]chan *UpdateChannelMessage),
|
||||
channelsMux: &sync.Mutex{},
|
||||
accountManager: server.NewManager(store),
|
||||
accountManager: accountManager,
|
||||
config: config,
|
||||
}, nil
|
||||
}
|
||||
|
@ -1,96 +0,0 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/coreos/go-oidc"
|
||||
"github.com/gorilla/sessions"
|
||||
middleware2 "github.com/wiretrustee/wiretrustee/management/server/http/middleware"
|
||||
"log"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// Callback handler used to receive a callback from the identity provider
|
||||
type Callback struct {
|
||||
authenticator *middleware2.Authenticator
|
||||
sessionStore sessions.Store
|
||||
}
|
||||
|
||||
func NewCallback(authenticator *middleware2.Authenticator, sessionStore sessions.Store) *Callback {
|
||||
return &Callback{
|
||||
authenticator: authenticator,
|
||||
sessionStore: sessionStore,
|
||||
}
|
||||
}
|
||||
|
||||
// ServeHTTP checks the user session, verifies the state, verifies the token, stores user profile in a session,
|
||||
// and in case of the successful auth redirects user to the main page
|
||||
func (h *Callback) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
session, err := h.sessionStore.Get(r, "auth-session")
|
||||
if err != nil {
|
||||
//todo redirect to the error page stating: "error occurred plz try again later and a link to login"
|
||||
//http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
http.Redirect(w, r, "/login", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
if r.URL.Query().Get("state") != session.Values["state"] {
|
||||
//todo redirect to the error page stating: "error authenticating plz try to login once again"
|
||||
//http.Error(w, "invalid state parameter", http.StatusBadRequest)
|
||||
http.Redirect(w, r, "/login", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
token, err := h.authenticator.Config.Exchange(context.TODO(), r.URL.Query().Get("code"))
|
||||
if err != nil {
|
||||
log.Printf("no token found: %v", err)
|
||||
//todo redirect to the error page stating: "error authenticating plz try to login once again"
|
||||
//w.WriteHeader(http.StatusUnauthorized)
|
||||
http.Redirect(w, r, "/login", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
rawIDToken, ok := token.Extra("id_token").(string)
|
||||
if !ok {
|
||||
//todo redirect to the error page stating: "error occurred plz try again later and a link to login"
|
||||
//http.Error(w, "no id_token field in oauth2 token.", http.StatusInternalServerError)
|
||||
http.Redirect(w, r, "/login", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
oidcConfig := &oidc.Config{
|
||||
ClientID: h.authenticator.Config.ClientID,
|
||||
}
|
||||
|
||||
idToken, err := h.authenticator.Provider.Verifier(oidcConfig).Verify(context.TODO(), rawIDToken)
|
||||
|
||||
if err != nil {
|
||||
//todo redirect to the error page stating: "error occurred plz try again later and a link to login"
|
||||
//http.Error(w, "failed to verify ID Token: "+err.Error(), http.StatusInternalServerError)
|
||||
http.Redirect(w, r, "/login", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
// get the userInfo from the token
|
||||
var profile map[string]interface{}
|
||||
if err := idToken.Claims(&profile); err != nil {
|
||||
//todo redirect to the error page stating: "error occurred plz try again later and a link to login"
|
||||
//http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
http.Redirect(w, r, "/login", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
session.Values["id_token"] = rawIDToken
|
||||
session.Values["access_token"] = token.AccessToken
|
||||
session.Values["profile"] = profile
|
||||
|
||||
err = session.Save(r, w)
|
||||
if err != nil {
|
||||
//todo redirect to the error page stating: "error occurred plz try again later and a link to login"
|
||||
//http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
http.Redirect(w, r, "/login", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
// redirect to logged in page
|
||||
http.Redirect(w, r, "/dashboard", http.StatusSeeOther)
|
||||
}
|
@ -1,44 +0,0 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/gorilla/sessions"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Dashboard is a handler of the main page of the app (dashboard)
|
||||
type Dashboard struct {
|
||||
sessionStore sessions.Store
|
||||
}
|
||||
|
||||
func NewDashboard(sessionStore sessions.Store) *Dashboard {
|
||||
return &Dashboard{
|
||||
sessionStore: sessionStore,
|
||||
}
|
||||
}
|
||||
|
||||
// ServeHTTP verifies if user is authenticated and returns a user dashboard
|
||||
func (h *Dashboard) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
session, err := h.sessionStore.Get(r, "auth-session")
|
||||
if err != nil {
|
||||
//todo redirect to the error page stating: "error occurred plz try again later and a link to login"
|
||||
//http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
http.Redirect(w, r, "/login", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
//todo get user account and relevant data to show
|
||||
profile := session.Values["profile"].(map[string]interface{})
|
||||
name := profile["name"]
|
||||
w.WriteHeader(200)
|
||||
_, err = io.Copy(w, strings.NewReader("hello "+fmt.Sprintf("%v", name)))
|
||||
if err != nil {
|
||||
return
|
||||
|
||||
}
|
||||
|
||||
//template.RenderTemplate(w, "dashboard", session.Values["profile"])
|
||||
}
|
@ -1,58 +0,0 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"github.com/gorilla/sessions"
|
||||
middleware2 "github.com/wiretrustee/wiretrustee/management/server/http/middleware"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// Login handler used to login a user
|
||||
type Login struct {
|
||||
authenticator *middleware2.Authenticator
|
||||
sessionStore sessions.Store
|
||||
}
|
||||
|
||||
func NewLogin(authenticator *middleware2.Authenticator, sessionStore sessions.Store) *Login {
|
||||
return &Login{
|
||||
authenticator: authenticator,
|
||||
sessionStore: sessionStore,
|
||||
}
|
||||
}
|
||||
|
||||
// ServeHTTP generates a new session state for a user and redirects the user to the auth URL
|
||||
func (h *Login) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// Generate random state
|
||||
b := make([]byte, 32)
|
||||
_, err := rand.Read(b)
|
||||
if err != nil {
|
||||
//todo redirect to the error page stating: "error occurred plz try again later and a link to login"
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
state := base64.StdEncoding.EncodeToString(b)
|
||||
|
||||
session, err := h.sessionStore.Get(r, "auth-session")
|
||||
if err != nil {
|
||||
switch err.(type) {
|
||||
case *fs.PathError:
|
||||
// a case when session doesn't exist in the store but was sent by the client in the cookie -> create new session ID
|
||||
// it appears that in this case session is always non empty object
|
||||
session.ID = "" //nolint
|
||||
default:
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
session.Values["state"] = state //nolint
|
||||
err = session.Save(r, w) //nolint
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
url := h.authenticator.Config.AuthCodeURL(state)
|
||||
http.Redirect(w, r, url, http.StatusTemporaryRedirect)
|
||||
}
|
@ -1,48 +0,0 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
// Logout logs out a user
|
||||
type Logout struct {
|
||||
authDomain string
|
||||
authClientId string
|
||||
}
|
||||
|
||||
func NewLogout(authDomain string, authClientId string) *Logout {
|
||||
return &Logout{authDomain: authDomain, authClientId: authClientId}
|
||||
}
|
||||
|
||||
// ServeHTTP redirects user to teh auth identity provider logout URL
|
||||
func (h *Logout) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
logoutUrl, err := url.Parse("https://" + h.authDomain)
|
||||
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
logoutUrl.Path += "/v2/logout"
|
||||
parameters := url.Values{}
|
||||
|
||||
var scheme string
|
||||
if r.TLS == nil {
|
||||
scheme = "http"
|
||||
} else {
|
||||
scheme = "https"
|
||||
}
|
||||
|
||||
returnTo, err := url.Parse(scheme + "://" + r.Host + "/login")
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
parameters.Add("returnTo", returnTo.String())
|
||||
parameters.Add("client_id", h.authClientId)
|
||||
logoutUrl.RawQuery = parameters.Encode()
|
||||
|
||||
http.Redirect(w, r, logoutUrl.String(), http.StatusTemporaryRedirect)
|
||||
}
|
67
management/server/http/handler/peers.go
Normal file
67
management/server/http/handler/peers.go
Normal file
@ -0,0 +1,67 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/wiretrustee/wiretrustee/management/server"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Peers is a handler that returns peers of the account
|
||||
type Peers struct {
|
||||
accountManager *server.AccountManager
|
||||
}
|
||||
|
||||
// PeerResponse is a response sent to the client
|
||||
type PeerResponse struct {
|
||||
Name string
|
||||
IP string
|
||||
Connected bool
|
||||
LastSeen time.Time
|
||||
Os string
|
||||
}
|
||||
|
||||
func NewPeers(accountManager *server.AccountManager) *Peers {
|
||||
return &Peers{
|
||||
accountManager: accountManager,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Peers) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
accountId := extractAccountIdFromRequestContext(r)
|
||||
//new user -> create a new account
|
||||
account, err := h.accountManager.GetOrCreateAccount(accountId)
|
||||
if err != nil {
|
||||
log.Errorf("failed getting user account %s: %v", accountId, err)
|
||||
http.Redirect(w, r, "/", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(200)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
respBody := []*PeerResponse{}
|
||||
for _, peer := range account.Peers {
|
||||
respBody = append(respBody, &PeerResponse{
|
||||
Name: peer.Key,
|
||||
IP: peer.IP.String(),
|
||||
LastSeen: time.Now(),
|
||||
Connected: false,
|
||||
Os: "Ubuntu 21.04 (Hirsute Hippo)",
|
||||
})
|
||||
}
|
||||
|
||||
err = json.NewEncoder(w).Encode(respBody)
|
||||
if err != nil {
|
||||
log.Errorf("failed encoding account peers %s: %v", accountId, err)
|
||||
http.Redirect(w, r, "/", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
case http.MethodOptions:
|
||||
default:
|
||||
http.Error(w, "", http.StatusNotFound)
|
||||
}
|
||||
}
|
58
management/server/http/handler/setupkeys.go
Normal file
58
management/server/http/handler/setupkeys.go
Normal file
@ -0,0 +1,58 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/wiretrustee/wiretrustee/management/server"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// SetupKeys is a handler that returns a list of setup keys of the account
|
||||
type SetupKeys struct {
|
||||
accountManager *server.AccountManager
|
||||
}
|
||||
|
||||
// SetupKeyResponse is a response sent to the client
|
||||
type SetupKeyResponse struct {
|
||||
Key string
|
||||
}
|
||||
|
||||
func NewSetupKeysHandler(accountManager *server.AccountManager) *SetupKeys {
|
||||
return &SetupKeys{
|
||||
accountManager: accountManager,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *SetupKeys) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
accountId := extractAccountIdFromRequestContext(r)
|
||||
//new user -> create a new account
|
||||
account, err := h.accountManager.GetOrCreateAccount(accountId)
|
||||
if err != nil {
|
||||
log.Errorf("failed getting user account %s: %v", accountId, err)
|
||||
http.Redirect(w, r, "/", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(200)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
respBody := []*SetupKeyResponse{}
|
||||
for _, key := range account.SetupKeys {
|
||||
respBody = append(respBody, &SetupKeyResponse{
|
||||
Key: key.Key,
|
||||
})
|
||||
}
|
||||
|
||||
err = json.NewEncoder(w).Encode(respBody)
|
||||
if err != nil {
|
||||
log.Errorf("failed encoding account peers %s: %v", accountId, err)
|
||||
http.Redirect(w, r, "/", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
case http.MethodOptions:
|
||||
default:
|
||||
http.Error(w, "", http.StatusNotFound)
|
||||
}
|
||||
}
|
15
management/server/http/handler/util.go
Normal file
15
management/server/http/handler/util.go
Normal file
@ -0,0 +1,15 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"github.com/golang-jwt/jwt"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// extractAccountIdFromRequestContext extracts accountId from the request context previously filled by the JWT token (after auth)
|
||||
func extractAccountIdFromRequestContext(r *http.Request) string {
|
||||
token := r.Context().Value("user").(*jwt.Token)
|
||||
claims := token.Claims.(jwt.MapClaims)
|
||||
|
||||
//actually a user id but for now we have a 1 to 1 mapping.
|
||||
return claims["sub"].(string)
|
||||
}
|
@ -1,39 +0,0 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"golang.org/x/oauth2"
|
||||
"log"
|
||||
|
||||
"github.com/coreos/go-oidc"
|
||||
)
|
||||
|
||||
type Authenticator struct {
|
||||
Provider *oidc.Provider
|
||||
Config oauth2.Config
|
||||
Ctx context.Context
|
||||
}
|
||||
|
||||
func NewAuthenticator(authDomain string, authClientId string, authClientSecret string, authCallback string) (*Authenticator, error) {
|
||||
ctx := context.Background()
|
||||
|
||||
provider, err := oidc.NewProvider(ctx, "https://"+authDomain+"/")
|
||||
if err != nil {
|
||||
log.Printf("failed to get provider: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
conf := oauth2.Config{
|
||||
ClientID: authClientId,
|
||||
ClientSecret: authClientSecret,
|
||||
RedirectURL: authCallback,
|
||||
Endpoint: provider.Endpoint(),
|
||||
Scopes: []string{oidc.ScopeOpenID, "profile"},
|
||||
}
|
||||
|
||||
return &Authenticator{
|
||||
Provider: provider,
|
||||
Config: conf,
|
||||
Ctx: ctx,
|
||||
}, nil
|
||||
}
|
@ -1,31 +0,0 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"github.com/gorilla/sessions"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type AuthMiddleware struct {
|
||||
sessionStore sessions.Store
|
||||
}
|
||||
|
||||
func NewAuth(sessionStore sessions.Store) *AuthMiddleware {
|
||||
return &AuthMiddleware{sessionStore: sessionStore}
|
||||
}
|
||||
|
||||
func (am *AuthMiddleware) IsAuthenticated(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
|
||||
|
||||
session, err := am.sessionStore.Get(r, "auth-session")
|
||||
if err != nil {
|
||||
//todo redirect to the error page stating: "error occurred plz try again later and a link to login"
|
||||
//http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
http.Redirect(w, r, "/login", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
if _, ok := session.Values["profile"]; !ok {
|
||||
http.Redirect(w, r, "/login", http.StatusSeeOther)
|
||||
} else {
|
||||
next(w, r)
|
||||
}
|
||||
}
|
92
management/server/http/middleware/handler.go
Normal file
92
management/server/http/middleware/handler.go
Normal file
@ -0,0 +1,92 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"github.com/golang-jwt/jwt"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
//Jwks is a collection of JSONWebKeys obtained from Config.HttpServerConfig.AuthKeysLocation
|
||||
type Jwks struct {
|
||||
Keys []JSONWebKeys `json:"keys"`
|
||||
}
|
||||
|
||||
//JSONWebKeys is a representation of a Jason Web Key
|
||||
type JSONWebKeys struct {
|
||||
Kty string `json:"kty"`
|
||||
Kid string `json:"kid"`
|
||||
Use string `json:"use"`
|
||||
N string `json:"n"`
|
||||
E string `json:"e"`
|
||||
X5c []string `json:"x5c"`
|
||||
}
|
||||
|
||||
//NewJwtMiddleware creates new middleware to verify the JWT token sent via Authorization header
|
||||
func NewJwtMiddleware(issuer string, audience string, keysLocation string) (*JWTMiddleware, error) {
|
||||
|
||||
keys, err := getPemKeys(keysLocation)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return New(Options{
|
||||
ValidationKeyGetter: func(token *jwt.Token) (interface{}, error) {
|
||||
// Verify 'aud' claim
|
||||
checkAud := token.Claims.(jwt.MapClaims).VerifyAudience(audience, false)
|
||||
if !checkAud {
|
||||
return token, errors.New("invalid audience")
|
||||
}
|
||||
// Verify 'issuer' claim
|
||||
checkIss := token.Claims.(jwt.MapClaims).VerifyIssuer(issuer, false)
|
||||
if !checkIss {
|
||||
return token, errors.New("invalid issuer")
|
||||
}
|
||||
|
||||
cert, err := getPemCert(token, keys)
|
||||
if err != nil {
|
||||
panic(err.Error())
|
||||
}
|
||||
|
||||
result, _ := jwt.ParseRSAPublicKeyFromPEM([]byte(cert))
|
||||
return result, nil
|
||||
},
|
||||
SigningMethod: jwt.SigningMethodRS256,
|
||||
EnableAuthOnOptions: true,
|
||||
}), nil
|
||||
}
|
||||
|
||||
func getPemKeys(keysLocation string) (*Jwks, error) {
|
||||
resp, err := http.Get(keysLocation)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var jwks = &Jwks{}
|
||||
err = json.NewDecoder(resp.Body).Decode(jwks)
|
||||
|
||||
if err != nil {
|
||||
return jwks, err
|
||||
}
|
||||
|
||||
return jwks, err
|
||||
}
|
||||
|
||||
func getPemCert(token *jwt.Token, jwks *Jwks) (string, error) {
|
||||
cert := ""
|
||||
|
||||
for k := range jwks.Keys {
|
||||
if token.Header["kid"] == jwks.Keys[k].Kid {
|
||||
cert = "-----BEGIN CERTIFICATE-----\n" + jwks.Keys[k].X5c[0] + "\n-----END CERTIFICATE-----"
|
||||
}
|
||||
}
|
||||
|
||||
if cert == "" {
|
||||
err := errors.New("unable to find appropriate key")
|
||||
return cert, err
|
||||
}
|
||||
|
||||
return cert, nil
|
||||
}
|
237
management/server/http/middleware/jwtmiddleware.go
Normal file
237
management/server/http/middleware/jwtmiddleware.go
Normal file
@ -0,0 +1,237 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/golang-jwt/jwt"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// A function called whenever an error is encountered
|
||||
type errorHandler func(w http.ResponseWriter, r *http.Request, err string)
|
||||
|
||||
// TokenExtractor is a function that takes a request as input and returns
|
||||
// either a token or an error. An error should only be returned if an attempt
|
||||
// to specify a token was found, but the information was somehow incorrectly
|
||||
// formed. In the case where a token is simply not present, this should not
|
||||
// be treated as an error. An empty string should be returned in that case.
|
||||
type TokenExtractor func(r *http.Request) (string, error)
|
||||
|
||||
// Options is a struct for specifying configuration options for the middleware.
|
||||
type Options struct {
|
||||
// The function that will return the Key to validate the JWT.
|
||||
// It can be either a shared secret or a public key.
|
||||
// Default value: nil
|
||||
ValidationKeyGetter jwt.Keyfunc
|
||||
// The name of the property in the request where the user information
|
||||
// from the JWT will be stored.
|
||||
// Default value: "user"
|
||||
UserProperty string
|
||||
// The function that will be called when there's an error validating the token
|
||||
// Default value:
|
||||
ErrorHandler errorHandler
|
||||
// A boolean indicating if the credentials are required or not
|
||||
// Default value: false
|
||||
CredentialsOptional bool
|
||||
// A function that extracts the token from the request
|
||||
// Default: FromAuthHeader (i.e., from Authorization header as bearer token)
|
||||
Extractor TokenExtractor
|
||||
// Debug flag turns on debugging output
|
||||
// Default: false
|
||||
Debug bool
|
||||
// When set, all requests with the OPTIONS method will use authentication
|
||||
// Default: false
|
||||
EnableAuthOnOptions bool
|
||||
// When set, the middelware verifies that tokens are signed with the specific signing algorithm
|
||||
// If the signing method is not constant the ValidationKeyGetter callback can be used to implement additional checks
|
||||
// Important to avoid security issues described here: https://auth0.com/blog/critical-vulnerabilities-in-json-web-token-libraries/
|
||||
// Default: nil
|
||||
SigningMethod jwt.SigningMethod
|
||||
}
|
||||
|
||||
type JWTMiddleware struct {
|
||||
Options Options
|
||||
}
|
||||
|
||||
func OnError(w http.ResponseWriter, r *http.Request, err string) {
|
||||
http.Error(w, err, http.StatusUnauthorized)
|
||||
}
|
||||
|
||||
// New constructs a new Secure instance with supplied options.
|
||||
func New(options ...Options) *JWTMiddleware {
|
||||
|
||||
var opts Options
|
||||
if len(options) == 0 {
|
||||
opts = Options{}
|
||||
} else {
|
||||
opts = options[0]
|
||||
}
|
||||
|
||||
if opts.UserProperty == "" {
|
||||
opts.UserProperty = "user"
|
||||
}
|
||||
|
||||
if opts.ErrorHandler == nil {
|
||||
opts.ErrorHandler = OnError
|
||||
}
|
||||
|
||||
if opts.Extractor == nil {
|
||||
opts.Extractor = FromAuthHeader
|
||||
}
|
||||
|
||||
return &JWTMiddleware{
|
||||
Options: opts,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *JWTMiddleware) logf(format string, args ...interface{}) {
|
||||
if m.Options.Debug {
|
||||
log.Printf(format, args...)
|
||||
}
|
||||
}
|
||||
|
||||
// HandlerWithNext is a special implementation for Negroni, but could be used elsewhere.
|
||||
func (m *JWTMiddleware) HandlerWithNext(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
|
||||
err := m.CheckJWT(w, r)
|
||||
|
||||
// If there was an error, do not call next.
|
||||
if err == nil && next != nil {
|
||||
next(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *JWTMiddleware) Handler(h http.Handler) http.Handler {
|
||||
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Let secure process the request. If it returns an error,
|
||||
// that indicates the request should not continue.
|
||||
err := m.CheckJWT(w, r)
|
||||
|
||||
// If there was an error, do not continue.
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
h.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
// FromAuthHeader is a "TokenExtractor" that takes a give request and extracts
|
||||
// the JWT token from the Authorization header.
|
||||
func FromAuthHeader(r *http.Request) (string, error) {
|
||||
authHeader := r.Header.Get("Authorization")
|
||||
if authHeader == "" {
|
||||
return "", nil // No error, just no token
|
||||
}
|
||||
|
||||
// TODO: Make this a bit more robust, parsing-wise
|
||||
authHeaderParts := strings.Fields(authHeader)
|
||||
if len(authHeaderParts) != 2 || strings.ToLower(authHeaderParts[0]) != "bearer" {
|
||||
return "", errors.New("Authorization header format must be Bearer {token}")
|
||||
}
|
||||
|
||||
return authHeaderParts[1], nil
|
||||
}
|
||||
|
||||
// FromParameter returns a function that extracts the token from the specified
|
||||
// query string parameter
|
||||
func FromParameter(param string) TokenExtractor {
|
||||
return func(r *http.Request) (string, error) {
|
||||
return r.URL.Query().Get(param), nil
|
||||
}
|
||||
}
|
||||
|
||||
// FromFirst returns a function that runs multiple token extractors and takes the
|
||||
// first token it finds
|
||||
func FromFirst(extractors ...TokenExtractor) TokenExtractor {
|
||||
return func(r *http.Request) (string, error) {
|
||||
for _, ex := range extractors {
|
||||
token, err := ex(r)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if token != "" {
|
||||
return token, nil
|
||||
}
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
}
|
||||
|
||||
func (m *JWTMiddleware) CheckJWT(w http.ResponseWriter, r *http.Request) error {
|
||||
if !m.Options.EnableAuthOnOptions {
|
||||
if r.Method == "OPTIONS" {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// Use the specified token extractor to extract a token from the request
|
||||
token, err := m.Options.Extractor(r)
|
||||
|
||||
// If debugging is turned on, log the outcome
|
||||
if err != nil {
|
||||
m.logf("Error extracting JWT: %v", err)
|
||||
} else {
|
||||
m.logf("Token extracted: %s", token)
|
||||
}
|
||||
|
||||
// If an error occurs, call the error handler and return an error
|
||||
if err != nil {
|
||||
m.Options.ErrorHandler(w, r, err.Error())
|
||||
return fmt.Errorf("Error extracting token: %w", err)
|
||||
}
|
||||
|
||||
// If the token is empty...
|
||||
if token == "" {
|
||||
// Check if it was required
|
||||
if m.Options.CredentialsOptional {
|
||||
m.logf(" No credentials found (CredentialsOptional=true)")
|
||||
// No error, just no token (and that is ok given that CredentialsOptional is true)
|
||||
return nil
|
||||
}
|
||||
|
||||
// If we get here, the required token is missing
|
||||
errorMsg := "Required authorization token not found"
|
||||
m.Options.ErrorHandler(w, r, errorMsg)
|
||||
m.logf(" Error: No credentials found (CredentialsOptional=false)")
|
||||
return fmt.Errorf(errorMsg)
|
||||
}
|
||||
|
||||
// Now parse the token
|
||||
parsedToken, err := jwt.Parse(token, m.Options.ValidationKeyGetter)
|
||||
|
||||
// Check if there was an error in parsing...
|
||||
if err != nil {
|
||||
m.logf("error parsing token: %v", err)
|
||||
m.Options.ErrorHandler(w, r, err.Error())
|
||||
return fmt.Errorf("Error parsing token: %w", err)
|
||||
}
|
||||
|
||||
if m.Options.SigningMethod != nil && m.Options.SigningMethod.Alg() != parsedToken.Header["alg"] {
|
||||
message := fmt.Sprintf("Expected %s signing method but token specified %s",
|
||||
m.Options.SigningMethod.Alg(),
|
||||
parsedToken.Header["alg"])
|
||||
m.logf("Error validating token algorithm: %s", message)
|
||||
m.Options.ErrorHandler(w, r, errors.New(message).Error())
|
||||
return fmt.Errorf("Error validating token algorithm: %s", message)
|
||||
}
|
||||
|
||||
// Check if the parsed token is valid...
|
||||
if !parsedToken.Valid {
|
||||
m.logf("Token is invalid")
|
||||
m.Options.ErrorHandler(w, r, "The token isn't valid")
|
||||
return errors.New("token is invalid")
|
||||
}
|
||||
|
||||
m.logf("JWT: %v", parsedToken)
|
||||
|
||||
// If we get here, everything worked and we can set the
|
||||
// user property in context.
|
||||
newRequest := r.WithContext(context.WithValue(r.Context(), m.Options.UserProperty, parsedToken)) //nolint
|
||||
// Update the current request with the new context information.
|
||||
*r = *newRequest
|
||||
return nil
|
||||
}
|
@ -2,40 +2,38 @@ package http
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/gob"
|
||||
log "github.com/sirupsen/logrus"
|
||||
s "github.com/wiretrustee/wiretrustee/management/server"
|
||||
handler2 "github.com/wiretrustee/wiretrustee/management/server/http/handler"
|
||||
middleware2 "github.com/wiretrustee/wiretrustee/management/server/http/middleware"
|
||||
"github.com/wiretrustee/wiretrustee/management/server/http/handler"
|
||||
"github.com/wiretrustee/wiretrustee/management/server/http/middleware"
|
||||
"golang.org/x/crypto/acme/autocert"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/codegangsta/negroni"
|
||||
"github.com/gorilla/sessions"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
server *http.Server
|
||||
config *s.HttpServerConfig
|
||||
certManager *autocert.Manager
|
||||
server *http.Server
|
||||
config *s.HttpServerConfig
|
||||
certManager *autocert.Manager
|
||||
accountManager *s.AccountManager
|
||||
}
|
||||
|
||||
// NewHttpsServer creates a new HTTPs server (with HTTPS support)
|
||||
// The listening address will be :443 no matter what was specified in s.HttpServerConfig.Address
|
||||
func NewHttpsServer(config *s.HttpServerConfig, certManager *autocert.Manager) *Server {
|
||||
func NewHttpsServer(config *s.HttpServerConfig, certManager *autocert.Manager, accountManager *s.AccountManager) *Server {
|
||||
server := &http.Server{
|
||||
Addr: config.Address,
|
||||
WriteTimeout: time.Second * 15,
|
||||
ReadTimeout: time.Second * 15,
|
||||
IdleTimeout: time.Second * 60,
|
||||
}
|
||||
return &Server{server: server, config: config, certManager: certManager}
|
||||
return &Server{server: server, config: config, certManager: certManager, accountManager: accountManager}
|
||||
}
|
||||
|
||||
// NewHttpServer creates a new HTTP server (without HTTPS)
|
||||
func NewHttpServer(config *s.HttpServerConfig) *Server {
|
||||
return NewHttpsServer(config, nil)
|
||||
func NewHttpServer(config *s.HttpServerConfig, accountManager *s.AccountManager) *Server {
|
||||
return NewHttpsServer(config, nil, accountManager)
|
||||
}
|
||||
|
||||
// Stop stops the http server
|
||||
@ -50,25 +48,23 @@ func (s *Server) Stop(ctx context.Context) error {
|
||||
// Start defines http handlers and starts the http server. Blocks until server is shutdown.
|
||||
func (s *Server) Start() error {
|
||||
|
||||
sessionStore := sessions.NewFilesystemStore("", []byte("something-very-secret"))
|
||||
authenticator, err := middleware2.NewAuthenticator(s.config.AuthDomain, s.config.AuthClientId, s.config.AuthClientSecret, s.config.AuthCallback)
|
||||
jwtMiddleware, err := middleware.NewJwtMiddleware(s.config.AuthIssuer, s.config.AuthAudience, s.config.AuthKeysLocation)
|
||||
if err != nil {
|
||||
log.Errorf("failed cerating authentication middleware %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
gob.Register(map[string]interface{}{})
|
||||
|
||||
r := http.NewServeMux()
|
||||
s.server.Handler = r
|
||||
|
||||
r.Handle("/login", handler2.NewLogin(authenticator, sessionStore))
|
||||
r.Handle("/logout", handler2.NewLogout(s.config.AuthDomain, s.config.AuthClientId))
|
||||
r.Handle("/callback", handler2.NewCallback(authenticator, sessionStore))
|
||||
r.Handle("/dashboard", negroni.New(
|
||||
negroni.HandlerFunc(middleware2.NewAuth(sessionStore).IsAuthenticated),
|
||||
negroni.Wrap(handler2.NewDashboard(sessionStore))),
|
||||
)
|
||||
// serve public website
|
||||
uiPath := filepath.Clean(s.config.UIFilesLocation)
|
||||
fs := http.FileServer(http.Dir(uiPath))
|
||||
r.Handle("/", fs)
|
||||
fsStatic := http.FileServer(http.Dir(filepath.Join(uiPath, "static/")))
|
||||
r.Handle("/static/", http.StripPrefix("/static/", fsStatic))
|
||||
|
||||
r.Handle("/api/peers", jwtMiddleware.Handler(handler.NewPeers(s.accountManager)))
|
||||
r.Handle("/api/setup-keys", jwtMiddleware.Handler(handler.NewSetupKeysHandler(s.accountManager)))
|
||||
http.Handle("/", r)
|
||||
|
||||
if s.certManager != nil {
|
||||
|
@ -1,21 +0,0 @@
|
||||
package template
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
func RenderTemplate(w http.ResponseWriter, tmpl string, data interface{}) {
|
||||
cwd, _ := os.Getwd()
|
||||
t, err := template.ParseFiles(filepath.Join(cwd, "./routes/"+tmpl+"/"+tmpl+".html"))
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
err = t.Execute(w, data)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
}
|
@ -426,7 +426,12 @@ func startServer(config *server.Config) (*grpc.Server, net.Listener) {
|
||||
lis, err := net.Listen("tcp", ":0")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
s := grpc.NewServer()
|
||||
mgmtServer, err := grpc2.NewServer(config)
|
||||
store, err := server.NewStore(config.Datadir)
|
||||
if err != nil {
|
||||
log.Fatalf("failed creating a store: %s: %v", config.Datadir, err)
|
||||
}
|
||||
accountManager := server.NewManager(store)
|
||||
mgmtServer, err := grpc2.NewServer(config, accountManager)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
mgmtProto.RegisterManagementServiceServer(s, mgmtServer)
|
||||
go func() {
|
||||
|
Loading…
Reference in New Issue
Block a user