diff --git a/routers/web/user/oauth.go b/routers/web/user/oauth.go index d9fc5eeaf9..642a7f33b0 100644 --- a/routers/web/user/oauth.go +++ b/routers/web/user/oauth.go @@ -207,6 +207,17 @@ func newAccessTokenResponse(grant *login.OAuth2Grant, serverKey, clientKey oauth idToken.Email = user.Email idToken.EmailVerified = user.IsActive } + if grant.ScopeContains("groups") { + groups, err := getOAuthGroupsForUser(user) + if err != nil { + log.Error("Error getting groups: %v", err) + return nil, &AccessTokenError{ + ErrorCode: AccessTokenErrorCodeInvalidRequest, + ErrorDescription: "server error", + } + } + idToken.Groups = groups + } signedIDToken, err = idToken.SignToken(clientKey) if err != nil { @@ -227,11 +238,12 @@ func newAccessTokenResponse(grant *login.OAuth2Grant, serverKey, clientKey oauth } type userInfoResponse struct { - Sub string `json:"sub"` - Name string `json:"name"` - Username string `json:"preferred_username"` - Email string `json:"email"` - Picture string `json:"picture"` + Sub string `json:"sub"` + Name string `json:"name"` + Username string `json:"preferred_username"` + Email string `json:"email"` + Picture string `json:"picture"` + Groups []string `json:"groups"` } // InfoOAuth manages request for userinfo endpoint @@ -241,6 +253,7 @@ func InfoOAuth(ctx *context.Context) { ctx.HandleText(http.StatusUnauthorized, "no valid authorization") return } + response := &userInfoResponse{ Sub: fmt.Sprint(ctx.User.ID), Name: ctx.User.FullName, @@ -248,9 +261,41 @@ func InfoOAuth(ctx *context.Context) { Email: ctx.User.Email, Picture: ctx.User.AvatarLink(), } + + groups, err := getOAuthGroupsForUser(ctx.User) + if err != nil { + ctx.ServerError("Oauth groups for user", err) + return + } + response.Groups = groups + ctx.JSON(http.StatusOK, response) } +// returns a list of "org" and "org:team" strings, +// that the given user is a part of. +func getOAuthGroupsForUser(user *models.User) ([]string, error) { + orgs, err := models.GetUserOrgsList(user) + if err != nil { + return nil, fmt.Errorf("GetUserOrgList: %v", err) + } + + var groups []string + for _, org := range orgs { + groups = append(groups, org.Name) + + if err := org.LoadTeams(); err != nil { + return nil, fmt.Errorf("LoadTeams: %v", err) + } + for _, team := range org.Teams { + if team.IsMember(user.ID) { + groups = append(groups, org.Name+":"+team.LowerName) + } + } + } + return groups, nil +} + // IntrospectOAuth introspects an oauth token func IntrospectOAuth(ctx *context.Context) { if ctx.User == nil { diff --git a/services/auth/source/oauth2/token.go b/services/auth/source/oauth2/token.go index 16d1220842..0c7c5d8caa 100644 --- a/services/auth/source/oauth2/token.go +++ b/services/auth/source/oauth2/token.go @@ -83,6 +83,9 @@ type OIDCToken struct { // Scope email Email string `json:"email,omitempty"` EmailVerified bool `json:"email_verified,omitempty"` + + // Groups are generated by organization and team names + Groups []string `json:"groups,omitempty"` } // SignToken signs an id_token with the (symmetric) client secret key diff --git a/templates/user/auth/oidc_wellknown.tmpl b/templates/user/auth/oidc_wellknown.tmpl index d4cbf7dfec..38e6900c38 100644 --- a/templates/user/auth/oidc_wellknown.tmpl +++ b/templates/user/auth/oidc_wellknown.tmpl @@ -18,7 +18,8 @@ "scopes_supported": [ "openid", "profile", - "email" + "email", + "groups" ], "claims_supported": [ "aud", @@ -34,7 +35,8 @@ "locale", "updated_at", "email", - "email_verified" + "email_verified", + "groups" ], "code_challenge_methods_supported": [ "plain",