github.com/spinnaker/spin@v1.30.0/config/auth/iap/service_account_auth_helper.go (about)

     1  // Copyright (c) 2019, Google Inc.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //   http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  //   Unless required by applicable law or agreed to in writing, software
    10  //   distributed under the License is distributed on an "AS IS" BASIS,
    11  //   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  //   See the License for the specific language governing permissions and
    13  //   limitations under the License.
    14  
    15  package config
    16  
    17  import (
    18  	"crypto/rsa"
    19  	"crypto/x509"
    20  	"encoding/json"
    21  	"encoding/pem"
    22  	"errors"
    23  	"fmt"
    24  	"io/ioutil"
    25  	"net/http"
    26  	"net/url"
    27  	"strings"
    28  	"time"
    29  
    30  	"golang.org/x/oauth2/jws"
    31  
    32  	oauth "golang.org/x/oauth2/google"
    33  )
    34  
    35  const (
    36  	audURL                  = "https://www.googleapis.com/oauth2/v4/token"
    37  	jwtBearerTokenGrantType = "urn:ietf:params:oauth:grant-type:jwt-bearer"
    38  	expirationSecs          = 3600
    39  )
    40  
    41  // GetIDTokenWithServiceAccount gets an IAP ID Token required for authenticating a service account with IAP.
    42  // For more info, see https://cloud.google.com/iap/docs/authentication-howto#authenticating_from_a_service_account
    43  func GetIDTokenWithServiceAccount(config Config) (string, error) {
    44  	creds, err := ioutil.ReadFile(config.ServiceAccountKeyPath)
    45  	if err != nil {
    46  		return "", fmt.Errorf("could not read service account creds file: %v", err)
    47  	}
    48  
    49  	// Creating and signing JWT token.
    50  
    51  	jwtc, err := oauth.JWTConfigFromJSON(creds)
    52  	if err != nil {
    53  		return "", fmt.Errorf("could not parse config from service account creds: %v", err)
    54  	}
    55  
    56  	pk, err := parseKey(jwtc.PrivateKey)
    57  	if err != nil {
    58  		return "", fmt.Errorf("invalid private json key: %v", err)
    59  	}
    60  
    61  	now := time.Now().Unix()
    62  
    63  	claims := &jws.ClaimSet{
    64  		Aud: audURL,
    65  		Iss: jwtc.Email,
    66  		Sub: jwtc.Email,
    67  		Iat: now,
    68  		Exp: now + expirationSecs,
    69  		PrivateClaims: map[string]interface{}{
    70  			"target_audience": config.IapClientId,
    71  		},
    72  	}
    73  
    74  	h := &jws.Header{
    75  		Algorithm: "RS256",
    76  		Typ:       "JWT",
    77  		KeyID:     jwtc.PrivateKeyID,
    78  	}
    79  
    80  	jwt, err := jws.Encode(h, claims, pk)
    81  	if err != nil {
    82  		return "", fmt.Errorf("failed to encode jwt: %v", err)
    83  	}
    84  
    85  	// Use the JWT to get the OIDC token.
    86  
    87  	form := url.Values{}
    88  	form.Add("grant_type", jwtBearerTokenGrantType)
    89  	form.Add("assertion", jwt)
    90  
    91  	req, err := http.NewRequest("POST", audURL, strings.NewReader(form.Encode()))
    92  	if err != nil {
    93  		return "", fmt.Errorf("failed to create request: %v", err)
    94  	}
    95  	req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
    96  
    97  	client := &http.Client{}
    98  
    99  	resp, err := client.Do(req)
   100  	if err != nil {
   101  		return "", fmt.Errorf("failed to get OIDC token: %s", err)
   102  	}
   103  	defer resp.Body.Close()
   104  
   105  	if resp.StatusCode != http.StatusOK {
   106  		return "", fmt.Errorf("failed to get successful response: %+v", resp)
   107  	}
   108  
   109  	tokenBody, err := ioutil.ReadAll(resp.Body)
   110  	if err != nil {
   111  		return "", fmt.Errorf("could not read response body of OIDC token request")
   112  	}
   113  
   114  	var payload map[string]interface{}
   115  	err = json.Unmarshal(tokenBody, &payload)
   116  	if err != nil {
   117  		return "", fmt.Errorf("failed to parse access token payload %q: %v", string(tokenBody), err)
   118  	}
   119  
   120  	return fmt.Sprintf("%v", payload["id_token"]), nil
   121  }
   122  
   123  func parseKey(key []byte) (*rsa.PrivateKey, error) {
   124  	block, _ := pem.Decode(key)
   125  	if block != nil {
   126  		key = block.Bytes
   127  	}
   128  	parsedKey, err := x509.ParsePKCS8PrivateKey(key)
   129  	if err != nil {
   130  		parsedKey, err = x509.ParsePKCS1PrivateKey(key)
   131  		if err != nil {
   132  			return nil, fmt.Errorf("private key should be a PEM or plain PKSC1 or PKCS8; parse error: %v", err)
   133  		}
   134  	}
   135  	parsed, ok := parsedKey.(*rsa.PrivateKey)
   136  	if !ok {
   137  		return nil, errors.New("private key is not an RSA key")
   138  	}
   139  	return parsed, nil
   140  }