Compare commits

..

23 Commits

Author SHA1 Message Date
Stavros
2385599c80 fix: omit port from cookie domain configuration 2025-01-29 17:55:21 +02:00
Stavros
6f184856f1 chore: bump version 2025-01-28 19:10:36 +02:00
Stavros
e2e3b3bdc6 refactor: use window.location.href for redirects 2025-01-28 19:08:00 +02:00
Stavros
3efcb26db1 refactor: remove sensitive info logging even in debug mode 2025-01-28 17:36:06 +02:00
Stavros
c54267f50d fix: parse users correctly 2025-01-26 22:40:55 +02:00
Stavros
4de12ce5c1 fix: no need to log that the provider is empty 2025-01-26 21:36:41 +02:00
Stavros
0cf0aafc14 fix: configure secrets before config validation 2025-01-26 21:13:26 +02:00
Stavros
80ea43184c chore: update readme 2025-01-26 20:52:35 +02:00
Stavros
3c4dffd479 chore: bump version 2025-01-26 20:52:06 +02:00
Stavros
f19f40f9fc feat: add secret file 2025-01-26 20:47:08 +02:00
Stavros
a243f22ac8 refactor: users are not a requirement when using oauth 2025-01-26 20:45:34 +02:00
Stavros
08d382c981 feat: add debug log level 2025-01-26 20:23:09 +02:00
Stavros
94f7debb10 feat: secrets file 2025-01-26 19:51:58 +02:00
Stavros
3b50d9303b refactor: use cookie store correctly 2025-01-26 19:51:58 +02:00
Stavros
d67133aca7 fix: get correct username from query params 2025-01-26 19:51:58 +02:00
Stavros
989ea8f229 refactor: rename email back to username 2025-01-26 19:51:58 +02:00
Stavros
708006decf refactor: move disable continue screen logic back to the continue screen 2025-01-26 19:51:14 +02:00
Stavros
682a918812 refactor: don't store oauth token in cookie 2025-01-26 11:05:11 +02:00
Stavros
389248cfe1 refactor: change cli about text 2025-01-25 21:11:56 +02:00
Stavros
81d25061df refactor: move disable continue logic in login screen 2025-01-25 21:11:09 +02:00
Stavros
f59697955d chore: update readme 2025-01-25 20:47:26 +02:00
Stavros
47d8f1e5aa chore: update utility commands 2025-01-25 20:36:04 +02:00
Stavros
e8d2e059a9 fix: pass cookie expiry to api config 2025-01-25 20:00:07 +02:00
23 changed files with 462 additions and 265 deletions

6
.gitignore vendored
View File

@@ -8,4 +8,8 @@ tinyauth
docker-compose.test.yml docker-compose.test.yml
# users file # users file
users.txt users.txt
# secret test file
secret.txt
secret_oauth.txt

View File

@@ -14,7 +14,7 @@
<br /> <br />
Tinyauth is a simple authentication middleware that adds simple email/password login to all of your docker apps. It is made for traefik but it can be extended to work with all reverse proxies like caddy and nginx. Tinyauth is a simple authentication middleware that adds simple username/password login or OAuth with Google, Github and any generic OAuth provider to all of your docker apps. It is made for traefik but it can be extended to work with all reverse proxies like caddy and nginx.
> [!WARNING] > [!WARNING]
> Tinyauth is in active development and configuration may change often. Please make sure to carefully read the release notes before updating. > Tinyauth is in active development and configuration may change often. Please make sure to carefully read the release notes before updating.

View File

