github.com/spinnaker/spin@v1.30.0/config/auth/iap/oauth.go (about)

     1  // Copyright (c) 2018, Snap Inc.
     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 config
    16  
    17  import (
    18  	"bytes"
    19  	"context"
    20  	"encoding/json"
    21  	"fmt"
    22  	"html/template"
    23  	"log"
    24  	"net"
    25  	"net/http"
    26  	"net/url"
    27  	"strings"
    28  	"time"
    29  
    30  	"golang.org/x/oauth2"
    31  	"golang.org/x/oauth2/google"
    32  )
    33  
    34  const (
    35  	googleTokenURL      = "https://www.googleapis.com/oauth2/v4/token"
    36  	oauthEmailScope     = "https://www.googleapis.com/auth/userinfo.email"
    37  	serverStateTokenLen = 60
    38  )
    39  
    40  // Configure the default request scopes, this can be overriden
    41  var oauthScopes = []string{oauthEmailScope}
    42  
    43  // Creates a new iapOAuthResponse object
    44  type iapOAuthResponse struct {
    45  	AccessToken  string `json:"access_token,omitempty"`
    46  	RefreshToken string `json:"refresh_token,omitempty"`
    47  	Type         string `json:"token_type,omitempty"`
    48  	Expires      int    `json:"expires_in,omitempty"`
    49  	IDToken      string `json:"id_token,omitempty"`
    50  }
    51  
    52  type oauthReceiver struct {
    53  	port         int
    54  	clientState  string
    55  	killMeNow    bool
    56  	mainClient   string
    57  	doneChan     chan error
    58  	callback     func(*oauth2.Token, *oauth2.Config, string) (string, error)
    59  	clientId     string
    60  	clientSecret string
    61  }
    62  
    63  // Cleanly exit the HTTP server.
    64  func (o *oauthReceiver) killWhenReady(n net.Conn, s http.ConnState) {
    65  	if o.killMeNow && s.String() == "idle" && o.mainClient == n.RemoteAddr().String() {
    66  		o.doneChan <- nil
    67  	}
    68  }
    69  
    70  // NewOAuthConfig creates a new OAuth config
    71  func (o *oauthReceiver) NewOAuthConfig() *oauth2.Config {
    72  	return &oauth2.Config{
    73  		ClientID:     o.clientId,
    74  		ClientSecret: o.clientSecret,
    75  		RedirectURL:  fmt.Sprintf("http://localhost:%d", o.port),
    76  		Scopes:       oauthScopes,
    77  		Endpoint:     google.Endpoint,
    78  	}
    79  }
    80  
    81  // check the iap's state
    82  func ValidIAPStateToken(state, clientState string) bool {
    83  	return state == clientState
    84  }
    85  
    86  // ServeHTTP will serve an HTTP server on localhost used to receive the OAuth callback
    87  func (o *oauthReceiver) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    88  	var tok *oauth2.Token
    89  	var conf *oauth2.Config
    90  
    91  	defer func() {
    92  		r := recover()
    93  		if r != nil {
    94  			log.Printf("panic: %s\n", r)
    95  			o.doneChan <- fmt.Errorf("%v", r)
    96  		}
    97  	}()
    98  	if r.URL.String() == "/favicon.ico" {
    99  		return
   100  	}
   101  	if r.URL.String() == "/robots.txt" {
   102  		return
   103  	}
   104  
   105  	o.mainClient = r.RemoteAddr
   106  
   107  	conf = o.NewOAuthConfig()
   108  	oauthCode := r.FormValue("code")
   109  	state := r.FormValue("state")
   110  	if oauthCode == "" || state == "" {
   111  		response := fmt.Errorf("Invalid response from Google's Account Service")
   112  		o.WebOutput(w, "Uh Oh!", response.Error())
   113  		o.doneChan <- response
   114  		return
   115  	}
   116  
   117  	validateStateFunc := ValidIAPStateToken
   118  
   119  	if !validateStateFunc(state, o.clientState) {
   120  		response := fmt.Errorf("Invalid state token received from server")
   121  		o.WebOutput(w, "Uh Oh!", response.Error())
   122  		o.doneChan <- response
   123  		return
   124  	}
   125  
   126  	tok, err := conf.Exchange(context.Background(), oauthCode)
   127  	if err != nil {
   128  		response := fmt.Errorf("Received invalid OAUTH token code: %v", err)
   129  		o.WebOutput(w, "Uh Oh!", response.Error())
   130  		o.doneChan <- response
   131  	}
   132  
   133  	msg, err := o.callback(tok, conf, state)
   134  	if err != nil {
   135  		o.WebOutput(w, "Uh Oh!", err.Error())
   136  	} else {
   137  		o.WebOutput(w, "Successfully authenticated to Spinnaker!", msg)
   138  	}
   139  	o.killMeNow = true
   140  }
   141  
   142  // WebOutput takes a header and message and displays them in a simple template
   143  // replacing the old plain white response page :)
   144  func (o *oauthReceiver) WebOutput(w http.ResponseWriter, header string, msg string) {
   145  	// Create basic template
   146  	t, err := template.New("base").Parse(defaultTemplate)
   147  	if err != nil {
   148  		fmt.Fprint(w, err)
   149  	}
   150  	err = t.Execute(w, map[string]interface{}{"header": header, "messages": strings.Split(msg, "\n")})
   151  	if err != nil {
   152  		fmt.Fprint(w, err)
   153  	}
   154  }
   155  
   156  // RequestIapIDToken implements the audience parameter required for accessing
   157  // IAP, see for more https://cloud.google.com/iap/docs/authentication-howto
   158  func RequestIapIDToken(token string, clientId string, clientSecret string, iapClientId string) (string, error) {
   159  	body := url.Values{}
   160  	body.Set("client_id", clientId)
   161  	body.Add("client_secret", clientSecret)
   162  	body.Add("refresh_token", token)
   163  	body.Add("grant_type", "refresh_token")
   164  	body.Add("audience", iapClientId)
   165  
   166  	// Create request to google
   167  	req, err := http.NewRequest("POST", googleTokenURL, bytes.NewBufferString(body.Encode()))
   168  	if err != nil {
   169  		return "", fmt.Errorf("Invalid request crafted when preparing IAP id_token, err: %s", err)
   170  	}
   171  	req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
   172  
   173  	client := http.Client{
   174  		Timeout: time.Second * 30,
   175  	}
   176  	resp, err := client.Do(req)
   177  	if err != nil {
   178  		return "", fmt.Errorf("Unable to exchange access_token for id_token with audience, err: %s", err)
   179  	}
   180  	defer resp.Body.Close()
   181  
   182  	if resp.StatusCode != http.StatusOK {
   183  		return "", fmt.Errorf("Invalid response received when getting IAP id_token, err: %s", err)
   184  	}
   185  
   186  	// Create a one time response struct
   187  	oauthResponse := &iapOAuthResponse{}
   188  	// Decode the response
   189  	err = json.NewDecoder(resp.Body).Decode(&oauthResponse)
   190  	if err != nil {
   191  		return "", fmt.Errorf("Invalid response received when decoding IAP id_token, err: %s", err)
   192  	}
   193  
   194  	// Check that it's not empty/blank
   195  	if len(oauthResponse.IDToken) <= 0 {
   196  		return "", fmt.Errorf("Invalid ID Token returned")
   197  	}
   198  
   199  	// return
   200  	return oauthResponse.IDToken, nil
   201  }