You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

oauth.go 19KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573
  1. // Copyright 2019 The Gitea Authors. All rights reserved.
  2. // Use of this source code is governed by a MIT-style
  3. // license that can be found in the LICENSE file.
  4. package user
  5. import (
  6. "encoding/base64"
  7. "fmt"
  8. "html"
  9. "net/http"
  10. "net/url"
  11. "strings"
  12. "code.gitea.io/gitea/models"
  13. "code.gitea.io/gitea/modules/base"
  14. "code.gitea.io/gitea/modules/context"
  15. "code.gitea.io/gitea/modules/log"
  16. "code.gitea.io/gitea/modules/setting"
  17. "code.gitea.io/gitea/modules/timeutil"
  18. "code.gitea.io/gitea/modules/web"
  19. "code.gitea.io/gitea/services/forms"
  20. "gitea.com/go-chi/binding"
  21. "github.com/dgrijalva/jwt-go"
  22. )
  23. const (
  24. tplGrantAccess base.TplName = "user/auth/grant"
  25. tplGrantError base.TplName = "user/auth/grant_error"
  26. )
  27. // TODO move error and responses to SDK or models
  28. // AuthorizeErrorCode represents an error code specified in RFC 6749
  29. type AuthorizeErrorCode string
  30. const (
  31. // ErrorCodeInvalidRequest represents the according error in RFC 6749
  32. ErrorCodeInvalidRequest AuthorizeErrorCode = "invalid_request"
  33. // ErrorCodeUnauthorizedClient represents the according error in RFC 6749
  34. ErrorCodeUnauthorizedClient AuthorizeErrorCode = "unauthorized_client"
  35. // ErrorCodeAccessDenied represents the according error in RFC 6749
  36. ErrorCodeAccessDenied AuthorizeErrorCode = "access_denied"
  37. // ErrorCodeUnsupportedResponseType represents the according error in RFC 6749
  38. ErrorCodeUnsupportedResponseType AuthorizeErrorCode = "unsupported_response_type"
  39. // ErrorCodeInvalidScope represents the according error in RFC 6749
  40. ErrorCodeInvalidScope AuthorizeErrorCode = "invalid_scope"
  41. // ErrorCodeServerError represents the according error in RFC 6749
  42. ErrorCodeServerError AuthorizeErrorCode = "server_error"
  43. // ErrorCodeTemporaryUnavailable represents the according error in RFC 6749
  44. ErrorCodeTemporaryUnavailable AuthorizeErrorCode = "temporarily_unavailable"
  45. )
  46. // AuthorizeError represents an error type specified in RFC 6749
  47. type AuthorizeError struct {
  48. ErrorCode AuthorizeErrorCode `json:"error" form:"error"`
  49. ErrorDescription string
  50. State string
  51. }
  52. // Error returns the error message
  53. func (err AuthorizeError) Error() string {
  54. return fmt.Sprintf("%s: %s", err.ErrorCode, err.ErrorDescription)
  55. }
  56. // AccessTokenErrorCode represents an error code specified in RFC 6749
  57. type AccessTokenErrorCode string
  58. const (
  59. // AccessTokenErrorCodeInvalidRequest represents an error code specified in RFC 6749
  60. AccessTokenErrorCodeInvalidRequest AccessTokenErrorCode = "invalid_request"
  61. // AccessTokenErrorCodeInvalidClient represents an error code specified in RFC 6749
  62. AccessTokenErrorCodeInvalidClient = "invalid_client"
  63. // AccessTokenErrorCodeInvalidGrant represents an error code specified in RFC 6749
  64. AccessTokenErrorCodeInvalidGrant = "invalid_grant"
  65. // AccessTokenErrorCodeUnauthorizedClient represents an error code specified in RFC 6749
  66. AccessTokenErrorCodeUnauthorizedClient = "unauthorized_client"
  67. // AccessTokenErrorCodeUnsupportedGrantType represents an error code specified in RFC 6749
  68. AccessTokenErrorCodeUnsupportedGrantType = "unsupported_grant_type"
  69. // AccessTokenErrorCodeInvalidScope represents an error code specified in RFC 6749
  70. AccessTokenErrorCodeInvalidScope = "invalid_scope"
  71. )
  72. // AccessTokenError represents an error response specified in RFC 6749
  73. type AccessTokenError struct {
  74. ErrorCode AccessTokenErrorCode `json:"error" form:"error"`
  75. ErrorDescription string `json:"error_description"`
  76. }
  77. // Error returns the error message
  78. func (err AccessTokenError) Error() string {
  79. return fmt.Sprintf("%s: %s", err.ErrorCode, err.ErrorDescription)
  80. }
  81. // TokenType specifies the kind of token
  82. type TokenType string
  83. const (
  84. // TokenTypeBearer represents a token type specified in RFC 6749
  85. TokenTypeBearer TokenType = "bearer"
  86. // TokenTypeMAC represents a token type specified in RFC 6749
  87. TokenTypeMAC = "mac"
  88. )
  89. // AccessTokenResponse represents a successful access token response
  90. type AccessTokenResponse struct {
  91. AccessToken string `json:"access_token"`
  92. TokenType TokenType `json:"token_type"`
  93. ExpiresIn int64 `json:"expires_in"`
  94. RefreshToken string `json:"refresh_token"`
  95. IDToken string `json:"id_token,omitempty"`
  96. }
  97. func newAccessTokenResponse(grant *models.OAuth2Grant, clientSecret string) (*AccessTokenResponse, *AccessTokenError) {
  98. if setting.OAuth2.InvalidateRefreshTokens {
  99. if err := grant.IncreaseCounter(); err != nil {
  100. return nil, &AccessTokenError{
  101. ErrorCode: AccessTokenErrorCodeInvalidGrant,
  102. ErrorDescription: "cannot increase the grant counter",
  103. }
  104. }
  105. }
  106. // generate access token to access the API
  107. expirationDate := timeutil.TimeStampNow().Add(setting.OAuth2.AccessTokenExpirationTime)
  108. accessToken := &models.OAuth2Token{
  109. GrantID: grant.ID,
  110. Type: models.TypeAccessToken,
  111. StandardClaims: jwt.StandardClaims{
  112. ExpiresAt: expirationDate.AsTime().Unix(),
  113. },
  114. }
  115. signedAccessToken, err := accessToken.SignToken()
  116. if err != nil {
  117. return nil, &AccessTokenError{
  118. ErrorCode: AccessTokenErrorCodeInvalidRequest,
  119. ErrorDescription: "cannot sign token",
  120. }
  121. }
  122. // generate refresh token to request an access token after it expired later
  123. refreshExpirationDate := timeutil.TimeStampNow().Add(setting.OAuth2.RefreshTokenExpirationTime * 60 * 60).AsTime().Unix()
  124. refreshToken := &models.OAuth2Token{
  125. GrantID: grant.ID,
  126. Counter: grant.Counter,
  127. Type: models.TypeRefreshToken,
  128. StandardClaims: jwt.StandardClaims{
  129. ExpiresAt: refreshExpirationDate,
  130. },
  131. }
  132. signedRefreshToken, err := refreshToken.SignToken()
  133. if err != nil {
  134. return nil, &AccessTokenError{
  135. ErrorCode: AccessTokenErrorCodeInvalidRequest,
  136. ErrorDescription: "cannot sign token",
  137. }
  138. }
  139. // generate OpenID Connect id_token
  140. signedIDToken := ""
  141. if grant.ScopeContains("openid") {
  142. app, err := models.GetOAuth2ApplicationByID(grant.ApplicationID)
  143. if err != nil {
  144. return nil, &AccessTokenError{
  145. ErrorCode: AccessTokenErrorCodeInvalidRequest,
  146. ErrorDescription: "cannot find application",
  147. }
  148. }
  149. idToken := &models.OIDCToken{
  150. StandardClaims: jwt.StandardClaims{
  151. ExpiresAt: expirationDate.AsTime().Unix(),
  152. Issuer: setting.AppURL,
  153. Audience: app.ClientID,
  154. Subject: fmt.Sprint(grant.UserID),
  155. },
  156. Nonce: grant.Nonce,
  157. }
  158. signedIDToken, err = idToken.SignToken(clientSecret)
  159. if err != nil {
  160. return nil, &AccessTokenError{
  161. ErrorCode: AccessTokenErrorCodeInvalidRequest,
  162. ErrorDescription: "cannot sign token",
  163. }
  164. }
  165. }
  166. return &AccessTokenResponse{
  167. AccessToken: signedAccessToken,
  168. TokenType: TokenTypeBearer,
  169. ExpiresIn: setting.OAuth2.AccessTokenExpirationTime,
  170. RefreshToken: signedRefreshToken,
  171. IDToken: signedIDToken,
  172. }, nil
  173. }
  174. // AuthorizeOAuth manages authorize requests
  175. func AuthorizeOAuth(ctx *context.Context) {
  176. form := web.GetForm(ctx).(*forms.AuthorizationForm)
  177. errs := binding.Errors{}
  178. errs = form.Validate(ctx.Req, errs)
  179. if len(errs) > 0 {
  180. errstring := ""
  181. for _, e := range errs {
  182. errstring += e.Error() + "\n"
  183. }
  184. ctx.ServerError("AuthorizeOAuth: Validate: ", fmt.Errorf("errors occurred during validation: %s", errstring))
  185. return
  186. }
  187. app, err := models.GetOAuth2ApplicationByClientID(form.ClientID)
  188. if err != nil {
  189. if models.IsErrOauthClientIDInvalid(err) {
  190. handleAuthorizeError(ctx, AuthorizeError{
  191. ErrorCode: ErrorCodeUnauthorizedClient,
  192. ErrorDescription: "Client ID not registered",
  193. State: form.State,
  194. }, "")
  195. return
  196. }
  197. ctx.ServerError("GetOAuth2ApplicationByClientID", err)
  198. return
  199. }
  200. if err := app.LoadUser(); err != nil {
  201. ctx.ServerError("LoadUser", err)
  202. return
  203. }
  204. if !app.ContainsRedirectURI(form.RedirectURI) {
  205. handleAuthorizeError(ctx, AuthorizeError{
  206. ErrorCode: ErrorCodeInvalidRequest,
  207. ErrorDescription: "Unregistered Redirect URI",
  208. State: form.State,
  209. }, "")
  210. return
  211. }
  212. if form.ResponseType != "code" {
  213. handleAuthorizeError(ctx, AuthorizeError{
  214. ErrorCode: ErrorCodeUnsupportedResponseType,
  215. ErrorDescription: "Only code response type is supported.",
  216. State: form.State,
  217. }, form.RedirectURI)
  218. return
  219. }
  220. // pkce support
  221. switch form.CodeChallengeMethod {
  222. case "S256":
  223. case "plain":
  224. if err := ctx.Session.Set("CodeChallengeMethod", form.CodeChallengeMethod); err != nil {
  225. handleAuthorizeError(ctx, AuthorizeError{
  226. ErrorCode: ErrorCodeServerError,
  227. ErrorDescription: "cannot set code challenge method",
  228. State: form.State,
  229. }, form.RedirectURI)
  230. return
  231. }
  232. if err := ctx.Session.Set("CodeChallengeMethod", form.CodeChallenge); err != nil {
  233. handleAuthorizeError(ctx, AuthorizeError{
  234. ErrorCode: ErrorCodeServerError,
  235. ErrorDescription: "cannot set code challenge",
  236. State: form.State,
  237. }, form.RedirectURI)
  238. return
  239. }
  240. // Here we're just going to try to release the session early
  241. if err := ctx.Session.Release(); err != nil {
  242. // we'll tolerate errors here as they *should* get saved elsewhere
  243. log.Error("Unable to save changes to the session: %v", err)
  244. }
  245. case "":
  246. break
  247. default:
  248. handleAuthorizeError(ctx, AuthorizeError{
  249. ErrorCode: ErrorCodeInvalidRequest,
  250. ErrorDescription: "unsupported code challenge method",
  251. State: form.State,
  252. }, form.RedirectURI)
  253. return
  254. }
  255. grant, err := app.GetGrantByUserID(ctx.User.ID)
  256. if err != nil {
  257. handleServerError(ctx, form.State, form.RedirectURI)
  258. return
  259. }
  260. // Redirect if user already granted access
  261. if grant != nil {
  262. code, err := grant.GenerateNewAuthorizationCode(form.RedirectURI, form.CodeChallenge, form.CodeChallengeMethod)
  263. if err != nil {
  264. handleServerError(ctx, form.State, form.RedirectURI)
  265. return
  266. }
  267. redirect, err := code.GenerateRedirectURI(form.State)
  268. if err != nil {
  269. handleServerError(ctx, form.State, form.RedirectURI)
  270. return
  271. }
  272. // Update nonce to reflect the new session
  273. if len(form.Nonce) > 0 {
  274. err := grant.SetNonce(form.Nonce)
  275. if err != nil {
  276. log.Error("Unable to update nonce: %v", err)
  277. }
  278. }
  279. ctx.Redirect(redirect.String(), 302)
  280. return
  281. }
  282. // show authorize page to grant access
  283. ctx.Data["Application"] = app
  284. ctx.Data["RedirectURI"] = form.RedirectURI
  285. ctx.Data["State"] = form.State
  286. ctx.Data["Scope"] = form.Scope
  287. ctx.Data["Nonce"] = form.Nonce
  288. ctx.Data["ApplicationUserLink"] = "<a href=\"" + html.EscapeString(setting.AppURL) + html.EscapeString(url.PathEscape(app.User.LowerName)) + "\">@" + html.EscapeString(app.User.Name) + "</a>"
  289. ctx.Data["ApplicationRedirectDomainHTML"] = "<strong>" + html.EscapeString(form.RedirectURI) + "</strong>"
  290. // TODO document SESSION <=> FORM
  291. err = ctx.Session.Set("client_id", app.ClientID)
  292. if err != nil {
  293. handleServerError(ctx, form.State, form.RedirectURI)
  294. log.Error(err.Error())
  295. return
  296. }
  297. err = ctx.Session.Set("redirect_uri", form.RedirectURI)
  298. if err != nil {
  299. handleServerError(ctx, form.State, form.RedirectURI)
  300. log.Error(err.Error())
  301. return
  302. }
  303. err = ctx.Session.Set("state", form.State)
  304. if err != nil {
  305. handleServerError(ctx, form.State, form.RedirectURI)
  306. log.Error(err.Error())
  307. return
  308. }
  309. // Here we're just going to try to release the session early
  310. if err := ctx.Session.Release(); err != nil {
  311. // we'll tolerate errors here as they *should* get saved elsewhere
  312. log.Error("Unable to save changes to the session: %v", err)
  313. }
  314. ctx.HTML(http.StatusOK, tplGrantAccess)
  315. }
  316. // GrantApplicationOAuth manages the post request submitted when a user grants access to an application
  317. func GrantApplicationOAuth(ctx *context.Context) {
  318. form := web.GetForm(ctx).(*forms.GrantApplicationForm)
  319. if ctx.Session.Get("client_id") != form.ClientID || ctx.Session.Get("state") != form.State ||
  320. ctx.Session.Get("redirect_uri") != form.RedirectURI {
  321. ctx.Error(http.StatusBadRequest)
  322. return
  323. }
  324. app, err := models.GetOAuth2ApplicationByClientID(form.ClientID)
  325. if err != nil {
  326. ctx.ServerError("GetOAuth2ApplicationByClientID", err)
  327. return
  328. }
  329. grant, err := app.CreateGrant(ctx.User.ID, form.Scope)
  330. if err != nil {
  331. handleAuthorizeError(ctx, AuthorizeError{
  332. State: form.State,
  333. ErrorDescription: "cannot create grant for user",
  334. ErrorCode: ErrorCodeServerError,
  335. }, form.RedirectURI)
  336. return
  337. }
  338. if len(form.Nonce) > 0 {
  339. err := grant.SetNonce(form.Nonce)
  340. if err != nil {
  341. log.Error("Unable to update nonce: %v", err)
  342. }
  343. }
  344. var codeChallenge, codeChallengeMethod string
  345. codeChallenge, _ = ctx.Session.Get("CodeChallenge").(string)
  346. codeChallengeMethod, _ = ctx.Session.Get("CodeChallengeMethod").(string)
  347. code, err := grant.GenerateNewAuthorizationCode(form.RedirectURI, codeChallenge, codeChallengeMethod)
  348. if err != nil {
  349. handleServerError(ctx, form.State, form.RedirectURI)
  350. return
  351. }
  352. redirect, err := code.GenerateRedirectURI(form.State)
  353. if err != nil {
  354. handleServerError(ctx, form.State, form.RedirectURI)
  355. return
  356. }
  357. ctx.Redirect(redirect.String(), 302)
  358. }
  359. // OIDCWellKnown generates JSON so OIDC clients know Gitea's capabilities
  360. func OIDCWellKnown(ctx *context.Context) {
  361. t := ctx.Render.TemplateLookup("user/auth/oidc_wellknown")
  362. ctx.Resp.Header().Set("Content-Type", "application/json")
  363. if err := t.Execute(ctx.Resp, ctx.Data); err != nil {
  364. log.Error("%v", err)
  365. ctx.Error(http.StatusInternalServerError)
  366. }
  367. }
  368. // AccessTokenOAuth manages all access token requests by the client
  369. func AccessTokenOAuth(ctx *context.Context) {
  370. form := *web.GetForm(ctx).(*forms.AccessTokenForm)
  371. if form.ClientID == "" {
  372. authHeader := ctx.Req.Header.Get("Authorization")
  373. authContent := strings.SplitN(authHeader, " ", 2)
  374. if len(authContent) == 2 && authContent[0] == "Basic" {
  375. payload, err := base64.StdEncoding.DecodeString(authContent[1])
  376. if err != nil {
  377. handleAccessTokenError(ctx, AccessTokenError{
  378. ErrorCode: AccessTokenErrorCodeInvalidRequest,
  379. ErrorDescription: "cannot parse basic auth header",
  380. })
  381. return
  382. }
  383. pair := strings.SplitN(string(payload), ":", 2)
  384. if len(pair) != 2 {
  385. handleAccessTokenError(ctx, AccessTokenError{
  386. ErrorCode: AccessTokenErrorCodeInvalidRequest,
  387. ErrorDescription: "cannot parse basic auth header",
  388. })
  389. return
  390. }
  391. form.ClientID = pair[0]
  392. form.ClientSecret = pair[1]
  393. }
  394. }
  395. switch form.GrantType {
  396. case "refresh_token":
  397. handleRefreshToken(ctx, form)
  398. return
  399. case "authorization_code":
  400. handleAuthorizationCode(ctx, form)
  401. return
  402. default:
  403. handleAccessTokenError(ctx, AccessTokenError{
  404. ErrorCode: AccessTokenErrorCodeUnsupportedGrantType,
  405. ErrorDescription: "Only refresh_token or authorization_code grant type is supported",
  406. })
  407. }
  408. }
  409. func handleRefreshToken(ctx *context.Context, form forms.AccessTokenForm) {
  410. token, err := models.ParseOAuth2Token(form.RefreshToken)
  411. if err != nil {
  412. handleAccessTokenError(ctx, AccessTokenError{
  413. ErrorCode: AccessTokenErrorCodeUnauthorizedClient,
  414. ErrorDescription: "client is not authorized",
  415. })
  416. return
  417. }
  418. // get grant before increasing counter
  419. grant, err := models.GetOAuth2GrantByID(token.GrantID)
  420. if err != nil || grant == nil {
  421. handleAccessTokenError(ctx, AccessTokenError{
  422. ErrorCode: AccessTokenErrorCodeInvalidGrant,
  423. ErrorDescription: "grant does not exist",
  424. })
  425. return
  426. }
  427. // check if token got already used
  428. if setting.OAuth2.InvalidateRefreshTokens && (grant.Counter != token.Counter || token.Counter == 0) {
  429. handleAccessTokenError(ctx, AccessTokenError{
  430. ErrorCode: AccessTokenErrorCodeUnauthorizedClient,
  431. ErrorDescription: "token was already used",
  432. })
  433. log.Warn("A client tried to use a refresh token for grant_id = %d was used twice!", grant.ID)
  434. return
  435. }
  436. accessToken, tokenErr := newAccessTokenResponse(grant, form.ClientSecret)
  437. if tokenErr != nil {
  438. handleAccessTokenError(ctx, *tokenErr)
  439. return
  440. }
  441. ctx.JSON(http.StatusOK, accessToken)
  442. }
  443. func handleAuthorizationCode(ctx *context.Context, form forms.AccessTokenForm) {
  444. app, err := models.GetOAuth2ApplicationByClientID(form.ClientID)
  445. if err != nil {
  446. handleAccessTokenError(ctx, AccessTokenError{
  447. ErrorCode: AccessTokenErrorCodeInvalidClient,
  448. ErrorDescription: fmt.Sprintf("cannot load client with client id: '%s'", form.ClientID),
  449. })
  450. return
  451. }
  452. if !app.ValidateClientSecret([]byte(form.ClientSecret)) {
  453. handleAccessTokenError(ctx, AccessTokenError{
  454. ErrorCode: AccessTokenErrorCodeUnauthorizedClient,
  455. ErrorDescription: "client is not authorized",
  456. })
  457. return
  458. }
  459. if form.RedirectURI != "" && !app.ContainsRedirectURI(form.RedirectURI) {
  460. handleAccessTokenError(ctx, AccessTokenError{
  461. ErrorCode: AccessTokenErrorCodeUnauthorizedClient,
  462. ErrorDescription: "client is not authorized",
  463. })
  464. return
  465. }
  466. authorizationCode, err := models.GetOAuth2AuthorizationByCode(form.Code)
  467. if err != nil || authorizationCode == nil {
  468. handleAccessTokenError(ctx, AccessTokenError{
  469. ErrorCode: AccessTokenErrorCodeUnauthorizedClient,
  470. ErrorDescription: "client is not authorized",
  471. })
  472. return
  473. }
  474. // check if code verifier authorizes the client, PKCE support
  475. if !authorizationCode.ValidateCodeChallenge(form.CodeVerifier) {
  476. handleAccessTokenError(ctx, AccessTokenError{
  477. ErrorCode: AccessTokenErrorCodeUnauthorizedClient,
  478. ErrorDescription: "client is not authorized",
  479. })
  480. return
  481. }
  482. // check if granted for this application
  483. if authorizationCode.Grant.ApplicationID != app.ID {
  484. handleAccessTokenError(ctx, AccessTokenError{
  485. ErrorCode: AccessTokenErrorCodeInvalidGrant,
  486. ErrorDescription: "invalid grant",
  487. })
  488. return
  489. }
  490. // remove token from database to deny duplicate usage
  491. if err := authorizationCode.Invalidate(); err != nil {
  492. handleAccessTokenError(ctx, AccessTokenError{
  493. ErrorCode: AccessTokenErrorCodeInvalidRequest,
  494. ErrorDescription: "cannot proceed your request",
  495. })
  496. }
  497. resp, tokenErr := newAccessTokenResponse(authorizationCode.Grant, form.ClientSecret)
  498. if tokenErr != nil {
  499. handleAccessTokenError(ctx, *tokenErr)
  500. return
  501. }
  502. // send successful response
  503. ctx.JSON(http.StatusOK, resp)
  504. }
  505. func handleAccessTokenError(ctx *context.Context, acErr AccessTokenError) {
  506. ctx.JSON(http.StatusBadRequest, acErr)
  507. }
  508. func handleServerError(ctx *context.Context, state string, redirectURI string) {
  509. handleAuthorizeError(ctx, AuthorizeError{
  510. ErrorCode: ErrorCodeServerError,
  511. ErrorDescription: "A server error occurred",
  512. State: state,
  513. }, redirectURI)
  514. }
  515. func handleAuthorizeError(ctx *context.Context, authErr AuthorizeError, redirectURI string) {
  516. if redirectURI == "" {
  517. log.Warn("Authorization failed: %v", authErr.ErrorDescription)
  518. ctx.Data["Error"] = authErr
  519. ctx.HTML(400, tplGrantError)
  520. return
  521. }
  522. redirect, err := url.Parse(redirectURI)
  523. if err != nil {
  524. ctx.ServerError("url.Parse", err)
  525. return
  526. }
  527. q := redirect.Query()
  528. q.Set("error", string(authErr.ErrorCode))
  529. q.Set("error_description", authErr.ErrorDescription)
  530. q.Set("state", authErr.State)
  531. redirect.RawQuery = q.Encode()
  532. ctx.Redirect(redirect.String(), 302)
  533. }