")
fmt.Fprintln(os.Stderr, "")
fmt.Fprintln(os.Stderr, "Options:")
flag.PrintDefaults()
fmt.Fprintln(os.Stderr, "")
fmt.Fprintln(os.Stderr, "Actions:")
fmt.Fprintln(os.Stderr, " get Generate credential [called by Git]")
fmt.Fprintln(os.Stderr, " configure Configure as Git credential helper")
fmt.Fprintln(os.Stderr, " unconfigure Unconfigure as Git credential helper")
fmt.Fprintln(os.Stderr, "")
fmt.Fprintln(os.Stderr, "See also https://github.com/hickford/git-credential-oauth")
}
flag.Parse()
if device {
c := configByHost["android.googlesource.com"]
c.ClientID = "897755559425-82ha835rqnprtctvm8shjc2p86bk0eru.apps.googleusercontent.com"
c.ClientSecret = "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"
configByHost["android.googlesource.com"] = c
}
args := flag.Args()
if len(args) != 1 {
flag.Usage()
os.Exit(2)
}
switch args[0] {
case "get":
printVersion()
input, err := io.ReadAll(os.Stdin)
if err != nil {
log.Fatalln(err)
}
pairs := parse(string(input))
if verbose {
fmt.Fprintln(os.Stderr, "input:", pairs)
}
host := pairs["host"]
looksLikeGitLab := strings.HasPrefix(host, "gitlab.") || strings.Contains(pairs["wwwauth[]"], `Realm="GitLab"`)
looksLikeGitea := strings.Contains(pairs["wwwauth[]"], `realm="Gitea"`)
looksLikeGitHub := strings.HasPrefix(host, "github.") || strings.Contains(pairs["wwwauth[]"], `realm="GitHub"`)
urll := fmt.Sprintf("%s://%s", pairs["protocol"], host)
c, found := configByHost[host]
if !found && strings.HasSuffix(host, ".googlesource.com") {
c = configByHost["android.googlesource.com"]
}
if !found && looksLikeGitLab {
// TODO: universal GitLab support with constant client id
// https://gitlab.com/gitlab-org/gitlab/-/issues/374172
// c.ClientID = ...
// assumes GitLab installed at domain root
c.Endpoint = oauth2.Endpoint{
AuthURL: fmt.Sprintf("%s/oauth/authorize", urll),
TokenURL: fmt.Sprintf("%s/oauth/token", urll),
}
c.Scopes = configByHost["gitlab.com"].Scopes
}
if !found && looksLikeGitea {
c.ClientID = "a4792ccc-144e-407e-86c9-5e7d8d9c3269"
c.Endpoint = oauth2.Endpoint{
AuthURL: fmt.Sprintf("%s/login/oauth/authorize", urll),
TokenURL: fmt.Sprintf("%s/login/oauth/access_token", urll),
}
c.Scopes = configByHost["gitea.com"].Scopes
}
if !found && looksLikeGitHub {
c.Endpoint = oauth2.Endpoint{
AuthURL: fmt.Sprintf("%s/login/oauth/authorize", urll),
TokenURL: fmt.Sprintf("%s/login/oauth/access_token", urll),
DeviceAuthURL: fmt.Sprintf("%s/login/device/code", urll),
}
c.Scopes = configByHost["github.com"].Scopes
}
gitPath, err := exec.LookPath("git")
if err == nil {
cmd := exec.Command(gitPath, "config", "--get-urlmatch", "credential.oauthClientId", urll)
bytes, err := cmd.Output()
if err == nil {
c.ClientID = strings.TrimSpace(string(bytes))
}
bytes, err = exec.Command(gitPath, "config", "--get-urlmatch", "credential.oauthClientSecret", urll).Output()
if err == nil {
c.ClientSecret = strings.TrimSpace(string(bytes))
}
bytes, err = exec.Command(gitPath, "config", "--get-urlmatch", "credential.oauthScopes", urll).Output()
if err == nil {
c.Scopes = []string{strings.TrimSpace(string(bytes))}
}
bytes, err = exec.Command(gitPath, "config", "--get-urlmatch", "credential.oauthAuthURL", urll).Output()
if err == nil {
c.Endpoint.AuthURL, err = urlResolveReference(urll, strings.TrimSpace(string(bytes)))
if err != nil {
log.Fatalln(err)
}
}
bytes, err = exec.Command(gitPath, "config", "--get-urlmatch", "credential.oauthTokenURL", urll).Output()
if err == nil {
c.Endpoint.TokenURL, err = urlResolveReference(urll, strings.TrimSpace(string(bytes)))
if err != nil {
log.Fatalln(err)
}
}
bytes, err = exec.Command(gitPath, "config", "--get-urlmatch", "credential.oauthRedirectURL", urll).Output()
if err == nil {
c.RedirectURL = strings.TrimSpace(string(bytes))
}
}
if c.ClientID == "" || c.Endpoint.AuthURL == "" || c.Endpoint.TokenURL == "" {
if looksLikeGitLab {
fmt.Fprintf(os.Stderr, "It looks like you're authenticating to a GitLab instance! To configure git-credential-oauth for host %s, follow the instructions at https://github.com/hickford/git-credential-oauth/issues/18. You may need to register an OAuth application at https://%s/-/profile/applications\n", host, host)
}
return
}
var token *oauth2.Token
if pairs["oauth_refresh_token"] != "" {
// Try refresh token (fast, doesn't open browser)
if verbose {
fmt.Fprintln(os.Stderr, "refreshing token...")
}
token, err = c.TokenSource(context.Background(), &oauth2.Token{RefreshToken: pairs["oauth_refresh_token"]}).Token()
if err != nil {
fmt.Fprintln(os.Stderr, "error during OAuth token refresh", err)
}
}
if token == nil {
// Generate new token (opens browser, may require user input)
if device {
token, err = getDeviceToken(c)
} else {
token, err = getToken(c)
}
if err != nil {
log.Fatalln(err)
}
}
if verbose {
fmt.Fprintln(os.Stderr, "token:", token)
}
var username string
if host == "bitbucket.org" {
// https://support.atlassian.com/bitbucket-cloud/docs/use-oauth-on-bitbucket-cloud/#Cloning-a-repository-with-an-access-token
username = "x-token-auth"
} else if looksLikeGitLab {
// https://docs.gitlab.com/ee/api/oauth2.html#access-git-over-https-with-access-token
username = "oauth2"
} else if pairs["username"] == "" {
username = "oauth2"
}
output := map[string]string{
"password": token.AccessToken,
}
if username != "" {
output["username"] = username
}
if !token.Expiry.IsZero() {
output["password_expiry_utc"] = fmt.Sprintf("%d", token.Expiry.UTC().Unix())
}
if token.RefreshToken != "" {
output["oauth_refresh_token"] = token.RefreshToken
}
if verbose {
fmt.Fprintln(os.Stderr, "output:", output)
}
for key, v := range output {
fmt.Printf("%s=%s\n", key, v)
}
case "configure", "unconfigure":
gitPath, err := exec.LookPath("git")
if err != nil {
log.Fatalln(err)
}
var commands []*exec.Cmd
if args[0] == "configure" {
var storage string
switch runtime.GOOS {
case "windows":
storage = "wincred"
case "darwin":
storage = "osxkeychain"
default:
storage = "cache --timeout 7200"
}
commands = []*exec.Cmd{exec.Command(gitPath, "config", "--global", "--unset-all", "credential.helper"),
exec.Command(gitPath, "config", "--global", "--add", "credential.helper", storage),
exec.Command(gitPath, "config", "--global", "--add", "credential.helper", "oauth")}
} else if args[0] == "unconfigure" {
commands = []*exec.Cmd{exec.Command(gitPath, "config", "--global", "--unset-all", "credential.helper", "oauth")}
}
for _, cmd := range commands {
cmd.Stderr = os.Stderr
cmd.Stdout = os.Stdout
if verbose {
fmt.Fprintln(os.Stderr, cmd)
}
err := cmd.Run()
// ignore exit status 5 "you try to unset an option which does not exist" https://git-scm.com/docs/git-config#_description
if err != nil && cmd.ProcessState.ExitCode() != 5 {
log.Fatalln(err)
}
}
fmt.Fprintf(os.Stderr, "%sd successfully\n", args[0])
}
}
var template string = `
Git authentication
Success. You may close this page and return to Git.
—git-credential-oauth %s
`
func getToken(c oauth2.Config) (*oauth2.Token, error) {
state := oauth2.GenerateVerifier()
queries := make(chan url.Values)
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// TODO: consider whether to show errors in browser or command line
queries <- r.URL.Query()
w.Header().Add("Content-Type", "text/html")
html := fmt.Sprintf(template, getVersion())
w.Write([]byte(html))
})
var server *httptest.Server
if c.RedirectURL == "" {
server = httptest.NewServer(handler)
c.RedirectURL = server.URL
} else {
server = httptest.NewUnstartedServer(handler)
url, err := url.Parse(c.RedirectURL)
if err != nil {
log.Fatalln(err)
}
origHostname := url.Hostname()
if url.Port() == "" {
url.Host += ":0"
}
l, err := net.Listen("tcp", url.Host)
if err != nil {
log.Fatalln(err)
}
server.Listener = l
server.Start()
url.Host = l.Addr().String()
if verbose {
fmt.Fprintf(os.Stderr, "listening on %s", url.Host)
}
if url.Hostname() != origHostname {
// restore original hostname such as 'localhost'
url.Host = fmt.Sprintf("%s:%s", origHostname, url.Port())
}
c.RedirectURL = url.String()
}
defer server.Close()
verifier := oauth2.GenerateVerifier()
authCodeURL := c.AuthCodeURL(state, oauth2.S256ChallengeOption(verifier))
fmt.Fprintf(os.Stderr, "Please complete authentication in your browser...\n%s\n", authCodeURL)
var open string
switch runtime.GOOS {
case "windows":
open = "start"
case "darwin":
open = "open"
default:
open = "xdg-open"
}
// TODO: wait for server to start before opening browser
if _, err := exec.LookPath(open); err == nil {
err = exec.Command(open, authCodeURL).Run()
if err != nil {
return nil, err
}
}
query := <-queries
server.Close()
if verbose {
fmt.Fprintln(os.Stderr, "query:", query)
}
if query.Get("state") != state {
return nil, fmt.Errorf("state mismatch")
}
code := query.Get("code")
return c.Exchange(context.Background(), code, oauth2.VerifierOption(verifier))
}
func getDeviceToken(c oauth2.Config) (*oauth2.Token, error) {
deviceAuth, err := c.DeviceAuth(context.Background())
if err != nil {
log.Fatalln(err)
}
if verbose {
fmt.Fprintln(os.Stderr, deviceAuth)
}
fmt.Fprintf(os.Stderr, "Please enter code %s at %s\n", deviceAuth.UserCode, deviceAuth.VerificationURI)
return c.DeviceAccessToken(context.Background(), deviceAuth)
}
func replaceHost(e oauth2.Endpoint, host string) oauth2.Endpoint {
e.AuthURL = replaceHostInURL(e.AuthURL, host)
e.TokenURL = replaceHostInURL(e.TokenURL, host)
e.DeviceAuthURL = replaceHostInURL(e.DeviceAuthURL, host)
return e
}
func replaceHostInURL(originalURL, host string) string {
if originalURL == "" {
return ""
}
u, err := url.Parse(originalURL)
if err != nil {
panic(err)
}
u.Host = host
return u.String()
}
func urlResolveReference(base, ref string) (string, error) {
base1, err := url.Parse(base)
if err != nil {
return "", err
}
ref1, err := url.Parse(ref)
if err != nil {
return "", err
}
return base1.ResolveReference(ref1).String(), nil
}
git-credential-oauth-0.11.0/main_test.go 0000664 0000000 0000000 00000000651 14510002445 0020111 0 ustar 00root root 0000000 0000000 package main
import (
"strings"
"testing"
)
func TestConfig(t *testing.T) {
for key, c := range configByHost {
if key == "android.googlesource.com" {
continue
}
if !strings.Contains(c.Endpoint.AuthURL, key) {
t.Errorf("bad auth url for key %s: %s", key, c.Endpoint.AuthURL)
}
if !strings.Contains(c.Endpoint.TokenURL, key) {
t.Errorf("bad token url for key %s: %s", key, c.Endpoint.TokenURL)
}
}
}