Compare commits

..

1 Commits

Author SHA1 Message Date
Stavros
1b145fd531 chore: bump version 2025-02-08 12:43:30 +02:00
20 changed files with 111 additions and 938 deletions

View File

@@ -1,37 +0,0 @@
---
name: Bug report
about: Create a report to help improve Tinyauth
title: "[BUG]"
labels: bug
assignees: steveiliop56
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Logs**
Please include the Tinyauth logs below, make sure to not include sensitive info.
**Device (please complete the following information):**
- OS: [e.g. iOS]
- Browser [e.g. chrome, safari]
- Tinyauth [e.g. v2.1.1]
- Docker [e.g. 27.3.1]
**
**Additional context**
Add any other context about the problem here.

View File

@@ -1,20 +0,0 @@
---
name: Feature request
about: Suggest an idea for this project
title: "[FEATURE]"
labels: enhancement
assignees: steveiliop56
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

View File

@@ -1,42 +0,0 @@
name: Tinyauth CI
on:
push:
branches:
- main
pull_request:
branches:
- main
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: "^1.23.2"
- name: Setup bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Install frontend dependencies
run: |
cd site
bun install
- name: Build frontend
run: |
cd site
bun run build
- name: Copy frontend
run: |
cp -r site/dist internal/assets/dist
- name: Run tests
run: go test -v ./...

View File

