// Copyright 2019 The Gitea Authors. All rights reserved. // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. package user import ( "fmt" "net/url" "github.com/dgrijalva/jwt-go" "github.com/go-macaron/binding" "code.gitea.io/gitea/models" "code.gitea.io/gitea/modules/auth" "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/util" ) const ( tplGrantAccess base.TplName = "user/auth/grant" tplGrantError base.TplName = "user/auth/grant_error" ) // TODO move error and responses to SDK or models // AuthorizeErrorCode represents an error code specified in RFC 6749 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 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) } // AccessTokenErrorCode represents an error code specified in RFC 6749 type AccessTokenErrorCode string const ( // AccessTokenErrorCodeInvalidRequest represents an error code specified in RFC 6749 AccessTokenErrorCodeInvalidRequest AccessTokenErrorCode = "invalid_request" // AccessTokenErrorCodeInvalidClient represents an error code specified in RFC 6749 AccessTokenErrorCodeInvalidClient = "invalid_client" // AccessTokenErrorCodeInvalidGrant represents an error code specified in RFC 6749 AccessTokenErrorCodeInvalidGrant = "invalid_grant" // AccessTokenErrorCodeUnauthorizedClient represents an error code specified in RFC 6749 AccessTokenErrorCodeUnauthorizedClient = "unauthorized_client" // AccessTokenErrorCodeUnsupportedGrantType represents an error code specified in RFC 6749 AccessTokenErrorCodeUnsupportedGrantType = "unsupported_grant_type" // AccessTokenErrorCodeInvalidScope represents an error code specified in RFC 6749 AccessTokenErrorCodeInvalidScope = "invalid_scope" ) // AccessTokenError represents an error response specified in RFC 6749 type AccessTokenError struct { ErrorCode AccessTokenErrorCode `json:"error" form:"error"` ErrorDescription string `json:"error_description"` } // Error returns the error message func (err AccessTokenError) Error() string { return fmt.Sprintf("%s: %s", err.ErrorCode, err.ErrorDescription) } // TokenType specifies the kind of token type TokenType string const ( // TokenTypeBearer represents a token type specified in RFC 6749 TokenTypeBearer TokenType = "bearer" // TokenTypeMAC represents a token type specified in RFC 6749 TokenTypeMAC = "mac" ) // AccessTokenResponse represents a successful access token response type AccessTokenResponse struct { AccessToken string `json:"access_token"` TokenType TokenType `json:"token_type"` ExpiresIn int64 `json:"expires_in"` // TODO implement RefreshToken RefreshToken string `json:"refresh_token"` } func newAccessTokenResponse(grant *models.OAuth2Grant) (*AccessTokenResponse, *AccessTokenError) { if err := grant.IncreaseCounter(); err != nil { return nil, &AccessTokenError{ ErrorCode: AccessTokenErrorCodeInvalidGrant, ErrorDescription: "cannot increase the grant counter", } } // generate access token to access the API expirationDate := util.TimeStampNow().Add(setting.OAuth2.AccessTokenExpirationTime) accessToken := &models.OAuth2Token{ GrantID: grant.ID, Type: models.TypeAccessToken, StandardClaims: jwt.StandardClaims{ ExpiresAt: expirationDate.AsTime().Unix(), }, } signedAccessToken, err := accessToken.SignToken() if err != nil { return nil, &AccessTokenError{ ErrorCode: AccessTokenErrorCodeInvalidRequest, ErrorDescription: "cannot sign token", } } // generate refresh token to request an access token after it expired later refreshExpirationDate := util.TimeStampNow().Add(setting.OAuth2.RefreshTokenExpirationTime * 60 * 60).AsTime().Unix() refreshToken := &models.OAuth2Token{ GrantID: grant.ID, Counter: grant.Counter, Type: models.TypeRefreshToken, StandardClaims: jwt.StandardClaims{ ExpiresAt: refreshExpirationDate, }, } signedRefreshToken, err := refreshToken.SignToken() if err != nil { return nil, &AccessTokenError{ ErrorCode: AccessTokenErrorCodeInvalidRequest, ErrorDescription: "cannot sign token", } } return &AccessTokenResponse{ AccessToken: signedAccessToken, TokenType: TokenTypeBearer, ExpiresIn: setting.OAuth2.AccessTokenExpirationTime, RefreshToken: signedRefreshToken, }, nil } // AuthorizeOAuth manages authorize requests func AuthorizeOAuth(ctx *context.Context, form auth.AuthorizationForm) { errs := binding.Errors{} errs = form.Validate(ctx.Context, errs) app, err := models.GetOAuth2ApplicationByClientID(form.ClientID) if err != nil { if models.IsErrOauthClientIDInvalid(err) { handleAuthorizeError(ctx, AuthorizeError{ ErrorCode: ErrorCodeUnauthorizedClient, ErrorDescription: "Client ID not registered", State: form.State, }, "") return } ctx.ServerError("GetOAuth2ApplicationByClientID", err) return } if err := app.LoadUser(); err != nil { ctx.ServerError("LoadUser", 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("CodeChallengeMethod", form.CodeChallenge); err != nil { handleAuthorizeError(ctx, AuthorizeError{ ErrorCode: ErrorCodeServerError, ErrorDescription: "cannot set code challenge", State: form.State, }, form.RedirectURI) return } break case "": break default: handleAuthorizeError(ctx, AuthorizeError{ ErrorCode: ErrorCodeInvalidRequest, ErrorDescription: "unsupported code challenge method", State: form.State, }, form.RedirectURI) return } grant, err := app.GetGrantByUserID(ctx.User.ID) if err != nil { handleServerError(ctx, form.State, form.RedirectURI) return } // Redirect if user already granted access if grant != nil { code, err := grant.GenerateNewAuthorizationCode(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 } ctx.Redirect(redirect.String(), 302) return } // show authorize page to grant access ctx.Data["Application"] = app ctx.Data["RedirectURI"] = form.RedirectURI ctx.Data["State"] = form.State ctx.Data["ApplicationUserLink"] = "@" + app.User.Name + "" ctx.Data["ApplicationRedirectDomainHTML"] = "" + form.RedirectURI + "" // TODO document SESSION <=> FORM ctx.Session.Set("client_id", app.ClientID) ctx.Session.Set("redirect_uri", form.RedirectURI) ctx.Session.Set("state", form.State) ctx.HTML(200, tplGrantAccess) } // GrantApplicationOAuth manages the post request submitted when a user grants access to an application func GrantApplicationOAuth(ctx *context.Context, form auth.GrantApplicationForm) { if ctx.Session.Get("client_id") != form.ClientID || ctx.Session.Get("state") != form.State || ctx.Session.Get("redirect_uri") != form.RedirectURI { ctx.Error(400) return } app, err := models.GetOAuth2ApplicationByClientID(form.ClientID) if err != nil { ctx.ServerError("GetOAuth2ApplicationByClientID", err) return } grant, err := app.CreateGrant(ctx.User.ID) if err != nil { handleAuthorizeError(ctx, AuthorizeError{ State: form.State, ErrorDescription: "cannot create grant for user", ErrorCode: ErrorCodeServerError, }, form.RedirectURI) return } var codeChallenge, codeChallengeMethod string codeChallenge, _ = ctx.Session.Get("CodeChallenge").(string) codeChallengeMethod, _ = ctx.Session.Get("CodeChallengeMethod").(string) code, err := grant.GenerateNewAuthorizationCode(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) } ctx.Redirect(redirect.String(), 302) } // AccessTokenOAuth manages all access token requests by the client func AccessTokenOAuth(ctx *context.Context, form auth.AccessTokenForm) { switch form.GrantType { case "refresh_token": handleRefreshToken(ctx, form) return case "authorization_code": handleAuthorizationCode(ctx, form) return default: handleAccessTokenError(ctx, AccessTokenError{ ErrorCode: AccessTokenErrorCodeUnsupportedGrantType, ErrorDescription: "Only refresh_token or authorization_code grant type is supported", }) } } func handleRefreshToken(ctx *context.Context, form auth.AccessTokenForm) { token, err := models.ParseOAuth2Token(form.RefreshToken) if err != nil { handleAccessTokenError(ctx, AccessTokenError{ ErrorCode: AccessTokenErrorCodeUnauthorizedClient, ErrorDescription: "client is not authorized", }) return } // get grant before increasing counter grant, err := models.GetOAuth2GrantByID(token.GrantID) if err != nil || grant == nil { handleAccessTokenError(ctx, AccessTokenError{ ErrorCode: AccessTokenErrorCodeInvalidGrant, ErrorDescription: "grant does not exist", }) return } // check if token got already used if grant.Counter != token.Counter || token.Counter == 0 { handleAccessTokenError(ctx, AccessTokenError{ ErrorCode: 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 := newAccessTokenResponse(grant) if tokenErr != nil { handleAccessTokenError(ctx, *tokenErr) return } ctx.JSON(200, accessToken) } func handleAuthorizationCode(ctx *context.Context, form auth.AccessTokenForm) { app, err := models.GetOAuth2ApplicationByClientID(form.ClientID) if err != nil { handleAccessTokenError(ctx, AccessTokenError{ ErrorCode: AccessTokenErrorCodeInvalidClient, ErrorDescription: "cannot load client", }) return } if !app.ValidateClientSecret([]byte(form.ClientSecret)) { handleAccessTokenError(ctx, AccessTokenError{ ErrorCode: AccessTokenErrorCodeUnauthorizedClient, ErrorDescription: "client is not authorized", }) return } if form.RedirectURI != "" && !app.ContainsRedirectURI(form.RedirectURI) { handleAccessTokenError(ctx, AccessTokenError{ ErrorCode: AccessTokenErrorCodeUnauthorizedClient, ErrorDescription: "client is not authorized", }) return } authorizationCode, err := models.GetOAuth2AuthorizationByCode(form.Code) if err != nil || authorizationCode == nil { handleAccessTokenError(ctx, AccessTokenError{ ErrorCode: AccessTokenErrorCodeUnauthorizedClient, ErrorDescription: "client is not authorized", }) return } // check if code verifier authorizes the client, PKCE support if !authorizationCode.ValidateCodeChallenge(form.CodeVerifier) { handleAccessTokenError(ctx, AccessTokenError{ ErrorCode: AccessTokenErrorCodeUnauthorizedClient, ErrorDescription: "client is not authorized", }) return } // check if granted for this application if authorizationCode.Grant.ApplicationID != app.ID { handleAccessTokenError(ctx, AccessTokenError{ ErrorCode: AccessTokenErrorCodeInvalidGrant, ErrorDescription: "invalid grant", }) return } // remove token from database to deny duplicate usage if err := authorizationCode.Invalidate(); err != nil { handleAccessTokenError(ctx, AccessTokenError{ ErrorCode: AccessTokenErrorCodeInvalidRequest, ErrorDescription: "cannot proceed your request", }) } resp, tokenErr := newAccessTokenResponse(authorizationCode.Grant) if tokenErr != nil { handleAccessTokenError(ctx, *tokenErr) return } // send successful response ctx.JSON(200, resp) } func handleAccessTokenError(ctx *context.Context, acErr AccessTokenError) { ctx.JSON(400, acErr) } func handleServerError(ctx *context.Context, state string, 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(400, 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(), 302) }