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  }