summaryrefslogtreecommitdiffstats
path: root/routers
diff options
context:
space:
mode:
authorJonas Franz <info@jonasfranz.software>2019-03-08 17:42:50 +0100
committertechknowlogick <matti@mdranta.net>2019-03-08 11:42:50 -0500
commite777c6bdc6f12f9152335f8bfd66b956aedc9957 (patch)
treeb79c9bc2d4f9402dcd15d993b088840e2fad8a54 /routers
parent9d3732dfd512273992855097bba1e909f098db23 (diff)
downloadgitea-e777c6bdc6f12f9152335f8bfd66b956aedc9957.tar.gz
gitea-e777c6bdc6f12f9152335f8bfd66b956aedc9957.zip
Integrate OAuth2 Provider (#5378)
Diffstat (limited to 'routers')
-rw-r--r--routers/install.go2
-rw-r--r--routers/routes/routes.go14
-rw-r--r--routers/user/oauth.go452
-rw-r--r--routers/user/setting/applications.go8
-rw-r--r--routers/user/setting/oauth2.go112
5 files changed, 587 insertions, 1 deletions
diff --git a/routers/install.go b/routers/install.go
index 5e97bcc93c..1344792661 100644
--- a/routers/install.go
+++ b/routers/install.go
@@ -266,7 +266,7 @@ func InstallPost(ctx *context.Context, form auth.InstallForm) {
cfg.Section("server").Key("LFS_START_SERVER").SetValue("true")
cfg.Section("server").Key("LFS_CONTENT_PATH").SetValue(form.LFSRootPath)
var secretKey string
- if secretKey, err = generate.NewLfsJwtSecret(); err != nil {
+ if secretKey, err = generate.NewJwtSecret(); err != nil {
ctx.RenderWithErr(ctx.Tr("install.lfs_jwt_secret_failed", err), tplInstall, &form)
return
}
diff --git a/routers/routes/routes.go b/routers/routes/routes.go
index b73b030a51..01da321e49 100644
--- a/routers/routes/routes.go
+++ b/routers/routes/routes.go
@@ -257,6 +257,14 @@ func RegisterRoutes(m *macaron.Macaron) {
})
}, reqSignOut)
+ m.Group("/login/oauth", func() {
+ m.Get("/authorize", bindIgnErr(auth.AuthorizationForm{}), user.AuthorizeOAuth)
+ m.Post("/grant", bindIgnErr(auth.GrantApplicationForm{}), user.GrantApplicationOAuth)
+ // TODO manage redirection
+ m.Post("/authorize", bindIgnErr(auth.AuthorizationForm{}), user.AuthorizeOAuth)
+ }, ignSignInAndCsrf, reqSignIn)
+ m.Post("/login/oauth/access_token", bindIgnErr(auth.AccessTokenForm{}), ignSignInAndCsrf, user.AccessTokenOAuth)
+
m.Group("/user/settings", func() {
m.Get("", userSetting.Profile)
m.Post("", bindIgnErr(auth.UpdateProfileForm{}), userSetting.ProfilePost)
@@ -291,6 +299,12 @@ func RegisterRoutes(m *macaron.Macaron) {
}, openIDSignInEnabled)
m.Post("/account_link", userSetting.DeleteAccountLink)
})
+ m.Group("/applications/oauth2", func() {
+ m.Get("/:id", userSetting.OAuth2ApplicationShow)
+ m.Post("/:id", bindIgnErr(auth.EditOAuth2ApplicationForm{}), userSetting.OAuthApplicationsEdit)
+ m.Post("", bindIgnErr(auth.EditOAuth2ApplicationForm{}), userSetting.OAuthApplicationsPost)
+ m.Post("/delete", userSetting.DeleteOAuth2Application)
+ })
m.Combo("/applications").Get(userSetting.Applications).
Post(bindIgnErr(auth.NewAccessTokenForm{}), userSetting.ApplicationsPost)
m.Post("/applications/delete", userSetting.DeleteApplication)
diff --git a/routers/user/oauth.go b/routers/user/oauth.go
new file mode 100644
index 0000000000..dbb3c4a391
--- /dev/null
+++ b/routers/user/oauth.go
@@ -0,0 +1,452 @@
+// 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"] = "<a href=\"" + setting.LocalURL + app.User.LowerName + "\">@" + app.User.Name + "</a>"
+ ctx.Data["ApplicationRedirectDomainHTML"] = "<strong>" + form.RedirectURI + "</strong>"
+ // 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)
+}
diff --git a/routers/user/setting/applications.go b/routers/user/setting/applications.go
index ac7252469a..bc8633f72d 100644
--- a/routers/user/setting/applications.go
+++ b/routers/user/setting/applications.go
@@ -74,4 +74,12 @@ func loadApplicationsData(ctx *context.Context) {
return
}
ctx.Data["Tokens"] = tokens
+ ctx.Data["EnableOAuth2"] = setting.OAuth2.Enable
+ if setting.OAuth2.Enable {
+ ctx.Data["Applications"], err = models.GetOAuth2ApplicationsByUserID(ctx.User.ID)
+ if err != nil {
+ ctx.ServerError("GetOAuth2ApplicationsByUserID", err)
+ return
+ }
+ }
}
diff --git a/routers/user/setting/oauth2.go b/routers/user/setting/oauth2.go
new file mode 100644
index 0000000000..7ae8e7b7f6
--- /dev/null
+++ b/routers/user/setting/oauth2.go
@@ -0,0 +1,112 @@
+// 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 setting
+
+import (
+ "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"
+)
+
+const (
+ tplSettingsOAuthApplications base.TplName = "user/settings/applications_oauth2_edit"
+)
+
+// OAuthApplicationsPost response for adding a oauth2 application
+func OAuthApplicationsPost(ctx *context.Context, form auth.EditOAuth2ApplicationForm) {
+ ctx.Data["Title"] = ctx.Tr("settings")
+ ctx.Data["PageIsSettingsApplications"] = true
+
+ if ctx.HasError() {
+ loadApplicationsData(ctx)
+
+ ctx.HTML(200, tplSettingsApplications)
+ return
+ }
+ // TODO validate redirect URI
+ app, err := models.CreateOAuth2Application(models.CreateOAuth2ApplicationOptions{
+ Name: form.Name,
+ RedirectURIs: []string{form.RedirectURI},
+ UserID: ctx.User.ID,
+ })
+ if err != nil {
+ ctx.ServerError("CreateOAuth2Application", err)
+ return
+ }
+ ctx.Flash.Success(ctx.Tr("settings.create_oauth2_application_success"))
+ ctx.Data["App"] = app
+ ctx.Data["ClientSecret"], err = app.GenerateClientSecret()
+ if err != nil {
+ ctx.ServerError("GenerateClientSecret", err)
+ return
+ }
+ ctx.HTML(200, tplSettingsOAuthApplications)
+}
+
+// OAuthApplicationsEdit response for editing oauth2 application
+func OAuthApplicationsEdit(ctx *context.Context, form auth.EditOAuth2ApplicationForm) {
+ ctx.Data["Title"] = ctx.Tr("settings")
+ ctx.Data["PageIsSettingsApplications"] = true
+
+ if ctx.HasError() {
+ loadApplicationsData(ctx)
+
+ ctx.HTML(200, tplSettingsApplications)
+ return
+ }
+ // TODO validate redirect URI
+ if err := models.UpdateOAuth2Application(models.UpdateOAuth2ApplicationOptions{
+ ID: ctx.ParamsInt64("id"),
+ Name: form.Name,
+ RedirectURIs: []string{form.RedirectURI},
+ UserID: ctx.User.ID,
+ }); err != nil {
+ ctx.ServerError("UpdateOAuth2Application", err)
+ return
+ }
+ var err error
+ if ctx.Data["App"], err = models.GetOAuth2ApplicationByID(ctx.ParamsInt64("id")); err != nil {
+ ctx.ServerError("GetOAuth2ApplicationByID", err)
+ return
+ }
+ ctx.Flash.Success(ctx.Tr("settings.update_oauth2_application_success"))
+ ctx.HTML(200, tplSettingsOAuthApplications)
+}
+
+// OAuth2ApplicationShow displays the given application
+func OAuth2ApplicationShow(ctx *context.Context) {
+ app, err := models.GetOAuth2ApplicationByID(ctx.ParamsInt64("id"))
+ if err != nil {
+ if models.IsErrOAuthApplicationNotFound(err) {
+ ctx.NotFound("Application not found", err)
+ return
+ }
+ ctx.ServerError("GetOAuth2ApplicationByID", err)
+ return
+ }
+ if app.UID != ctx.User.ID {
+ ctx.NotFound("Application not found", nil)
+ return
+ }
+ ctx.Data["App"] = app
+ ctx.HTML(200, tplSettingsOAuthApplications)
+}
+
+// DeleteOAuth2Application deletes the given oauth2 application
+func DeleteOAuth2Application(ctx *context.Context) {
+ if err := models.DeleteOAuth2Application(ctx.QueryInt64("id"), ctx.User.ID); err != nil {
+ ctx.ServerError("DeleteOAuth2Application", err)
+ return
+ }
+ log.Trace("OAuth2 Application deleted: %s", ctx.User.Name)
+
+ ctx.Flash.Success(ctx.Tr("settings.remove_oauth2_application_success"))
+ ctx.JSON(200, map[string]interface{}{
+ "redirect": setting.AppSubURL + "/user/settings/applications",
+ })
+}