Skip to content

Commit

Permalink
cookieless implementation
Browse files Browse the repository at this point in the history
- Cookieless support
- code cleanup
- duplicate method removal
  • Loading branch information
jkhosravian-ping authored Jun 19, 2024
2 parents 2a5dfb0 + fe7e234 commit 7a9d633
Show file tree
Hide file tree
Showing 5 changed files with 83 additions and 28 deletions.
2 changes: 2 additions & 0 deletions demo-server/templates/index-template.handlebars
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,8 @@
// flowType: PfAuthnWidget.FLOW_TYPE_AUTHZ, // Optional parameter
// client_id: '', // redirectless requires this configuration
// response_type: 'token', // redirectless requires this configuration
// cookieless: true, // Optional parameter to enable cookieless mode
// stateHeaderName: 'X-Pf-Authn-Api-State', // Optional parameter to set the state header name
onAuthorizationSuccess: function (response) {
showResponse(JSON.stringify(response));
},
Expand Down
29 changes: 26 additions & 3 deletions docs/redirectless.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ Single-page web applications can also use redirectless mode if administrators co
## Usage
To use the redirectless flow:
- Create an instance of the widget by providing PingFederate's base URL and the necessary options.
- Create a `configuration` object to provide the necessary redirectless settings.
- Create a `configuration` object to provide the necessary redirectless settings.
- Call `initRedirectless` and pass the `configuration` object as an argument.

Here's an example:
Here's an example:
```javascript
var authnWidget = new PfAuthnWidget("https://localhost", { divId: 'authnwidget' });
var config = {
Expand All @@ -40,6 +40,29 @@ Create a configuration object that contains the `onAuthorizationRequest` and `on
### OAuth 2.0 Device Authorization Grant
Create a configuration object containing the `onAuthorizationSuccess` function and a `flowType` attribute set to `PfAuthnWidget.FLOW_TYPE_USER_AUTHZ`. This configuration initializes the Authentication API Widget to interact with PingFederate's user authorization endpoint. Optionally, the `user_code` attribute can be provided. If provided, it is passed to the user authorization endpoint as a query parameter, which will trigger a state where the user must confirm the code (rather than having to enter it). An example is present [here](#oauth-20-device-authorization).

### Cookieless
The cookieless configuration allows the widget to operate without using HTTP cookies. The PingFederate OAuth 2.0 client must be configured appropriatly in order for this mode to work correctly.

By enabling this mode, the widget will handle the state management required by the cookieless functionality. The `cookieless` and `stateHeaderName` are attributes controlling this mode.
- `cookieless` attribute is boolean, defaulting to `false`
- `stateHeaderName` attribute specifices the header name to send the state back to PingFederate. If not specified the default `X-Pf-Authn-Api-State` value will be used.

An example of the cookieless configuration can be found below:
```javascript
var config = {
client_id: 'test',
response_type: 'token',
cookieless: true,
stateHeaderName: 'X-Pf-Authn-Api-State',
onAuthorizationSuccess: function (response) {
console.log(response);
},
onAuthorizationFailed: function (response) {
console.log(response);
}
};
```

### Callback function descriptions
#### `onAuthorizationRequest` function
This callback function is called during the authorization request. It has no arguments and it's expected to return a JavaScript `Promise`, which completes the authorization request call to PingFederate.
Expand All @@ -64,7 +87,7 @@ The `options` attribute `credentials: 'include'` is required to ensure the brows
This callback function returns the result of the transaction to the webpage containing the Authentication API widget. The protocol response is passed to this function as the first argument when the Authentication API widget calls it.
[PingAccess redirectless support](/docs/pingaccessRedirectless.md).