@@ -1,128 +0,0 @@
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, religion, or sexual identity
and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our
community include:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
* Focusing on what is best not just for us as individuals, but for the
overall community
Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or
advances of any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email
address, without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
.
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series
of actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or
permanent ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within
the community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.0, available at
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
Community Impact Guidelines were inspired by [Mozilla's code of conduct
enforcement ladder](https://github.com/mozilla/diversity).
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see the FAQ at
https://www.contributor-covenant.org/faq. Translations are available at
https://www.contributor-covenant.org/translations.

View File

@@ -1,81 +0,0 @@
# Contributing
Contributing is relatively easy.
## Requirements
- Bun
- Golang v1.23.2 and above
- Git
- Docker
## Cloning the repository
You firstly need to clone the repository with:
```sh
git clone https://github.com/steveiliop56/tinyauth
cd tinyauth
```
## Install requirements
Now it's time to install the requirements, firstly the Go ones:
```sh
go mod download
```
And now the site ones:
```sh
cd site
bun i
```
## Developing locally
In order to develop the app locally you need to build the frontend and copy it to the assets folder in order for Go to embed it and host it. In order to build the frontend run:
```sh
cd site
bun run build
cd ..
```
Copy it to the assets folder:
```sh
rm -rf internal/assets/dist
cp -r site/dist internal/assets/dist
```
Finally either run the app with:
```sh
go run main.go
```
Or build it with:
```sh
go build
```
> [!WARNING]
> Make sure you have set the environment variables when running outside of docker else the app will fail.
## Developing in docker
My recommended development method is docker so I can test that both my image works and that the app responds correctly to traefik. In my setup I have set these two DNS records in my DNS server:
```
*.dev.local -> 127.0.0.1
dev.local -> 127.0.0.1
```
Then I can just make sure the domains are correct in the example docker compose file and do:
```sh
docker compose -f docker-compose.dev.yml up --build
```

View File

@@ -8,8 +8,8 @@
<img alt="License" src="https://img.shields.io/github/license/steveiliop56/tinyauth">
<img alt="Release" src="https://img.shields.io/github/v/release/steveiliop56/tinyauth">
<img alt="Commit activity" src="https://img.shields.io/github/commit-activity/w/steveiliop56/tinyauth">
<img alt="Actions Workflow Status" src="https://img.shields.io/github/actions/workflow/status/steveiliop56/tinyauth/release.yml">
<img alt="Issues" src="https://img.shields.io/github/issues/steveiliop56/tinyauth">
<img alt="Tinyauth CI" src="https://github.com/steveiliop56/tinyauth/actions/workflows/ci.yml/badge.svg">
</div>
<br />

View File

@@ -1,9 +0,0 @@
# Security Policy
## Supported Versions
Please always use the latest available Tinyauth version which can be found [here](https://github.com/steveiliop56/tinyauth/releases/latest). Older versions (especially major) may contain security issues which I cannot go back and fix.
## Reporting a Vulnerability
Due to the nature of this app, it needs to be secure. If you find any security issues in the OAuth or login flow of the app please contact me at <steve@doesmycode.work> and include a concise description of the issue. Please do not use the issues section for reporting major security issues.

View File

@@ -13,12 +13,10 @@ import (
)
var interactive bool
var username string
var password string
var docker bool
// i stands for input
var iUsername string
var iPassword string
var CreateCmd = &cobra.Command{
Use: "create",
Short: "Create a user",
@@ -32,13 +30,13 @@ var CreateCmd = &cobra.Command{
// Create huh form
form := huh.NewForm(
huh.NewGroup(
huh.NewInput().Title("Username").Value(&iUsername).Validate((func(s string) error {
huh.NewInput().Title("Username").Value(&username).Validate((func(s string) error {
if s == "" {
return errors.New("username cannot be empty")
}
return nil
})),
huh.NewInput().Title("Password").Value(&iPassword).Validate((func(s string) error {
huh.NewInput().Title("Password").Value(&password).Validate((func(s string) error {
if s == "" {
return errors.New("password cannot be empty")
}
@@ -59,21 +57,20 @@ var CreateCmd = &cobra.Command{
}
// Do we have username and password?
if iUsername == "" || iPassword == "" {
if username == "" || password == "" {
log.Error().Msg("Username and password cannot be empty")
}
log.Info().Str("username", iUsername).Str("password", iPassword).Bool("docker", docker).Msg("Creating user")
log.Info().Str("username", username).Str("password", password).Bool("docker", docker).Msg("Creating user")
// Hash password
password, passwordErr := bcrypt.GenerateFromPassword([]byte(iPassword), bcrypt.DefaultCost)
passwordByte, passwordErr := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if passwordErr != nil {
log.Fatal().Err(passwordErr).Msg("Failed to hash password")
}
// Convert password to string
passwordString := string(password)
passwordString := string(passwordByte)
// Escape $ for docker
if docker {
@@ -81,14 +78,14 @@ var CreateCmd = &cobra.Command{
}
// Log user created
log.Info().Str("user", fmt.Sprintf("%s:%s", iUsername, passwordString)).Msg("User created")
log.Info().Str("user", fmt.Sprintf("%s:%s", username, passwordString)).Msg("User created")
},
}
func init() {
// Flags
CreateCmd.Flags().BoolVarP(&interactive, "interactive", "i", false, "Create a user interactively")
CreateCmd.Flags().BoolVar(&interactive, "interactive", false, "Create a user interactively")
CreateCmd.Flags().BoolVar(&docker, "docker", false, "Format output for docker")
CreateCmd.Flags().StringVar(&iUsername, "username", "", "Username")
CreateCmd.Flags().StringVar(&iPassword, "password", "", "Password")
CreateCmd.Flags().StringVar(&username, "username", "", "Username")
CreateCmd.Flags().StringVar(&password, "password", "", "Password")
}

View File

@@ -12,12 +12,10 @@ import (
)
var interactive bool
var username string
var password string
var docker bool
// i stands for input
var iUsername string
var iPassword string
var iUser string
var user string
var VerifyCmd = &cobra.Command{
Use: "verify",
@@ -32,19 +30,19 @@ var VerifyCmd = &cobra.Command{
// Create huh form
form := huh.NewForm(
huh.NewGroup(
huh.NewInput().Title("User (username:hash)").Value(&iUser).Validate((func(s string) error {
huh.NewInput().Title("User (username:hash)").Value(&user).Validate((func(s string) error {
if s == "" {
return errors.New("user cannot be empty")
}
return nil
})),
huh.NewInput().Title("Username").Value(&iUsername).Validate((func(s string) error {
huh.NewInput().Title("Username").Value(&username).Validate((func(s string) error {
if s == "" {
return errors.New("username cannot be empty")
}
return nil
})),
huh.NewInput().Title("Password").Value(&iPassword).Validate((func(s string) error {
huh.NewInput().Title("Password").Value(&password).Validate((func(s string) error {
if s == "" {
return errors.New("password cannot be empty")
}
@@ -65,28 +63,28 @@ var VerifyCmd = &cobra.Command{
}
// Do we have username, password and user?
if iUsername == "" || iPassword == "" || iUser == "" {
if username == "" || password == "" || user == "" {
log.Fatal().Msg("Username, password and user cannot be empty")
}
log.Info().Str("user", iUser).Str("username", iUsername).Str("password", iPassword).Bool("docker", docker).Msg("Verifying user")
log.Info().Str("user", user).Str("username", username).Str("password", password).Bool("docker", docker).Msg("Verifying user")
// Split username and password hash
username, hash, ok := strings.Cut(iUser, ":")
// Split username and password
userSplit := strings.Split(user, ":")
if !ok {
if userSplit[1] == "" {
log.Fatal().Msg("User is not formatted correctly")
}
// Replace $$ with $ if formatted for docker
if docker {
hash = strings.ReplaceAll(hash, "$$", "$")
userSplit[1] = strings.ReplaceAll(userSplit[1], "$$", "$")
}
// Compare username and password
verifyErr := bcrypt.CompareHashAndPassword([]byte(hash), []byte(iPassword))
verifyErr := bcrypt.CompareHashAndPassword([]byte(userSplit[1]), []byte(password))
if verifyErr != nil || username != iUsername {
if verifyErr != nil || username != userSplit[0] {
log.Fatal().Msg("Username or password incorrect")
} else {
log.Info().Msg("Verification successful")
@@ -98,7 +96,7 @@ func init() {
// Flags
VerifyCmd.Flags().BoolVarP(&interactive, "interactive", "i", false, "Create a user interactively")
VerifyCmd.Flags().BoolVar(&docker, "docker", false, "Is the user formatted for docker?")
VerifyCmd.Flags().StringVar(&iUsername, "username", "", "Username")
VerifyCmd.Flags().StringVar(&iPassword, "password", "", "Password")
VerifyCmd.Flags().StringVar(&iUser, "user", "", "Hash (username:hash combination)")
VerifyCmd.Flags().StringVar(&username, "username", "", "Username")
VerifyCmd.Flags().StringVar(&password, "password", "", "Password")
VerifyCmd.Flags().StringVar(&user, "user", "", "Hash (username:hash combination)")
}

View File

@@ -7,6 +7,8 @@ services:
- 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
@@ -30,4 +32,3 @@ services:
traefik.enable: true
traefik.http.routers.tinyauth.rule: Host(`tinyauth.dev.local`)
traefik.http.services.tinyauth.loadbalancer.server.port: 3000
traefik.http.middlewares.tinyauth.forwardauth.address: http://tinyauth:3000/api/auth

View File

@@ -7,6 +7,8 @@ services:
- 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
@@ -28,4 +30,3 @@ services:
traefik.enable: true
traefik.http.routers.tinyauth.rule: Host(`tinyauth.example.com`)
traefik.http.services.tinyauth.loadbalancer.server.port: 3000
traefik.http.middlewares.tinyauth.forwardauth.address: http://tinyauth:3000/api/auth

View File

@@ -121,12 +121,7 @@ func (api *API) SetupRoutes() {
bindErr := c.BindUri(&proxy)
// Handle error
if bindErr != nil {
log.Error().Err(bindErr).Msg("Failed to bind URI")
c.JSON(400, gin.H{
"status": 400,
"message": "Bad Request",
})
if api.handleError(c, "Failed to bind URI", bindErr) {
return
}
@@ -135,9 +130,6 @@ func (api *API) SetupRoutes() {
// Get user context
userContext := api.Hooks.UseUserContext(c)
// Check if using basic auth
_, _, basicAuth := c.Request.BasicAuth()
// Get headers
uri := c.Request.Header.Get("X-Forwarded-Uri")
proto := c.Request.Header.Get("X-Forwarded-Proto")
@@ -152,8 +144,8 @@ func (api *API) SetupRoutes() {
// Check if there was an error
if appAllowedErr != nil {
// Return 501 if nginx is the proxy or if the request is using basic auth
if proxy.Proxy == "nginx" || basicAuth {
// Return 501 if nginx is the proxy or if the request is using an Authorization header
if proxy.Proxy == "nginx" || c.GetHeader("Authorization") != "" {
log.Error().Err(appAllowedErr).Msg("Failed to check if app is allowed")
c.JSON(501, gin.H{
"status": 501,
@@ -174,24 +166,36 @@ func (api *API) SetupRoutes() {
if !appAllowed {
log.Warn().Str("username", userContext.Username).Str("host", host).Msg("User not allowed")
// Return 401 if nginx is the proxy or if the request is using an Authorization header
if proxy.Proxy == "nginx" || basicAuth {
c.Header("WWW-Authenticate", "Basic realm=\"tinyauth\"")
c.JSON(401, gin.H{
"status": 401,
"message": "Unauthorized",
})
return
}
// Build query
queries, queryErr := query.Values(types.UnauthorizedQuery{
Username: userContext.Username,
Resource: strings.Split(host, ".")[0],
})
// Handle error (no need to check for nginx/headers since we are sure we are using caddy/traefik)
if api.handleError(c, "Failed to build query", queryErr) {
// Check if there was an error
if queryErr != nil {
// Return 501 if nginx is the proxy or if the request is using an Authorization header
if proxy.Proxy == "nginx" || c.GetHeader("Authorization") != "" {
log.Error().Err(queryErr).Msg("Failed to build query")
c.JSON(501, gin.H{
"status": 501,
"message": "Internal Server Error",
})
return
}
// Return the internal server error page
if api.handleError(c, "Failed to build query", queryErr) {
return
}
}
// Return 401 if nginx is the proxy or if the request is using an Authorization header
if proxy.Proxy == "nginx" || c.GetHeader("Authorization") != "" {
c.JSON(401, gin.H{
"status": 401,
"message": "Unauthorized",
})
return
}
@@ -216,8 +220,7 @@ func (api *API) SetupRoutes() {
log.Debug().Msg("Unauthorized")
// Return 401 if nginx is the proxy or if the request is using an Authorization header
if proxy.Proxy == "nginx" || basicAuth {
c.Header("WWW-Authenticate", "Basic realm=\"tinyauth\"")
if proxy.Proxy == "nginx" || c.GetHeader("Authorization") != "" {
c.JSON(401, gin.H{
"status": 401,
"message": "Unauthorized",
@@ -230,13 +233,13 @@ func (api *API) SetupRoutes() {
RedirectURI: fmt.Sprintf("%s://%s%s", proto, host, uri),
})
log.Debug().Interface("redirect_uri", fmt.Sprintf("%s://%s%s", proto, host, uri)).Msg("Redirecting to login")
// Handle error (no need to check for nginx/headers since we are sure we are using caddy/traefik)
if api.handleError(c, "Failed to build query", queryErr) {
return
}
log.Debug().Interface("redirect_uri", fmt.Sprintf("%s://%s%s", proto, host, uri)).Msg("Redirecting to login")
// Redirect to login
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/?%s", api.Config.AppURL, queries.Encode()))
})
@@ -335,7 +338,6 @@ func (api *API) SetupRoutes() {
// We are not logged in so return unauthorized
if !userContext.IsLoggedIn {
log.Debug().Msg("Unauthorized")
c.Header("WWW-Authenticate", "Basic realm=\"tinyauth\"")
c.JSON(200, gin.H{
"status": 200,
"message": "Unauthorized",

View File

@@ -1,199 +0,0 @@
package api_test
import (
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
"tinyauth/internal/api"
"tinyauth/internal/auth"
"tinyauth/internal/docker"
"tinyauth/internal/hooks"
"tinyauth/internal/providers"
"tinyauth/internal/types"
"github.com/magiconair/properties/assert"
)
// Simple API config for tests
var apiConfig = types.APIConfig{
Port: 8080,
Address: "0.0.0.0",
Secret: "super-secret-api-thing-for-tests", // It is 32 chars long
AppURL: "http://tinyauth.localhost",
CookieSecure: false,
CookieExpiry: 3600,
DisableContinue: false,
}
// Cookie
var cookie string
// User
var user = types.User{
Username: "user",
Password: "$2a$10$AvGHLTYv3xiRJ0xV9xs3XeVIlkGTygI9nqIamFYB5Xu.5.0UWF7B6", // pass
}
// We need all this to be able to test the API
func getAPI(t *testing.T) *api.API {
// Create docker service
docker := docker.NewDocker()
// Initialize docker
dockerErr := docker.Init()
// Check if there was an error
if dockerErr != nil {
t.Fatalf("Failed to initialize docker: %v", dockerErr)
}
// Create auth service
auth := auth.NewAuth(docker, types.Users{
{
Username: user.Username,
Password: user.Password,
},
}, nil, apiConfig.CookieExpiry)
// Create providers service
providers := providers.NewProviders(types.OAuthConfig{})
// Initialize providers
providers.Init()
// Create hooks service
hooks := hooks.NewHooks(auth, providers)
// Create API
api := api.NewAPI(apiConfig, hooks, auth, providers)
// Setup routes
api.Init()
api.SetupRoutes()
return api
}
// Test login (we will need this for the other tests)
func TestLogin(t *testing.T) {
t.Log("Testing login")
// Get API
api := getAPI(t)
// Create recorder
recorder := httptest.NewRecorder()
// Create request
user := types.LoginRequest{
Username: "user",
Password: "pass",
}
json, err := json.Marshal(user)
// Check if there was an error
if err != nil {
t.Fatalf("Error marshalling json: %v", err)
}
// Create request
req, err := http.NewRequest("POST", "/api/login", strings.NewReader(string(json)))
// Check if there was an error
if err != nil {
t.Fatalf("Error creating request: %v", err)
}
// Serve the request
api.Router.ServeHTTP(recorder, req)
// Assert
assert.Equal(t, recorder.Code, http.StatusOK)
// Get the cookie
cookie = recorder.Result().Cookies()[0].Value
// Check if the cookie is set
if cookie == "" {
t.Fatalf("Cookie not set")
}
}
// Test status
func TestStatus(t *testing.T) {
t.Log("Testing status")
// Get API
api := getAPI(t)
// Create recorder
recorder := httptest.NewRecorder()
// Create request
req, err := http.NewRequest("GET", "/api/status", nil)
// Check if there was an error
if err != nil {
t.Fatalf("Error creating request: %v", err)
}
// Set the cookie
req.AddCookie(&http.Cookie{
Name: "tinyauth",
Value: cookie,
})
// Serve the request
api.Router.ServeHTTP(recorder, req)
// Assert
assert.Equal(t, recorder.Code, http.StatusOK)
// Parse the body
body := recorder.Body.String()
if !strings.Contains(body, "user") {
t.Fatalf("Expected user in body")
}
}
// Test logout
func TestLogout(t *testing.T) {
t.Log("Testing logout")
// Get API
api := getAPI(t)
// Create recorder
recorder := httptest.NewRecorder()
// Create request
req, err := http.NewRequest("POST", "/api/logout", nil)
// Check if there was an error
if err != nil {
t.Fatalf("Error creating request: %v", err)
}
// Set the cookie
req.AddCookie(&http.Cookie{
Name: "tinyauth",
Value: cookie,
})
// Serve the request
api.Router.ServeHTTP(recorder, req)
// Assert
assert.Equal(t, recorder.Code, http.StatusOK)
// Check if the cookie is different (means go sessions flushed it)
if recorder.Result().Cookies()[0].Value == cookie {
t.Fatalf("Cookie not flushed")
}
}
// TODO: Testing for the oauth stuff

View File

@@ -211,18 +211,38 @@ func (auth *Auth) ResourceAllowed(context types.UserContext, host string) (bool,
return true, nil
}
func (auth *Auth) GetBasicAuth(c *gin.Context) *types.User {
func (auth *Auth) GetBasicAuth(c *gin.Context) types.User {
// Get the Authorization header
username, password, ok := c.Request.BasicAuth()
header := c.GetHeader("Authorization")
// If not ok, return an empty user
if !ok {
return nil
// If the header is empty, return an empty user
if header == "" {
return types.User{}
}
// Split the header
headerSplit := strings.Split(header, " ")
if len(headerSplit) != 2 {
return types.User{}
}
// Check if the header is Basic
if headerSplit[0] != "Basic" {
return types.User{}
}
// Split the credentials
credentials := strings.Split(headerSplit[1], ":")
// If the credentials are not in the correct format, return an empty user
if len(credentials) != 2 {
return types.User{}
}
// Return the user
return &types.User{
Username: username,
Password: password,
return types.User{
Username: credentials[0],
Password: credentials[1],
}
}

View File

@@ -27,7 +27,7 @@ func (hooks *Hooks) UseUserContext(c *gin.Context) types.UserContext {
basic := hooks.Auth.GetBasicAuth(c)
// Check if basic auth is set
if basic != nil {
if basic.Username != "" {
log.Debug().Msg("Got basic auth")
// Check if user exists and password is correct

View File

@@ -50,7 +50,7 @@ func ParseUsers(users string) (types.Users, error) {
return usersParsed, nil
}
// Root url parses parses a hostname and returns the root domain (e.g. sub1.sub2.domain.com -> sub2.domain.com)
// Root url parses parses a hostname and returns the root domain (e.g. sub1.sub2.domain.com -> domain.com)
func GetRootURL(urlSrc string) (string, error) {
// Make sure the url is valid
urlParsed, parseErr := url.Parse(urlSrc)
@@ -176,6 +176,11 @@ func GetUsers(conf string, file string) (types.Users, error) {
return ParseUsers(users)
}
// Check if any of the OAuth providers are configured based on the client id and secret
func OAuthConfigured(config types.Config) bool {
return (config.GithubClientId != "" && config.GithubClientSecret != "") || (config.GoogleClientId != "" && config.GoogleClientSecret != "") || (config.GenericClientId != "" && config.GenericClientSecret != "") || (config.TailscaleClientId != "" && config.TailscaleClientSecret != "")
}
// Parse the docker labels to the tinyauth labels struct
func GetTinyauthLabels(labels map[string]string) types.TinyauthLabels {
// Create a new tinyauth labels struct
@@ -202,8 +207,3 @@ func GetTinyauthLabels(labels map[string]string) types.TinyauthLabels {
// Return the tinyauth labels
return tinyauthLabels
}
// Check if any of the OAuth providers are configured based on the client id and secret
func OAuthConfigured(config types.Config) bool {
return (config.GithubClientId != "" && config.GithubClientSecret != "") || (config.GoogleClientId != "" && config.GoogleClientSecret != "") || (config.GenericClientId != "" && config.GenericClientSecret != "") || (config.TailscaleClientId != "" && config.TailscaleClientSecret != "")
}

View File

@@ -1,315 +0,0 @@
package utils_test
import (
"os"
"reflect"
"testing"
"tinyauth/internal/types"
"tinyauth/internal/utils"
)
// Test the parse users function
func TestParseUsers(t *testing.T) {
t.Log("Testing parse users with a valid string")
// Test the parse users function with a valid string
users := "user1:pass1,user2:pass2"
expected := types.Users{
{
Username: "user1",
Password: "pass1",
},
{
Username: "user2",
Password: "pass2",
},
}
result, err := utils.ParseUsers(users)
// Check if there was an error
if err != nil {
t.Fatalf("Error parsing users: %v", err)
}
// Check if the result is equal to the expected
if !reflect.DeepEqual(expected, result) {
t.Fatalf("Expected %v, got %v", expected, result)
}
t.Log("Testing parse users with an invalid string")
// Test the parse users function with an invalid string
users = "user1:pass1,user2"
_, err = utils.ParseUsers(users)
// There should be an error
if err == nil {
t.Fatalf("Expected error parsing users")
}
}
// Test the get root url function
func TestGetRootURL(t *testing.T) {
t.Log("Testing get root url with a valid url")
// Test the get root url function with a valid url
url := "https://sub1.sub2.domain.com:8080"
expected := "sub2.domain.com"
result, err := utils.GetRootURL(url)
// Check if there was an error
if err != nil {
t.Fatalf("Error getting root url: %v", err)
}
// Check if the result is equal to the expected
if expected != result {
t.Fatalf("Expected %v, got %v", expected, result)
}
}
// Test the read file function
func TestReadFile(t *testing.T) {
t.Log("Creating a test file")
// Create a test file
err := os.WriteFile("/tmp/test.txt", []byte("test"), 0644)
// Check if there was an error
if err != nil {
t.Fatalf("Error creating test file: %v", err)
}
// Test the read file function
t.Log("Testing read file with a valid file")
data, err := utils.ReadFile("/tmp/test.txt")
// Check if there was an error
if err != nil {
t.Fatalf("Error reading file: %v", err)
}
// Check if the data is equal to the expected
if data != "test" {
t.Fatalf("Expected test, got %v", data)
}
// Cleanup the test file
t.Log("Cleaning up test file")
err = os.Remove("/tmp/test.txt")
// Check if there was an error
if err != nil {
t.Fatalf("Error cleaning up test file: %v", err)
}
}
// Test the parse file to line function
func TestParseFileToLine(t *testing.T) {
t.Log("Testing parse file to line with a valid string")
// Test the parse file to line function with a valid string
content := "user1:pass1\nuser2:pass2"
expected := "user1:pass1,user2:pass2"
result := utils.ParseFileToLine(content)
// Check if the result is equal to the expected
if expected != result {
t.Fatalf("Expected %v, got %v", expected, result)
}
}
// Test the get secret function
func TestGetSecret(t *testing.T) {
t.Log("Testing get secret with an empty config and file")
// Test the get secret function with an empty config and file
conf := ""
file := "/tmp/test.txt"
expected := "test"
// Create file
err := os.WriteFile(file, []byte(expected), 0644)
// Check if there was an error
if err != nil {
t.Fatalf("Error creating test file: %v", err)
}
// Test
result := utils.GetSecret(conf, file)
// Check if the result is equal to the expected
if result != expected {
t.Fatalf("Expected %v, got %v", expected, result)
}
t.Log("Testing get secret with an empty file and a valid config")
// Test the get secret function with an empty file and a valid config
result = utils.GetSecret(expected, "")
// Check if the result is equal to the expected
if result != expected {
t.Fatalf("Expected %v, got %v", expected, result)
}
t.Log("Testing get secret with both a valid config and file")
// Test the get secret function with both a valid config and file
result = utils.GetSecret(expected, file)
// Check if the result is equal to the expected
if result != expected {
t.Fatalf("Expected %v, got %v", expected, result)
}
// Cleanup the test file
t.Log("Cleaning up test file")
err = os.Remove(file)
// Check if there was an error
if err != nil {
t.Fatalf("Error cleaning up test file: %v", err)
}
}
// Test the get users function
func TestGetUsers(t *testing.T) {
t.Log("Testing get users with a config and no file")
// Test the get users function with a config and no file
conf := "user1:pass1,user2:pass2"
file := ""
expected := types.Users{
{
Username: "user1",
Password: "pass1",
},
{
Username: "user2",
Password: "pass2",
},
}
result, err := utils.GetUsers(conf, file)
// Check if there was an error
if err != nil {
t.Fatalf("Error getting users: %v", err)
}
// Check if the result is equal to the expected
if !reflect.DeepEqual(expected, result) {
t.Fatalf("Expected %v, got %v", expected, result)
}
t.Log("Testing get users with a file and no config")
// Test the get users function with a file and no config
conf = ""
file = "/tmp/test.txt"
expected = types.Users{
{
Username: "user1",
Password: "pass1",
},
{
Username: "user2",
Password: "pass2",
},
}
// Create file
err = os.WriteFile(file, []byte("user1:pass1\nuser2:pass2"), 0644)
// Check if there was an error
if err != nil {
t.Fatalf("Error creating test file: %v", err)
}
// Test
result, err = utils.GetUsers(conf, file)
// Check if there was an error
if err != nil {
t.Fatalf("Error getting users: %v", err)
}
// Check if the result is equal to the expected
if !reflect.DeepEqual(expected, result) {
t.Fatalf("Expected %v, got %v", expected, result)
}
// Test the get users function with both a config and file
t.Log("Testing get users with both a config and file")
conf = "user3:pass3"
expected = types.Users{
{
Username: "user3",
Password: "pass3",
},
{
Username: "user1",
Password: "pass1",
},
{
Username: "user2",
Password: "pass2",
},
}
result, err = utils.GetUsers(conf, file)
// Check if there was an error
if err != nil {
t.Fatalf("Error getting users: %v", err)
}
// Check if the result is equal to the expected
if !reflect.DeepEqual(expected, result) {
t.Fatalf("Expected %v, got %v", expected, result)
}
// Cleanup the test file
t.Log("Cleaning up test file")
err = os.Remove(file)
// Check if there was an error
if err != nil {
t.Fatalf("Error cleaning up test file: %v", err)
}
}
// Test the tinyauth labels function
func TestGetTinyauthLabels(t *testing.T) {
t.Log("Testing get tinyauth labels with a valid map")
// Test the get tinyauth labels function with a valid map
labels := map[string]string{
"tinyauth.users": "user1,user2",
"tinyauth.oauth.whitelist": "user1,user2",
"random": "random",
}
expected := types.TinyauthLabels{
Users: []string{"user1", "user2"},
OAuthWhitelist: []string{"user1", "user2"},
}
result := utils.GetTinyauthLabels(labels)
// Check if the result is equal to the expected
if !reflect.DeepEqual(expected, result) {
t.Fatalf("Expected %v, got %v", expected, result)
}
}

View File

@@ -8,7 +8,7 @@ import { ReactNode } from "react";
export const ContinuePage = () => {
const queryString = window.location.search;
const params = new URLSearchParams(queryString);
const redirectUri = params.get("redirect_uri") ?? "";
const redirectUri = params.get("redirect_uri");
const { isLoggedIn, disableContinue } = useUserContext();
@@ -16,7 +16,7 @@ export const ContinuePage = () => {
return <Navigate to={`/login?redirect_uri=${redirectUri}`} />;
}
if (redirectUri === "null" || redirectUri === "") {
if (redirectUri === "null") {
return <Navigate to="/" />;
}
@@ -27,29 +27,15 @@ export const ContinuePage = () => {
color: "blue",
});
setTimeout(() => {
window.location.href = redirectUri;
window.location.href = redirectUri!;
}, 500);
};
const urlParsed = URL.parse(redirectUri);
if (urlParsed === null) {
return (
<ContinuePageLayout>
<Text size="xl" fw={700}>
Invalid Redirect
</Text>
<Text>
The redirect URL is invalid, please contact the app owner to fix the
issue.
</Text>
</ContinuePageLayout>
);
}
const urlParsed = URL.parse(redirectUri!);
if (
window.location.protocol === "https:" &&
urlParsed.protocol === "http:"
urlParsed!.protocol === "http:"
) {
return (
<ContinuePageLayout>
@@ -68,7 +54,7 @@ export const ContinuePage = () => {
}
if (disableContinue) {
window.location.href = redirectUri;
window.location.href = redirectUri!;
return (
<ContinuePageLayout>
<Text size="xl" fw={700}>

View File

@@ -24,10 +24,9 @@ import { TailscaleIcon } from "../icons/tailscale";
export const LoginPage = () => {
const queryString = window.location.search;
const params = new URLSearchParams(queryString);
const redirectUri = params.get("redirect_uri") ?? "";
const redirectUri = params.get("redirect_uri");
const { isLoggedIn, configuredProviders } = useUserContext();
const oauthProviders = configuredProviders.filter(
(value) => value !== "username",
);
@@ -70,7 +69,7 @@ export const LoginPage = () => {
color: "green",
});
setTimeout(() => {
if (redirectUri === "null" || redirectUri === "") {
if (redirectUri === "null") {
window.location.replace("/");
} else {
window.location.replace(`/continue?redirect_uri=${redirectUri}`);

View File

@@ -5,10 +5,10 @@ import { Navigate } from "react-router";
export const UnauthorizedPage = () => {
const queryString = window.location.search;
const params = new URLSearchParams(queryString);
const username = params.get("username") ?? "";
const resource = params.get("resource") ?? "";
const username = params.get("username");
const resource = params.get("resource");
if (username === "null" || username === "") {
if (username === "null") {
return <Navigate to="/" />;
}
@@ -20,7 +20,7 @@ export const UnauthorizedPage = () => {
</Text>
<Text>
The user with username <Code>{username}</Code> is not authorized to{" "}
{resource !== "null" && resource !== "" ? (
{resource !== "null" ? (
<span>
access the <Code>{resource}</Code> resource.
</span>