@@ -308,6 +308,7 @@ func viewLogin(app *App, w http.ResponseWriter, r *http.Request) error { | |||
LoginUsername string | |||
OauthSlack bool | |||
OauthWriteAs bool | |||
OauthGitlab bool | |||
}{ | |||
pageForReq(app, r), | |||
r.FormValue("to"), | |||
@@ -316,6 +317,7 @@ 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 != "", | |||
} | |||
if earlyError != "" { | |||
@@ -69,6 +69,16 @@ type ( | |||
CallbackProxyAPI string `ini:"callback_proxy_api"` | |||
} | |||
GitlabOauthCfg struct { | |||
ClientID string `ini:"client_id"` | |||
ClientSecret string `ini:"client_secret"` | |||
AuthLocation string `ini:"auth_location"` | |||
TokenLocation string `ini:"token_location"` | |||
InspectLocation string `ini:"inspect_location"` | |||
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 +138,7 @@ type ( | |||
App AppCfg `ini:"app"` | |||
SlackOauth SlackOauthCfg `ini:"oauth.slack"` | |||
WriteAsOauth WriteAsOauthCfg `ini:"oauth.writeas"` | |||
GitlabOauth GitlabOauthCfg `ini:"oauth.gitlab"` | |||
} | |||
) | |||
@@ -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) | |||
} | |||
@@ -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,33 @@ 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 | |||
} | |||
oauthClient := gitlabOauthClient{ | |||
ClientID: app.Config().GitlabOauth.ClientID, | |||
ClientSecret: app.Config().GitlabOauth.ClientSecret, | |||
ExchangeLocation: config.OrDefaultString(app.Config().GitlabOauth.TokenLocation, gitlabExchangeLocation), | |||
InspectLocation: config.OrDefaultString(app.Config().GitlabOauth.InspectLocation, gitlabIdentityLocation), | |||
AuthLocation: config.OrDefaultString(app.Config().GitlabOauth.AuthLocation, gitlabAuthLocation), | |||
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(), | |||
@@ -0,0 +1,116 @@ | |||
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 ( | |||
gitlabAuthLocation = "https://gitlab.com/oauth/authorize" | |||
gitlabExchangeLocation = "https://gitlab.com/oauth/token" | |||
gitlabIdentityLocation = "https://gitlab.com/api/v4/user" | |||
) | |||
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 | |||
} |
@@ -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>GitLab</strong></a> | |||
{{ end }} | |||
</div> | |||
<div class="or"> | |||
@@ -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 | |||