Merge pull request #277 from paddatrapper/oauth-gitlab

Add Gitlab OAuth
This commit is contained in:
Matt Baer 2020-03-16 12:18:45 -04:00 committed by GitHub
commit 5b6e008118
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 168 additions and 3 deletions

View File

@ -308,6 +308,8 @@ func viewLogin(app *App, w http.ResponseWriter, r *http.Request) error {
LoginUsername string
OauthSlack bool
OauthWriteAs bool
OauthGitlab bool
GitlabHost string
}{
pageForReq(app, r),
r.FormValue("to"),
@ -316,6 +318,8 @@ func viewLogin(app *App, w http.ResponseWriter, r *http.Request) error {
getTempInfo(app, "login-user", r, w),
app.Config().SlackOauth.ClientID != "",
app.Config().WriteAsOauth.ClientID != "",
app.Config().GitlabOauth.ClientID != "",
config.OrDefaultString(app.Config().GitlabOauth.DisplayName, gitlabDisplayName),
}
if earlyError != "" {

View File

@ -69,6 +69,15 @@ type (
CallbackProxyAPI string `ini:"callback_proxy_api"`
}
GitlabOauthCfg 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"`
}
SlackOauthCfg struct {
ClientID string `ini:"client_id"`
ClientSecret string `ini:"client_secret"`
@ -128,6 +137,7 @@ type (
App AppCfg `ini:"app"`
SlackOauth SlackOauthCfg `ini:"oauth.slack"`
WriteAsOauth WriteAsOauthCfg `ini:"oauth.writeas"`
GitlabOauth GitlabOauthCfg `ini:"oauth.gitlab"`
}
)

View File

@ -2512,7 +2512,7 @@ func (db *datastore) GetCollectionLastPostTime(id int64) (*time.Time, error) {
func (db *datastore) GenerateOAuthState(ctx context.Context, provider, clientID string) (string, error) {
state := store.Generate62RandomString(24)
_, err := db.ExecContext(ctx, "INSERT INTO oauth_client_states (state, provider, client_id, used, created_at) VALUES (?, ?, ?, FALSE, NOW())", state, provider, clientID)
_, err := db.ExecContext(ctx, "INSERT INTO oauth_client_states (state, provider, client_id, used, created_at) VALUES (?, ?, ?, FALSE, " + db.now() + ")", state, provider, clientID)
if err != nil {
return "", fmt.Errorf("unable to record oauth client state: %w", err)
}

View File

@ -149,7 +149,7 @@ func configureWriteAsOauth(parentHandler *Handler, r *mux.Router, app *App) {
callbackLocation: app.Config().App.Host + "/oauth/callback/write.as",
httpClient: config.DefaultHTTPClient(),
}
callbackLocation = app.Config().SlackOauth.CallbackProxy
callbackLocation = app.Config().WriteAsOauth.CallbackProxy
}
oauthClient := writeAsOauthClient{
@ -165,6 +165,34 @@ func configureWriteAsOauth(parentHandler *Handler, r *mux.Router, app *App) {
}
}
func configureGitlabOauth(parentHandler *Handler, r *mux.Router, app *App) {
if app.Config().GitlabOauth.ClientID != "" {
callbackLocation := app.Config().App.Host + "/oauth/callback/gitlab"
var callbackProxy *callbackProxyClient = nil
if app.Config().GitlabOauth.CallbackProxy != "" {
callbackProxy = &callbackProxyClient{
server: app.Config().GitlabOauth.CallbackProxyAPI,
callbackLocation: app.Config().App.Host + "/oauth/callback/gitlab",
httpClient: config.DefaultHTTPClient(),
}
callbackLocation = app.Config().GitlabOauth.CallbackProxy
}
address := config.OrDefaultString(app.Config().GitlabOauth.Host, gitlabHost)
oauthClient := gitlabOauthClient{
ClientID: app.Config().GitlabOauth.ClientID,
ClientSecret: app.Config().GitlabOauth.ClientSecret,
ExchangeLocation: address + "/oauth/token",
InspectLocation: address + "/api/v4/user",
AuthLocation: address + "/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(),

115
oauth_gitlab.go Normal file
View File

@ -0,0 +1,115 @@
package writefreely
import (
"context"
"errors"
"net/http"
"net/url"
"strings"
)
type gitlabOauthClient struct {
ClientID string
ClientSecret string
AuthLocation string
ExchangeLocation string
InspectLocation string
CallbackLocation string
HttpClient HttpClient
}
var _ oauthClient = gitlabOauthClient{}
const (
gitlabHost = "https://gitlab.com"
gitlabDisplayName = "GitLab"
)
func (c gitlabOauthClient) GetProvider() string {
return "gitlab"
}
func (c gitlabOauthClient) GetClientID() string {
return c.ClientID
}
func (c gitlabOauthClient) GetCallbackLocation() string {
return c.CallbackLocation
}
func (c gitlabOauthClient) 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 gitlabOauthClient) 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 gitlabOauthClient) 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
}

View File

@ -32,6 +32,10 @@ hr.short {
box-sizing: border-box;
font-size: 17px;
}
#gitlab-login {
box-sizing: border-box;
font-size: 17px;
}
</style>
{{end}}
{{define "content"}}
@ -42,7 +46,7 @@ hr.short {
{{range .Flashes}}<li class="urgent">{{.}}</li>{{end}}
</ul>{{end}}
{{ if or .OauthSlack .OauthWriteAs }}
{{ if or .OauthSlack .OauthWriteAs .OauthGitlab }}
<div class="row content-container signinbtns">
{{ if .OauthSlack }}
<a class="loginbtn" href="/oauth/slack"><img alt="Sign in with Slack" height="40" width="172" src="/img/sign_in_with_slack.png" srcset="/img/sign_in_with_slack.png 1x, /img/sign_in_with_slack@2x.png 2x" /></a>
@ -50,6 +54,9 @@ hr.short {
{{ if .OauthWriteAs }}
<a class="btn cta loginbtn" id="writeas-login" href="/oauth/write.as">Sign in with <strong>Write.as</strong></a>
{{ end }}
{{ if .OauthGitlab }}
<a class="btn cta loginbtn" id="gitlab-login" href="/oauth/gitlab">Sign in with <strong>{{ .GitlabDisplayName }}</strong></a>
{{ end }}
</div>
<div class="or">

View File

@ -75,6 +75,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())
// Set up dyamic page handlers
// Handle auth