| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680 |
- // Copyright 2024 The Gitea Authors. All rights reserved.
- // SPDX-License-Identifier: MIT
-
- package auth
-
- import (
- "fmt"
- "html"
- "html/template"
- "net/http"
- "net/url"
- "strconv"
- "strings"
-
- "code.gitea.io/gitea/models/auth"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/auth/httpauth"
- "code.gitea.io/gitea/modules/json"
- "code.gitea.io/gitea/modules/log"
- "code.gitea.io/gitea/modules/setting"
- "code.gitea.io/gitea/modules/templates"
- "code.gitea.io/gitea/modules/web"
- auth_service "code.gitea.io/gitea/services/auth"
- "code.gitea.io/gitea/services/context"
- "code.gitea.io/gitea/services/forms"
- "code.gitea.io/gitea/services/oauth2_provider"
-
- "gitea.com/go-chi/binding"
- jwt "github.com/golang-jwt/jwt/v5"
- )
-
- const (
- tplGrantAccess templates.TplName = "user/auth/grant"
- tplGrantError templates.TplName = "user/auth/grant_error"
- )
-
- // TODO move error and responses to SDK or models
-
- // AuthorizeErrorCode represents an error code specified in RFC 6749
- // https://datatracker.ietf.org/doc/html/rfc6749#section-4.2.2.1
- type AuthorizeErrorCode string
-
- const (
- // ErrorCodeInvalidRequest represents the according error in RFC 6749
- ErrorCodeInvalidRequest AuthorizeErrorCode = "invalid_request"
- // ErrorCodeUnauthorizedClient represents the according error in RFC 6749
- ErrorCodeUnauthorizedClient AuthorizeErrorCode = "unauthorized_client"
- // ErrorCodeAccessDenied represents the according error in RFC 6749
- ErrorCodeAccessDenied AuthorizeErrorCode = "access_denied"
- // ErrorCodeUnsupportedResponseType represents the according error in RFC 6749
- ErrorCodeUnsupportedResponseType AuthorizeErrorCode = "unsupported_response_type"
- // ErrorCodeInvalidScope represents the according error in RFC 6749
- ErrorCodeInvalidScope AuthorizeErrorCode = "invalid_scope"
- // ErrorCodeServerError represents the according error in RFC 6749
- ErrorCodeServerError AuthorizeErrorCode = "server_error"
- // ErrorCodeTemporaryUnavailable represents the according error in RFC 6749
- ErrorCodeTemporaryUnavailable AuthorizeErrorCode = "temporarily_unavailable"
- )
-
- // AuthorizeError represents an error type specified in RFC 6749
- // https://datatracker.ietf.org/doc/html/rfc6749#section-4.2.2.1
- type AuthorizeError struct {
- ErrorCode AuthorizeErrorCode `json:"error" form:"error"`
- ErrorDescription string
- State string
- }
-
- // Error returns the error message
- func (err AuthorizeError) Error() string {
- return fmt.Sprintf("%s: %s", err.ErrorCode, err.ErrorDescription)
- }
-
- // errCallback represents a oauth2 callback error
- type errCallback struct {
- Code string
- Description string
- }
-
- func (err errCallback) Error() string {
- return err.Description
- }
-
- type userInfoResponse struct {
- Sub string `json:"sub"`
- Name string `json:"name"`
- PreferredUsername string `json:"preferred_username"`
- Email string `json:"email"`
- Picture string `json:"picture"`
- Groups []string `json:"groups"`
- }
-
- // InfoOAuth manages request for userinfo endpoint
- func InfoOAuth(ctx *context.Context) {
- if ctx.Doer == nil || ctx.Data["AuthedMethod"] != (&auth_service.OAuth2{}).Name() {
- ctx.Resp.Header().Set("WWW-Authenticate", `Bearer realm="Gitea OAuth2"`)
- ctx.PlainText(http.StatusUnauthorized, "no valid authorization")
- return
- }
-
- response := &userInfoResponse{
- Sub: strconv.FormatInt(ctx.Doer.ID, 10),
- Name: ctx.Doer.DisplayName(),
- PreferredUsername: ctx.Doer.Name,
- Email: ctx.Doer.Email,
- Picture: ctx.Doer.AvatarLink(ctx),
- }
-
- var accessTokenScope auth.AccessTokenScope
- if auHead := ctx.Req.Header.Get("Authorization"); auHead != "" {
- if parsed, ok := httpauth.ParseAuthorizationHeader(auHead); ok && parsed.BearerToken != nil {
- accessTokenScope, _ = auth_service.GetOAuthAccessTokenScopeAndUserID(ctx, parsed.BearerToken.Token)
- }
- }
-
- // since version 1.22 does not verify if groups should be public-only,
- // onlyPublicGroups will be set only if 'public-only' is included in a valid scope
- onlyPublicGroups, _ := accessTokenScope.PublicOnly()
- groups, err := oauth2_provider.GetOAuthGroupsForUser(ctx, ctx.Doer, onlyPublicGroups)
- if err != nil {
- ctx.ServerError("Oauth groups for user", err)
- return
- }
- response.Groups = groups
-
- ctx.JSON(http.StatusOK, response)
- }
-
- // IntrospectOAuth introspects an oauth token
- func IntrospectOAuth(ctx *context.Context) {
- clientIDValid := false
- authHeader := ctx.Req.Header.Get("Authorization")
- if parsed, ok := httpauth.ParseAuthorizationHeader(authHeader); ok && parsed.BasicAuth != nil {
- clientID, clientSecret := parsed.BasicAuth.Username, parsed.BasicAuth.Password
- app, err := auth.GetOAuth2ApplicationByClientID(ctx, clientID)
- if err != nil && !auth.IsErrOauthClientIDInvalid(err) {
- // this is likely a database error; log it and respond without details
- log.Error("Error retrieving client_id: %v", err)
- ctx.HTTPError(http.StatusInternalServerError)
- return
- }
- clientIDValid = err == nil && app.ValidateClientSecret([]byte(clientSecret))
- }
- if !clientIDValid {
- ctx.Resp.Header().Set("WWW-Authenticate", `Basic realm="Gitea OAuth2"`)
- ctx.PlainText(http.StatusUnauthorized, "no valid authorization")
- return
- }
-
- var response struct {
- Active bool `json:"active"`
- Scope string `json:"scope,omitempty"`
- Username string `json:"username,omitempty"`
- jwt.RegisteredClaims
- }
-
- form := web.GetForm(ctx).(*forms.IntrospectTokenForm)
- token, err := oauth2_provider.ParseToken(form.Token, oauth2_provider.DefaultSigningKey)
- if err == nil {
- grant, err := auth.GetOAuth2GrantByID(ctx, token.GrantID)
- if err == nil && grant != nil {
- app, err := auth.GetOAuth2ApplicationByID(ctx, grant.ApplicationID)
- if err == nil && app != nil {
- response.Active = true
- response.Scope = grant.Scope
- response.RegisteredClaims = oauth2_provider.NewJwtRegisteredClaimsFromUser(app.ClientID, grant.UserID, nil /*exp*/)
- }
- if user, err := user_model.GetUserByID(ctx, grant.UserID); err == nil {
- response.Username = user.Name
- }
- }
- }
-
- ctx.JSON(http.StatusOK, response)
- }
-
- // AuthorizeOAuth manages authorize requests
- func AuthorizeOAuth(ctx *context.Context) {
- form := web.GetForm(ctx).(*forms.AuthorizationForm)
- errs := binding.Errors{}
- errs = form.Validate(ctx.Req, errs)
- if len(errs) > 0 {
- errstring := ""
- for _, e := range errs {
- errstring += e.Error() + "\n"
- }
- ctx.ServerError("AuthorizeOAuth: Validate: ", fmt.Errorf("errors occurred during validation: %s", errstring))
- return
- }
-
- app, err := auth.GetOAuth2ApplicationByClientID(ctx, form.ClientID)
- if err != nil {
- if auth.IsErrOauthClientIDInvalid(err) {
- handleAuthorizeError(ctx, AuthorizeError{
- ErrorCode: ErrorCodeUnauthorizedClient,
- ErrorDescription: "Client ID not registered",
- State: form.State,
- }, "")
- return
- }
- ctx.ServerError("GetOAuth2ApplicationByClientID", err)
- return
- }
-
- var user *user_model.User
- if app.UID != 0 {
- user, err = user_model.GetUserByID(ctx, app.UID)
- if err != nil {
- ctx.ServerError("GetUserByID", err)
- return
- }
- }
-
- if !app.ContainsRedirectURI(form.RedirectURI) {
- handleAuthorizeError(ctx, AuthorizeError{
- ErrorCode: ErrorCodeInvalidRequest,
- ErrorDescription: "Unregistered Redirect URI",
- State: form.State,
- }, "")
- return
- }
-
- if form.ResponseType != "code" {
- handleAuthorizeError(ctx, AuthorizeError{
- ErrorCode: ErrorCodeUnsupportedResponseType,
- ErrorDescription: "Only code response type is supported.",
- State: form.State,
- }, form.RedirectURI)
- return
- }
-
- // pkce support
- switch form.CodeChallengeMethod {
- case "S256":
- case "plain":
- if err := ctx.Session.Set("CodeChallengeMethod", form.CodeChallengeMethod); err != nil {
- handleAuthorizeError(ctx, AuthorizeError{
- ErrorCode: ErrorCodeServerError,
- ErrorDescription: "cannot set code challenge method",
- State: form.State,
- }, form.RedirectURI)
- return
- }
- if err := ctx.Session.Set("CodeChallenge", form.CodeChallenge); err != nil {
- handleAuthorizeError(ctx, AuthorizeError{
- ErrorCode: ErrorCodeServerError,
- ErrorDescription: "cannot set code challenge",
- State: form.State,
- }, form.RedirectURI)
- return
- }
- // Here we're just going to try to release the session early
- if err := ctx.Session.Release(); err != nil {
- // we'll tolerate errors here as they *should* get saved elsewhere
- log.Error("Unable to save changes to the session: %v", err)
- }
- case "":
- // "Authorization servers SHOULD reject authorization requests from native apps that don't use PKCE by returning an error message"
- // https://datatracker.ietf.org/doc/html/rfc8252#section-8.1
- if !app.ConfidentialClient {
- // "the authorization endpoint MUST return the authorization error response with the "error" value set to "invalid_request""
- // https://datatracker.ietf.org/doc/html/rfc7636#section-4.4.1
- handleAuthorizeError(ctx, AuthorizeError{
- ErrorCode: ErrorCodeInvalidRequest,
- ErrorDescription: "PKCE is required for public clients",
- State: form.State,
- }, form.RedirectURI)
- return
- }
- default:
- // "If the server supporting PKCE does not support the requested transformation, the authorization endpoint MUST return the authorization error response with "error" value set to "invalid_request"."
- // https://www.rfc-editor.org/rfc/rfc7636#section-4.4.1
- handleAuthorizeError(ctx, AuthorizeError{
- ErrorCode: ErrorCodeInvalidRequest,
- ErrorDescription: "unsupported code challenge method",
- State: form.State,
- }, form.RedirectURI)
- return
- }
-
- grant, err := app.GetGrantByUserID(ctx, ctx.Doer.ID)
- if err != nil {
- handleServerError(ctx, form.State, form.RedirectURI)
- return
- }
-
- // Redirect if user already granted access and the application is confidential or trusted otherwise
- // I.e. always require authorization for untrusted public clients as recommended by RFC 6749 Section 10.2
- if (app.ConfidentialClient || app.SkipSecondaryAuthorization) && grant != nil {
- code, err := grant.GenerateNewAuthorizationCode(ctx, form.RedirectURI, form.CodeChallenge, form.CodeChallengeMethod)
- if err != nil {
- handleServerError(ctx, form.State, form.RedirectURI)
- return
- }
- redirect, err := code.GenerateRedirectURI(form.State)
- if err != nil {
- handleServerError(ctx, form.State, form.RedirectURI)
- return
- }
- // Update nonce to reflect the new session
- if len(form.Nonce) > 0 {
- err := grant.SetNonce(ctx, form.Nonce)
- if err != nil {
- log.Error("Unable to update nonce: %v", err)
- }
- }
- ctx.Redirect(redirect.String())
- return
- }
-
- // check if additional scopes
- ctx.Data["AdditionalScopes"] = oauth2_provider.GrantAdditionalScopes(form.Scope) != auth.AccessTokenScopeAll
-
- // show authorize page to grant access
- ctx.Data["Application"] = app
- ctx.Data["RedirectURI"] = form.RedirectURI
- ctx.Data["State"] = form.State
- ctx.Data["Scope"] = form.Scope
- ctx.Data["Nonce"] = form.Nonce
- if user != nil {
- ctx.Data["ApplicationCreatorLinkHTML"] = template.HTML(fmt.Sprintf(`<a href="%s">@%s</a>`, html.EscapeString(user.HomeLink()), html.EscapeString(user.Name)))
- } else {
- ctx.Data["ApplicationCreatorLinkHTML"] = template.HTML(fmt.Sprintf(`<a href="%s">%s</a>`, html.EscapeString(setting.AppSubURL+"/"), html.EscapeString(setting.AppName)))
- }
- ctx.Data["ApplicationRedirectDomainHTML"] = template.HTML("<strong>" + html.EscapeString(form.RedirectURI) + "</strong>")
- // TODO document SESSION <=> FORM
- err = ctx.Session.Set("client_id", app.ClientID)
- if err != nil {
- handleServerError(ctx, form.State, form.RedirectURI)
- log.Error(err.Error())
- return
- }
- err = ctx.Session.Set("redirect_uri", form.RedirectURI)
- if err != nil {
- handleServerError(ctx, form.State, form.RedirectURI)
- log.Error(err.Error())
- return
- }
- err = ctx.Session.Set("state", form.State)
- if err != nil {
- handleServerError(ctx, form.State, form.RedirectURI)
- log.Error(err.Error())
- return
- }
- // Here we're just going to try to release the session early
- if err := ctx.Session.Release(); err != nil {
- // we'll tolerate errors here as they *should* get saved elsewhere
- log.Error("Unable to save changes to the session: %v", err)
- }
- ctx.HTML(http.StatusOK, tplGrantAccess)
- }
-
- // GrantApplicationOAuth manages the post request submitted when a user grants access to an application
- func GrantApplicationOAuth(ctx *context.Context) {
- form := web.GetForm(ctx).(*forms.GrantApplicationForm)
- if ctx.Session.Get("client_id") != form.ClientID || ctx.Session.Get("state") != form.State ||
- ctx.Session.Get("redirect_uri") != form.RedirectURI {
- ctx.HTTPError(http.StatusBadRequest)
- return
- }
-
- if !form.Granted {
- handleAuthorizeError(ctx, AuthorizeError{
- State: form.State,
- ErrorDescription: "the request is denied",
- ErrorCode: ErrorCodeAccessDenied,
- }, form.RedirectURI)
- return
- }
-
- app, err := auth.GetOAuth2ApplicationByClientID(ctx, form.ClientID)
- if err != nil {
- ctx.ServerError("GetOAuth2ApplicationByClientID", err)
- return
- }
- grant, err := app.GetGrantByUserID(ctx, ctx.Doer.ID)
- if err != nil {
- handleServerError(ctx, form.State, form.RedirectURI)
- return
- }
- if grant == nil {
- grant, err = app.CreateGrant(ctx, ctx.Doer.ID, form.Scope)
- if err != nil {
- handleAuthorizeError(ctx, AuthorizeError{
- State: form.State,
- ErrorDescription: "cannot create grant for user",
- ErrorCode: ErrorCodeServerError,
- }, form.RedirectURI)
- return
- }
- } else if grant.Scope != form.Scope {
- handleAuthorizeError(ctx, AuthorizeError{
- State: form.State,
- ErrorDescription: "a grant exists with different scope",
- ErrorCode: ErrorCodeServerError,
- }, form.RedirectURI)
- return
- }
-
- if len(form.Nonce) > 0 {
- err := grant.SetNonce(ctx, form.Nonce)
- if err != nil {
- log.Error("Unable to update nonce: %v", err)
- }
- }
-
- var codeChallenge, codeChallengeMethod string
- codeChallenge, _ = ctx.Session.Get("CodeChallenge").(string)
- codeChallengeMethod, _ = ctx.Session.Get("CodeChallengeMethod").(string)
-
- code, err := grant.GenerateNewAuthorizationCode(ctx, form.RedirectURI, codeChallenge, codeChallengeMethod)
- if err != nil {
- handleServerError(ctx, form.State, form.RedirectURI)
- return
- }
- redirect, err := code.GenerateRedirectURI(form.State)
- if err != nil {
- handleServerError(ctx, form.State, form.RedirectURI)
- return
- }
- ctx.Redirect(redirect.String(), http.StatusSeeOther)
- }
-
- // OIDCWellKnown generates JSON so OIDC clients know Gitea's capabilities
- func OIDCWellKnown(ctx *context.Context) {
- if !setting.OAuth2.Enabled {
- http.NotFound(ctx.Resp, ctx.Req)
- return
- }
- jwtRegisteredClaims := oauth2_provider.NewJwtRegisteredClaimsFromUser("well-known", 0, nil)
- ctx.Data["OidcIssuer"] = jwtRegisteredClaims.Issuer // use the consistent issuer from the JWT registered claims
- ctx.Data["OidcBaseUrl"] = strings.TrimSuffix(setting.AppURL, "/")
- ctx.Data["SigningKeyMethodAlg"] = oauth2_provider.DefaultSigningKey.SigningMethod().Alg()
- ctx.JSONTemplate("user/auth/oidc_wellknown")
- }
-
- // OIDCKeys generates the JSON Web Key Set
- func OIDCKeys(ctx *context.Context) {
- jwk, err := oauth2_provider.DefaultSigningKey.ToJWK()
- if err != nil {
- log.Error("Error converting signing key to JWK: %v", err)
- ctx.HTTPError(http.StatusInternalServerError)
- return
- }
-
- jwk["use"] = "sig"
-
- jwks := map[string][]map[string]string{
- "keys": {
- jwk,
- },
- }
-
- ctx.Resp.Header().Set("Content-Type", "application/json")
- enc := json.NewEncoder(ctx.Resp)
- if err := enc.Encode(jwks); err != nil {
- log.Error("Failed to encode representation as json. Error: %v", err)
- }
- }
-
- // AccessTokenOAuth manages all access token requests by the client
- func AccessTokenOAuth(ctx *context.Context) {
- form := *web.GetForm(ctx).(*forms.AccessTokenForm)
- // if there is no ClientID or ClientSecret in the request body, fill these fields by the Authorization header and ensure the provided field matches the Authorization header
- if form.ClientID == "" || form.ClientSecret == "" {
- if authHeader := ctx.Req.Header.Get("Authorization"); authHeader != "" {
- parsed, ok := httpauth.ParseAuthorizationHeader(authHeader)
- if !ok || parsed.BasicAuth == nil {
- handleAccessTokenError(ctx, oauth2_provider.AccessTokenError{
- ErrorCode: oauth2_provider.AccessTokenErrorCodeInvalidRequest,
- ErrorDescription: "cannot parse basic auth header",
- })
- return
- }
- clientID, clientSecret := parsed.BasicAuth.Username, parsed.BasicAuth.Password
- // validate that any fields present in the form match the Basic auth header
- if form.ClientID != "" && form.ClientID != clientID {
- handleAccessTokenError(ctx, oauth2_provider.AccessTokenError{
- ErrorCode: oauth2_provider.AccessTokenErrorCodeInvalidRequest,
- ErrorDescription: "client_id in request body inconsistent with Authorization header",
- })
- return
- }
- form.ClientID = clientID
- if form.ClientSecret != "" && form.ClientSecret != clientSecret {
- handleAccessTokenError(ctx, oauth2_provider.AccessTokenError{
- ErrorCode: oauth2_provider.AccessTokenErrorCodeInvalidRequest,
- ErrorDescription: "client_secret in request body inconsistent with Authorization header",
- })
- return
- }
- form.ClientSecret = clientSecret
- }
- }
-
- serverKey := oauth2_provider.DefaultSigningKey
- clientKey := serverKey
- if serverKey.IsSymmetric() {
- var err error
- clientKey, err = oauth2_provider.CreateJWTSigningKey(serverKey.SigningMethod().Alg(), []byte(form.ClientSecret))
- if err != nil {
- handleAccessTokenError(ctx, oauth2_provider.AccessTokenError{
- ErrorCode: oauth2_provider.AccessTokenErrorCodeInvalidRequest,
- ErrorDescription: "Error creating signing key",
- })
- return
- }
- }
-
- switch form.GrantType {
- case "refresh_token":
- handleRefreshToken(ctx, form, serverKey, clientKey)
- case "authorization_code":
- handleAuthorizationCode(ctx, form, serverKey, clientKey)
- default:
- handleAccessTokenError(ctx, oauth2_provider.AccessTokenError{
- ErrorCode: oauth2_provider.AccessTokenErrorCodeUnsupportedGrantType,
- ErrorDescription: "Only refresh_token or authorization_code grant type is supported",
- })
- }
- }
-
- func handleRefreshToken(ctx *context.Context, form forms.AccessTokenForm, serverKey, clientKey oauth2_provider.JWTSigningKey) {
- app, err := auth.GetOAuth2ApplicationByClientID(ctx, form.ClientID)
- if err != nil {
- handleAccessTokenError(ctx, oauth2_provider.AccessTokenError{
- ErrorCode: oauth2_provider.AccessTokenErrorCodeInvalidClient,
- ErrorDescription: fmt.Sprintf("cannot load client with client id: %q", form.ClientID),
- })
- return
- }
- // "The authorization server MUST ... require client authentication for confidential clients"
- // https://datatracker.ietf.org/doc/html/rfc6749#section-6
- if app.ConfidentialClient && !app.ValidateClientSecret([]byte(form.ClientSecret)) {
- errorDescription := "invalid client secret"
- if form.ClientSecret == "" {
- errorDescription = "invalid empty client secret"
- }
- // "invalid_client ... Client authentication failed"
- // https://datatracker.ietf.org/doc/html/rfc6749#section-5.2
- handleAccessTokenError(ctx, oauth2_provider.AccessTokenError{
- ErrorCode: oauth2_provider.AccessTokenErrorCodeInvalidClient,
- ErrorDescription: errorDescription,
- })
- return
- }
-
- token, err := oauth2_provider.ParseToken(form.RefreshToken, serverKey)
- if err != nil {
- handleAccessTokenError(ctx, oauth2_provider.AccessTokenError{
- ErrorCode: oauth2_provider.AccessTokenErrorCodeUnauthorizedClient,
- ErrorDescription: "unable to parse refresh token",
- })
- return
- }
- // get grant before increasing counter
- grant, err := auth.GetOAuth2GrantByID(ctx, token.GrantID)
- if err != nil || grant == nil {
- handleAccessTokenError(ctx, oauth2_provider.AccessTokenError{
- ErrorCode: oauth2_provider.AccessTokenErrorCodeInvalidGrant,
- ErrorDescription: "grant does not exist",
- })
- return
- }
-
- // check if token got already used
- if setting.OAuth2.InvalidateRefreshTokens && (grant.Counter != token.Counter || token.Counter == 0) {
- handleAccessTokenError(ctx, oauth2_provider.AccessTokenError{
- ErrorCode: oauth2_provider.AccessTokenErrorCodeUnauthorizedClient,
- ErrorDescription: "token was already used",
- })
- log.Warn("A client tried to use a refresh token for grant_id = %d was used twice!", grant.ID)
- return
- }
- accessToken, tokenErr := oauth2_provider.NewAccessTokenResponse(ctx, grant, serverKey, clientKey)
- if tokenErr != nil {
- handleAccessTokenError(ctx, *tokenErr)
- return
- }
- ctx.JSON(http.StatusOK, accessToken)
- }
-
- func handleAuthorizationCode(ctx *context.Context, form forms.AccessTokenForm, serverKey, clientKey oauth2_provider.JWTSigningKey) {
- app, err := auth.GetOAuth2ApplicationByClientID(ctx, form.ClientID)
- if err != nil {
- handleAccessTokenError(ctx, oauth2_provider.AccessTokenError{
- ErrorCode: oauth2_provider.AccessTokenErrorCodeInvalidClient,
- ErrorDescription: fmt.Sprintf("cannot load client with client id: '%s'", form.ClientID),
- })
- return
- }
- if app.ConfidentialClient && !app.ValidateClientSecret([]byte(form.ClientSecret)) {
- errorDescription := "invalid client secret"
- if form.ClientSecret == "" {
- errorDescription = "invalid empty client secret"
- }
- handleAccessTokenError(ctx, oauth2_provider.AccessTokenError{
- ErrorCode: oauth2_provider.AccessTokenErrorCodeUnauthorizedClient,
- ErrorDescription: errorDescription,
- })
- return
- }
- if form.RedirectURI != "" && !app.ContainsRedirectURI(form.RedirectURI) {
- handleAccessTokenError(ctx, oauth2_provider.AccessTokenError{
- ErrorCode: oauth2_provider.AccessTokenErrorCodeUnauthorizedClient,
- ErrorDescription: "unexpected redirect URI",
- })
- return
- }
- authorizationCode, err := auth.GetOAuth2AuthorizationByCode(ctx, form.Code)
- if err != nil || authorizationCode == nil {
- handleAccessTokenError(ctx, oauth2_provider.AccessTokenError{
- ErrorCode: oauth2_provider.AccessTokenErrorCodeUnauthorizedClient,
- ErrorDescription: "client is not authorized",
- })
- return
- }
- // check if code verifier authorizes the client, PKCE support
- if !authorizationCode.ValidateCodeChallenge(form.CodeVerifier) {
- handleAccessTokenError(ctx, oauth2_provider.AccessTokenError{
- ErrorCode: oauth2_provider.AccessTokenErrorCodeUnauthorizedClient,
- ErrorDescription: "failed PKCE code challenge",
- })
- return
- }
- // check if granted for this application
- if authorizationCode.Grant.ApplicationID != app.ID {
- handleAccessTokenError(ctx, oauth2_provider.AccessTokenError{
- ErrorCode: oauth2_provider.AccessTokenErrorCodeInvalidGrant,
- ErrorDescription: "invalid grant",
- })
- return
- }
- // remove token from database to deny duplicate usage
- if err := authorizationCode.Invalidate(ctx); err != nil {
- handleAccessTokenError(ctx, oauth2_provider.AccessTokenError{
- ErrorCode: oauth2_provider.AccessTokenErrorCodeInvalidRequest,
- ErrorDescription: "cannot proceed your request",
- })
- }
- resp, tokenErr := oauth2_provider.NewAccessTokenResponse(ctx, authorizationCode.Grant, serverKey, clientKey)
- if tokenErr != nil {
- handleAccessTokenError(ctx, *tokenErr)
- return
- }
- // send successful response
- ctx.JSON(http.StatusOK, resp)
- }
-
- func handleAccessTokenError(ctx *context.Context, acErr oauth2_provider.AccessTokenError) {
- ctx.JSON(http.StatusBadRequest, acErr)
- }
-
- func handleServerError(ctx *context.Context, state, redirectURI string) {
- handleAuthorizeError(ctx, AuthorizeError{
- ErrorCode: ErrorCodeServerError,
- ErrorDescription: "A server error occurred",
- State: state,
- }, redirectURI)
- }
-
- func handleAuthorizeError(ctx *context.Context, authErr AuthorizeError, redirectURI string) {
- if redirectURI == "" {
- log.Warn("Authorization failed: %v", authErr.ErrorDescription)
- ctx.Data["Error"] = authErr
- ctx.HTML(http.StatusBadRequest, tplGrantError)
- return
- }
- redirect, err := url.Parse(redirectURI)
- if err != nil {
- ctx.ServerError("url.Parse", err)
- return
- }
- q := redirect.Query()
- q.Set("error", string(authErr.ErrorCode))
- q.Set("error_description", authErr.ErrorDescription)
- q.Set("state", authErr.State)
- redirect.RawQuery = q.Encode()
- ctx.Redirect(redirect.String(), http.StatusSeeOther)
- }
|