mirror of
https://github.com/writeas/writefreely
synced 2024-12-08 14:35:44 +01:00
4753eef550
The upstream library now has the changes we'd merged in this fork, so no need to use our (very outdated) fork anymore.
179 lines
4.8 KiB
Go
179 lines
4.8 KiB
Go
/*
|
|
* Copyright © 2019-2020 Musing Studio 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/gosimple/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", ServerUserAgent(""))
|
|
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", ServerUserAgent(""))
|
|
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,
|
|
}
|
|
}
|