137 lines
4.5 KiB
Go
137 lines
4.5 KiB
Go
/*
|
|
Timelinize
|
|
Copyright (c) 2013 Matthew Holt
|
|
|
|
This program is free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU Affero General Public License as published
|
|
by the Free Software Foundation, either version 3 of the License, or
|
|
(at your option) any later version.
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU Affero General Public License for more details.
|
|
|
|
You should have received a copy of the GNU Affero General Public License
|
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
package timeline
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/http"
|
|
|
|
"github.com/timelinize/timelinize/internal/oauth2client"
|
|
"golang.org/x/oauth2"
|
|
)
|
|
|
|
// oauth2App returns an oauth2client.App for the OAuth2 provider
|
|
// with the given ID.
|
|
func oauth2App(providerID string, scopes []string) oauth2client.App {
|
|
// TODO: if we ever allow user-configurable OAuth2 apps (so they can have their own rate limits and such)
|
|
// then using a LocalAppSource would be the way to go:
|
|
// cfg := oauth2.Config{
|
|
// ClientID: "NFJuNXlmSFFhWEdYcDRsMlFETko6MTpjaQ",
|
|
// ClientSecret: "t0la1nDJRMz2JDuj2z7OWIF01kM4_Vtkvie4-mij3DmoGFozZO",
|
|
// Endpoint: oauth2.Endpoint{
|
|
// AuthURL: "https://twitter.com/i/oauth2/authorize",
|
|
// TokenURL: "https://api.twitter.com/2/oauth2/token",
|
|
// AuthStyle: oauth2.AuthStyleInHeader, // TODO: this might be only for Twitter... maybe make this customizable in the oauth credentials config?
|
|
// },
|
|
// RedirectURL: oauth2client.DefaultRedirectURL,
|
|
// Scopes: scopes,
|
|
// }
|
|
// return oauth2client.LocalAppSource{OAuth2Config: &cfg}, nil
|
|
///////////////////////////
|
|
|
|
return oauth2client.RemoteAppSource{
|
|
ProxyURL: "http://localhost:7233/oauth2", // TODO: put url of backend oauth2proxy here
|
|
ProviderID: providerID,
|
|
Scopes: scopes,
|
|
}
|
|
}
|
|
|
|
// NewOAuth2HTTPClient returns a new HTTP client which performs
|
|
// HTTP requests that are authenticated with an oauth2.Token
|
|
// stored with the account acc.
|
|
func (acc Account) NewOAuth2HTTPClient(ctx context.Context, oa OAuth2) (*http.Client, error) {
|
|
// load the existing token for this account from the database;
|
|
// note that OAuth-enabled accounts might not have an authorization
|
|
// set, and that's OK, the user might not be using the API
|
|
var tkn *oauth2.Token
|
|
if len(acc.authorization) > 0 {
|
|
err := unmarshalGob(acc.authorization, &tkn)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("gob-decoding OAuth2 token: %w", err)
|
|
}
|
|
if tkn == nil || tkn.AccessToken == "" {
|
|
return nil, fmt.Errorf("OAuth2 token is empty: %+v", tkn)
|
|
}
|
|
}
|
|
|
|
// load the service's "oauth app", which can provide both tokens and
|
|
// oauth configs -- in this case, we need the oauth config; we should
|
|
// already have a token
|
|
oapp := oauth2App(oa.ProviderID, oa.Scopes)
|
|
|
|
// obtain a token source from the oauth's config so that it can keep
|
|
// the token refreshed if it expires
|
|
src := oapp.TokenSource(ctx, tkn)
|
|
|
|
// finally, create an HTTP client that authenticates using the token,
|
|
// but wrapping the underlying token source so we can persist any
|
|
// changes to the database
|
|
return oauth2.NewClient(ctx, &persistedTokenSource{
|
|
tl: acc.tl,
|
|
ts: src,
|
|
accountID: acc.ID,
|
|
token: tkn,
|
|
}), nil
|
|
}
|
|
|
|
// authorizeWithOAuth2 gets an initial OAuth2 token from the user.
|
|
// It requires OAuth2AppSource to be set or it will panic.
|
|
func authorizeWithOAuth2(ctx context.Context, oc OAuth2) ([]byte, error) {
|
|
src := oauth2App(oc.ProviderID, oc.Scopes)
|
|
tkn, err := src.InitialToken(ctx)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("getting token from source: %w", err)
|
|
}
|
|
return marshalGob(tkn)
|
|
}
|
|
|
|
// persistedTokenSource wraps a TokenSource for
|
|
// a particular account and persists any changes
|
|
// to the account's token to the database.
|
|
type persistedTokenSource struct {
|
|
tl *Timeline
|
|
ts oauth2.TokenSource
|
|
accountID int64
|
|
token *oauth2.Token
|
|
}
|
|
|
|
func (ps *persistedTokenSource) Token() (*oauth2.Token, error) {
|
|
tkn, err := ps.ts.Token()
|
|
if err != nil {
|
|
return tkn, err
|
|
}
|
|
|
|
// store an updated token in the DB
|
|
if tkn.AccessToken != ps.token.AccessToken {
|
|
ps.token = tkn
|
|
|
|
authBytes, err := marshalGob(tkn)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("gob-encoding new OAuth2 token: %w", err)
|
|
}
|
|
|
|
_, err = ps.tl.db.WritePool.ExecContext(context.TODO(), `UPDATE accounts SET authorization=? WHERE id=?`, authBytes, ps.accountID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("storing refreshed OAuth2 token: %w", err)
|
|
}
|
|
}
|
|
|
|
return tkn, nil
|
|
}
|