From ae39f02812bcfe903e956220c890bfb7b9bb9ff4 Mon Sep 17 00:00:00 2001 From: Emile Date: Wed, 19 Feb 2025 19:53:25 +0100 Subject: removed the backend, added the frontend with oidc support So I've added oidc support which is nice, yet I have to test this with some https foo, so I'm pushing this. --- nix/templates/goapp/frontend/src/db.go | 37 +++ nix/templates/goapp/frontend/src/handlers.go | 236 ++++++++++++++++++++ nix/templates/goapp/frontend/src/init.go | 76 +++++++ nix/templates/goapp/frontend/src/log.go | 34 +++ nix/templates/goapp/frontend/src/main.go | 96 ++++++++ nix/templates/goapp/frontend/src/sqlitestore.go | 285 ++++++++++++++++++++++++ nix/templates/goapp/frontend/src/templates.go | 42 ++++ nix/templates/goapp/frontend/src/types.go | 65 ++++++ nix/templates/goapp/frontend/src/util.go | 58 +++++ 9 files changed, 929 insertions(+) create mode 100644 nix/templates/goapp/frontend/src/db.go create mode 100644 nix/templates/goapp/frontend/src/handlers.go create mode 100644 nix/templates/goapp/frontend/src/init.go create mode 100644 nix/templates/goapp/frontend/src/log.go create mode 100644 nix/templates/goapp/frontend/src/main.go create mode 100644 nix/templates/goapp/frontend/src/sqlitestore.go create mode 100644 nix/templates/goapp/frontend/src/templates.go create mode 100644 nix/templates/goapp/frontend/src/types.go create mode 100644 nix/templates/goapp/frontend/src/util.go (limited to 'nix/templates/goapp/frontend/src') 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 +} -- cgit 1.4.1