go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/server/auth/openid/id_token.go (about)

     1  // Copyright 2017 The LUCI Authors.
     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 openid
    16  
    17  import (
    18  	"context"
    19  	"time"
    20  
    21  	"go.chromium.org/luci/auth/jwt"
    22  	"go.chromium.org/luci/common/clock"
    23  	"go.chromium.org/luci/common/errors"
    24  )
    25  
    26  // IDToken is a verified deserialized ID token.
    27  //
    28  // See https://developers.google.com/identity/protocols/OpenIDConnect.
    29  type IDToken struct {
    30  	Iss           string `json:"iss"`
    31  	AtHash        string `json:"at_hash"`
    32  	EmailVerified bool   `json:"email_verified"`
    33  	Sub           string `json:"sub"`
    34  	Azp           string `json:"azp"`
    35  	Email         string `json:"email"`
    36  	Profile       string `json:"profile"`
    37  	Picture       string `json:"picture"`
    38  	Name          string `json:"name"`
    39  	Aud           string `json:"aud"`
    40  	Iat           int64  `json:"iat"`
    41  	Exp           int64  `json:"exp"`
    42  	Nonce         string `json:"nonce"`
    43  	Hd            string `json:"hd"`
    44  
    45  	// This section is present only for tokens generated via GCE Metadata server
    46  	// in "full" format.
    47  	Google struct {
    48  		ComputeEngine struct {
    49  			InstanceCreationTimestamp int64  `json:"instance_creation_timestamp"`
    50  			InstanceID                string `json:"instance_id"`
    51  			InstanceName              string `json:"instance_name"`
    52  			ProjectID                 string `json:"project_id"`
    53  			ProjectNumber             int64  `json:"project_number"`
    54  			Zone                      string `json:"zone"`
    55  		} `json:"compute_engine"`
    56  	} `json:"google"`
    57  }
    58  
    59  const allowedClockSkew = 30 * time.Second
    60  
    61  // VerifyIDToken deserializes and verifies the ID token.
    62  //
    63  // It checks the signature, expiration time and verifies fields `iss` and
    64  // `email_verified`.
    65  //
    66  // It checks `aud` and `sub` are present, but does NOT verify them any further.
    67  // It is the caller's responsibility to do so.
    68  //
    69  // This is a fast local operation.
    70  func VerifyIDToken(ctx context.Context, token string, keys jwt.SignatureVerifier, issuer string) (*IDToken, error) {
    71  	// See https://developers.google.com/identity/protocols/OpenIDConnect#validatinganidtoken
    72  
    73  	tok := &IDToken{}
    74  	if err := jwt.VerifyAndDecode(token, tok, keys); err != nil {
    75  		return nil, errors.Annotate(err, "bad ID token").Err()
    76  	}
    77  
    78  	exp := time.Unix(tok.Exp, 0)
    79  	now := clock.Now(ctx)
    80  
    81  	switch {
    82  	case tok.Iss != issuer && "https://"+tok.Iss != issuer:
    83  		return nil, errors.Reason("bad ID token: expecting issuer %q, got %q", issuer, tok.Iss).Err()
    84  	case exp.Add(allowedClockSkew).Before(now):
    85  		return nil, errors.Reason("bad ID token: expired %s ago", now.Sub(exp)).Err()
    86  	case !tok.EmailVerified:
    87  		return nil, errors.Reason("bad ID token: the email %q is not verified", tok.Email).Err()
    88  	case tok.Aud == "":
    89  		return nil, errors.Reason("bad ID token: the audience is missing").Err()
    90  	case tok.Sub == "":
    91  		return nil, errors.Reason("bad ID token: the subject is missing").Err()
    92  	}
    93  
    94  	return tok, nil
    95  }