mirror of
				https://github.com/steveiliop56/tinyauth.git
				synced 2025-11-03 23:55:44 +00:00 
			
		
		
		
	Compare commits
	
		
			10 Commits
		
	
	
		
			v1.0.0-alp
			...
			v1.0.0-bet
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					e8d2e059a9 | ||
| 
						 | 
					2c7a3fc801 | ||
| 
						 | 
					61fffb9708 | ||
| 
						 | 
					9d2aef163b | ||
| 
						 | 
					cc480085c5 | ||
| 
						 | 
					2c7144937a | ||
| 
						 | 
					c7ec788ce1 | ||
| 
						 | 
					96a373a794 | ||
| 
						 | 
					c5a8639822 | ||
| 
						 | 
					b87cb54d91 | 
@@ -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 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.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## 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.
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										18
									
								
								cmd/root.go
									
									
									
									
									
								
							
							
						
						
									
										18
									
								
								cmd/root.go
									
									
									
									
									
								
							@@ -56,6 +56,9 @@ var rootCmd = &cobra.Command{
 | 
				
			|||||||
		users, parseErr := utils.ParseUsers(usersString)
 | 
							users, parseErr := utils.ParseUsers(usersString)
 | 
				
			||||||
		HandleError(parseErr, "Failed to parse users")
 | 
							HandleError(parseErr, "Failed to parse users")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Create oauth whitelist
 | 
				
			||||||
 | 
							oauthWhitelist := utils.ParseCommaString(config.OAuthWhitelist)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// Create OAuth config
 | 
							// Create OAuth config
 | 
				
			||||||
		oauthConfig := types.OAuthConfig{
 | 
							oauthConfig := types.OAuthConfig{
 | 
				
			||||||
			GithubClientId:      config.GithubClientId,
 | 
								GithubClientId:      config.GithubClientId,
 | 
				
			||||||
@@ -64,15 +67,15 @@ 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:       utils.ParseCommaString(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,
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// 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 +94,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
 | 
				
			||||||
@@ -134,8 +138,10 @@ func init() {
 | 
				
			|||||||
	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.")
 | 
				
			||||||
	viper.BindEnv("port", "PORT")
 | 
						viper.BindEnv("port", "PORT")
 | 
				
			||||||
	viper.BindEnv("address", "ADDRESS")
 | 
						viper.BindEnv("address", "ADDRESS")
 | 
				
			||||||
	viper.BindEnv("secret", "SECRET")
 | 
						viper.BindEnv("secret", "SECRET")
 | 
				
			||||||
@@ -152,7 +158,9 @@ func init() {
 | 
				
			|||||||
	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", "WHITELIST")
 | 
				
			||||||
 | 
						viper.BindEnv("cookie-expiry", "COOKIE_EXPIRY")
 | 
				
			||||||
	viper.BindPFlags(rootCmd.Flags())
 | 
						viper.BindPFlags(rootCmd.Flags())
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -55,20 +55,12 @@ func (api *API) Init() {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	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 +68,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))
 | 
				
			||||||
@@ -279,46 +272,48 @@ 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
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		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
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		provider := api.Providers.GetProvider(providerName.Provider)
 | 
							provider := api.Providers.GetProvider(providerName.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)
 | 
							token, tokenErr := provider.ExchangeToken(code)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if tokenErr != nil {
 | 
							if handleApiError(c, "Failed to exchange token", tokenErr) {
 | 
				
			||||||
			log.Error().Err(tokenErr).Msg("Failed to exchange token")
 | 
					 | 
				
			||||||
			c.JSON(500, gin.H{
 | 
					 | 
				
			||||||
				"status":  500,
 | 
					 | 
				
			||||||
				"message": "Internal Server Error",
 | 
					 | 
				
			||||||
			})
 | 
					 | 
				
			||||||
			return
 | 
								return
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							email, emailErr := api.Providers.GetUser(providerName.Provider)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							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{
 | 
				
			||||||
 | 
									Email: email,
 | 
				
			||||||
 | 
								})
 | 
				
			||||||
 | 
								if handleApiError(c, "Failed to build query", unauthorizedQueryErr) {
 | 
				
			||||||
 | 
									return
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								c.Redirect(http.StatusPermanentRedirect, fmt.Sprintf("%s/unauthorized?%s", api.Config.AppURL, unauthorizedQuery.Encode()))
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		session := sessions.Default(c)
 | 
							session := sessions.Default(c)
 | 
				
			||||||
		session.Set("tinyauth_sid", fmt.Sprintf("%s:%s", providerName.Provider, token))
 | 
							session.Set("tinyauth_sid", fmt.Sprintf("%s:%s", providerName.Provider, token))
 | 
				
			||||||
		session.Save()
 | 
							session.Save()
 | 
				
			||||||
@@ -334,20 +329,15 @@ func (api *API) SetupRoutes() {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
		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 {
 | 
							if handleApiError(c, "Failed to build query", redirectQueryErr) {
 | 
				
			||||||
			log.Error().Err(queryErr).Msg("Failed to build query")
 | 
					 | 
				
			||||||
			c.JSON(501, gin.H{
 | 
					 | 
				
			||||||
				"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 +369,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
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -6,14 +6,16 @@ import (
 | 
				
			|||||||
	"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(email string) *types.User {
 | 
				
			||||||
@@ -29,3 +31,15 @@ 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
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -105,6 +105,17 @@ func (hooks *Hooks) UseUserContext(c *gin.Context) (types.UserContext, error) {
 | 
				
			|||||||
		}, nil
 | 
							}, 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,
 | 
							Email:      email,
 | 
				
			||||||
		IsLoggedIn: true,
 | 
							IsLoggedIn: true,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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,
 | 
				
			||||||
@@ -102,7 +102,7 @@ func (providers *Providers) GetUser(provider string) (string, error) {
 | 
				
			|||||||
			return "", nil
 | 
								return "", nil
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		client := providers.Generic.GetClient()
 | 
							client := providers.Generic.GetClient()
 | 
				
			||||||
		email, emailErr := GetGenericEmail(client, providers.Config.GenericUserInfoURL)
 | 
							email, emailErr := GetGenericEmail(client, providers.Config.GenericUserURL)
 | 
				
			||||||
		if emailErr != nil {
 | 
							if emailErr != nil {
 | 
				
			||||||
			return "", emailErr
 | 
								return "", emailErr
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -35,8 +35,10 @@ type Config struct {
 | 
				
			|||||||
	GenericScopes       string `mapstructure:"generic-scopes"`
 | 
						GenericScopes       string `mapstructure:"generic-scopes"`
 | 
				
			||||||
	GenericAuthURL      string `mapstructure:"generic-auth-url"`
 | 
						GenericAuthURL      string `mapstructure:"generic-auth-url"`
 | 
				
			||||||
	GenericTokenURL     string `mapstructure:"generic-token-url"`
 | 
						GenericTokenURL     string `mapstructure:"generic-token-url"`
 | 
				
			||||||
	GenericUserInfoURL  string `mapstructure:"generic-user-info-url"`
 | 
						GenericUserURL      string `mapstructure:"generic-user-info-url"`
 | 
				
			||||||
	DisableContinue     bool   `mapstructure:"disable-continue"`
 | 
						DisableContinue     bool   `mapstructure:"disable-continue"`
 | 
				
			||||||
 | 
						OAuthWhitelist      string `mapstructure:"oauth-whitelist"`
 | 
				
			||||||
 | 
						CookieExpiry        int    `mapstructure:"cookie-expiry"`
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type UserContext struct {
 | 
					type UserContext struct {
 | 
				
			||||||
@@ -52,6 +54,7 @@ type APIConfig struct {
 | 
				
			|||||||
	Secret          string
 | 
						Secret          string
 | 
				
			||||||
	AppURL          string
 | 
						AppURL          string
 | 
				
			||||||
	CookieSecure    bool
 | 
						CookieSecure    bool
 | 
				
			||||||
 | 
						CookieExpiry    int
 | 
				
			||||||
	DisableContinue bool
 | 
						DisableContinue bool
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -62,10 +65,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 +81,7 @@ type OAuthProviders struct {
 | 
				
			|||||||
	Google    *oauth.OAuth
 | 
						Google    *oauth.OAuth
 | 
				
			||||||
	Microsoft *oauth.OAuth
 | 
						Microsoft *oauth.OAuth
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type UnauthorizedQuery struct {
 | 
				
			||||||
 | 
						Email string `url:"email"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -74,3 +74,10 @@ func ParseFileToLine(content string) string {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	return strings.Join(users, ",")
 | 
						return strings.Join(users, ",")
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func ParseCommaString(str string) []string {
 | 
				
			||||||
 | 
						if str == "" {
 | 
				
			||||||
 | 
							return []string{}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return strings.Split(str, ",")
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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>
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										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>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
							
								
								
									
										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 email = params.get("email");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const { isLoggedIn } = useUserContext();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (isLoggedIn) {
 | 
				
			||||||
 | 
					    return <Navigate to="/" />;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (email === "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 email address <Code>{email}</Code> is not authorized to
 | 
				
			||||||
 | 
					          login.
 | 
				
			||||||
 | 
					        </Text>
 | 
				
			||||||
 | 
					        <Button
 | 
				
			||||||
 | 
					          fullWidth
 | 
				
			||||||
 | 
					          mt="xl"
 | 
				
			||||||
 | 
					          onClick={() => window.location.replace("/login")}
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          Try again
 | 
				
			||||||
 | 
					        </Button>
 | 
				
			||||||
 | 
					      </Paper>
 | 
				
			||||||
 | 
					    </Layout>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
		Reference in New Issue
	
	Block a user