Skip to content

add support for first adapter #1427

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
176 changes: 176 additions & 0 deletions pkg/provider/pingfed/example/first-adapter.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@

<!DOCTYPE html>


<!-- template name: identifier.first.template.html -->


<html lang="en" dir="ltr">
<head>
<title>Sign On</title>
<base href="https://id.example.com/">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<meta name="viewport"
content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no"/>
<meta http-equiv="x-ua-compatible" content="IE=edge"/>
<meta http-equiv="Content-Security-Policy" content="default-src 'self' 'nonce-SBa4fPYaLrHhvUst';" />
<link rel="stylesheet" type="text/css" href="assets/css/main.css"/>
<script src='assets/scripts/pf-general.js'></script>
</head>

<body>

<div class="ping-container ping-signin login-template">

<!--
if there is a logo present in the 'company-logo' container,
then 'has-logo' class should be added to 'ping-header' container.
-->
<div class="ping-header">
</div>
<!-- .ping-header -->
<div class="ping-body-container">
<br/>
<p>Login using your email address</p>
<form method="POST" action="/as/m8pSR6nSxN/resume/as/authorization.ping" autocomplete="off">

<div id="identifierInputLabel" class="ping-input-label">
Email
</div>

<div id="identifierField" class="ping-input-container">
<input id="identifierInput" type="text" size="36" name="subject" value=""
autocorrect="off" autocapitalize="off"/>
<div class="place-bottom type-alert tooltip-text" id="username-text">
<div class="icon">!</div>
Please fill out this field.
</div>
</div>

<div id="postButton" class="ping-buttons">
<a id="signOnButton" class="ping-button normal allow"
title="Next">
Next
</a>
</div><!-- .ping-buttons -->


<input type="hidden" name="clear.previous.selected.subject" id="clear.previous.selected.subject" value=""/>
<input type="hidden" name="cancel.identifier.selection" value="false"/>
</form>
</div><!-- .ping-body-container -->

<div class="ping-footer-container">
<div class="ping-footer">
<div class="ping-credits"></div>
<div class="ping-copyright">© Copyright 2023 Ping Identity. All rights reserved.</div>
</div>
<!-- .ping-footer -->
</div>
<!-- .ping-footer-container -->

</div><!-- .ping-container -->

<script type="text/javascript" nonce="SBa4fPYaLrHhvUst">

window.onload = function() {
toggleMobile(false);

}

registerEventHandlerForClass('identifier-first__account-select', 'click', function(e) {
selectIdentifier(e.target.id);
});
registerEventHandlerForClass('identifier-first__account-name', 'click', function(e) {
selectIdentifier(e.target.parentNode.id);
});
registerEventHandlerForClass('identifier-first__remove-account', 'click', function(e) {
removeIdentifier(e.target.parentNode.id);
});

registerEventHandler('showIdentifierInputLink', 'click', showIdentifierInputBox);
registerEventHandler('signOnButton', 'click', postOk);
registerEventHandler('cancelLink', 'click', postCancel);
handleReturnPress('identifierInput', postOk);

var formSubmitted = false;

function removeIdentifier(existingIdentifier) {
document.forms[0]['clear.previous.selected.subject'].value = existingIdentifier;
document.forms[0]['subject'].value = '';
submitForm();
}

function showIdentifierInputBox() {
// update the title text
document.title = 'Sign On';
// update company-logo-div-text
document.getElementById("company-logo-div-text").textContent = 'Sign On';
// hide the identifier selection list
document.getElementById("existingAccountsSelectionList").style.display = "none";
// show the identifier input box
document.getElementById("identifierInputLabel").style.display = "inline";
document.getElementById("identifierInput").style.display = "inline";
document.getElementById("postButton").style.display = "inline-block";
document.getElementById("identifierField").style.display="inline-block";

setFocus('identifierInput');
}

function showExistingAccounts() {
document.title = 'Choose an Account';
document.getElementById("company-logo-div-text").textContent = 'Choose an Account';
document.getElementById("existingAccountsSelectionList").style.display = "inline";

document.getElementById("identifierInputLabel").style.display = "none";
document.getElementById("identifierInput").style.display = "none";
document.getElementById("postButton").style.display = "none";
document.getElementById("identifierField").style.display="none";
}

function selectIdentifier(identifier) {
document.forms[0]['subject'].value = identifier;
postOk();
}

function postOk() {
if (formSubmitted)
return true;

formSubmitted = true;
var hasError = false;

// remove error tips
if (document.forms[0]['subject'].value !== '') {
document.getElementById('username-text').className = 'place-bottom type-alert tooltip-text';
}
// Add back
if (document.forms[0]['subject'].value === '') {
document.getElementById('username-text').className += ' show';
hasError = true;
}
else {
submitForm()
}
if (hasError) {
formSubmitted = false;
}
}

function postCancel()
{
document.forms[0]['cancel.identifier.selection'].value = 'true';
submitForm()
}

function submitForm()
{
document.forms[0].submit();
}


</script>

</body>
</html>

