Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

v1 #6

Merged
merged 38 commits into from
May 10, 2024
Merged

v1 #6

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
22c4725
new: getting started with V2
paganotoni Mar 22, 2024
d6387c8
adding options
paganotoni Mar 23, 2024
844fb4a
feature: some progress
paganotoni Mar 23, 2024
9fad629
updating gitignore
paganotoni Mar 23, 2024
48b4fd0
feature: some progress on the implementation
paganotoni Mar 25, 2024
1a38116
task: some reorganization here and there
paganotoni Mar 26, 2024
95a475a
task: improving logging
paganotoni Mar 26, 2024
57a5141
code: removing unused token length
paganotoni Mar 26, 2024
4db9cf3
task: adding email sending hook
paganotoni Mar 26, 2024
7ba4830
fix: fixing code comparison
paganotoni Mar 26, 2024
e6af769
sending email from the sample app
paganotoni Mar 27, 2024
da82a60
feature: changing the token input
paganotoni Mar 27, 2024
d33f719
task: adding tests for the handlers defined
paganotoni Mar 29, 2024
66800bf
task: updating readme
paganotoni Mar 29, 2024
9cbdacf
task: improvements to the error code
paganotoni Mar 29, 2024
a50feb4
content: UI tweaks
paganotoni Mar 29, 2024
9d96e2a
task: adding some TODOS
paganotoni Mar 29, 2024
6dd0a17
task: adding login option
paganotoni Mar 31, 2024
8120bf3
feature: adding custom product name
paganotoni Mar 31, 2024
34ea710
feature: allowing to define the product name
paganotoni Mar 31, 2024
50f5561
docs: adding some docs in the README
paganotoni Mar 31, 2024
6ab273b
task: updating some tests
paganotoni Mar 31, 2024
7e4f669
task: updating github action
paganotoni Mar 31, 2024
82dc5a6
some renaming on the github action
paganotoni Mar 31, 2024
03756bb
task: adding sample image logo
paganotoni Mar 31, 2024
2ec7b0e
ui: using sample inc logo image
paganotoni Mar 31, 2024
5de0561
task: adding logo sample
paganotoni Mar 31, 2024
73a4303
ui: using correct version of the logo
paganotoni Mar 31, 2024
aaba540
task: adding logout option
paganotoni Mar 31, 2024
1f4a47d
more tweaking here and there
paganotoni Mar 31, 2024
32ee121
more tweaks here and there
paganotoni Mar 31, 2024
bac5e8b
tweak: file naming adjustments
paganotoni Mar 31, 2024
a47c523
task: minor tweaks:
paganotoni Apr 1, 2024
c617a26
task: adding the set icon method and field to the struct
MateoCaicedoW Apr 4, 2024
119f81d
task: adding logo, icon, and product name to handle_login.go and hand…
MateoCaicedoW Apr 4, 2024
05612c3
task: updating handle_code.go and handle_code.html, add logo, icon, a…
MateoCaicedoW Apr 4, 2024
000e851
task: adding some tests
MateoCaicedoW Apr 4, 2024
9eacd53
Merge pull request #7 from wawandco/task-adding-icon-and-title
paganotoni Apr 4, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 21 additions & 21 deletions .github/workflows/checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,33 +11,33 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Set up Go
uses: actions/setup-go@v3
uses: actions/setup-go@v5
with:
go-version: 1.17
- uses: actions/checkout@v3
go-version: 1.22
- uses: actions/checkout@v4
- name: golangci-lint
uses: golangci/golangci-lint-action@v2
uses: golangci/golangci-lint-action@v4
with:
skip-go-installation: true
args: --version
version: v1.44.2
args: --version
version: v1.54
tests:
name: tests
needs: lint
runs-on: ubuntu-latest
steps:
- name: Set up Go 1.17
uses: actions/setup-go@v2
with:
go-version: '1.17'
id: go
- uses: actions/checkout@v2
- uses: actions/cache@v1
with:
path: ~/go/pkg/mod
key: ${{ runner.os }}-go-${{ hashFiles('go.sum') }}
restore-keys: |
${{ runner.os }}-go
- name: Buffalo Tests
run: |
go test --cover ./... -v
- name: Set up Go 1.22
uses: actions/setup-go@v5
with:
go-version: "1.22"
id: go
- uses: actions/checkout@v4
- uses: actions/cache@v4
with:
path: ~/go/pkg/mod
key: ${{ runner.os }}-go-${{ hashFiles('go.sum') }}
restore-keys: |
${{ runner.os }}-go
- name: test
run: |
go test --cover ./... -v
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
.DS_Store
bin
bin
tmp
290 changes: 49 additions & 241 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,256 +1,64 @@
![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.

<div style="padding: 20px; text-align: center;">
<img src="./showcase-login.jpg" width="350" height="350" style="display: inline; margin-right: 30px;"/>
<img src="./showcase-sent.jpg" width="350" height="350" style="display: inline"/>
</div>

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.
Maildoor is an email based authentication library that allows users to sign up and sign in to your application using their email address. It is a pluggable library that can be used with any go http server.

#### FinderFn
### Usage

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:
Using maildoor is as simple as creating a new instance of the maildoor.Handler and passing it to your http server.

```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",
...
// Initialize the maildoor handler
auth := maildoor.New(
maildoor.Logo("https://example.com/logo.png"),
maildoor.ProductName("My App"))
maildoor.Prefix("/auth/"), // Prefix for the routes

// Defines the email sending mechanism which is up to the
// host application to implement.
maildoor.EmailSender(func(to, html, txt string) error{
// send email
return nil
}),

// Defines the email validation mechanism
maildoor.EmailValidator(func(email string) bool {
// validate email
return true
}),

// Defines what to do after the user has successfuly logged in
// This is where you would set the user session or redirect to a private page
maildoor.AfterLogin(func w http.ResponseWriter, r http.Request) {
http.Redirect(w, r, "/private", http.StatusFound)
}),

// Defines what to do after the user has successfuly loged out
// This is where you would clear the user session or redirect to a login page
maildoor.Logout(func(w http.ResponseWriter, r *http.Request){
http.Redirect(w, r, "/auth/login", http.StatusFound)
}),
})

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
mux := http.NewServeMux()
mux.Handle("/auth", auth)
mux.Handle("/private", secure(privateHandler))
http.ListenAndServe(":8080", mux)
```
## 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.

## Features

- Pluggable http.Handler that can be used with any go http server
- Customizable email sending mechanism
- Customizable email validation mechanism
- Customizable logo
- Customizable product name

### Roadmap

- Custom token storage mechanism
- Out of the box time bound token generation
- Customizable templates (Bring your own).
- Time based token expiration out the box
- Prevend CSRF attacks with token
Binary file removed assets/images/favicon.png
Binary file not shown.
Binary file removed assets/images/favicon_dark.png
Binary file not shown.
Binary file removed assets/images/maildoor_icon.png
Binary file not shown.
Binary file removed assets/images/maildoor_icon_dark.png
Binary file not shown.
Binary file removed assets/images/maildoor_logo_2.png
Binary file not shown.
Loading
Loading