api: Add Client.maxScope and Client.refreshTokenExpirySeconds options

This commit is contained in:
Paul Bienkowski 2021-02-23 18:45:19 +01:00
parent 768f0f541b
commit 39e1d2a9f4
2 changed files with 37 additions and 11 deletions

View file

@ -6,9 +6,25 @@ const schema = new mongoose.Schema(
validRedirectUris: [{ type: String }],
// this implementation deals with public clients only, so the following fields are not required:
// scope: {type: String, required: true, default: '*'}, // max possible scope
// confidential: {type: Boolean}, // whether this is a non-public, aka confidential client
// clientSecret: { type: String },
// Set `refreshTokenExpirySeconds` to null to issue no refresh tokens. Set
// to a number of seconds to issue refresh tokens with that duration. No
// infinite tokens are ever issued, set to big number to simulate that.
refreshTokenExpirySeconds: {
type: Number,
required: false,
defaultValue: null,
min: 1, // 0 would make no sense, use `null` to issue no token
max: 1000 * 24 * 60 * 60, // 1000 days, nearly 3 years
},
// Set to a scope which cannot be exceeded when requesting client tokens.
// Clients must manually request a scope that is smaller or equal to this
// scope to get a valid response. Scopes are not automatically truncated.
// Leave empty or set to `"*"` for unlimited scopes in this client.
maxScope: { type: String, required: false },
},
{ timestamps: true },
);

View file

@ -1,7 +1,6 @@
const router = require('express').Router();
const passport = require('passport');
const { URL } = require('url');
const querystring = require('querystring');
const { AuthorizationCode, AccessToken, RefreshToken, Client } = require('../models');
const wrapRoute = require('../_helpers/wrapRoute');
@ -96,7 +95,7 @@ const isValidRedirectUriFor = (redirectUri) => (redirectUriPattern) => {
// is because we cannot know beforehand which IP the OBS will be running at.
// But since it is usually accessed via local IP, we can allow all local IP
// ranges. This special case must only be used in clients that have a very
// restricted maxScope as well, to prevent misuse should an attack through
// restricted `maxScope` as well, to prevent misuse should an attack through
// this be successful.
// This special case does however enforce TLS ("https://"), for it prevents
// usage in a non-TLS-secured web server. At least passive sniffing of the
@ -173,6 +172,13 @@ router.get(
});
}
if (client.maxScope && !scopeIncludes(scope, client.maxScope)) {
return redirectWithParams(res, redirectUri, {
error: 'access_denied',
error_description: 'the requested scope is not valid for this client',
});
}
// Ok, let's save all this in the session, and show a dialog for the
// decision to the user.
@ -303,21 +309,25 @@ router.get(
await AuthorizationCode.deleteOne({ _id: authorizationCode._id });
const accessToken = AccessToken.generate(authorizationCode.client, authorizationCode.user, authorizationCode.scope);
await accessToken.save();
const refreshToken = RefreshToken.generate(
authorizationCode.client,
authorizationCode.user,
authorizationCode.scope,
);
await Promise.all([accessToken.save(), refreshToken.save()]);
let refreshToken;
if (client.refreshTokenExpirySeconds != null) {
refreshToken = RefreshToken.generate(
authorizationCode.client,
authorizationCode.user,
authorizationCode.scope,
client.refreshTokenExpirySeconds,
);
await refreshToken.save();
}
return res.json({
access_token: accessToken.token,
token_type: 'Bearer',
expires_in: Math.round((accessToken.expiresAt - new Date().getTime()) / 1000),
refresh_token: refreshToken.token,
scope: accessToken.scope,
...(refreshToken != null ? { refresh_token: refreshToken.token } : {}),
});
}),
);