github.com/grailbio/base@v0.0.11/cmd/ticket-server/googleblesser.go (about) 1 // Copyright 2018 GRAIL, Inc. All rights reserved. 2 // Use of this source code is governed by the Apache-2.0 3 // license that can be found in the LICENSE file. 4 5 package main 6 7 import ( 8 "context" 9 "fmt" 10 "strings" 11 "time" 12 13 "github.com/coreos/go-oidc" 14 "github.com/grailbio/base/common/log" 15 v23context "v.io/v23/context" 16 "v.io/v23/rpc" 17 "v.io/v23/security" 18 ) 19 20 const ( 21 issuer = "https://accounts.google.com" 22 audience = "27162366543-edih9cqc3t8p5hn9ord1k1n7h4oajfhm.apps.googleusercontent.com" 23 24 extensionPrefix = "google" 25 ) 26 27 var ( 28 hostedDomains []string 29 ) 30 31 func googleBlesserInit(googleUserDomainList []string) { 32 hostedDomains = googleUserDomainList 33 } 34 35 func (c *claims) checkClaims() error { 36 if !c.EmailVerified { 37 return fmt.Errorf("ID token doesn't have a verified email") 38 } 39 40 if !stringInSlice(hostedDomains, c.HostedDomain) { 41 return fmt.Errorf("ID token has a wrong hosted domain: got %q, want %q", c.HostedDomain, strings.Join(hostedDomains, ",")) 42 } 43 44 if !stringInSlice(hostedDomains, emailDomain(c.Email)) { 45 return fmt.Errorf("ID token does not have a sufix with a authorized email domain (%q): %q", strings.Join(hostedDomains, ","), c.Email) 46 } 47 return nil 48 } 49 50 type claims struct { 51 HostedDomain string `json:"hd"` 52 EmailVerified bool `json:"email_verified"` 53 Email string `json:"email"` 54 } 55 56 type googleBlesser struct { 57 verifier *oidc.IDTokenVerifier 58 expirationInterval time.Duration 59 } 60 61 func newGoogleBlesser(ctx *v23context.T, expiration time.Duration, domains []string) *googleBlesser { 62 googleBlesserInit(domains) 63 64 provider, err := oidc.NewProvider(context.Background(), issuer) 65 if err != nil { 66 log.Error(ctx, err.Error()) 67 } 68 return &googleBlesser{ 69 verifier: provider.Verifier(&oidc.Config{ClientID: audience}), 70 expirationInterval: expiration, 71 } 72 } 73 74 func (blesser *googleBlesser) BlessGoogle(ctx *v23context.T, call rpc.ServerCall, idToken string) (security.Blessings, error) { 75 remoteAddress := call.RemoteEndpoint().Address 76 log.Info(ctx, "bless Google request", "remoteAddr", remoteAddress, "idToken", idToken, "idTokenLen", len(idToken)) 77 var empty security.Blessings 78 79 oidcIDToken, err := blesser.verifier.Verify(ctx, idToken) 80 if err != nil { 81 return empty, err 82 } 83 var claims claims 84 if err := oidcIDToken.Claims(&claims); err != nil { 85 return empty, nil 86 } 87 log.Debug(ctx, "", "oidcIDToken", oidcIDToken, "claims", claims) 88 89 if err := claims.checkClaims(); err != nil { 90 return empty, err 91 } 92 93 // ext will be something like 'google:razvanm@grailbio.com'. 94 ext := strings.Join([]string{extensionPrefix, claims.Email}, security.ChainSeparator) 95 96 securityCall := call.Security() 97 if securityCall.LocalPrincipal() == nil { 98 return empty, fmt.Errorf("server misconfiguration") 99 } 100 101 pubKey := securityCall.RemoteBlessings().PublicKey() 102 caveat, err := security.NewExpiryCaveat(time.Now().Add(blesser.expirationInterval)) 103 if err != nil { 104 return empty, err 105 } 106 return securityCall.LocalPrincipal().Bless(pubKey, securityCall.LocalBlessings(), ext, caveat) 107 }