mirror of
https://github.com/writefreely/writefreely
synced 2024-11-28 11:30:18 +00:00
cf3d5588c2
Now, on OAuth signup form, we create a unique username with random appended string only if there's a conflict. Previously, this was always happening during the Slack OAuth flow. This has the benefit of preventing username collisions for all OAuth providers.
178 lines
4.8 KiB
Go
178 lines
4.8 KiB
Go
/*
|
|
* Copyright © 2019-2020 A Bunch Tell LLC.
|
|
*
|
|
* This file is part of WriteFreely.
|
|
*
|
|
* WriteFreely is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU Affero General Public License, included
|
|
* in the LICENSE file in this source code package.
|
|
*/
|
|
|
|
package writefreely
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"github.com/writeas/slug"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
)
|
|
|
|
type slackOauthClient struct {
|
|
ClientID string
|
|
ClientSecret string
|
|
TeamID string
|
|
CallbackLocation string
|
|
HttpClient HttpClient
|
|
}
|
|
|
|
type slackExchangeResponse struct {
|
|
OK bool `json:"ok"`
|
|
AccessToken string `json:"access_token"`
|
|
Scope string `json:"scope"`
|
|
TeamName string `json:"team_name"`
|
|
TeamID string `json:"team_id"`
|
|
Error string `json:"error"`
|
|
}
|
|
|
|
type slackIdentity struct {
|
|
Name string `json:"name"`
|
|
ID string `json:"id"`
|
|
Email string `json:"email"`
|
|
}
|
|
|
|
type slackTeam struct {
|
|
Name string `json:"name"`
|
|
ID string `json:"id"`
|
|
}
|
|
|
|
type slackUserIdentityResponse struct {
|
|
OK bool `json:"ok"`
|
|
User slackIdentity `json:"user"`
|
|
Team slackTeam `json:"team"`
|
|
Error string `json:"error"`
|
|
}
|
|
|
|
const (
|
|
slackAuthLocation = "https://slack.com/oauth/authorize"
|
|
slackExchangeLocation = "https://slack.com/api/oauth.access"
|
|
slackIdentityLocation = "https://slack.com/api/users.identity"
|
|
)
|
|
|
|
var _ oauthClient = slackOauthClient{}
|
|
|
|
func (c slackOauthClient) GetProvider() string {
|
|
return "slack"
|
|
}
|
|
|
|
func (c slackOauthClient) GetClientID() string {
|
|
return c.ClientID
|
|
}
|
|
|
|
func (c slackOauthClient) GetCallbackLocation() string {
|
|
return c.CallbackLocation
|
|
}
|
|
|
|
func (c slackOauthClient) buildLoginURL(state string) (string, error) {
|
|
u, err := url.Parse(slackAuthLocation)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
q := u.Query()
|
|
q.Set("client_id", c.ClientID)
|
|
q.Set("scope", "identity.basic identity.email identity.team")
|
|
q.Set("redirect_uri", c.CallbackLocation)
|
|
q.Set("state", state)
|
|
|
|
// If this param is not set, the user can select which team they
|
|
// authenticate through and then we'd have to match the configured team
|
|
// against the profile get. That is extra work in the post-auth phase
|
|
// that we don't want to do.
|
|
q.Set("team", c.TeamID)
|
|
|
|
// The Slack OAuth docs don't explicitly list this one, but it is part of
|
|
// the spec, so we include it anyway.
|
|
q.Set("response_type", "code")
|
|
u.RawQuery = q.Encode()
|
|
return u.String(), nil
|
|
}
|
|
|
|
func (c slackOauthClient) exchangeOauthCode(ctx context.Context, code string) (*TokenResponse, error) {
|
|
form := url.Values{}
|
|
// The oauth.access documentation doesn't explicitly mention this
|
|
// parameter, but it is part of the spec, so we include it anyway.
|
|
// https://api.slack.com/methods/oauth.access
|
|
form.Add("grant_type", "authorization_code")
|
|
form.Add("redirect_uri", c.CallbackLocation)
|
|
form.Add("code", code)
|
|
req, err := http.NewRequest("POST", slackExchangeLocation, 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 slackExchangeResponse
|
|
if err := limitedJsonUnmarshal(resp.Body, tokenRequestMaxLen, &tokenResponse); err != nil {
|
|
return nil, err
|
|
}
|
|
if !tokenResponse.OK {
|
|
return nil, errors.New(tokenResponse.Error)
|
|
}
|
|
return tokenResponse.TokenResponse(), nil
|
|
}
|
|
|
|
func (c slackOauthClient) inspectOauthAccessToken(ctx context.Context, accessToken string) (*InspectResponse, error) {
|
|
req, err := http.NewRequest("GET", slackIdentityLocation, 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 slackUserIdentityResponse
|
|
if err := limitedJsonUnmarshal(resp.Body, infoRequestMaxLen, &inspectResponse); err != nil {
|
|
return nil, err
|
|
}
|
|
if !inspectResponse.OK {
|
|
return nil, errors.New(inspectResponse.Error)
|
|
}
|
|
return inspectResponse.InspectResponse(), nil
|
|
}
|
|
|
|
func (resp slackUserIdentityResponse) InspectResponse() *InspectResponse {
|
|
return &InspectResponse{
|
|
UserID: resp.User.ID,
|
|
Username: slug.Make(resp.User.Name),
|
|
DisplayName: resp.User.Name,
|
|
Email: resp.User.Email,
|
|
}
|
|
}
|
|
|
|
func (resp slackExchangeResponse) TokenResponse() *TokenResponse {
|
|
return &TokenResponse{
|
|
AccessToken: resp.AccessToken,
|
|
}
|
|
}
|