Skip to content

Commit 7a9d633

Browse files
cookieless implementation
- Cookieless support - code cleanup - duplicate method removal
2 parents 2a5dfb0 + fe7e234 commit 7a9d633

File tree

5 files changed

+83
-28
lines changed

5 files changed

+83
-28
lines changed

demo-server/templates/index-template.handlebars

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,8 @@
8888
// flowType: PfAuthnWidget.FLOW_TYPE_AUTHZ, // Optional parameter
8989
// client_id: '', // redirectless requires this configuration
9090
// response_type: 'token', // redirectless requires this configuration
91+
// cookieless: true, // Optional parameter to enable cookieless mode
92+
// stateHeaderName: 'X-Pf-Authn-Api-State', // Optional parameter to set the state header name
9193
onAuthorizationSuccess: function (response) {
9294
showResponse(JSON.stringify(response));
9395
},

docs/redirectless.md

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,10 @@ Single-page web applications can also use redirectless mode if administrators co
1212
## Usage
1313
To use the redirectless flow:
1414
- Create an instance of the widget by providing PingFederate's base URL and the necessary options.
15-
- Create a `configuration` object to provide the necessary redirectless settings.
15+
- Create a `configuration` object to provide the necessary redirectless settings.
1616
- Call `initRedirectless` and pass the `configuration` object as an argument.
1717

18-
Here's an example:
18+
Here's an example:
1919
```javascript
2020
var authnWidget = new PfAuthnWidget("https://localhost", { divId: 'authnwidget' });
2121
var config = {
@@ -40,6 +40,29 @@ Create a configuration object that contains the `onAuthorizationRequest` and `on
4040
### OAuth 2.0 Device Authorization Grant
4141
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).
4242

