Skip to content

Commit

Permalink
feat: add Device Flow experimental/draft feature
Browse files Browse the repository at this point in the history
Based on
[OAuth 2.0 Device Flow for Browserless and Input Constrained Devices - draft 10](https://tools.ietf.org/html/draft-ietf-oauth-device-flow-10)
with added OIDC flavor

- device_authorization_endpoint accepts additional OIDC defined params (i.e. claims, request, request_uri, etc...) and processes them in the same way a regular OIDC authorization endpoint would
- device_authorization_endpoint ignores response_type, response_mode, state, redirect_uri params

> This OAuth 2.0 authorization flow for browserless and input
> constrained devices, often referred to as the device flow, enables
> OAuth clients to request user authorization from devices that have an
> Internet connection, but don't have an easy input method (such as a
> smart TV, media console, picture frame, or printer), or lack a
> suitable browser for a more traditional OAuth flow.  This
> authorization flow instructs the user to perform the authorization
> request on a secondary device, such as a smartphone.  There is no
> requirement for communication between the constrained device and the
> user's secondary device.
  • Loading branch information
panva committed Jul 16, 2018
1 parent 3aca2c8 commit 461a8e3
Show file tree
Hide file tree
Showing 81 changed files with 3,436 additions and 471 deletions.
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,7 @@ This toggle makes the OP only include End-User claims in the ID Token as defined
```js
provider.use(async function introspectionTokenType(ctx, next) {
await next();
if (ctx._matchedRouteName === 'introspection') {
if (ctx.oidc.route === 'introspection') {
const token = ctx.oidc.entities.AccessToken || ctx.oidc.entities.ClientCredentials || ctx.oidc.entities.RefreshToken;
switch (token && token.kind) {
Expand Down
166 changes: 152 additions & 14 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ is a good starting point to get an idea of what you should provide.
- [cookies.names](#cookiesnames)
- [cookies.short](#cookiesshort)
- [cookies.thirdPartyCheckUrl](#cookiesthirdpartycheckurl)
- [deviceCodeSuccess](#devicecodesuccess)
- [discovery](#discovery)
- [extraClientMetadata](#extraclientmetadata)
- [extraClientMetadata.properties](#extraclientmetadataproperties)
Expand Down Expand Up @@ -72,6 +73,8 @@ is a good starting point to get an idea of what you should provide.
- [ttl](#ttl)
- [uniqueness](#uniqueness)
- [unsupported](#unsupported)
- [userCodeConfirmSource](#usercodeconfirmsource)
- [userCodeInputSource](#usercodeinputsource)

<!-- /TOC -->

Expand Down Expand Up @@ -802,18 +805,38 @@ You can push custom middleware to be executed before and after oidc-provider.

```js
provider.use(async (ctx, next) => {
// pre-processing
// you may target a specific action here by matching `ctx.path`
/** pre-processing
* you may target a specific action here by matching `ctx.path`
*/
console.log('middleware pre', ctx.method, ctx.path);

await next();
console.log('middleware post', ctx.method, ctx._matchedRouteName);
// post-processing
// since internal route matching was already executed you may target a specific action here
// checking `ctx._matchedRouteName`, the unique route names used are "authorization", "token",
// "discovery", "registration", "userinfo", "resume", "certificates", "webfinger",
// "client", "client_update", "client_delete", "introspection", "revocation",
// "check_session" and "end_session". ctx.method === 'OPTIONS' is then useful for filtering out
// CORS Pre-flights
/** post-processing
* since internal route matching was already executed you may target a specific action here
* checking `ctx.oidc.route`, the unique route names used are
*
* `authorization`
* `certificates`
* `client_delete`
* `client_update`
* `code_verification`
* `device_authorization`
* `device_resume`
* `end_session`
* `introspection`
* `registration`
* `resume`
* `revocation`
* `token`
* `userinfo`
* `webfinger`
* `check_session`
* `client`
* `discovery`
*
* ctx.method === 'OPTIONS' is then useful for filtering out CORS Pre-flights
*/
console.log('middleware post', ctx.method, ctx.oidc.route);
});
```

Expand Down Expand Up @@ -1040,6 +1063,34 @@ default value:
'https://cdn.rawgit.com/panva/3rdpartycookiecheck/92fead3f/start.html'
```

### deviceCodeSuccess

HTML source rendered when device code feature renders a success page for the User-Agent.

affects: device code success page

default value:
```js
async deviceCodeSuccess(ctx) {
// @param ctx - koa request context
const {
clientId, clientName, clientUri, initiateLoginUri, logoUri, policyUri, tosUri,
} = ctx.oidc.client;
ctx.body = `<!DOCTYPE html>
<head>
<title>Sign-in Success</title>
<style>/* css and html classes omitted for brevity, see lib/helpers/defaults.js */</style>
</head>
<body>
<div>
<h1>Sign-in Success</h1>
<p>Your login ${clientName ? `with ${clientName}` : ''} was successful, you can now close this page.</p>
</div>
</body>
</html>`;
}
```