Here is an example:
Here is an example:
```js
var config = {
onAuthorizationSuccess: function (response) {
Expand Down
14 changes: 7 additions & 7 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,11 @@ export default class AuthnWidget {
redirectlessConfigValidator(configuration);
this.addPostRenderCallback('COMPLETED', (state) => completeStateCallback(state, configuration));
this.addPostRenderCallback('FAILED', (state) => failedStateCallback(state, configuration));
const isCookieless = Boolean(configuration.cookieless);
this.store.setCookieless(isCookieless);
if (isCookieless) {
this.store.setStateHeader(configuration.stateHeaderName || 'X-PF-Authn-API-State');
}
this.store
.dispatch('INIT_REDIRECTLESS', null, configuration)
.catch((err) => this.generalErrorRenderer(err.message));
Expand Down Expand Up @@ -1072,11 +1077,6 @@ export default class AuthnWidget {
}
}

registerMfaChangeDeviceEventHandler() {
document.getElementById('changeDevice')
.addEventListener('click', this.handleMfaDeviceChange);
}

registerCASChangeMethodEventHandler() {
document.getElementById('useAlternateMethod')
.addEventListener('click', this.handleCASUseAlternateMethod);
Expand Down Expand Up @@ -1373,6 +1373,7 @@ export default class AuthnWidget {
widgetDiv.innerHTML = template(params);
}
}

handleIdVerificationInProgress() {
setTimeout(() => {
this.store.dispatch('POST_FLOW', 'poll', '{}');
Expand Down Expand Up @@ -1402,8 +1403,7 @@ export default class AuthnWidget {
this.store
.dispatch('GET_FLOW')
.catch(() => this.generalErrorRenderer(AuthnWidget.COMMUNICATION_ERROR_MSG));
}
else {
} else {
this.pollCheckGetHandler = setTimeout(() => {
this.pollCheckGet(currentVerificationCode, timeout);
}, timeout);
Expand Down
36 changes: 27 additions & 9 deletions src/store.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { initRedirectless } from './utils/redirectless';
import FetchUtil from './utils/fetchUtil';
import fetchUtil from "./utils/fetchUtil";

export default class Store {
constructor(flowId, baseUrl, checkRecaptcha, options) {
Expand All @@ -14,6 +13,16 @@ export default class Store {
this.checkRecaptcha = checkRecaptcha;
this.pendingState = {};
this.registrationflow = false;
this.cookieless = false;
this.stateHeader = '';
}

setCookieless(flag) {
this.cookieless = flag;
}

setStateHeader(stateHeader) {
this.stateHeader = stateHeader;
}

getStore() {
Expand All @@ -25,7 +34,7 @@ export default class Store {
}

async getState() {
let result = await this.fetchUtil.getFlow(this.flowId);
let result = await this.fetchUtil.getFlow(this.flowId, this.buildHeaders());
return await result.json();
}

Expand Down Expand Up @@ -80,7 +89,7 @@ export default class Store {
let json;
let timeout;
if (document.querySelector("#spinnerId")) {
timeout = setTimeout(function () {
timeout = setTimeout(function() {
document.querySelector('#spinnerId').style.display = 'block';
if (document.querySelector("#AuthnWidgetForm")) {
document.querySelector("#AuthnWidgetForm").style.display = 'none';
Expand All @@ -92,14 +101,14 @@ export default class Store {
}
switch (method) {
case 'GET_FLOW':
result = await this.fetchUtil.getFlow(this.flowId);
result = await this.fetchUtil.getFlow(this.flowId, this.buildHeaders());
break;
case 'INIT_REDIRECTLESS':
result = await initRedirectless(this.baseUrl, payload);
break;
case 'POST_FLOW':
default:
result = await this.fetchUtil.postFlow(this.flowId, actionid, payload);
result = await this.fetchUtil.postFlow(this.flowId, actionid, payload, this.buildHeaders());
break;
}
json = await result.json();
Expand Down Expand Up @@ -155,13 +164,15 @@ export default class Store {
} else {
if (json.code === 'RESOURCE_NOT_FOUND') {
this.state = {};
}
else {
} else {
let errors = this.getErrorDetails(json);
delete combinedData.failedValidators;
delete combinedData.satisfiedValidators;
delete combinedData.userMessages;
combinedData = { ...errors, ...this.state };
if (json._pf_authn_api_state) {
combinedData = { ...combinedData, _pf_authn_api_state: json._pf_authn_api_state };
}
}
}
let daysToExpireMsg;
Expand Down Expand Up @@ -236,7 +247,6 @@ export default class Store {
return errors;
}


notifyListeners() {
console.log('notifying # of listeners: ' + this.listeners.length);
this.listeners.forEach(observer => observer(this.prevState, this.state));
Expand All @@ -248,7 +258,15 @@ export default class Store {
}

async poll(actionId = 'poll', body = '{}') {
let result = await this.fetchUtil.postFlow(this.flowId, actionId, body);
let result = await this.fetchUtil.postFlow(this.flowId, actionId, body, this.buildHeaders());
return await result.json();
}

buildHeaders() {
let headers = new Map();
if (this.cookieless) {
headers.set(this.stateHeader, `${this.state._pf_authn_api_state}`);
}
return headers;
}
}
30 changes: 21 additions & 9 deletions src/utils/fetchUtil.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
export default class fetchUtil {
constructor(baseUrl, useActionParam=false) {
constructor(baseUrl, useActionParam = false) {
this.baseUrl = baseUrl;
this.useActionParam = useActionParam;
this.cookieless = false;
}

doRequest(method, flowId, actionId, body) {
configCookieless(flag) {
this.cookieless = flag;
}

doRequest(method, flowId, actionId, body, httpHeaders) {
var FLOWS_ENDPOINT = '/pf-ws/authn/flows/';
var url = this.baseUrl + FLOWS_ENDPOINT + flowId;
var headers = {
Expand All @@ -15,26 +20,33 @@ export default class fetchUtil {
var contentType = 'application/json';
if (this.useActionParam) {
url = url + '?action=' + actionId;
}
else {
} else {
contentType = 'application/vnd.pingidentity.' + actionId + '+json';
}
headers['Content-Type'] = contentType;
}
// add more headers
httpHeaders.forEach((value, key) => {
headers[key] = value;
});
//options
var options = {
headers: headers,
method: method,
body: body,
credentials: 'include'
}
// include credentials
if (!this.cookieless) {
options.credentials = 'include'
}
return fetch(url, options);
}

getFlow(flowId) {
return this.doRequest('GET', flowId);
getFlow(flowId, headers = new Map()) {
return this.doRequest('GET', flowId, null, null, headers);
}

postFlow(flowId, actionId, body) {
return this.doRequest('POST', flowId, actionId, body);
postFlow(flowId, actionId, body, headers = new Map()) {
return this.doRequest('POST', flowId, actionId, body, headers);
}
}

0 comments on commit 7a9d633

Please sign in to comment.