diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..23105c4 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +iap_curl diff --git a/README.md b/README.md new file mode 100644 index 0000000..83f7dbd --- /dev/null +++ b/README.md @@ -0,0 +1,29 @@ +iap_curl +======== + +curl wrapper for making HTTP request to IAP-protected app in CLI more easier than curl + +## Usage + +```console +$ export GOOGLE_APPLICATION_CREDENTIALS="/path/to/service-account.json" +$ export IAP_CLIENT_ID="342624545358-asdfd8fas9df8sd7ga0sdguadfpvqp69.apps.googleusercontent.com" +$ +$ iap_curl http://iap-protected.webapp.com +``` + +The option of iap_curl is fully compatible with curl one. + +## Installation + +``` +$ go get github.com/b4b4r07/iap_curl +``` + +## License + +MIT + +## Author + +b4b4r07 diff --git a/curl.go b/curl.go new file mode 100644 index 0000000..3af346e --- /dev/null +++ b/curl.go @@ -0,0 +1,28 @@ +package main + +import ( + "os" + "os/exec" + "runtime" +) + +func doCurl(args []string) error { + // Check if you have curl command + command := "curl" + if _, err := exec.LookPath(command); err != nil { + return err + } + for _, arg := range args { + command += " " + arg + } + var cmd *exec.Cmd + if runtime.GOOS == "windows" { + cmd = exec.Command("cmd", "/c", command) + } else { + cmd = exec.Command("sh", "-c", command) + } + cmd.Stderr = os.Stderr + cmd.Stdout = os.Stdout + cmd.Stdin = os.Stdin + return cmd.Run() +} diff --git a/iap.go b/iap.go new file mode 100644 index 0000000..51c01be --- /dev/null +++ b/iap.go @@ -0,0 +1,114 @@ +package main + +import ( + "context" + "crypto/rsa" + "crypto/x509" + "encoding/json" + "encoding/pem" + "errors" + "fmt" + "io/ioutil" + "net/url" + "time" + + "golang.org/x/oauth2" + "golang.org/x/oauth2/google" + "golang.org/x/oauth2/jws" +) + +func readRsaPrivateKey(bytes []byte) (*rsa.PrivateKey, error) { + block, _ := pem.Decode(bytes) + if block == nil { + return nil, errors.New("invalid private key data") + } + + var key *rsa.PrivateKey + var err error + if block.Type == "RSA PRIVATE KEY" { + key, err = x509.ParsePKCS1PrivateKey(block.Bytes) + if err != nil { + return nil, err + } + } else if block.Type == "PRIVATE KEY" { + keyInterface, err := x509.ParsePKCS8PrivateKey(block.Bytes) + if err != nil { + return nil, err + } + var ok bool + key, ok = keyInterface.(*rsa.PrivateKey) + if !ok { + return nil, errors.New("not RSA private key") + } + } else { + return nil, fmt.Errorf("invalid private key type : %s", block.Type) + } + + key.Precompute() + + if err := key.Validate(); err != nil { + return nil, err + } + + return key, nil +} + +func getToken(saPath, clientID string) (token string, err error) { + sa, err := ioutil.ReadFile(saPath) + if err != nil { + return + } + conf, err := google.JWTConfigFromJSON(sa) + if err != nil { + return + } + rsaKey, _ := readRsaPrivateKey(conf.PrivateKey) + iat := time.Now() + exp := iat.Add(time.Hour) + jwt := &jws.ClaimSet{ + Iss: conf.Email, + Aud: TokenURI, + Iat: iat.Unix(), + Exp: exp.Unix(), + PrivateClaims: map[string]interface{}{ + "target_audience": clientID, + }, + } + jwsHeader := &jws.Header{ + Algorithm: "RS256", + Typ: "JWT", + } + + msg, err := jws.Encode(jwsHeader, jwt, rsaKey) + if err != nil { + return + } + + v := url.Values{} + v.Set("grant_type", "urn:ietf:params:oauth:grant-type:jwt-bearer") + v.Set("assertion", msg) + + ctx := context.Background() + hc := oauth2.NewClient(ctx, nil) + resp, err := hc.PostForm(TokenURI, v) + if err != nil { + return + } + defer resp.Body.Close() + + body, err := ioutil.ReadAll(resp.Body) + + var tokenRes struct { + AccessToken string `json:"access_token"` + TokenType string `json:"token_type"` + IDToken string `json:"id_token"` + ExpiresIn int64 `json:"expires_in"` + } + + if err := json.Unmarshal(body, &tokenRes); err != nil { + return token, err + } + + token = tokenRes.IDToken + return +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..d29a565 --- /dev/null +++ b/main.go @@ -0,0 +1,65 @@ +package main + +import ( + "fmt" + "os" +) + +const ( + TokenURI = "https://www.googleapis.com/oauth2/v4/token" + + GoogleApplicationCredentials = "GOOGLE_APPLICATION_CREDENTIALS" + IAPClientID = "IAP_CLIENT_ID" +) + +var helpText string = `Usage: curl +` + +func main() { + os.Exit(run(os.Args[1:])) +} + +func run(args []string) int { + var ( + creds = os.Getenv(GoogleApplicationCredentials) + clientID = os.Getenv(IAPClientID) + ) + + if len(args) > 0 { + if args[0] == "-h" || args[0] == "--help" { + fmt.Fprint(os.Stderr, helpText) + return 1 + } + } + + if creds == "" { + fmt.Fprintf(os.Stderr, "Error: %s is missing", GoogleApplicationCredentials) + return 1 + } + + if clientID == "" { + fmt.Fprintf(os.Stderr, "Error: %s is missing", IAPClientID) + return 1 + } + + token, err := getToken(creds, clientID) + if err != nil { + fmt.Fprintf(os.Stderr, "Error: %v\n", err.Error()) + return 1 + } + + authHeader := fmt.Sprintf("'Authorization: Bearer %s'", token) + curlArgs := append( + // For IAP header + []string{"-H", authHeader}, + // Original args + args..., + ) + + if err := doCurl(curlArgs); err != nil { + fmt.Fprintf(os.Stderr, "Error: %v\n", err.Error()) + return 1 + } + + return 0 +}