@@ -1,8 +1,12 @@
package cmd package cmd
import ( import (
"os"
"strings"
"time"
cmd "tinyauth/cmd/user" cmd "tinyauth/cmd/user"
"tinyauth/internal/api" "tinyauth/internal/api"
"tinyauth/internal/assets"
"tinyauth/internal/auth" "tinyauth/internal/auth"
"tinyauth/internal/hooks" "tinyauth/internal/hooks"
"tinyauth/internal/providers" "tinyauth/internal/providers"
@@ -10,6 +14,7 @@ import (
"tinyauth/internal/utils" "tinyauth/internal/utils"
"github.com/go-playground/validator/v10" "github.com/go-playground/validator/v10"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/viper" "github.com/spf13/viper"
@@ -17,47 +22,43 @@ import (
var rootCmd = &cobra.Command{ var rootCmd = &cobra.Command{
Use: "tinyauth", Use: "tinyauth",
Short: "An extremely simple traefik forward auth proxy.", Short: "The simplest way to protect your apps with a login screen.",
Long: `Tinyauth is an extremely simple traefik forward-auth login screen that makes securing your apps easy.`, Long: `Tinyauth is a simple authentication middleware that adds simple username/password login or OAuth with Google, Github and any generic OAuth provider to all of your docker apps.`,
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
// Logger
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr, TimeFormat: time.RFC3339}).With().Timestamp().Logger().Level(zerolog.FatalLevel)
// Get config // Get config
log.Info().Msg("Parsing config")
var config types.Config var config types.Config
parseErr := viper.Unmarshal(&config) parseErr := viper.Unmarshal(&config)
HandleError(parseErr, "Failed to parse config") HandleError(parseErr, "Failed to parse config")
// Secrets
config.Secret = utils.GetSecret(config.Secret, config.SecretFile)
config.GithubClientSecret = utils.GetSecret(config.GithubClientSecret, config.GithubClientSecretFile)
config.GoogleClientSecret = utils.GetSecret(config.GoogleClientSecret, config.GoogleClientSecretFile)
config.GenericClientSecret = utils.GetSecret(config.GenericClientSecret, config.GenericClientSecretFile)
// Validate config // Validate config
log.Info().Msg("Validating config")
validator := validator.New() validator := validator.New()
validateErr := validator.Struct(config) validateErr := validator.Struct(config)
HandleError(validateErr, "Invalid config") HandleError(validateErr, "Failed to validate config")
// Parse users // Logger
log.Logger = log.Level(zerolog.Level(config.LogLevel))
log.Info().Str("version", assets.Version).Msg("Starting tinyauth")
// Users
log.Info().Msg("Parsing users") log.Info().Msg("Parsing users")
users, usersErr := utils.GetUsers(config.Users, config.UsersFile)
if config.UsersFile == "" && config.Users == "" { if (len(users) == 0 || usersErr != nil) && !utils.OAuthConfigured(config) {
log.Fatal().Msg("No users provided") log.Fatal().Err(usersErr).Msg("Failed to parse users")
} }
usersString := 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 := utils.ParseFileToLine(usersFromFile)
if usersString != "" {
usersString = usersString + "," + usersFromFileParsed
} else {
usersString = usersFromFileParsed
}
}
users, parseErr := utils.ParseUsers(usersString)
HandleError(parseErr, "Failed to parse users")
// Create oauth whitelist // Create oauth whitelist
oauthWhitelist := utils.ParseCommaString(config.OAuthWhitelist) oauthWhitelist := strings.Split(config.OAuthWhitelist, ",")
log.Debug().Msg("Parsed OAuth whitelist")
// Create OAuth config // Create OAuth config
oauthConfig := types.OAuthConfig{ oauthConfig := types.OAuthConfig{
@@ -67,13 +68,15 @@ var rootCmd = &cobra.Command{
GoogleClientSecret: config.GoogleClientSecret, GoogleClientSecret: config.GoogleClientSecret,
GenericClientId: config.GenericClientId, GenericClientId: config.GenericClientId,
GenericClientSecret: config.GenericClientSecret, GenericClientSecret: config.GenericClientSecret,
GenericScopes: utils.ParseCommaString(config.GenericScopes), GenericScopes: strings.Split(config.GenericScopes, ","),
GenericAuthURL: config.GenericAuthURL, GenericAuthURL: config.GenericAuthURL,
GenericTokenURL: config.GenericTokenURL, GenericTokenURL: config.GenericTokenURL,
GenericUserURL: config.GenericUserURL, GenericUserURL: config.GenericUserURL,
AppURL: config.AppURL, AppURL: config.AppURL,
} }
log.Debug().Msg("Parsed OAuth config")
// Create auth service // Create auth service
auth := auth.NewAuth(users, oauthWhitelist) auth := auth.NewAuth(users, oauthWhitelist)
@@ -94,6 +97,7 @@ var rootCmd = &cobra.Command{
AppURL: config.AppURL, AppURL: config.AppURL,
CookieSecure: config.CookieSecure, CookieSecure: config.CookieSecure,
DisableContinue: config.DisableContinue, DisableContinue: config.DisableContinue,
CookieExpiry: config.CookieExpiry,
}, hooks, auth, providers) }, hooks, auth, providers)
// Setup routes // Setup routes
@@ -124,16 +128,20 @@ func init() {
rootCmd.Flags().Int("port", 3000, "Port to run the server on.") rootCmd.Flags().Int("port", 3000, "Port to run the server on.")
rootCmd.Flags().String("address", "0.0.0.0", "Address to bind the server to.") 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("secret", "", "Secret to use for the cookie.")
rootCmd.Flags().String("secret-file", "", "Path to a file containing the secret.")
rootCmd.Flags().String("app-url", "", "The tinyauth URL.") 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", "", "Comma separated list of users in the format username:hash.")
rootCmd.Flags().String("users-file", "", "Path to a file containing users in the format username:bcrypt-hashed-password.") rootCmd.Flags().String("users-file", "", "Path to a file containing users in the format username:hash.")
rootCmd.Flags().Bool("cookie-secure", false, "Send cookie over secure connection only.") rootCmd.Flags().Bool("cookie-secure", false, "Send cookie over secure connection only.")
rootCmd.Flags().String("github-client-id", "", "Github OAuth client ID.") rootCmd.Flags().String("github-client-id", "", "Github OAuth client ID.")
rootCmd.Flags().String("github-client-secret", "", "Github OAuth client secret.") rootCmd.Flags().String("github-client-secret", "", "Github OAuth client secret.")
rootCmd.Flags().String("github-client-secret-file", "", "Github OAuth client secret file.")
rootCmd.Flags().String("google-client-id", "", "Google OAuth client ID.") rootCmd.Flags().String("google-client-id", "", "Google OAuth client ID.")
rootCmd.Flags().String("google-client-secret", "", "Google OAuth client secret.") rootCmd.Flags().String("google-client-secret", "", "Google OAuth client secret.")
rootCmd.Flags().String("google-client-secret-file", "", "Google OAuth client secret file.")
rootCmd.Flags().String("generic-client-id", "", "Generic OAuth client ID.") rootCmd.Flags().String("generic-client-id", "", "Generic OAuth client ID.")
rootCmd.Flags().String("generic-client-secret", "", "Generic OAuth client secret.") rootCmd.Flags().String("generic-client-secret", "", "Generic OAuth client secret.")
rootCmd.Flags().String("generic-client-secret-file", "", "Generic OAuth client secret file.")
rootCmd.Flags().String("generic-scopes", "", "Generic OAuth scopes.") rootCmd.Flags().String("generic-scopes", "", "Generic OAuth scopes.")
rootCmd.Flags().String("generic-auth-url", "", "Generic OAuth auth URL.") rootCmd.Flags().String("generic-auth-url", "", "Generic OAuth auth URL.")
rootCmd.Flags().String("generic-token-url", "", "Generic OAuth token URL.") rootCmd.Flags().String("generic-token-url", "", "Generic OAuth token URL.")
@@ -141,25 +149,31 @@ func init() {
rootCmd.Flags().Bool("disable-continue", false, "Disable continue screen and redirect to app directly.") rootCmd.Flags().Bool("disable-continue", false, "Disable continue screen and redirect to app directly.")
rootCmd.Flags().String("oauth-whitelist", "", "Comma separated list of email addresses to whitelist when using OAuth.") rootCmd.Flags().String("oauth-whitelist", "", "Comma separated list of email addresses to whitelist when using OAuth.")
rootCmd.Flags().Int("cookie-expiry", 86400, "Cookie expiration time in seconds.") rootCmd.Flags().Int("cookie-expiry", 86400, "Cookie expiration time in seconds.")
rootCmd.Flags().Int("log-level", 1, "Log level.")
viper.BindEnv("port", "PORT") viper.BindEnv("port", "PORT")
viper.BindEnv("address", "ADDRESS") viper.BindEnv("address", "ADDRESS")
viper.BindEnv("secret", "SECRET") viper.BindEnv("secret", "SECRET")
viper.BindEnv("secret-file", "SECRET_FILE")
viper.BindEnv("app-url", "APP_URL") viper.BindEnv("app-url", "APP_URL")
viper.BindEnv("users", "USERS") viper.BindEnv("users", "USERS")
viper.BindEnv("users-file", "USERS_FILE") viper.BindEnv("users-file", "USERS_FILE")
viper.BindEnv("cookie-secure", "COOKIE_SECURE") viper.BindEnv("cookie-secure", "COOKIE_SECURE")
viper.BindEnv("github-client-id", "GITHUB_CLIENT_ID") viper.BindEnv("github-client-id", "GITHUB_CLIENT_ID")
viper.BindEnv("github-client-secret", "GITHUB_CLIENT_SECRET") viper.BindEnv("github-client-secret", "GITHUB_CLIENT_SECRET")
viper.BindEnv("github-client-secret-file", "GITHUB_CLIENT_SECRET_FILE")
viper.BindEnv("google-client-id", "GOOGLE_CLIENT_ID") viper.BindEnv("google-client-id", "GOOGLE_CLIENT_ID")
viper.BindEnv("google-client-secret", "GOOGLE_CLIENT_SECRET") viper.BindEnv("google-client-secret", "GOOGLE_CLIENT_SECRET")
viper.BindEnv("google-client-secret-file", "GOOGLE_CLIENT_SECRET_FILE")
viper.BindEnv("generic-client-id", "GENERIC_CLIENT_ID") viper.BindEnv("generic-client-id", "GENERIC_CLIENT_ID")
viper.BindEnv("generic-client-secret", "GENERIC_CLIENT_SECRET") viper.BindEnv("generic-client-secret", "GENERIC_CLIENT_SECRET")
viper.BindEnv("generic-client-secret-file", "GENERIC_CLIENT_SECRET_FILE")
viper.BindEnv("generic-scopes", "GENERIC_SCOPES") viper.BindEnv("generic-scopes", "GENERIC_SCOPES")
viper.BindEnv("generic-auth-url", "GENERIC_AUTH_URL") viper.BindEnv("generic-auth-url", "GENERIC_AUTH_URL")
viper.BindEnv("generic-token-url", "GENERIC_TOKEN_URL") viper.BindEnv("generic-token-url", "GENERIC_TOKEN_URL")
viper.BindEnv("generic-user-url", "GENERIC_USER_URL") viper.BindEnv("generic-user-url", "GENERIC_USER_URL")
viper.BindEnv("disable-continue", "DISABLE_CONTINUE") viper.BindEnv("disable-continue", "DISABLE_CONTINUE")
viper.BindEnv("oauth-whitelist", "WHITELIST") viper.BindEnv("oauth-whitelist", "OAUTH_WHITELIST")
viper.BindEnv("cookie-expiry", "COOKIE_EXPIRY") viper.BindEnv("cookie-expiry", "COOKIE_EXPIRY")
viper.BindEnv("log-level", "LOG_LEVEL")
viper.BindPFlags(rootCmd.Flags()) viper.BindPFlags(rootCmd.Flags())
} }

View File

@@ -6,6 +6,7 @@ import (
"strings" "strings"
"github.com/charmbracelet/huh" "github.com/charmbracelet/huh"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
@@ -21,6 +22,8 @@ var CreateCmd = &cobra.Command{
Short: "Create a user", Short: "Create a user",
Long: `Create a user either interactively or by passing flags.`, Long: `Create a user either interactively or by passing flags.`,
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
log.Logger = log.Level(zerolog.InfoLevel)
if interactive { if interactive {
form := huh.NewForm( form := huh.NewForm(
huh.NewGroup( huh.NewGroup(

View File

@@ -8,12 +8,17 @@ import (
) )
func UserCmd() *cobra.Command { func UserCmd() *cobra.Command {
// Create the user command
userCmd := &cobra.Command{ userCmd := &cobra.Command{
Use: "user", Use: "user",
Short: "User utilities", Short: "User utilities",
Long: `Utilities for creating and verifying tinyauth compatible users.`, Long: `Utilities for creating and verifying tinyauth compatible users.`,
} }
// Add subcommands
userCmd.AddCommand(create.CreateCmd) userCmd.AddCommand(create.CreateCmd)
userCmd.AddCommand(verify.VerifyCmd) userCmd.AddCommand(verify.VerifyCmd)
// Return the user command
return userCmd return userCmd
} }

View File

@@ -5,6 +5,7 @@ import (
"strings" "strings"
"github.com/charmbracelet/huh" "github.com/charmbracelet/huh"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
@@ -19,12 +20,14 @@ var user string
var VerifyCmd = &cobra.Command{ var VerifyCmd = &cobra.Command{
Use: "verify", Use: "verify",
Short: "Verify a user is set up correctly", Short: "Verify a user is set up correctly",
Long: `Verify a user is set up correctly meaning that it has a correct password.`, Long: `Verify a user is set up correctly meaning that it has a correct username and password.`,
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
log.Logger = log.Level(zerolog.InfoLevel)
if interactive { if interactive {
form := huh.NewForm( form := huh.NewForm(
huh.NewGroup( huh.NewGroup(
huh.NewInput().Title("User (user:hash)").Value(&user).Validate((func(s string) error { huh.NewInput().Title("User (username:hash)").Value(&user).Validate((func(s string) error {
if s == "" { if s == "" {
return errors.New("user cannot be empty") return errors.New("user cannot be empty")
} }
@@ -86,5 +89,5 @@ func init() {
VerifyCmd.Flags().BoolVar(&docker, "docker", false, "Is the user formatted for docker?") VerifyCmd.Flags().BoolVar(&docker, "docker", false, "Is the user formatted for docker?")
VerifyCmd.Flags().StringVar(&username, "username", "", "Username") VerifyCmd.Flags().StringVar(&username, "username", "", "Username")
VerifyCmd.Flags().StringVar(&password, "password", "", "Password") VerifyCmd.Flags().StringVar(&password, "password", "", "Password")
VerifyCmd.Flags().StringVar(&user, "user", "", "Hash (user:hash combination)") VerifyCmd.Flags().StringVar(&user, "user", "", "Hash (username:hash combination)")
} }

View File

@@ -42,17 +42,22 @@ type API struct {
func (api *API) Init() { func (api *API) Init() {
gin.SetMode(gin.ReleaseMode) gin.SetMode(gin.ReleaseMode)
log.Debug().Msg("Setting up router")
router := gin.New() router := gin.New()
router.Use(zerolog()) router.Use(zerolog())
log.Debug().Msg("Setting up assets")
dist, distErr := fs.Sub(assets.Assets, "dist") dist, distErr := fs.Sub(assets.Assets, "dist")
if distErr != nil { if distErr != nil {
log.Fatal().Err(distErr).Msg("Failed to get UI assets") log.Fatal().Err(distErr).Msg("Failed to get UI assets")
} }
log.Debug().Msg("Setting up file server")
fileServer := http.FileServer(http.FS(dist)) fileServer := http.FileServer(http.FS(dist))
log.Debug().Msg("Setting up cookie store")
store := cookie.NewStore([]byte(api.Config.Secret)) store := cookie.NewStore([]byte(api.Config.Secret))
log.Debug().Msg("Getting domain")
domain, domainErr := utils.GetRootURL(api.Config.AppURL) domain, domainErr := utils.GetRootURL(api.Config.AppURL)
if domainErr != nil { if domainErr != nil {
@@ -90,18 +95,11 @@ func (api *API) Init() {
func (api *API) SetupRoutes() { func (api *API) SetupRoutes() {
api.Router.GET("/api/auth", func(c *gin.Context) { api.Router.GET("/api/auth", func(c *gin.Context) {
userContext, userContextErr := api.Hooks.UseUserContext(c) log.Debug().Msg("Checking auth")
userContext := api.Hooks.UseUserContext(c)
if userContextErr != nil {
log.Error().Err(userContextErr).Msg("Failed to get user context")
c.JSON(500, gin.H{
"status": 500,
"message": "Internal Server Error",
})
return
}
if userContext.IsLoggedIn { if userContext.IsLoggedIn {
log.Debug().Msg("Authenticated")
c.JSON(200, gin.H{ c.JSON(200, gin.H{
"status": 200, "status": 200,
"message": "Authenticated", "message": "Authenticated",
@@ -116,6 +114,8 @@ func (api *API) SetupRoutes() {
RedirectURI: fmt.Sprintf("%s://%s%s", proto, host, uri), 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")
if queryErr != nil { if queryErr != nil {
log.Error().Err(queryErr).Msg("Failed to build query") log.Error().Err(queryErr).Msg("Failed to build query")
c.JSON(501, gin.H{ c.JSON(501, gin.H{
@@ -142,9 +142,12 @@ func (api *API) SetupRoutes() {
return return
} }
user := api.Auth.GetUser(login.Email) log.Debug().Msg("Got login request")
user := api.Auth.GetUser(login.Username)
if user == nil { if user == nil {
log.Debug().Str("username", login.Username).Msg("User not found")
c.JSON(401, gin.H{ c.JSON(401, gin.H{
"status": 401, "status": 401,
"message": "Unauthorized", "message": "Unauthorized",
@@ -153,6 +156,7 @@ func (api *API) SetupRoutes() {
} }
if !api.Auth.CheckPassword(*user, login.Password) { if !api.Auth.CheckPassword(*user, login.Password) {
log.Debug().Str("username", login.Username).Msg("Password incorrect")
c.JSON(401, gin.H{ c.JSON(401, gin.H{
"status": 401, "status": 401,
"message": "Unauthorized", "message": "Unauthorized",
@@ -160,9 +164,12 @@ func (api *API) SetupRoutes() {
return return
} }
session := sessions.Default(c) log.Debug().Msg("Password correct, logging in")
session.Set("tinyauth_sid", fmt.Sprintf("email:%s", login.Email))
session.Save() api.Auth.CreateSessionCookie(c, &types.SessionCookie{
Username: login.Username,
Provider: "username",
})
c.JSON(200, gin.H{ c.JSON(200, gin.H{
"status": 200, "status": 200,
@@ -171,9 +178,9 @@ func (api *API) SetupRoutes() {
}) })
api.Router.POST("/api/logout", func(c *gin.Context) { api.Router.POST("/api/logout", func(c *gin.Context) {
session := sessions.Default(c) api.Auth.DeleteSessionCookie(c)
session.Delete("tinyauth_sid")
session.Save() log.Debug().Msg("Cleaning up redirect cookie")
c.SetCookie("tinyauth_redirect_uri", "", -1, "/", api.Domain, api.Config.CookieSecure, true) c.SetCookie("tinyauth_redirect_uri", "", -1, "/", api.Domain, api.Config.CookieSecure, true)
@@ -184,39 +191,40 @@ func (api *API) SetupRoutes() {
}) })
api.Router.GET("/api/status", func(c *gin.Context) { api.Router.GET("/api/status", func(c *gin.Context) {
userContext, userContextErr := api.Hooks.UseUserContext(c) log.Debug().Msg("Checking status")
userContext := api.Hooks.UseUserContext(c)
if userContextErr != nil { configuredProviders := api.Providers.GetConfiguredProviders()
log.Error().Err(userContextErr).Msg("Failed to get user context")
c.JSON(500, gin.H{ if api.Auth.UserAuthConfigured() {
"status": 500, configuredProviders = append(configuredProviders, "username")
"message": "Internal Server Error",
})
return
} }
if !userContext.IsLoggedIn { if !userContext.IsLoggedIn {
log.Debug().Msg("Unauthenticated")
c.JSON(200, gin.H{ c.JSON(200, gin.H{
"status": 200, "status": 200,
"message": "Unauthenticated", "message": "Unauthenticated",
"email": "", "username": "",
"isLoggedIn": false, "isLoggedIn": false,
"oauth": false, "oauth": false,
"provider": "", "provider": "",
"configuredProviders": api.Providers.GetConfiguredProviders(), "configuredProviders": configuredProviders,
"disableContinue": api.Config.DisableContinue, "disableContinue": api.Config.DisableContinue,
}) })
return return
} }
log.Debug().Interface("userContext", userContext).Strs("configuredProviders", configuredProviders).Bool("disableContinue", api.Config.DisableContinue).Msg("Authenticated")
c.JSON(200, gin.H{ c.JSON(200, gin.H{
"status": 200, "status": 200,
"message": "Authenticated", "message": "Authenticated",
"email": userContext.Email, "username": userContext.Username,
"isLoggedIn": userContext.IsLoggedIn, "isLoggedIn": userContext.IsLoggedIn,
"oauth": userContext.OAuth, "oauth": userContext.OAuth,
"provider": userContext.Provider, "provider": userContext.Provider,
"configuredProviders": api.Providers.GetConfiguredProviders(), "configuredProviders": configuredProviders,
"disableContinue": api.Config.DisableContinue, "disableContinue": api.Config.DisableContinue,
}) })
}) })
@@ -242,6 +250,8 @@ func (api *API) SetupRoutes() {
return return
} }
log.Debug().Msg("Got OAuth request")
provider := api.Providers.GetProvider(request.Provider) provider := api.Providers.GetProvider(request.Provider)
if provider == nil { if provider == nil {
@@ -252,11 +262,16 @@ func (api *API) SetupRoutes() {
return return
} }
log.Debug().Str("provider", request.Provider).Msg("Got provider")
authURL := provider.GetAuthURL() authURL := provider.GetAuthURL()
log.Debug().Msg("Got auth URL")
redirectURI := c.Query("redirect_uri") redirectURI := c.Query("redirect_uri")
if redirectURI != "" { if redirectURI != "" {
log.Debug().Str("redirectURI", redirectURI).Msg("Setting redirect cookie")
c.SetCookie("tinyauth_redirect_uri", redirectURI, 3600, "/", api.Domain, api.Config.CookieSecure, true) c.SetCookie("tinyauth_redirect_uri", redirectURI, 3600, "/", api.Domain, api.Config.CookieSecure, true)
} }
@@ -276,6 +291,8 @@ func (api *API) SetupRoutes() {
return return
} }
log.Debug().Interface("provider", providerName.Provider).Msg("Got provider name")
code := c.Query("code") code := c.Query("code")
if code == "" { if code == "" {
@@ -284,14 +301,20 @@ func (api *API) SetupRoutes() {
return return
} }
log.Debug().Msg("Got code")
provider := api.Providers.GetProvider(providerName.Provider) provider := api.Providers.GetProvider(providerName.Provider)
log.Debug().Str("provider", providerName.Provider).Msg("Got provider")
if provider == nil { if provider == nil {
c.Redirect(http.StatusPermanentRedirect, "/not-found") c.Redirect(http.StatusPermanentRedirect, "/not-found")
return return
} }
token, tokenErr := provider.ExchangeToken(code) _, tokenErr := provider.ExchangeToken(code)
log.Debug().Msg("Got token")
if handleApiError(c, "Failed to exchange token", tokenErr) { if handleApiError(c, "Failed to exchange token", tokenErr) {
return return
@@ -299,6 +322,8 @@ func (api *API) SetupRoutes() {
email, emailErr := api.Providers.GetUser(providerName.Provider) email, emailErr := api.Providers.GetUser(providerName.Provider)
log.Debug().Str("email", email).Msg("Got email")
if handleApiError(c, "Failed to get user", emailErr) { if handleApiError(c, "Failed to get user", emailErr) {
return return
} }
@@ -306,7 +331,7 @@ func (api *API) SetupRoutes() {
if !api.Auth.EmailWhitelisted(email) { if !api.Auth.EmailWhitelisted(email) {
log.Warn().Str("email", email).Msg("Email not whitelisted") log.Warn().Str("email", email).Msg("Email not whitelisted")
unauthorizedQuery, unauthorizedQueryErr := query.Values(types.UnauthorizedQuery{ unauthorizedQuery, unauthorizedQueryErr := query.Values(types.UnauthorizedQuery{
Email: email, Username: email,
}) })
if handleApiError(c, "Failed to build query", unauthorizedQueryErr) { if handleApiError(c, "Failed to build query", unauthorizedQueryErr) {
return return
@@ -314,9 +339,12 @@ func (api *API) SetupRoutes() {
c.Redirect(http.StatusPermanentRedirect, fmt.Sprintf("%s/unauthorized?%s", api.Config.AppURL, unauthorizedQuery.Encode())) c.Redirect(http.StatusPermanentRedirect, fmt.Sprintf("%s/unauthorized?%s", api.Config.AppURL, unauthorizedQuery.Encode()))
} }
session := sessions.Default(c) log.Debug().Msg("Email whitelisted")
session.Set("tinyauth_sid", fmt.Sprintf("%s:%s", providerName.Provider, token))
session.Save() api.Auth.CreateSessionCookie(c, &types.SessionCookie{
Username: email,
Provider: providerName.Provider,
})
redirectURI, redirectURIErr := c.Cookie("tinyauth_redirect_uri") redirectURI, redirectURIErr := c.Cookie("tinyauth_redirect_uri")
@@ -327,12 +355,16 @@ func (api *API) SetupRoutes() {
}) })
} }
log.Debug().Str("redirectURI", redirectURI).Msg("Got redirect URI")
c.SetCookie("tinyauth_redirect_uri", "", -1, "/", api.Domain, api.Config.CookieSecure, true) c.SetCookie("tinyauth_redirect_uri", "", -1, "/", api.Domain, api.Config.CookieSecure, true)
redirectQuery, redirectQueryErr := query.Values(types.LoginQuery{ redirectQuery, redirectQueryErr := query.Values(types.LoginQuery{
RedirectURI: redirectURI, RedirectURI: redirectURI,
}) })
log.Debug().Msg("Got redirect query")
if handleApiError(c, "Failed to build query", redirectQueryErr) { if handleApiError(c, "Failed to build query", redirectQueryErr) {
return return
} }

View File

@@ -1 +1 @@
v1.0.0 v2.0.2

View File

@@ -3,6 +3,9 @@ package auth
import ( import (
"tinyauth/internal/types" "tinyauth/internal/types"
"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"
"github.com/rs/zerolog/log"
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
) )
@@ -18,9 +21,9 @@ type Auth struct {
OAuthWhitelist []string OAuthWhitelist []string
} }
func (auth *Auth) GetUser(email string) *types.User { func (auth *Auth) GetUser(username string) *types.User {
for _, user := range auth.Users { for _, user := range auth.Users {
if user.Email == email { if user.Username == username {
return &user return &user
} }
} }
@@ -43,3 +46,46 @@ func (auth *Auth) EmailWhitelisted(emailSrc string) bool {
} }
return false return false
} }
func (auth *Auth) CreateSessionCookie(c *gin.Context, data *types.SessionCookie) {
log.Debug().Msg("Creating session cookie")
sessions := sessions.Default(c)
log.Debug().Msg("Setting session cookie")
sessions.Set("username", data.Username)
sessions.Set("provider", data.Provider)
sessions.Save()
}
func (auth *Auth) DeleteSessionCookie(c *gin.Context) {
log.Debug().Msg("Deleting session cookie")
sessions := sessions.Default(c)
sessions.Clear()
sessions.Save()
}
func (auth *Auth) GetSessionCookie(c *gin.Context) (types.SessionCookie, error) {
log.Debug().Msg("Getting session cookie")
sessions := sessions.Default(c)
cookieUsername := sessions.Get("username")
cookieProvider := sessions.Get("provider")
username, usernameOk := cookieUsername.(string)
provider, providerOk := cookieProvider.(string)
log.Debug().Str("username", username).Str("provider", provider).Msg("Parsed cookie")
if !usernameOk || !providerOk {
log.Warn().Msg("Session cookie invalid")
return types.SessionCookie{}, nil
}
return types.SessionCookie{
Username: username,
Provider: provider,
}, nil
}
func (auth *Auth) UserAuthConfigured() bool {
return len(auth.Users) > 0
}

View File

@@ -1,14 +1,12 @@
package hooks package hooks
import ( import (
"strings"
"tinyauth/internal/auth" "tinyauth/internal/auth"
"tinyauth/internal/providers" "tinyauth/internal/providers"
"tinyauth/internal/types" "tinyauth/internal/types"
"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"golang.org/x/oauth2" "github.com/rs/zerolog/log"
) )
func NewHooks(auth *auth.Auth, providers *providers.Providers) *Hooks { func NewHooks(auth *auth.Auth, providers *providers.Providers) *Hooks {
@@ -23,103 +21,60 @@ type Hooks struct {
Providers *providers.Providers Providers *providers.Providers
} }
func (hooks *Hooks) UseUserContext(c *gin.Context) (types.UserContext, error) { func (hooks *Hooks) UseUserContext(c *gin.Context) types.UserContext {
session := sessions.Default(c) cookie, cookiErr := hooks.Auth.GetSessionCookie(c)
sessionCookie := session.Get("tinyauth_sid")
if sessionCookie == nil { if cookiErr != nil {
log.Error().Err(cookiErr).Msg("Failed to get session cookie")
return types.UserContext{ return types.UserContext{
Email: "", Username: "",
IsLoggedIn: false, IsLoggedIn: false,
OAuth: false, OAuth: false,
Provider: "", Provider: "",
}, nil }
} }
data, dataOk := sessionCookie.(string) if cookie.Provider == "username" {
log.Debug().Msg("Provider is username")
if !dataOk { if hooks.Auth.GetUser(cookie.Username) != nil {
return types.UserContext{ log.Debug().Msg("User exists")
Email: "",
IsLoggedIn: false,
OAuth: false,
Provider: "",
}, nil
}
split := strings.Split(data, ":")
if len(split) != 2 {
return types.UserContext{
Email: "",
IsLoggedIn: false,
OAuth: false,
Provider: "",
}, nil
}
sessionType := split[0]
sessionValue := split[1]
if sessionType == "email" {
user := hooks.Auth.GetUser(sessionValue)
if user == nil {
return types.UserContext{ return types.UserContext{
Email: "", Username: cookie.Username,
IsLoggedIn: true,
OAuth: false,
Provider: "",
}
}
}
log.Debug().Msg("Provider is not username")
provider := hooks.Providers.GetProvider(cookie.Provider)
if provider != nil {
log.Debug().Msg("Provider exists")
if !hooks.Auth.EmailWhitelisted(cookie.Username) {
log.Error().Str("email", cookie.Username).Msg("Email is not whitelisted")
hooks.Auth.DeleteSessionCookie(c)
return types.UserContext{
Username: "",
IsLoggedIn: false, IsLoggedIn: false,
OAuth: false, OAuth: false,
Provider: "", Provider: "",
}, nil }
} }
log.Debug().Msg("Email is whitelisted")
return types.UserContext{ return types.UserContext{
Email: sessionValue, Username: cookie.Username,
IsLoggedIn: true, IsLoggedIn: true,
OAuth: false, OAuth: true,
Provider: "", Provider: cookie.Provider,
}, nil }
}
provider := hooks.Providers.GetProvider(sessionType)
if provider == nil {
return types.UserContext{
Email: "",
IsLoggedIn: false,
OAuth: false,
Provider: "",
}, nil
}
provider.Token = &oauth2.Token{
AccessToken: sessionValue,
}
email, emailErr := hooks.Providers.GetUser(sessionType)
if emailErr != nil {
return types.UserContext{
Email: "",
IsLoggedIn: false,
OAuth: false,
Provider: "",
}, nil
}
if !hooks.Auth.EmailWhitelisted(email) {
session.Delete("tinyauth_sid")
session.Save()
return types.UserContext{
Email: "",
IsLoggedIn: false,
OAuth: false,
Provider: "",
}, nil
} }
return types.UserContext{ return types.UserContext{
Email: email, Username: "",
IsLoggedIn: true, IsLoggedIn: false,
OAuth: true, OAuth: false,
Provider: sessionType, Provider: "",
}, nil }
} }

View File

@@ -4,6 +4,8 @@ import (
"encoding/json" "encoding/json"
"io" "io"
"net/http" "net/http"
"github.com/rs/zerolog/log"
) )
type GenericUserInfoResponse struct { type GenericUserInfoResponse struct {
@@ -17,12 +19,16 @@ func GetGenericEmail(client *http.Client, url string) (string, error) {
return "", resErr return "", resErr
} }
log.Debug().Msg("Got response from generic provider")
body, bodyErr := io.ReadAll(res.Body) body, bodyErr := io.ReadAll(res.Body)
if bodyErr != nil { if bodyErr != nil {
return "", bodyErr return "", bodyErr
} }
log.Debug().Msg("Read body from generic provider")
var user GenericUserInfoResponse var user GenericUserInfoResponse
jsonErr := json.Unmarshal(body, &user) jsonErr := json.Unmarshal(body, &user)
@@ -31,5 +37,7 @@ func GetGenericEmail(client *http.Client, url string) (string, error) {
return "", jsonErr return "", jsonErr
} }
log.Debug().Msg("Parsed user from generic provider")
return user.Email, nil return user.Email, nil
} }

View File

@@ -5,6 +5,8 @@ import (
"errors" "errors"
"io" "io"
"net/http" "net/http"
"github.com/rs/zerolog/log"
) )
type GithubUserInfoResponse []struct { type GithubUserInfoResponse []struct {
@@ -23,12 +25,16 @@ func GetGithubEmail(client *http.Client) (string, error) {
return "", resErr return "", resErr
} }
log.Debug().Msg("Got response from github")
body, bodyErr := io.ReadAll(res.Body) body, bodyErr := io.ReadAll(res.Body)
if bodyErr != nil { if bodyErr != nil {
return "", bodyErr return "", bodyErr
} }
log.Debug().Msg("Read body from github")
var emails GithubUserInfoResponse var emails GithubUserInfoResponse
jsonErr := json.Unmarshal(body, &emails) jsonErr := json.Unmarshal(body, &emails)
@@ -37,6 +43,8 @@ func GetGithubEmail(client *http.Client) (string, error) {
return "", jsonErr return "", jsonErr
} }
log.Debug().Msg("Parsed emails from github")
for _, email := range emails { for _, email := range emails {
if email.Primary { if email.Primary {
return email.Email, nil return email.Email, nil

View File

@@ -4,6 +4,8 @@ import (
"encoding/json" "encoding/json"
"io" "io"
"net/http" "net/http"
"github.com/rs/zerolog/log"
) )
type GoogleUserInfoResponse struct { type GoogleUserInfoResponse struct {
@@ -21,12 +23,16 @@ func GetGoogleEmail(client *http.Client) (string, error) {
return "", resErr return "", resErr
} }
log.Debug().Msg("Got response from google")
body, bodyErr := io.ReadAll(res.Body) body, bodyErr := io.ReadAll(res.Body)
if bodyErr != nil { if bodyErr != nil {
return "", bodyErr return "", bodyErr
} }
log.Debug().Msg("Read body from google")
var user GoogleUserInfoResponse var user GoogleUserInfoResponse
jsonErr := json.Unmarshal(body, &user) jsonErr := json.Unmarshal(body, &user)
@@ -35,5 +41,7 @@ func GetGoogleEmail(client *http.Client) (string, error) {
return "", jsonErr return "", jsonErr
} }
log.Debug().Msg("Parsed user from google")
return user.Email, nil return user.Email, nil
} }

View File

@@ -79,33 +79,42 @@ func (providers *Providers) GetUser(provider string) (string, error) {
switch provider { switch provider {
case "github": case "github":
if providers.Github == nil { if providers.Github == nil {
log.Debug().Msg("Github provider not configured")
return "", nil return "", nil
} }
client := providers.Github.GetClient() client := providers.Github.GetClient()
log.Debug().Msg("Got client from github")
email, emailErr := GetGithubEmail(client) email, emailErr := GetGithubEmail(client)
if emailErr != nil { if emailErr != nil {
return "", emailErr return "", emailErr
} }
log.Debug().Msg("Got email from github")
return email, nil return email, nil
case "google": case "google":
if providers.Google == nil { if providers.Google == nil {
log.Debug().Msg("Google provider not configured")
return "", nil return "", nil
} }
client := providers.Google.GetClient() client := providers.Google.GetClient()
log.Debug().Msg("Got client from google")
email, emailErr := GetGoogleEmail(client) email, emailErr := GetGoogleEmail(client)
if emailErr != nil { if emailErr != nil {
return "", emailErr return "", emailErr
} }
log.Debug().Msg("Got email from google")
return email, nil return email, nil
case "generic": case "generic":
if providers.Generic == nil { if providers.Generic == nil {
log.Debug().Msg("Generic provider not configured")
return "", nil return "", nil
} }
client := providers.Generic.GetClient() client := providers.Generic.GetClient()
log.Debug().Msg("Got client from generic")
email, emailErr := GetGenericEmail(client, providers.Config.GenericUserURL) email, emailErr := GetGenericEmail(client, providers.Config.GenericUserURL)
if emailErr != nil { if emailErr != nil {
return "", emailErr return "", emailErr
} }
log.Debug().Msg("Got email from generic")
return email, nil return email, nil
default: default:
return "", nil return "", nil

View File

@@ -7,42 +7,47 @@ type LoginQuery struct {
} }
type LoginRequest struct { type LoginRequest struct {
Email string `json:"email"` Username string `json:"username"`
Password string `json:"password"` Password string `json:"password"`
} }
type User struct { type User struct {
Email string Username string
Password string Password string
} }
type Users []User type Users []User
type Config struct { type Config struct {
Port int `validate:"number" mapstructure:"port"` Port int `mapstructure:"port" validate:"required"`
Address string `mapstructure:"address, ip4_addr"` Address string `validate:"required,ip4_addr" mapstructure:"address"`
Secret string `validate:"required,len=32" mapstructure:"secret"` Secret string `validate:"required,len=32" mapstructure:"secret"`
AppURL string `validate:"required,url" mapstructure:"app-url"` SecretFile string `mapstructure:"secret-file"`
Users string `mapstructure:"users"` AppURL string `validate:"required,url" mapstructure:"app-url"`
UsersFile string `mapstructure:"users-file"` Users string `mapstructure:"users"`
CookieSecure bool `mapstructure:"cookie-secure"` UsersFile string `mapstructure:"users-file"`
GithubClientId string `mapstructure:"github-client-id"` CookieSecure bool `mapstructure:"cookie-secure"`
GithubClientSecret string `mapstructure:"github-client-secret"` GithubClientId string `mapstructure:"github-client-id"`
GoogleClientId string `mapstructure:"google-client-id"` GithubClientSecret string `mapstructure:"github-client-secret"`
GoogleClientSecret string `mapstructure:"google-client-secret"` GithubClientSecretFile string `mapstructure:"github-client-secret-file"`
GenericClientId string `mapstructure:"generic-client-id"` GoogleClientId string `mapstructure:"google-client-id"`
GenericClientSecret string `mapstructure:"generic-client-secret"` GoogleClientSecret string `mapstructure:"google-client-secret"`
GenericScopes string `mapstructure:"generic-scopes"` GoogleClientSecretFile string `mapstructure:"google-client-secret-file"`
GenericAuthURL string `mapstructure:"generic-auth-url"` GenericClientId string `mapstructure:"generic-client-id"`
GenericTokenURL string `mapstructure:"generic-token-url"` GenericClientSecret string `mapstructure:"generic-client-secret"`
GenericUserURL string `mapstructure:"generic-user-info-url"` GenericClientSecretFile string `mapstructure:"generic-client-secret-file"`
DisableContinue bool `mapstructure:"disable-continue"` GenericScopes string `mapstructure:"generic-scopes"`
OAuthWhitelist string `mapstructure:"oauth-whitelist"` GenericAuthURL string `mapstructure:"generic-auth-url"`
CookieExpiry int `mapstructure:"cookie-expiry"` GenericTokenURL string `mapstructure:"generic-token-url"`
GenericUserURL string `mapstructure:"generic-user-info-url"`
DisableContinue bool `mapstructure:"disable-continue"`
OAuthWhitelist string `mapstructure:"oauth-whitelist"`
CookieExpiry int `mapstructure:"cookie-expiry"`
LogLevel int8 `mapstructure:"log-level" validate:"min=-1,max=5"`
} }
type UserContext struct { type UserContext struct {
Email string Username string
IsLoggedIn bool IsLoggedIn bool
OAuth bool OAuth bool
Provider string Provider string
@@ -83,5 +88,10 @@ type OAuthProviders struct {
} }
type UnauthorizedQuery struct { type UnauthorizedQuery struct {
Email string `url:"email"` Username string `url:"username"`
}
type SessionCookie struct {
Username string
Provider string
} }

View File

@@ -6,9 +6,12 @@ import (
"os" "os"
"strings" "strings"
"tinyauth/internal/types" "tinyauth/internal/types"
"github.com/rs/zerolog/log"
) )
func ParseUsers(users string) (types.Users, error) { func ParseUsers(users string) (types.Users, error) {
log.Debug().Msg("Parsing users")
var usersParsed types.Users var usersParsed types.Users
userList := strings.Split(users, ",") userList := strings.Split(users, ",")
@@ -22,11 +25,13 @@ func ParseUsers(users string) (types.Users, error) {
return types.Users{}, errors.New("invalid user format") return types.Users{}, errors.New("invalid user format")
} }
usersParsed = append(usersParsed, types.User{ usersParsed = append(usersParsed, types.User{
Email: userSplit[0], Username: userSplit[0],
Password: userSplit[1], Password: userSplit[1],
}) })
} }
log.Debug().Msg("Parsed users")
return usersParsed, nil return usersParsed, nil
} }
@@ -37,21 +42,21 @@ func GetRootURL(urlSrc string) (string, error) {
return "", parseErr return "", parseErr
} }
urlSplitted := strings.Split(urlParsed.Host, ".") urlSplitted := strings.Split(urlParsed.Hostname(), ".")
urlFinal := strings.Join(urlSplitted[1:], ".") urlFinal := strings.Join(urlSplitted[1:], ".")
return urlFinal, nil return urlFinal, nil
} }
func GetUsersFromFile(usersFile string) (string, error) { func ReadFile(file string) (string, error) {
_, statErr := os.Stat(usersFile) _, statErr := os.Stat(file)
if statErr != nil { if statErr != nil {
return "", statErr return "", statErr
} }
data, readErr := os.ReadFile(usersFile) data, readErr := os.ReadFile(file)
if readErr != nil { if readErr != nil {
return "", readErr return "", readErr
@@ -69,15 +74,57 @@ func ParseFileToLine(content string) string {
continue continue
} }
users = append(users, line) users = append(users, strings.TrimSpace(line))
} }
return strings.Join(users, ",") return strings.Join(users, ",")
} }
func ParseCommaString(str string) []string { func GetSecret(conf string, file string) string {
if str == "" { if conf == "" && file == "" {
return []string{} return ""
} }
return strings.Split(str, ",")
if conf != "" {
return conf
}
contents, err := ReadFile(file)
if err != nil {
return ""
}
return contents
}
func GetUsers(conf string, file string) (types.Users, error) {
var users string
if conf == "" && file == "" {
return types.Users{}, errors.New("no users provided")
}
if conf != "" {
log.Debug().Msg("Using users from config")
users += conf
}
if file != "" {
fileContents, fileErr := ReadFile(file)
if fileErr == nil {
log.Debug().Msg("Using users from file")
if users != "" {
users += ","
}
users += ParseFileToLine(fileContents)
}
}
return ParseUsers(users)
}
func OAuthConfigured(config types.Config) bool {
return (config.GithubClientId != "" && config.GithubClientSecret != "") || (config.GoogleClientId != "" && config.GoogleClientSecret != "") || (config.GenericClientId != "" && config.GenericClientSecret != "")
} }

View File

@@ -4,7 +4,6 @@ import (
"os" "os"
"time" "time"
"tinyauth/cmd" "tinyauth/cmd"
"tinyauth/internal/assets"
"github.com/rs/zerolog" "github.com/rs/zerolog"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
@@ -12,8 +11,7 @@ import (
func main() { func main() {
// Logger // Logger
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr, TimeFormat: time.RFC3339}).With().Timestamp().Logger() log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr, TimeFormat: time.RFC3339}).With().Timestamp().Logger().Level(zerolog.FatalLevel)
log.Info().Str("version", assets.Version).Msg("Starting tinyauth")
// Run cmd // Run cmd
cmd.Execute() cmd.Execute()

Binary file not shown.

View File

@@ -1,8 +1,9 @@
import { Button, Paper, Text } from "@mantine/core"; import { Button, Code, Paper, Text } from "@mantine/core";
import { notifications } from "@mantine/notifications"; import { notifications } from "@mantine/notifications";
import { Navigate } from "react-router"; import { Navigate } from "react-router";
import { useUserContext } from "../context/user-context"; import { useUserContext } from "../context/user-context";
import { Layout } from "../components/layouts/layout"; import { Layout } from "../components/layouts/layout";
import { ReactNode } from "react";
export const ContinuePage = () => { export const ContinuePage = () => {
const queryString = window.location.search; const queryString = window.location.search;
@@ -12,11 +13,11 @@ export const ContinuePage = () => {
const { isLoggedIn, disableContinue } = useUserContext(); const { isLoggedIn, disableContinue } = useUserContext();
if (!isLoggedIn) { if (!isLoggedIn) {
return <Navigate to="/login" />; return <Navigate to={`/login?redirect_uri=${redirectUri}`} />;
} }
if (disableContinue && redirectUri !== "null") { if (redirectUri === "null") {
window.location.replace(redirectUri!); return <Navigate to="/" />;
} }
const redirect = () => { const redirect = () => {
@@ -26,31 +27,62 @@ export const ContinuePage = () => {
color: "blue", color: "blue",
}); });
setTimeout(() => { setTimeout(() => {
window.location.replace(redirectUri!); window.location.href = redirectUri!;
}, 500); }, 500);
}; };
const urlParsed = URL.parse(redirectUri!);
if (
window.location.protocol === "https:" &&
urlParsed!.protocol === "http:"
) {
return (
<ContinuePageLayout>
<Text size="xl" fw={700}>
Insecure Redirect
</Text>
<Text>
Your are logged in but trying to redirect from <Code>https</Code> to{" "}
<Code>http</Code>, please click the button to redirect.
</Text>
<Button fullWidth mt="xl" onClick={redirect}>
Continue
</Button>
</ContinuePageLayout>
);
}
if (disableContinue) {
window.location.href = redirectUri!;
return (
<ContinuePageLayout>
<Text size="xl" fw={700}>
Redirecting
</Text>
<Text>You should be redirected to your app soon.</Text>
</ContinuePageLayout>
);
}
return (
<ContinuePageLayout>
<Text size="xl" fw={700}>
Continue
</Text>
<Text>Click the button to continue to your app.</Text>
<Button fullWidth mt="xl" onClick={redirect}>
Continue
</Button>
</ContinuePageLayout>
);
};
export const ContinuePageLayout = ({ children }: { children: ReactNode }) => {
return ( return (
<Layout> <Layout>
<Paper shadow="md" p={30} mt={30} radius="md" withBorder> <Paper shadow="md" p={30} mt={30} radius="md" withBorder>
{redirectUri !== "null" ? ( {children}
<>
<Text size="xl" fw={700}>
Continue
</Text>
<Text>Click the button to continue to your app.</Text>
<Button fullWidth mt="xl" onClick={redirect}>
Continue
</Button>
</>
) : (
<>
<Text size="xl" fw={700}>
Logged in
</Text>
<Text>You are now signed in and can use your apps.</Text>
</>
)}
</Paper> </Paper>
</Layout> </Layout>
); );

View File

@@ -32,7 +32,7 @@ export const LoginPage = () => {
} }
const schema = z.object({ const schema = z.object({
email: z.string().email(), username: z.string(),
password: z.string(), password: z.string(),
}); });
@@ -41,7 +41,7 @@ export const LoginPage = () => {
const form = useForm({ const form = useForm({
mode: "uncontrolled", mode: "uncontrolled",
initialValues: { initialValues: {
email: "", username: "",
password: "", password: "",
}, },
validate: zodResolver(schema), validate: zodResolver(schema),
@@ -54,7 +54,7 @@ export const LoginPage = () => {
onError: () => { onError: () => {
notifications.show({ notifications.show({
title: "Failed to login", title: "Failed to login",
message: "Check your email and password", message: "Check your username and password",
color: "red", color: "red",
}); });
}, },
@@ -65,8 +65,12 @@ export const LoginPage = () => {
color: "green", color: "green",
}); });
setTimeout(() => { setTimeout(() => {
window.location.replace(`/continue?redirect_uri=${redirectUri}`); if (redirectUri === "null") {
}); window.location.replace("/");
} else {
window.location.replace(`/continue?redirect_uri=${redirectUri}`);
}
}, 500);
}, },
}); });
@@ -84,7 +88,14 @@ export const LoginPage = () => {
}); });
}, },
onSuccess: (data) => { onSuccess: (data) => {
window.location.replace(data.data.url); notifications.show({
title: "Redirecting",
message: "Redirecting to your OAuth provider",
color: "blue",
});
setTimeout(() => {
window.location.href = data.data.url;
}, 500);
}, },
}); });
@@ -153,40 +164,44 @@ export const LoginPage = () => {
</Grid.Col> </Grid.Col>
)} )}
</Grid> </Grid>
<Divider {configuredProviders.includes("username") && (
label="Or continue with email" <Divider
labelPosition="center" label="Or continue with password"
my="lg" labelPosition="center"
/> my="lg"
/>
)}
</> </>
)} )}
<form onSubmit={form.onSubmit(handleSubmit)}> {configuredProviders.includes("username") && (
<TextInput <form onSubmit={form.onSubmit(handleSubmit)}>
label="Email" <TextInput
placeholder="user@example.com" label="Username"
required placeholder="user@example.com"
disabled={loginMutation.isLoading} required
key={form.key("email")} disabled={loginMutation.isLoading}
{...form.getInputProps("email")} key={form.key("username")}
/> {...form.getInputProps("username")}
<PasswordInput />
label="Password" <PasswordInput
placeholder="password" label="Password"
required placeholder="password"
mt="md" required
disabled={loginMutation.isLoading} mt="md"
key={form.key("password")} disabled={loginMutation.isLoading}
{...form.getInputProps("password")} key={form.key("password")}
/> {...form.getInputProps("password")}
<Button />
fullWidth <Button
mt="xl" fullWidth
type="submit" mt="xl"
loading={loginMutation.isLoading} type="submit"
> loading={loginMutation.isLoading}
Login >
</Button> Login
</form> </Button>
</form>
)}
</Paper> </Paper>
</Layout> </Layout>
); );

View File

@@ -8,7 +8,7 @@ import { Layout } from "../components/layouts/layout";
import { capitalize } from "../utils/utils"; import { capitalize } from "../utils/utils";
export const LogoutPage = () => { export const LogoutPage = () => {
const { isLoggedIn, email, oauth, provider } = useUserContext(); const { isLoggedIn, username, oauth, provider } = useUserContext();
if (!isLoggedIn) { if (!isLoggedIn) {
return <Navigate to="/login" />; return <Navigate to="/login" />;
@@ -32,7 +32,7 @@ export const LogoutPage = () => {
color: "green", color: "green",
}); });
setTimeout(() => { setTimeout(() => {
window.location.reload(); window.location.replace("/login");
}, 500); }, 500);
}, },
}); });
@@ -44,7 +44,7 @@ export const LogoutPage = () => {
Logout Logout
</Text> </Text>
<Text> <Text>
You are currently logged in as <Code>{email}</Code> You are currently logged in as <Code>{username}</Code>
{oauth && ` using ${capitalize(provider)}`}. Click the button below to {oauth && ` using ${capitalize(provider)}`}. Click the button below to
log out. log out.
</Text> </Text>

View File

@@ -6,7 +6,7 @@ import { Navigate } from "react-router";
export const UnauthorizedPage = () => { export const UnauthorizedPage = () => {
const queryString = window.location.search; const queryString = window.location.search;
const params = new URLSearchParams(queryString); const params = new URLSearchParams(queryString);
const email = params.get("email"); const username = params.get("username");
const { isLoggedIn } = useUserContext(); const { isLoggedIn } = useUserContext();
@@ -14,7 +14,7 @@ export const UnauthorizedPage = () => {
return <Navigate to="/" />; return <Navigate to="/" />;
} }
if (email === "null") { if (username === "null") {
return <Navigate to="/" />; return <Navigate to="/" />;
} }
@@ -25,7 +25,7 @@ export const UnauthorizedPage = () => {
Unauthorized Unauthorized
</Text> </Text>
<Text> <Text>
The user with email address <Code>{email}</Code> is not authorized to The user with username <Code>{username}</Code> is not authorized to
login. login.
</Text> </Text>
<Button <Button

View File

@@ -2,7 +2,7 @@ import { z } from "zod";
export const userContextSchema = z.object({ export const userContextSchema = z.object({
isLoggedIn: z.boolean(), isLoggedIn: z.boolean(),
email: z.string(), username: z.string(),
oauth: z.boolean(), oauth: z.boolean(),
provider: z.string(), provider: z.string(),
configuredProviders: z.array(z.string()), configuredProviders: z.array(z.string()),