Skip to content

Commit

Permalink
Update the googleapps IDP provider to work with changes to Google log…
Browse files Browse the repository at this point in the history
…in page while maintaining gaialogin compatibility
  • Loading branch information
aaronthebaron committed Jun 6, 2024
1 parent 07a7eb0 commit a157c1e
Show file tree
Hide file tree
Showing 3 changed files with 161 additions and 11 deletions.
93 changes: 93 additions & 0 deletions pkg/provider/googleapps/example/form-password-challengeid-3.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
<!doctype html>
<html lang="en-US" dir="ltr">
<head>
<base href="https://accounts.google.com/v3/signin/">
<link ref="preconnect" href="//www.gstatic.com">
<meta name="referrer" content="origin">
<link rel="canonical" href="https://accounts.googele.com/v3/signin/challenge/pwd">
<meta name="chrome" content="nointentdetection">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="">
<title>Sign in - Google Accounts</title>
</head>
<body>
<div class="BDEI9 LZgQXe">
<div class="Ha17qf" data-auto-init="Card">
<div class="Or16q">
<div data-view-id="b5STy">
<div class="gEc4r">
<img src="//ssl.gstatic.com/images/branding/googlelogo/2x/googlelogo_color_74x24dp.png" class="TrZEUc" alt="Google" width="74" height="24">
</div>
<div class="EQIoSc" jsname="bN97Pc" data-use-configureable-escape-action="true">
<div jsname="paFcre">
<div class="aMfydd" jsname="tJHJj">
<h1 class="Tn0LBd" jsname="r4nke">Welcome</h1>
<p class="a2CQh" jsname="VdSJob"></p>
<div class="C7uRJc">
<a href="/v3/signin/identifier?continue=https://accounts.google.com/o/saml2/initsso?idpid%3DXXXXXX%26spid%3DYYYYYY%26forceauthn%3Dfalse%26hl%3Den%26loc%3DUS&amp;dsh=foo:bar&amp;faa=1&amp;flowEntry=ServiceLogin&amp;flowName=WebLiteSignIn&amp;followup=https://accounts.google.com/o/saml2/initsso?idpid%3DXXXXXX%26spid%3DYYYYYY%26forceauthn%3Dfalse%26hl%3Den%26loc%3DUS&amp;hl=en_US&amp;ifkv=ABCDEFGHIJKLMNOPQRSTUVWXYZ?hl%3Den" class="HDuqac EI77qf TrZEUc cd29Sd iiFyne" tabindex="0" aria-label="test-id3@example.com selected. Switch account" jsname="af8ijd">
<div class="BOs5fd">
<div jsname="bQIQze" class="wJxLsd" data-profile-identifier translate="no">test-id3@example.com</div>
</a>
</div>
</div>
</div>
<form action="/v3/signin/challenge/pwd?TL=ABCDEFGHIJKLMNOPQRSTUVWXYZ&amp;cid=2&amp;continue=https://accounts.google.com/o/saml2/initsso?idpid%3DXXXXXX%26spid%3DYYYYYY%26forceauthn%3Dfalse%26hl%3Den%26loc%3DUS&amp;dsh=foo:bar&amp;faa=1&amp;flowEntry=ServiceLogin&amp;flowName=WebLiteSignIn&amp;followup=https://accounts.google.com/o/saml2/initsso?idpid%3DXXXXXX%26spid%3DYYYYYY%26forceauthn%3Dfalse%26hl%3Den%26loc%3DUS&amp;hl=en_US&amp;ifkv=ABCDEFGHIJKLMNOPQRSTUVWXYZ?hl%3Den" method="POST" novalidate>
<div class="iEhbme" jsname="rEuO1b">
<section class="aN1Vld fegW5d rNe0id eLNT1d S7S4N" aria-hidden="true" jsname="INM6z" aria-live="assertive" aria-atomic="true">
<header class="wSQNd" jsname="tJHJj">
<h2 class="cySqRb TrZEUc">
<span class="zlrrr" aria-hidden="true" jsname="Bz112c"><svg aria-hidden="true" class="hZUije GxLRef" fill="currentColor" focusable="false" width="20px" height="20px" viewBox="0 0 24 24" xmlns="https://www.w3.org/2000/svg"><path d="M1 21h22L12 2 1 21zm12-3h-2v-2h2v2zm0-4h-2v-4h2v4z"></path></svg>
</span>
<span jsname="Ud7fr">Too many failed attempts</span>
</h2>
<div jsname="HSrbLb" aria-hidden="true"></div>
</header>
</section>
<section class="aN1Vld " jsname="dZbRZb"><div class="yOnVIb" jsname="MZArnb">
<span jsname="xdJtEf">
<input type="hidden" name="bgresponse" value="js_disabled" id="bgresponse" style="display:none">
</span>
<input type="text" name="identifier" class="hJIRO" tabindex="-1" aria-hidden="true" spellcheck="false" value="test-id3@example.com" jsname="KKx9x" autocomplete="off" id="hiddenEmail">
<div class="Fu5aXd" jsname="vZSTIf">
<div class="Flfooc">
<div class="TRuRhd YKooDc">
<div class="fjpXlc">
<label class="dXXNOd">
<input class="xyezD" jsname="Ufn6O" type="password" name="Passwd" id="password" autofocus autocapitalize="off" autocomplete="current-password" dir="ltr"/>
<div class="nWPx2e">
<div class="YhhY8"></div>
<div class="CCQ94b">Enter your password</div>
<div class="tNASEf"></div>
</div>
</label>
</div>
</div>
</div>
<div class="F3wxlc" jsname="h9d3hd"></div>
<div class="NHVGlc" jsname="JIbuQc"></div>
</div>
<input type="hidden" name="TrustDevice" value="true">
<input type="hidden" name="historicalPassword" value="false">
</div>
</section>
<input type="hidden" name="" value="test-id3@example.com" jsname="m2Owvb" id="identifierId">
</div>
<div class="i2knIc" jsname="DH6Rkf">
<div class="wg0fFb" jsname="DhK0U">
<div class="RhTxBf" jsname="k77Iif">
<button name="action" class="JnOM6e TrZEUc rDisVe" value="1" jsname="Njthtb" id="passwordNext">Next</button>
</div>
<div class="tmMcIf" jsname="QkNstf">
<button name="action" class="JnOM6e TrZEUc KXbQ4b" value="3" jsname="gVmDzc">Try another way</button>
</div>
</div>
</div>
<input type="hidden" name="at" value="ALt4Ve3-7VDKsElQQfRT91gYiMG8:1717681198454">
</form>
</div>
</div>
</div>
</div>
</div>
</body>
</html>
43 changes: 32 additions & 11 deletions pkg/provider/googleapps/googleapps.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,14 @@ func (kc *Client) Authenticate(loginDetails *creds.LoginDetails) (string, error)
// Post email address w/o password, then Get the password-input page
passwordURL, passwordForm, err := kc.loadLoginPage(authURL+"?hl=en&loc=US", loginDetails.URL+"&hl=en&loc=US", authForm)
if err != nil {
return "", errors.Wrap(err, "error loading login page")
//if failed, try with "identifier"
authForm.Set("Email", "") // Clear previous key
authForm.Set("identifier", loginDetails.Username)
passwordURL, passwordForm, err = kc.loadLoginPage(authURL+"?hl=en&loc=US", loginDetails.URL+"&hl=en&loc=US", authForm)

if err != nil {
return "", errors.Wrap(err, "error loading login page")
}
}

