Add groups scope/claim to OIDC/OAuth2 Provider (#17367)
* Add groups scope/claim to OICD/OAuth2 Add support for groups claim as part of the OIDC/OAuth2 flow. Groups is a list of "org" and "org:team" strings to allow clients to authorize based on the groups a user is part of. Signed-off-by: Nico Schieder <code@nico-schieder.de> Co-authored-by: zeripath <art27@cantab.net> Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
		
							parent
							
								
									af96286f22
								
							
						
					
					
						commit
						870f5fbc41
					
				|  | @ -207,6 +207,17 @@ func newAccessTokenResponse(grant *login.OAuth2Grant, serverKey, clientKey oauth | ||||||
| 			idToken.Email = user.Email | 			idToken.Email = user.Email | ||||||
| 			idToken.EmailVerified = user.IsActive | 			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) | 		signedIDToken, err = idToken.SignToken(clientKey) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
|  | @ -232,6 +243,7 @@ type userInfoResponse struct { | ||||||
| 	Username string   `json:"preferred_username"` | 	Username string   `json:"preferred_username"` | ||||||
| 	Email    string   `json:"email"` | 	Email    string   `json:"email"` | ||||||
| 	Picture  string   `json:"picture"` | 	Picture  string   `json:"picture"` | ||||||
|  | 	Groups   []string `json:"groups"` | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // InfoOAuth manages request for userinfo endpoint
 | // InfoOAuth manages request for userinfo endpoint
 | ||||||
|  | @ -241,6 +253,7 @@ func InfoOAuth(ctx *context.Context) { | ||||||
| 		ctx.HandleText(http.StatusUnauthorized, "no valid authorization") | 		ctx.HandleText(http.StatusUnauthorized, "no valid authorization") | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|  | 
 | ||||||
| 	response := &userInfoResponse{ | 	response := &userInfoResponse{ | ||||||
| 		Sub:      fmt.Sprint(ctx.User.ID), | 		Sub:      fmt.Sprint(ctx.User.ID), | ||||||
| 		Name:     ctx.User.FullName, | 		Name:     ctx.User.FullName, | ||||||
|  | @ -248,9 +261,41 @@ func InfoOAuth(ctx *context.Context) { | ||||||
| 		Email:    ctx.User.Email, | 		Email:    ctx.User.Email, | ||||||
| 		Picture:  ctx.User.AvatarLink(), | 		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) | 	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
 | // IntrospectOAuth introspects an oauth token
 | ||||||
| func IntrospectOAuth(ctx *context.Context) { | func IntrospectOAuth(ctx *context.Context) { | ||||||
| 	if ctx.User == nil { | 	if ctx.User == nil { | ||||||
|  |  | ||||||
|  | @ -83,6 +83,9 @@ type OIDCToken struct { | ||||||
| 	// Scope email
 | 	// Scope email
 | ||||||
| 	Email         string `json:"email,omitempty"` | 	Email         string `json:"email,omitempty"` | ||||||
| 	EmailVerified bool   `json:"email_verified,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
 | // SignToken signs an id_token with the (symmetric) client secret key
 | ||||||
|  |  | ||||||
|  | @ -18,7 +18,8 @@ | ||||||
|     "scopes_supported": [ |     "scopes_supported": [ | ||||||
|         "openid", |         "openid", | ||||||
|         "profile", |         "profile", | ||||||
|         "email" |         "email", | ||||||
|  |         "groups" | ||||||
|     ], |     ], | ||||||
|     "claims_supported": [ |     "claims_supported": [ | ||||||
|         "aud", |         "aud", | ||||||
|  | @ -34,7 +35,8 @@ | ||||||
|         "locale", |         "locale", | ||||||
|         "updated_at", |         "updated_at", | ||||||
|         "email", |         "email", | ||||||
|         "email_verified" |         "email_verified", | ||||||
|  |         "groups" | ||||||
|     ], |     ], | ||||||
|     "code_challenge_methods_supported": [ |     "code_challenge_methods_supported": [ | ||||||
|         "plain", |         "plain", | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue