mirror of
				https://github.com/steveiliop56/tinyauth.git
				synced 2025-11-04 08:05:42 +00:00 
			
		
		
		
	feat: add support for required oauth groups
This commit is contained in:
		@@ -42,6 +42,7 @@
 | 
			
		||||
    "unauthorizedTitle": "Unauthorized",
 | 
			
		||||
    "unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.",
 | 
			
		||||
    "unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.",
 | 
			
		||||
    "unauthorizedGroupsSubtitle": "The user with username <Code>{{username}}</Code> is not in the groups required by the resource <Code>{{resource}}</Code>.",
 | 
			
		||||
    "unauthorizedButton": "Try again",
 | 
			
		||||
    "untrustedRedirectTitle": "Untrusted redirect",
 | 
			
		||||
    "untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<Code>{{domain}}</Code>). Are you sure you want to continue?",
 | 
			
		||||
 
 | 
			
		||||
@@ -42,6 +42,7 @@
 | 
			
		||||
    "unauthorizedTitle": "Unauthorized",
 | 
			
		||||
    "unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.",
 | 
			
		||||
    "unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.",
 | 
			
		||||
    "unauthorizedGroupsSubtitle": "The user with username <Code>{{username}}</Code> is not in the groups required by the resource <Code>{{resource}}</Code>.",
 | 
			
		||||
    "unauthorizedButton": "Try again",
 | 
			
		||||
    "untrustedRedirectTitle": "Untrusted redirect",
 | 
			
		||||
    "untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<Code>{{domain}}</Code>). Are you sure you want to continue?",
 | 
			
		||||
 
 | 
			
		||||
@@ -3,11 +3,13 @@ import { Layout } from "../components/layouts/layout";
 | 
			
		||||
import { Navigate } from "react-router";
 | 
			
		||||
import { isQueryValid } from "../utils/utils";
 | 
			
		||||
import { Trans, useTranslation } from "react-i18next";
 | 
			
		||||
import React from "react";
 | 
			
		||||
 | 
			
		||||