27 changes: 27 additions & 0 deletions pkg/provider/pingfed/pingfed.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,9 @@ func (ac *Client) follow(ctx context.Context, req *http.Request) (string, error)
logger.WithField("type", "saml-response").WithField("saml-response", string(decodedSamlResponse)).Debug("doc detect")
return samlResponse, nil
}
} else if docIsFirst(doc) {
logger.WithField("type", "first form adapter").Debug("doc detect")
handler = ac.handleFirst
} else if docIsFormSamlRequest(doc) {
logger.WithField("type", "saml-request").Debug("doc detect")
handler = ac.handleFormRedirect
Expand Down Expand Up @@ -157,6 +160,26 @@ func (ac *Client) handleLogin(ctx context.Context, doc *goquery.Document, _ *url
return ctx, req, err
}

func (ac *Client) handleFirst(ctx context.Context, doc *goquery.Document, _ *url.URL) (context.Context, *http.Request, error) {
loginDetails, ok := ctx.Value(ctxKey("login")).(*creds.LoginDetails)
if !ok {
return ctx, nil, fmt.Errorf("no context value for 'login'")
}

form, err := page.NewFormFromDocument(doc, "form")
if err != nil {
return ctx, nil, errors.Wrap(err, "error extracting login form")
}

form.Values.Set("subject", loginDetails.Username)
form.Values.Set("clear.previous.selected.subject", "")
form.Values.Set("cancel.identifier.selection", "false")
form.URL = makeAbsoluteURL(form.URL, loginDetails.URL)

req, err := form.BuildRequest()
return ctx, req, err
}

func (ac *Client) handleCheckWebAuthn(ctx context.Context, doc *goquery.Document, requestURL *url.URL) (context.Context, *http.Request, error) {
form, err := page.NewFormFromDocument(doc, "form")
if err != nil {
Expand Down Expand Up @@ -302,6 +325,10 @@ func docIsLogin(doc *goquery.Document) bool {
return doc.Has("input[name=\"pf.pass\"]").Size() == 1 || doc.Has("input[name=\"PASSWORD\"]").Size() == 1
}

func docIsFirst(doc *goquery.Document) bool {
return doc.Has("input[name=\"subject\"]").Size() == 1
}

func docIsOTP(doc *goquery.Document) bool {
return doc.Has("form#otp-form").Size() == 1
}
Expand Down
35 changes: 35 additions & 0 deletions pkg/provider/pingfed/pingfed_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,30 +39,42 @@ var docTests = []struct {
{docIsLogin, "example/swipe-number.html", false},
{docIsLogin, "example/form-redirect.html", false},
{docIsLogin, "example/webauthn.html", false},
{docIsLogin, "example/first-adapter.html", false},
{docIsOTP, "example/login.html", false},
{docIsOTP, "example/otp.html", true},
{docIsOTP, "example/swipe.html", false},
{docIsOTP, "example/swipe-number.html", false},
{docIsOTP, "example/form-redirect.html", false},
{docIsOTP, "example/webauthn.html", false},
{docIsOTP, "example/first-adapter.html", false},
{docIsSwipe, "example/login.html", false},
{docIsSwipe, "example/otp.html", false},
{docIsSwipe, "example/swipe.html", true},
{docIsSwipe, "example/swipe-number.html", true},
{docIsSwipe, "example/form-redirect.html", false},
{docIsSwipe, "example/webauthn.html", false},
{docIsSwipe, "example/first-adapter.html", false},
{docIsFormRedirect, "example/login.html", false},
{docIsFormRedirect, "example/otp.html", false},
{docIsFormRedirect, "example/swipe.html", false},
{docIsFormRedirect, "example/swipe-number.html", false},
{docIsFormRedirect, "example/form-redirect.html", true},
{docIsFormRedirect, "example/webauthn.html", false},
{docIsFormRedirect, "example/first-adapter.html", false},
{docIsWebAuthn, "example/login.html", false},
{docIsWebAuthn, "example/otp.html", false},
{docIsWebAuthn, "example/swipe.html", false},
{docIsWebAuthn, "example/swipe-number.html", false},
{docIsWebAuthn, "example/form-redirect.html", false},
{docIsWebAuthn, "example/webauthn.html", true},
{docIsWebAuthn, "example/first-adapter.html", false},
{docIsFirst, "example/first-adapter.html", true},
{docIsFirst, "example/login.html", false},
{docIsFirst, "example/otp.html", false},
{docIsFirst, "example/swipe.html", false},
{docIsFirst, "example/swipe-number.html", false},
{docIsFirst, "example/form-redirect.html", false},
{docIsFirst, "example/webauthn.html", false},
}

func TestDocTypes(t *testing.T) {
Expand Down Expand Up @@ -232,3 +244,26 @@ func TestHandleWebAuthn(t *testing.T) {
s := string(b[:])
require.Contains(t, s, "isWebAuthnSupportedByBrowser=false")
}

func TestHandleFirst(t *testing.T) {
ac := Client{}
loginDetails := creds.LoginDetails{
Username: "user@domain",
}
ctx := context.WithValue(context.Background(), ctxKey("login"), &loginDetails)

data, err := os.ReadFile("example/first-adapter.html")
require.Nil(t, err)

doc, err := goquery.NewDocumentFromReader(bytes.NewReader(data))
require.Nil(t, err)

_, req, err := ac.handleFirst(ctx, doc, &url.URL{})
require.Nil(t, err)

b, err := io.ReadAll(req.Body)
require.Nil(t, err)

s := string(b[:])
require.Contains(t, s, "subject=user%40domain")
}