Chromebook Single Sign-On

If you use a (i.e. Netscaler) 3rd party identity provider for your Google Cloud to sign into your Chromebooks you might have noticed a second password prompt. This happens depending on your login flow (2FA) when the Chromebook can’t extract the login information automatically to cache it for offline authentication. Google provides an API with which we can solve this for Netscaler.

Google API

Google offers 3rd parties an API which can be tied into the login process in order to communicate the credentials to Chromebook and solve this problem.

The API offers three different functions which need to be included in the login process

FunctionUse
initialize()Initializes the API and provides information if a supported Chrome device is being used or not. Has to be executed at the beginning of the login flow.
add()Sends the credentials (username+password) to Chrome to “make note of it” (not final storage yet). Has to be executed when sending the credentials to the IDP for validation.
complete()Tells Chrome that the “noted” credentials were correct. Has to be executed after successful validation of the credentials by the IDP.

Netscaler Integration

I’ve integrated the API-functions by using CustomCredentialHandlers and a custom nFactor flow. CustomCredentialHandlers are a lesser known possibility to extend Netscalers already powerful authentication framework even further.

Basically I’m using two CredentialHandler. The first one for initializes the API and sends the credentials on the existing login-form. The second one is an additional “transparent” factor which completes the SSO and auto-submits the factor upon completion. A true “noschema” factor was not possible as it doesn’t allow us to execute any JavaScript.

The following schema illustrates the high level components of the flow:

Login Schema

The following schema displays the user+password prompt and calls the first part of the API integration. The session ID is handed over to the CustomCredentialHandler to be used as a token to track the API requests.

/nsconfig/loginschema/SingleAuth_GSSO.xml:

<?xml version="1.0" encoding="UTF-8"?>
<AuthenticateResponse xmlns="http://citrix.com/authentication/response/1">
<Status>success</Status>
<Result>more-info</Result>
<StateContext/>
<AuthenticationRequirements>
<PostBack>/nf/auth/doAuthentication.do</PostBack>
<CancelPostBack>/nf/auth/doLogoff.do</CancelPostBack>
<CancelButtonText>Cancel</CancelButtonText>
<Requirements>
<Requirement><Credential><ID>login</ID><SaveID>ExplicitForms-Username</SaveID><Type>username</Type></Credential><Label><Text>singleauth_user_name</Text><Type>nsg-login-label</Type></Label><Input><Text><Secret>false</Secret><ReadOnly>false</ReadOnly><InitialValue/><Constraint>.+</Constraint></Text></Input></Requirement>
<Requirement><Credential><ID>passwd</ID><SaveID>ExplicitForms-Password</SaveID><Type>password</Type></Credential><Label><Text>singleauth_password</Text><Type>nsg-login-label</Type></Label><Input><Text><Secret>true</Secret><ReadOnly>false</ReadOnly><InitialValue/><Constraint>.+</Constraint></Text></Input></Requirement>
<Requirement><Credential><ID>gsso-step1</ID><Type>gsso-step1</Type><Input><Text><Hidden>true</Hidden><InitialValue>${AAA.USER.SESSIONID}</InitialValue></Text></Input></Credential></Requirement>
<Requirement><Credential><ID>loginBtn</ID><Type>none</Type></Credential><Label><Type>none</Type></Label><Input><Button>singleauth_log_on</Button></Input></Requirement>
</Requirements>
</AuthenticationRequirements>
</AuthenticateResponse>

The following schema finishes the API integration at the end of a successful login flow. The session ID is handed over to the CustomCredentialHandler to be used as a token to track the API requests.

/nsconfig/loginschema/SingleAuth_GSSO.xml:

<?xml version="1.0" encoding="UTF-8"?>
<AuthenticateResponse xmlns="http://citrix.com/authentication/response/1">
<Status>success</Status>
<Result>more-info</Result>
<StateContext/>
<AuthenticationRequirements>
<PostBack>/nf/auth/doAuthentication.do</PostBack>
<CancelPostBack>/nf/auth/doLogoff.do</CancelPostBack>
<CancelButtonText>Cancel</CancelButtonText>
<Requirements>
<Requirement><Credential><Type>none</Type></Credential><Label><Text>gsso_continue</Text><Type>nsg_confirmation</Type></Label><Input/></Requirement>
<Requirement><Credential><ID>gsso-step2</ID><Type>gsso-step2</Type><Input><Text><Hidden>true</Hidden><InitialValue>${AAA.USER.SESSIONID}</InitialValue></Text></Input></Credential></Requirement>
<Requirement><Credential><ID>loginBtn</ID><Type>none</Type></Credential><Label><Type>none</Type></Label><Input><Button>singleauth_log_on</Button></Input></Requirement>
</Requirements>
</AuthenticationRequirements>
</AuthenticateResponse>

