lib/translator/translator.go (146 lines of code) (raw):

// Copyright 2019 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package translator import ( "context" "encoding/json" "fmt" "io/ioutil" "net/http" "strings" "github.com/GoogleCloudPlatform/healthcare-federated-access-services/lib/ga4gh" /* copybara-comment: ga4gh */ ) // Translator is used to convert an HTTP bearer authorization string that is _not_ in // the normal Identity format into an Identity. This is useful when // interoperating with systems that do not yet provide a GA4GH identity. type Translator interface { TranslateToken(ctx context.Context, auth string) (*ga4gh.Identity, error) } // FetchUserinfoClaims calls the /userinfo endpoint of an issuer to fetch additional claims. func FetchUserinfoClaims(ctx context.Context, client *http.Client, id *ga4gh.Identity, tok string, translator Translator) (*ga4gh.Identity, error) { // Issue a Get request to the issuer's /userinfo endpoint. // TODO: use JWKS to discover the /userinfo endpoint. contentType, userInfo, err := issueGetRequest(ctx, client, strings.TrimSuffix(id.Issuer, "/")+"/userinfo", tok) if err != nil { return nil, err } // Convert the /userinfo response to an identity based on the content type. var userinfo ga4gh.Identity switch contentType { case "application/json": if err := json.Unmarshal(userInfo, &userinfo); err != nil { return nil, fmt.Errorf("inspecting user info claims: %v", err) } case "application/jwt": if translator == nil { return nil, fmt.Errorf("unpack application/jwt failed: translator not provided") } tok, err := translator.TranslateToken(ctx, string(userInfo)) if err != nil { return nil, fmt.Errorf("inspecting signed user info claims: %v", err) } if tok.Issuer != id.Issuer { return nil, fmt.Errorf("incorrect issuer in user info claims: got: %q, expected: %q", tok.Issuer, id.Issuer) } userinfo = *tok default: return nil, fmt.Errorf("unsupported content type returned by /userinfo endpoint: %q", contentType) } mergeIdentityWithUserinfo(id, &userinfo) return id, nil } func mergeIdentityWithUserinfo(id *ga4gh.Identity, userinfo *ga4gh.Identity) { if len(id.Subject) == 0 { id.Subject = userinfo.Subject } if len(id.Issuer) == 0 { id.Issuer = userinfo.Issuer } if id.IssuedAt == 0 { id.IssuedAt = userinfo.IssuedAt } if id.NotBefore == 0 { id.NotBefore = userinfo.NotBefore } if id.Expiry == 0 { id.Expiry = userinfo.Expiry } if len(id.Scope) == 0 { id.Scope = userinfo.Scope } if len(id.Scp) == 0 { id.Scp = userinfo.Scp } if len(id.Audiences) == 0 { id.Audiences = userinfo.Audiences } if len(id.AuthorizedParty) == 0 { id.AuthorizedParty = userinfo.AuthorizedParty } if len(id.ID) == 0 { id.ID = userinfo.ID } if len(id.Nonce) == 0 { id.Nonce = userinfo.Nonce } if len(id.GA4GH) == 0 { id.GA4GH = userinfo.GA4GH } if len(id.IdentityProvider) == 0 { id.IdentityProvider = userinfo.IdentityProvider } if len(id.Identities) == 0 { id.Identities = userinfo.Identities } if len(id.Username) == 0 { id.Username = userinfo.Username } if len(id.Email) == 0 { id.Email = userinfo.Email } if len(id.Name) == 0 { id.Name = userinfo.Name } if len(id.Nickname) == 0 { id.Nickname = userinfo.Nickname } if len(id.GivenName) == 0 { id.GivenName = userinfo.GivenName } if len(id.FamilyName) == 0 { id.FamilyName = userinfo.FamilyName } if len(id.MiddleName) == 0 { id.MiddleName = userinfo.MiddleName } if len(id.ZoneInfo) == 0 { id.ZoneInfo = userinfo.ZoneInfo } if len(id.Locale) == 0 { id.Locale = userinfo.Locale } if len(id.Picture) == 0 { id.Picture = userinfo.Picture } if len(id.Profile) == 0 { id.Profile = userinfo.Profile } if len(id.Realm) == 0 { id.Realm = userinfo.Realm } if len(id.VisaJWTs) == 0 { id.VisaJWTs = userinfo.VisaJWTs } } func issueGetRequest(ctx context.Context, client *http.Client, url, acTok string) (string, []byte, error) { req, err := http.NewRequest("GET", url, nil) if err != nil { return "", []byte{}, fmt.Errorf("failed to create request: %v", err) } req.Header.Add("Authorization", "Bearer "+acTok) resp, err := client.Do(req.WithContext(ctx)) if err != nil { return "", []byte{}, fmt.Errorf("failed to send request: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return "", []byte{}, fmt.Errorf("response returned error code %q: %q", resp.Status, resp.Body) } contentType := strings.ToLower(strings.Split(resp.Header.Get("Content-Type"), ";")[0]) body, err := ioutil.ReadAll(resp.Body) if err != nil { return "", []byte{}, fmt.Errorf("failed to ready response body: %v", err) } return contentType, body, nil }