logger.Debugf("loginURL: %s", passwordURL)
Expand Down Expand Up @@ -311,11 +318,13 @@ func (kc *Client) loadChallengePage(submitURL string, referer string, authForm u

secondFactorHeader := "This extra step shows it’s really you trying to sign in"
secondFactorHeader2 := "This extra step shows that it’s really you trying to sign in"
secondFactorHeader3 := "2-Step Verification"
secondFactorHeaderJp := "2 段階認証プロセス"

// have we been asked for 2-Step Verification
if extractNodeText(doc, "h2", secondFactorHeader) != "" ||
extractNodeText(doc, "h2", secondFactorHeader2) != "" ||
extractNodeText(doc, "h1", secondFactorHeader3) != "" ||
extractNodeText(doc, "h1", secondFactorHeaderJp) != "" {

responseForm, secondActionURL, err := extractInputsByFormID(doc, "challenge")
Expand All @@ -326,7 +335,7 @@ func (kc *Client) loadChallengePage(submitURL string, referer string, authForm u
logger.Debugf("secondActionURL: %s", secondActionURL)

switch {
case strings.Contains(secondActionURL, "challenge/totp/"): // handle TOTP challenge
case strings.Contains(secondActionURL, "challenge/totp"): // handle TOTP challenge

var token = loginDetails.MFAToken
if token == "" {
Expand All @@ -337,7 +346,7 @@ func (kc *Client) loadChallengePage(submitURL string, referer string, authForm u
responseForm.Set("TrustDevice", "on") // Don't ask again on this computer

return kc.loadResponsePage(secondActionURL, submitURL, responseForm)
case strings.Contains(secondActionURL, "challenge/ipp/"): // handle SMS challenge
case strings.Contains(secondActionURL, "challenge/ipp"): // handle SMS challenge

if extractNodeText(doc, "button", "Send text message") != "" {
responseForm.Set("SendMethod", "SMS") // extractInputsByFormID does not extract the name and value from <button> tag that is the form submit
Expand Down Expand Up @@ -366,7 +375,7 @@ func (kc *Client) loadChallengePage(submitURL string, referer string, authForm u

return kc.loadResponsePage(secondActionURL, submitURL, responseForm)

case strings.Contains(secondActionURL, "challenge/sk/"): // handle u2f challenge
case strings.Contains(secondActionURL, "challenge/sk"): // handle u2f challenge
facetComponents, err := url.Parse(secondActionURL)
if err != nil {
return nil, errors.Wrap(err, "unable to parse action URL for U2F challenge")
Expand All @@ -389,7 +398,7 @@ func (kc *Client) loadChallengePage(submitURL string, referer string, authForm u
responseForm.Set("TrustDevice", "on")

return kc.loadResponsePage(secondActionURL, submitURL, responseForm)
case strings.Contains(secondActionURL, "challenge/az/"): // handle phone challenge
case strings.Contains(secondActionURL, "challenge/az"): // handle phone challenge

dataAttrs := extractDataAttributes(doc, "div[data-context]", []string{"data-context", "data-gapi-url", "data-tx-id", "data-api-key", "data-tx-lifetime"})

Expand All @@ -411,7 +420,7 @@ func (kc *Client) loadChallengePage(submitURL string, referer string, authForm u

return kc.loadResponsePage(secondActionURL, submitURL, responseForm)

case strings.Contains(secondActionURL, "challenge/dp/"): // handle device push challenge
case strings.Contains(secondActionURL, "challenge/dp"): // handle device push challenge
if extraNumber := extractDevicePushExtraNumber(doc); extraNumber != "" {
log.Println("Check your phone and tap 'Yes' on the prompt, then tap the number:")
log.Printf("\t%v\n", extraNumber)
Expand All @@ -427,7 +436,7 @@ func (kc *Client) loadChallengePage(submitURL string, referer string, authForm u
responseForm.Set("TrustDevice", "on") // Don't ask again on this computer
return kc.loadResponsePage(secondActionURL, submitURL, responseForm)

case strings.Contains(secondActionURL, "challenge/skotp/"): // handle one-time HOTP challenge
case strings.Contains(secondActionURL, "challenge/skotp"): // handle one-time HOTP challenge
log.Println("Get a one-time code by visiting https://g.co/sc on another device where you can use your security key")
var token = prompter.RequestSecurityCode("000 000")

Expand Down Expand Up @@ -599,14 +608,26 @@ func mustFindErrorMsg(doc *goquery.Document) string {
}

func extractInputsByFormID(doc *goquery.Document, formID ...string) (url.Values, string, error) {
// First try to find form by specific id
for _, id := range formID {
formData, actionURL, err := extractInputsByFormQuery(doc, fmt.Sprintf("#%s", id))
if err != nil && strings.HasPrefix(err.Error(), "could not find form with query ") {
continue
if err == nil && actionURL != "" {
return formData, actionURL, nil
}
return formData, actionURL, err
}
return url.Values{}, "", errors.New("could not find any forms matching the provided IDs")

// If no form found by id or actionURL in the previous forms, search for any form
formData, actionURL, err := extractInputsByFormQuery(doc, "")
if err != nil && actionURL != "" {
return formData, actionURL, errors.New("could not find any forms with actions")
}

if len(formData) == 0 {
return nil, "", errors.New("could not find any forms")
}

// Fallback in case no forms with actionURL were found
return formData, actionURL, err
}

func extractInputsByFormQuery(doc *goquery.Document, formQuery string) (url.Values, string, error) {
Expand Down
36 changes: 36 additions & 0 deletions pkg/provider/googleapps/googleapps_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,16 @@ func TestContentContainsMessage2(t *testing.T) {
require.Equal(t, "This extra step shows that it’s really you trying to sign in", txt)
}

func TestContentContainsMessage3(t *testing.T) {
html := `<html><body><h1>2-Step Verification</h1></body></html>`

doc, err := goquery.NewDocumentFromReader(strings.NewReader(html))
require.Nil(t, err)

txt := extractNodeText(doc, "h1", "2-Step Verification")
require.Equal(t, "2-Step Verification", txt)
}

func TestPasswordFormChallengeId1(t *testing.T) {
data, err := os.ReadFile("example/form-password-challengeid-1.html")
require.Nil(t, err)
Expand Down Expand Up @@ -133,6 +143,32 @@ func TestPasswordFormChallengeId2(t *testing.T) {
require.Empty(t, passwordForm.Get("Passwd"))
}

func TestPasswordFormChallengeId3(t *testing.T) {
data, err := os.ReadFile("example/form-password-challengeid-3.html")
require.Nil(t, err)

ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write(data)
}))
defer ts.Close()

opts := &provider.HTTPClientOptions{IsWithRetries: false}
kc := Client{client: &provider.HTTPClient{Client: http.Client{}, Options: opts}}
loginDetails := &creds.LoginDetails{URL: ts.URL, Username: "test-id3@example.com", Password: "test123"}

authForm := url.Values{}
authForm.Set("bgresponse", "js_enabled")
authForm.Set("identifier", loginDetails.Username)

passwordURL, passwordForm, err := kc.loadLoginPage(ts.URL, loginDetails.URL+"&hl=en&loc=US", authForm)
require.Nil(t, err)
require.NotEmpty(t, passwordURL)
// check pre-filled email
require.NotEmpty(t, passwordForm.Get("identifier"))
// check password form
require.Empty(t, passwordForm.Get("Passwd"))
}

func TestChallengePage(t *testing.T) {

data, err := os.ReadFile("example/challenge-totp.html")
Expand Down

0 comments on commit a157c1e

Please sign in to comment.