1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 package impersonate
16
17 import (
18 "bytes"
19 "context"
20 "encoding/json"
21 "errors"
22 "fmt"
23 "net/http"
24 "time"
25
26 "cloud.google.com/go/auth"
27 "cloud.google.com/go/auth/credentials"
28 "cloud.google.com/go/auth/httptransport"
29 "cloud.google.com/go/auth/internal"
30 )
31
32
33 type IDTokenOptions struct {
34
35
36 Audience string
37
38
39 TargetPrincipal string
40
41
42
43 IncludeEmail bool
44
45
46
47
48 Delegates []string
49
50
51
52
53 Credentials *auth.Credentials
54
55
56
57 Client *http.Client
58 }
59
60 func (o *IDTokenOptions) validate() error {
61 if o == nil {
62 return errors.New("impersonate: options must be provided")
63 }
64 if o.Audience == "" {
65 return errors.New("impersonate: audience must be provided")
66 }
67 if o.TargetPrincipal == "" {
68 return errors.New("impersonate: target service account must be provided")
69 }
70 return nil
71 }
72
73 var (
74 defaultScope = "https://www.googleapis.com/auth/cloud-platform"
75 )
76
77
78
79
80
81
82 func NewIDTokenCredentials(opts *IDTokenOptions) (*auth.Credentials, error) {
83 if err := opts.validate(); err != nil {
84 return nil, err
85 }
86 var client *http.Client
87 var creds *auth.Credentials
88 if opts.Client == nil && opts.Credentials == nil {
89 var err error
90
91 creds, err = credentials.DetectDefault(&credentials.DetectOptions{
92 Scopes: []string{defaultScope},
93 UseSelfSignedJWT: true,
94 })
95 if err != nil {
96 return nil, err
97 }
98 client, err = httptransport.NewClient(&httptransport.Options{
99 Credentials: creds,
100 })
101 if err != nil {
102 return nil, err
103 }
104 } else if opts.Client == nil {
105 creds = opts.Credentials
106 client = internal.CloneDefaultClient()
107 if err := httptransport.AddAuthorizationMiddleware(client, opts.Credentials); err != nil {
108 return nil, err
109 }
110 } else {
111 client = opts.Client
112 }
113
114 itp := impersonatedIDTokenProvider{
115 client: client,
116 targetPrincipal: opts.TargetPrincipal,
117 audience: opts.Audience,
118 includeEmail: opts.IncludeEmail,
119 }
120 for _, v := range opts.Delegates {
121 itp.delegates = append(itp.delegates, formatIAMServiceAccountName(v))
122 }
123
124 var udp auth.CredentialsPropertyProvider
125 if creds != nil {
126 udp = auth.CredentialsPropertyFunc(creds.UniverseDomain)
127 }
128 return auth.NewCredentials(&auth.CredentialsOptions{
129 TokenProvider: auth.NewCachedTokenProvider(itp, nil),
130 UniverseDomainProvider: udp,
131 }), nil
132 }
133
134 type generateIDTokenRequest struct {
135 Audience string `json:"audience"`
136 IncludeEmail bool `json:"includeEmail"`
137 Delegates []string `json:"delegates,omitempty"`
138 }
139
140 type generateIDTokenResponse struct {
141 Token string `json:"token"`
142 }
143
144 type impersonatedIDTokenProvider struct {
145 client *http.Client
146
147 targetPrincipal string
148 audience string
149 includeEmail bool
150 delegates []string
151 }
152
153 func (i impersonatedIDTokenProvider) Token(ctx context.Context) (*auth.Token, error) {
154 genIDTokenReq := generateIDTokenRequest{
155 Audience: i.audience,
156 IncludeEmail: i.includeEmail,
157 Delegates: i.delegates,
158 }
159 bodyBytes, err := json.Marshal(genIDTokenReq)
160 if err != nil {
161 return nil, fmt.Errorf("impersonate: unable to marshal request: %w", err)
162 }
163
164 url := fmt.Sprintf("%s/v1/%s:generateIdToken", iamCredentialsEndpoint, formatIAMServiceAccountName(i.targetPrincipal))
165 req, err := http.NewRequest("POST", url, bytes.NewReader(bodyBytes))
166 if err != nil {
167 return nil, fmt.Errorf("impersonate: unable to create request: %w", err)
168 }
169 req.Header.Set("Content-Type", "application/json")
170 resp, err := i.client.Do(req)
171 if err != nil {
172 return nil, fmt.Errorf("impersonate: unable to generate ID token: %w", err)
173 }
174 defer resp.Body.Close()
175 body, err := internal.ReadAll(resp.Body)
176 if err != nil {
177 return nil, fmt.Errorf("impersonate: unable to read body: %w", err)
178 }
179 if c := resp.StatusCode; c < 200 || c > 299 {
180 return nil, fmt.Errorf("impersonate: status code %d: %s", c, body)
181 }
182
183 var generateIDTokenResp generateIDTokenResponse
184 if err := json.Unmarshal(body, &generateIDTokenResp); err != nil {
185 return nil, fmt.Errorf("impersonate: unable to parse response: %w", err)
186 }
187 return &auth.Token{
188 Value: generateIDTokenResp.Token,
189
190 Expiry: time.Now().Add(1 * time.Hour),
191 }, nil
192 }
193
View as plain text