diff --git a/frontend/src/lib/i18n/locales/en-US.json b/frontend/src/lib/i18n/locales/en-US.json
index e0ca837..0eae0bc 100644
--- a/frontend/src/lib/i18n/locales/en-US.json
+++ b/frontend/src/lib/i18n/locales/en-US.json
@@ -42,6 +42,7 @@
"unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource {{resource}}.",
"unauthorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
"unauthorizedGroupsSubtitle": "The user with username {{username}} is not in the groups required by the resource {{resource}}.",
+ "unauthorizedIpSubtitle": "Your IP address {{ip}} is not authorized to access the resource {{resource}}.",
"unauthorizedButton": "Try again",
"untrustedRedirectTitle": "Untrusted redirect",
"untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain ({{domain}}). Are you sure you want to continue?",
diff --git a/frontend/src/lib/i18n/locales/en.json b/frontend/src/lib/i18n/locales/en.json
index e0ca837..0eae0bc 100644
--- a/frontend/src/lib/i18n/locales/en.json
+++ b/frontend/src/lib/i18n/locales/en.json
@@ -42,6 +42,7 @@
"unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource {{resource}}.",
"unauthorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
"unauthorizedGroupsSubtitle": "The user with username {{username}} is not in the groups required by the resource {{resource}}.",
+ "unauthorizedIpSubtitle": "Your IP address {{ip}} is not authorized to access the resource {{resource}}.",
"unauthorizedButton": "Try again",
"untrustedRedirectTitle": "Untrusted redirect",
"untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain ({{domain}}). Are you sure you want to continue?",
diff --git a/frontend/src/pages/unauthorized-page.tsx b/frontend/src/pages/unauthorized-page.tsx
index e41cad8..e0bd6ca 100644
--- a/frontend/src/pages/unauthorized-page.tsx
+++ b/frontend/src/pages/unauthorized-page.tsx
@@ -17,8 +17,9 @@ export const UnauthorizedPage = () => {
const username = searchParams.get("username");
const resource = searchParams.get("resource");
const groupErr = searchParams.get("groupErr");
+ const ip = searchParams.get("ip");
- if (!username) {
+ if (!username && !ip) {
return ;
}
@@ -41,6 +42,10 @@ export const UnauthorizedPage = () => {
i18nKey = "unauthorizedGroupsSubtitle";
}
+ if (ip) {
+ i18nKey = "unauthorizedIpSubtitle";
+ }
+
return (
@@ -55,6 +60,7 @@ export const UnauthorizedPage = () => {
values={{
username,
resource,
+ ip,
}}
/>
diff --git a/internal/auth/auth.go b/internal/auth/auth.go
index 9f09d24..55a6622 100644
--- a/internal/auth/auth.go
+++ b/internal/auth/auth.go
@@ -351,3 +351,44 @@ func (auth *Auth) GetBasicAuth(c *gin.Context) *types.User {
Password: password,
}
}
+
+func (auth *Auth) CheckIP(c *gin.Context, labels types.Labels) bool {
+ // Get the IP address from the request
+ ip := c.ClientIP()
+
+ // Check if the IP is in block list
+ for _, blocked := range labels.IP.Block {
+ res, err := utils.FilterIP(blocked, ip)
+ if err != nil {
+ log.Warn().Err(err).Str("item", blocked).Msg("Invalid IP/CIDR in block list")
+ continue
+ }
+ if res {
+ log.Warn().Str("ip", ip).Str("item", blocked).Msg("IP is in blocked list, denying access")
+ return false
+ }
+ }
+
+ // For every IP in the allow list, check if the IP matches
+ for _, allowed := range labels.IP.Allow {
+ res, err := utils.FilterIP(allowed, ip)
+ if err != nil {
+ log.Warn().Err(err).Str("item", allowed).Msg("Invalid IP/CIDR in allow list")
+ continue
+ }
+ if res {
+ log.Debug().Str("ip", ip).Str("item", allowed).Msg("IP is in allowed list, allowing access")
+ return true
+ }
+ }
+
+ // If not in allowed range and allowed range is not empty, deny access
+ if len(labels.IP.Allow) > 0 {
+ log.Warn().Str("ip", ip).Msg("IP not in allow list, denying access")
+ return false
+ }
+
+ log.Debug().Str("ip", ip).Msg("IP not in allow or block list, allowing by default")
+
+ return true
+}
diff --git a/internal/handlers/handlers.go b/internal/handlers/handlers.go
index 5ba49e0..aac5f17 100644
--- a/internal/handlers/handlers.go
+++ b/internal/handlers/handlers.go
@@ -96,6 +96,38 @@ func (h *Handlers) AuthHandler(c *gin.Context) {
return
}
+ // Check if the IP is allowed/blocked
+ ip := c.ClientIP()
+ if !h.Auth.CheckIP(c, labels) {
+ log.Warn().Str("ip", ip).Msg("IP not allowed")
+
+ if proxy.Proxy == "nginx" || !isBrowser {
+ c.JSON(403, gin.H{
+ "status": 403,
+ "message": "Forbidden",
+ })
+ return
+ }
+
+ values := types.UnauthorizedQuery{
+ Resource: strings.Split(host, ".")[0],
+ IP: ip,
+ }
+
+ // Build query
+ queries, err := query.Values(values)
+
+ // Handle error
+ if err != nil {
+ log.Error().Err(err).Msg("Failed to build queries")
+ c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", h.Config.AppURL))
+ return
+ }
+
+ c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/unauthorized?%s", h.Config.AppURL, queries.Encode()))
+ return
+ }
+
// Check if auth is enabled
authEnabled, err := h.Auth.AuthEnabled(c, labels)
diff --git a/internal/types/api.go b/internal/types/api.go
index 8e5d73a..fbf8bf7 100644
--- a/internal/types/api.go
+++ b/internal/types/api.go
@@ -21,6 +21,7 @@ type UnauthorizedQuery struct {
Username string `url:"username"`
Resource string `url:"resource"`
GroupErr bool `url:"groupErr"`
+ IP string `url:"ip"`
}
// Proxy is the uri parameters for the proxy endpoint
diff --git a/internal/types/config.go b/internal/types/config.go
index 80c6805..edb7123 100644
--- a/internal/types/config.go
+++ b/internal/types/config.go
@@ -105,6 +105,12 @@ type BasicLabels struct {
Password string
}
+// IP labels for a tinyauth protected container
+type IPLabels struct {
+ Allow []string
+ Block []string
+}
+
// Labels is a struct that contains the labels for a tinyauth protected container
type Labels struct {
Users string
@@ -113,4 +119,5 @@ type Labels struct {
Domain string
Basic BasicLabels
OAuth OAuthLabels
+ IP IPLabels
}
diff --git a/internal/utils/utils.go b/internal/utils/utils.go
index 39bd762..8209802 100644
--- a/internal/utils/utils.go
+++ b/internal/utils/utils.go
@@ -3,6 +3,7 @@ package utils
import (
"encoding/base64"
"errors"
+ "net"
"net/url"
"os"
"regexp"
@@ -202,7 +203,7 @@ func GetLabels(labels map[string]string) (types.Labels, error) {
var labelsParsed types.Labels
// Decode the labels into the labels struct
- err := parser.Decode(labels, &labelsParsed, "tinyauth", "tinyauth.users", "tinyauth.allowed", "tinyauth.headers", "tinyauth.domain", "tinyauth.basic", "tinyauth.oauth")
+ err := parser.Decode(labels, &labelsParsed, "tinyauth", "tinyauth.users", "tinyauth.allowed", "tinyauth.headers", "tinyauth.domain", "tinyauth.basic", "tinyauth.oauth", "tinyauth.ip")
// Check if there was an error
if err != nil {
@@ -368,3 +369,39 @@ func GetBasicAuth(username string, password string) string {
// Encode the auth string to base64
return base64.StdEncoding.EncodeToString([]byte(auth))
}
+
+// Check if an IP is contained in a CIDR range/matches a single IP
+func FilterIP(filter string, ip string) (bool, error) {
+ // Convert the check IP to an IP instance
+ ipAddr := net.ParseIP(ip)
+
+ // Check if the filter is a CIDR range
+ if strings.Contains(filter, "/") {
+ // Parse the CIDR range
+ _, cidr, err := net.ParseCIDR(filter)
+
+ // Check if there was an error
+ if err != nil {
+ return false, err
+ }
+
+ // Check if the IP is in the CIDR range
+ return cidr.Contains(ipAddr), nil
+ }
+
+ // Parse the filter as a single IP
+ ipFilter := net.ParseIP(filter)
+
+ // Check if the IP is valid
+ if ipFilter == nil {
+ return false, errors.New("invalid IP address in filter")
+ }
+
+ // Check if the IP matches the filter
+ if ipFilter.Equal(ipAddr) {
+ return true, nil
+ }
+
+ // If the filter is not a CIDR range or a single IP, return false
+ return false, nil
+}