diff --git a/README.md b/README.md
index 19bf770501df77a7bbffebc65129d6cf633e64e1..271376d65e931d516edfb0d1433ccd55662b3c73 100644
--- a/README.md
+++ b/README.md
@@ -79,7 +79,7 @@ Dex implements the following connectors:
 | [Bitbucket Cloud](https://dexidp.io/docs/connectors/bitbucketcloud/) | yes | yes | no | alpha | |
 | [OpenShift](https://dexidp.io/docs/connectors/openshift/) | no | yes | no | alpha | |
 | [Atlassian Crowd](https://dexidp.io/docs/connectors/atlassiancrowd/) | yes | yes | yes * | beta | preferred_username claim must be configured through config |
-| [Gitea](https://dexidp.io/docs/connectors/gitea/) | yes | no | yes | alpha | |
+| [Gitea](https://dexidp.io/docs/connectors/gitea/) | yes | no | yes | beta | |
 | [OpenStack Keystone](https://dexidp.io/docs/connectors/keystone/) | yes | yes | no | alpha | |
 
 Stable, beta, and alpha are defined as:
diff --git a/connector/gitea/gitea.go b/connector/gitea/gitea.go
index cd371d37416319799a61bc103b20cded9640363f..6b02099414d2391af86956fadcb9152c6a5ff39b 100644
--- a/connector/gitea/gitea.go
+++ b/connector/gitea/gitea.go
@@ -20,11 +20,26 @@ import (
 
 // Config holds configuration options for gitea logins.
 type Config struct {
-	BaseURL      string `json:"baseURL"`
-	ClientID     string `json:"clientID"`
-	ClientSecret string `json:"clientSecret"`
-	RedirectURI  string `json:"redirectURI"`
-	UseLoginAsID bool   `json:"useLoginAsID"`
+	BaseURL       string `json:"baseURL"`
+	ClientID      string `json:"clientID"`
+	ClientSecret  string `json:"clientSecret"`
+	RedirectURI   string `json:"redirectURI"`
+	Orgs          []Org  `json:"orgs"`
+	LoadAllGroups bool   `json:"loadAllGroups"`
+	UseLoginAsID  bool   `json:"useLoginAsID"`
+}
+
+// Org holds org-team filters, in which teams are optional.
+type Org struct {
+	// Organization name in gitea (not slug, full name). Only users in this gitea
+	// organization can authenticate.
+	Name string `json:"name"`
+
+	// Names of teams in a gitea organization. A user will be able to
+	// authenticate if they are members of at least one of these teams. Users
+	// in the organization can authenticate if this field is omitted from the
+	// config file.
+	Teams []string `json:"teams,omitempty"`
 }
 
 type giteaUser struct {
@@ -35,18 +50,20 @@ type giteaUser struct {
 	IsAdmin  bool   `json:"is_admin"`
 }
 
-// Open returns a strategy for logging in through GitLab.
+// Open returns a strategy for logging in through Gitea
 func (c *Config) Open(id string, logger log.Logger) (connector.Connector, error) {
 	if c.BaseURL == "" {
 		c.BaseURL = "https://gitea.com"
 	}
 	return &giteaConnector{
-		baseURL:      c.BaseURL,
-		redirectURI:  c.RedirectURI,
-		clientID:     c.ClientID,
-		clientSecret: c.ClientSecret,
-		logger:       logger,
-		useLoginAsID: c.UseLoginAsID,
+		baseURL:       c.BaseURL,
+		redirectURI:   c.RedirectURI,
+		orgs:          c.Orgs,
+		clientID:      c.ClientID,
+		clientSecret:  c.ClientSecret,
+		logger:        logger,
+		loadAllGroups: c.LoadAllGroups,
+		useLoginAsID:  c.UseLoginAsID,
 	}, nil
 }
 
@@ -64,10 +81,13 @@ var (
 type giteaConnector struct {
 	baseURL      string
 	redirectURI  string
+	orgs         []Org
 	clientID     string
 	clientSecret string
 	logger       log.Logger
 	httpClient   *http.Client
+	// if set to true and no orgs are configured then connector loads all user claims (all orgs and team)
+	loadAllGroups bool
 	// if set to true will use the user's handle rather than their numeric id as the ID
 	useLoginAsID bool
 }
@@ -130,6 +150,7 @@ func (c *giteaConnector) HandleCallback(s connector.Scopes, r *http.Request) (id
 	if username == "" {
 		username = user.Email
 	}
+
 	identity = connector.Identity{
 		UserID:            strconv.Itoa(user.ID),
 		Username:          username,
@@ -141,6 +162,15 @@ func (c *giteaConnector) HandleCallback(s connector.Scopes, r *http.Request) (id
 		identity.UserID = user.Username
 	}
 
+	// Only set identity.Groups if 'orgs', 'org', or 'groups' scope are specified.
+	if c.groupsRequired() {
+		groups, err := c.getGroups(ctx, client)
+		if err != nil {
+			return identity, err
+		}
+		identity.Groups = groups
+	}
+
 	if s.OfflineAccess {
 		data := connectorData{
 			AccessToken:  token.AccessToken,
@@ -232,9 +262,132 @@ func (c *giteaConnector) Refresh(ctx context.Context, s connector.Scopes, ident
 	ident.PreferredUsername = user.Username
 	ident.Email = user.Email
 
+	// Only set identity.Groups if 'orgs', 'org', or 'groups' scope are specified.
+	if c.groupsRequired() {
+		groups, err := c.getGroups(ctx, client)
+		if err != nil {
+			return ident, err
+		}
+		ident.Groups = groups
+	}
+
 	return ident, nil
 }
 
+// getGroups retrieves Gitea orgs and teams a user is in, if any.
+func (c *giteaConnector) getGroups(ctx context.Context, client *http.Client) ([]string, error) {
+	if len(c.orgs) > 0 {
+		return c.groupsForOrgs(ctx, client)
+	} else if c.loadAllGroups {
+		return c.userGroups(ctx, client)
+	}
+	return nil, nil
+}
+
+// formatTeamName returns unique team name.
+// Orgs might have the same team names. To make team name unique it should be prefixed with the org name.
+func formatTeamName(org string, team string) string {
+	return fmt.Sprintf("%s:%s", org, team)
+}
+
+// groupsForOrgs returns list of groups that user belongs to in approved list
+func (c *giteaConnector) groupsForOrgs(ctx context.Context, client *http.Client) ([]string, error) {
+	groups, err := c.userGroups(ctx, client)
+	if err != nil {
+		return groups, err
+	}
+
+	keys := make(map[string]bool)
+	for _, o := range c.orgs {
+		keys[o.Name] = true
+		if o.Teams != nil {
+			for _, t := range o.Teams {
+				keys[formatTeamName(o.Name, t)] = true
+			}
+		}
+	}
+	atLeastOne := false
+	filteredGroups := make([]string, 0)
+	for _, g := range groups {
+		if _, value := keys[g]; value {
+			filteredGroups = append(filteredGroups, g)
+			atLeastOne = true
+		}
+	}
+
+	if !atLeastOne {
+		return []string{}, fmt.Errorf("gitea: User does not belong to any of the approved groups")
+	}
+	return filteredGroups, nil
+}
+
+type organization struct {
+	ID   int64  `json:"id"`
+	Name string `json:"username"`
+}
+
+type team struct {
+	ID           int64         `json:"id"`
+	Name         string        `json:"name"`
+	Organization *organization `json:"organization"`
+}
+
+func (c *giteaConnector) userGroups(ctx context.Context, client *http.Client) ([]string, error) {
+	apiURL := c.baseURL + "/api/v1/user/teams"
+	groups := make([]string, 0)
+	page := 1
+	limit := 20
+	for {
+		var teams []team
+		req, err := http.NewRequest("GET", fmt.Sprintf("%s?page=%d&limit=%d", apiURL, page, limit), nil)
+		if err != nil {
+			return groups, fmt.Errorf("gitea: new req: %v", err)
+		}
+
+		req = req.WithContext(ctx)
+		resp, err := client.Do(req)
+		if err != nil {
+			return groups, fmt.Errorf("gitea: get URL %v", err)
+		}
+		defer resp.Body.Close()
+
+		if resp.StatusCode != http.StatusOK {
+			body, err := io.ReadAll(resp.Body)
+			if err != nil {
+				return groups, fmt.Errorf("gitea: read body: %v", err)
+			}
+			return groups, fmt.Errorf("%s: %s", resp.Status, body)
+		}
+
+		if err := json.NewDecoder(resp.Body).Decode(&teams); err != nil {
+			return groups, fmt.Errorf("failed to decode response: %v", err)
+		}
+
+		if len(teams) == 0 {
+			break
+		}
+
+		for _, t := range teams {
+			groups = append(groups, t.Organization.Name)
+			groups = append(groups, formatTeamName(t.Organization.Name, t.Name))
+		}
+
+		page++
+	}
+
+	// remove duplicate slice variables
+	keys := make(map[string]struct{})
+	list := []string{}
+	for _, group := range groups {
+		if _, exists := keys[group]; !exists {
+			keys[group] = struct{}{}
+			list = append(list, group)
+		}
+	}
+	groups = list
+	return groups, nil
+}
+
 // user queries the Gitea API for profile information using the provided client. The HTTP
 // client is expected to be constructed by the golang.org/x/oauth2 package, which inserts
 // a bearer token as part of the request.
@@ -264,3 +417,8 @@ func (c *giteaConnector) user(ctx context.Context, client *http.Client) (giteaUs
 	}
 	return u, nil
 }
+
+// groupsRequired returns whether dex needs to request groups from Gitea.
+func (c *giteaConnector) groupsRequired() bool {
+	return len(c.orgs) > 0 || c.loadAllGroups
+}