gitea源码

oauth_test.go 38KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044
  1. // Copyright 2019 The Gitea Authors. All rights reserved.
  2. // SPDX-License-Identifier: MIT
  3. package integration
  4. import (
  5. "bytes"
  6. "encoding/base64"
  7. "fmt"
  8. "io"
  9. "net/http"
  10. "net/http/httptest"
  11. "strings"
  12. "testing"
  13. asymkey_model "code.gitea.io/gitea/models/asymkey"
  14. auth_model "code.gitea.io/gitea/models/auth"
  15. "code.gitea.io/gitea/models/db"
  16. "code.gitea.io/gitea/models/unittest"
  17. user_model "code.gitea.io/gitea/models/user"
  18. "code.gitea.io/gitea/modules/json"
  19. "code.gitea.io/gitea/modules/setting"
  20. api "code.gitea.io/gitea/modules/structs"
  21. "code.gitea.io/gitea/modules/test"
  22. "code.gitea.io/gitea/modules/util"
  23. "code.gitea.io/gitea/services/auth/source/oauth2"
  24. "code.gitea.io/gitea/services/oauth2_provider"
  25. "code.gitea.io/gitea/tests"
  26. "github.com/markbates/goth"
  27. "github.com/markbates/goth/gothic"
  28. "github.com/stretchr/testify/assert"
  29. "github.com/stretchr/testify/require"
  30. )
  31. func TestOAuth2Provider(t *testing.T) {
  32. defer tests.PrepareTestEnv(t)()
  33. t.Run("AuthorizeNoClientID", testAuthorizeNoClientID)
  34. t.Run("AuthorizeUnregisteredRedirect", testAuthorizeUnregisteredRedirect)
  35. t.Run("AuthorizeUnsupportedResponseType", testAuthorizeUnsupportedResponseType)
  36. t.Run("AuthorizeUnsupportedCodeChallengeMethod", testAuthorizeUnsupportedCodeChallengeMethod)
  37. t.Run("AuthorizeLoginRedirect", testAuthorizeLoginRedirect)
  38. t.Run("OAuth2WellKnown", testOAuth2WellKnown)
  39. }
  40. func testAuthorizeNoClientID(t *testing.T) {
  41. req := NewRequest(t, "GET", "/login/oauth/authorize")
  42. ctx := loginUser(t, "user2")
  43. resp := ctx.MakeRequest(t, req, http.StatusBadRequest)
  44. assert.Contains(t, resp.Body.String(), "Client ID not registered")
  45. }
  46. func testAuthorizeUnregisteredRedirect(t *testing.T) {
  47. req := NewRequest(t, "GET", "/login/oauth/authorize?client_id=da7da3ba-9a13-4167-856f-3899de0b0138&redirect_uri=UNREGISTERED&response_type=code&state=thestate")
  48. ctx := loginUser(t, "user1")
  49. resp := ctx.MakeRequest(t, req, http.StatusBadRequest)
  50. assert.Contains(t, resp.Body.String(), "Unregistered Redirect URI")
  51. }
  52. func testAuthorizeUnsupportedResponseType(t *testing.T) {
  53. req := NewRequest(t, "GET", "/login/oauth/authorize?client_id=da7da3ba-9a13-4167-856f-3899de0b0138&redirect_uri=a&response_type=UNEXPECTED&state=thestate")
  54. ctx := loginUser(t, "user1")
  55. resp := ctx.MakeRequest(t, req, http.StatusSeeOther)
  56. u, err := resp.Result().Location()
  57. assert.NoError(t, err)
  58. assert.Equal(t, "unsupported_response_type", u.Query().Get("error"))
  59. assert.Equal(t, "Only code response type is supported.", u.Query().Get("error_description"))
  60. }
  61. func testAuthorizeUnsupportedCodeChallengeMethod(t *testing.T) {
  62. req := NewRequest(t, "GET", "/login/oauth/authorize?client_id=da7da3ba-9a13-4167-856f-3899de0b0138&redirect_uri=a&response_type=code&state=thestate&code_challenge_method=UNEXPECTED")
  63. ctx := loginUser(t, "user1")
  64. resp := ctx.MakeRequest(t, req, http.StatusSeeOther)
  65. u, err := resp.Result().Location()
  66. assert.NoError(t, err)
  67. assert.Equal(t, "invalid_request", u.Query().Get("error"))
  68. assert.Equal(t, "unsupported code challenge method", u.Query().Get("error_description"))
  69. }
  70. func testAuthorizeLoginRedirect(t *testing.T) {
  71. req := NewRequest(t, "GET", "/login/oauth/authorize")
  72. assert.Contains(t, MakeRequest(t, req, http.StatusSeeOther).Body.String(), "/user/login")
  73. }
  74. func TestAuthorizeShow(t *testing.T) {
  75. defer tests.PrepareTestEnv(t)()
  76. req := NewRequest(t, "GET", "/login/oauth/authorize?client_id=da7da3ba-9a13-4167-856f-3899de0b0138&redirect_uri=a&response_type=code&state=thestate")
  77. ctx := loginUser(t, "user4")
  78. resp := ctx.MakeRequest(t, req, http.StatusOK)
  79. htmlDoc := NewHTMLParser(t, resp.Body)
  80. AssertHTMLElement(t, htmlDoc, "#authorize-app", true)
  81. htmlDoc.GetCSRF()
  82. }
  83. func TestAuthorizeRedirectWithExistingGrant(t *testing.T) {
  84. defer tests.PrepareTestEnv(t)()
  85. req := NewRequest(t, "GET", "/login/oauth/authorize?client_id=da7da3ba-9a13-4167-856f-3899de0b0138&redirect_uri=https%3A%2F%2Fexample.com%2Fxyzzy&response_type=code&state=thestate")
  86. ctx := loginUser(t, "user1")
  87. resp := ctx.MakeRequest(t, req, http.StatusSeeOther)
  88. u, err := resp.Result().Location()
  89. assert.NoError(t, err)
  90. assert.Equal(t, "thestate", u.Query().Get("state"))
  91. assert.Greaterf(t, len(u.Query().Get("code")), 30, "authorization code '%s' should be longer then 30", u.Query().Get("code"))
  92. u.RawQuery = ""
  93. assert.Equal(t, "https://example.com/xyzzy", u.String())
  94. }
  95. func TestAuthorizePKCERequiredForPublicClient(t *testing.T) {
  96. defer tests.PrepareTestEnv(t)()
  97. req := NewRequest(t, "GET", "/login/oauth/authorize?client_id=ce5a1322-42a7-11ed-b878-0242ac120002&redirect_uri=http%3A%2F%2F127.0.0.1&response_type=code&state=thestate")
  98. ctx := loginUser(t, "user1")
  99. resp := ctx.MakeRequest(t, req, http.StatusSeeOther)
  100. u, err := resp.Result().Location()
  101. assert.NoError(t, err)
  102. assert.Equal(t, "invalid_request", u.Query().Get("error"))
  103. assert.Equal(t, "PKCE is required for public clients", u.Query().Get("error_description"))
  104. }
  105. func TestAccessTokenExchange(t *testing.T) {
  106. defer tests.PrepareTestEnv(t)()
  107. req := NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{
  108. "grant_type": "authorization_code",
  109. "client_id": "da7da3ba-9a13-4167-856f-3899de0b0138",
  110. "client_secret": "4MK8Na6R55smdCY0WuCCumZ6hjRPnGY5saWVRHHjJiA=",
  111. "redirect_uri": "a",
  112. "code": "authcode",
  113. "code_verifier": "N1Zo9-8Rfwhkt68r1r29ty8YwIraXR8eh_1Qwxg7yQXsonBt",
  114. })
  115. resp := MakeRequest(t, req, http.StatusOK)
  116. type response struct {
  117. AccessToken string `json:"access_token"`
  118. TokenType string `json:"token_type"`
  119. ExpiresIn int64 `json:"expires_in"`
  120. RefreshToken string `json:"refresh_token"`
  121. }
  122. parsed := new(response)
  123. assert.NoError(t, json.Unmarshal(resp.Body.Bytes(), parsed))
  124. assert.Greater(t, len(parsed.AccessToken), 10)
  125. assert.Greater(t, len(parsed.RefreshToken), 10)
  126. }
  127. func TestAccessTokenExchangeWithPublicClient(t *testing.T) {
  128. defer tests.PrepareTestEnv(t)()
  129. req := NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{
  130. "grant_type": "authorization_code",
  131. "client_id": "ce5a1322-42a7-11ed-b878-0242ac120002",
  132. "redirect_uri": "http://127.0.0.1",
  133. "code": "authcodepublic",
  134. "code_verifier": "N1Zo9-8Rfwhkt68r1r29ty8YwIraXR8eh_1Qwxg7yQXsonBt",
  135. })
  136. resp := MakeRequest(t, req, http.StatusOK)
  137. type response struct {
  138. AccessToken string `json:"access_token"`
  139. TokenType string `json:"token_type"`
  140. ExpiresIn int64 `json:"expires_in"`
  141. RefreshToken string `json:"refresh_token"`
  142. }
  143. parsed := new(response)
  144. assert.NoError(t, json.Unmarshal(resp.Body.Bytes(), parsed))
  145. assert.Greater(t, len(parsed.AccessToken), 10)
  146. assert.Greater(t, len(parsed.RefreshToken), 10)
  147. }
  148. func TestAccessTokenExchangeJSON(t *testing.T) {
  149. defer tests.PrepareTestEnv(t)()
  150. req := NewRequestWithJSON(t, "POST", "/login/oauth/access_token", map[string]string{
  151. "grant_type": "authorization_code",
  152. "client_id": "da7da3ba-9a13-4167-856f-3899de0b0138",
  153. "client_secret": "4MK8Na6R55smdCY0WuCCumZ6hjRPnGY5saWVRHHjJiA=",
  154. "redirect_uri": "a",
  155. "code": "authcode",
  156. "code_verifier": "N1Zo9-8Rfwhkt68r1r29ty8YwIraXR8eh_1Qwxg7yQXsonBt",
  157. })
  158. resp := MakeRequest(t, req, http.StatusOK)
  159. type response struct {
  160. AccessToken string `json:"access_token"`
  161. TokenType string `json:"token_type"`
  162. ExpiresIn int64 `json:"expires_in"`
  163. RefreshToken string `json:"refresh_token"`
  164. }
  165. parsed := new(response)
  166. assert.NoError(t, json.Unmarshal(resp.Body.Bytes(), parsed))
  167. assert.Greater(t, len(parsed.AccessToken), 10)
  168. assert.Greater(t, len(parsed.RefreshToken), 10)
  169. }
  170. func TestAccessTokenExchangeWithoutPKCE(t *testing.T) {
  171. defer tests.PrepareTestEnv(t)()
  172. req := NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{
  173. "grant_type": "authorization_code",
  174. "client_id": "da7da3ba-9a13-4167-856f-3899de0b0138",
  175. "client_secret": "4MK8Na6R55smdCY0WuCCumZ6hjRPnGY5saWVRHHjJiA=",
  176. "redirect_uri": "a",
  177. "code": "authcode",
  178. })
  179. resp := MakeRequest(t, req, http.StatusBadRequest)
  180. parsedError := new(oauth2_provider.AccessTokenError)
  181. assert.NoError(t, json.Unmarshal(resp.Body.Bytes(), parsedError))
  182. assert.Equal(t, "unauthorized_client", string(parsedError.ErrorCode))
  183. assert.Equal(t, "failed PKCE code challenge", parsedError.ErrorDescription)
  184. }
  185. func TestAccessTokenExchangeWithInvalidCredentials(t *testing.T) {
  186. defer tests.PrepareTestEnv(t)()
  187. // invalid client id
  188. req := NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{
  189. "grant_type": "authorization_code",
  190. "client_id": "???",
  191. "client_secret": "4MK8Na6R55smdCY0WuCCumZ6hjRPnGY5saWVRHHjJiA=",
  192. "redirect_uri": "a",
  193. "code": "authcode",
  194. "code_verifier": "N1Zo9-8Rfwhkt68r1r29ty8YwIraXR8eh_1Qwxg7yQXsonBt",
  195. })
  196. resp := MakeRequest(t, req, http.StatusBadRequest)
  197. parsedError := new(oauth2_provider.AccessTokenError)
  198. assert.NoError(t, json.Unmarshal(resp.Body.Bytes(), parsedError))
  199. assert.Equal(t, "invalid_client", string(parsedError.ErrorCode))
  200. assert.Equal(t, "cannot load client with client id: '???'", parsedError.ErrorDescription)
  201. // invalid client secret
  202. req = NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{
  203. "grant_type": "authorization_code",
  204. "client_id": "da7da3ba-9a13-4167-856f-3899de0b0138",
  205. "client_secret": "???",
  206. "redirect_uri": "a",
  207. "code": "authcode",
  208. "code_verifier": "N1Zo9-8Rfwhkt68r1r29ty8YwIraXR8eh_1Qwxg7yQXsonBt",
  209. })
  210. resp = MakeRequest(t, req, http.StatusBadRequest)
  211. parsedError = new(oauth2_provider.AccessTokenError)
  212. assert.NoError(t, json.Unmarshal(resp.Body.Bytes(), parsedError))
  213. assert.Equal(t, "unauthorized_client", string(parsedError.ErrorCode))
  214. assert.Equal(t, "invalid client secret", parsedError.ErrorDescription)
  215. // invalid redirect uri
  216. req = NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{
  217. "grant_type": "authorization_code",
  218. "client_id": "da7da3ba-9a13-4167-856f-3899de0b0138",
  219. "client_secret": "4MK8Na6R55smdCY0WuCCumZ6hjRPnGY5saWVRHHjJiA=",
  220. "redirect_uri": "???",
  221. "code": "authcode",
  222. "code_verifier": "N1Zo9-8Rfwhkt68r1r29ty8YwIraXR8eh_1Qwxg7yQXsonBt",
  223. })
  224. resp = MakeRequest(t, req, http.StatusBadRequest)
  225. parsedError = new(oauth2_provider.AccessTokenError)
  226. assert.NoError(t, json.Unmarshal(resp.Body.Bytes(), parsedError))
  227. assert.Equal(t, "unauthorized_client", string(parsedError.ErrorCode))
  228. assert.Equal(t, "unexpected redirect URI", parsedError.ErrorDescription)
  229. // invalid authorization code
  230. req = NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{
  231. "grant_type": "authorization_code",
  232. "client_id": "da7da3ba-9a13-4167-856f-3899de0b0138",
  233. "client_secret": "4MK8Na6R55smdCY0WuCCumZ6hjRPnGY5saWVRHHjJiA=",
  234. "redirect_uri": "a",
  235. "code": "???",
  236. "code_verifier": "N1Zo9-8Rfwhkt68r1r29ty8YwIraXR8eh_1Qwxg7yQXsonBt",
  237. })
  238. resp = MakeRequest(t, req, http.StatusBadRequest)
  239. parsedError = new(oauth2_provider.AccessTokenError)
  240. assert.NoError(t, json.Unmarshal(resp.Body.Bytes(), parsedError))
  241. assert.Equal(t, "unauthorized_client", string(parsedError.ErrorCode))
  242. assert.Equal(t, "client is not authorized", parsedError.ErrorDescription)
  243. // invalid grant_type
  244. req = NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{
  245. "grant_type": "???",
  246. "client_id": "da7da3ba-9a13-4167-856f-3899de0b0138",
  247. "client_secret": "4MK8Na6R55smdCY0WuCCumZ6hjRPnGY5saWVRHHjJiA=",
  248. "redirect_uri": "a",
  249. "code": "authcode",
  250. "code_verifier": "N1Zo9-8Rfwhkt68r1r29ty8YwIraXR8eh_1Qwxg7yQXsonBt",
  251. })
  252. resp = MakeRequest(t, req, http.StatusBadRequest)
  253. parsedError = new(oauth2_provider.AccessTokenError)
  254. assert.NoError(t, json.Unmarshal(resp.Body.Bytes(), parsedError))
  255. assert.Equal(t, "unsupported_grant_type", string(parsedError.ErrorCode))
  256. assert.Equal(t, "Only refresh_token or authorization_code grant type is supported", parsedError.ErrorDescription)
  257. }
  258. func TestAccessTokenExchangeWithBasicAuth(t *testing.T) {
  259. defer tests.PrepareTestEnv(t)()
  260. req := NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{
  261. "grant_type": "authorization_code",
  262. "redirect_uri": "a",
  263. "code": "authcode",
  264. "code_verifier": "N1Zo9-8Rfwhkt68r1r29ty8YwIraXR8eh_1Qwxg7yQXsonBt",
  265. })
  266. req.Header.Add("Authorization", "Basic ZGE3ZGEzYmEtOWExMy00MTY3LTg1NmYtMzg5OWRlMGIwMTM4OjRNSzhOYTZSNTVzbWRDWTBXdUNDdW1aNmhqUlBuR1k1c2FXVlJISGpKaUE9")
  267. resp := MakeRequest(t, req, http.StatusOK)
  268. type response struct {
  269. AccessToken string `json:"access_token"`
  270. TokenType string `json:"token_type"`
  271. ExpiresIn int64 `json:"expires_in"`
  272. RefreshToken string `json:"refresh_token"`
  273. }
  274. parsed := new(response)
  275. assert.NoError(t, json.Unmarshal(resp.Body.Bytes(), parsed))
  276. assert.Greater(t, len(parsed.AccessToken), 10)
  277. assert.Greater(t, len(parsed.RefreshToken), 10)
  278. // use wrong client_secret
  279. req = NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{
  280. "grant_type": "authorization_code",
  281. "redirect_uri": "a",
  282. "code": "authcode",
  283. "code_verifier": "N1Zo9-8Rfwhkt68r1r29ty8YwIraXR8eh_1Qwxg7yQXsonBt",
  284. })
  285. req.Header.Add("Authorization", "Basic ZGE3ZGEzYmEtOWExMy00MTY3LTg1NmYtMzg5OWRlMGIwMTM4OmJsYWJsYQ==")
  286. resp = MakeRequest(t, req, http.StatusBadRequest)
  287. parsedError := new(oauth2_provider.AccessTokenError)
  288. assert.NoError(t, json.Unmarshal(resp.Body.Bytes(), parsedError))
  289. assert.Equal(t, "unauthorized_client", string(parsedError.ErrorCode))
  290. assert.Equal(t, "invalid client secret", parsedError.ErrorDescription)
  291. // missing header
  292. req = NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{
  293. "grant_type": "authorization_code",
  294. "redirect_uri": "a",
  295. "code": "authcode",
  296. "code_verifier": "N1Zo9-8Rfwhkt68r1r29ty8YwIraXR8eh_1Qwxg7yQXsonBt",
  297. })
  298. resp = MakeRequest(t, req, http.StatusBadRequest)
  299. parsedError = new(oauth2_provider.AccessTokenError)
  300. assert.NoError(t, json.Unmarshal(resp.Body.Bytes(), parsedError))
  301. assert.Equal(t, "invalid_client", string(parsedError.ErrorCode))
  302. assert.Equal(t, "cannot load client with client id: ''", parsedError.ErrorDescription)
  303. // client_id inconsistent with Authorization header
  304. req = NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{
  305. "grant_type": "authorization_code",
  306. "redirect_uri": "a",
  307. "code": "authcode",
  308. "client_id": "inconsistent",
  309. })
  310. req.Header.Add("Authorization", "Basic ZGE3ZGEzYmEtOWExMy00MTY3LTg1NmYtMzg5OWRlMGIwMTM4OjRNSzhOYTZSNTVzbWRDWTBXdUNDdW1aNmhqUlBuR1k1c2FXVlJISGpKaUE9")
  311. resp = MakeRequest(t, req, http.StatusBadRequest)
  312. parsedError = new(oauth2_provider.AccessTokenError)
  313. assert.NoError(t, json.Unmarshal(resp.Body.Bytes(), parsedError))
  314. assert.Equal(t, "invalid_request", string(parsedError.ErrorCode))
  315. assert.Equal(t, "client_id in request body inconsistent with Authorization header", parsedError.ErrorDescription)
  316. // client_secret inconsistent with Authorization header
  317. req = NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{
  318. "grant_type": "authorization_code",
  319. "redirect_uri": "a",
  320. "code": "authcode",
  321. "client_secret": "inconsistent",
  322. })
  323. req.Header.Add("Authorization", "Basic ZGE3ZGEzYmEtOWExMy00MTY3LTg1NmYtMzg5OWRlMGIwMTM4OjRNSzhOYTZSNTVzbWRDWTBXdUNDdW1aNmhqUlBuR1k1c2FXVlJISGpKaUE9")
  324. resp = MakeRequest(t, req, http.StatusBadRequest)
  325. parsedError = new(oauth2_provider.AccessTokenError)
  326. assert.NoError(t, json.Unmarshal(resp.Body.Bytes(), parsedError))
  327. assert.Equal(t, "invalid_request", string(parsedError.ErrorCode))
  328. assert.Equal(t, "client_secret in request body inconsistent with Authorization header", parsedError.ErrorDescription)
  329. }
  330. func TestRefreshTokenInvalidation(t *testing.T) {
  331. defer tests.PrepareTestEnv(t)()
  332. req := NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{
  333. "grant_type": "authorization_code",
  334. "client_id": "da7da3ba-9a13-4167-856f-3899de0b0138",
  335. "client_secret": "4MK8Na6R55smdCY0WuCCumZ6hjRPnGY5saWVRHHjJiA=",
  336. "redirect_uri": "a",
  337. "code": "authcode",
  338. "code_verifier": "N1Zo9-8Rfwhkt68r1r29ty8YwIraXR8eh_1Qwxg7yQXsonBt",
  339. })
  340. resp := MakeRequest(t, req, http.StatusOK)
  341. type response struct {
  342. AccessToken string `json:"access_token"`
  343. TokenType string `json:"token_type"`
  344. ExpiresIn int64 `json:"expires_in"`
  345. RefreshToken string `json:"refresh_token"`
  346. }
  347. parsed := new(response)
  348. assert.NoError(t, json.Unmarshal(resp.Body.Bytes(), parsed))
  349. // test without invalidation
  350. setting.OAuth2.InvalidateRefreshTokens = false
  351. req = NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{
  352. "grant_type": "refresh_token",
  353. "client_id": "da7da3ba-9a13-4167-856f-3899de0b0138",
  354. // omit secret
  355. "redirect_uri": "a",
  356. "refresh_token": parsed.RefreshToken,
  357. })
  358. resp = MakeRequest(t, req, http.StatusBadRequest)
  359. parsedError := new(oauth2_provider.AccessTokenError)
  360. assert.NoError(t, json.Unmarshal(resp.Body.Bytes(), parsedError))
  361. assert.Equal(t, "invalid_client", string(parsedError.ErrorCode))
  362. assert.Equal(t, "invalid empty client secret", parsedError.ErrorDescription)
  363. req = NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{
  364. "grant_type": "refresh_token",
  365. "client_id": "da7da3ba-9a13-4167-856f-3899de0b0138",
  366. "client_secret": "4MK8Na6R55smdCY0WuCCumZ6hjRPnGY5saWVRHHjJiA=",
  367. "redirect_uri": "a",
  368. "refresh_token": "UNEXPECTED",
  369. })
  370. resp = MakeRequest(t, req, http.StatusBadRequest)
  371. parsedError = new(oauth2_provider.AccessTokenError)
  372. assert.NoError(t, json.Unmarshal(resp.Body.Bytes(), parsedError))
  373. assert.Equal(t, "unauthorized_client", string(parsedError.ErrorCode))
  374. assert.Equal(t, "unable to parse refresh token", parsedError.ErrorDescription)
  375. req = NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{
  376. "grant_type": "refresh_token",
  377. "client_id": "da7da3ba-9a13-4167-856f-3899de0b0138",
  378. "client_secret": "4MK8Na6R55smdCY0WuCCumZ6hjRPnGY5saWVRHHjJiA=",
  379. "redirect_uri": "a",
  380. "refresh_token": parsed.RefreshToken,
  381. })
  382. bs, err := io.ReadAll(req.Body)
  383. assert.NoError(t, err)
  384. req.Body = io.NopCloser(bytes.NewReader(bs))
  385. MakeRequest(t, req, http.StatusOK)
  386. req.Body = io.NopCloser(bytes.NewReader(bs))
  387. MakeRequest(t, req, http.StatusOK)
  388. // test with invalidation
  389. setting.OAuth2.InvalidateRefreshTokens = true
  390. req.Body = io.NopCloser(bytes.NewReader(bs))
  391. MakeRequest(t, req, http.StatusOK)
  392. // repeat request should fail
  393. req.Body = io.NopCloser(bytes.NewReader(bs))
  394. resp = MakeRequest(t, req, http.StatusBadRequest)
  395. parsedError = new(oauth2_provider.AccessTokenError)
  396. assert.NoError(t, json.Unmarshal(resp.Body.Bytes(), parsedError))
  397. assert.Equal(t, "unauthorized_client", string(parsedError.ErrorCode))
  398. assert.Equal(t, "token was already used", parsedError.ErrorDescription)
  399. }
  400. func TestOAuthIntrospection(t *testing.T) {
  401. defer tests.PrepareTestEnv(t)()
  402. req := NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{
  403. "grant_type": "authorization_code",
  404. "client_id": "da7da3ba-9a13-4167-856f-3899de0b0138",
  405. "client_secret": "4MK8Na6R55smdCY0WuCCumZ6hjRPnGY5saWVRHHjJiA=",
  406. "redirect_uri": "a",
  407. "code": "authcode",
  408. "code_verifier": "N1Zo9-8Rfwhkt68r1r29ty8YwIraXR8eh_1Qwxg7yQXsonBt",
  409. })
  410. resp := MakeRequest(t, req, http.StatusOK)
  411. type response struct {
  412. AccessToken string `json:"access_token"`
  413. TokenType string `json:"token_type"`
  414. ExpiresIn int64 `json:"expires_in"`
  415. RefreshToken string `json:"refresh_token"`
  416. }
  417. parsed := new(response)
  418. assert.NoError(t, json.Unmarshal(resp.Body.Bytes(), parsed))
  419. assert.Greater(t, len(parsed.AccessToken), 10)
  420. assert.Greater(t, len(parsed.RefreshToken), 10)
  421. // successful request with a valid client_id/client_secret and a valid token
  422. req = NewRequestWithValues(t, "POST", "/login/oauth/introspect", map[string]string{
  423. "token": parsed.AccessToken,
  424. })
  425. req.Header.Add("Authorization", "Basic ZGE3ZGEzYmEtOWExMy00MTY3LTg1NmYtMzg5OWRlMGIwMTM4OjRNSzhOYTZSNTVzbWRDWTBXdUNDdW1aNmhqUlBuR1k1c2FXVlJISGpKaUE9")
  426. resp = MakeRequest(t, req, http.StatusOK)
  427. type introspectResponse struct {
  428. Active bool `json:"active"`
  429. Scope string `json:"scope,omitempty"`
  430. Username string `json:"username"`
  431. }
  432. introspectParsed := new(introspectResponse)
  433. assert.NoError(t, json.Unmarshal(resp.Body.Bytes(), introspectParsed))
  434. assert.True(t, introspectParsed.Active)
  435. assert.Equal(t, "user1", introspectParsed.Username)
  436. // successful request with a valid client_id/client_secret, but an invalid token
  437. req = NewRequestWithValues(t, "POST", "/login/oauth/introspect", map[string]string{
  438. "token": "xyzzy",
  439. })
  440. req.Header.Add("Authorization", "Basic ZGE3ZGEzYmEtOWExMy00MTY3LTg1NmYtMzg5OWRlMGIwMTM4OjRNSzhOYTZSNTVzbWRDWTBXdUNDdW1aNmhqUlBuR1k1c2FXVlJISGpKaUE9")
  441. resp = MakeRequest(t, req, http.StatusOK)
  442. introspectParsed = new(introspectResponse)
  443. assert.NoError(t, json.Unmarshal(resp.Body.Bytes(), introspectParsed))
  444. assert.False(t, introspectParsed.Active)
  445. // unsuccessful request with an invalid client_id/client_secret
  446. req = NewRequestWithValues(t, "POST", "/login/oauth/introspect", map[string]string{
  447. "token": parsed.AccessToken,
  448. })
  449. req.Header.Add("Authorization", "Basic ZGE3ZGEzYmEtOWExMy00MTY3LTg1NmYtMzg5OWRlMGIwMTM4OjRNSzhOYTZSNTVzbWRDWTBXdUNDdW1aNmhqUlBuR1k1c2FXVlJISGpK")
  450. resp = MakeRequest(t, req, http.StatusUnauthorized)
  451. assert.Contains(t, resp.Body.String(), "no valid authorization")
  452. }
  453. func TestOAuth_GrantScopesReadUserFailRepos(t *testing.T) {
  454. defer tests.PrepareTestEnv(t)()
  455. user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
  456. appBody := api.CreateOAuth2ApplicationOptions{
  457. Name: "oauth-provider-scopes-test",
  458. RedirectURIs: []string{
  459. "a",
  460. },
  461. ConfidentialClient: true,
  462. }
  463. req := NewRequestWithJSON(t, "POST", "/api/v1/user/applications/oauth2", &appBody).
  464. AddBasicAuth(user.Name)
  465. resp := MakeRequest(t, req, http.StatusCreated)
  466. var app *api.OAuth2Application
  467. DecodeJSON(t, resp, &app)
  468. grant := &auth_model.OAuth2Grant{
  469. ApplicationID: app.ID,
  470. UserID: user.ID,
  471. Scope: "openid read:user",
  472. }
  473. err := db.Insert(t.Context(), grant)
  474. require.NoError(t, err)
  475. assert.Contains(t, grant.Scope, "openid read:user")
  476. ctx := loginUser(t, user.Name)
  477. authorizeURL := fmt.Sprintf("/login/oauth/authorize?client_id=%s&redirect_uri=a&response_type=code&state=thestate", app.ClientID)
  478. authorizeReq := NewRequest(t, "GET", authorizeURL)
  479. authorizeResp := ctx.MakeRequest(t, authorizeReq, http.StatusSeeOther)
  480. authcode := strings.Split(strings.Split(authorizeResp.Body.String(), "?code=")[1], "&amp")[0]
  481. accessTokenReq := NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{
  482. "grant_type": "authorization_code",
  483. "client_id": app.ClientID,
  484. "client_secret": app.ClientSecret,
  485. "redirect_uri": "a",
  486. "code": authcode,
  487. })
  488. accessTokenResp := ctx.MakeRequest(t, accessTokenReq, 200)
  489. type response struct {
  490. AccessToken string `json:"access_token"`
  491. TokenType string `json:"token_type"`
  492. ExpiresIn int64 `json:"expires_in"`
  493. RefreshToken string `json:"refresh_token"`
  494. }
  495. parsed := new(response)
  496. require.NoError(t, json.Unmarshal(accessTokenResp.Body.Bytes(), parsed))
  497. userReq := NewRequest(t, "GET", "/api/v1/user")
  498. userReq.SetHeader("Authorization", "Bearer "+parsed.AccessToken)
  499. userResp := MakeRequest(t, userReq, http.StatusOK)
  500. type userResponse struct {
  501. Login string `json:"login"`
  502. Email string `json:"email"`
  503. }
  504. userParsed := new(userResponse)
  505. require.NoError(t, json.Unmarshal(userResp.Body.Bytes(), userParsed))
  506. assert.Contains(t, userParsed.Email, "user2@example.com")
  507. errorReq := NewRequest(t, "GET", "/api/v1/users/user2/repos")
  508. errorReq.SetHeader("Authorization", "Bearer "+parsed.AccessToken)
  509. errorResp := MakeRequest(t, errorReq, http.StatusForbidden)
  510. type errorResponse struct {
  511. Message string `json:"message"`
  512. }
  513. errorParsed := new(errorResponse)
  514. require.NoError(t, json.Unmarshal(errorResp.Body.Bytes(), errorParsed))
  515. assert.Contains(t, errorParsed.Message, "token does not have at least one of required scope(s), required=[read:repository]")
  516. }
  517. func TestOAuth_GrantScopesReadRepositoryFailOrganization(t *testing.T) {
  518. defer tests.PrepareTestEnv(t)()
  519. user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
  520. appBody := api.CreateOAuth2ApplicationOptions{
  521. Name: "oauth-provider-scopes-test",
  522. RedirectURIs: []string{
  523. "a",
  524. },
  525. ConfidentialClient: true,
  526. }
  527. req := NewRequestWithJSON(t, "POST", "/api/v1/user/applications/oauth2", &appBody).
  528. AddBasicAuth(user.Name)
  529. resp := MakeRequest(t, req, http.StatusCreated)
  530. var app *api.OAuth2Application
  531. DecodeJSON(t, resp, &app)
  532. grant := &auth_model.OAuth2Grant{
  533. ApplicationID: app.ID,
  534. UserID: user.ID,
  535. Scope: "openid read:user read:repository",
  536. }
  537. err := db.Insert(t.Context(), grant)
  538. require.NoError(t, err)
  539. assert.Contains(t, grant.Scope, "openid read:user read:repository")
  540. ctx := loginUser(t, user.Name)
  541. authorizeURL := fmt.Sprintf("/login/oauth/authorize?client_id=%s&redirect_uri=a&response_type=code&state=thestate", app.ClientID)
  542. authorizeReq := NewRequest(t, "GET", authorizeURL)
  543. authorizeResp := ctx.MakeRequest(t, authorizeReq, http.StatusSeeOther)
  544. authcode := strings.Split(strings.Split(authorizeResp.Body.String(), "?code=")[1], "&amp")[0]
  545. accessTokenReq := NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{
  546. "grant_type": "authorization_code",
  547. "client_id": app.ClientID,
  548. "client_secret": app.ClientSecret,
  549. "redirect_uri": "a",
  550. "code": authcode,
  551. })
  552. accessTokenResp := ctx.MakeRequest(t, accessTokenReq, http.StatusOK)
  553. type response struct {
  554. AccessToken string `json:"access_token"`
  555. TokenType string `json:"token_type"`
  556. ExpiresIn int64 `json:"expires_in"`
  557. RefreshToken string `json:"refresh_token"`
  558. }
  559. parsed := new(response)
  560. require.NoError(t, json.Unmarshal(accessTokenResp.Body.Bytes(), parsed))
  561. userReq := NewRequest(t, "GET", "/api/v1/users/user2/repos")
  562. userReq.SetHeader("Authorization", "Bearer "+parsed.AccessToken)
  563. userResp := MakeRequest(t, userReq, http.StatusOK)
  564. type repo struct {
  565. FullRepoName string `json:"full_name"`
  566. Private bool `json:"private"`
  567. }
  568. var reposCaptured []repo
  569. require.NoError(t, json.Unmarshal(userResp.Body.Bytes(), &reposCaptured))
  570. reposExpected := []repo{
  571. {
  572. FullRepoName: "user2/repo1",
  573. Private: false,
  574. },
  575. {
  576. FullRepoName: "user2/repo2",
  577. Private: true,
  578. },
  579. {
  580. FullRepoName: "user2/repo15",
  581. Private: true,
  582. },
  583. {
  584. FullRepoName: "user2/repo16",
  585. Private: true,
  586. },
  587. {
  588. FullRepoName: "user2/repo20",
  589. Private: true,
  590. },
  591. {
  592. FullRepoName: "user2/utf8",
  593. Private: false,
  594. },
  595. {
  596. FullRepoName: "user2/commits_search_test",
  597. Private: false,
  598. },
  599. {
  600. FullRepoName: "user2/git_hooks_test",
  601. Private: false,
  602. },
  603. {
  604. FullRepoName: "user2/glob",
  605. Private: false,
  606. },
  607. {
  608. FullRepoName: "user2/lfs",
  609. Private: true,
  610. },
  611. {
  612. FullRepoName: "user2/scoped_label",
  613. Private: true,
  614. },
  615. {
  616. FullRepoName: "user2/readme-test",
  617. Private: true,
  618. },
  619. {
  620. FullRepoName: "user2/repo-release",
  621. Private: false,
  622. },
  623. {
  624. FullRepoName: "user2/commitsonpr",
  625. Private: false,
  626. },
  627. }
  628. assert.Equal(t, reposExpected, reposCaptured)
  629. errorReq := NewRequest(t, "GET", "/api/v1/users/user2/orgs")
  630. errorReq.SetHeader("Authorization", "Bearer "+parsed.AccessToken)
  631. errorResp := MakeRequest(t, errorReq, http.StatusForbidden)
  632. type errorResponse struct {
  633. Message string `json:"message"`
  634. }
  635. errorParsed := new(errorResponse)
  636. require.NoError(t, json.Unmarshal(errorResp.Body.Bytes(), errorParsed))
  637. assert.Contains(t, errorParsed.Message, "token does not have at least one of required scope(s), required=[read:user read:organization]")
  638. }
  639. func TestOAuth_GrantScopesClaimPublicOnlyGroups(t *testing.T) {
  640. defer tests.PrepareTestEnv(t)()
  641. user := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "user2"})
  642. appBody := api.CreateOAuth2ApplicationOptions{
  643. Name: "oauth-provider-scopes-test",
  644. RedirectURIs: []string{
  645. "a",
  646. },
  647. ConfidentialClient: true,
  648. }
  649. appReq := NewRequestWithJSON(t, "POST", "/api/v1/user/applications/oauth2", &appBody).
  650. AddBasicAuth(user.Name)
  651. appResp := MakeRequest(t, appReq, http.StatusCreated)
  652. var app *api.OAuth2Application
  653. DecodeJSON(t, appResp, &app)
  654. grant := &auth_model.OAuth2Grant{
  655. ApplicationID: app.ID,
  656. UserID: user.ID,
  657. Scope: "openid groups read:user public-only",
  658. }
  659. err := db.Insert(t.Context(), grant)
  660. require.NoError(t, err)
  661. assert.ElementsMatch(t, []string{"openid", "groups", "read:user", "public-only"}, strings.Split(grant.Scope, " "))
  662. ctx := loginUser(t, user.Name)
  663. authorizeURL := fmt.Sprintf("/login/oauth/authorize?client_id=%s&redirect_uri=a&response_type=code&state=thestate", app.ClientID)
  664. authorizeReq := NewRequest(t, "GET", authorizeURL)
  665. authorizeResp := ctx.MakeRequest(t, authorizeReq, http.StatusSeeOther)
  666. authcode := strings.Split(strings.Split(authorizeResp.Body.String(), "?code=")[1], "&amp")[0]
  667. accessTokenReq := NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{
  668. "grant_type": "authorization_code",
  669. "client_id": app.ClientID,
  670. "client_secret": app.ClientSecret,
  671. "redirect_uri": "a",
  672. "code": authcode,
  673. })
  674. accessTokenResp := ctx.MakeRequest(t, accessTokenReq, http.StatusOK)
  675. type response struct {
  676. AccessToken string `json:"access_token"`
  677. TokenType string `json:"token_type"`
  678. ExpiresIn int64 `json:"expires_in"`
  679. RefreshToken string `json:"refresh_token"`
  680. IDToken string `json:"id_token,omitempty"`
  681. }
  682. parsed := new(response)
  683. require.NoError(t, json.Unmarshal(accessTokenResp.Body.Bytes(), parsed))
  684. parts := strings.Split(parsed.IDToken, ".")
  685. payload, _ := base64.RawURLEncoding.DecodeString(parts[1])
  686. type IDTokenClaims struct {
  687. Groups []string `json:"groups"`
  688. }
  689. claims := new(IDTokenClaims)
  690. require.NoError(t, json.Unmarshal(payload, claims))
  691. userinfoReq := NewRequest(t, "GET", "/login/oauth/userinfo")
  692. userinfoReq.SetHeader("Authorization", "Bearer "+parsed.AccessToken)
  693. userinfoResp := MakeRequest(t, userinfoReq, http.StatusOK)
  694. type userinfoResponse struct {
  695. Login string `json:"login"`
  696. Email string `json:"email"`
  697. Groups []string `json:"groups"`
  698. }
  699. userinfoParsed := new(userinfoResponse)
  700. require.NoError(t, json.Unmarshal(userinfoResp.Body.Bytes(), userinfoParsed))
  701. assert.Contains(t, userinfoParsed.Email, "user2@example.com")
  702. // test both id_token and call to /login/oauth/userinfo
  703. for _, publicGroup := range []string{
  704. "org17",
  705. "org17:test_team",
  706. "org3",
  707. "org3:owners",
  708. "org3:team1",
  709. "org3:teamcreaterepo",
  710. } {
  711. assert.Contains(t, claims.Groups, publicGroup)
  712. assert.Contains(t, userinfoParsed.Groups, publicGroup)
  713. }
  714. for _, privateGroup := range []string{
  715. "private_org35",
  716. "private_org35_team24",
  717. } {
  718. assert.NotContains(t, claims.Groups, privateGroup)
  719. assert.NotContains(t, userinfoParsed.Groups, privateGroup)
  720. }
  721. }
  722. func TestOAuth_GrantScopesClaimAllGroups(t *testing.T) {
  723. defer tests.PrepareTestEnv(t)()
  724. user := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "user2"})
  725. appBody := api.CreateOAuth2ApplicationOptions{
  726. Name: "oauth-provider-scopes-test",
  727. RedirectURIs: []string{
  728. "a",
  729. },
  730. ConfidentialClient: true,
  731. }
  732. appReq := NewRequestWithJSON(t, "POST", "/api/v1/user/applications/oauth2", &appBody).
  733. AddBasicAuth(user.Name)
  734. appResp := MakeRequest(t, appReq, http.StatusCreated)
  735. var app *api.OAuth2Application
  736. DecodeJSON(t, appResp, &app)
  737. grant := &auth_model.OAuth2Grant{
  738. ApplicationID: app.ID,
  739. UserID: user.ID,
  740. Scope: "openid groups",
  741. }
  742. err := db.Insert(t.Context(), grant)
  743. require.NoError(t, err)
  744. assert.ElementsMatch(t, []string{"openid", "groups"}, strings.Split(grant.Scope, " "))
  745. ctx := loginUser(t, user.Name)
  746. authorizeURL := fmt.Sprintf("/login/oauth/authorize?client_id=%s&redirect_uri=a&response_type=code&state=thestate", app.ClientID)
  747. authorizeReq := NewRequest(t, "GET", authorizeURL)
  748. authorizeResp := ctx.MakeRequest(t, authorizeReq, http.StatusSeeOther)
  749. authcode := strings.Split(strings.Split(authorizeResp.Body.String(), "?code=")[1], "&amp")[0]
  750. accessTokenReq := NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{
  751. "grant_type": "authorization_code",
  752. "client_id": app.ClientID,
  753. "client_secret": app.ClientSecret,
  754. "redirect_uri": "a",
  755. "code": authcode,
  756. })
  757. accessTokenResp := ctx.MakeRequest(t, accessTokenReq, http.StatusOK)
  758. type response struct {
  759. AccessToken string `json:"access_token"`
  760. TokenType string `json:"token_type"`
  761. ExpiresIn int64 `json:"expires_in"`
  762. RefreshToken string `json:"refresh_token"`
  763. IDToken string `json:"id_token,omitempty"`
  764. }
  765. parsed := new(response)
  766. require.NoError(t, json.Unmarshal(accessTokenResp.Body.Bytes(), parsed))
  767. parts := strings.Split(parsed.IDToken, ".")
  768. payload, _ := base64.RawURLEncoding.DecodeString(parts[1])
  769. type IDTokenClaims struct {
  770. Groups []string `json:"groups"`
  771. }
  772. claims := new(IDTokenClaims)
  773. require.NoError(t, json.Unmarshal(payload, claims))
  774. userinfoReq := NewRequest(t, "GET", "/login/oauth/userinfo")
  775. userinfoReq.SetHeader("Authorization", "Bearer "+parsed.AccessToken)
  776. userinfoResp := MakeRequest(t, userinfoReq, http.StatusOK)
  777. type userinfoResponse struct {
  778. Login string `json:"login"`
  779. Email string `json:"email"`
  780. Groups []string `json:"groups"`
  781. }
  782. userinfoParsed := new(userinfoResponse)
  783. require.NoError(t, json.Unmarshal(userinfoResp.Body.Bytes(), userinfoParsed))
  784. assert.Contains(t, userinfoParsed.Email, "user2@example.com")
  785. // test both id_token and call to /login/oauth/userinfo
  786. for _, group := range []string{
  787. "org17",
  788. "org17:test_team",
  789. "org3",
  790. "org3:owners",
  791. "org3:team1",
  792. "org3:teamcreaterepo",
  793. "private_org35",
  794. "private_org35:team24",
  795. } {
  796. assert.Contains(t, claims.Groups, group)
  797. assert.Contains(t, userinfoParsed.Groups, group)
  798. }
  799. }
  800. func testOAuth2WellKnown(t *testing.T) {
  801. urlOpenidConfiguration := "/.well-known/openid-configuration"
  802. defer test.MockVariableValue(&setting.AppURL, "https://try.gitea.io/")()
  803. req := NewRequest(t, "GET", urlOpenidConfiguration)
  804. resp := MakeRequest(t, req, http.StatusOK)
  805. var respMap map[string]any
  806. DecodeJSON(t, resp, &respMap)
  807. assert.Equal(t, "https://try.gitea.io", respMap["issuer"])
  808. assert.Equal(t, "https://try.gitea.io/login/oauth/authorize", respMap["authorization_endpoint"])
  809. assert.Equal(t, "https://try.gitea.io/login/oauth/access_token", respMap["token_endpoint"])
  810. assert.Equal(t, "https://try.gitea.io/login/oauth/keys", respMap["jwks_uri"])
  811. assert.Equal(t, "https://try.gitea.io/login/oauth/userinfo", respMap["userinfo_endpoint"])
  812. assert.Equal(t, "https://try.gitea.io/login/oauth/introspect", respMap["introspection_endpoint"])
  813. assert.Equal(t, []any{"RS256"}, respMap["id_token_signing_alg_values_supported"])
  814. defer test.MockVariableValue(&setting.OAuth2.Enabled, false)()
  815. MakeRequest(t, NewRequest(t, "GET", urlOpenidConfiguration), http.StatusNotFound)
  816. }
  817. func addOAuth2Source(t *testing.T, authName string, cfg oauth2.Source) {
  818. cfg.Provider = util.IfZero(cfg.Provider, "gitea")
  819. err := auth_model.CreateSource(t.Context(), &auth_model.Source{
  820. Type: auth_model.OAuth2,
  821. Name: authName,
  822. IsActive: true,
  823. Cfg: &cfg,
  824. })
  825. require.NoError(t, err)
  826. }
  827. func TestSignInOauthCallbackSyncSSHKeys(t *testing.T) {
  828. defer tests.PrepareTestEnv(t)()
  829. var mockServer *httptest.Server
  830. mockServer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  831. switch r.URL.Path {
  832. case "/.well-known/openid-configuration":
  833. _, _ = w.Write([]byte(`{
  834. "issuer": "` + mockServer.URL + `",
  835. "authorization_endpoint": "` + mockServer.URL + `/authorize",
  836. "token_endpoint": "` + mockServer.URL + `/token",
  837. "userinfo_endpoint": "` + mockServer.URL + `/userinfo"
  838. }`))
  839. default:
  840. http.NotFound(w, r)
  841. }
  842. }))
  843. defer mockServer.Close()
  844. ctx := t.Context()
  845. oauth2Source := oauth2.Source{
  846. Provider: "openidConnect",
  847. ClientID: "test-client-id",
  848. SSHPublicKeyClaimName: "sshpubkey",
  849. FullNameClaimName: "name",
  850. OpenIDConnectAutoDiscoveryURL: mockServer.URL + "/.well-known/openid-configuration",
  851. }
  852. addOAuth2Source(t, "test-oidc-source", oauth2Source)
  853. authSource, err := auth_model.GetActiveOAuth2SourceByAuthName(ctx, "test-oidc-source")
  854. require.NoError(t, err)
  855. sshKey1 := "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICV0MGX/W9IvLA4FXpIuUcdDcbj5KX4syHgsTy7soVgf"
  856. sshKey2 := "sk-ssh-ed25519@openssh.com AAAAGnNrLXNzaC1lZDI1NTE5QG9wZW5zc2guY29tAAAAIE7kM1R02+4ertDKGKEDcKG0s+2vyDDcIvceJ0Gqv5f1AAAABHNzaDo="
  857. sshKey3 := "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEHjnNEfE88W1pvBLdV3otv28x760gdmPao3lVD5uAt9"
  858. cases := []struct {
  859. testName string
  860. mockFullName string
  861. mockRawData map[string]any
  862. expectedSSHPubKeys []string
  863. }{
  864. {
  865. testName: "Login1",
  866. mockFullName: "FullName1",
  867. mockRawData: map[string]any{"sshpubkey": []any{sshKey1 + " any-comment"}},
  868. expectedSSHPubKeys: []string{sshKey1},
  869. },
  870. {
  871. testName: "Login2",
  872. mockFullName: "FullName2",
  873. mockRawData: map[string]any{"sshpubkey": []any{sshKey2 + " any-comment", sshKey3}},
  874. expectedSSHPubKeys: []string{sshKey2, sshKey3},
  875. },
  876. {
  877. testName: "Login3",
  878. mockFullName: "FullName3",
  879. mockRawData: map[string]any{},
  880. expectedSSHPubKeys: []string{},
  881. },
  882. }
  883. session := emptyTestSession(t)
  884. for _, c := range cases {
  885. t.Run(c.testName, func(t *testing.T) {
  886. defer test.MockVariableValue(&setting.OAuth2Client.Username, "")()
  887. defer test.MockVariableValue(&setting.OAuth2Client.EnableAutoRegistration, true)()
  888. defer test.MockVariableValue(&gothic.CompleteUserAuth, func(res http.ResponseWriter, req *http.Request) (goth.User, error) {
  889. return goth.User{
  890. Provider: authSource.Cfg.(*oauth2.Source).Provider,
  891. UserID: "oidc-userid",
  892. Email: "oidc-email@example.com",
  893. RawData: c.mockRawData,
  894. Name: c.mockFullName,
  895. }, nil
  896. })()
  897. req := NewRequest(t, "GET", "/user/oauth2/test-oidc-source/callback?code=XYZ&state=XYZ")
  898. session.MakeRequest(t, req, http.StatusSeeOther)
  899. user := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "oidc-userid"})
  900. keys, _, err := db.FindAndCount[asymkey_model.PublicKey](ctx, asymkey_model.FindPublicKeyOptions{
  901. ListOptions: db.ListOptionsAll,
  902. OwnerID: user.ID,
  903. LoginSourceID: authSource.ID,
  904. })
  905. require.NoError(t, err)
  906. var sshPubKeys []string
  907. for _, key := range keys {
  908. sshPubKeys = append(sshPubKeys, key.Content)
  909. }
  910. assert.ElementsMatch(t, c.expectedSSHPubKeys, sshPubKeys)
  911. assert.Equal(t, c.mockFullName, user.FullName)
  912. })
  913. }
  914. }