mirror of
https://github.com/netbirdio/netbird.git
synced 2026-04-16 15:26:40 +00: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:
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user