In addition the following language files in the theme should be edited to replace the display-message in the final factor that is displayed if the auto-submit didn’t work.

/var/netscaler/logon/themes/[themename]/strings.en.json:

{"gsso_continue":"Google SSO is being performed, if you are not redirected automatically please click on \"Log on\""}

Custom Credential Handler

In the script.js corresponding to the theme the two credential handlers are defined.

The first handler calls initialize() and attaches an additional event to the click() handler of the submit-button. This will call the add() function upon submitting of the form. In addition it reads the session ID provided by the schema above and uses it as the token for the API request.

/var/netscaler/logon/themes/[themename]/script.js:

/*
 * Credential Handler: gsso-step1
 * Performs first part of Google API integration (Initialization plus adding the credentials)
 * Reference: https://www.chromium.org/administrators/advanced-integration-for-saml-sso-on-chrome-devices/
 * Usage: <Requirement><Credential><ID>gsso-step1</ID><Type>gsso-step1</Type><Input><Text><Hidden>true</Hidden><InitialValue>${AAA.USER.SESSIONID}</InitialValue></Text></Input></Credential></Requirement>
 */
CTXS.ExtensionAPI.addCustomCredentialHandler({
    // The name of the credential, must match the type returned by the server
    getCredentialTypeName: function () { return "gsso-step1"; },
    // Generate HTML for the custom credential
    getCredentialTypeMarkup: function (requirements) {
	// Get session ID
	var sessionId = requirements.input.text.initialValue;

	// Load Google API
	var gapi = document.createElement("script");
	gapi.type = "text/javascript";
	gapi.src = "https://ssl.gstatic.com/accounts/chrome/users-1.0.js";
	document.head.appendChild(gapi);
	
	// Rest we perform as onload-callback because script takes time to load
	apiOnLoadCallback = function() {
		// Initialize Google API
		google.principal.initialize(function() {}); // With an empty callback - we don't need the info provided - and it seems to be empty anyway
		
		// Add Google API credential passing on Login
		$('#loginBtn').click(function(){		
			// Submit Credentials to Google API
			google.principal.add({
				token: sessionId,
				user: $('#login')[0].value,
				passwordBytes: $('#passwd')[0].value,
				keyType: 'KEY_TYPE_PASSWORD_PLAIN'
			},
			function() {
			});
		 })
	}
	gapi.onload = apiOnLoadCallback;
	
	// Return an empty DIV, the factor is hidden anyway
	var div = $("<div></div>");
        return div;
    }
});

The second handler also reads the supplied session ID. It then confirms the authentication for Google. It also auto-submits the form to continue automatically. To prevent getting stuck upon API load failure we’re also attaching a on-error eventhandler to auto-submit in case the API fails to load.

/var/netscaler/logon/themes/[themename]/script.js:

/*
 * Credential Handler: gsso-step2
 * Performs first part of Google API integration (Initialization plus adding the credentials)
 * Reference: https://www.chromium.org/administrators/advanced-integration-for-saml-sso-on-chrome-devices/
 * Usage: <Requirement><Credential><ID>gsso-step2</ID><Type>gsso-step2</Type><Input><Text><Hidden>true</Hidden><InitialValue>${AAA.USER.SESSIONID}</InitialValue></Text></Input></Credential></Requirement>
 */
CTXS.ExtensionAPI.addCustomCredentialHandler({
    // The name of the credential, must match the type returned by the server
    getCredentialTypeName: function () { return "gsso-step2"; },
	// Generate HTML for the custom credential
    getCredentialTypeMarkup: function (requirements) {
	// Get session ID
	var sessionId = requirements.input.text.initialValue;

	// Load Google API
	var gapi = document.createElement("script");
	gapi.type = "text/javascript";
	gapi.src = "https://ssl.gstatic.com/accounts/chrome/users-1.0.js";
	document.head.appendChild(gapi);
	
	// Rest we perform as callback because script takes time to load
	// On successful load...
	apiOnLoadCallback = function() {
		// Tell Google API Authentication succeeded
		google.principal.complete({
			token: sessionId
		},
		function() {
		});
		
		// Auto complete the form
		// return;
		$('#loginBtn').click();
	}
	gapi.onload = apiOnLoadCallback;
	
	// On failed load...
	apiOnErrorCallback = function() {
		// Just auto complete the form
		$('#loginBtn').click();
	}
	gapi.onerror = apiOnErrorCallback;
	
	// Return an empty DIV, the factor is hidden anyway
	var div = $("<div></div>");
        return div;
    }
});

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.