1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
|
package main
import (
"fmt"
"net/http"
"os"
"os/exec"
"runtime"
"time"
)
func main() {
if len(os.Args) < 2 {
fmt.Fprintln(os.Stderr, "usage: git-credential-cosmocr <get|store|erase>")
os.Exit(2)
}
op := os.Args[1]
attrs := parseAttrs(os.Stdin)
c := cache{path: defaultCachePath()}
host := attrs["host"]
switch op {
case "get":
runGet(c, host, os.Stdout)
case "erase":
// git calls erase after the credential was rejected (e.g. token expired
// server-side); drop it so the next get re-authenticates.
_ = c.del(host)
case "store":
// The helper manages its own cache; nothing to do.
default:
fmt.Fprintf(os.Stderr, "git-credential-cosmocr: unknown operation %q\n", op)
os.Exit(2)
}
}
func runGet(c cache, host string, out *os.File) {
// Serve a cached, still-valid token without a round trip.
if tok, ok := c.get(host); ok && !expired(tok, time.Now()) {
emit(out, tok)
return
}
cfg := configFromEnv()
tok, err := cfg.login(useDeviceFlow())
if err != nil {
fmt.Fprintf(cfg.stderr, "git-credential-cosmocr: %v\n", err)
os.Exit(1)
}
if err := c.set(host, tok); err != nil {
fmt.Fprintf(cfg.stderr, "git-credential-cosmocr: warning: could not cache token: %v\n", err)
}
emit(out, tok)
}
// emit writes the token to git as Basic-auth credentials. The username is the
// token's subject; git ignores it for our server but including it keeps the
// credential record meaningful.
func emit(out *os.File, tok string) {
info, _ := decodeToken(tok)
attrs := map[string]string{"username": info.Sub, "password": tok}
if info.Exp > 0 {
attrs["password_expiry_utc"] = fmt.Sprintf("%d", info.Exp)
}
writeAttrs(out, attrs)
}
func configFromEnv() config {
vaultURL := os.Getenv("COSMOCR_VAULT_URL")
if vaultURL == "" {
vaultURL = "https://vault.cosmocr.at"
}
aud := os.Getenv("COSMOCR_AUDIENCE")
if aud == "" {
aud = "gitweb"
}
return config{
vaultURL: vaultURL,
audience: aud,
client: &http.Client{Timeout: 30 * time.Second},
openURL: openBrowser,
stderr: os.Stderr,
timeout: 3 * time.Minute,
}
}
// useDeviceFlow reports whether to skip the browser and use the device-code
// flow: when explicitly requested, or on a headless machine with no display.
func useDeviceFlow() bool {
if os.Getenv("COSMOCR_DEVICE") == "1" {
return true
}
switch runtime.GOOS {
case "linux":
return os.Getenv("DISPLAY") == "" && os.Getenv("WAYLAND_DISPLAY") == ""
default:
return false
}
}
// openBrowser best-effort launches the user's browser at url.
func openBrowser(url string) {
var cmd string
var args []string
switch runtime.GOOS {
case "darwin":
cmd = "open"
case "windows":
cmd, args = "rundll32", []string{"url.dll,FileProtocolHandler"}
default:
cmd = "xdg-open"
}
_ = exec.Command(cmd, append(args, url)...).Start()
}
|