1
0
Fork 0
timelinize/internal/oauth2client/oauth2.go
Matthew Holt a46f42de15
Move oauth2client package to internal
This is a legacy package I wrote in the earlier days of Timeliner (and maybe even photobak?) that made it easier to access cloud services protected by individual OAuth accounts... I am not sure if we will use it in Timelinize but I'm holding onto it for now.
2026-01-16 14:59:37 -07:00

123 lines
4.2 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 oauth2client implements a pluggable OAuth2 client that can service
// either local or remote applications.
package oauth2client
import (
"context"
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"fmt"
"io"
mathrand "math/rand"
"net/http"
"time"
"golang.org/x/oauth2"
)
// Getter is a type that can get an OAuth2 auth code.
// It must enforce that the state parameter of the
// redirected request matches expectedStateVal.
type Getter interface {
Get(ctx context.Context, expectedStateVal, authCodeURL string) (code string, err error)
}
// AuthCodeExchangeInfo generates a state and a code verifier challenge string,
// along with the assembled URL for a request to get an authorization code.
func AuthCodeExchangeInfo(cfg *oauth2.Config) (CodeExchangeInfo, error) {
const stateValLength = 14
state := randString(stateValLength)
// support PKCE; we use the "S256" method which is theoretically superior to "plain"
pkceVerifier, err := generatePKCEVerifier()
if err != nil {
return CodeExchangeInfo{}, fmt.Errorf("generating PKCE verifier: %w", err)
}
pkceVerifierSha256 := sha256.Sum256([]byte(pkceVerifier))
pkceVerifierSha256Base64 := base64.RawURLEncoding.EncodeToString(pkceVerifierSha256[:])
return CodeExchangeInfo{
State: state,
CodeVerifier: pkceVerifier,
AuthCodeURL: cfg.AuthCodeURL(state,
oauth2.AccessTypeOffline,
// PKCE extension
oauth2.SetAuthURLParam("code_challenge", pkceVerifierSha256Base64),
oauth2.SetAuthURLParam("code_challenge_method", "S256"),
),
}, nil
}
// generatePKCEVerifier generates a PKCE code verifier described at
// https://www.oauth.com/oauth2-servers/pkce/authorization-request/.
// "This is a cryptographically random string using the characters A-Z,
// a-z, 0-9, and the punctuation characters -._~ (hyphen, period,
// underscore, and tilde), between 43 and 128 characters long."
//
// The resulting string meets these criteria even if it does not
// exercise the full range of the character set.
func generatePKCEVerifier() (string, error) {
const minLength = 43
p := make([]byte, minLength) // encoded length is longer, but this guarantees at least 43 characters
if _, err := io.ReadFull(rand.Reader, p); err != nil {
return "", err
}
return base64.RawURLEncoding.EncodeToString(p), nil
}
// randString is not safe for cryptographic use.
func randString(n int) string {
const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
b := make([]byte, n)
for i := range b {
b[i] = letterBytes[mathrand.Intn(len(letterBytes))] //nolint:gosec // the whole point is that it's not crypto-safe, it's fine
}
return string(b)
}
type (
// CodeExchangeInfo holds information for obtaining an auth code.
CodeExchangeInfo struct {
State string `json:"state"`
CodeVerifier string `json:"code_verifier"` // plaintext value (PKCE extension)
AuthCodeURL string `json:"auth_code_url"` // fully-assembled URL
}
// App provides a way to get an initial OAuth2 token
// as well as a continuing token source.
App interface {
InitialToken(ctx context.Context) (*oauth2.Token, error)
TokenSource(ctx context.Context, token *oauth2.Token) oauth2.TokenSource
}
)
// httpClient is the HTTP client to use for OAuth2 requests.
var httpClient = &http.Client{
Timeout: 10 * time.Second,
}
// DefaultRedirectURL is the default URL to
// which to redirect clients after a code
// has been obtained. Redirect URLs may
// have to be registered with your OAuth2
// provider.
const DefaultRedirectURL = "http://localhost:8008/oauth2-redirect"