mirror of
https://github.com/steveiliop56/tinyauth.git
synced 2026-02-22 00:42:03 +00:00
Compare commits
6 Commits
7ca79d4532
...
ce25f9561f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ce25f9561f | ||
|
|
f24595b24e | ||
|
|
285edba88c | ||
|
|
51d95fa455 | ||
|
|
fd16f91011 | ||
|
|
fb671139cd |
4
Makefile
4
Makefile
@@ -71,6 +71,10 @@ develop-infisical:
|
||||
prod:
|
||||
docker compose -f $(PROD_COMPOSE) up --force-recreate --pull=always --remove-orphans
|
||||
|
||||
# Production - Infisical
|
||||
prod-infisical:
|
||||
infisical run --env=dev -- docker compose -f $(PROD_COMPOSE) up --force-recreate --pull=always --remove-orphans
|
||||
|
||||
# SQL
|
||||
.PHONY: sql
|
||||
sql:
|
||||
|
||||
@@ -28,7 +28,20 @@ func healthcheckCmd() *cli.Command {
|
||||
Run: func(args []string) error {
|
||||
tlog.NewSimpleLogger().Init()
|
||||
|
||||
appUrl := os.Getenv("TINYAUTH_APPURL")
|
||||
appUrl := "http://127.0.0.1:3000"
|
||||
|
||||
appUrlEnv := os.Getenv("TINYAUTH_APPURL")
|
||||
srvAddr := os.Getenv("TINYAUTH_SERVER_ADDRESS")
|
||||
srvPort := os.Getenv("TINYAUTH_SERVER_PORT")
|
||||
|
||||
if appUrlEnv != "" {
|
||||
appUrl = appUrlEnv
|
||||
}
|
||||
|
||||
// Local-direct connection is preferred over the public app URL
|
||||
if srvAddr != "" && srvPort != "" {
|
||||
appUrl = fmt.Sprintf("http://%s:%s", srvAddr, srvPort)
|
||||
}
|
||||
|
||||
if len(args) > 0 {
|
||||
appUrl = args[0]
|
||||
|
||||
@@ -160,7 +160,7 @@ code {
|
||||
}
|
||||
|
||||
pre {
|
||||
@apply bg-accent border border-border rounded-md p-2;
|
||||
@apply bg-accent border border-border rounded-md p-2 whitespace-break-spaces;
|
||||
}
|
||||
|
||||
.lead {
|
||||
|
||||
2
go.mod
2
go.mod
@@ -11,6 +11,7 @@ require (
|
||||
github.com/charmbracelet/huh v0.8.0
|
||||
github.com/docker/docker v28.5.2+incompatible
|
||||
github.com/gin-gonic/gin v1.11.0
|
||||
github.com/go-jose/go-jose/v4 v4.1.3
|
||||
github.com/go-ldap/ldap/v3 v3.4.12
|
||||
github.com/golang-migrate/migrate/v4 v4.19.1
|
||||
github.com/google/go-querystring v1.2.0
|
||||
@@ -61,7 +62,6 @@ require (
|
||||
github.com/gabriel-vasile/mimetype v1.4.10 // indirect
|
||||
github.com/gin-contrib/sse v1.1.0 // indirect
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.1.3 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
type BootstrapApp struct {
|
||||
config config.Config
|
||||
context struct {
|
||||
appUrl string
|
||||
uuid string
|
||||
cookieDomain string
|
||||
sessionCookieName string
|
||||
@@ -42,10 +43,20 @@ func NewBootstrapApp(config config.Config) *BootstrapApp {
|
||||
}
|
||||
|
||||
func (app *BootstrapApp) Setup() error {
|
||||
// get app url
|
||||
appUrl, err := url.Parse(app.config.AppURL)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
app.context.appUrl = appUrl.Scheme + "://" + appUrl.Host
|
||||
|
||||
// validate session config
|
||||
if app.config.Auth.SessionMaxLifetime != 0 && app.config.Auth.SessionMaxLifetime < app.config.Auth.SessionExpiry {
|
||||
return fmt.Errorf("session max lifetime cannot be less than session expiry")
|
||||
}
|
||||
|
||||
// Parse users
|
||||
users, err := utils.GetUsers(app.config.Auth.Users, app.config.Auth.UsersFile)
|
||||
|
||||
@@ -62,16 +73,12 @@ func (app *BootstrapApp) Setup() error {
|
||||
secret := utils.GetSecret(provider.ClientSecret, provider.ClientSecretFile)
|
||||
provider.ClientSecret = secret
|
||||
provider.ClientSecretFile = ""
|
||||
app.context.oauthProviders[name] = provider
|
||||
}
|
||||
|
||||
for id := range config.OverrideProviders {
|
||||
if provider, exists := app.context.oauthProviders[id]; exists {
|
||||
if provider.RedirectURL == "" {
|
||||
provider.RedirectURL = app.config.AppURL + "/api/oauth/callback/" + id
|
||||
app.context.oauthProviders[id] = provider
|
||||
}
|
||||
if provider.RedirectURL == "" {
|
||||
provider.RedirectURL = app.context.appUrl + "/api/oauth/callback/" + name
|
||||
}
|
||||
|
||||
app.context.oauthProviders[name] = provider
|
||||
}
|
||||
|
||||
for id, provider := range app.context.oauthProviders {
|
||||
@@ -92,7 +99,7 @@ func (app *BootstrapApp) Setup() error {
|
||||
}
|
||||
|
||||
// Get cookie domain
|
||||
cookieDomain, err := utils.GetCookieDomain(app.config.AppURL)
|
||||
cookieDomain, err := utils.GetCookieDomain(app.context.appUrl)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -101,7 +108,6 @@ func (app *BootstrapApp) Setup() error {
|
||||
app.context.cookieDomain = cookieDomain
|
||||
|
||||
// Cookie names
|
||||
appUrl, _ := url.Parse(app.config.AppURL) // Already validated
|
||||
app.context.uuid = utils.GenerateUUID(appUrl.Hostname())
|
||||
cookieId := strings.Split(app.context.uuid, "-")[0]
|
||||
app.context.sessionCookieName = fmt.Sprintf("%s-%s", config.SessionCookieName, cookieId)
|
||||
|
||||
@@ -3,6 +3,7 @@ package bootstrap
|
||||
import (
|
||||
"github.com/steveiliop56/tinyauth/internal/repository"
|
||||
"github.com/steveiliop56/tinyauth/internal/service"
|
||||
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
|
||||
)
|
||||
|
||||
type Services struct {
|
||||
@@ -31,7 +32,8 @@ func (app *BootstrapApp) initServices(queries *repository.Queries) (Services, er
|
||||
err := ldapService.Init()
|
||||
|
||||
if err != nil {
|
||||
return Services{}, err
|
||||
tlog.App.Warn().Err(err).Msg("Failed to setup LDAP service, starting without it")
|
||||
ldapService.Unconfigure()
|
||||
}
|
||||
|
||||
services.ldapService = ldapService
|
||||
|
||||
@@ -97,6 +97,11 @@ func (controller *OIDCController) GetClientInfo(c *gin.Context) {
|
||||
}
|
||||
|
||||
func (controller *OIDCController) Authorize(c *gin.Context) {
|
||||
if !controller.oidc.IsConfigured() {
|
||||
controller.authorizeError(c, errors.New("err_oidc_not_configured"), "OIDC not configured", "This instance is not configured for OIDC", "", "", "")
|
||||
return
|
||||
}
|
||||
|
||||
userContext, err := utils.GetContext(c)
|
||||
|
||||
if err != nil {
|
||||
@@ -177,6 +182,14 @@ func (controller *OIDCController) Authorize(c *gin.Context) {
|
||||
}
|
||||
|
||||
func (controller *OIDCController) Token(c *gin.Context) {
|
||||
if !controller.oidc.IsConfigured() {
|
||||
tlog.App.Warn().Msg("OIDC not configured")
|
||||
c.JSON(404, gin.H{
|
||||
"error": "not_found",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
var req TokenRequest
|
||||
|
||||
err := c.Bind(&req)
|
||||
@@ -306,6 +319,14 @@ func (controller *OIDCController) Token(c *gin.Context) {
|
||||
}
|
||||
|
||||
func (controller *OIDCController) Userinfo(c *gin.Context) {
|
||||
if !controller.oidc.IsConfigured() {
|
||||
tlog.App.Warn().Msg("OIDC not configured")
|
||||
c.JSON(404, gin.H{
|
||||
"error": "not_found",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
authorization := c.GetHeader("Authorization")
|
||||
|
||||
tokenType, token, ok := strings.Cut(authorization, " ")
|
||||
|
||||
@@ -2,7 +2,6 @@ package controller
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/steveiliop56/tinyauth/internal/repository"
|
||||
@@ -114,8 +113,8 @@ func (controller *UserController) loginHandler(c *gin.Context) {
|
||||
|
||||
err := controller.auth.CreateSessionCookie(c, &repository.Session{
|
||||
Username: user.Username,
|
||||
Name: utils.Capitalize(req.Username),
|
||||
Email: fmt.Sprintf("%s@%s", strings.ToLower(req.Username), controller.config.CookieDomain),
|
||||
Name: utils.Capitalize(user.Username),
|
||||
Email: utils.CompileUserEmail(user.Username, controller.config.CookieDomain),
|
||||
Provider: "local",
|
||||
TotpPending: true,
|
||||
})
|
||||
@@ -141,7 +140,7 @@ func (controller *UserController) loginHandler(c *gin.Context) {
|
||||
sessionCookie := repository.Session{
|
||||
Username: req.Username,
|
||||
Name: utils.Capitalize(req.Username),
|
||||
Email: fmt.Sprintf("%s@%s", strings.ToLower(req.Username), controller.config.CookieDomain),
|
||||
Email: utils.CompileUserEmail(req.Username, controller.config.CookieDomain),
|
||||
Provider: "local",
|
||||
}
|
||||
|
||||
@@ -255,7 +254,7 @@ func (controller *UserController) totpHandler(c *gin.Context) {
|
||||
sessionCookie := repository.Session{
|
||||
Username: user.Username,
|
||||
Name: utils.Capitalize(user.Username),
|
||||
Email: fmt.Sprintf("%s@%s", strings.ToLower(user.Username), controller.config.CookieDomain),
|
||||
Email: utils.CompileUserEmail(user.Username, controller.config.CookieDomain),
|
||||
Provider: "local",
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -186,7 +185,7 @@ func (m *ContextMiddleware) Middleware() gin.HandlerFunc {
|
||||
c.Set("context", &config.UserContext{
|
||||
Username: user.Username,
|
||||
Name: utils.Capitalize(user.Username),
|
||||
Email: fmt.Sprintf("%s@%s", strings.ToLower(user.Username), m.config.CookieDomain),
|
||||
Email: utils.CompileUserEmail(user.Username, m.config.CookieDomain),
|
||||
Provider: "local",
|
||||
IsLoggedIn: true,
|
||||
TotpEnabled: user.TotpSecret != "",
|
||||
@@ -208,7 +207,7 @@ func (m *ContextMiddleware) Middleware() gin.HandlerFunc {
|
||||
c.Set("context", &config.UserContext{
|
||||
Username: basic.Username,
|
||||
Name: utils.Capitalize(basic.Username),
|
||||
Email: fmt.Sprintf("%s@%s", strings.ToLower(basic.Username), m.config.CookieDomain),
|
||||
Email: utils.CompileUserEmail(basic.Username, m.config.CookieDomain),
|
||||
Provider: "ldap",
|
||||
IsLoggedIn: true,
|
||||
LdapGroups: strings.Join(ldapUser.Groups, ","),
|
||||
|
||||
@@ -24,10 +24,11 @@ type LdapServiceConfig struct {
|
||||
}
|
||||
|
||||
type LdapService struct {
|
||||
config LdapServiceConfig
|
||||
conn *ldapgo.Conn
|
||||
mutex sync.RWMutex
|
||||
cert *tls.Certificate
|
||||
config LdapServiceConfig
|
||||
conn *ldapgo.Conn
|
||||
mutex sync.RWMutex
|
||||
cert *tls.Certificate
|
||||
isConfigured bool
|
||||
}
|
||||
|
||||
func NewLdapService(config LdapServiceConfig) *LdapService {
|
||||
@@ -36,16 +37,33 @@ func NewLdapService(config LdapServiceConfig) *LdapService {
|
||||
}
|
||||
}
|
||||
|
||||
// If you have an ldap address then you must need ldap
|
||||
func (ldap *LdapService) IsConfigured() bool {
|
||||
return ldap.config.Address != ""
|
||||
return ldap.isConfigured
|
||||
}
|
||||
|
||||
func (ldap *LdapService) Unconfigure() error {
|
||||
if !ldap.isConfigured {
|
||||
return nil
|
||||
}
|
||||
|
||||
if ldap.conn != nil {
|
||||
if err := ldap.conn.Close(); err != nil {
|
||||
return fmt.Errorf("failed to close LDAP connection: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
ldap.isConfigured = false
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ldap *LdapService) Init() error {
|
||||
if !ldap.IsConfigured() {
|
||||
if ldap.config.Address == "" {
|
||||
ldap.isConfigured = false
|
||||
return nil
|
||||
}
|
||||
|
||||
ldap.isConfigured = true
|
||||
|
||||
// Check whether authentication with client certificate is possible
|
||||
if ldap.config.AuthCert != "" && ldap.config.AuthKey != "" {
|
||||
cert, err := tls.LoadX509KeyPair(ldap.config.AuthCert, ldap.config.AuthKey)
|
||||
|
||||
@@ -83,12 +83,13 @@ type OIDCServiceConfig struct {
|
||||
}
|
||||
|
||||
type OIDCService struct {
|
||||
config OIDCServiceConfig
|
||||
queries *repository.Queries
|
||||
clients map[string]config.OIDCClientConfig
|
||||
privateKey *rsa.PrivateKey
|
||||
publicKey crypto.PublicKey
|
||||
issuer string
|
||||
config OIDCServiceConfig
|
||||
queries *repository.Queries
|
||||
clients map[string]config.OIDCClientConfig
|
||||
privateKey *rsa.PrivateKey
|
||||
publicKey crypto.PublicKey
|
||||
issuer string
|
||||
isConfigured bool
|
||||
}
|
||||
|
||||
func NewOIDCService(config OIDCServiceConfig, queries *repository.Queries) *OIDCService {
|
||||
@@ -98,9 +99,19 @@ func NewOIDCService(config OIDCServiceConfig, queries *repository.Queries) *OIDC
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: A cleanup routine is needed to clean up expired tokens/code/userinfo
|
||||
func (service *OIDCService) IsConfigured() bool {
|
||||
return service.isConfigured
|
||||
}
|
||||
|
||||
func (service *OIDCService) Init() error {
|
||||
// If not configured, skip init
|
||||
if len(service.config.Clients) == 0 {
|
||||
service.isConfigured = false
|
||||
return nil
|
||||
}
|
||||
|
||||
service.isConfigured = true
|
||||
|
||||
// Ensure issuer is https
|
||||
uissuer, err := url.Parse(service.config.Issuer)
|
||||
|
||||
@@ -207,6 +218,7 @@ func (service *OIDCService) Init() error {
|
||||
}
|
||||
client.ClientSecretFile = ""
|
||||
service.clients[id] = client
|
||||
tlog.App.Info().Str("id", client.ID).Msg("Registered OIDC client")
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -49,3 +49,11 @@ func TestCoalesceToString(t *testing.T) {
|
||||
// Test with nil input
|
||||
assert.Equal(t, "", utils.CoalesceToString(nil))
|
||||
}
|
||||
|
||||
func TestCompileUserEmail(t *testing.T) {
|
||||
// Test with valid email
|
||||
assert.Equal(t, "user@example.com", utils.CompileUserEmail("user@example.com", "example.com"))
|
||||
|
||||
// Test with invalid email
|
||||
assert.Equal(t, "user@example.com", utils.CompileUserEmail("user", "example.com"))
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ package utils
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/mail"
|
||||
"strings"
|
||||
|
||||
"github.com/steveiliop56/tinyauth/internal/config"
|
||||
@@ -90,3 +92,13 @@ func ParseUser(userStr string) (config.User, error) {
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func CompileUserEmail(username string, domain string) string {
|
||||
_, err := mail.ParseAddress(username)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Sprintf("%s@%s", strings.ToLower(username), domain)
|
||||
}
|
||||
|
||||
return username
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user