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 }