Commit e81e4690 authored by Lee Brown's avatar Lee Brown

Completed user login with session authentication

parent 227af02f
......@@ -381,7 +381,7 @@ schema migrations before running any unit tests.
To login to the local Postgres container, use the following command:
```bash
docker exec -it saas-starter-kit_postgres_1 /bin/bash
bash-4.4# psql -u postgres shared
bash-4.4# psql -U postgres shared
```
The example project currently only includes a few tables. As more functionality is built into both the web-app and
......
package handlers
import (
"geeks-accelerator/oss/saas-starter-kit/internal/platform/web/webcontext"
"log"
"net/http"
"os"
......@@ -15,7 +16,7 @@ import (
)
// API returns a handler for a set of routes.
func API(shutdown chan os.Signal, log *log.Logger, env web.Env, masterDB *sqlx.DB, redis *redis.Client, authenticator *auth.Authenticator, globalMids ...web.Middleware) http.Handler {
func API(shutdown chan os.Signal, log *log.Logger, env webcontext.Env, masterDB *sqlx.DB, redis *redis.Client, authenticator *auth.Authenticator, globalMids ...web.Middleware) http.Handler {
// Define base middlewares applied to all requests.
middlewares := []web.Middleware{
......@@ -43,14 +44,14 @@ func API(shutdown chan os.Signal, log *log.Logger, env web.Env, masterDB *sqlx.D
MasterDB: masterDB,
TokenGenerator: authenticator,
}
app.Handle("GET", "/v1/users", u.Find, mid.Authenticate(authenticator))
app.Handle("POST", "/v1/users", u.Create, mid.Authenticate(authenticator), mid.HasRole(auth.RoleAdmin))
app.Handle("GET", "/v1/users/:id", u.Read, mid.Authenticate(authenticator))
app.Handle("PATCH", "/v1/users", u.Update, mid.Authenticate(authenticator))
app.Handle("PATCH", "/v1/users/password", u.UpdatePassword, mid.Authenticate(authenticator))
app.Handle("PATCH", "/v1/users/archive", u.Archive, mid.Authenticate(authenticator), mid.HasRole(auth.RoleAdmin))
app.Handle("DELETE", "/v1/users/:id", u.Delete, mid.Authenticate(authenticator), mid.HasRole(auth.RoleAdmin))
app.Handle("PATCH", "/v1/users/switch-account/:account_id", u.SwitchAccount, mid.Authenticate(authenticator))
app.Handle("GET", "/v1/users", u.Find, mid.AuthenticateHeader(authenticator))
app.Handle("POST", "/v1/users", u.Create, mid.AuthenticateHeader(authenticator), mid.HasRole(auth.RoleAdmin))
app.Handle("GET", "/v1/users/:id", u.Read, mid.AuthenticateHeader(authenticator))
app.Handle("PATCH", "/v1/users", u.Update, mid.AuthenticateHeader(authenticator))
app.Handle("PATCH", "/v1/users/password", u.UpdatePassword, mid.AuthenticateHeader(authenticator))
app.Handle("PATCH", "/v1/users/archive", u.Archive, mid.AuthenticateHeader(authenticator), mid.HasRole(auth.RoleAdmin))
app.Handle("DELETE", "/v1/users/:id", u.Delete, mid.AuthenticateHeader(authenticator), mid.HasRole(auth.RoleAdmin))
app.Handle("PATCH", "/v1/users/switch-account/:account_id", u.SwitchAccount, mid.AuthenticateHeader(authenticator))
// This route is not authenticated
app.Handle("POST", "/v1/oauth/token", u.Token)
......@@ -59,19 +60,19 @@ func API(shutdown chan os.Signal, log *log.Logger, env web.Env, masterDB *sqlx.D
ua := UserAccount{
MasterDB: masterDB,
}
app.Handle("GET", "/v1/user_accounts", ua.Find, mid.Authenticate(authenticator))
app.Handle("POST", "/v1/user_accounts", ua.Create, mid.Authenticate(authenticator), mid.HasRole(auth.RoleAdmin))
app.Handle("GET", "/v1/user_accounts/:id", ua.Read, mid.Authenticate(authenticator))
app.Handle("PATCH", "/v1/user_accounts", ua.Update, mid.Authenticate(authenticator))
app.Handle("PATCH", "/v1/user_accounts/archive", ua.Archive, mid.Authenticate(authenticator), mid.HasRole(auth.RoleAdmin))
app.Handle("DELETE", "/v1/user_accounts", ua.Delete, mid.Authenticate(authenticator), mid.HasRole(auth.RoleAdmin))
app.Handle("GET", "/v1/user_accounts", ua.Find, mid.AuthenticateHeader(authenticator))
app.Handle("POST", "/v1/user_accounts", ua.Create, mid.AuthenticateHeader(authenticator), mid.HasRole(auth.RoleAdmin))
app.Handle("GET", "/v1/user_accounts/:id", ua.Read, mid.AuthenticateHeader(authenticator))
app.Handle("PATCH", "/v1/user_accounts", ua.Update, mid.AuthenticateHeader(authenticator))
app.Handle("PATCH", "/v1/user_accounts/archive", ua.Archive, mid.AuthenticateHeader(authenticator), mid.HasRole(auth.RoleAdmin))
app.Handle("DELETE", "/v1/user_accounts", ua.Delete, mid.AuthenticateHeader(authenticator), mid.HasRole(auth.RoleAdmin))
// Register account endpoints.
a := Account{
MasterDB: masterDB,
}
app.Handle("GET", "/v1/accounts/:id", a.Read, mid.Authenticate(authenticator))
app.Handle("PATCH", "/v1/accounts", a.Update, mid.Authenticate(authenticator), mid.HasRole(auth.RoleAdmin))
app.Handle("GET", "/v1/accounts/:id", a.Read, mid.AuthenticateHeader(authenticator))
app.Handle("PATCH", "/v1/accounts", a.Update, mid.AuthenticateHeader(authenticator), mid.HasRole(auth.RoleAdmin))
// Register signup endpoints.
s := Signup{
......@@ -83,12 +84,12 @@ func API(shutdown chan os.Signal, log *log.Logger, env web.Env, masterDB *sqlx.D
p := Project{
MasterDB: masterDB,
}
app.Handle("GET", "/v1/projects", p.Find, mid.Authenticate(authenticator))
app.Handle("POST", "/v1/projects", p.Create, mid.Authenticate(authenticator), mid.HasRole(auth.RoleAdmin))
app.Handle("GET", "/v1/projects/:id", p.Read, mid.Authenticate(authenticator))
app.Handle("PATCH", "/v1/projects", p.Update, mid.Authenticate(authenticator), mid.HasRole(auth.RoleAdmin))
app.Handle("PATCH", "/v1/projects/archive", p.Archive, mid.Authenticate(authenticator), mid.HasRole(auth.RoleAdmin))
app.Handle("DELETE", "/v1/projects/:id", p.Delete, mid.Authenticate(authenticator), mid.HasRole(auth.RoleAdmin))
app.Handle("GET", "/v1/projects", p.Find, mid.AuthenticateHeader(authenticator))
app.Handle("POST", "/v1/projects", p.Create, mid.AuthenticateHeader(authenticator), mid.HasRole(auth.RoleAdmin))
app.Handle("GET", "/v1/projects/:id", p.Read, mid.AuthenticateHeader(authenticator))
app.Handle("PATCH", "/v1/projects", p.Update, mid.AuthenticateHeader(authenticator), mid.HasRole(auth.RoleAdmin))
app.Handle("PATCH", "/v1/projects/archive", p.Archive, mid.AuthenticateHeader(authenticator), mid.HasRole(auth.RoleAdmin))
app.Handle("DELETE", "/v1/projects/:id", p.Delete, mid.AuthenticateHeader(authenticator), mid.HasRole(auth.RoleAdmin))
// Register swagger documentation.
// TODO: Add authentication. Current authenticator requires an Authorization header
......
......@@ -91,6 +91,7 @@ func main() {
HostNames []string `envconfig:"HOST_NAMES" example:"alternative-subdomain.eproc.tech"`
EnableHTTPS bool `default:"false" envconfig:"ENABLE_HTTPS"`
TemplateDir string `default:"./templates" envconfig:"TEMPLATE_DIR"`
WebAppBaseUrl string `default:"http://127.0.0.1:3000" envconfig:"WEB_APP_BASE_URL" example:"www.eproc.tech"`
DebugHost string `default:"0.0.0.0:4000" envconfig:"DEBUG_HOST"`
ShutdownTimeout time.Duration `default:"5s" envconfig:"SHUTDOWN_TIMEOUT"`
}
......
......@@ -2,32 +2,63 @@ package handlers
import (
"context"
"geeks-accelerator/oss/saas-starter-kit/internal/platform/auth"
"fmt"
"net/http"
"geeks-accelerator/oss/saas-starter-kit/internal/platform/auth"
"geeks-accelerator/oss/saas-starter-kit/internal/platform/web"
"geeks-accelerator/oss/saas-starter-kit/internal/platform/web/webcontext"
project_routes "geeks-accelerator/oss/saas-starter-kit/internal/project-routes"
"github.com/jmoiron/sqlx"
)
// User represents the User API method handler set.
// Root represents the Root API method handler set.
type Root struct {
MasterDB *sqlx.DB
Renderer web.Renderer
// ADD OTHER STATE LIKE THE LOGGER AND CONFIG HERE.
MasterDB *sqlx.DB
Renderer web.Renderer
ProjectRoutes project_routes.ProjectRoutes
}
// List returns all the existing users in the system.
func (u *Root) Index(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
// Index determines if the user has authentication and loads the associated page.
func (h *Root) Index(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
// Force users to login to access the index page.
if claims, err := auth.ClaimsFromContext(ctx); err != nil || !claims.HasAuth() {
http.Redirect(w, r, "/user/login", http.StatusFound)
return nil
if claims, err := auth.ClaimsFromContext(ctx); err == nil && claims.HasAuth() {
return h.indexDashboard(ctx, w, r, params)
}
return h.indexDefault(ctx, w, r, params)
}
// indexDashboard loads the dashboard for a user when they are authenticated.
func (h *Root) indexDashboard(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
data := map[string]interface{}{
"imgSizes": []int{100, 200, 300, 400, 500},
}
return u.Renderer.Render(ctx, w, r, tmplLayoutBase, "root-index.tmpl", web.MIMETextHTMLCharsetUTF8, http.StatusOK, data)
return h.Renderer.Render(ctx, w, r, tmplLayoutBase, "root-dashboard.gohtml", web.MIMETextHTMLCharsetUTF8, http.StatusOK, data)
}
// indexDefault loads the root index page when a user has no authentication.
func (u *Root) indexDefault(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
http.Redirect(w, r, "/user/login", http.StatusFound)
return nil
}
// IndexHtml redirects /index.html to the website root page.
func (u *Root) IndexHtml(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
http.Redirect(w, r, "/", http.StatusMovedPermanently)
return nil
}
// RobotHandler returns a robots.txt response.
func (h *Root) RobotTxt(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
if webcontext.ContextEnv(ctx) != webcontext.Env_Prod {
txt := "User-agent: *\nDisallow: /"
return web.RespondText(ctx, w, txt, http.StatusOK)
}
sitemapUrl := h.ProjectRoutes.WebAppUrl("/sitemap.xml")
txt := fmt.Sprintf("User-agent: *\nDisallow: /ping\nDisallow: /status\nDisallow: /debug/\nSitemap: %s", sitemapUrl)
return web.RespondText(ctx, w, txt, http.StatusOK)
}
......@@ -12,6 +12,7 @@ import (
"geeks-accelerator/oss/saas-starter-kit/internal/platform/web"
"geeks-accelerator/oss/saas-starter-kit/internal/platform/web/webcontext"
"geeks-accelerator/oss/saas-starter-kit/internal/platform/web/weberror"
project_routes "geeks-accelerator/oss/saas-starter-kit/internal/project-routes"
"github.com/jmoiron/sqlx"
"gopkg.in/DataDog/dd-trace-go.v1/contrib/go-redis/redis"
)
......@@ -22,7 +23,7 @@ const (
)
// API returns a handler for a set of routes.
func APP(shutdown chan os.Signal, log *log.Logger, env webcontext.Env, staticDir, templateDir string, masterDB *sqlx.DB, redis *redis.Client, authenticator *auth.Authenticator, renderer web.Renderer, globalMids ...web.Middleware) http.Handler {
func APP(shutdown chan os.Signal, log *log.Logger, env webcontext.Env, staticDir, templateDir string, masterDB *sqlx.DB, redis *redis.Client, authenticator *auth.Authenticator, projectRoutes project_routes.ProjectRoutes, renderer web.Renderer, globalMids ...web.Middleware) http.Handler {
// Define base middlewares applied to all requests.
middlewares := []web.Middleware{
......@@ -42,7 +43,7 @@ func APP(shutdown chan os.Signal, log *log.Logger, env webcontext.Env, staticDir
MasterDB: masterDB,
Renderer: renderer,
}
app.Handle("GET", "/projects", p.Index, mid.HasAuth())
app.Handle("GET", "/projects", p.Index, mid.AuthenticateSessionRequired(authenticator), mid.HasAuth())
// Register user management and authentication endpoints.
u := User{
......@@ -69,12 +70,14 @@ func APP(shutdown chan os.Signal, log *log.Logger, env webcontext.Env, staticDir
// Register root
r := Root{
MasterDB: masterDB,
Renderer: renderer,
MasterDB: masterDB,
Renderer: renderer,
ProjectRoutes: projectRoutes,
}
// This route is not authenticated
app.Handle("GET", "/index.html", r.Index)
app.Handle("GET", "/", r.Index)
app.Handle("GET", "/", r.Index, mid.AuthenticateSessionOptional(authenticator))
app.Handle("GET", "/index.html", r.IndexHtml)
app.Handle("GET", "/robots.txt", r.RobotTxt)
// Register health check endpoint. This route is not authenticated.
check := Check{
......@@ -84,6 +87,7 @@ func APP(shutdown chan os.Signal, log *log.Logger, env webcontext.Env, staticDir
}
app.Handle("GET", "/v1/health", check.Health)
// Handle static files/pages. Render a custom 404 page when file not found.
static := func(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
err := web.StaticHandler(ctx, w, r, params, staticDir, "")
if err != nil {
......
......@@ -27,6 +27,11 @@ type Signup struct {
// Step1 handles collecting the first detailed needed to create a new account.
func (h *Signup) Step1(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
ctxValues, err := webcontext.ContextValues(ctx)
if err != nil {
return err
}
//
req := new(signup.SignupRequest)
data := make(map[string]interface{})
......@@ -46,7 +51,7 @@ func (h *Signup) Step1(ctx context.Context, w http.ResponseWriter, r *http.Reque
}
// Execute the account / user signup.
res, err := signup.Signup(ctx, claims, h.MasterDB, *req, time.Now())
_, err = signup.Signup(ctx, claims, h.MasterDB, *req, ctxValues.Now)
if err != nil {
switch errors.Cause(err) {
case account.ErrForbidden:
......@@ -54,21 +59,27 @@ func (h *Signup) Step1(ctx context.Context, w http.ResponseWriter, r *http.Reque
default:
if verr, ok := weberror.NewValidationError(ctx, err); ok {
data["validationErrors"] = verr.(*weberror.Error)
return nil
} else {
return err
}
}
} else {
// Authenticated the new user.
userAuth, err := user.Authenticate(ctx, h.MasterDB, h.Authenticator, res.User.Email, req.User.Password, time.Hour, time.Now())
if err != nil {
return err
}
}
// Authenticated the new user.
token, err := user.Authenticate(ctx, h.MasterDB, h.Authenticator, req.User.Email, req.User.Password, time.Hour, ctxValues.Now)
if err != nil {
return err
}
_ = userAuth.Expiry
_ = userAuth.AccessToken
// Add the token to the users session.
err = handleSessionToken(ctx, w, r, token)
if err != nil {
return err
}
// Redirect the user to the dashboard.
http.Redirect(w, r, "/", http.StatusFound)
}
return nil
......
......@@ -2,11 +2,20 @@ package handlers
import (
"context"
"geeks-accelerator/oss/saas-starter-kit/internal/platform/auth"
"net/http"
"time"
"geeks-accelerator/oss/saas-starter-kit/internal/account"
"geeks-accelerator/oss/saas-starter-kit/internal/platform/auth"
"geeks-accelerator/oss/saas-starter-kit/internal/platform/web"
"geeks-accelerator/oss/saas-starter-kit/internal/platform/web/webcontext"
"geeks-accelerator/oss/saas-starter-kit/internal/platform/web/weberror"
"geeks-accelerator/oss/saas-starter-kit/internal/user"
"github.com/gorilla/schema"
"github.com/gorilla/sessions"
"github.com/jmoiron/sqlx"
"github.com/pborman/uuid"
"github.com/pkg/errors"
)
// User represents the User API method handler set.
......@@ -16,20 +25,138 @@ type User struct {
Authenticator *auth.Authenticator
}
type UserLoginRequest struct {
user.AuthenticateRequest
RememberMe bool
}
// List returns all the existing users in the system.
func (u *User) Login(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
func (h *User) Login(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
ctxValues, err := webcontext.ContextValues(ctx)
if err != nil {
return err
}
//
req := new(UserLoginRequest)
data := make(map[string]interface{})
f := func() error {
if r.Method == http.MethodPost {
err := r.ParseForm()
if err != nil {
return err
}
decoder := schema.NewDecoder()
if err := decoder.Decode(req, r.PostForm); err != nil {
return err
}
if err := webcontext.Validator().Struct(req); err != nil {
if ne, ok := weberror.NewValidationError(ctx, err); ok {
data["validationErrors"] = ne.(*weberror.Error)
return nil
} else {
return err
}
}
sessionTTL := time.Hour
if req.RememberMe {
sessionTTL = time.Hour * 36
}
// Authenticated the user.
token, err := user.Authenticate(ctx, h.MasterDB, h.Authenticator, req.Email, req.Password, sessionTTL, ctxValues.Now)
if err != nil {
switch errors.Cause(err) {
case account.ErrForbidden:
return web.RespondError(ctx, w, weberror.NewError(ctx, err, http.StatusForbidden))
default:
if verr, ok := weberror.NewValidationError(ctx, err); ok {
data["validationErrors"] = verr.(*weberror.Error)
return nil
} else {
return err
}
}
}
// Add the token to the users session.
err = handleSessionToken(ctx, w, r, token)
if err != nil {
return err
}
// Redirect the user to the dashboard.
http.Redirect(w, r, "/", http.StatusFound)
}
return nil
}
if err := f(); err != nil {
return web.RenderError(ctx, w, r, err, h.Renderer, tmplLayoutBase, tmplContentErrorGeneric, web.MIMETextHTMLCharsetUTF8)
}
return u.Renderer.Render(ctx, w, r, tmplLayoutBase, "user-login.tmpl", web.MIMETextHTMLCharsetUTF8, http.StatusOK, nil)
data["form"] = req
if verr, ok := weberror.NewValidationError(ctx, webcontext.Validator().Struct(UserLoginRequest{})); ok {
data["validationDefaults"] = verr.(*weberror.Error)
}
return h.Renderer.Render(ctx, w, r, tmplLayoutBase, "user-login.tmpl", web.MIMETextHTMLCharsetUTF8, http.StatusOK, data)
}
// List returns all the existing users in the system.
func (u *User) Logout(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
// handleSessionToken persists the access token to the session for request authentication.
func handleSessionToken(ctx context.Context, w http.ResponseWriter, r *http.Request, token user.Token) error {
if token.AccessToken == "" {
return errors.New("accessToken is required.")
}
sess := webcontext.ContextSession(ctx)
if sess.IsNew {
sess.ID = uuid.NewRandom().String()
}
sess.Options = &sessions.Options{
Path: "/",
MaxAge: int(token.TTL.Seconds()),
HttpOnly: false,
}
sess = webcontext.SessionWithAccessToken(sess, token.AccessToken)
if err := sess.Save(r, w); err != nil {
return err
}
return nil
}
// Logout handles removing authentication for the user.
func (h *User) Logout(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
sess := webcontext.ContextSession(ctx)
// Set the access token to empty to logout the user.
sess = webcontext.SessionWithAccessToken(sess, "")
if err := sess.Save(r, w); err != nil {
return err
}
// Redirect the user to the root page.
http.Redirect(w, r, "/", http.StatusFound)
return u.Renderer.Render(ctx, w, r, tmplLayoutBase, "user-logout.tmpl", web.MIMETextHTMLCharsetUTF8, http.StatusOK, nil)
return nil
}
// List returns all the existing users in the system.
func (u *User) ForgotPassword(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
func (h *User) ForgotPassword(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
return u.Renderer.Render(ctx, w, r, tmplLayoutBase, "user-forgot-password.tmpl", web.MIMETextHTMLCharsetUTF8, http.StatusOK, nil)
return h.Renderer.Render(ctx, w, r, tmplLayoutBase, "user-forgot-password.tmpl", web.MIMETextHTMLCharsetUTF8, http.StatusOK, nil)
}
......@@ -6,9 +6,6 @@ import (
"encoding/json"
"expvar"
"fmt"
"geeks-accelerator/oss/saas-starter-kit/internal/platform/auth"
"geeks-accelerator/oss/saas-starter-kit/internal/platform/web/webcontext"
"geeks-accelerator/oss/saas-starter-kit/internal/platform/web/weberror"
"html/template"
"log"
"net"
......@@ -25,18 +22,25 @@ import (
"geeks-accelerator/oss/saas-starter-kit/cmd/web-app/handlers"
"geeks-accelerator/oss/saas-starter-kit/internal/mid"
"geeks-accelerator/oss/saas-starter-kit/internal/platform/auth"
"geeks-accelerator/oss/saas-starter-kit/internal/platform/devops"
"geeks-accelerator/oss/saas-starter-kit/internal/platform/flag"
img_resize "geeks-accelerator/oss/saas-starter-kit/internal/platform/img-resize"
"geeks-accelerator/oss/saas-starter-kit/internal/platform/web"
template_renderer "geeks-accelerator/oss/saas-starter-kit/internal/platform/web/template-renderer"
"geeks-accelerator/oss/saas-starter-kit/internal/platform/web/webcontext"
"geeks-accelerator/oss/saas-starter-kit/internal/platform/web/weberror"
project_routes "geeks-accelerator/oss/saas-starter-kit/internal/project-routes"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/credentials"
"github.com/aws/aws-sdk-go/aws/ec2metadata"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/go-redis/redis"
"github.com/gorilla/securecookie"
"github.com/gorilla/sessions"
"github.com/kelseyhightower/envconfig"
"github.com/lib/pq"
"github.com/pkg/errors"
"golang.org/x/crypto/acme"
"golang.org/x/crypto/acme/autocert"
awstrace "gopkg.in/DataDog/dd-trace-go.v1/contrib/aws/aws-sdk-go/aws"
......@@ -90,6 +94,9 @@ func main() {
CloudFrontEnabled bool `envconfig:"CLOUDFRONT_ENABLED"`
ImgResizeEnabled bool `envconfig:"IMG_RESIZE_ENABLED"`
}
WebApiBaseUrl string `default:"http://127.0.0.1:3001" envconfig:"WEB_API_BASE_URL" example:"http://api.eproc.tech"`
SessionKey string `default:"" envconfig:"SESSION_KEY"`
SessionName string `default:"" envconfig:"SESSION_NAME"`
DebugHost string `default:"0.0.0.0:4000" envconfig:"DEBUG_HOST"`
ShutdownTimeout time.Duration `default:"5s" envconfig:"SHUTDOWN_TIMEOUT"`
}
......@@ -382,8 +389,50 @@ func main() {
serviceMiddlewares = append(serviceMiddlewares, redirect)
}
// Init session store
if cfg.Service.SessionName == "" {
cfg.Service.SessionName = fmt.Sprintf("%s-session", cfg.Service.Name)
}
// Set the session key if not provided in the config.
if cfg.Service.SessionKey == "" {
// AWS secrets manager ID for storing the session key. This is optional and only will be used
// if a valid AWS session is provided.
secretID := filepath.Join(cfg.Aws.SecretsManagerConfigPrefix, "session")
// If AWS is enabled, check the Secrets Manager for the session key.
if awsSession != nil {
cfg.Service.SessionKey, err = devops.SecretManagerGetString(awsSession, secretID)
if err != nil && errors.Cause(err) != devops.ErrSecreteNotFound {
log.Fatalf("main : Session : %+v", err)
}
}
// If the session key is still empty, generate a new key.
if cfg.Service.SessionKey == "" {
cfg.Service.SessionKey = string(securecookie.GenerateRandomKey(32))
if awsSession != nil {
err = devops.SecretManagerPutString(awsSession, secretID, cfg.Service.SessionKey)
if err != nil {
log.Fatalf("main : Session : %+v", err)
}
}
}
}
// Generate the new session store and append it to the global list of middlewares.
sessionStore := sessions.NewCookieStore([]byte(cfg.Service.SessionKey))
serviceMiddlewares = append(serviceMiddlewares, mid.Session(sessionStore, cfg.Service.SessionName))
// =========================================================================
// URL Formatter
projectRoutes, err := project_routes.New(cfg.Service.WebApiBaseUrl, cfg.Service.BaseUrl)
if err != nil {
log.Fatalf("main : project routes : %+v", cfg.Service.BaseUrl, err)
}
// s3UrlFormatter is a help function used by to convert an s3 key to
// a publicly available image URL.
var staticS3UrlFormatter func(string) string
......@@ -402,15 +451,7 @@ func main() {
return s3UrlFormatter(p)
}
} else {
baseUrl, err := url.Parse(cfg.Service.BaseUrl)
if err != nil {
log.Fatalf("main : url Parse(%s) : %+v", cfg.Service.BaseUrl, err)
}
staticS3UrlFormatter = func(p string) string {
baseUrl.Path = p
return baseUrl.String()
}
staticS3UrlFormatter = projectRoutes.WebAppUrl
}
// staticUrlFormatter is a help function used by template functions defined below.
......@@ -735,7 +776,7 @@ func main() {
if cfg.HTTP.Host != "" {
api := http.Server{
Addr: cfg.HTTP.Host,
Handler: handlers.APP(shutdown, log, cfg.Env, cfg.Service.StaticFiles.Dir, cfg.Service.TemplateDir, masterDb, redisClient, authenticator, renderer, serviceMiddlewares...),
Handler: handlers.APP(shutdown, log, cfg.Env, cfg.Service.StaticFiles.Dir, cfg.Service.TemplateDir, masterDb, redisClient, authenticator, projectRoutes, renderer, serviceMiddlewares...),
ReadTimeout: cfg.HTTP.ReadTimeout,
WriteTimeout: cfg.HTTP.WriteTimeout,
MaxHeaderBytes: 1 << 20,
......@@ -752,7 +793,7 @@ func main() {
if cfg.HTTPS.Host != "" {
api := http.Server{
Addr: cfg.HTTPS.Host,
Handler: handlers.APP(shutdown, log, cfg.Env, cfg.Service.StaticFiles.Dir, cfg.Service.TemplateDir, masterDb, redisClient, authenticator, renderer, serviceMiddlewares...),
Handler: handlers.APP(shutdown, log, cfg.Env, cfg.Service.StaticFiles.Dir, cfg.Service.TemplateDir, masterDb, redisClient, authenticator, projectRoutes, renderer, serviceMiddlewares...),
ReadTimeout: cfg.HTTPS.ReadTimeout,
WriteTimeout: cfg.HTTPS.WriteTimeout,
MaxHeaderBytes: 1 << 20,
......
......@@ -20,22 +20,24 @@
<div class="text-center">
<h1 class="h4 text-gray-900 mb-4">Welcome Back!</h1>
</div>
<form class="user">
<form class="user" method="post" novalidate>
<div class="form-group">
<input type="email" class="form-control form-control-user" id="loginEmail" aria-describedby="emailHelp" placeholder="Enter Email Address...">
<input type="email" class="form-control form-control-user {{ ValidationFieldClass $.validationErrors "AuthenticateRequest.Email" }}" name="Email" value="{{ $.form.Email }}" placeholder="Enter Email Address...">
{{template "invalid-feedback" dict "validationDefaults" $.validationDefaults "validationErrors" $.validationErrors "fieldName" "AuthenticateRequest.Email" }}
</div>
<div class="form-group">
<input type="password" class="form-control form-control-user" id="loginPassword" placeholder="Password">
<input type="password" class="form-control form-control-user {{ ValidationFieldClass $.validationErrors "AuthenticateRequest.Password" }}" name="Password" value="{{ $.form.Password }}" placeholder="Password">
{{template "invalid-feedback" dict "validationDefaults" $.validationDefaults "validationErrors" $.validationErrors "fieldName" "AuthenticateRequest.Password" }}
</div>
<div class="form-group">
<div class="custom-control custom-checkbox small">
<input type="checkbox" class="custom-control-input" id="customCheck">
<label class="custom-control-label" for="customCheck">Remember Me</label>
<input type="checkbox" class="custom-control-input" id="checkRemberMe" name="RememberMe" value="1" {{ if $.form.RememberMe }}checked="checked"{{end}}>
<label class="custom-control-label" for="checkRemberMe">Remember Me</label>
</div>
</div>
<a href="index.html" class="btn btn-primary btn-user btn-block">
<button class="btn btn-primary btn-user btn-block">
Login
</a>
</button>
<hr>
</form>
<hr>
......
......@@ -59,7 +59,7 @@
<div class="modal-body">Select "Logout" below if you are ready to end your current session.</div>
<div class="modal-footer">
<button class="btn btn-secondary" type="button" data-dismiss="modal">Cancel</button>
<a class="btn btn-primary" href="/logout">Logout</a>
<a class="btn btn-primary" href="/user/logout">Logout</a>
</div>
</div>
</div>
......
......@@ -20,6 +20,8 @@ require (
github.com/google/go-cmp v0.3.0
github.com/google/uuid v1.1.1 // indirect
github.com/gorilla/schema v1.1.0
github.com/gorilla/securecookie v1.1.1
github.com/gorilla/sessions v1.2.0
github.com/huandu/go-sqlbuilder v1.4.1
github.com/iancoleman/strcase v0.0.0-20190422225806-e506e3ef7365
github.com/jmoiron/sqlx v1.2.0
......
......@@ -71,6 +71,9 @@ github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/schema v1.1.0 h1:CamqUDOFUBqzrvxuz2vEwo8+SUdwsluFh7IlzJh30LY=
github.com/gorilla/schema v1.1.0/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU=
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
github.com/gorilla/sessions v1.2.0 h1:S7P+1Hm5V/AT9cjEcUD5uDaQSX0OE577aCXgoaKpYbQ=
github.com/gorilla/sessions v1.2.0/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/huandu/go-sqlbuilder v1.4.1 h1:DYGFGLbOUXhtQ2kwO1uyDIPJbsztmVWdPPDyxi0EJGw=
......
......@@ -2,12 +2,13 @@ package mid
import (
"context"
"geeks-accelerator/oss/saas-starter-kit/internal/platform/web/weberror"
"net/http"
"strings"
"geeks-accelerator/oss/saas-starter-kit/internal/platform/auth"
"geeks-accelerator/oss/saas-starter-kit/internal/platform/web"
"geeks-accelerator/oss/saas-starter-kit/internal/platform/web/webcontext"
"geeks-accelerator/oss/saas-starter-kit/internal/platform/web/weberror"
"github.com/pkg/errors"
"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer"
)
......@@ -22,17 +23,18 @@ func ErrorForbidden(ctx context.Context) error {
}
// Authenticate validates a JWT from the `Authorization` header.
func Authenticate(authenticator *auth.Authenticator) web.Middleware {
func AuthenticateHeader(authenticator *auth.Authenticator) web.Middleware {
// This is the actual middleware function to be executed.
f := func(after web.Handler) web.Handler {
// Wrap this handler around the next one provided.
h := func(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
span, ctx := tracer.StartSpanFromContext(ctx, "internal.mid.Authenticate")
span, ctx := tracer.StartSpanFromContext(ctx, "internal.mid.AuthenticateHeader")
defer span.Finish()
m := func() error {
authHdr := r.Header.Get("Authorization")
if authHdr == "" {
err := errors.New("missing Authorization header")
......@@ -71,6 +73,65 @@ func Authenticate(authenticator *auth.Authenticator) web.Middleware {
return f
}
// AuthenticateSessionRequired requires a JWT access token to be loaded from the session.
func AuthenticateSessionRequired(authenticator *auth.Authenticator) web.Middleware {
return authenticateSession(authenticator, true)
}