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 }