mirror of
https://github.com/steveiliop56/tinyauth.git
synced 2025-11-02 23:25:45 +00:00
Compare commits
32 Commits
v1.0.0-alp
...
v2.0.2-bet
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2385599c80 | ||
|
|
6f184856f1 | ||
|
|
e2e3b3bdc6 | ||
|
|
3efcb26db1 | ||
|
|
c54267f50d | ||
|
|
4de12ce5c1 | ||
|
|
0cf0aafc14 | ||
|
|
80ea43184c | ||
|
|
3c4dffd479 | ||
|
|
f19f40f9fc | ||
|
|
a243f22ac8 | ||
|
|
08d382c981 | ||
|
|
94f7debb10 | ||
|
|
3b50d9303b | ||
|
|
d67133aca7 | ||
|
|
989ea8f229 | ||
|
|
708006decf | ||
|
|
682a918812 | ||
|
|
389248cfe1 | ||
|
|
81d25061df | ||
|
|
f59697955d | ||
|
|
47d8f1e5aa | ||
|
|
e8d2e059a9 | ||
|
|
2c7a3fc801 | ||
|
|
61fffb9708 | ||
|
|
9d2aef163b | ||
|
|
cc480085c5 | ||
|
|
2c7144937a | ||
|
|
c7ec788ce1 | ||
|
|
96a373a794 | ||
|
|
c5a8639822 | ||
|
|
b87cb54d91 |
4
.gitignore
vendored
4
.gitignore
vendored
@@ -9,3 +9,7 @@ docker-compose.test.yml
|
|||||||
|
|
||||||
# users file
|
# users file
|
||||||
users.txt
|
users.txt
|
||||||
|
|
||||||
|
# secret test file
|
||||||
|
secret.txt
|
||||||
|
secret_oauth.txt
|
||||||
@@ -35,7 +35,7 @@ COPY ./cmd ./cmd
|
|||||||
COPY ./internal ./internal
|
COPY ./internal ./internal
|
||||||
COPY --from=site-builder /site/dist ./internal/assets/dist
|
COPY --from=site-builder /site/dist ./internal/assets/dist
|
||||||
|
|
||||||
RUN go build
|
RUN CGO_ENABLED=0 go build
|
||||||
|
|
||||||
# Runner
|
# Runner
|
||||||
FROM alpine:3.21 AS runner
|
FROM alpine:3.21 AS runner
|
||||||
|
|||||||
50
README.md
50
README.md
@@ -1,36 +1,46 @@
|
|||||||
# Tinyauth - The simplest way to protect your apps with a login screen
|
<div align="center">
|
||||||
|
<img alt="Tinyauth" title="Tinyauth" width="256" src="site/public/logo.png">
|
||||||
|
<h1>Tinyauth</h1>
|
||||||
|
<p>The easiest way to secure your apps with a login screen.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
Tinyauth is an extremely simple traefik middleware that adds a login screen to all of your apps that are using the traefik reverse proxy. Tinyauth is configurable through environment variables and it is only 20MB in size.
|
<div align="center">
|
||||||
|
<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">
|
||||||
|
</div>
|
||||||
|
|
||||||
## Getting started
|
<br />
|
||||||
|
|
||||||
Tinyauth is extremely easy to run since it's shipped as a docker container. The guide on how to get started is available on the website [here](https://tinyauth.doesmycode.work/).
|
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.
|
||||||
|
|
||||||
## FAQ
|
> [!WARNING]
|
||||||
|
> Tinyauth is in active development and configuration may change often. Please make sure to carefully read the release notes before updating.
|
||||||
|
|
||||||
### Why?
|
> [!NOTE]
|
||||||
|
> Tinyauth is intended for homelab use and it is not made for production use cases. If you are looking for something production ready please use [authentik](https://goauthentik.io).
|
||||||
|
|
||||||
Why make this project? Well, we all know that more powerful alternatives like authentik and authelia exist, but when I tried to use them, I felt overwhelmed with all the configration options and environment variables I had to configure in order for them to work. So, I decided to make a small alternative in Go to both test my skills and cover my simple login screen needs.
|
## Getting Started
|
||||||
|
|
||||||
### Is this secure?
|
You can easily get started with tinyauth by following the guide on the documentation [here](https://tinyauth.doesmycode.work/docs/getting-started.html). There is also an available docker compose file [here](./docker-compose.example.yml) that has traefik, nginx and tinyauth to demonstrate its capabilities.
|
||||||
|
|
||||||
Probably, the sessions are managed with the gin sessions package so it should be very secure. It is definitely not made for production but it could easily serve as a simple login screen to all of your homelab apps.
|
## Documentation
|
||||||
|
|
||||||
### Do I need to login every time?
|
You can find documentation and guides on all available configuration of tinyauth [here](https://tinyauth.doesmycode.work).
|
||||||
|
|
||||||
No, when you login, tinyauth sets a `tinyauth` cookie in your browser that applies to all of the subdomains of your domain.
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
Tinyauth is licensed under the GNU General Public License v3.0. TL;DR — You may copy, distribute and modify the software as long as you track changes/dates in source files. Any modifications to or software including (via compiler) GPL-licensed code must also be made available under the GPL along with build & install instructions.
|
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
Any contributions to the codebase are welcome! I am not a cybersecurity person so my code may have a security issue, if you find something that could be used to exploit and bypass tinyauth please let me know as soon as possible so I can fix it.
|
All contributions to the codebase are welcome! If you have any recommendations on how to improve security or find a security issue in tinyauth please open an issue or pull request so it can be fixed as soon as possible!
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Tinyauth is licensed under the GNU General Public License v3.0. TL;DR — You may copy, distribute and modify the software as long as you track changes/dates in source files. Any modifications to or software including (via compiler) GPL-licensed code must also be made available under the GPL along with build & install instructions. For more information about the license check the [license](./LICENSE) file.
|
||||||
|
|
||||||
## Acknowledgements
|
## Acknowledgements
|
||||||
|
|
||||||
Credits for the logo go to:
|
Credits for the logo of this app go to:
|
||||||
|
|
||||||
- Freepik for providing the hat and police badge.
|
- **Freepik** for providing the police hat and logo.
|
||||||
- Renee French for making the gopher logo.
|
- **Renee French** for the original gopher logo.
|
||||||
|
|||||||
83
cmd/root.go
83
cmd/root.go
@@ -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,44 +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
|
// Create oauth whitelist
|
||||||
|
oauthWhitelist := strings.Split(config.OAuthWhitelist, ",")
|
||||||
if config.UsersFile != "" {
|
log.Debug().Msg("Parsed OAuth whitelist")
|
||||||
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 config
|
// Create OAuth config
|
||||||
oauthConfig := types.OAuthConfig{
|
oauthConfig := types.OAuthConfig{
|
||||||
@@ -64,15 +68,17 @@ var rootCmd = &cobra.Command{
|
|||||||
GoogleClientSecret: config.GoogleClientSecret,
|
GoogleClientSecret: config.GoogleClientSecret,
|
||||||
GenericClientId: config.GenericClientId,
|
GenericClientId: config.GenericClientId,
|
||||||
GenericClientSecret: config.GenericClientSecret,
|
GenericClientSecret: config.GenericClientSecret,
|
||||||
GenericScopes: config.GenericScopes,
|
GenericScopes: strings.Split(config.GenericScopes, ","),
|
||||||
GenericAuthURL: config.GenericAuthURL,
|
GenericAuthURL: config.GenericAuthURL,
|
||||||
GenericTokenURL: config.GenericTokenURL,
|
GenericTokenURL: config.GenericTokenURL,
|
||||||
GenericUserInfoURL: config.GenericUserInfoURL,
|
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)
|
auth := auth.NewAuth(users, oauthWhitelist)
|
||||||
|
|
||||||
// Create OAuth providers service
|
// Create OAuth providers service
|
||||||
providers := providers.NewProviders(oauthConfig)
|
providers := providers.NewProviders(oauthConfig)
|
||||||
@@ -91,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
|
||||||
@@ -121,38 +128,52 @@ 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.")
|
||||||
rootCmd.Flags().String("generic-user-info-url", "", "Generic OAuth user info URL.")
|
rootCmd.Flags().String("generic-user-url", "", "Generic OAuth user info URL.")
|
||||||
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().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-info-url", "GENERIC_USER_INFO_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", "OAUTH_WHITELIST")
|
||||||
|
viper.BindEnv("cookie-expiry", "COOKIE_EXPIRY")
|
||||||
|
viper.BindEnv("log-level", "LOG_LEVEL")
|
||||||
viper.BindPFlags(rootCmd.Flags())
|
viper.BindPFlags(rootCmd.Flags())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
@@ -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)")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,33 +42,30 @@ 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)
|
||||||
|
|
||||||
log.Info().Str("domain", domain).Msg("Using domain for cookies")
|
|
||||||
|
|
||||||
if domainErr != nil {
|
if domainErr != nil {
|
||||||
log.Fatal().Err(domainErr).Msg("Failed to get domain")
|
log.Fatal().Err(domainErr).Msg("Failed to get domain")
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
var isSecure bool
|
log.Info().Str("domain", domain).Msg("Using domain for cookies")
|
||||||
|
|
||||||
if api.Config.CookieSecure {
|
|
||||||
isSecure = true
|
|
||||||
} else {
|
|
||||||
isSecure = false
|
|
||||||
}
|
|
||||||
|
|
||||||
api.Domain = fmt.Sprintf(".%s", domain)
|
api.Domain = fmt.Sprintf(".%s", domain)
|
||||||
|
|
||||||
@@ -76,7 +73,8 @@ func (api *API) Init() {
|
|||||||
Domain: api.Domain,
|
Domain: api.Domain,
|
||||||
Path: "/",
|
Path: "/",
|
||||||
HttpOnly: true,
|
HttpOnly: true,
|
||||||
Secure: isSecure,
|
Secure: api.Config.CookieSecure,
|
||||||
|
MaxAge: api.Config.CookieExpiry,
|
||||||
})
|
})
|
||||||
|
|
||||||
router.Use(sessions.Sessions("tinyauth", store))
|
router.Use(sessions.Sessions("tinyauth", store))
|
||||||
@@ -97,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",
|
||||||
@@ -123,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{
|
||||||
@@ -149,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",
|
||||||
@@ -160,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",
|
||||||
@@ -167,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,
|
||||||
@@ -178,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)
|
||||||
|
|
||||||
@@ -191,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,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -249,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 {
|
||||||
@@ -259,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)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -279,49 +287,64 @@ func (api *API) SetupRoutes() {
|
|||||||
|
|
||||||
bindErr := c.BindUri(&providerName)
|
bindErr := c.BindUri(&providerName)
|
||||||
|
|
||||||
if bindErr != nil {
|
if handleApiError(c, "Failed to bind URI", bindErr) {
|
||||||
log.Error().Err(bindErr).Msg("Failed to bind URI")
|
|
||||||
c.JSON(400, gin.H{
|
|
||||||
"status": 400,
|
|
||||||
"message": "Bad Request",
|
|
||||||
})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.Debug().Interface("provider", providerName.Provider).Msg("Got provider name")
|
||||||
|
|
||||||
code := c.Query("code")
|
code := c.Query("code")
|
||||||
|
|
||||||
if code == "" {
|
if code == "" {
|
||||||
c.JSON(400, gin.H{
|
log.Error().Msg("No code provided")
|
||||||
"status": 400,
|
c.Redirect(http.StatusPermanentRedirect, "/error")
|
||||||
"message": "Bad Request",
|
|
||||||
})
|
|
||||||
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.JSON(404, gin.H{
|
c.Redirect(http.StatusPermanentRedirect, "/not-found")
|
||||||
"status": 404,
|
|
||||||
"message": "Not Found",
|
|
||||||
})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
token, tokenErr := provider.ExchangeToken(code)
|
_, tokenErr := provider.ExchangeToken(code)
|
||||||
|
|
||||||
if tokenErr != nil {
|
log.Debug().Msg("Got token")
|
||||||
log.Error().Err(tokenErr).Msg("Failed to exchange token")
|
|
||||||
c.JSON(500, gin.H{
|
if handleApiError(c, "Failed to exchange token", tokenErr) {
|
||||||
"status": 500,
|
|
||||||
"message": "Internal Server Error",
|
|
||||||
})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
session := sessions.Default(c)
|
email, emailErr := api.Providers.GetUser(providerName.Provider)
|
||||||
session.Set("tinyauth_sid", fmt.Sprintf("%s:%s", providerName.Provider, token))
|
|
||||||
session.Save()
|
log.Debug().Str("email", email).Msg("Got email")
|
||||||
|
|
||||||
|
if handleApiError(c, "Failed to get user", emailErr) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !api.Auth.EmailWhitelisted(email) {
|
||||||
|
log.Warn().Str("email", email).Msg("Email not whitelisted")
|
||||||
|
unauthorizedQuery, unauthorizedQueryErr := query.Values(types.UnauthorizedQuery{
|
||||||
|
Username: email,
|
||||||
|
})
|
||||||
|
if handleApiError(c, "Failed to build query", unauthorizedQueryErr) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.Redirect(http.StatusPermanentRedirect, fmt.Sprintf("%s/unauthorized?%s", api.Config.AppURL, unauthorizedQuery.Encode()))
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debug().Msg("Email whitelisted")
|
||||||
|
|
||||||
|
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")
|
||||||
|
|
||||||
@@ -332,22 +355,21 @@ 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)
|
||||||
|
|
||||||
queries, queryErr := query.Values(types.LoginQuery{
|
redirectQuery, redirectQueryErr := query.Values(types.LoginQuery{
|
||||||
RedirectURI: redirectURI,
|
RedirectURI: redirectURI,
|
||||||
})
|
})
|
||||||
|
|
||||||
if queryErr != nil {
|
log.Debug().Msg("Got redirect query")
|
||||||
log.Error().Err(queryErr).Msg("Failed to build query")
|
|
||||||
c.JSON(501, gin.H{
|
if handleApiError(c, "Failed to build query", redirectQueryErr) {
|
||||||
"status": 501,
|
|
||||||
"message": "Internal Server Error",
|
|
||||||
})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c.Redirect(http.StatusPermanentRedirect, fmt.Sprintf("%s/continue?%s", api.Config.AppURL, queries.Encode()))
|
c.Redirect(http.StatusPermanentRedirect, fmt.Sprintf("%s/continue?%s", api.Config.AppURL, redirectQuery.Encode()))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -379,3 +401,12 @@ func zerolog() gin.HandlerFunc {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func handleApiError(c *gin.Context, msg string, err error) bool {
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msg(msg)
|
||||||
|
c.Redirect(http.StatusPermanentRedirect, "/error")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
v1.0.0
|
v2.0.2
|
||||||
@@ -3,22 +3,27 @@ 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"
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewAuth(userList types.Users) *Auth {
|
func NewAuth(userList types.Users, oauthWhitelist []string) *Auth {
|
||||||
return &Auth{
|
return &Auth{
|
||||||
Users: userList,
|
Users: userList,
|
||||||
|
OAuthWhitelist: oauthWhitelist,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type Auth struct {
|
type Auth struct {
|
||||||
Users types.Users
|
Users types.Users
|
||||||
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -29,3 +34,58 @@ func (auth *Auth) CheckPassword(user types.User, password string) bool {
|
|||||||
hashedPasswordErr := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password))
|
hashedPasswordErr := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password))
|
||||||
return hashedPasswordErr == nil
|
return hashedPasswordErr == nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (auth *Auth) EmailWhitelisted(emailSrc string) bool {
|
||||||
|
if len(auth.OAuthWhitelist) == 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
for _, email := range auth.OAuthWhitelist {
|
||||||
|
if email == emailSrc {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -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,92 +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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return types.UserContext{
|
return types.UserContext{
|
||||||
Email: email,
|
Username: "",
|
||||||
IsLoggedIn: true,
|
IsLoggedIn: false,
|
||||||
OAuth: true,
|
OAuth: false,
|
||||||
Provider: sessionType,
|
Provider: "",
|
||||||
}, nil
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ func (providers *Providers) Init() {
|
|||||||
ClientID: providers.Config.GenericClientId,
|
ClientID: providers.Config.GenericClientId,
|
||||||
ClientSecret: providers.Config.GenericClientSecret,
|
ClientSecret: providers.Config.GenericClientSecret,
|
||||||
RedirectURL: fmt.Sprintf("%s/api/oauth/callback/generic", providers.Config.AppURL),
|
RedirectURL: fmt.Sprintf("%s/api/oauth/callback/generic", providers.Config.AppURL),
|
||||||
Scopes: []string{providers.Config.GenericScopes},
|
Scopes: providers.Config.GenericScopes,
|
||||||
Endpoint: oauth2.Endpoint{
|
Endpoint: oauth2.Endpoint{
|
||||||
AuthURL: providers.Config.GenericAuthURL,
|
AuthURL: providers.Config.GenericAuthURL,
|
||||||
TokenURL: providers.Config.GenericTokenURL,
|
TokenURL: providers.Config.GenericTokenURL,
|
||||||
@@ -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()
|
||||||
email, emailErr := GetGenericEmail(client, providers.Config.GenericUserInfoURL)
|
log.Debug().Msg("Got client from generic")
|
||||||
|
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
|
||||||
|
|||||||
@@ -7,40 +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"`
|
||||||
GenericUserInfoURL string `mapstructure:"generic-user-info-url"`
|
GenericClientSecretFile string `mapstructure:"generic-client-secret-file"`
|
||||||
DisableContinue bool `mapstructure:"disable-continue"`
|
GenericScopes string `mapstructure:"generic-scopes"`
|
||||||
|
GenericAuthURL string `mapstructure:"generic-auth-url"`
|
||||||
|
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
|
||||||
@@ -52,6 +59,7 @@ type APIConfig struct {
|
|||||||
Secret string
|
Secret string
|
||||||
AppURL string
|
AppURL string
|
||||||
CookieSecure bool
|
CookieSecure bool
|
||||||
|
CookieExpiry int
|
||||||
DisableContinue bool
|
DisableContinue bool
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,10 +70,10 @@ type OAuthConfig struct {
|
|||||||
GoogleClientSecret string
|
GoogleClientSecret string
|
||||||
GenericClientId string
|
GenericClientId string
|
||||||
GenericClientSecret string
|
GenericClientSecret string
|
||||||
GenericScopes string
|
GenericScopes []string
|
||||||
GenericAuthURL string
|
GenericAuthURL string
|
||||||
GenericTokenURL string
|
GenericTokenURL string
|
||||||
GenericUserInfoURL string
|
GenericUserURL string
|
||||||
AppURL string
|
AppURL string
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,3 +86,12 @@ type OAuthProviders struct {
|
|||||||
Google *oauth.OAuth
|
Google *oauth.OAuth
|
||||||
Microsoft *oauth.OAuth
|
Microsoft *oauth.OAuth
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type UnauthorizedQuery struct {
|
||||||
|
Username string `url:"username"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SessionCookie struct {
|
||||||
|
Username string
|
||||||
|
Provider string
|
||||||
|
}
|
||||||
|
|||||||
@@ -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,8 +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 GetSecret(conf string, file string) string {
|
||||||
|
if conf == "" && file == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
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 != "")
|
||||||
|
}
|
||||||
|
|||||||
4
main.go
4
main.go
@@ -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()
|
||||||
|
|||||||
BIN
site/bun.lockb
BIN
site/bun.lockb
Binary file not shown.
@@ -13,6 +13,8 @@ import { LoginPage } from "./pages/login-page.tsx";
|
|||||||
import { LogoutPage } from "./pages/logout-page.tsx";
|
import { LogoutPage } from "./pages/logout-page.tsx";
|
||||||
import { ContinuePage } from "./pages/continue-page.tsx";
|
import { ContinuePage } from "./pages/continue-page.tsx";
|
||||||
import { NotFoundPage } from "./pages/not-found-page.tsx";
|
import { NotFoundPage } from "./pages/not-found-page.tsx";
|
||||||
|
import { UnauthorizedPage } from "./pages/unauthorized-page.tsx";
|
||||||
|
import { InternalServerError } from "./pages/internal-server-error.tsx";
|
||||||
|
|
||||||
const queryClient = new QueryClient({
|
const queryClient = new QueryClient({
|
||||||
defaultOptions: {
|
defaultOptions: {
|
||||||
@@ -34,6 +36,8 @@ createRoot(document.getElementById("root")!).render(
|
|||||||
<Route path="/login" element={<LoginPage />} />
|
<Route path="/login" element={<LoginPage />} />
|
||||||
<Route path="/logout" element={<LogoutPage />} />
|
<Route path="/logout" element={<LogoutPage />} />
|
||||||
<Route path="/continue" element={<ContinuePage />} />
|
<Route path="/continue" element={<ContinuePage />} />
|
||||||
|
<Route path="/unauthorized" element={<UnauthorizedPage />} />
|
||||||
|
<Route path="/error" element={<InternalServerError />} />
|
||||||
<Route path="*" element={<NotFoundPage />} />
|
<Route path="*" element={<NotFoundPage />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
21
site/src/pages/internal-server-error.tsx
Normal file
21
site/src/pages/internal-server-error.tsx
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { Button, Paper, Text } from "@mantine/core";
|
||||||
|
import { Layout } from "../components/layouts/layout";
|
||||||
|
|
||||||
|
export const InternalServerError = () => {
|
||||||
|
return (
|
||||||
|
<Layout>
|
||||||
|
<Paper shadow="md" p={30} mt={30} radius="md" withBorder>
|
||||||
|
<Text size="xl" fw={700}>
|
||||||
|
Internal Server Error
|
||||||
|
</Text>
|
||||||
|
<Text>
|
||||||
|
An error occured on the server and it currently cannot serve your
|
||||||
|
request.
|
||||||
|
</Text>
|
||||||
|
<Button fullWidth mt="xl" onClick={() => window.location.replace("/")}>
|
||||||
|
Try again
|
||||||
|
</Button>
|
||||||
|
</Paper>
|
||||||
|
</Layout>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
41
site/src/pages/unauthorized-page.tsx
Normal file
41
site/src/pages/unauthorized-page.tsx
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import { Button, Code, Paper, Text } from "@mantine/core";
|
||||||
|
import { Layout } from "../components/layouts/layout";
|
||||||
|
import { useUserContext } from "../context/user-context";
|
||||||
|
import { Navigate } from "react-router";
|
||||||
|
|
||||||
|
export const UnauthorizedPage = () => {
|
||||||
|
const queryString = window.location.search;
|
||||||
|
const params = new URLSearchParams(queryString);
|
||||||
|
const username = params.get("username");
|
||||||
|
|
||||||
|
const { isLoggedIn } = useUserContext();
|
||||||
|
|
||||||
|
if (isLoggedIn) {
|
||||||
|
return <Navigate to="/" />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (username === "null") {
|
||||||
|
return <Navigate to="/" />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Layout>
|
||||||
|
<Paper shadow="md" p={30} mt={30} radius="md" withBorder>
|
||||||
|
<Text size="xl" fw={700}>
|
||||||
|
Unauthorized
|
||||||
|
</Text>
|
||||||
|
<Text>
|
||||||
|
The user with username <Code>{username}</Code> is not authorized to
|
||||||
|
login.
|
||||||
|
</Text>
|
||||||
|
<Button
|
||||||
|
fullWidth
|
||||||
|
mt="xl"
|
||||||
|
onClick={() => window.location.replace("/login")}
|
||||||
|
>
|
||||||
|
Try again
|
||||||
|
</Button>
|
||||||
|
</Paper>
|
||||||
|
</Layout>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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()),
|
||||||
|
|||||||
Reference in New Issue
Block a user