43+
### Cookieless
44+
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.
45+
46+
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.
47+
- `cookieless` attribute is boolean, defaulting to `false`
48+
- `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.
49+
50+
An example of the cookieless configuration can be found below:
51+
```javascript
52+
var config = {
53+
client_id: 'test',
54+
response_type: 'token',
55+
cookieless: true,
56+
stateHeaderName: 'X-Pf-Authn-Api-State',
57+
onAuthorizationSuccess: function (response) {
58+
console.log(response);
59+
},
60+
onAuthorizationFailed: function (response) {
61+
console.log(response);
62+
}
63+
};
64+
```
65+
4366
### Callback function descriptions
4467
#### `onAuthorizationRequest` function
4568
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.
@@ -64,7 +87,7 @@ The `options` attribute `credentials: 'include'` is required to ensure the brows
6487
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.
6588
[PingAccess redirectless support](/docs/pingaccessRedirectless.md).
6689

67-
Here is an example:
90+
Here is an example:
6891
```js
6992
var config = {
7093
onAuthorizationSuccess: function (response) {

src/index.js

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -369,6 +369,11 @@ export default class AuthnWidget {
369369
redirectlessConfigValidator(configuration);
370370
this.addPostRenderCallback('COMPLETED', (state) => completeStateCallback(state, configuration));
371371
this.addPostRenderCallback('FAILED', (state) => failedStateCallback(state, configuration));
372+
const isCookieless = Boolean(configuration.cookieless);
373+
this.store.setCookieless(isCookieless);
374+
if (isCookieless) {
375+
this.store.setStateHeader(configuration.stateHeaderName || 'X-PF-Authn-API-State');
376+
}
372377
this.store
373378
.dispatch('INIT_REDIRECTLESS', null, configuration)
374379
.catch((err) => this.generalErrorRenderer(err.message));
@@ -1072,11 +1077,6 @@ export default class AuthnWidget {
10721077
}
10731078
}
10741079

1075-
registerMfaChangeDeviceEventHandler() {
1076-
document.getElementById('changeDevice')
1077-
.addEventListener('click', this.handleMfaDeviceChange);
1078-
}
1079-
10801080
registerCASChangeMethodEventHandler() {
10811081
document.getElementById('useAlternateMethod')
10821082
.addEventListener('click', this.handleCASUseAlternateMethod);
@@ -1373,6 +1373,7 @@ export default class AuthnWidget {
13731373
widgetDiv.innerHTML = template(params);
13741374
}
13751375
}
1376+
13761377
handleIdVerificationInProgress() {
13771378
setTimeout(() => {
13781379
this.store.dispatch('POST_FLOW', 'poll', '{}');
@@ -1402,8 +1403,7 @@ export default class AuthnWidget {
14021403
this.store
14031404
.dispatch('GET_FLOW')
14041405
.catch(() => this.generalErrorRenderer(AuthnWidget.COMMUNICATION_ERROR_MSG));
1405-
}
1406-
else {
1406+
} else {
14071407
this.pollCheckGetHandler = setTimeout(() => {
14081408
this.pollCheckGet(currentVerificationCode, timeout);
14091409
}, timeout);

src/store.js

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { initRedirectless } from './utils/redirectless';
22
import FetchUtil from './utils/fetchUtil';
3-
import fetchUtil from "./utils/fetchUtil";
43

54
export default class Store {
65
constructor(flowId, baseUrl, checkRecaptcha, options) {
@@ -14,6 +13,16 @@ export default class Store {
1413
this.checkRecaptcha = checkRecaptcha;
1514
this.pendingState = {};
1615
this.registrationflow = false;
16+
this.cookieless = false;
17+
this.stateHeader = '';
18+
}
19+
20+
setCookieless(flag) {
21+
this.cookieless = flag;
22+
}
23+
24+
setStateHeader(stateHeader) {
25+
this.stateHeader = stateHeader;
1726
}
1827

1928
getStore() {
@@ -25,7 +34,7 @@ export default class Store {
2534
}
2635

2736
async getState() {
28-
let result = await this.fetchUtil.getFlow(this.flowId);
37+
let result = await this.fetchUtil.getFlow(this.flowId, this.buildHeaders());
2938
return await result.json();
3039
}
3140

@@ -80,7 +89,7 @@ export default class Store {
8089
let json;
8190
let timeout;
8291
if (document.querySelector("#spinnerId")) {
83-
timeout = setTimeout(function () {
92+
timeout = setTimeout(function() {
8493
document.querySelector('#spinnerId').style.display = 'block';
8594
if (document.querySelector("#AuthnWidgetForm")) {
8695
document.querySelector("#AuthnWidgetForm").style.display = 'none';
@@ -92,14 +101,14 @@ export default class Store {
92101
}
93102
switch (method) {
94103
case 'GET_FLOW':
95-
result = await this.fetchUtil.getFlow(this.flowId);
104+
result = await this.fetchUtil.getFlow(this.flowId, this.buildHeaders());
96105
break;
97106
case 'INIT_REDIRECTLESS':
98107
result = await initRedirectless(this.baseUrl, payload);
99108
break;
100109
case 'POST_FLOW':
101110
default:
102-
result = await this.fetchUtil.postFlow(this.flowId, actionid, payload);
111+
result = await this.fetchUtil.postFlow(this.flowId, actionid, payload, this.buildHeaders());
103112
break;
104113
}
105114
json = await result.json();
@@ -155,13 +164,15 @@ export default class Store {
155164
} else {
156165
if (json.code === 'RESOURCE_NOT_FOUND') {
157166
this.state = {};
158-
}
159-
else {
167+
} else {
160168
let errors = this.getErrorDetails(json);
161169
delete combinedData.failedValidators;
162170
delete combinedData.satisfiedValidators;
163171
delete combinedData.userMessages;
164172
combinedData = { ...errors, ...this.state };
173+
if (json._pf_authn_api_state) {
174+
combinedData = { ...combinedData, _pf_authn_api_state: json._pf_authn_api_state };
175+
}
165176
}
166177
}
167178
let daysToExpireMsg;
@@ -236,7 +247,6 @@ export default class Store {
236247
return errors;
237248
}
238249

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

250260
async poll(actionId = 'poll', body = '{}') {
251-
let result = await this.fetchUtil.postFlow(this.flowId, actionId, body);
261+
let result = await this.fetchUtil.postFlow(this.flowId, actionId, body, this.buildHeaders());
252262
return await result.json();
253263
}
264+
265+
buildHeaders() {
266+
let headers = new Map();
267+
if (this.cookieless) {
268+
headers.set(this.stateHeader, `${this.state._pf_authn_api_state}`);
269+
}
270+
return headers;
271+
}
254272
}

src/utils/fetchUtil.js

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
11
export default class fetchUtil {
2-
constructor(baseUrl, useActionParam=false) {
2+
constructor(baseUrl, useActionParam = false) {
33
this.baseUrl = baseUrl;
44
this.useActionParam = useActionParam;
5+
this.cookieless = false;
56
}
67

7-
doRequest(method, flowId, actionId, body) {
8+
configCookieless(flag) {
9+
this.cookieless = flag;
10+
}
11+
12+
doRequest(method, flowId, actionId, body, httpHeaders) {
813
var FLOWS_ENDPOINT = '/pf-ws/authn/flows/';
914
var url = this.baseUrl + FLOWS_ENDPOINT + flowId;
1015
var headers = {
@@ -15,26 +20,33 @@ export default class fetchUtil {
1520
var contentType = 'application/json';
1621
if (this.useActionParam) {
1722
url = url + '?action=' + actionId;
18-
}
19-
else {
23+
} else {
2024
contentType = 'application/vnd.pingidentity.' + actionId + '+json';
2125
}
2226
headers['Content-Type'] = contentType;
2327
}
28+
// add more headers
29+
httpHeaders.forEach((value, key) => {
30+
headers[key] = value;
31+
});
32+
//options
2433
var options = {
2534
headers: headers,
2635
method: method,
2736
body: body,
28-
credentials: 'include'
37+
}
38+
// include credentials
39+
if (!this.cookieless) {
40+
options.credentials = 'include'
2941
}
3042
return fetch(url, options);
3143
}
3244

33-
getFlow(flowId) {
34-
return this.doRequest('GET', flowId);
45+
getFlow(flowId, headers = new Map()) {
46+
return this.doRequest('GET', flowId, null, null, headers);
3547
}
3648

37-
postFlow(flowId, actionId, body) {
38-
return this.doRequest('POST', flowId, actionId, body);
49+
postFlow(flowId, actionId, body, headers = new Map()) {
50+
return this.doRequest('POST', flowId, actionId, body, headers);
3951
}
4052
}

0 commit comments

Comments
 (0)