### discovery

Pass additional properties to this object to extend the discovery document
Expand Down Expand Up @@ -1091,9 +1142,9 @@ validator(key, value, metadata) {

### extraParams

Pass an iterable object (i.e. Array or set of strings) to extend the parameters recognised by the authorization endpoint. These parameters are then available in `ctx.oidc.params` as well as passed to interaction session details
Pass an iterable object (i.e. Array or set of strings) to extend the parameters recognised by the authorization and device authorization endpoints. These parameters are then available in `ctx.oidc.params` as well as passed to interaction session details

affects: authorization, interaction
affects: authorization, device_authorization, interaction

default value:
```js
Expand All @@ -1117,6 +1168,7 @@ default value:
claimsParameter: false,
clientCredentials: false,
conformIdTokenClaims: false,
deviceCode: false,
encryption: false,
frontchannelLogout: false,
introspection: false,
Expand Down Expand Up @@ -1172,6 +1224,7 @@ default value:
AccessToken: undefined,
AuthorizationCode: undefined,
RefreshToken: undefined,
DeviceCode: undefined,
ClientCredentials: undefined,
InitialAccessToken: undefined,
RegistrationAccessToken: undefined }
Expand Down Expand Up @@ -1271,13 +1324,16 @@ default value:

### logoutSource

HTML source to which a logout form source is passed when session management renders a confirmation prompt for the User-Agent.
HTML source rendered when when session management feature renders a confirmation prompt for the User-Agent.

affects: session management

default value:
```js
async logoutSource(ctx, form) {
// @param ctx - koa request context
// @param form - form source (id="op.logoutForm") to be embedded in the page and submitted by
// the End-User
ctx.body = `<!DOCTYPE html>
<head>
<title>Logout Request</title>
Expand Down Expand Up @@ -1426,12 +1482,14 @@ default value:
{ authorization: '/auth',
certificates: '/certs',
check_session: '/session/check',
device_authorization: '/device/auth',
end_session: '/session/end',
introspection: '/token/introspection',
registration: '/reg',
revocation: '/token/revocation',
token: '/token',
userinfo: '/me' }
userinfo: '/me',
code_verification: '/device' }
```

### scopes
Expand Down Expand Up @@ -1484,6 +1542,7 @@ default value:
{ AccessToken: 3600,
AuthorizationCode: 600,
ClientCredentials: 600,
DeviceCode: 600,
IdToken: 3600,
RefreshToken: 1209600 }
```
Expand Down Expand Up @@ -1525,6 +1584,85 @@ default value:
userinfoEncryptionEncValues: [],
userinfoSigningAlgValues: [] }
```

### userCodeConfirmSource

HTML source rendered when device code feature renders an a confirmation prompt for ther User-Agent.

affects: device code authorization confirmation

default value:
```js
async userCodeConfirmSource(ctx, form, client, deviceInfo) {
// @param ctx - koa request context
// @param form - form source (id="op.deviceConfirmForm") to be embedded in the page and
// submitted by the End-User.
// @param deviceInfo - device information from the device_authorization_endpoint call
const {
clientId, clientName, clientUri, logoUri, policyUri, tosUri,
} = ctx.oidc.client;
ctx.body = `<!DOCTYPE html>
<head>
<title>Device Login Confirmation</title>
<style>/* css and html classes omitted for brevity, see lib/helpers/defaults.js */</style>
</head>
<body>
<div>
<h1>Confirm Device</h1>
<p>
You are about to authorize a <code>${clientName || clientId}</code> device client on IP <code>${deviceInfo.ip}</code>, identified by <code>${deviceInfo.userAgent}</code>
<br/><br/>
If you did not initiate this action and/or are unaware of such device in your possession please close this window.
</p>
${form}
<button autofocus type="submit" form="op.deviceConfirmForm">Continue</button>
<div>
<a href="">[ Cancel ]</a>
</div>
</div>
</body>
</html>`;
}
```

### userCodeInputSource

HTML source rendered when device code feature renders an input prompt for the User-Agent.

affects: device code input

default value:
```js
async userCodeInputSource(ctx, form, out, err) {
// @param ctx - koa request context
// @param form - form source (id="op.deviceInputForm") to be embedded in the page and submitted
// by the End-User.
// @param out - if an error is returned the out object contains details that are fit to be
// rendered, i.e. does not include internal error messages
// @param err - error object with an optional userCode property passed when the form is being
// re-rendered due to code missing/invalid/expired
let msg;
if (err && (err.userCode || err.name === 'NoCodeError')) {
msg = '<p>The code you entered is incorrect. Try again</p>';
} else if (err) {
msg = '<p>There was an error processing your request</p>';
}
ctx.body = `<!DOCTYPE html>
<head>
<title>Sign-in</title>
<style>/* css and html classes omitted for brevity, see lib/helpers/defaults.js */</style>
</head>
<body>
<div>
<h1>Sign-in</h1>
${msg}
${form}
<button type="submit" form="op.deviceInputForm">Continue</button>
</div>
</body>
</html>`;
}
```
<!-- END CONF OPTIONS -->

[client-metadata]: https://openid.net/specs/openid-connect-registration-1_0.html#ClientMetadata
Expand Down
6 changes: 4 additions & 2 deletions docs/update-configuration.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
/* eslint-disable no-param-reassign */

const { createInterface: readline } = require('readline');
const { inspect } = require('util');
const { createReadStream, writeFileSync, readFileSync } = require('fs');
const values = require('../lib/helpers/defaults');

const { get } = require('lodash');
const { inspect } = require('util');

const values = require('../lib/helpers/defaults');

function capitalizeSentences(copy) {
return copy.replace(/\. [a-z]/g, match => `. ${match.slice(-1).toUpperCase()}`);
Expand Down
23 changes: 22 additions & 1 deletion example/my_adapter.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ class MyAdapter {
* @constructor
* @param {string} name Name of the oidc-provider model. One of "Session", "AccessToken",
* "AuthorizationCode", "RefreshToken", "ClientCredentials", "Client", "InitialAccessToken",
* "RegistrationAccessToken"
* "RegistrationAccessToken", "DeviceCode"
*
*/
constructor(name) {
Expand Down Expand Up @@ -67,6 +67,13 @@ class MyAdapter {
* - redirectUri {string} - redirect_uri value from an authorization request
* - scope {string} - scope value from on authorization request
* - sid {string} - session identifier the token comes from
* - params {object} - [DeviceCode only] an object with the authorization request parameters
* as requested by the client with device_authorization_endpoint
* - deviceInfo {object} - [DeviceCode only] an object with details about the
* device_authorization_endpoint request
* - error {string} - [DeviceCode only] - error from authnz to be returned to the polling client
* - errorDescription {string} - [DeviceCode only] - error_description from authnz to be returned
* to the polling client
*
*
* when `jwt`
Expand Down Expand Up @@ -113,6 +120,20 @@ class MyAdapter {

}

/**
*
* Return previously stored instance of DeviceCode. You only need this method for the deviceCode
* feature
*
* @return {Promise} Promise fulfilled with either Object (when found and not dropped yet due to
* expiration) or falsy value when not found anymore. Rejected with error when encountered.
* @param {string} userCode the user_code value associated with a DeviceCode instance
*
*/
async findByUserCode(userCode) {

}

/**
*
* Mark a stored oidc-provider model as consumed (not yet expired though!). Future finds for this
Expand Down
36 changes: 36 additions & 0 deletions lib/actions/authorization/assign_claims.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
const { merge } = require('lodash');

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

/*
* If claims parameter is provided and supported handles it's validation
* - should not be combined with rt none
* - should be JSON serialized object with id_token or userinfo properties as objects
* - claims.userinfo should not be used if authorization result is not access_token
*
* Merges requested claims with auth_time as requested if max_age is provided or require_auth_time
* is configured for the client.
*
* Merges requested claims with acr as requested if acr_values is provided
*
* @throws: invalid_request
*/
module.exports = provider => async function assignClaims(ctx, next) {
const { params } = ctx.oidc;

if (params.claims !== undefined && instance(provider).configuration('features.claimsParameter')) {
ctx.oidc.claims = JSON.parse(params.claims);
}

if (params.max_age || ctx.oidc.client.requireAuthTime || ctx.oidc.prompts.includes('login')) {
merge(ctx.oidc.claims, { id_token: { auth_time: { essential: true } } });
}

const acrValues = params.acr_values;

if (acrValues) {
merge(ctx.oidc.claims, { id_token: { acr: { values: acrValues.split(' ') } } });
}

await next();
};
10 changes: 6 additions & 4 deletions lib/actions/authorization/authorization_emit.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@ const Debug = require('debug');
const accepted = new Debug('oidc-provider:authentication:accepted');
const resumed = new Debug('oidc-provider:authentication:resumed');

const authorizationRoutes = new Set(['authorization', 'code_verification']);

module.exports = provider => async function authorizationEmit(ctx, next) {
if (ctx.oidc.result) {
resumed('uuid=%s %o', ctx.oidc.uuid, ctx.oidc.result);
provider.emit('interaction.ended', ctx);
} else {
if (authorizationRoutes.has(ctx.oidc.route)) {
accepted('uuid=%s %o', ctx.oidc.uuid, ctx.oidc.params);
provider.emit('authorization.accepted', ctx);
} else {
resumed('uuid=%s %o', ctx.oidc.uuid, ctx.oidc.result);
provider.emit('interaction.ended', ctx);
}
await next();
};
Loading

0 comments on commit 461a8e3

Please sign in to comment.