Skip to content

Commit

Permalink
feat: add script tag nonce resolution helper for session management a…
Browse files Browse the repository at this point in the history
…nd wmrm (#584)

resolves #583
  • Loading branch information
panva authored Nov 15, 2019
1 parent 8c179c9 commit b32b8e6
Show file tree
Hide file tree
Showing 9 changed files with 114 additions and 22 deletions.
44 changes: 38 additions & 6 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -755,7 +755,7 @@ _**default value**_:
mask: '****-****',
successSource: [AsyncFunction: successSource], // see expanded details below
userCodeConfirmSource: [AsyncFunction: userCodeConfirmSource], // see expanded details below
userCodeInputSource: [AsyncFunction: userCodeInputSource]
userCodeInputSource: [AsyncFunction: userCodeInputSource] // see expanded details below
}
```
<details>
Expand Down Expand Up @@ -975,7 +975,7 @@ _**default value**_:
```js
{
enabled: false,
logoutPendingSource: [AsyncFunction: logoutPendingSource]
logoutPendingSource: [AsyncFunction: logoutPendingSource] // see expanded details below
}
```
<details>
Expand Down Expand Up @@ -1275,7 +1275,7 @@ _**default value**_:
idFactory: [Function: idFactory], // see expanded details below
initialAccessToken: false,
policies: undefined,
secretFactory: [Function: secretFactory]
secretFactory: [Function: secretFactory] // see expanded details below
}
```
<details>
Expand Down Expand Up @@ -1698,7 +1698,8 @@ _**default value**_:
```js
{
enabled: false,
keepHeaders: false
keepHeaders: false,
scriptNonce: [Function: scriptNonce] // see expanded details below
}
```
<details>
Expand All @@ -1718,6 +1719,18 @@ _**default value**_:
false
```

#### scriptNonce

When using `nonce-{random}` CSP policy use this helper function to resolve a nonce to add to the <script> tags in the `check_session_iframe` html source.


_**default value**_:
```js
scriptNonce(ctx) {
return undefined;
}
```

</details>

### features.userinfo
Expand Down Expand Up @@ -1745,9 +1758,28 @@ Enables `web_message` response mode.
_**default value**_:
```js
{
enabled: false
enabled: false,
scriptNonce: [Function: scriptNonce] // see expanded details below
}
```
<details>
<summary>(Click to expand) features.webMessageResponseMode options details</summary>
<br>


#### scriptNonce

When using `nonce-{random}` CSP policy use this helper function to resolve a nonce to add to the <script> tag in the rendered web_message response mode html source


_**default value**_:
```js
scriptNonce(ctx) {
return undefined;
}
```

</details>

### acrValues

Expand Down Expand Up @@ -2146,7 +2178,7 @@ _**default value**_:
jwt: undefined,
paseto: undefined
},
jwtAccessTokenSigningAlg: [AsyncFunction: jwtAccessTokenSigningAlg]
jwtAccessTokenSigningAlg: [AsyncFunction: jwtAccessTokenSigningAlg] // see expanded details below
}
```
<a name="formats-to-enable-jwt-access-tokens"></a><details>
Expand Down
2 changes: 1 addition & 1 deletion docs/update-configuration.js
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,7 @@ const props = [
case 'object': {
const output = inspect(value, { compact: false, sorted: true });
append(expand(output).split('\n').map((line) => {
line = line.replace(/(\[(?:Async)?Function: \w+\],)/, '$1 // see expanded details below');
line = line.replace(/(\[(?:Async)?Function: \w+\],?)/, '$1 // see expanded details below');
return line;
}).join('\n'));
break;
Expand Down
17 changes: 11 additions & 6 deletions lib/actions/check_session.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,27 +9,32 @@ const PARAM_LIST = new Set(['client_id', 'origin']);

module.exports = {
get: async function checkSessionIframe(ctx, next) {
const removeHeaders = !instance(ctx.oidc.provider).configuration('features.sessionManagement.keepHeaders');
if (removeHeaders) {
const { keepHeaders, scriptNonce } = instance(ctx.oidc.provider).configuration('features.sessionManagement');
const csp = ctx.response.get('Content-Security-Policy');
if (!keepHeaders) {
ctx.response.remove('X-Frame-Options');
const csp = ctx.response.get('Content-Security-Policy');
if (csp.includes('frame-ancestors')) {
ctx.response.set('Content-Security-Policy', csp.replace(/ ?frame-ancestors [^;]+;/, ''));
}
}

let nonce;
if (csp && csp.includes('nonce-')) {
nonce = scriptNonce(ctx);
}

ctx.type = 'html';
ctx.body = `<!DOCTYPE html>
<html>
<head lang="en">
<meta charset="UTF-8">
<title>Session Management - OP iframe</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jsSHA/2.3.1/sha256.js" integrity="sha256-NyuvLfsvfCfE+ceV6/W19H+qVp3M8c9FzAgj72CW39w=" crossorigin="anonymous"></script>
<script src="https://polyfill.io/v3/polyfill.min.js?flags=gated&features=fetch"></script>
<script ${nonce ? `nonce="${nonce}" ` : ''}src="https://cdnjs.cloudflare.com/ajax/libs/jsSHA/2.3.1/sha256.js" integrity="sha256-NyuvLfsvfCfE+ceV6/W19H+qVp3M8c9FzAgj72CW39w=" crossorigin="anonymous"></script>
<script ${nonce ? `nonce="${nonce}" ` : ''}src="https://polyfill.io/v3/polyfill.min.js?flags=gated&features=fetch"></script>
</head>
<body>
<script type="application/javascript">
<script ${nonce ? `nonce="${nonce}" ` : ' '}type="application/javascript">
(function () {
var originCheckResult;
Expand Down
26 changes: 25 additions & 1 deletion lib/helpers/defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -1213,6 +1213,17 @@ const DEFAULTS = {
* middleware or your app server, otherwise you shouldn't have the need to touch this option.
*/
keepHeaders: false,

/*
* features.sessionManagement.scriptNonce
*
* description: When using `nonce-{random}` CSP policy use this helper function to resolve a
* nonce to add to the <script> tags in the `check_session_iframe` html source.
*/
scriptNonce(ctx) { // eslint-disable-line no-unused-vars
shouldChange('features.sessionManagement.scriptNonce', 'specify the nonce attribute for the check_session_iframe html scripts');
return undefined;
},
},

/*
Expand Down Expand Up @@ -1250,7 +1261,20 @@ const DEFAULTS = {
* [koa](https://www.npmjs.com/package/koa-helmet)) it is especially advised for your interaction
* views routes if Web Message Response Mode is available on your deployment.
*/
webMessageResponseMode: { enabled: false },
webMessageResponseMode: {
enabled: false,

/*
* features.webMessageResponseMode.scriptNonce
*
* description: When using `nonce-{random}` CSP policy use this helper function to resolve a
* nonce to add to the <script> tag in the rendered web_message response mode html source
*/
scriptNonce(ctx) { // eslint-disable-line no-unused-vars
shouldChange('features.webMessageResponseMode.scriptNonce', 'specify the nonce attribute for the web_message response mode html scripts');
return undefined;
},
},
},

/*
Expand Down
10 changes: 9 additions & 1 deletion lib/response_modes/web_message.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
const jsesc = require('jsesc');

const instance = require('../helpers/weak_cache');

const statusCodes = new Set([200, 400, 500]);

module.exports = function webMessage(ctx, redirectUri, response) {
const { scriptNonce } = instance(ctx.oidc.provider).configuration('features.webMessageResponseMode');
ctx.type = 'html';

if (!statusCodes.has(ctx.status)) {
Expand All @@ -22,12 +25,17 @@ module.exports = function webMessage(ctx, redirectUri, response) {
web_message_target: ctx.oidc.params.web_message_target,
}, { json: true, isScriptContext: true });

let nonce;
if (csp && csp.includes('nonce-')) {
nonce = scriptNonce(ctx);
}

ctx.body = `<!DOCTYPE html>
<head>
<title>Web Message Response</title>
</head>
<body>
<script>
<script ${nonce ? `nonce="${nonce}" ` : ''}type="application/javascript">
(function(win, doc) {
var data = ${data};
Expand Down
27 changes: 25 additions & 2 deletions test/session_management/check_session_endpoint.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,41 @@ describe('check_session_iframe', () => {
before(function () {
this.provider.use(async (ctx, next) => {
ctx.response.set('X-Frame-Options', 'SAMEORIGIN');
ctx.response.set('Content-Security-Policy', "default-src 'none'; frame-ancestors 'self' example.com *.example.net; script-src 'self'; connect-src 'self'; img-src 'self'; style-src 'self';");
ctx.response.set('Content-Security-Policy', "default-src 'none'; frame-ancestors 'self' example.com *.example.net; script-src 'self' 'nonce-foo'; connect-src 'self'; img-src 'self'; style-src 'self';");
await next();
});
});
before(function () {
const { scriptNonce: orig } = i(this.provider).configuration('features.sessionManagement');
this.orig = orig;
});

afterEach(function () {
i(this.provider).configuration('features.sessionManagement').scriptNonce = this.orig;
});

it('responds with frameable html', async function () {
await this.agent.get('/session/check')
.expect(200)
.expect('content-type', /text\/html/)
.expect((response) => {
expect(response.headers['x-frame-options']).not.to.be.ok;
expect(response.headers['content-security-policy']).not.to.match(/frame-ancestors/);
expect(response.text).not.to.contain('nonce="foo"');
});

i(this.provider).configuration('features.sessionManagement').scriptNonce = (ctx) => {
expect(ctx.oidc).to.be.ok;
return 'foo';
};

it('responds with frameable html', function () {
return this.agent.get('/session/check')
.expect(200)
.expect('content-type', /text\/html/)
.expect((response) => {
expect(response.headers['x-frame-options']).not.to.be.ok;
expect(response.headers['content-security-policy']).not.to.match(/frame-ancestors/);
expect(response.text).to.contain('nonce="foo"');
});
});

Expand Down
2 changes: 1 addition & 1 deletion test/web_message/web_message.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ describe('configuration features.webMessageResponseMode', () => {
before(function () {
this.provider.use(async (ctx, next) => {
ctx.response.set('X-Frame-Options', 'SAMEORIGIN');
ctx.response.set('Content-Security-Policy', "default-src 'none'; frame-ancestors 'self' example.com *.example.net; script-src 'self'; connect-src 'self'; img-src 'self'; style-src 'self';");
ctx.response.set('Content-Security-Policy', "default-src 'none'; frame-ancestors 'self' example.com *.example.net; script-src 'self' 'nonce-foo'; connect-src 'self'; img-src 'self'; style-src 'self';");
await next();
});
});
Expand Down
4 changes: 2 additions & 2 deletions types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -897,15 +897,15 @@ export interface Configuration {
};
dPoP?: { enabled?: boolean, iatTolerance?: number, ack?: 'id-03' },

sessionManagement?: { enabled?: boolean, keepHeaders?: boolean, ack?: 28 },
sessionManagement?: { enabled?: boolean, keepHeaders?: boolean, ack?: 28, scriptNonce?: (ctx: KoaContextWithOIDC) => string },

backchannelLogout?: { enabled?: boolean, ack?: 4 },

ietfJWTAccessTokenProfile?: { enabled?: boolean, ack?: 2 },

fapiRW?: { enabled?: boolean, ack?: 'id02-rev.3' },

webMessageResponseMode?: { enabled?: boolean, ack?: 'id-00' },
webMessageResponseMode?: { enabled?: boolean, ack?: 'id-00', scriptNonce?: (ctx: KoaContextWithOIDC) => string },

jwtIntrospection?: { enabled?: boolean, ack?: 8 },

Expand Down
4 changes: 2 additions & 2 deletions types/oidc-provider-tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -280,9 +280,9 @@ const provider = new Provider('https://op.example.com', {
introspection: { enabled: false },
userinfo: { enabled: false },
jwtUserinfo: { enabled: false },
webMessageResponseMode: { enabled: false, ack: 'id-00' },
webMessageResponseMode: { enabled: false, ack: 'id-00', scriptNonce() { return "foo"; } },
revocation: { enabled: false },
sessionManagement: { enabled: false, ack: 28, keepHeaders: false },
sessionManagement: { enabled: false, ack: 28, keepHeaders: false, scriptNonce() { return "foo"; } },
jwtIntrospection: { enabled: false, ack: 8 },
jwtResponseModes: { enabled: false, ack: 2 },
pushedAuthorizationRequests: { enabled: false, ack: 0 },
Expand Down

0 comments on commit b32b8e6

Please sign in to comment.