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 }