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

     1  // Copyright 2015 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 deprecated
    16  
    17  import (
    18  	"context"
    19  	"errors"
    20  	"fmt"
    21  	"net/url"
    22  	"time"
    23  
    24  	"go.chromium.org/luci/server/auth"
    25  	"go.chromium.org/luci/server/auth/internal"
    26  	"go.chromium.org/luci/server/auth/openid"
    27  	"go.chromium.org/luci/server/tokens"
    28  )
    29  
    30  // Note: this file is a part of deprecated CookieAuthMethod implementation.
    31  
    32  // openIDStateToken is used to generate `state` parameter used in OpenID flow to
    33  // pass state between our app and authentication backend.
    34  var openIDStateToken = tokens.TokenKind{
    35  	Algo:       tokens.TokenAlgoHmacSHA256,
    36  	Expiration: 30 * time.Minute,
    37  	SecretKey:  "openid_state_token",
    38  	Version:    1,
    39  }
    40  
    41  // authenticationURI returns an URI to redirect a user to in order to
    42  // authenticate via OpenID.
    43  //
    44  // This is step 1 of the authentication flow. Generate authentication URL and
    45  // redirect user's browser to it. After consent screen, redirect_uri will be
    46  // called (via user's browser) with `state` and authorization code passed to it,
    47  // eventually resulting in a call to 'handle_authorization_code'.
    48  func authenticationURI(ctx context.Context, cfg *Settings, state map[string]string) (string, error) {
    49  	if cfg.ClientID == "" || cfg.RedirectURI == "" || cfg.DiscoveryURL == "" {
    50  		return "", ErrNotConfigured
    51  	}
    52  
    53  	// Grab authorization URL from discovery doc.
    54  	discovery, err := openid.FetchDiscoveryDoc(ctx, cfg.DiscoveryURL)
    55  	if err != nil {
    56  		return "", err
    57  	}
    58  	if discovery.AuthorizationEndpoint == "" {
    59  		return "", errors.New("openid: bad discovery doc, empty authorization_endpoint")
    60  	}
    61  
    62  	// Wrap state into HMAC-protected token.
    63  	stateTok, err := openIDStateToken.Generate(ctx, nil, state, 0)
    64  	if err != nil {
    65  		return "", err
    66  	}
    67  
    68  	// Generate final URL.
    69  	v := url.Values{}
    70  	v.Set("client_id", cfg.ClientID)
    71  	v.Set("redirect_uri", cfg.RedirectURI)
    72  	v.Set("response_type", "code")
    73  	v.Set("scope", "openid email profile")
    74  	v.Set("prompt", "select_account")
    75  	v.Set("state", stateTok)
    76  	return discovery.AuthorizationEndpoint + "?" + v.Encode(), nil
    77  }
    78  
    79  // validateStateToken validates 'state' token passed to redirect_uri. Returns
    80  // whatever `state` was passed to authenticationURI.
    81  func validateStateToken(ctx context.Context, stateTok string) (map[string]string, error) {
    82  	return openIDStateToken.Validate(ctx, stateTok, nil)
    83  }
    84  
    85  // handleAuthorizationCode exchange `code` for user ID token and user profile.
    86  func handleAuthorizationCode(ctx context.Context, cfg *Settings, code string) (uid string, u *auth.User, err error) {
    87  	if cfg.ClientID == "" || cfg.ClientSecret == "" || cfg.RedirectURI == "" {
    88  		return "", nil, ErrNotConfigured
    89  	}
    90  
    91  	// Validate the discover doc has necessary fields to proceed.
    92  	discovery, err := openid.FetchDiscoveryDoc(ctx, cfg.DiscoveryURL)
    93  	switch {
    94  	case err != nil:
    95  		return "", nil, err
    96  	case discovery.TokenEndpoint == "":
    97  		return "", nil, errors.New("openid: bad discovery doc, empty token_endpoint")
    98  	}
    99  
   100  	// Prepare a request to exchange authorization code for the ID token.
   101  	v := url.Values{}
   102  	v.Set("code", code)
   103  	v.Set("client_id", cfg.ClientID)
   104  	v.Set("client_secret", cfg.ClientSecret)
   105  	v.Set("redirect_uri", cfg.RedirectURI)
   106  	v.Set("grant_type", "authorization_code")
   107  	payload := v.Encode()
   108  
   109  	// Send POST to the token endpoint with URL-encoded parameters to get back the
   110  	// ID token. There's more stuff in the reply, we don't need it.
   111  	var token struct {
   112  		IDToken string `json:"id_token"`
   113  	}
   114  	req := internal.Request{
   115  		Method: "POST",
   116  		URL:    discovery.TokenEndpoint,
   117  		Body:   []byte(payload),
   118  		Headers: map[string]string{
   119  			"Content-Type": "application/x-www-form-urlencoded",
   120  		},
   121  		Out: &token,
   122  	}
   123  	if err := req.Do(ctx); err != nil {
   124  		return "", nil, err
   125  	}
   126  
   127  	// Unpack the ID token to grab the user information from it.
   128  	tok, user, err := openid.UserFromIDToken(ctx, token.IDToken, discovery)
   129  	if err != nil {
   130  		return "", nil, err
   131  	}
   132  	// Make sure the token was created via the expected OAuth client.
   133  	if tok.Aud != cfg.ClientID {
   134  		return "", nil, fmt.Errorf("bad ID token - expecting audience %q, got %q", cfg.ClientID, tok.Aud)
   135  	}
   136  	return tok.Sub, user, nil
   137  }