gitea源码

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271
  1. // Copyright 2024 The Gitea Authors. All rights reserved.
  2. // SPDX-License-Identifier: MIT
  3. package oauth2_provider
  4. import (
  5. "context"
  6. "fmt"
  7. "slices"
  8. "strconv"
  9. "strings"
  10. auth "code.gitea.io/gitea/models/auth"
  11. "code.gitea.io/gitea/models/db"
  12. org_model "code.gitea.io/gitea/models/organization"
  13. user_model "code.gitea.io/gitea/models/user"
  14. "code.gitea.io/gitea/modules/log"
  15. "code.gitea.io/gitea/modules/setting"
  16. api "code.gitea.io/gitea/modules/structs"
  17. "code.gitea.io/gitea/modules/timeutil"
  18. "code.gitea.io/gitea/modules/util"
  19. "github.com/golang-jwt/jwt/v5"
  20. )
  21. // AccessTokenErrorCode represents an error code specified in RFC 6749
  22. // https://datatracker.ietf.org/doc/html/rfc6749#section-5.2
  23. type AccessTokenErrorCode string
  24. const (
  25. // AccessTokenErrorCodeInvalidRequest represents an error code specified in RFC 6749
  26. AccessTokenErrorCodeInvalidRequest AccessTokenErrorCode = "invalid_request"
  27. // AccessTokenErrorCodeInvalidClient represents an error code specified in RFC 6749
  28. AccessTokenErrorCodeInvalidClient = "invalid_client"
  29. // AccessTokenErrorCodeInvalidGrant represents an error code specified in RFC 6749
  30. AccessTokenErrorCodeInvalidGrant = "invalid_grant"
  31. // AccessTokenErrorCodeUnauthorizedClient represents an error code specified in RFC 6749
  32. AccessTokenErrorCodeUnauthorizedClient = "unauthorized_client"
  33. // AccessTokenErrorCodeUnsupportedGrantType represents an error code specified in RFC 6749
  34. AccessTokenErrorCodeUnsupportedGrantType = "unsupported_grant_type"
  35. // AccessTokenErrorCodeInvalidScope represents an error code specified in RFC 6749
  36. AccessTokenErrorCodeInvalidScope = "invalid_scope"
  37. )
  38. // AccessTokenError represents an error response specified in RFC 6749
  39. // https://datatracker.ietf.org/doc/html/rfc6749#section-5.2
  40. type AccessTokenError struct {
  41. ErrorCode AccessTokenErrorCode `json:"error" form:"error"`
  42. ErrorDescription string `json:"error_description"`
  43. }
  44. // Error returns the error message
  45. func (err AccessTokenError) Error() string {
  46. return fmt.Sprintf("%s: %s", err.ErrorCode, err.ErrorDescription)
  47. }
  48. // TokenType specifies the kind of token
  49. type TokenType string
  50. const (
  51. // TokenTypeBearer represents a token type specified in RFC 6749
  52. TokenTypeBearer TokenType = "bearer"
  53. // TokenTypeMAC represents a token type specified in RFC 6749
  54. TokenTypeMAC = "mac"
  55. )
  56. // AccessTokenResponse represents a successful access token response
  57. // https://datatracker.ietf.org/doc/html/rfc6749#section-4.2.2
  58. type AccessTokenResponse struct {
  59. AccessToken string `json:"access_token"`
  60. TokenType TokenType `json:"token_type"`
  61. ExpiresIn int64 `json:"expires_in"`
  62. RefreshToken string `json:"refresh_token"`
  63. IDToken string `json:"id_token,omitempty"`
  64. }
  65. // GrantAdditionalScopes returns valid scopes coming from grant
  66. func GrantAdditionalScopes(grantScopes string) auth.AccessTokenScope {
  67. // scopes_supported from templates/user/auth/oidc_wellknown.tmpl
  68. generalScopesSupported := []string{
  69. "openid",
  70. "profile",
  71. "email",
  72. "groups",
  73. }
  74. var accessScopes []string // the scopes for access control, but not for general information
  75. for scope := range strings.SplitSeq(grantScopes, " ") {
  76. if scope != "" && !slices.Contains(generalScopesSupported, scope) {
  77. accessScopes = append(accessScopes, scope)
  78. }
  79. }
  80. // since version 1.22, access tokens grant full access to the API
  81. // with this access is reduced only if additional scopes are provided
  82. if len(accessScopes) > 0 {
  83. accessTokenScope := auth.AccessTokenScope(strings.Join(accessScopes, ","))
  84. if normalizedAccessTokenScope, err := accessTokenScope.Normalize(); err == nil {
  85. return normalizedAccessTokenScope
  86. }
  87. // TODO: if there are invalid access scopes (err != nil),
  88. // then it is treated as "all", maybe in the future we should make it stricter to return an error
  89. // at the moment, to avoid breaking 1.22 behavior, invalid tokens are also treated as "all"
  90. }
  91. // fallback, empty access scope is treated as "all" access
  92. return auth.AccessTokenScopeAll
  93. }
  94. func NewJwtRegisteredClaimsFromUser(clientID string, grantUserID int64, exp *jwt.NumericDate) jwt.RegisteredClaims {
  95. // https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfig
  96. // The issuer value returned MUST be identical to the Issuer URL that was used as the prefix to /.well-known/openid-configuration
  97. // to retrieve the configuration information. This MUST also be identical to the "iss" Claim value in ID Tokens issued from this Issuer.
  98. // * https://accounts.google.com/.well-known/openid-configuration
  99. // * https://github.com/login/oauth/.well-known/openid-configuration
  100. return jwt.RegisteredClaims{
  101. Issuer: strings.TrimSuffix(setting.AppURL, "/"),
  102. Audience: []string{clientID},
  103. Subject: strconv.FormatInt(grantUserID, 10),
  104. ExpiresAt: exp,
  105. }
  106. }
  107. func NewAccessTokenResponse(ctx context.Context, grant *auth.OAuth2Grant, serverKey, clientKey JWTSigningKey) (*AccessTokenResponse, *AccessTokenError) {
  108. if setting.OAuth2.InvalidateRefreshTokens {
  109. if err := grant.IncreaseCounter(ctx); err != nil {
  110. return nil, &AccessTokenError{
  111. ErrorCode: AccessTokenErrorCodeInvalidGrant,
  112. ErrorDescription: "cannot increase the grant counter",
  113. }
  114. }
  115. }
  116. // generate access token to access the API
  117. expirationDate := timeutil.TimeStampNow().Add(setting.OAuth2.AccessTokenExpirationTime)
  118. accessToken := &Token{
  119. GrantID: grant.ID,
  120. Kind: KindAccessToken,
  121. RegisteredClaims: jwt.RegisteredClaims{
  122. ExpiresAt: jwt.NewNumericDate(expirationDate.AsTime()),
  123. },
  124. }
  125. signedAccessToken, err := accessToken.SignToken(serverKey)
  126. if err != nil {
  127. return nil, &AccessTokenError{
  128. ErrorCode: AccessTokenErrorCodeInvalidRequest,
  129. ErrorDescription: "cannot sign token",
  130. }
  131. }
  132. // generate refresh token to request an access token after it expired later
  133. refreshExpirationDate := timeutil.TimeStampNow().Add(setting.OAuth2.RefreshTokenExpirationTime * 60 * 60).AsTime()
  134. refreshToken := &Token{
  135. GrantID: grant.ID,
  136. Counter: grant.Counter,
  137. Kind: KindRefreshToken,
  138. RegisteredClaims: jwt.RegisteredClaims{
  139. ExpiresAt: jwt.NewNumericDate(refreshExpirationDate),
  140. },
  141. }
  142. signedRefreshToken, err := refreshToken.SignToken(serverKey)
  143. if err != nil {
  144. return nil, &AccessTokenError{
  145. ErrorCode: AccessTokenErrorCodeInvalidRequest,
  146. ErrorDescription: "cannot sign token",
  147. }
  148. }
  149. // generate OpenID Connect id_token
  150. signedIDToken := ""
  151. if grant.ScopeContains("openid") {
  152. app, err := auth.GetOAuth2ApplicationByID(ctx, grant.ApplicationID)
  153. if err != nil {
  154. return nil, &AccessTokenError{
  155. ErrorCode: AccessTokenErrorCodeInvalidRequest,
  156. ErrorDescription: "cannot find application",
  157. }
  158. }
  159. user, err := user_model.GetUserByID(ctx, grant.UserID)
  160. if err != nil {
  161. if user_model.IsErrUserNotExist(err) {
  162. return nil, &AccessTokenError{
  163. ErrorCode: AccessTokenErrorCodeInvalidRequest,
  164. ErrorDescription: "cannot find user",
  165. }
  166. }
  167. log.Error("Error loading user: %v", err)
  168. return nil, &AccessTokenError{
  169. ErrorCode: AccessTokenErrorCodeInvalidRequest,
  170. ErrorDescription: "server error",
  171. }
  172. }
  173. idToken := &OIDCToken{
  174. RegisteredClaims: NewJwtRegisteredClaimsFromUser(app.ClientID, grant.UserID, jwt.NewNumericDate(expirationDate.AsTime())),
  175. Nonce: grant.Nonce,
  176. }
  177. if grant.ScopeContains("profile") {
  178. idToken.Name = user.DisplayName()
  179. idToken.PreferredUsername = user.Name
  180. idToken.Profile = user.HTMLURL(ctx)
  181. idToken.Picture = user.AvatarLink(ctx)
  182. idToken.Website = user.Website
  183. idToken.Locale = user.Language
  184. idToken.UpdatedAt = user.UpdatedUnix
  185. }
  186. if grant.ScopeContains("email") {
  187. idToken.Email = user.Email
  188. idToken.EmailVerified = user.IsActive
  189. }
  190. if grant.ScopeContains("groups") {
  191. accessTokenScope := GrantAdditionalScopes(grant.Scope)
  192. // since version 1.22 does not verify if groups should be public-only,
  193. // onlyPublicGroups will be set only if 'public-only' is included in a valid scope
  194. onlyPublicGroups, _ := accessTokenScope.PublicOnly()
  195. groups, err := GetOAuthGroupsForUser(ctx, user, onlyPublicGroups)
  196. if err != nil {
  197. log.Error("Error getting groups: %v", err)
  198. return nil, &AccessTokenError{
  199. ErrorCode: AccessTokenErrorCodeInvalidRequest,
  200. ErrorDescription: "server error",
  201. }
  202. }
  203. idToken.Groups = groups
  204. }
  205. signedIDToken, err = idToken.SignToken(clientKey)
  206. if err != nil {
  207. return nil, &AccessTokenError{
  208. ErrorCode: AccessTokenErrorCodeInvalidRequest,
  209. ErrorDescription: "cannot sign token",
  210. }
  211. }
  212. }
  213. return &AccessTokenResponse{
  214. AccessToken: signedAccessToken,
  215. TokenType: TokenTypeBearer,
  216. ExpiresIn: setting.OAuth2.AccessTokenExpirationTime,
  217. RefreshToken: signedRefreshToken,
  218. IDToken: signedIDToken,
  219. }, nil
  220. }
  221. // GetOAuthGroupsForUser returns a list of "org" and "org:team" strings, that the given user is a part of.
  222. func GetOAuthGroupsForUser(ctx context.Context, user *user_model.User, onlyPublicGroups bool) ([]string, error) {
  223. orgs, err := db.Find[org_model.Organization](ctx, org_model.FindOrgOptions{
  224. UserID: user.ID,
  225. IncludeVisibility: util.Iif(onlyPublicGroups, api.VisibleTypePublic, api.VisibleTypePrivate),
  226. })
  227. if err != nil {
  228. return nil, fmt.Errorf("GetUserOrgList: %w", err)
  229. }
  230. orgTeams, err := org_model.OrgList(orgs).LoadTeams(ctx)
  231. if err != nil {
  232. return nil, fmt.Errorf("LoadTeams: %w", err)
  233. }
  234. var groups []string
  235. for _, org := range orgs {
  236. groups = append(groups, org.Name)
  237. for _, team := range orgTeams[org.ID] {
  238. if team.IsMember(ctx, user.ID) {
  239. groups = append(groups, org.Name+":"+team.LowerName)
  240. }
  241. }
  242. }
  243. return groups, nil
  244. }