mirror of
https://github.com/steveiliop56/tinyauth.git
synced 2025-10-28 12:45:47 +00:00
Compare commits
9 Commits
v0.1.0
...
v0.2.0-bet
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6596e4dea6 | ||
|
|
a2e6231cc4 | ||
|
|
644b343a1b | ||
|
|
7817add9b4 | ||
|
|
fcaa3779d5 | ||
|
|
e2f97d1fbe | ||
|
|
4f4645f32b | ||
|
|
d0c1aae1e7 | ||
|
|
b8a134ed12 |
8
.gitignore
vendored
8
.gitignore
vendored
@@ -2,4 +2,10 @@
|
||||
internal/assets/dist
|
||||
|
||||
# binaries
|
||||
tinyauth
|
||||
tinyauth
|
||||
|
||||
# test docker compose
|
||||
docker-compose.test.yml
|
||||
|
||||
# users file
|
||||
users.txt
|
||||
34
README.md
34
README.md
@@ -1,38 +1,16 @@
|
||||
# Tinyauth - The easiest way to secure your traefik apps with a login screen
|
||||
# Tinyauth - The simplest way to protect your apps with a login screen
|
||||
|
||||
Tinyauth is an extremely simple traefik forward auth proxy that adds a login screen to all of your apps that are using the traefik reverse proxy. Tinyauth is configurable through environment variables and it is only 20MB in size.
|
||||
|
||||
## Screenshots
|
||||
|
||||
Talk is cheap, here are some screenshots:
|
||||
|
||||
| | |
|
||||
| --------------------------------------- | ----------------------------------------- |
|
||||
|  |  |
|
||||
|  |  |
|
||||
Tinyauth is an extremely simple traefik middleware that adds a login screen to all of your apps that are using the traefik reverse proxy. Tinyauth is configurable through environment variables and it is only 20MB in size.
|
||||
|
||||
## Getting started
|
||||
|
||||
Tinyauth is extremely easy to run since it's shipped as a docker container. I chose to bundle it with busybox so as you can easily debug the API (e.g. using curl) and have some simple linux tools. If you want to get started with an example just check the example docker compose file [here](./docker-compose.example.yml)
|
||||
|
||||
## Environment variables
|
||||
|
||||
Tinyauth accepts the following environment variables:
|
||||
|
||||
| Name | Description | Default | Required |
|
||||
| ---------- | ------------------------------------------------------- | ------- | -------- |
|
||||
| `PORT` | The port the API listens on. | 3000 | no |
|
||||
| `ADDRESS` | The address the API binds on. | 0.0.0.0 | no |
|
||||
| `SECRET` | A 32 character long string used for the sessions. | - | yes |
|
||||
| `ROOT_URL` | The base URL of your domain. (e.g. https://example.com) | - | yes |
|
||||
| `APP_URL` | The Tinyauth URL. (e.g. https://tinyauth.example.com) | - | yes |
|
||||
| `USERS` | Comma seperated list of `user:bcrypt-password-hash`. | - | yes |
|
||||
Tinyauth is extremely easy to run since it's shipped as a docker container. The guide on how to get started is available on the website [here](https://tinyauth.doesmycode.work/).
|
||||
|
||||
## FAQ
|
||||
|
||||
### Why?
|
||||
|
||||
Why make this project? Well we all know that more powerful alternatives like authentik and authelia exist but when I tried to use them I felt overwhelmed with all the configration options and environment variables I had to configure in order for them to work, so, I decided to make a small alternative in Go to both test my skills and cover my simple login screen needs.
|
||||
Why make this project? Well, we all know that more powerful alternatives like authentik and authelia exist, but when I tried to use them, I felt overwhelmed with all the configration options and environment variables I had to configure in order for them to work. So, I decided to make a small alternative in Go to both test my skills and cover my simple login screen needs.
|
||||
|
||||
### Is this secure?
|
||||
|
||||
@@ -40,7 +18,7 @@ Probably, the sessions are managed with the gin sessions package so it should be
|
||||
|
||||
### Do I need to login every time?
|
||||
|
||||
No, when you login tinyauth sets a `tinyauth` cookie in your browser that applies to all of the subdomains of the root URL you set.
|
||||
No, when you login, tinyauth sets a `tinyauth` cookie in your browser that applies to all of the subdomains of your domain.
|
||||
|
||||
## License
|
||||
|
||||
@@ -48,7 +26,7 @@ Tinyauth is licensed under the GNU General Public License v3.0. TL;DR — You ma
|
||||
|
||||
## Contributing
|
||||
|
||||
Any contributions to the codebase are welcome! I am not a cybersecurity person so my code may have some vulnerability, if you find something that could be used to exploit and bypass tinyauth please tell me as soon as possible so I can fix it.
|
||||
Any contributions to the codebase are welcome! I am not a cybersecurity person so my code may have a security issue, if you find something that could be used to exploit and bypass tinyauth please let me know as soon as possible so I can fix it.
|
||||
|
||||
## Acknowledgements
|
||||
|
||||
|
||||
33
cmd/root.go
33
cmd/root.go
@@ -2,6 +2,7 @@ package cmd
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
"tinyauth/internal/api"
|
||||
"tinyauth/internal/assets"
|
||||
@@ -36,10 +37,30 @@ var rootCmd = &cobra.Command{
|
||||
validateErr := validator.Struct(config)
|
||||
HandleError(validateErr, "Invalid config")
|
||||
|
||||
// Create users list
|
||||
log.Info().Msg("Creating users list")
|
||||
userList, createErr := utils.CreateUsersList(config.Users)
|
||||
HandleError(createErr, "Failed to create users list")
|
||||
// Parse users
|
||||
log.Info().Msg("Parsing users")
|
||||
|
||||
if config.UsersFile == "" && config.Users == "" {
|
||||
log.Fatal().Msg("No users provided")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
users := config.Users
|
||||
|
||||
if config.UsersFile != "" {
|
||||
log.Info().Msg("Reading users from file")
|
||||
usersFromFile, readErr := utils.GetUsersFromFile(config.UsersFile)
|
||||
HandleError(readErr, "Failed to read users from file")
|
||||
usersFromFileParsed := strings.Join(strings.Split(usersFromFile, "\n"), ",")
|
||||
if users != "" {
|
||||
users = users + "," + usersFromFileParsed
|
||||
} else {
|
||||
users = usersFromFileParsed
|
||||
}
|
||||
}
|
||||
|
||||
userList, createErr := utils.ParseUsers(users)
|
||||
HandleError(createErr, "Failed to parse users")
|
||||
|
||||
// Start server
|
||||
log.Info().Msg("Starting server")
|
||||
@@ -66,14 +87,14 @@ func init() {
|
||||
rootCmd.Flags().IntP("port", "p", 3000, "Port to run the server on.")
|
||||
rootCmd.Flags().String("address", "0.0.0.0", "Address to bind the server to.")
|
||||
rootCmd.Flags().String("secret", "", "Secret to use for the cookie.")
|
||||
rootCmd.Flags().String("root-url", "", "Root URL of traefik.")
|
||||
rootCmd.Flags().String("app-url", "", "The tinyauth URL.")
|
||||
rootCmd.Flags().String("users", "", "Comma separated list of users in the format username:bcrypt-hashed-password.")
|
||||
rootCmd.Flags().String("users-file", "", "Path to a file containing users in the format username:bcrypt-hashed-password.")
|
||||
viper.BindEnv("port", "PORT")
|
||||
viper.BindEnv("address", "ADDRESS")
|
||||
viper.BindEnv("secret", "SECRET")
|
||||
viper.BindEnv("root-url", "ROOT_URL")
|
||||
viper.BindEnv("app-url", "APP_URL")
|
||||
viper.BindEnv("users", "USERS")
|
||||
viper.BindEnv("users-file", "USERS_FILE")
|
||||
viper.BindPFlags(rootCmd.Flags())
|
||||
}
|
||||
|
||||
34
docker-compose.dev.yml
Normal file
34
docker-compose.dev.yml
Normal file
@@ -0,0 +1,34 @@
|
||||
services:
|
||||
traefik:
|
||||
container_name: traefik
|
||||
image: traefik:v3.3
|
||||
command: --api.insecure=true --providers.docker
|
||||
ports:
|
||||
- 80:80
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
labels:
|
||||
traefik.http.middlewares.tinyauth.forwardauth.address: http://tinyauth:3000/api/auth
|
||||
|
||||
nginx:
|
||||
container_name: nginx
|
||||
image: nginx:latest
|
||||
labels:
|
||||
traefik.enable: true
|
||||
traefik.http.routers.nginx.rule: Host(`nginx.dev.local`)
|
||||
traefik.http.services.nginx.loadbalancer.server.port: 80
|
||||
traefik.http.routers.nginx.middlewares: tinyauth
|
||||
|
||||
tinyauth:
|
||||
container_name: tinyauth
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
environment:
|
||||
- SECRET=some-random-32-chars-string
|
||||
- APP_URL=http://tinyauth.dev.local
|
||||
- USERS=user:$$2a$$10$$UdLYoJ5lgPsC0RKqYH/jMua7zIn0g9kPqWmhYayJYLaZQ/FTmH2/u # user:password
|
||||
labels:
|
||||
traefik.enable: true
|
||||
traefik.http.routers.tinyauth.rule: Host(`tinyauth.dev.local`)
|
||||
traefik.http.services.tinyauth.loadbalancer.server.port: 3000
|
||||
@@ -15,7 +15,7 @@ services:
|
||||
image: nginx:latest
|
||||
labels:
|
||||
traefik.enable: true
|
||||
traefik.http.routers.nginx.rule: Host(`nginx.dev.local`)
|
||||
traefik.http.routers.nginx.rule: Host(`nginx.example.com`)
|
||||
traefik.http.services.nginx.loadbalancer.server.port: 80
|
||||
traefik.http.routers.nginx.middlewares: tinyauth
|
||||
|
||||
@@ -24,10 +24,9 @@ services:
|
||||
image: ghcr.io/steveiliop56/tinyauth:latest
|
||||
environment:
|
||||
- SECRET=some-random-32-chars-string
|
||||
- ROOT_URL=https://example.com
|
||||
- APP_URL=https://tinyauth.example.com
|
||||
- USERS=user:$$2a$$10$$UdLYoJ5lgPsC0RKqYH/jMua7zIn0g9kPqWmhYayJYLaZQ/FTmH2/u
|
||||
- USERS=user:$$2a$$10$$UdLYoJ5lgPsC0RKqYH/jMua7zIn0g9kPqWmhYayJYLaZQ/FTmH2/u # user:password
|
||||
labels:
|
||||
traefik.enable: true
|
||||
traefik.http.routers.tinyauth.rule: Host(`tinyauth.dev.local`)
|
||||
traefik.http.routers.tinyauth.rule: Host(`tinyauth.example.com`)
|
||||
traefik.http.services.tinyauth.loadbalancer.server.port: 3000
|
||||
|
||||
@@ -9,7 +9,9 @@ import (
|
||||
"time"
|
||||
"tinyauth/internal/assets"
|
||||
"tinyauth/internal/auth"
|
||||
"tinyauth/internal/hooks"
|
||||
"tinyauth/internal/types"
|
||||
"tinyauth/internal/utils"
|
||||
|
||||
"github.com/gin-contrib/sessions"
|
||||
"github.com/gin-contrib/sessions/cookie"
|
||||
@@ -32,8 +34,13 @@ func Run(config types.Config, users types.UserList) {
|
||||
fileServer := http.FileServer(http.FS(dist))
|
||||
store := cookie.NewStore([]byte(config.Secret))
|
||||
|
||||
domain := strings.Split(config.RootURL, "://")[1]
|
||||
domain, domainErr := utils.GetRootURL(config.AppURL)
|
||||
|
||||
if domainErr != nil {
|
||||
log.Fatal().Err(domainErr).Msg("Failed to get domain")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
store.Options(sessions.Options{
|
||||
Domain: fmt.Sprintf(".%s", domain),
|
||||
Path: "/",
|
||||
@@ -52,20 +59,14 @@ func Run(config types.Config, users types.UserList) {
|
||||
})
|
||||
|
||||
router.GET("/api/auth", func (c *gin.Context) {
|
||||
session := sessions.Default(c)
|
||||
value := session.Get("tinyauth")
|
||||
userContext := hooks.UseUserContext(c, users)
|
||||
|
||||
if value != nil {
|
||||
usernameString, ok := value.(string)
|
||||
if ok {
|
||||
if auth.FindUser(users, usernameString) != nil {
|
||||
c.JSON(200, gin.H{
|
||||
"status": 200,
|
||||
"message": "Authorized",
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
if userContext.IsLoggedIn {
|
||||
c.JSON(200, gin.H{
|
||||
"status": 200,
|
||||
"message": "Authenticated",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
uri := c.Request.Header.Get("X-Forwarded-Uri")
|
||||
@@ -139,29 +140,23 @@ func Run(config types.Config, users types.UserList) {
|
||||
})
|
||||
|
||||
router.GET("/api/status", func (c *gin.Context) {
|
||||
session := sessions.Default(c)
|
||||
value := session.Get("tinyauth")
|
||||
userContext := hooks.UseUserContext(c, users)
|
||||
|
||||
if value != nil {
|
||||
usernameString, ok := value.(string)
|
||||
if ok {
|
||||
if auth.FindUser(users, usernameString) != nil {
|
||||
c.JSON(200, gin.H{
|
||||
"status": 200,
|
||||
"isLoggedIn": true,
|
||||
"username": usernameString,
|
||||
"version": assets.Version,
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
if !userContext.IsLoggedIn {
|
||||
c.JSON(200, gin.H{
|
||||
"status": 200,
|
||||
"message": "Unauthenticated",
|
||||
"username": "",
|
||||
"isLoggedIn": false,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, gin.H{
|
||||
"status": 200,
|
||||
"isLoggedIn": false,
|
||||
"username": "",
|
||||
"version": assets.Version,
|
||||
"message": "Authenticated",
|
||||
"username": userContext.Username,
|
||||
"isLoggedIn": true,
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
v0.1.0
|
||||
v0.2.0
|
||||
44
internal/hooks/hooks.go
Normal file
44
internal/hooks/hooks.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package hooks
|
||||
|
||||
import (
|
||||
"tinyauth/internal/auth"
|
||||
"tinyauth/internal/types"
|
||||
|
||||
"github.com/gin-contrib/sessions"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func UseUserContext(c *gin.Context, userList types.UserList) (types.UserContext) {
|
||||
session := sessions.Default(c)
|
||||
cookie := session.Get("tinyauth")
|
||||
|
||||
if cookie == nil {
|
||||
return types.UserContext{
|
||||
Username: "",
|
||||
IsLoggedIn: false,
|
||||
}
|
||||
}
|
||||
|
||||
username, ok := cookie.(string)
|
||||
|
||||
if !ok {
|
||||
return types.UserContext{
|
||||
Username: "",
|
||||
IsLoggedIn: false,
|
||||
}
|
||||
}
|
||||
|
||||
user := auth.FindUser(userList, username)
|
||||
|
||||
if user == nil {
|
||||
return types.UserContext{
|
||||
Username: "",
|
||||
IsLoggedIn: false,
|
||||
}
|
||||
}
|
||||
|
||||
return types.UserContext{
|
||||
Username: username,
|
||||
IsLoggedIn: true,
|
||||
}
|
||||
}
|
||||
@@ -22,7 +22,12 @@ type Config struct {
|
||||
Port int `validate:"number" mapstructure:"port"`
|
||||
Address string `mapstructure:"address, ip4_addr"`
|
||||
Secret string `validate:"required,len=32" mapstructure:"secret"`
|
||||
RootURL string `validate:"required,url" mapstructure:"root-url"`
|
||||
AppURL string `validate:"required,url" mapstructure:"app-url"`
|
||||
Users string `validate:"required" mapstructure:"users"`
|
||||
Users string `mapstructure:"users"`
|
||||
UsersFile string `mapstructure:"users-file"`
|
||||
}
|
||||
|
||||
type UserContext struct {
|
||||
Username string
|
||||
IsLoggedIn bool
|
||||
}
|
||||
@@ -2,16 +2,18 @@ package utils
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"tinyauth/internal/types"
|
||||
)
|
||||
|
||||
func CreateUsersList(users string) (types.UserList, error) {
|
||||
func ParseUsers(users string) (types.UserList, error) {
|
||||
var userList types.UserList
|
||||
userListString := strings.Split(users, ",")
|
||||
|
||||
if len(userListString) == 0 {
|
||||
return types.UserList{}, errors.New("no users found")
|
||||
return types.UserList{}, errors.New("invalid user format")
|
||||
}
|
||||
|
||||
for _, user := range userListString {
|
||||
@@ -26,4 +28,34 @@ func CreateUsersList(users string) (types.UserList, error) {
|
||||
}
|
||||
|
||||
return userList, nil
|
||||
}
|
||||
}
|
||||
|
||||
func GetRootURL(urlSrc string) (string, error) {
|
||||
urlParsed, parseErr := url.Parse(urlSrc)
|
||||
|
||||
if parseErr != nil {
|
||||
return "", parseErr
|
||||
}
|
||||
|
||||
urlSplitted := strings.Split(urlParsed.Host, ".")
|
||||
|
||||
urlFinal := urlSplitted[len(urlSplitted)-2] + "." + urlSplitted[len(urlSplitted)-1]
|
||||
|
||||
return urlFinal, nil
|
||||
}
|
||||
|
||||
func GetUsersFromFile(usersFile string) (string, error) {
|
||||
_, statErr := os.Stat(usersFile)
|
||||
|
||||
if statErr != nil {
|
||||
return "", statErr
|
||||
}
|
||||
|
||||
data, readErr := os.ReadFile(usersFile)
|
||||
|
||||
if readErr != nil {
|
||||
return "", readErr
|
||||
}
|
||||
|
||||
return string(data), nil
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ export const ContinuePage = () => {
|
||||
return (
|
||||
<Layout>
|
||||
<Paper shadow="md" p={30} mt={30} radius="md" withBorder>
|
||||
{redirectUri ? (
|
||||
{redirectUri !== "null" ? (
|
||||
<>
|
||||
<Text size="xl" fw={700}>
|
||||
Continue
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Button, Paper, Text } from "@mantine/core";
|
||||
import { Button, Code, Paper, Text } from "@mantine/core";
|
||||
import { notifications } from "@mantine/notifications";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import axios from "axios";
|
||||
@@ -43,8 +43,8 @@ export const LogoutPage = () => {
|
||||
Logout
|
||||
</Text>
|
||||
<Text>
|
||||
You are currently logged in as {username}, click the button below to
|
||||
log out.
|
||||
You are currently logged in as <Code>{username}</Code>, click the
|
||||
button below to log out.
|
||||
</Text>
|
||||
<Button
|
||||
fullWidth
|
||||
|
||||
Reference in New Issue
Block a user