refactor: move grpc and http APIs to separate packages

This commit is contained in:
braginini
2021-08-07 13:51:17 +02:00
parent 08d44b1d5f
commit 9f0c86c28e
11 changed files with 39 additions and 36 deletions

View File

@@ -0,0 +1,96 @@
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)
}

View File

@@ -0,0 +1,44 @@
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"])
}

View File

@@ -0,0 +1,58 @@
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)
}

View File

@@ -0,0 +1,48 @@
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)
}

View File

@@ -0,0 +1,39 @@
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
}

View File

@@ -0,0 +1,31 @@
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)
}
}

View File

@@ -0,0 +1,91 @@
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"
"golang.org/x/crypto/acme/autocert"
"net/http"
"time"
"github.com/codegangsta/negroni"
"github.com/gorilla/sessions"
)
type Server struct {
server *http.Server
config *s.HttpServerConfig
certManager *autocert.Manager
}
// 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 {
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}
}
// NewHttpServer creates a new HTTP server (without HTTPS)
func NewHttpServer(config *s.HttpServerConfig) *Server {
return NewHttpsServer(config, nil)
}
// Stop stops the http server
func (s *Server) Stop(ctx context.Context) error {
err := s.server.Shutdown(ctx)
if err != nil {
return err
}
return nil
}
// 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)
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))),
)
http.Handle("/", r)
if s.certManager != nil {
// if HTTPS is enabled we reuse the listener from the cert manager
listener := s.certManager.Listener()
log.Infof("http server listening on %s", listener.Addr())
if err = http.Serve(listener, s.certManager.HTTPHandler(r)); err != nil {
log.Errorf("failed to serve https server: %v", err)
return err
}
} else {
log.Infof("http server listening on %s", s.server.Addr)
if err = s.server.ListenAndServe(); err != nil {
log.Errorf("failed to serve http server: %v", err)
return err
}
}
return nil
}

View File

@@ -0,0 +1,21 @@
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)
}
}