export const UnauthorizedPage = () => {
 | 
			
		||||
  const queryString = window.location.search;
 | 
			
		||||
  const params = new URLSearchParams(queryString);
 | 
			
		||||
  const username = params.get("username") ?? "";
 | 
			
		||||
  const groupErr = params.get("groupErr") ?? "";
 | 
			
		||||
  const resource = params.get("resource") ?? "";
 | 
			
		||||
 | 
			
		||||
  const { t } = useTranslation();
 | 
			
		||||
@@ -16,6 +18,47 @@ export const UnauthorizedPage = () => {
 | 
			
		||||
    return <Navigate to="/" />;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (isQueryValid(resource) && !isQueryValid(groupErr)) {
 | 
			
		||||
    return (
 | 
			
		||||
      <UnauthorizedLayout>
 | 
			
		||||
          <Trans
 | 
			
		||||
            i18nKey="unauthorizedResourceSubtitle"
 | 
			
		||||
            t={t}
 | 
			
		||||
            components={{ Code: <Code /> }}
 | 
			
		||||
            values={{ resource, username }}
 | 
			
		||||
          />
 | 
			
		||||
      </UnauthorizedLayout>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (isQueryValid(groupErr) && isQueryValid(resource)) {
 | 
			
		||||
    return (
 | 
			
		||||
      <UnauthorizedLayout>
 | 
			
		||||
        <Trans
 | 
			
		||||
        i18nKey="unauthorizedGroupsSubtitle"
 | 
			
		||||
        t={t}
 | 
			
		||||
        components={{ Code: <Code /> }}
 | 
			
		||||
        values={{ username, resource }}
 | 
			
		||||
         />
 | 
			
		||||
      </UnauthorizedLayout>
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <UnauthorizedLayout>
 | 
			
		||||
      <Trans
 | 
			
		||||
        i18nKey="unaothorizedLoginSubtitle"
 | 
			
		||||
        t={t}
 | 
			
		||||
        components={{ Code: <Code /> }}
 | 
			
		||||
        values={{ username }}
 | 
			
		||||
      />
 | 
			
		||||
    </UnauthorizedLayout>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const UnauthorizedLayout = ({ children }: { children: React.ReactNode }) => {
 | 
			
		||||
  const { t } = useTranslation();
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Layout>
 | 
			
		||||
      <Paper shadow="md" p={30} mt={30} radius="md" withBorder>
 | 
			
		||||
@@ -23,25 +66,7 @@ export const UnauthorizedPage = () => {
 | 
			
		||||
          {t("Unauthorized")}
 | 
			
		||||
        </Text>
 | 
			
		||||
        <Text>
 | 
			
		||||
          {isQueryValid(resource) ? (
 | 
			
		||||
            <Text>
 | 
			
		||||
              <Trans
 | 
			
		||||
                i18nKey="unauthorizedResourceSubtitle"
 | 
			
		||||
                t={t}
 | 
			
		||||
                components={{ Code: <Code /> }}
 | 
			
		||||
                values={{ resource, username }}
 | 
			
		||||
              />
 | 
			
		||||
            </Text>
 | 
			
		||||
          ) : (
 | 
			
		||||
            <Text>
 | 
			
		||||
              <Trans
 | 
			
		||||
                i18nKey="unaothorizedLoginSubtitle"
 | 
			
		||||
                t={t}
 | 
			
		||||
                components={{ Code: <Code /> }}
 | 
			
		||||
                values={{ username }}
 | 
			
		||||
              />
 | 
			
		||||
            </Text>
 | 
			
		||||
          )}
 | 
			
		||||
        {children}
 | 
			
		||||
        </Text>
 | 
			
		||||
        <Button
 | 
			
		||||
          fullWidth
 | 
			
		||||
 
 | 
			
		||||
@@ -165,6 +165,7 @@ func (auth *Auth) CreateSessionCookie(c *gin.Context, data *types.SessionCookie)
 | 
			
		||||
	session.Values["provider"] = data.Provider
 | 
			
		||||
	session.Values["expiry"] = time.Now().Add(time.Duration(sessionExpiry) * time.Second).Unix()
 | 
			
		||||
	session.Values["totpPending"] = data.TotpPending
 | 
			
		||||
	session.Values["oauthGroups"] = data.OAuthGroups
 | 
			
		||||
 | 
			
		||||
	// Save session
 | 
			
		||||
	err = session.Save(c.Request, c.Writer)
 | 
			
		||||
@@ -213,7 +214,7 @@ func (auth *Auth) GetSessionCookie(c *gin.Context) (types.SessionCookie, error)
 | 
			
		||||
		return types.SessionCookie{}, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	log.Debug().Interface("session", session).Msg("Got session")
 | 
			
		||||
	log.Debug().Msg("Got session")
 | 
			
		||||
 | 
			
		||||
	// Get data from session
 | 
			
		||||
	username, usernameOk := session.Values["username"].(string)
 | 
			
		||||
@@ -222,8 +223,9 @@ func (auth *Auth) GetSessionCookie(c *gin.Context) (types.SessionCookie, error)
 | 
			
		||||
	provider, providerOK := session.Values["provider"].(string)
 | 
			
		||||
	expiry, expiryOk := session.Values["expiry"].(int64)
 | 
			
		||||
	totpPending, totpPendingOk := session.Values["totpPending"].(bool)
 | 
			
		||||
	oauthGroups, oauthGroupsOk := session.Values["oauthGroups"].(string)
 | 
			
		||||
 | 
			
		||||
	if !usernameOk || !providerOK || !expiryOk || !totpPendingOk || !emailOk || !nameOk {
 | 
			
		||||
	if !usernameOk || !providerOK || !expiryOk || !totpPendingOk || !emailOk || !nameOk || !oauthGroupsOk {
 | 
			
		||||
		log.Warn().Msg("Session cookie is invalid")
 | 
			
		||||
 | 
			
		||||
		// If any data is missing, delete the session cookie
 | 
			
		||||
@@ -244,7 +246,7 @@ func (auth *Auth) GetSessionCookie(c *gin.Context) (types.SessionCookie, error)
 | 
			
		||||
		return types.SessionCookie{}, nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	log.Debug().Str("username", username).Str("provider", provider).Int64("expiry", expiry).Bool("totpPending", totpPending).Str("name", name).Str("email", email).Msg("Parsed cookie")
 | 
			
		||||
	log.Debug().Str("username", username).Str("provider", provider).Int64("expiry", expiry).Bool("totpPending", totpPending).Str("name", name).Str("email", email).Str("oauthGroups", oauthGroups).Msg("Parsed cookie")
 | 
			
		||||
 | 
			
		||||
	// Return the cookie
 | 
			
		||||
	return types.SessionCookie{
 | 
			
		||||
@@ -253,6 +255,7 @@ func (auth *Auth) GetSessionCookie(c *gin.Context) (types.SessionCookie, error)
 | 
			
		||||
		Email:       email,
 | 
			
		||||
		Provider:    provider,
 | 
			
		||||
		TotpPending: totpPending,
 | 
			
		||||
		OAuthGroups: oauthGroups,
 | 
			
		||||
	}, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -261,48 +264,46 @@ func (auth *Auth) UserAuthConfigured() bool {
 | 
			
		||||
	return len(auth.Config.Users) > 0
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (auth *Auth) ResourceAllowed(c *gin.Context, context types.UserContext) (bool, error) {
 | 
			
		||||
	// Get headers
 | 
			
		||||
	host := c.Request.Header.Get("X-Forwarded-Host")
 | 
			
		||||
 | 
			
		||||
	// Get app id
 | 
			
		||||
	appId := strings.Split(host, ".")[0]
 | 
			
		||||
 | 
			
		||||
	// Get the container labels
 | 
			
		||||
	labels, err := auth.Docker.GetLabels(appId)
 | 
			
		||||
 | 
			
		||||
	// If there is an error, return false
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return false, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
func (auth *Auth) ResourceAllowed(c *gin.Context, context types.UserContext, labels types.TinyauthLabels) bool {
 | 
			
		||||
	// Check if oauth is allowed
 | 
			
		||||
	if context.OAuth {
 | 
			
		||||
		log.Debug().Msg("Checking OAuth whitelist")
 | 
			
		||||
		return utils.CheckWhitelist(labels.OAuthWhitelist, context.Username), nil
 | 
			
		||||
		return utils.CheckWhitelist(labels.OAuthWhitelist, context.Username)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Check users
 | 
			
		||||
	log.Debug().Msg("Checking users")
 | 
			
		||||
 | 
			
		||||
	return utils.CheckWhitelist(labels.Users, context.Username), nil
 | 
			
		||||
	return utils.CheckWhitelist(labels.Users, context.Username)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (auth *Auth) AuthEnabled(c *gin.Context) (bool, error) {
 | 
			
		||||
func (auth *Auth) OAuthGroup(c *gin.Context, context types.UserContext, labels types.TinyauthLabels) bool {
 | 
			
		||||
	// Check if groups are required
 | 
			
		||||
	if labels.OAuthGroups == "" {
 | 
			
		||||
		return true
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Split the groups by comma (no need to parse since they are from the API response)
 | 
			
		||||
	oauthGroups := strings.Split(context.OAuthGroups, ",")
 | 
			
		||||
 | 
			
		||||
	// For every group check if it is in the required groups
 | 
			
		||||
	for _, group := range oauthGroups {
 | 
			
		||||
		if utils.CheckWhitelist(labels.OAuthGroups, group) {
 | 
			
		||||
			log.Debug().Str("group", group).Msg("Group is in required groups")
 | 
			
		||||
			return true
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// No groups matched
 | 
			
		||||
	log.Debug().Msg("No groups matched")
 | 
			
		||||
 | 
			
		||||
	// Return false
 | 
			
		||||
	return false
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (auth *Auth) AuthEnabled(c *gin.Context, labels types.TinyauthLabels) (bool, error) {
 | 
			
		||||
	// Get headers
 | 
			
		||||
	uri := c.Request.Header.Get("X-Forwarded-Uri")
 | 
			
		||||
	host := c.Request.Header.Get("X-Forwarded-Host")
 | 
			
		||||
 | 
			
		||||
	// Get app id
 | 
			
		||||
	appId := strings.Split(host, ".")[0]
 | 
			
		||||
 | 
			
		||||
	// Get the container labels
 | 
			
		||||
	labels, err := auth.Docker.GetLabels(appId)
 | 
			
		||||
 | 
			
		||||
	// If there is an error, auth enabled
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return true, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Check if the allowed label is empty
 | 
			
		||||
	if labels.Allowed == "" {
 | 
			
		||||
 
 | 
			
		||||
@@ -6,6 +6,7 @@ var TinyauthLabels = []string{
 | 
			
		||||
	"tinyauth.users",
 | 
			
		||||
	"tinyauth.allowed",
 | 
			
		||||
	"tinyauth.headers",
 | 
			
		||||
	"tinyauth.oauth.groups",
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Claims are the OIDC supported claims (including preferd username for some reason)
 | 
			
		||||
@@ -13,4 +14,5 @@ type Claims struct {
 | 
			
		||||
	Name              string   `json:"name"`
 | 
			
		||||
	Email             string   `json:"email"`
 | 
			
		||||
	PreferredUsername string   `json:"preferred_username"`
 | 
			
		||||
	Groups            []string `json:"groups"`
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -69,12 +69,15 @@ func (h *Handlers) AuthHandler(c *gin.Context) {
 | 
			
		||||
	proto := c.Request.Header.Get("X-Forwarded-Proto")
 | 
			
		||||
	host := c.Request.Header.Get("X-Forwarded-Host")
 | 
			
		||||
 | 
			
		||||
	// Check if auth is enabled
 | 
			
		||||
	authEnabled, err := h.Auth.AuthEnabled(c)
 | 
			
		||||
	// Get the app id
 | 
			
		||||
	appId := strings.Split(host, ".")[0]
 | 
			
		||||
 | 
			
		||||
	// Get the container labels
 | 
			
		||||
	labels, err := h.Docker.GetLabels(appId)
 | 
			
		||||
 | 
			
		||||
	// Check if there was an error
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Error().Err(err).Msg("Failed to check if app is allowed")
 | 
			
		||||
		log.Error().Err(err).Msg("Failed to get container labels")
 | 
			
		||||
 | 
			
		||||
		if proxy.Proxy == "nginx" || !isBrowser {
 | 
			
		||||
			c.JSON(500, gin.H{
 | 
			
		||||
@@ -88,11 +91,8 @@ func (h *Handlers) AuthHandler(c *gin.Context) {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Get the app id
 | 
			
		||||
	appId := strings.Split(host, ".")[0]
 | 
			
		||||
 | 
			
		||||
	// Get the container labels
 | 
			
		||||
	labels, err := h.Docker.GetLabels(appId)
 | 
			
		||||
	// Check if auth is enabled
 | 
			
		||||
	authEnabled, err := h.Auth.AuthEnabled(c, labels)
 | 
			
		||||
 | 
			
		||||
	// Check if there was an error
 | 
			
		||||
	if err != nil {
 | 
			
		||||
@@ -131,23 +131,7 @@ func (h *Handlers) AuthHandler(c *gin.Context) {
 | 
			
		||||
		log.Debug().Msg("Authenticated")
 | 
			
		||||
 | 
			
		||||
		// Check if user is allowed to access subdomain, if request is nginx.example.com the subdomain (resource) is nginx
 | 
			
		||||
		appAllowed, err := h.Auth.ResourceAllowed(c, userContext)
 | 
			
		||||
 | 
			
		||||
		// Check if there was an error
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Error().Err(err).Msg("Failed to check if app is allowed")
 | 
			
		||||
 | 
			
		||||
			if proxy.Proxy == "nginx" || !isBrowser {
 | 
			
		||||
				c.JSON(500, gin.H{
 | 
			
		||||
					"status":  500,
 | 
			
		||||
					"message": "Internal Server Error",
 | 
			
		||||
				})
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			c.Redirect(http.StatusPermanentRedirect, fmt.Sprintf("%s/error", h.Config.AppURL))
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		appAllowed := h.Auth.ResourceAllowed(c, userContext, labels)
 | 
			
		||||
 | 
			
		||||
		log.Debug().Bool("appAllowed", appAllowed).Msg("Checking if app is allowed")
 | 
			
		||||
 | 
			
		||||
@@ -184,9 +168,51 @@ func (h *Handlers) AuthHandler(c *gin.Context) {
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		log.Debug().Interface("labels", labels).Msg("Got labels")
 | 
			
		||||
 | 
			
		||||
		// Check if user is in required groups
 | 
			
		||||
		groupOk := h.Auth.OAuthGroup(c, userContext, labels)
 | 
			
		||||
 | 
			
		||||
		log.Debug().Bool("groupOk", groupOk).Msg("Checking if user is in required groups")
 | 
			
		||||
 | 
			
		||||
		// The user is not allowed to access the app
 | 
			
		||||
		if !groupOk {
 | 
			
		||||
			log.Warn().Str("username", userContext.Username).Str("host", host).Msg("User is not in required groups")
 | 
			
		||||
 | 
			
		||||
			// Set WWW-Authenticate header
 | 
			
		||||
			c.Header("WWW-Authenticate", "Basic realm=\"tinyauth\"")
 | 
			
		||||
 | 
			
		||||
			if proxy.Proxy == "nginx" || !isBrowser {
 | 
			
		||||
				c.JSON(401, gin.H{
 | 
			
		||||
					"status":  401,
 | 
			
		||||
					"message": "Unauthorized",
 | 
			
		||||
				})
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			// Build query
 | 
			
		||||
			queries, err := query.Values(types.UnauthorizedQuery{
 | 
			
		||||
				Username: userContext.Username,
 | 
			
		||||
				Resource: strings.Split(host, ".")[0],
 | 
			
		||||
				GroupErr: true,
 | 
			
		||||
			})
 | 
			
		||||
 | 
			
		||||
			// Handle error (no need to check for nginx/headers since we are sure we are using caddy/traefik)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				log.Error().Err(err).Msg("Failed to build queries")
 | 
			
		||||
				c.Redirect(http.StatusPermanentRedirect, fmt.Sprintf("%s/error", h.Config.AppURL))
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			// We are using caddy/traefik so redirect
 | 
			
		||||
			c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/unauthorized?%s", h.Config.AppURL, queries.Encode()))
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		c.Header("Remote-User", userContext.Username)
 | 
			
		||||
		c.Header("Remote-Name", userContext.Name)
 | 
			
		||||
		c.Header("Remote-Email", userContext.Email)
 | 
			
		||||
		c.Header("Remote-Groups", userContext.OAuthGroups)
 | 
			
		||||
 | 
			
		||||
		// Set the rest of the headers
 | 
			
		||||
		for key, value := range labels.Headers {
 | 
			
		||||
@@ -688,6 +714,7 @@ func (h *Handlers) OauthCallbackHandler(c *gin.Context) {
 | 
			
		||||
		Name:        name,
 | 
			
		||||
		Email:       user.Email,
 | 
			
		||||
		Provider:    providerName.Provider,
 | 
			
		||||
		OAuthGroups: strings.Join(user.Groups, ","),
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	// Check if we have a redirect URI
 | 
			
		||||
 
 | 
			
		||||
@@ -120,6 +120,7 @@ func (hooks *Hooks) UseUserContext(c *gin.Context) types.UserContext {
 | 
			
		||||
			IsLoggedIn:  true,
 | 
			
		||||
			OAuth:       true,
 | 
			
		||||
			Provider:    cookie.Provider,
 | 
			
		||||
			OAuthGroups: cookie.OAuthGroups,
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -20,6 +20,7 @@ type OAuthRequest struct {
 | 
			
		||||
type UnauthorizedQuery struct {
 | 
			
		||||
	Username string `url:"username"`
 | 
			
		||||
	Resource string `url:"resource"`
 | 
			
		||||
	GroupErr bool   `url:"groupErr"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Proxy is the uri parameters for the proxy endpoint
 | 
			
		||||
 
 | 
			
		||||
@@ -29,6 +29,7 @@ type SessionCookie struct {
 | 
			
		||||
	Email       string
 | 
			
		||||
	Provider    string
 | 
			
		||||
	TotpPending bool
 | 
			
		||||
	OAuthGroups string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// TinyauthLabels is the labels for the tinyauth container
 | 
			
		||||
@@ -37,6 +38,7 @@ type TinyauthLabels struct {
 | 
			
		||||
	Users          string
 | 
			
		||||
	Allowed        string
 | 
			
		||||
	Headers        map[string]string
 | 
			
		||||
	OAuthGroups    string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// UserContext is the context for the user
 | 
			
		||||
@@ -48,6 +50,7 @@ type UserContext struct {
 | 
			
		||||
	OAuth       bool
 | 
			
		||||
	Provider    string
 | 
			
		||||
	TotpPending bool
 | 
			
		||||
	OAuthGroups string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// LoginAttempt tracks information about login attempts for rate limiting
 | 
			
		||||
 
 | 
			
		||||
@@ -204,6 +204,8 @@ func GetTinyauthLabels(labels map[string]string) types.TinyauthLabels {
 | 
			
		||||
					}
 | 
			
		||||
					tinyauthLabels.Headers[headerSplit[0]] = headerSplit[1]
 | 
			
		||||
				}
 | 
			
		||||
			case "tinyauth.oauth.groups":
 | 
			
		||||
				tinyauthLabels.OAuthGroups = value
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user