From 22c4725ed0341b4553020f9669ab4c6298cb39de Mon Sep 17 00:00:00 2001 From: Antonio Pagano Date: Fri, 22 Mar 2024 17:24:48 -0500 Subject: [PATCH 01/37] new: getting started with V2 --- README.md | 252 --------------------- buildtemplate.go | 58 ----- cookie_valuer.go | 7 - defaulttokenmanager.go | 20 -- emailable.go | 11 - errors.go | 13 -- errtokenmanager_test.go | 17 -- go.mod | 4 +- go.sum | 2 - handler.go | 103 --------- internal/internal.go | 9 + internal/layout.html | 18 ++ internal/login.go | 26 +++ internal/login.html | 44 ++++ internal/logo.png | Bin 0 -> 52681 bytes {templates => internal}/message.html.email | 0 {templates => internal}/message.txt.email | 0 internal/sample/cmd/main.go | 17 ++ internal/sample/private.go | 7 + internal/token.go | 39 ++++ internal/token.html | 61 +++++ internal/token_invalid.html | 29 +++ internal/validate.go | 54 +++++ jwt.go | 49 ---- logger.go | 55 ----- logger_test.go | 54 ----- login.go | 43 ---- login_test.go | 101 --------- loginhooks.go | 59 ----- logout.go | 11 - logout_test.go | 27 --- maildoor.go | 73 +----- maildoor_test.go | 93 -------- message.go | 27 --- options.go | 85 ------- paths.go | 19 -- productconfig.go | 10 - sample/cmd/sample/main.go | 23 -- sample/user.go | 10 - sample/web/authenticated.go | 31 --- sample/web/finder.go | 12 - sample/web/maildoor.go | 45 ---- sample/web/private.go | 19 -- sample/web/public.go | 19 -- sample/web/server.go | 56 ----- sample/web/templates/private.html | 25 -- sample/web/templates/public.html | 47 ---- send.go | 113 --------- send_test.go | 234 ------------------- showcase-cover.png | Bin 133812 -> 0 bytes showcase-login.jpg | Bin 40977 -> 0 bytes showcase-sent.jpg | Bin 43881 -> 0 bytes smtp.go | 75 ------ smtp_test.go | 142 ------------ templates/emailsent.html | 23 -- templates/layout.html | 15 -- templates/login.html | 49 ---- testuser_test.go | 7 - tmp/sample | Bin 0 -> 9984290 bytes tokenmanager.go | 19 -- validate.go | 38 ---- validate_test.go | 139 ------------ value_encoder.go | 42 ---- 63 files changed, 314 insertions(+), 2366 deletions(-) delete mode 100644 buildtemplate.go delete mode 100644 cookie_valuer.go delete mode 100644 defaulttokenmanager.go delete mode 100644 emailable.go delete mode 100644 errors.go delete mode 100644 errtokenmanager_test.go delete mode 100644 handler.go create mode 100644 internal/internal.go create mode 100644 internal/layout.html create mode 100644 internal/login.go create mode 100644 internal/login.html create mode 100644 internal/logo.png rename {templates => internal}/message.html.email (100%) rename {templates => internal}/message.txt.email (100%) create mode 100644 internal/sample/cmd/main.go create mode 100644 internal/sample/private.go create mode 100644 internal/token.go create mode 100644 internal/token.html create mode 100644 internal/token_invalid.html create mode 100644 internal/validate.go delete mode 100644 jwt.go delete mode 100644 logger.go delete mode 100644 logger_test.go delete mode 100644 login.go delete mode 100644 login_test.go delete mode 100644 loginhooks.go delete mode 100644 logout.go delete mode 100644 logout_test.go delete mode 100644 maildoor_test.go delete mode 100644 message.go delete mode 100644 options.go delete mode 100644 paths.go delete mode 100644 productconfig.go delete mode 100644 sample/cmd/sample/main.go delete mode 100644 sample/user.go delete mode 100644 sample/web/authenticated.go delete mode 100644 sample/web/finder.go delete mode 100644 sample/web/maildoor.go delete mode 100644 sample/web/private.go delete mode 100644 sample/web/public.go delete mode 100644 sample/web/server.go delete mode 100644 sample/web/templates/private.html delete mode 100644 sample/web/templates/public.html delete mode 100644 send.go delete mode 100644 send_test.go delete mode 100644 showcase-cover.png delete mode 100644 showcase-login.jpg delete mode 100644 showcase-sent.jpg delete mode 100644 smtp.go delete mode 100644 smtp_test.go delete mode 100644 templates/emailsent.html delete mode 100644 templates/layout.html delete mode 100644 templates/login.html delete mode 100644 testuser_test.go create mode 100755 tmp/sample delete mode 100644 tokenmanager.go delete mode 100644 validate.go delete mode 100644 validate_test.go delete mode 100644 value_encoder.go diff --git a/README.md b/README.md index 269069f..53f15b2 100644 --- a/README.md +++ b/README.md @@ -1,256 +1,4 @@ ![tests workflow](https://github.com/wawandco/maildoor/actions/workflows/test.yml/badge.svg) ![report card](https://goreportcard.com/badge/github.com/wawandco/maildoor) - # Maildoor - -![maildoor banner](./showcase-cover.png) - -Maildoor is an email based authentication library for Go (lang), powered by Go `embed` package, JWT's and TailwindCSS. Maildoor provides simple and beautiful user interface that is easy to use and customize with your logo. - -
- - -
- -But the UI is not all, Maildoor ships as a Go handler that contains the needed endpoints to login users by emailing tokens to their email addresses instead of using passwords. Maildoor allows to define application specific behaviors as part of the authentication process. - - -## Installation - -This library is intended to be used as a dependency in your Go project. Installation implies go-getting the package with: - -```sh -go get github.com/wawandco/maildoor@latest -``` - -And then using it accordingly in your app. See the Usage section for detailed instructions on usage. -## Usage - -Maildoor instances satisfy the http.Handler interface and can be mounted into Mupliplexers. To initialize a Maildoor instance, use the New function: - -```go - // Initialize the maildoor handler to take care of the web requests. - auth, err := maildoor.NewWithOptions( - os.Getenv("SECRET_KEY"), - - maildoor.UseFinder(finder), - maildoor.UseTokenManager(maildoor.DefaultTokenManager(os.Getenv("SECRET_KEY"))), - maildoor.UseSender( - maildoor.NewSMTPSender(maildoor.SMTPOptions{ - From: os.Getenv("SMTP_FROM_EMAIL"), - Host: os.Getenv("SMTP_HOST"), // p.e. "smtp.gmail.com", - Port: os.Getenv("SMTP_PORT"), //"587", - Password: os.Getenv("SMTP_PASSWORD"), - }), - ), - ) - - if err != nil { - return nil, fmt.Errorf("error initializing maildoor: %w", err) - } -``` - -After initializing the Maildoor instance, you can mount it into a multiplexer: - -```go -mux := http.NewServeMux() -mux.Handle("/auth/", auth) // Set the prefix - -fmt.Println("Listening on port 8080") -if err := http.ListenAndServe(":8080", server); err != nil { - panic(fmt.Errorf("error starting server: %w", err)) -} -``` -### Options - -After seeing how to initialize the Maildoor Instance, lets dig a deeper into what some of these options mean. - -#### FinderFn - -The finder function is used to find a user by email address. The logic for looking up users is up to the application developer, but it should return an `Emailable` instance to be used on the signin flow. The signature of the finder function is: - -```go -func(string) (Emailable, error) -``` - -Where the string is the email address or token to identify the user. -#### SenderFn - -Maildoor does not take care of sending your emails, instead it expects you to provide a function that will do this. This function will be called when a user requests a token to be sent to their email address and will be passed the message that needs to be send to the user. - -The sender function signature is: - -```go -func(*maildoor.Message) error -``` - -When this function returns an error the sign-in flow redirects the user to the login page with an error message. - -#### AfterLoginFn - -AfterLoginFn is a function that is called after a user has successfully logged in. It is passed the request instance, the response and user that has just logged in. Within this function typically the application does things like setting a session cookie and redirecting the user to a secure page. As with the sender function, its up to the application to decide what happens within the afterLogin function. - -Its signature is: - -```go -func(http.ResponseWriter, *http.Request, Emailable) error -``` - -#### LogoutFn - -Similar than the afterLogin function, the logout function is called after a user has successfully logged out. It is passed the request instance, the response and user that has just logged out. Within this function typically the application does things like clearing the session cookie and redirecting the landing page. As with the afterLoginFn function, it's up to the application to decide what happens within the logout function. - -Its signature is: - -```go -func(http.ResponseWriter, *http.Request) error -``` - -#### BaseURL - -The baseURL is the base URL where the app is running. By default its `http://localhost:8080` but you can override this value by setting the BaseURL option. - -#### Prefix - -Prefix of the maildoor routes, by default it is `/auth`. You can override this value by setting the Prefix option. When using a multiplexer, make sure to set the prefix to the same value as the one used in the maildoor instance. - -```go - -auth, err := maildoor.New(maildoor.Options{ -... - Prefix: "/auth", -... -}) - -mux.Handle("/auth/", auth) // Correct -mux.Handle("/other/", auth) // Incorrect -``` - -#### Product - -Product allows to set some product related settings for the signin flow. This helps branding the pages rendered to the user. The product can specify the name of the product, the logo and the favicon. - -#### TokenManager - -TokenManager is a very important part of the authentication process. It is responsible for generating and validating tokens across the email authentication process. Maildoor provides a default implementation which uses JWT tokens, whether the application uses JWT or not, it should provide a token manager. A token manager should meet the TokenManager interface. - -```go -type TokenManager interface { - Generate(Emailable) (string, error) - Validate(string) (string, error) -} -``` - -To use the default token manager, you can use your key to build it: - -```go -maildoor.DefaultTokenManager(os.Getenv("TOKEN_MANAGER_SECRET")) -``` - -#### CSRFTokenSecret - -This option sets the secret used by the signin form to protect against CSRF attacks. We recommend to pull this value from an environment variable or secret storage. -#### Logger -Logger option allows application to set your own logger. If this is not specified Maildoor will use a muteLogger, which will not print anything out. There is a BasicLogger that can be used if needed. Also, if there is the need for a custom logger you can implement the Logger interface. - -```go -// Logger interface defines the minimum set of methods -// that a logger should satisfy to be used by the library. -type Logger interface { - // Log a message at the Info level. - Info(args ...interface{}) - - // Log a formatted message at the Info level. - Infof(format string, args ...interface{}) - - // Log a message at the Error level. - Error(args ...interface{}) - - // Log a formatted message at the Error level. - Errorf(format string, args ...interface{}) -} -``` - -### Login Workflow - -The following chart shows the authentication process flow followed by the Maildoor library. - -```mermaid -graph LR; - login(Login page)-->send(email sender); - send-->|User found or no error|sendemail(Email sent); - send-->|Error finding user|login; - sendemail-.-click(User Clicked link); - click-->verification(token verification); - verification-->|token expired|login; - verification-->|error verifying|login; - verification-->afterlogin(Application Afterlogin); -``` - -### The HTTP Endpoints - -Maildoor is an http.Handler, which means it receives requests and responds to them. The Maildoor handler is mounted on a prefix, which is set by the application developer. Under that prefix the handler responds to the following endpoints: - -#### GET:/auth/login -This is the login form. It renders a form with a CSRF token and a submit button. In here the user is asked to enter their email address. - -#### POST:/auth/send -This endpoint is hit by the login form. It receives the email address and the CSRF token from the user and upon confirmation with the `FinderFn` it sends a link with token to the user's email address. -#### GET:/auth/validate -This endpoint is where the email link is validated. It receives the token from the URL and it validates the token. If the token is valid, it runs the `AfterLoginFn` function. -#### DELETE:/auth/logout -This endpoint is intended to be used by the app to logout the user. It just run the `LogoutFn` function. - -#### GET:/assets/* -This endpoint serves static assets. It is mounted on the `/assets` prefix and it will serve all files from the `/assets` directory such as images and css needed for the form. - - -### Sample Application - -Within the sample folder you can find a go application that illustrates the usage of Maildoor. To run it from the command line you can use: - -```sh -go run ./sample/cmd/sample -``` -## FAQ - -I do not use SMTP for sending, what should I do? -> Each application is free to send emails as it desires, in the sample application we use a [sendgrid](https://sendgrid.com/) SMTP authentication sender. - -How to I customize the email logo and product? -> These can be customized by setting the `Logo` and `Favicon` in the Product settings. - -Can I change the email copy (Subject or content)? -> Yes, you can change the subject and the content of the email. Maildoor will provide a Message struct that to your application implementation of the SenderFn, within there you can decide to change the subject and the content of the email. - -I don't want to use JWT for my tokens, what should I do? -> As long as you provide a TokenManager, Maildoor will use the token manager to generate and validate tokens. - -What should I do in the `AfterLoginFn` hook? -> Typically session and cookie management after login, but other things such as logging and threat detection can be done in there. - -How do I secure my application to prevent unauthorized access? -> Typically you would have a middleware that secures your private routes. Maildoor does not provide such middleware but it typically checks a session either in cookies or other persistence mean. - -## Guiding Principles - -- Use standard Go library as much as possible to avoid external dependencies. -- Application logic should live in the application, not in the library. -## TODO - -- [x] Add: Login flow diagram -- [x] Build: Default SMTPSender -- [x] Optimize: CSS to only be the one used (Tailwind CSS can do this) -- [x] Add: Login/sent screenshots -- [x] Add: Default afterLogin and logout hook functions (Cookie based) -- [x] Add: Secure cookie for the default afterLogin hook function. -- [ ] Design: Authentication Middleware ❓ -- [ ] Research: flash error messages instead of using parameters -- [ ] Add: Error pages (500 and 404) -- [ ] Design: Custom messages -- [ ] Design: Custom templates. - - - - diff --git a/buildtemplate.go b/buildtemplate.go deleted file mode 100644 index 2ea9897..0000000 --- a/buildtemplate.go +++ /dev/null @@ -1,58 +0,0 @@ -package maildoor - -import ( - "embed" - "fmt" - "html/template" - "io" - "path/filepath" - txtTemplate "text/template" -) - -var ( - //go:embed templates - templates embed.FS -) - -// buildTemplate for the passed template and data on a passed writer -// this is helpful to be able to render the templates in a generic way -// across different handlers. -func buildTemplate(tpath string, w io.Writer, data interface{}) error { - content, err := templates.ReadFile(tpath) - if err != nil { - return err - } - - // Non HTML templates do not use layouts. - if filepath.Ext(tpath) != ".html" { - t, err := txtTemplate.New(tpath).Parse(string(content)) - if err != nil { - return err - } - - return t.Execute(w, data) - } - - layout, err := templates.ReadFile("templates/layout.html") - if err != nil { - return err - } - - htmlTemplate, err := template.New("layout").Parse(string(layout)) - if err != nil { - return fmt.Errorf("error parsing layout template: %w", err) - } - - contents := fmt.Sprintf(`{{define "content"}}%s{{end}}`, string(content)) - htmlTemplate, err = template.Must(htmlTemplate.Clone()).Parse(contents) - if err != nil { - return fmt.Errorf("error parsing template %w", err) - } - - err = htmlTemplate.Execute(w, data) - if err != nil { - return err - } - - return nil -} diff --git a/cookie_valuer.go b/cookie_valuer.go deleted file mode 100644 index 918e1fa..0000000 --- a/cookie_valuer.go +++ /dev/null @@ -1,7 +0,0 @@ -package maildoor - -import "net/http" - -type CookieValuer interface { - CookieValue(r *http.Request) (string, error) -} diff --git a/defaulttokenmanager.go b/defaulttokenmanager.go deleted file mode 100644 index 2043e86..0000000 --- a/defaulttokenmanager.go +++ /dev/null @@ -1,20 +0,0 @@ -package maildoor - -import "time" - -// DefaultTokenManager is the default token manager which is -// an alias for a byte slice that will implement the TokenManager -// interface by using JWT. -type DefaultTokenManager []byte - -// Generate a JWT token that lasts for 30 minutes. The duration of the token -// is specified within the ExpiresAt claim. -func (dm DefaultTokenManager) Generate(user Emailable) (string, error) { - return GenerateJWT(30*time.Minute, []byte(dm)) -} - -// Validate the passed token and returns true if the token is valid. This implementation -// checks that the ExpiresAt claim to check that the token has not expired. -func (dm DefaultTokenManager) Validate(token string) (bool, error) { - return ValidateJWT(token, []byte(dm)) -} diff --git a/emailable.go b/emailable.go deleted file mode 100644 index a5ec8a1..0000000 --- a/emailable.go +++ /dev/null @@ -1,11 +0,0 @@ -package maildoor - -// Emailable is the type that will be returned from the -// finder, for maildoor to work it needs to be able to -// use a type that can provide an email address. -type Emailable interface { - // EmailAddress returns the email address of the user - // that will be authenticated. This address is used to - // send the authentication email to the user. - EmailAddress() string -} diff --git a/errors.go b/errors.go deleted file mode 100644 index f076cee..0000000 --- a/errors.go +++ /dev/null @@ -1,13 +0,0 @@ -package maildoor - -// ecodes holds the error messages for the supported error codes. -// these get rendered in the login page error box. -var ecodes = map[string]string{ - "E1": "😥 something happened while trying to find a user account with the given email. Please try again.", - "E2": "We're sorry, the specified token has already expired. Please enter your email again to receive a new one.", - "E3": "The token you have entered is invalid. Please enter your email again to receive a new one.", - "E4": "🤔 something was out of order with your previous login attempt. Please try again.", - "E5": "😥 something happened while attempting to send the login email. Please try again.", - "E6": "😥 an error occurred while generating authentication token. Please try again.", - "E7": "😥 an error occurred login in specified user. Please try again.", -} diff --git a/errtokenmanager_test.go b/errtokenmanager_test.go deleted file mode 100644 index 2338647..0000000 --- a/errtokenmanager_test.go +++ /dev/null @@ -1,17 +0,0 @@ -package maildoor_test - -import ( - "fmt" - - "github.com/wawandco/maildoor" -) - -type errTokenManager string - -func (et errTokenManager) Generate(maildoor.Emailable) (string, error) { - return "", fmt.Errorf("%s", string(et)) -} - -func (et errTokenManager) Validate(tt string) (bool, error) { - return true, fmt.Errorf("%s", string(et)) -} diff --git a/go.mod b/go.mod index 60a5979..3b0e62c 100644 --- a/go.mod +++ b/go.mod @@ -1,5 +1,3 @@ module github.com/wawandco/maildoor -go 1.17 - -require github.com/golang-jwt/jwt/v4 v4.2.0 +go 1.22 diff --git a/go.sum b/go.sum index ec4ea7a..e69de29 100644 --- a/go.sum +++ b/go.sum @@ -1,2 +0,0 @@ -github.com/golang-jwt/jwt/v4 v4.2.0 h1:besgBTC8w8HjP6NzQdxwKH9Z5oQMZ24ThTrHp3cZ8eU= -github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= diff --git a/handler.go b/handler.go deleted file mode 100644 index 9cbf464..0000000 --- a/handler.go +++ /dev/null @@ -1,103 +0,0 @@ -package maildoor - -import ( - "net/http" - "path" - "strings" -) - -// handler takes care of processing different actions against the maildoor -// server, such as login, send, validate, logout, most of these involve calling -// the corresponding functions provided by the host application. -type handler struct { - prefix string - baseURL string - csrfTokenSecret string - - // Product settings - product productConfig - - finderFn func(token string) (Emailable, error) - senderFn func(message *Message) error - afterLoginFn func(w http.ResponseWriter, r *http.Request, user Emailable) error - logoutFn func(w http.ResponseWriter, r *http.Request) error - - tokenManager TokenManager - logger Logger - - // Serves the static assets such as css and images - assetsServer http.Handler - - valueEncoder valueEncoder -} - -func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { - h.logger.Info(r.Method, ":", r.URL.Path) - - r.URL.Path = strings.TrimSuffix(r.URL.Path, "/") - if !strings.HasPrefix(r.URL.Path, h.prefix) { - r.URL.Path = path.Join(h.prefix, r.URL.Path) - } - - err := r.ParseForm() - if err != nil { - h.logger.Errorf("error parsing form: %v", err) - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - // Overriding the request method to allow for browsers - // to do DELETE/PATCH/PUT requests. - if r.Form.Get("_method") != "" { - h.logger.Infof("Request method upgraded to be %v", r.Form.Get("_method")) - r.Method = r.Form.Get("_method") - } - - if r.URL.Path == h.prefix { - http.Redirect(w, r, path.Join(h.prefix, "/login/"), http.StatusFound) - return - } - - if r.URL.Path == path.Join(h.prefix, "/login/") && r.Method == http.MethodGet { - h.login(w, r) - - return - } - - if r.URL.Path == path.Join(h.prefix, "/send/") && r.Method == http.MethodPost { - h.send(w, r) - - return - } - - if r.URL.Path == path.Join(h.prefix, "/validate/") && r.Method == http.MethodGet { - h.validate(w, r) - - return - } - - if r.URL.Path == path.Join(h.prefix, "/logout/") && r.Method == http.MethodDelete { - h.logout(w, r) - - return - } - - if strings.HasPrefix(r.URL.Path, path.Join(h.prefix, "assets")) && r.Method == http.MethodGet { - // Trimming the prefix to get the path of the asset - r.URL.Path = strings.Replace(r.URL.Path, path.Join(h.prefix), "", 1) - h.assetsServer.ServeHTTP(w, r) - - return - } - - http.NotFound(w, r) -} - -func (h handler) CookieValue(r *http.Request) (string, error) { - v, err := r.Cookie(DefaultCookieName) - if err != nil { - return "", err - } - - return h.valueEncoder.Decode(v.Value) -} diff --git a/internal/internal.go b/internal/internal.go new file mode 100644 index 0000000..7ccf646 --- /dev/null +++ b/internal/internal.go @@ -0,0 +1,9 @@ +package internal + +import "embed" + +//go:embed *.html +var templates embed.FS + +//go:embed *.png +var Assets embed.FS diff --git a/internal/layout.html b/internal/layout.html new file mode 100644 index 0000000..ca02c68 --- /dev/null +++ b/internal/layout.html @@ -0,0 +1,18 @@ + + + + + + + {{block "title" .}}Maildoor{{end}} + + + + + + +
+ {{block "yield" .}}{{end}} +
+ + diff --git a/internal/login.go b/internal/login.go new file mode 100644 index 0000000..6de56c4 --- /dev/null +++ b/internal/login.go @@ -0,0 +1,26 @@ +package internal + +import ( + "html/template" + "net/http" +) + +// Login enpoint renders the login page to enter the user +// identifier. +func Login(w http.ResponseWriter, r *http.Request){ + page := struct{ + Logo string + }{} + + + tt, err := template.ParseFS(templates, "layout.html", "login.html") + if err != nil { + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + + err = tt.Execute(w, page) + if err != nil { + return + } +} diff --git a/internal/login.html b/internal/login.html new file mode 100644 index 0000000..d30cc32 --- /dev/null +++ b/internal/login.html @@ -0,0 +1,44 @@ +{{define "yield"}} +
+
+
+ product logo +
+ +
+

+ Welcome 👋 +

+
+ + +
+ + +
+

+ Please enter the email address associated with your account, + our system will email an access code to that address upon successful + identification of your account. +

+ +
+ +
+ +
+ +
+
+ +
+ +
+
+
+
+{{end}} diff --git a/internal/logo.png b/internal/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..cbc7de83c3c54d70e7f0cabbc3dfd6101ee0cbbb GIT binary patch literal 52681 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

FyhQ#T#eEb7{s} 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 cJZ}XU7?id+ + @layer base { + input[type="number"]::-webkit-inner-spin-button, + input[type="number"]::-webkit-outer-spin-button { + -webkit-appearance: none; + margin: 0; + } + } + +

+{{end}} diff --git a/internal/token_invalid.html b/internal/token_invalid.html new file mode 100644 index 0000000..c495d90 --- /dev/null +++ b/internal/token_invalid.html @@ -0,0 +1,29 @@ + + +
+

Invalid Token

+ + +
+ + + + + + + +
+ + +
diff --git a/internal/validate.go b/internal/validate.go new file mode 100644 index 0000000..4de543f --- /dev/null +++ b/internal/validate.go @@ -0,0 +1,54 @@ +package internal + +import ( + "fmt" + "html/template" + "net/http" + "strings" +) + + +func Validate(w http.ResponseWriter, r *http.Request) { + err := r.ParseForm() + if err != nil { + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + + email:= r.FormValue("email") + + // Find a combination of token and email in the server + // call the afterlogin hook with the email + // remove the token from the server + token := strings.Join(r.Form["code[]"], "") + valid := token == tokens[email] + if !valid { + // Generate a token and store it in the server + // Save the user email in the session + tt, err := template.ParseFS(templates, "token_invalid.html") + if err != nil { + fmt.Println(err) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + + err = tt.Execute(w, struct{ + Logo string + Email string + }{ + Email: email, + }) + + if err != nil { + fmt.Println(err) + return + } + + return + } + + delete(tokens, email) + + w.Write([]byte("Welcome")) + // Do the after login hook +} diff --git a/jwt.go b/jwt.go deleted file mode 100644 index 20a57ec..0000000 --- a/jwt.go +++ /dev/null @@ -1,49 +0,0 @@ -package maildoor - -import ( - "fmt" - "strings" - "time" - - "github.com/golang-jwt/jwt/v4" -) - -// GenerateJWT token with the specified duration and secret. -func GenerateJWT(d time.Duration, secret []byte) (string, error) { - expiration := time.Now().Add(d).Format(time.RFC3339) - t := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ - "ExpiresAt": expiration, - }) - - return t.SignedString(secret) -} - -// ValidateJWT token with the specified secret. -func ValidateJWT(tt string, secret []byte) (bool, error) { - tokenString := strings.TrimSpace(tt) - t, err := jwt.ParseWithClaims(tokenString, &jwt.MapClaims{}, jwtKeyFunc(secret)) - - if err != nil { - return false, fmt.Errorf("error parsing error: %w", err) - } - - cl := t.Claims.(*jwt.MapClaims) - expires, err := time.Parse(time.RFC3339, (*cl)["ExpiresAt"].(string)) - - if err != nil || expires.Before(time.Now()) { - return false, nil - } - - return err == nil, err -} - -func jwtKeyFunc(key []byte) func(token *jwt.Token) (interface{}, error) { - return func(token *jwt.Token) (interface{}, error) { - _, ok := token.Method.(*jwt.SigningMethodHMAC) - if !ok { - return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"]) - } - - return key, nil - } -} diff --git a/logger.go b/logger.go deleted file mode 100644 index 7c71701..0000000 --- a/logger.go +++ /dev/null @@ -1,55 +0,0 @@ -package maildoor - -import "log" - -var ( - // default logger is a mute logger as we don't - // want to spam the logs unless explicitly told by - // the user. - defaultLogger Logger = muteLogger(1) - - // BasicLogger is a simple logger that prints to stdout - // using the `log` package. - BasicLogger = stdOutLogger(2) -) - -// Logger interface defines the minimum set of methods -// that a logger should satisfy to be used by the library. -type Logger interface { - // Log a message at the Info level. - Info(args ...interface{}) - - // Log a formatted message at the Info level. - Infof(format string, args ...interface{}) - - // Log a message at the Error level. - Error(args ...interface{}) - - // Log a formatted message at the Error level. - Errorf(format string, args ...interface{}) -} - -type muteLogger int - -func (l muteLogger) Info(args ...interface{}) {} -func (l muteLogger) Infof(format string, args ...interface{}) {} -func (l muteLogger) Error(args ...interface{}) {} -func (l muteLogger) Errorf(format string, args ...interface{}) {} - -type stdOutLogger int - -func (l stdOutLogger) Info(args ...interface{}) { - log.Print("level=info ", args) -} - -func (l stdOutLogger) Infof(format string, args ...interface{}) { - log.Printf("level=info "+format, args) -} - -func (l stdOutLogger) Error(args ...interface{}) { - log.Print("level=info ", args) -} - -func (l stdOutLogger) Errorf(format string, args ...interface{}) { - log.Printf("level=info "+format, args) -} diff --git a/logger_test.go b/logger_test.go deleted file mode 100644 index 44e0a24..0000000 --- a/logger_test.go +++ /dev/null @@ -1,54 +0,0 @@ -package maildoor_test - -import ( - "fmt" - "net/http" - "net/http/httptest" - "testing" - - "github.com/wawandco/maildoor" - "github.com/wawandco/maildoor/internal/testhelpers" -) - -// stringLogger is a logger that writes to a string -// it serves as a way to demonstrate how to implement -// a custom logger if the application needs to log to -// other than stdout. -type stringLogger struct { - content string -} - -func (sl *stringLogger) Info(els ...interface{}) { - sl.content += "level=info " - sl.content += fmt.Sprint(els...) - sl.content += "\n" -} - -func (sl *stringLogger) Infof(format string, args ...interface{}) { - sl.content += fmt.Sprintf("level=info %v \n"+format, args...) -} - -func (sl *stringLogger) Error(els ...interface{}) { - sl.content += "level=error " - sl.content += fmt.Sprint(els...) - sl.content += "\n" -} - -func (sl *stringLogger) Errorf(format string, args ...interface{}) { - sl.content += fmt.Sprintf("level=error %v \n"+format, args...) -} - -func TestCustomLogger(t *testing.T) { - lg := &stringLogger{} - h, err := maildoor.NewWithOptions("secret", maildoor.UseLogger(lg)) - - testhelpers.NoError(t, err) - w := httptest.NewRecorder() - - req := httptest.NewRequest(http.MethodGet, "/auth/login/", nil) - h.ServeHTTP(w, req) - testhelpers.Equals(t, http.StatusOK, w.Code) - - testhelpers.Contains(t, lg.content, "level=info") - testhelpers.Contains(t, lg.content, "/auth/login") -} diff --git a/login.go b/login.go deleted file mode 100644 index d9008d9..0000000 --- a/login.go +++ /dev/null @@ -1,43 +0,0 @@ -package maildoor - -import ( - "net/http" -) - -// login function renders the login page, it also renders conditionally -// errors because when some of the other endpoints fail, it will redirect -// to this page. -func (h handler) login(w http.ResponseWriter, r *http.Request) { - token, err := GenerateJWT(csrfDuration, []byte(h.csrfTokenSecret)) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - data := struct { - Title string - Action string - Logo string - Favicon string - CSRFToken string - Error string - - StylesPath string - }{ - Title: "Login Page", - Action: h.sendPath(), - Logo: h.product.LogoURL, - Favicon: h.product.FaviconURL, - CSRFToken: token, - Error: ecodes[r.Form.Get("error")], - - StylesPath: h.stylesPath(), - } - - err = buildTemplate("templates/login.html", w, data) - - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } -} diff --git a/login_test.go b/login_test.go deleted file mode 100644 index 8c810c4..0000000 --- a/login_test.go +++ /dev/null @@ -1,101 +0,0 @@ -package maildoor_test - -import ( - "net/http" - "net/http/httptest" - "regexp" - "testing" - - "github.com/wawandco/maildoor" - "github.com/wawandco/maildoor/internal/testhelpers" -) - -func TestLogin(t *testing.T) { - h, err := maildoor.NewWithOptions("secret") - - testhelpers.NoError(t, err) - - t.Run("Content", func(tt *testing.T) { - req := httptest.NewRequest(http.MethodGet, "/auth/login/", nil) - w := httptest.NewRecorder() - - h.ServeHTTP(w, req) - testhelpers.Equals(tt, http.StatusOK, w.Code) - - content := w.Body.String() - - testhelpers.Contains(tt, content, "Welcome Back 👋") - testhelpers.Contains(tt, content, "/auth/send") - testhelpers.Contains(tt, content, ``) - }, - }, - - { - name: "Invalid Error", - url: "/auth/login/?err=SOMETHING", - val: func(t *testing.T, content string) { - testhelpers.NotContains(tt, content, `