api: Move OAuth2.0 client definitions to config file, from MongoDB
This commit is contained in:
parent
470dfe339d
commit
4779965377
|
@ -7,5 +7,21 @@
|
|||
"mongodb": {
|
||||
"url": "mongodb://mongo/obsTest",
|
||||
"debug": true
|
||||
}
|
||||
},
|
||||
"oAuth2Clients": [
|
||||
{
|
||||
"clientId": "b730f8d2-d93c-4c68-9ff0-dfac8da76ee2",
|
||||
"validRedirectUris": ["http://localhost:3001/redirect"],
|
||||
"refreshTokenExpirySeconds": 604800,
|
||||
"maxScope": "*",
|
||||
"title": "OBS Portal"
|
||||
},
|
||||
{
|
||||
"clientId": "a2958209-4045-4ec9-8cb3-1156abba7de3",
|
||||
"validRedirectUris": ["__LOCAL__"],
|
||||
"maxScope": "track.upload",
|
||||
"refreshTokenExpirySeconds": 86400000,
|
||||
"title": "OpenBikeSensor"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -15,5 +15,21 @@
|
|||
"mongodb": {
|
||||
"url": "mongodb://user:pass@host/obs",
|
||||
"debug": false
|
||||
}
|
||||
},
|
||||
"oAuth2Clients": [
|
||||
{
|
||||
"clientId": "CHANGEME",
|
||||
"validRedirectUris": ["https://site.example.com/redirect"],
|
||||
"refreshTokenExpirySeconds": 604800,
|
||||
"maxScope": "*",
|
||||
"title": "OBS Portal"
|
||||
},
|
||||
{
|
||||
"clientId": "CHANGEME",
|
||||
"validRedirectUris": ["__LOCAL__"],
|
||||
"maxScope": "track.upload",
|
||||
"refreshTokenExpirySeconds": 86400000,
|
||||
"title": "OpenBikeSensor"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -25,6 +25,30 @@ const configSchema = Joi.object({
|
|||
url: Joi.string().required(),
|
||||
debug: Joi.boolean().default(process.env.NODE_ENV !== 'production'),
|
||||
}).required(),
|
||||
|
||||
oAuth2Clients: Joi.array()
|
||||
.default([])
|
||||
.items(
|
||||
Joi.object({
|
||||
title: Joi.string().required(),
|
||||
clientId: Joi.string().required(),
|
||||
validRedirectUris: Joi.array().required().items(Joi.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: Joi.number()
|
||||
.default(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: Joi.string().required(),
|
||||
}),
|
||||
),
|
||||
}).required();
|
||||
|
||||
const configFiles = [
|
||||
|
|
|
@ -6,7 +6,7 @@ const schema = new mongoose.Schema(
|
|||
{
|
||||
token: { index: true, type: String, required: true, unique: true },
|
||||
user: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true },
|
||||
client: { type: mongoose.Schema.Types.ObjectId, ref: 'Client', required: true },
|
||||
clientId: { type: String, required: true },
|
||||
expiresAt: { type: Date, required: true },
|
||||
scope: { type: String, required: true, defaultValue: '*' },
|
||||
},
|
||||
|
@ -31,15 +31,13 @@ class AccessToken extends mongoose.Model {
|
|||
return 'Bearer ' + this.token;
|
||||
}
|
||||
|
||||
static generate(client, user, scope = '*', expiresInSeconds = 24 * 60 * 60) {
|
||||
static generate(options, expiresInSeconds = 24 * 60 * 60) {
|
||||
const token = crypto.randomBytes(32).toString('hex');
|
||||
|
||||
return new AccessToken({
|
||||
...options,
|
||||
token,
|
||||
user,
|
||||
client,
|
||||
expiresAt: new Date(new Date().getTime() + 1000 * expiresInSeconds),
|
||||
scope,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@ const schema = new mongoose.Schema(
|
|||
{
|
||||
code: { type: String, unique: true, required: true },
|
||||
user: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true },
|
||||
client: { type: mongoose.Schema.Types.ObjectId, ref: 'Client', required: true },
|
||||
clientId: { type: String, required: true },
|
||||
scope: { type: String, required: true, defaultValue: '*' },
|
||||
redirectUri: { type: String, required: true },
|
||||
expiresAt: { type: Date, required: true },
|
||||
|
@ -15,17 +15,13 @@ const schema = new mongoose.Schema(
|
|||
);
|
||||
|
||||
class AuthorizationCode extends mongoose.Model {
|
||||
static generate(client, user, redirectUri, scope, codeChallenge, expiresInSeconds = 60) {
|
||||
static generate(options, expiresInSeconds = 60) {
|
||||
const code = crypto.randomBytes(8).toString('hex');
|
||||
|
||||
return new AuthorizationCode({
|
||||
...options,
|
||||
code,
|
||||
user,
|
||||
client,
|
||||
redirectUri,
|
||||
expiresAt: new Date(new Date().getTime() + 1000 * expiresInSeconds),
|
||||
scope,
|
||||
codeChallenge,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,37 +0,0 @@
|
|||
const mongoose = require('mongoose');
|
||||
|
||||
const schema = new mongoose.Schema(
|
||||
{
|
||||
title: { type: String, required: true },
|
||||
clientId: { type: String, required: true }, // this is external, so we do not use the ObjectID
|
||||
validRedirectUris: [{ type: String }],
|
||||
|
||||
// this implementation deals with public clients only, so the following fields are not required:
|
||||
// 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 },
|
||||
);
|
||||
|
||||
class Client extends mongoose.Model {}
|
||||
|
||||
mongoose.model(Client, schema);
|
||||
|
||||
module.exports = Client;
|
|
@ -8,7 +8,7 @@ const schema = new mongoose.Schema(
|
|||
{
|
||||
token: { index: true, type: String, required: true, unique: true },
|
||||
user: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true },
|
||||
client: { type: mongoose.Schema.Types.ObjectId, ref: 'Client', required: true },
|
||||
clientId: { type: String, required: true },
|
||||
expiresAt: { type: Date, required: false },
|
||||
scope: { type: String, required: true, defaultValue: '*' },
|
||||
},
|
||||
|
@ -29,21 +29,15 @@ class RefreshToken extends mongoose.Model {
|
|||
return this.expiresAt == null || this.expiresAt < new Date()
|
||||
}
|
||||
|
||||
static generate(client, user, scope = '*', expiresInSeconds = 24 * 60 * 60) {
|
||||
static generate(options, expiresInSeconds = 24 * 60 * 60) {
|
||||
const token = crypto.randomBytes(32).toString('hex');
|
||||
|
||||
return new RefreshToken({
|
||||
...options,
|
||||
token,
|
||||
client,
|
||||
user,
|
||||
expiresAt: new Date(new Date().getTime() + 1000 * expiresInSeconds),
|
||||
scope,
|
||||
});
|
||||
}
|
||||
|
||||
genererateAccessToken(expiresInSeconds = undefined) {
|
||||
return AccessToken.generate(this.user, this.scope, expiresInSeconds)
|
||||
}
|
||||
}
|
||||
|
||||
mongoose.model(RefreshToken, schema);
|
||||
|
|
|
@ -2,6 +2,7 @@ const mongoose = require('mongoose');
|
|||
const uniqueValidator = require('mongoose-unique-validator');
|
||||
const crypto = require('crypto');
|
||||
const jwt = require('jsonwebtoken');
|
||||
const config = require('../config')
|
||||
|
||||
const schema = new mongoose.Schema(
|
||||
{
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
module.exports.AccessToken = require('./AccessToken')
|
||||
module.exports.AuthorizationCode = require('./AuthorizationCode')
|
||||
module.exports.Client = require('./Client')
|
||||
module.exports.Comment = require('./Comment')
|
||||
module.exports.RefreshToken = require('./RefreshToken')
|
||||
module.exports.Track = require('./Track')
|
||||
|
|
|
@ -3,10 +3,10 @@ const passport = require('passport');
|
|||
const { URL } = require('url');
|
||||
const { createChallenge } = require('pkce');
|
||||
|
||||
const { AuthorizationCode, AccessToken, RefreshToken, Client } = require('../models');
|
||||
const { AuthorizationCode, AccessToken, RefreshToken } = require('../models');
|
||||
const auth = require('../passport');
|
||||
const wrapRoute = require('../_helpers/wrapRoute');
|
||||
const config = require('../config')
|
||||
const config = require('../config');
|
||||
|
||||
// Check whether the "bigScope" fully includes the "smallScope".
|
||||
function scopeIncludes(smallScope, bigScope) {
|
||||
|
@ -49,7 +49,7 @@ function isValidScope(scope) {
|
|||
|
||||
router.use((req, res, next) => {
|
||||
res.locals.user = req.user;
|
||||
res.locals.mainFrontendUrl = config.mainFrontendUrl
|
||||
res.locals.mainFrontendUrl = config.mainFrontendUrl;
|
||||
next();
|
||||
});
|
||||
|
||||
|
@ -170,7 +170,7 @@ router.get(
|
|||
return returnError(res, 'invalid_request', 'client_id parameter required');
|
||||
}
|
||||
|
||||
const client = await Client.findOne({ clientId });
|
||||
const client = await config.oAuth2Clients.find((c) => c.clientId === clientId);
|
||||
if (!client) {
|
||||
return returnError(res, 'invalid_client', 'unknown client');
|
||||
}
|
||||
|
@ -269,13 +269,17 @@ router.post(
|
|||
});
|
||||
}
|
||||
|
||||
const client = await Client.findOne({ clientId });
|
||||
|
||||
// invalidate the transaction
|
||||
req.session.authorizationTransaction = null;
|
||||
|
||||
if (req.path === '/authorize/approve') {
|
||||
const code = AuthorizationCode.generate(client, req.user, redirectUri, scope, codeChallenge);
|
||||
const code = AuthorizationCode.generate({
|
||||
clientId,
|
||||
user: req.user,
|
||||
redirectUri,
|
||||
scope,
|
||||
codeChallenge,
|
||||
});
|
||||
await code.save();
|
||||
|
||||
return redirectWithParams(res, redirectUri, { code: code.code, scope });
|
||||
|
@ -336,7 +340,7 @@ router.get(
|
|||
return returnError(res, 'invalid_request', 'code_verifier parameter required');
|
||||
}
|
||||
|
||||
const client = await Client.findOne({ clientId });
|
||||
const client = await config.oAuth2Clients.find((c) => c.clientId === clientId);
|
||||
|
||||
if (!client) {
|
||||
await destroyAuthCode();
|
||||
|
@ -356,7 +360,7 @@ router.get(
|
|||
await destroyAuthCode();
|
||||
return returnError(res, 'invalid_grant', 'invalid authorization code');
|
||||
}
|
||||
if (!client._id.equals(authorizationCode.client)) {
|
||||
if (clientId !== authorizationCode.clientId) {
|
||||
await destroyAuthCode();
|
||||
return returnError(res, 'invalid_grant', 'invalid authorization code');
|
||||
}
|
||||
|
@ -368,15 +372,21 @@ router.get(
|
|||
// invalidate auth code now, before generating tokens
|
||||
await AuthorizationCode.deleteOne({ _id: authorizationCode._id });
|
||||
|
||||
const accessToken = AccessToken.generate(authorizationCode.client, authorizationCode.user, authorizationCode.scope);
|
||||
const accessToken = AccessToken.generate({
|
||||
clientId: authorizationCode.clientId,
|
||||
user: authorizationCode.user,
|
||||
scope: authorizationCode.scope,
|
||||
});
|
||||
await accessToken.save();
|
||||
|
||||
let refreshToken;
|
||||
if (client.refreshTokenExpirySeconds != null) {
|
||||
refreshToken = RefreshToken.generate(
|
||||
authorizationCode.client,
|
||||
authorizationCode.user,
|
||||
authorizationCode.scope,
|
||||
{
|
||||
clientId: authorizationCode.clientId,
|
||||
user: authorizationCode.user,
|
||||
scope: authorizationCode.scope,
|
||||
},
|
||||
client.refreshTokenExpirySeconds,
|
||||
);
|
||||
await refreshToken.save();
|
||||
|
@ -399,7 +409,7 @@ router.get(
|
|||
router.get(
|
||||
'/.well-known/oauth-authorization-server',
|
||||
wrapRoute(async (req, res) => {
|
||||
const baseUrl = config.baseUrl.replace(/\/+$/, '')
|
||||
const baseUrl = config.baseUrl.replace(/\/+$/, '');
|
||||
|
||||
return res.json({
|
||||
issuer: baseUrl,
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"auth": {
|
||||
"server": "http://localhost:3000",
|
||||
"clientId": "123",
|
||||
"clientId": "b730f8d2-d93c-4c68-9ff0-dfac8da76ee2",
|
||||
"scope": "*",
|
||||
"redirectUri": "http://localhost:3001/redirect"
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue