about summary refs log tree commit diff
path: root/nix/templates/goapp/frontend/src
diff options
context:
space:
mode:
Diffstat (limited to 'nix/templates/goapp/frontend/src')
-rw-r--r--nix/templates/goapp/frontend/src/db.go37
-rw-r--r--nix/templates/goapp/frontend/src/handlers.go236
-rw-r--r--nix/templates/goapp/frontend/src/init.go76
-rw-r--r--nix/templates/goapp/frontend/src/log.go34
-rw-r--r--nix/templates/goapp/frontend/src/main.go96
-rw-r--r--nix/templates/goapp/frontend/src/sqlitestore.go285
-rw-r--r--nix/templates/goapp/frontend/src/templates.go42
-rw-r--r--nix/templates/goapp/frontend/src/types.go65
-rw-r--r--nix/templates/goapp/frontend/src/util.go58
9 files changed, 929 insertions, 0 deletions
diff --git a/nix/templates/goapp/frontend/src/db.go b/nix/templates/goapp/frontend/src/db.go
new file mode 100644
index 0000000..fd3605a
--- /dev/null
+++ b/nix/templates/goapp/frontend/src/db.go
@@ -0,0 +1,37 @@
+package main
+
+import (
+	"database/sql"
+	"log"
+
+	_ "github.com/mattn/go-sqlite3"
+)
+
+const create string = `
+CREATE TABLE IF NOT EXISTS users (
+	id INTEGER NOT NULL PRIMARY KEY,
+	created_at DATETIME NOT NULL,
+	name TEXT,
+	passwordHash TEXT
+);
+`
+
+type State struct {
+	db       *sql.DB      // the database storing the "business data"
+	sessions *SqliteStore // the database storing sessions
+}
+
+func NewState() (*State, error) {
+	db, err := sql.Open("sqlite3", databasePath)
+	if err != nil {
+		log.Println("Error opening the db: ", err)
+		return nil, err
+	}
+	if _, err := db.Exec(create); err != nil {
+		log.Println("Error creating the tables: ", err)
+		return nil, err
+	}
+	return &State{
+		db: db,
+	}, nil
+}
diff --git a/nix/templates/goapp/frontend/src/handlers.go b/nix/templates/goapp/frontend/src/handlers.go
new file mode 100644
index 0000000..8fdd325
--- /dev/null
+++ b/nix/templates/goapp/frontend/src/handlers.go
@@ -0,0 +1,236 @@
+package main
+
+import (
+	"fmt"
+	"html/template"
+	"log"
+	"net/http"
+
+	"github.com/coreos/go-oidc/v3/oidc"
+	"github.com/gorilla/sessions"
+	"golang.org/x/oauth2"
+)
+
+func indexHandler(w http.ResponseWriter, r *http.Request) {
+	session, err := globalState.sessions.Get(r, "session")
+	if err != nil {
+		log.Println("error getting the session")
+	}
+
+	tpl := indexTplData{
+		Error: r.FormValue("error"),
+	}
+
+	tpl.Breadcrumbs = []Breadcrumb{
+		{
+			Link{"a", "b"},
+			[]Link{
+				{"c", "d"},
+				{"e", "f"},
+			},
+		},
+		{
+			Link{"g", "h"},
+			[]Link{
+				{"i", "j"},
+				{"k", "l"},
+			},
+		},
+	}
+	tpl.NextLinks = []Link{
+		{"Login", "/login"},
+	}
+
+	if logged, ok := session.Values["logged"].(bool); ok && logged {
+		tpl.LoggedIn = true
+		tpl.Claims.IDToken = session.Values["id_token"].(Claims)
+		tpl.Claims.UserInfo = session.Values["userinfo"].(Claims)
+
+		if len(options.GroupsFilter) >= 1 {
+			for _, group := range tpl.Claims.UserInfo.Groups {
+				if isStringInSlice(group, options.GroupsFilter) {
+					tpl.Groups = append(tpl.Groups, filterText(group, options.Filters))
+				}
+			}
+		} else {
+			tpl.Groups = filterSliceOfText(tpl.Claims.UserInfo.Groups, options.Filters)
+		}
+
+		tpl.Claims.IDToken.PreferredUsername = filterText(tpl.Claims.IDToken.PreferredUsername, options.Filters)
+		tpl.Claims.UserInfo.PreferredUsername = filterText(tpl.Claims.UserInfo.PreferredUsername, options.Filters)
+		tpl.Claims.IDToken.Audience = filterSliceOfText(tpl.Claims.IDToken.Audience, options.Filters)
+		tpl.Claims.UserInfo.Audience = filterSliceOfText(tpl.Claims.UserInfo.Audience, options.Filters)
+		tpl.Claims.IDToken.Issuer = filterText(tpl.Claims.IDToken.Issuer, options.Filters)
+		tpl.Claims.UserInfo.Issuer = filterText(tpl.Claims.UserInfo.Issuer, options.Filters)
+		tpl.Claims.IDToken.Email = filterText(tpl.Claims.IDToken.Email, options.Filters)
+		tpl.Claims.UserInfo.Email = filterText(tpl.Claims.UserInfo.Email, options.Filters)
+		tpl.Claims.IDToken.Name = filterText(tpl.Claims.IDToken.Name, options.Filters)
+		tpl.Claims.UserInfo.Name = filterText(tpl.Claims.UserInfo.Name, options.Filters)
+		tpl.RawToken = rawTokens[tpl.Claims.IDToken.JWTIdentifier]
+		tpl.AuthorizeCodeURL = acURLs[tpl.Claims.IDToken.JWTIdentifier].String()
+	}
+
+	w.Header().Add("Content-Type", "text/html")
+
+	// get the template
+	t, err := template.New("index").Funcs(templateFuncMap).ParseGlob(fmt.Sprintf("%s/*.html", options.TemplatesPath))
+	if err != nil {
+		log.Printf("Error reading the template Path: %s/*.html", options.TemplatesPath)
+		log.Println(err)
+		w.WriteHeader(http.StatusInternalServerError)
+		w.Write([]byte("500 - Error reading template file"))
+		return
+	}
+
+	// exec!
+	err = t.ExecuteTemplate(w, "index", tpl)
+	if err != nil {
+		log.Println(err)
+	}
+}
+
+func loginHandler(w http.ResponseWriter, r *http.Request) {
+	log.Println("[ ] Getting the global session from the session cookie:")
+	session, err := globalState.sessions.Get(r, options.CookieName)
+	if err != nil {
+		log.Println("[ ] Error getting the cookie")
+		writeErr(w, nil, "error getting cookie", http.StatusInternalServerError)
+		return
+	}
+
+	log.Println("[ ] Setting the redirect URL")
+	session.Values["redirect-url"] = "/"
+
+	log.Println("[ ] Saving the session")
+	if err = session.Save(r, w); err != nil {
+		writeErr(w, err, "error saving session", http.StatusInternalServerError)
+		return
+	}
+
+	log.Printf("[ ] Redirecting to %s", oauth2Config.AuthCodeURL("random-string"))
+	http.Redirect(w, r, oauth2Config.AuthCodeURL("random-string-here"), http.StatusFound)
+}
+
+func logoutHandler(w http.ResponseWriter, r *http.Request) {
+	session, err := globalState.sessions.Get(r, options.CookieName)
+	if err != nil {
+		writeErr(w, err, "error getting cookie", http.StatusInternalServerError)
+		return
+	}
+
+	// wet the session
+	session.Values = make(map[interface{}]interface{})
+
+	if err = session.Save(r, w); err != nil {
+		writeErr(w, err, "error saving session", http.StatusInternalServerError)
+		return
+	}
+
+	http.Redirect(w, r, "/", http.StatusFound)
+}
+
+func oauthCallbackHandler(res http.ResponseWriter, req *http.Request) {
+	log.Println("hit the oauth callback handler")
+	if req.FormValue("error") != "" {
+		log.Printf("got an error from the idp: %s", req.FormValue("error"))
+		http.Redirect(res, req, fmt.Sprintf("/error?%s", req.Form.Encode()), http.StatusFound)
+
+		return
+	}
+
+	var (
+		token      *oauth2.Token
+		idToken    *oidc.IDToken
+		err        error
+		idTokenRaw string
+		ok         bool
+	)
+
+	// The state should be checked here in production
+	if token, err = oauth2Config.Exchange(req.Context(), req.URL.Query().Get("code")); err != nil {
+		log.Println("Unable to exchange authorization code for tokens")
+		writeErr(res, err, "unable to exchange authorization code for tokens", http.StatusInternalServerError)
+		return
+	}
+
+	// Extract the ID Token from OAuth2 token.
+	if idTokenRaw, ok = token.Extra("id_token").(string); !ok {
+		log.Println("missing id token")
+		writeErr(res, nil, "missing id token", http.StatusInternalServerError)
+		return
+	}
+
+	// Parse and verify ID Token payload.
+	if idToken, err = verifier.Verify(req.Context(), idTokenRaw); err != nil {
+		log.Printf("unable to verify id token or token is invalid: %+v", idTokenRaw)
+		writeErr(res, err, "unable to verify id token or token is invalid", http.StatusInternalServerError)
+		return
+	}
+
+	// Extract custom claims
+	claimsIDToken := Claims{}
+
+	if err = idToken.Claims(&claimsIDToken); err != nil {
+		log.Printf("unable to decode id token claims: %+v", &claimsIDToken)
+		writeErr(res, err, "unable to decode id token claims", http.StatusInternalServerError)
+		return
+	}
+
+	var userinfo *oidc.UserInfo
+
+	if userinfo, err = provider.UserInfo(req.Context(), oauth2.StaticTokenSource(token)); err != nil {
+		log.Printf("unable to retreive userinfo claims")
+		writeErr(res, err, "unable to retrieve userinfo claims", http.StatusInternalServerError)
+		return
+	}
+
+	claimsUserInfo := Claims{}
+
+	if err = userinfo.Claims(&claimsUserInfo); err != nil {
+		log.Printf("unable to decode userinfo claims")
+		writeErr(res, err, "unable to decode userinfo claims", http.StatusInternalServerError)
+		return
+	}
+
+	var session *sessions.Session
+
+	if session, err = globalState.sessions.Get(req, options.CookieName); err != nil {
+		log.Printf("unable to get session from cookie")
+		writeErr(res, err, "unable to get session from cookie", http.StatusInternalServerError)
+		return
+	}
+
+	session.Values["id_token"] = claimsIDToken
+	session.Values["userinfo"] = claimsUserInfo
+	session.Values["logged"] = true
+	rawTokens[claimsIDToken.JWTIdentifier] = idTokenRaw
+	acURLs[claimsIDToken.JWTIdentifier] = req.URL
+
+	if err = session.Save(req, res); err != nil {
+		log.Printf("unable to save session")
+		writeErr(res, err, "unable to save session", http.StatusInternalServerError)
+		return
+	}
+
+	var redirectUrl string
+
+	if redirectUrl, ok = session.Values["redirect-url"].(string); ok {
+		log.Printf("all fine!")
+		http.Redirect(res, req, redirectUrl, http.StatusFound)
+		return
+	}
+
+	http.Redirect(res, req, "/", http.StatusFound)
+}
+
+func writeErr(w http.ResponseWriter, err error, msg string, statusCode int) {
+	switch {
+	case err == nil:
+		log.Println(msg)
+		http.Error(w, msg, statusCode)
+	default:
+		log.Println(msg)
+		log.Println(err)
+		http.Error(w, fmt.Errorf("%s: %w", msg, err).Error(), statusCode)
+	}
+}
diff --git a/nix/templates/goapp/frontend/src/init.go b/nix/templates/goapp/frontend/src/init.go
new file mode 100644
index 0000000..97e58f0
--- /dev/null
+++ b/nix/templates/goapp/frontend/src/init.go
@@ -0,0 +1,76 @@
+package main
+
+import (
+	"context"
+	"fmt"
+	"log"
+	"net/url"
+	"os"
+	"strings"
+
+	"github.com/coreos/go-oidc/v3/oidc"
+	"golang.org/x/oauth2"
+)
+
+func logInit() loggingMiddleware {
+	log.Println("[i] Setting up logging...")
+	logFile, err := os.OpenFile(options.LogFilePath, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0664)
+	if err != nil {
+		log.Fatal("Error opening the server.log file: ", err)
+	}
+	return loggingMiddleware{logFile}
+}
+
+func dbInit() {
+	log.Println("[i] Setting up Global State Struct...")
+	s, err := NewState()
+	if err != nil {
+		log.Fatal("Error creating the NewState(): ", err)
+	}
+	globalState = s
+}
+
+func sessionInit() {
+	log.Println("[i] Setting up Session Storage...")
+	store, err := NewSqliteStore(
+		sessiondbPath,
+		"sessions",
+		"/",
+		3600,
+		[]byte(os.Getenv("SESSION_KEY")))
+	if err != nil {
+		panic(err)
+	}
+	globalState.sessions = store
+}
+
+func oauth2Init() (err error) {
+	log.Println("[i] Setting up oauth2...")
+	var redirectURL *url.URL
+	if _, redirectURL, err = getURLs(options.PublicURL); err != nil {
+		return fmt.Errorf("could not parse public url: %w", err)
+	}
+
+	log.Printf("[ ] provider_url: %s", options.Issuer)
+	log.Printf("[ ] redirect_url: %s", redirectURL.String())
+
+	if provider, err = oidc.NewProvider(context.Background(), options.Issuer); err != nil {
+		log.Println("Error init oidc provider: ", err)
+		return fmt.Errorf("error initializing oidc provider: %w", err)
+	}
+
+	verifier = provider.Verifier(&oidc.Config{ClientID: options.ClientID})
+	log.Printf("[ ] ClientID: %s", options.ClientID)
+	log.Printf("[ ] ClientSecret: %s", options.ClientSecret)
+	log.Printf("[ ] redirectURL: %s", redirectURL.String())
+	log.Printf("[ ] providerEndpoint: %+v", provider.Endpoint())
+	log.Printf("[ ] Scopes: %s", options.Scopes)
+	oauth2Config = oauth2.Config{
+		ClientID:     options.ClientID,
+		ClientSecret: options.ClientSecret,
+		RedirectURL:  redirectURL.String(),
+		Endpoint:     provider.Endpoint(),
+		Scopes:       strings.Split(options.Scopes, ","),
+	}
+	return nil
+}
diff --git a/nix/templates/goapp/frontend/src/log.go b/nix/templates/goapp/frontend/src/log.go
new file mode 100644
index 0000000..5af719a
--- /dev/null
+++ b/nix/templates/goapp/frontend/src/log.go
@@ -0,0 +1,34 @@
+package main
+
+import (
+	"net/http"
+	"os"
+
+	"github.com/gorilla/handlers"
+)
+
+// Defines a middleware containing a logfile
+//
+// This is done to combine gorilla/handlers with gorilla/mux middlewares to
+// just use r.Use(logger.Middleware) once instead of adding this to all
+// handlers manually (Yes, I'm really missing macros in Go...)
+type loggingMiddleware struct {
+	logFile *os.File
+}
+
+func (l *loggingMiddleware) Middleware(next http.Handler) http.Handler {
+	return handlers.LoggingHandler(l.logFile, next)
+}
+
+func authMiddleware(next http.Handler) http.Handler {
+	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		session, _ := globalState.sessions.Get(r, "session")
+		username := session.Values["username"]
+
+		if username == nil {
+			http.Redirect(w, r, "/login", http.StatusSeeOther)
+		} else {
+			next.ServeHTTP(w, r)
+		}
+	})
+}
diff --git a/nix/templates/goapp/frontend/src/main.go b/nix/templates/goapp/frontend/src/main.go
new file mode 100644
index 0000000..fcf4224
--- /dev/null
+++ b/nix/templates/goapp/frontend/src/main.go
@@ -0,0 +1,96 @@
+package main
+
+import (
+	"crypto/tls"
+	"fmt"
+	"log"
+	"net/http"
+	"net/url"
+	"time"
+
+	"github.com/coreos/go-oidc/v3/oidc"
+	"github.com/gorilla/mux"
+	"github.com/spf13/cobra"
+	"golang.org/x/oauth2"
+)
+
+var (
+	host          string
+	port          int
+	databasePath  string
+	logFilePath   string
+	sessiondbPath string
+	templatesPath string
+	globalState   *State
+
+	options      Options
+	oauth2Config oauth2.Config
+	provider     *oidc.Provider
+	verifier     *oidc.IDTokenVerifier
+
+	rawTokens = make(map[string]string)
+	acURLs    = make(map[string]*url.URL)
+)
+
+func main() {
+
+	http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
+
+	rootCmd := &cobra.Command{Use: "goapp", RunE: root}
+
+	rootCmd.Flags().StringVar(&options.Host, "host", "0.0.0.0", "Specifies the tcp host to listen on")
+	rootCmd.Flags().IntVar(&options.Port, "port", 8080, "Specifies the port to listen on")
+	rootCmd.Flags().StringVar(&options.PublicURL, "public-url", "http://localhost:8080/", "Specifies the root URL to generate the redirect URI")
+	rootCmd.Flags().StringVar(&options.ClientID, "id", "", "Specifies the OpenID Connect Client ID")
+	rootCmd.Flags().StringVarP(&options.ClientSecret, "secret", "s", "", "Specifies the OpenID Connect Client Secret")
+	rootCmd.Flags().StringVarP(&options.Issuer, "issuer", "i", "", "Specifies the URL for the OpenID Connect OP")
+	rootCmd.Flags().StringVar(&options.Scopes, "scopes", "openid,profile,email,groups", "Specifies the OpenID Connect scopes to request")
+	rootCmd.Flags().StringVar(&options.CookieName, "cookie-name", "oidc-client", "Specifies the storage cookie name to use")
+	rootCmd.Flags().StringSliceVar(&options.Filters, "filters", []string{}, "If specified filters the specified text from html output (not json) out of the email addresses, display names, audience, etc")
+	rootCmd.Flags().StringSliceVar(&options.GroupsFilter, "groups-filter", []string{}, "If specified only shows the groups in this list")
+	rootCmd.Flags().StringVar(&options.LogFilePath, "logpath", "./server.log", "Specifies the path to store the server logs at")
+	rootCmd.Flags().StringVar(&options.TemplatesPath, "templatespath", "./templates", "Specifies the path to where the templates are stored")
+
+	_ = rootCmd.MarkFlagRequired("id")
+	_ = rootCmd.MarkFlagRequired("secret")
+	_ = rootCmd.MarkFlagRequired("issuer")
+
+	if err := rootCmd.Execute(); err != nil {
+		log.Fatal(err)
+	}
+}
+
+func root(cmd *cobra.Command, args []string) (err error) {
+
+	logger := logInit()
+	oauth2Init()
+	dbInit()
+	sessionInit()
+
+	r := mux.NewRouter()
+	r.Use(logger.Middleware)
+	r.HandleFunc("/", indexHandler)
+	r.HandleFunc("/login", loginHandler)
+	//  r.HandleFunc("/logout", )
+	//  r.HandleFunc("/error", loginHandler)
+	r.HandleFunc("/oauth2/callback", oauthCallbackHandler)
+	//  r.HandleFunc("/json", loginHandler)
+	//  r.HandleFunc("/jwt.json", loginHandler)
+
+	// endpoints with auth needed
+	auth_needed := r.PathPrefix("/").Subrouter()
+	auth_needed.Use(authMiddleware)
+	auth_needed.HandleFunc("/logout", logoutHandler)
+
+	serverAddress := fmt.Sprintf("%s:%d", options.Host, options.Port)
+	srv := &http.Server{
+		Handler:      r,
+		Addr:         serverAddress,
+		WriteTimeout: 15 * time.Second,
+		ReadTimeout:  15 * time.Second,
+	}
+
+	log.Printf("[i] Running the server on %s", serverAddress)
+	log.Fatal(srv.ListenAndServe())
+	return
+}
diff --git a/nix/templates/goapp/frontend/src/sqlitestore.go b/nix/templates/goapp/frontend/src/sqlitestore.go
new file mode 100644
index 0000000..34e31e4
--- /dev/null
+++ b/nix/templates/goapp/frontend/src/sqlitestore.go
@@ -0,0 +1,285 @@
+/*
+	Gorilla Sessions backend for SQLite.
+
+Copyright (c) 2013 Contributors. See the list of contributors in the CONTRIBUTORS file for details.
+
+This software is licensed under a MIT style license available in the LICENSE file.
+*/
+package main
+
+import (
+	"database/sql"
+	"encoding/gob"
+	"errors"
+	"fmt"
+	"log"
+	"net/http"
+	"strings"
+	"time"
+
+	"github.com/gorilla/securecookie"
+	"github.com/gorilla/sessions"
+	_ "modernc.org/sqlite"
+)
+
+type SqliteStore struct {
+	db         DB
+	stmtInsert *sql.Stmt
+	stmtDelete *sql.Stmt
+	stmtUpdate *sql.Stmt
+	stmtSelect *sql.Stmt
+
+	Codecs  []securecookie.Codec
+	Options *sessions.Options
+	table   string
+}
+
+type sessionRow struct {
+	id         string
+	data       string
+	createdOn  time.Time
+	modifiedOn time.Time
+	expiresOn  time.Time
+}
+
+type DB interface {
+	Exec(query string, args ...interface{}) (sql.Result, error)
+	Prepare(query string) (*sql.Stmt, error)
+	Close() error
+}
+
+func init() {
+	gob.Register(time.Time{})
+	gob.Register(Claims{})
+}
+
+func NewSqliteStore(endpoint string, tableName string, path string, maxAge int, keyPairs ...[]byte) (*SqliteStore, error) {
+	db, err := sql.Open("sqlite3", endpoint)
+	if err != nil {
+		return nil, err
+	}
+
+	return NewSqliteStoreFromConnection(db, tableName, path, maxAge, keyPairs...)
+}
+
+func NewSqliteStoreFromConnection(db DB, tableName string, path string, maxAge int, keyPairs ...[]byte) (*SqliteStore, error) {
+	// Make sure table name is enclosed.
+	tableName = "`" + strings.Trim(tableName, "`") + "`"
+
+	cTableQ := "CREATE TABLE IF NOT EXISTS " +
+		tableName + " (id INTEGER PRIMARY KEY, " +
+		"session_data LONGBLOB, " +
+		"created_on TIMESTAMP DEFAULT 0, " +
+		"modified_on TIMESTAMP DEFAULT CURRENT_TIMESTAMP, " +
+		"expires_on TIMESTAMP DEFAULT 0);"
+	if _, err := db.Exec(cTableQ); err != nil {
+		return nil, err
+	}
+
+	insQ := "INSERT INTO " + tableName +
+		"(id, session_data, created_on, modified_on, expires_on) VALUES (NULL, ?, ?, ?, ?)"
+	stmtInsert, stmtErr := db.Prepare(insQ)
+	if stmtErr != nil {
+		return nil, stmtErr
+	}
+
+	delQ := "DELETE FROM " + tableName + " WHERE id = ?"
+	stmtDelete, stmtErr := db.Prepare(delQ)
+	if stmtErr != nil {
+		return nil, stmtErr
+	}
+
+	updQ := "UPDATE " + tableName + " SET session_data = ?, created_on = ?, expires_on = ? " +
+		"WHERE id = ?"
+	stmtUpdate, stmtErr := db.Prepare(updQ)
+	if stmtErr != nil {
+		return nil, stmtErr
+	}
+
+	selQ := "SELECT id, session_data, created_on, modified_on, expires_on from " +
+		tableName + " WHERE id = ?"
+	stmtSelect, stmtErr := db.Prepare(selQ)
+	if stmtErr != nil {
+		return nil, stmtErr
+	}
+
+	return &SqliteStore{
+		db:         db,
+		stmtInsert: stmtInsert,
+		stmtDelete: stmtDelete,
+		stmtUpdate: stmtUpdate,
+		stmtSelect: stmtSelect,
+		Codecs:     securecookie.CodecsFromPairs(keyPairs...),
+		Options: &sessions.Options{
+			Path:   path,
+			MaxAge: maxAge,
+		},
+		table: tableName,
+	}, nil
+}
+
+func (m *SqliteStore) Close() {
+	m.stmtSelect.Close()
+	m.stmtUpdate.Close()
+	m.stmtDelete.Close()
+	m.stmtInsert.Close()
+	m.db.Close()
+}
+
+func (m *SqliteStore) Get(r *http.Request, name string) (*sessions.Session, error) {
+	return sessions.GetRegistry(r).Get(m, name)
+}
+
+func (m *SqliteStore) New(r *http.Request, name string) (*sessions.Session, error) {
+	session := sessions.NewSession(m, name)
+	session.Options = &sessions.Options{
+		Path:   m.Options.Path,
+		MaxAge: m.Options.MaxAge,
+	}
+	session.IsNew = true
+	var err error
+	if cook, errCookie := r.Cookie(name); errCookie == nil {
+		err = securecookie.DecodeMulti(name, cook.Value, &session.ID, m.Codecs...)
+		if err == nil {
+			err = m.load(session)
+			if err == nil {
+				session.IsNew = false
+			} else {
+				err = nil
+			}
+		}
+	}
+	return session, err
+}
+
+func (m *SqliteStore) Save(r *http.Request, w http.ResponseWriter, session *sessions.Session) error {
+	var err error
+	if session.ID == "" {
+		if err = m.insert(session); err != nil {
+			return err
+		}
+	} else if err = m.save(session); err != nil {
+		return err
+	}
+	encoded, err := securecookie.EncodeMulti(session.Name(), session.ID, m.Codecs...)
+	if err != nil {
+		return err
+	}
+	http.SetCookie(w, sessions.NewCookie(session.Name(), encoded, session.Options))
+	return nil
+}
+
+func (m *SqliteStore) insert(session *sessions.Session) error {
+	var createdOn time.Time
+	var modifiedOn time.Time
+	var expiresOn time.Time
+	crOn := session.Values["created_on"]
+	if crOn == nil {
+		createdOn = time.Now()
+	} else {
+		createdOn = crOn.(time.Time)
+	}
+	modifiedOn = createdOn
+	exOn := session.Values["expires_on"]
+	if exOn == nil {
+		expiresOn = time.Now().Add(time.Second * time.Duration(session.Options.MaxAge))
+	} else {
+		expiresOn = exOn.(time.Time)
+	}
+	delete(session.Values, "created_on")
+	delete(session.Values, "expires_on")
+	delete(session.Values, "modified_on")
+
+	encoded, encErr := securecookie.EncodeMulti(session.Name(), session.Values, m.Codecs...)
+	if encErr != nil {
+		return encErr
+	}
+	res, insErr := m.stmtInsert.Exec(encoded, createdOn, modifiedOn, expiresOn)
+	if insErr != nil {
+		return insErr
+	}
+	lastInserted, lInsErr := res.LastInsertId()
+	if lInsErr != nil {
+		return lInsErr
+	}
+	session.ID = fmt.Sprintf("%d", lastInserted)
+	return nil
+}
+
+func (m *SqliteStore) Delete(r *http.Request, w http.ResponseWriter, session *sessions.Session) error {
+
+	// Set cookie to expire.
+	options := *session.Options
+	options.MaxAge = -1
+	http.SetCookie(w, sessions.NewCookie(session.Name(), "", &options))
+	// Clear session values.
+	for k := range session.Values {
+		delete(session.Values, k)
+	}
+
+	_, delErr := m.stmtDelete.Exec(session.ID)
+	if delErr != nil {
+		return delErr
+	}
+	return nil
+}
+
+func (m *SqliteStore) save(session *sessions.Session) error {
+	if session.IsNew == true {
+		return m.insert(session)
+	}
+	var createdOn time.Time
+	var expiresOn time.Time
+	crOn := session.Values["created_on"]
+	if crOn == nil {
+		createdOn = time.Now()
+	} else {
+		createdOn = crOn.(time.Time)
+	}
+
+	exOn := session.Values["expires_on"]
+	if exOn == nil {
+		expiresOn = time.Now().Add(time.Second * time.Duration(session.Options.MaxAge))
+		log.Print("nil")
+	} else {
+		expiresOn = exOn.(time.Time)
+		if expiresOn.Sub(time.Now().Add(time.Second*time.Duration(session.Options.MaxAge))) < 0 {
+			expiresOn = time.Now().Add(time.Second * time.Duration(session.Options.MaxAge))
+		}
+	}
+
+	delete(session.Values, "created_on")
+	delete(session.Values, "expires_on")
+	delete(session.Values, "modified_on")
+	encoded, encErr := securecookie.EncodeMulti(session.Name(), session.Values, m.Codecs...)
+	if encErr != nil {
+		return encErr
+	}
+	_, updErr := m.stmtUpdate.Exec(encoded, createdOn, expiresOn, session.ID)
+	if updErr != nil {
+		return updErr
+	}
+	return nil
+}
+
+func (m *SqliteStore) load(session *sessions.Session) error {
+	row := m.stmtSelect.QueryRow(session.ID)
+	sess := sessionRow{}
+	scanErr := row.Scan(&sess.id, &sess.data, &sess.createdOn, &sess.modifiedOn, &sess.expiresOn)
+	if scanErr != nil {
+		return scanErr
+	}
+	if sess.expiresOn.Sub(time.Now()) < 0 {
+		log.Printf("Session expired on %s, but it is %s now.", sess.expiresOn, time.Now())
+		return errors.New("Session expired")
+	}
+	err := securecookie.DecodeMulti(session.Name(), sess.data, &session.Values, m.Codecs...)
+	if err != nil {
+		return err
+	}
+	session.Values["created_on"] = sess.createdOn
+	session.Values["modified_on"] = sess.modifiedOn
+	session.Values["expires_on"] = sess.expiresOn
+	return nil
+
+}
diff --git a/nix/templates/goapp/frontend/src/templates.go b/nix/templates/goapp/frontend/src/templates.go
new file mode 100644
index 0000000..5ae9397
--- /dev/null
+++ b/nix/templates/goapp/frontend/src/templates.go
@@ -0,0 +1,42 @@
+package main
+
+import (
+	"html/template"
+	"strings"
+)
+
+var (
+	templateFuncMap = template.FuncMap{
+		"stringsJoin":      strings.Join,
+		"stringsEqualFold": strings.EqualFold,
+		"isStringInSlice":  isStringInSlice,
+	}
+)
+
+type indexTplData struct {
+	Title, Description, RawToken string
+
+	Breadcrumbs []Breadcrumb
+	NextLinks   []Link
+
+	Error            string
+	LoggedIn         bool
+	Claims           tplClaims
+	Groups           []string
+	AuthorizeCodeURL string
+}
+
+type Link struct {
+	Name   string
+	Target string
+}
+
+type Breadcrumb struct {
+	Main    Link
+	Options []Link
+}
+
+type tplClaims struct {
+	IDToken  Claims
+	UserInfo Claims
+}
diff --git a/nix/templates/goapp/frontend/src/types.go b/nix/templates/goapp/frontend/src/types.go
new file mode 100644
index 0000000..7efcc70
--- /dev/null
+++ b/nix/templates/goapp/frontend/src/types.go
@@ -0,0 +1,65 @@
+package main
+
+type Claims struct {
+	JWTIdentifier                       string   `json:"jti"`
+	Issuer                              string   `json:"iss"`
+	Subject                             string   `json:"sub"`
+	Nonce                               string   `json:"nonce"`
+	Expires                             int64    `json:"exp"`
+	IssueTime                           int64    `json:"iat"`
+	RequestedAt                         int64    `json:"rat"`
+	AuthorizeTime                       int64    `json:"auth_time"`
+	NotBefore                           int64    `json:"nbf"`
+	Audience                            []string `json:"aud"`
+	Scope                               []string `json:"scp"`
+	ScopeString                         string   `json:"scope"`
+	AccessTokenHash                     string   `json:"at_hash"`
+	CodeHash                            string   `json:"c_hash"`
+	AuthenticationContextClassReference string   `json:"acr"`
+	AuthenticationMethodsReference      []string `json:"amr"`
+
+	Name                string       `json:"name"`
+	GivenName           string       `json:"given_name"`
+	FamilyName          string       `json:"family_name"`
+	MiddleName          string       `json:"middle_name"`
+	Nickname            string       `json:"nickname"`
+	PreferredUsername   string       `json:"preferred_username"`
+	Profile             string       `jsoon:"profile"`
+	Picture             string       `json:"picture"`
+	Website             string       `json:"website"`
+	Gender              string       `json:"gender"`
+	Birthdate           string       `json:"birthdate"`
+	ZoneInfo            string       `json:"zoneinfo"`
+	Locale              string       `json:"locale"`
+	UpdatedAt           int64        `json:"updated_at"`
+	Email               string       `json:"email"`
+	EmailAlts           []string     `json:"alt_emails"`
+	EmailVerified       bool         `json:"email_verified"`
+	PhoneNumber         string       `json:"phone_number"`
+	PhoneNumberVerified bool         `json:"phone_number_verified"`
+	Address             ClamsAddress `json:"address"`
+	Groups              []string     `json:"groups"`
+}
+
+type ClamsAddress struct {
+	StreetAddress string `json:"street_address"`
+	Locality      string `json:"locality"`
+	Region        string `json:"region"`
+	PostalCode    string `json:"postal_code"`
+	Country       string `json:"country"`
+}
+
+type Options struct {
+	Host          string
+	Port          int
+	LogFilePath   string
+	TemplatesPath string
+	ClientID      string
+	ClientSecret  string
+	Issuer        string
+	PublicURL     string
+	Scopes        string
+	CookieName    string
+	Filters       []string
+	GroupsFilter  []string
+}
diff --git a/nix/templates/goapp/frontend/src/util.go b/nix/templates/goapp/frontend/src/util.go
new file mode 100644
index 0000000..89d28ba
--- /dev/null
+++ b/nix/templates/goapp/frontend/src/util.go
@@ -0,0 +1,58 @@
+package main
+
+import (
+	"fmt"
+	"net/url"
+	"path"
+	"strings"
+)
+
+func isStringInSlice(s string, slice []string) bool {
+	for _, x := range slice {
+		if s == x {
+			return true
+		}
+	}
+
+	return false
+}
+
+func filterText(input string, filters []string) (output string) {
+	if len(filters) == 0 {
+		return input
+	}
+
+	for _, filter := range filters {
+		input = strings.Replace(input, filter, strings.Repeat("*", len(filter)), -1)
+	}
+
+	return input
+}
+
+func filterSliceOfText(input []string, filters []string) (output []string) {
+	for _, item := range input {
+		output = append(output, filterText(item, filters))
+	}
+
+	return output
+}
+
+func getURLs(rootURL string) (publicURL *url.URL, redirectURL *url.URL, err error) {
+	if publicURL, err = url.Parse(rootURL); err != nil {
+		return nil, nil, err
+	}
+
+	if publicURL.Scheme != "http" && publicURL.Scheme != "https" {
+		return nil, nil, fmt.Errorf("scheme must be http or https but it is '%s'", publicURL.Scheme)
+	}
+
+	if !strings.HasSuffix(publicURL.Path, "/") {
+		publicURL.Path += "/"
+	}
+
+	redirectURL = &url.URL{}
+	*redirectURL = *publicURL
+	redirectURL.Path = path.Join(redirectURL.Path, "/oauth2/callback")
+
+	return publicURL, redirectURL, nil
+}