From c798a44f697c69f394113e4fb8c5f2bfc268c839 Mon Sep 17 00:00:00 2001 From: gytisrepecka Date: Fri, 3 Apr 2020 13:26:59 +0300 Subject: [PATCH] Added Gitea OAuth login and account management. --- account.go | 13 +++- config/config.go | 10 +++ oauth.go | 28 +++++++++ oauth_gitea.go | 115 +++++++++++++++++++++++++++++++++++ pages/login.tmpl | 9 ++- routes.go | 1 + static/img/mark/gitea.png | Bin 0 -> 4693 bytes templates/user/settings.tmpl | 10 ++- 8 files changed, 183 insertions(+), 3 deletions(-) create mode 100644 oauth_gitea.go create mode 100644 static/img/mark/gitea.png diff --git a/account.go b/account.go index d32f503..42e9982 100644 --- a/account.go +++ b/account.go @@ -311,6 +311,8 @@ func viewLogin(app *App, w http.ResponseWriter, r *http.Request) error { OauthWriteAs bool OauthGitlab bool GitlabDisplayName string + OauthGitea bool + GiteaDisplayName string }{ pageForReq(app, r), r.FormValue("to"), @@ -321,6 +323,8 @@ func viewLogin(app *App, w http.ResponseWriter, r *http.Request) error { app.Config().WriteAsOauth.ClientID != "", app.Config().GitlabOauth.ClientID != "", config.OrDefaultString(app.Config().GitlabOauth.DisplayName, gitlabDisplayName), + app.Config().GiteaOauth.ClientID != "", + config.OrDefaultString(app.Config().GiteaOauth.DisplayName, giteaDisplayName), } if earlyError != "" { @@ -1046,6 +1050,7 @@ func viewSettings(app *App, u *User, w http.ResponseWriter, r *http.Request) err enableOauthSlack := app.Config().SlackOauth.ClientID != "" enableOauthWriteAs := app.Config().WriteAsOauth.ClientID != "" enableOauthGitLab := app.Config().GitlabOauth.ClientID != "" + enableOauthGitea := app.Config().GiteaOauth.ClientID != "" oauthAccounts, err := app.db.GetOauthAccounts(r.Context(), u.ID) if err != nil { @@ -1060,10 +1065,12 @@ func viewSettings(app *App, u *User, w http.ResponseWriter, r *http.Request) err enableOauthWriteAs = false case "gitlab": enableOauthGitLab = false + case "gitea": + enableOauthGitea = false } } - displayOauthSection := enableOauthSlack || enableOauthWriteAs || enableOauthGitLab || len(oauthAccounts) > 0 + displayOauthSection := enableOauthSlack || enableOauthWriteAs || enableOauthGitLab || enableOauthGitea || len(oauthAccounts) > 0 obj := struct { *UserPage @@ -1077,6 +1084,8 @@ func viewSettings(app *App, u *User, w http.ResponseWriter, r *http.Request) err OauthWriteAs bool OauthGitLab bool GitLabDisplayName string + OauthGitea bool + GiteaDisplayName string }{ UserPage: NewUserPage(app, r, u, "Account Settings", flashes), Email: fullUser.EmailClear(app.keys), @@ -1089,6 +1098,8 @@ func viewSettings(app *App, u *User, w http.ResponseWriter, r *http.Request) err OauthWriteAs: enableOauthWriteAs, OauthGitLab: enableOauthGitLab, GitLabDisplayName: config.OrDefaultString(app.Config().GitlabOauth.DisplayName, gitlabDisplayName), + OauthGitea: enableOauthGitea, + GiteaDisplayName: config.OrDefaultString(app.Config().GiteaOauth.DisplayName, giteaDisplayName), } showUserPage(w, "settings", obj) diff --git a/config/config.go b/config/config.go index 520dd59..7ccdfba 100644 --- a/config/config.go +++ b/config/config.go @@ -86,6 +86,15 @@ type ( CallbackProxyAPI string `ini:"callback_proxy_api"` } + GiteaOauthCfg struct { + ClientID string `ini:"client_id"` + ClientSecret string `ini:"client_secret"` + Host string `ini:"host"` + DisplayName string `ini:"display_name"` + CallbackProxy string `ini:"callback_proxy"` + CallbackProxyAPI string `ini:"callback_proxy_api"` + } + // AppCfg holds values that affect how the application functions AppCfg struct { SiteName string `ini:"site_name"` @@ -138,6 +147,7 @@ type ( SlackOauth SlackOauthCfg `ini:"oauth.slack"` WriteAsOauth WriteAsOauthCfg `ini:"oauth.writeas"` GitlabOauth GitlabOauthCfg `ini:"oauth.gitlab"` + GiteaOauth GiteaOauthCfg `ini:"oauth.gitea"` } ) diff --git a/oauth.go b/oauth.go index 9073f75..902c31b 100644 --- a/oauth.go +++ b/oauth.go @@ -208,6 +208,34 @@ func configureGitlabOauth(parentHandler *Handler, r *mux.Router, app *App) { } } +func configureGiteaOauth(parentHandler *Handler, r *mux.Router, app *App) { + if app.Config().GiteaOauth.ClientID != "" { + callbackLocation := app.Config().App.Host + "/oauth/callback/gitea" + + var callbackProxy *callbackProxyClient = nil + if app.Config().GiteaOauth.CallbackProxy != "" { + callbackProxy = &callbackProxyClient{ + server: app.Config().GiteaOauth.CallbackProxyAPI, + callbackLocation: app.Config().App.Host + "/oauth/callback/gitea", + httpClient: config.DefaultHTTPClient(), + } + callbackLocation = app.Config().GiteaOauth.CallbackProxy + } + + address := config.OrDefaultString(app.Config().GiteaOauth.Host, giteaHost) + oauthClient := giteaOauthClient{ + ClientID: app.Config().GiteaOauth.ClientID, + ClientSecret: app.Config().GiteaOauth.ClientSecret, + ExchangeLocation: address + "/login/oauth/access_token", + InspectLocation: address + "/api/v1/user", + AuthLocation: address + "/login/oauth/authorize", + HttpClient: config.DefaultHTTPClient(), + CallbackLocation: callbackLocation, + } + configureOauthRoutes(parentHandler, r, app, oauthClient, callbackProxy) + } +} + func configureOauthRoutes(parentHandler *Handler, r *mux.Router, app *App, oauthClient oauthClient, callbackProxy *callbackProxyClient) { handler := &oauthHandler{ Config: app.Config(), diff --git a/oauth_gitea.go b/oauth_gitea.go new file mode 100644 index 0000000..42331bf --- /dev/null +++ b/oauth_gitea.go @@ -0,0 +1,115 @@ +package writefreely + +import ( + "context" + "errors" + "net/http" + "net/url" + "strings" +) + +type giteaOauthClient struct { + ClientID string + ClientSecret string + AuthLocation string + ExchangeLocation string + InspectLocation string + CallbackLocation string + HttpClient HttpClient +} + +var _ oauthClient = giteaOauthClient{} + +const ( + giteaHost = "https://source.gyt.is" + giteaDisplayName = "Gitea" +) + +func (c giteaOauthClient) GetProvider() string { + return "gitea" +} + +func (c giteaOauthClient) GetClientID() string { + return c.ClientID +} + +func (c giteaOauthClient) GetCallbackLocation() string { + return c.CallbackLocation +} + +func (c giteaOauthClient) buildLoginURL(state string) (string, error) { + u, err := url.Parse(c.AuthLocation) + if err != nil { + return "", err + } + q := u.Query() + q.Set("client_id", c.ClientID) + q.Set("redirect_uri", c.CallbackLocation) + q.Set("response_type", "code") + q.Set("state", state) + // q.Set("scope", "read_user") + u.RawQuery = q.Encode() + return u.String(), nil +} + +func (c giteaOauthClient) exchangeOauthCode(ctx context.Context, code string) (*TokenResponse, error) { + form := url.Values{} + form.Add("grant_type", "authorization_code") + form.Add("redirect_uri", c.CallbackLocation) + // form.Add("scope", "read_user") + form.Add("code", code) + req, err := http.NewRequest("POST", c.ExchangeLocation, strings.NewReader(form.Encode())) + if err != nil { + return nil, err + } + req.WithContext(ctx) + req.Header.Set("User-Agent", "writefreely") + req.Header.Set("Accept", "application/json") + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + req.SetBasicAuth(c.ClientID, c.ClientSecret) + + resp, err := c.HttpClient.Do(req) + if err != nil { + return nil, err + } + if resp.StatusCode != http.StatusOK { + return nil, errors.New("unable to exchange code for access token") + } + + var tokenResponse TokenResponse + if err := limitedJsonUnmarshal(resp.Body, tokenRequestMaxLen, &tokenResponse); err != nil { + return nil, err + } + if tokenResponse.Error != "" { + return nil, errors.New(tokenResponse.Error) + } + return &tokenResponse, nil +} + +func (c giteaOauthClient) inspectOauthAccessToken(ctx context.Context, accessToken string) (*InspectResponse, error) { + req, err := http.NewRequest("GET", c.InspectLocation, nil) + if err != nil { + return nil, err + } + req.WithContext(ctx) + req.Header.Set("User-Agent", "writefreely") + req.Header.Set("Accept", "application/json") + req.Header.Set("Authorization", "Bearer "+accessToken) + + resp, err := c.HttpClient.Do(req) + if err != nil { + return nil, err + } + if resp.StatusCode != http.StatusOK { + return nil, errors.New("unable to inspect access token") + } + + var inspectResponse InspectResponse + if err := limitedJsonUnmarshal(resp.Body, infoRequestMaxLen, &inspectResponse); err != nil { + return nil, err + } + if inspectResponse.Error != "" { + return nil, errors.New(inspectResponse.Error) + } + return &inspectResponse, nil +} diff --git a/pages/login.tmpl b/pages/login.tmpl index 94087f3..c09b16e 100644 --- a/pages/login.tmpl +++ b/pages/login.tmpl @@ -36,6 +36,10 @@ hr.short { box-sizing: border-box; font-size: 17px; } +#gitea-login { + box-sizing: border-box; + font-size: 17px; +} {{end}} {{define "content"}} @@ -46,7 +50,7 @@ hr.short { {{range .Flashes}}
  • {{.}}
  • {{end}} {{end}} - {{ if or .OauthSlack .OauthWriteAs .OauthGitlab }} + {{ if or .OauthSlack .OauthWriteAs .OauthGitlab .OauthGitea }}
    {{ if .OauthSlack }} Sign in with Slack @@ -57,6 +61,9 @@ hr.short { {{ if .OauthGitlab }} Sign in with {{.GitlabDisplayName}} {{ end }} + {{ if .OauthGitea }} + Sign in with {{.GiteaDisplayName}} + {{ end }}
    diff --git a/routes.go b/routes.go index b34bd3d..a6efa35 100644 --- a/routes.go +++ b/routes.go @@ -76,6 +76,7 @@ func InitRoutes(apper Apper, r *mux.Router) *mux.Router { configureSlackOauth(handler, write, apper.App()) configureWriteAsOauth(handler, write, apper.App()) configureGitlabOauth(handler, write, apper.App()) + configureGiteaOauth(handler, write, apper.App()) // Set up dyamic page handlers // Handle auth diff --git a/static/img/mark/gitea.png b/static/img/mark/gitea.png new file mode 100644 index 0000000000000000000000000000000000000000..f58bb625e8933d4d2142d981d9dfc269ba57c75c GIT binary patch literal 4693 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 {{ end }} - {{ if or .OauthSlack .OauthWriteAs .OauthGitLab }} + {{ if or .OauthSlack .OauthWriteAs .OauthGitLab .OauthGitea }}

    Link External Accounts

    Connect additional accounts to enable logging in with those providers, instead of using your username and password.

    @@ -122,6 +122,14 @@ h3 { font-weight: normal; }
    {{ end }} + {{ if .OauthGitea }} + + {{ end }}
    {{ end }}