1
0
Fork 0
timelinize/timeline/oauth2.go
2026-01-16 15:08:16 -07:00

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
}