Skip to content
Snippets Groups Projects
Unverified Commit b14af492 authored by Louis's avatar Louis :fire:
Browse files

Mostly implement oauth server

parent b3ca5777
No related branches found
No related tags found
No related merge requests found
module.exports = {
up: (migration, Types) => {
return migration.createTable('oauth_authorization_codes', {
id: {
type: Types.UUID,
primaryKey: true,
defaultValue: Types.UUIDV4,
allowNull: false,
},
client_id: {
type: Types.UUID,
allowNull: false,
onDelete: 'CASCADE',
onUpdate: 'CASCADE',
references: {
model: 'oauth_clients',
key: 'id',
},
},
user_id: {
type: Types.UUID,
allowNull: false,
onDelete: 'CASCADE',
onUpdate: 'CASCADE',
references: {
model: 'users',
key: 'id',
},
},
scope: {
type: Types.TEXT,
allowNull: true,
},
meta: {
type: Types.JSONB,
defaultValue: {},
allowNull: false,
},
created_at: {
type: Types.DATE,
defaultValue: Types.fn('now'),
allowNull: false,
},
updated_at: {
type: Types.DATE,
defaultValue: Types.fn('now'),
allowNull: false,
},
deleted_at: {
type: Types.DATE,
defaultValue: null,
allowNull: true,
},
})
},
down: (migration, Types) => {
return migration.dropTable('oauth_refresh_tokens')
},
}
{
"name": "HotTrash",
"version": "0.1.0",
"lockfileVersion": 1,
"requires": true,
......@@ -890,9 +891,12 @@
}
},
"basic-auth": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-0.0.1.tgz",
"integrity": "sha1-Md22WEP2w1xv6nvrRqmHy4zhiSQ="
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz",
"integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==",
"requires": {
"safe-buffer": "5.1.2"
}
},
"bcrypt-pbkdf": {
"version": "1.0.2",
......@@ -1243,6 +1247,22 @@
"resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
"integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ="
},
"co-bluebird": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/co-bluebird/-/co-bluebird-1.1.0.tgz",
"integrity": "sha1-yLnzqTIKftMJh9zKGlw8/1llXHw=",
"requires": {
"bluebird": "^2.10.0",
"co-use": "^1.1.0"
},
"dependencies": {
"bluebird": {
"version": "2.11.0",
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-2.11.0.tgz",
"integrity": "sha1-U0uQM8AiyVecVro7Plpcqvu2UOE="
}
}
},
"co-body": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/co-body/-/co-body-6.0.0.tgz",
......@@ -1254,6 +1274,11 @@
"type-is": "^1.6.16"
}
},
"co-use": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/co-use/-/co-use-1.1.0.tgz",
"integrity": "sha1-xrs83xDLc17Kqdru2kbXJclKTmI="
},
"collection-visit": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz",
......@@ -3464,6 +3489,11 @@
"integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=",
"dev": true
},
"is-generator": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/is-generator/-/is-generator-1.0.3.tgz",
"integrity": "sha1-wUwhBX7TbjKNuANHlmxpP4hjifM="
},
"is-generator-fn": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz",
......@@ -4477,6 +4507,21 @@
"requires": {
"oauth2-server": "^2.3.0",
"thenify": "^3.0.0"
},
"dependencies": {
"basic-auth": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-0.0.1.tgz",
"integrity": "sha1-Md22WEP2w1xv6nvrRqmHy4zhiSQ="
},
"oauth2-server": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/oauth2-server/-/oauth2-server-2.4.1.tgz",
"integrity": "sha1-2m3QVMAh7JwpQ59dGijeY9ArcWw=",
"requires": {
"basic-auth": "~0.0.1"
}
}
}
},
"koa-router": {
......@@ -4968,11 +5013,16 @@
"dev": true
},
"oauth2-server": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/oauth2-server/-/oauth2-server-2.4.1.tgz",
"integrity": "sha1-2m3QVMAh7JwpQ59dGijeY9ArcWw=",
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/oauth2-server/-/oauth2-server-3.0.1.tgz",
"integrity": "sha512-LFAT4MeTaOgdW+b8YMVMsPhJ8LrbSfVkYZRPgRmELJEJoXcchb/L4b9/lEmgpeNtjH8PlFiqof+YwI+y/oJuOg==",
"requires": {
"basic-auth": "~0.0.1"
"basic-auth": "^2.0.0",
"bluebird": "^3.5.1",
"lodash": "^4.17.10",
"promisify-any": "^2.0.1",
"statuses": "^1.5.0",
"type-is": "^1.6.16"
}
},
"object-assign": {
......@@ -5441,6 +5491,23 @@
"integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==",
"dev": true
},
"promisify-any": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/promisify-any/-/promisify-any-2.0.1.tgz",
"integrity": "sha1-QD4AqIE/F1JCq1D+M6afjuzkcwU=",
"requires": {
"bluebird": "^2.10.0",
"co-bluebird": "^1.1.0",
"is-generator": "^1.0.2"
},
"dependencies": {
"bluebird": {
"version": "2.11.0",
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-2.11.0.tgz",
"integrity": "sha1-U0uQM8AiyVecVro7Plpcqvu2UOE="
}
}
},
"prompts": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/prompts/-/prompts-2.2.1.tgz",
......
{
"name": "",
"name": "HotTrash",
"version": "0.1.0",
"description": "",
"main": "index.js",
......@@ -26,6 +26,7 @@
"lodash": "^4.17.15",
"moment": "^2.24.0",
"moment-range": "^4.0.2",
"oauth2-server": "^3.0.1",
"pg": "^7.12.1",
"scrypt-kdf": "^2.0.1",
"sequelize": "^5.21.2",
......
const timestamps = require('./properties/timestamps')
module.exports = (sequelize, DataTypes) => {
const Model = sequelize.define('AccessToken', Object.assign(
{
id: {
type: DataTypes.UUID,
primaryKey: true,
defaultValue: DataTypes.UUIDV4,
validate: {
isUUID: 4,
},
},
token: {
type: DataTypes.TEXT,
},
scope: {
type: DataTypes.TEXT,
},
expires_at: {
type: DataTypes.DATE,
},
meta: {
type: DataTypes.JSONB,
}
},
timestamps(DataTypes),
), {
getterMethods: {
scopes() {
return this.getDataValue('scope').split(' ')
},
},
setterMethods: {
scopes(value) {
if (Array.isArray(value)) {
return this.setDataValue('scope', value.join(' '))
} else {
return this.setDataValue('scope', String(value))
}
},
},
paranoid: true,
tableName: 'oauth_access_tokens',
})
Model.getPolyIdentifier = () => 'oauth_access_token'
Model.getRelationIdentifier = () => 'oauth_access_tokens'
Model.prototype.toJSON = function userToJSON() {
const user = this.user ? { user: this.user } : { }
return {
id: this.id,
token: this.token,
scopes: this.scopes,
expires_at: this.expires_at,
...user,
meta: this.meta,
created_at: this.created_at,
updated_at: this.updated_at,
}
}
Model.prototype.handleIncludes = async function(includes, loaders = null) {
}
Model.associate = function defineModelAssociations(models) {
Model.belongsTo(models.User, { foreignKey: 'user_id' })
Model.belongsTo(models.OAuthClient, { foreignKey: 'client_id' })
}
Model.relations = [
]
return Model
}
const timestamps = require('./properties/timestamps')
module.exports = (sequelize, DataTypes) => {
const Model = sequelize.define('AuthorizationCode', Object.assign(
{
id: {
type: DataTypes.UUID,
primaryKey: true,
defaultValue: DataTypes.UUIDV4,
validate: {
isUUID: 4,
},
},
scope: {
type: DataTypes.TEXT,
},
expires_at: {
type: DataTypes.DATE,
},
meta: {
type: DataTypes.JSONB,
}
},
timestamps(DataTypes),
), {
getterMethods: {
scopes() {
return this.getDataValue('scope').split(' ')
},
},
setterMethods: {
scopes(value) {
if (Array.isArray(value)) {
return this.setDataValue('scope', value.join(' '))
} else {
return this.setDataValue('scope', String(value))
}
},
},
paranoid: true,
tableName: 'oauth_authorization_codes',
})
Model.getPolyIdentifier = () => 'oauth_authorization_code'
Model.getRelationIdentifier = () => 'oauth_authorization_codes'
Model.prototype.toJSON = function userToJSON() {
const user = this.user ? { user: this.user } : { }
const client = this.user ? { user: this.user } : { }
return {
id: this.id,
scopes: this.scopes,
expires_at: this.expires_at,
...user,
meta: this.meta,
created_at: this.created_at,
updated_at: this.updated_at,
}
}
Model.prototype.handleIncludes = async function(includes, loaders = null) {
}
Model.associate = function defineModelAssociations(models) {
Model.belongsTo(models.User, { foreignKey: 'user_id' })
Model.belongsTo(models.OAuthClient, { foreignKey: 'client_id' })
}
Model.relations = [
]
return Model
}
const timestamps = require('./properties/timestamps')
module.exports = (sequelize, DataTypes) => {
const Model = sequelize.define('OAuthClient', Object.assign(
{
id: {
type: DataTypes.UUID,
primaryKey: true,
defaultValue: DataTypes.UUIDV4,
validate: {
isUUID: 4,
},
},
secret: {
type: DataTypes.TEXT,
},
redirect_uris: {
type: DataTypes.ARRAY(DataTypes.TEXT),
},
grant_types: {
type: DataTypes.ARRAY(DataTypes.TEXT),
},
meta: {
type: DataTypes.JSONB,
}
},
timestamps(DataTypes),
), {
paranoid: true,
tableName: 'oauth_clients',
})
Model.getPolyIdentifier = () => 'oauth_client'
Model.getRelationIdentifier = () => 'oauth_clients'
Model.prototype.toJSON = function userToJSON() {
return {
id: this.id,
secret: this.secret,
redirect_uris: this.redirect_uris,
grant_types: this.grant_types,
meta: this.meta,
created_at: this.created_at,
updated_at: this.updated_at,
}
}
Model.prototype.handleIncludes = async function(includes, loaders = null) {
}
Model.generateClient = async function(userId, internal = false) {
const crypto = require('core/utils/crypto')
const secret = await crypto.secureHexString(32)
return Model.create({
owner_id: userId,
secret,
grant_types: ['authorization_code', 'refresh_token'],
internal,
})
}
Model.associate = function defineModelAssociations(models) {
Model.belongsTo(models.User, { as: 'owner', foreignKey: 'owner_id' })
Model.belongsToMany(models.User, { as: 'users', through: models.AccessToken, otherKey: 'user_id', foreignKey: 'client_id' })
Model.hasMany(models.AccessToken, { foreignKey: 'client_id' })
Model.hasMany(models.RefreshToken, { foreignKey: 'client_id' })
}
Model.relations = [
]
return Model
}
const timestamps = require('./properties/timestamps')
module.exports = (sequelize, DataTypes) => {
const Model = sequelize.define('RefreshToken', Object.assign(
{
id: {
type: DataTypes.UUID,
primaryKey: true,
defaultValue: DataTypes.UUIDV4,
validate: {
isUUID: 4,
},
},
token: {
type: DataTypes.TEXT,
},
scope: {
type: DataTypes.TEXT,
},
expires_at: {
type: DataTypes.DATE,
},
meta: {
type: DataTypes.JSONB,
}
},
timestamps(DataTypes),
), {
getterMethods: {
scopes() {
return this.getDataValue('scope').split(' ')
},
},
setterMethods: {
scopes(value) {
if (Array.isArray(value)) {
return this.setDataValue('scope', value.join(' '))
} else {
return this.setDataValue('scope', String(value))
}
},
},
paranoid: true,
tableName: 'oauth_refresh_tokens',
})
Model.getPolyIdentifier = () => 'oauth_refresh_token'
Model.getRelationIdentifier = () => 'oauth_refresh_tokens'
Model.prototype.toJSON = function userToJSON() {
const user = this.user ? { user: this.user } : { }
return {
id: this.id,
token: this.token,
scopes: this.scopes,
expires_at: this.expires_at,
...user,
meta: this.meta,
created_at: this.created_at,
updated_at: this.updated_at,
}
}
Model.prototype.handleIncludes = async function(includes, loaders = null) {
}
Model.associate = function defineModelAssociations(models) {
Model.belongsTo(models.User, { foreignKey: 'user_id' })
Model.belongsTo(models.OAuthClient, { foreignKey: 'client_id' })
}
Model.relations = [
]
return Model
}
const timestamps = require('./properties/timestamps')
module.exports = (sequelize, DataTypes) => {
const Model = sequelize.define('User', Object.assign(
{
id: {
type: DataTypes.UUID,
primaryKey: true,
defaultValue: DataTypes.UUIDV4,
},
name: {
type: DataTypes.TEXT,
},
email: {
type: DataTypes.TEXT,
},
password: {
type: DataTypes.TEXT,
},
reset_token: {
type: DataTypes.TEXT,
},
meta: {
type: DataTypes.JSONB,
}
},
timestamps(DataTypes),
), {
paranoid: true,
tableName: 'users',
})
Model.getPolyIdentifier = () => 'user'
Model.getRelationIdentifier = () => 'users'
Model.prototype.toJSON = function userToJSON() {
return {
id: this.id,
name: this.name,
email: this.email,
meta: this.meta,
created_at: this.created_at,
updated_at: this.updated_at,
}
}
Model.prototype.setPlaintextPassword = async function(password) {
const crypto = require('core/utils/crypto')
this.setDataValue('password', await crypto.hash(password))
}
Model.prototype.asToken = async function() {
const crypto = require('core/utils/crypto')
return await crypto.encrypt(JSON.stringify({ session: this.id }))
}
Model.prototype.checkPassword = async function(password) {
const crypto = require('core/utils/crypto')
if (this.password == null) {
return false
}
return await crypto.verify(this.password, password)
}
Model.createSystemUser = async function() {
const crypto = require('core/utils/crypto')
const user = Model.build({
id: '00000000-0000-0000-0000-000000000000',
name: 'System',
email: 'louis@microhacks.co.uk',
})
await user.setPlaintextPassword(await crypto.secureHexString(32))
await user.save()
return user
}
Model.prototype.handleIncludes = async function(includes, loaders = null) {
}
Model.associate = function defineModelAssociations(models) {
}
Model.relations = [
]
return Model
}
'use strict';
const { fs, config, env } = require('bootstrap')
const path = require('path')
const Sequelize = require('sequelize')
const fs = require('fs');
const path = require('path');
const Sequelize = require('sequelize');
const basename = path.basename(__filename);
const env = process.env.NODE_ENV || 'development';
const config = require(__dirname + '/../../config/sequelize.js')[env];
const db = {};
let sequelize;
if (config.use_env_variable) {
sequelize = new Sequelize(process.env[config.use_env_variable], config);
} else {
sequelize = new Sequelize(config.database, config.username, config.password, config);
if (env('CLI', false)) {
require('dotenv').config()
}
fs
.readdirSync(__dirname)
.filter(file => {
return (file.indexOf('.') !== 0) && (file !== basename) && (file.slice(-3) === '.js');
})
.forEach(file => {
const model = sequelize['import'](path.join(__dirname, file));
db[model.name] = model;
});
const basename = path.basename(__filename)
const node_env = process.env.NODE_ENV || 'development'
const conf = config('sequelize')[node_env]
const db = {}
const sequelize = new Sequelize(
config('database.database'),
config('database.username'),
config('database.password'),
conf
)
fs.list(__dirname)
.filter(file => (file[0] !== '.') && (file !== basename) && (file.slice(-3) === '.js'))
.forEach(file => {
const model = sequelize.import(path.join(__dirname, file))
db[model.name] = model
})
Object.keys(db).forEach(modelName => {
if (db[modelName].associate) {
db[modelName].associate(db);
}
});
const model = db[modelName]
// Support traditional Sequelize 'associate' static function
if (model.hasOwnProperty('associate')) {
model.associate(db)
}
// Support array based association composition
if (model.hasOwnProperty('relations')) {
model.relations.forEach(assoc => assoc(model, db))
}
})
db.sequelize = sequelize;
db.Sequelize = Sequelize;
db.sequelize = sequelize
db.Sequelize = Sequelize
module.exports = db;
module.exports = db
module.exports = DataTypes => ({
created_at: {
type: DataTypes.DATE,
validate: {
isDate: true,
},
},
updated_at: {
type: DataTypes.DATE,
validate: {
isDate: true,
},
},
deleted_at: {
type: DataTypes.DATE,
validate: {
isDate: true,
},
},
})
const OAuthServer = require('oauth2-server')
const { Sequelize: { Op }, User, OAuthClient, AccessToken, RefreshToken } = require('database/models')
const crypto = require('core/utils/crypto')
function createTokenPair(user, client, access, refresh = {}) {
return {
accessToken: access.token,
refreshToken: refresh.token,
accessTokenExpiresAt: access.expires_at,
refreshTokenExpiresAt: refresh.expires_at,
scope: access.scope,
client,
user,
}
}
function createClientEncryptedToken(client, user, scope) {
const payload = {
client_id: client.id,
user_id: user.id,
scope: scope,
}
if (client.secret == null) {
return crypto.encrypt(payload)
}
return crypto.encryptWith(Buffer.from(client.secret, 'hex'), payload)
}
const model = {
generateAccessToken: createClientEncryptedToken,
generateRefreshToken: createClientEncryptedToken,
generateAuthorizationCode: createClientEncryptedToken,
getClient: function getOauthClient(clientId, clientSecret) {
const where = {
id: { [Op.eq]: clientId },
}
if (clientSecret != null) {
where.secret = { [Op.eq]: clientSecret }
}
return OAuthClient.findOne({ where })
},
getAccessToken(token) {
return AccessToken.findOne({ include: [ { model: User }, { model: OAuthClient } ], where: { token: { [Op.eq]: token } } })
},
getRefreshToken(token) {
return RefreshToken.findOne({ include: [ { model: User }, { model: OAuthClient } ], where: { token: { [Op.eq]: token } } })
},
getUser: async function getAuthUser(email, password) {
const user = await User.findOne({
where: { email: { [Op.eq]: email } },
}, { })
if (user != null) {
const valid = await crypto.verify(user.password, password)
if (valid) {
return user
}
}
return false
},
saveToken: async function saveAccessToken(token, client, user) {
const accessToken = {
client_id: client.id,
user_id: user.id,
token: token.accessToken,
scope: token.scope || null,
}
if (token.accessTokenExpiresAt != null) {
accessToken.expires_at = token.accessTokenExpiresAt
}
const accessTokenModel = await AccessToken.create(accessToken)
let refreshTokenModel // = undefined
if (token.refreshToken != null) {
const refreshToken = {
client_id: client.id,
user_id: user.id,
token: token.refreshToken,
scope: token.scope || null,
}
if (token.refreshTokenExpiresAt != null) {
refreshToken.expires_at = token.refreshTokenExpiresAt
}
refreshTokenModel = await RefreshToken.create(refreshToken)
}
return createTokenPair(user, client, accessTokenModel, refreshTokenModel)
},
revokeToken: async function revokeRefreshToken({ refreshToken, client, user }) {
const deletions = await RefreshToken.destroy({
where: {
token: { [Op.eq]: refreshToken },
client_id: { [Op.eq]: client.id },
user_id: { [Op.eq]: user.id },
},
})
return deletions > 0
},
verifyScope(token, scope) {
if (!token.scope) {
return false
}
const requestedScopes = scope.split(' ')
const authorizedScopes = token.scopes
return requestedScopes.every(s => authorizedScopes.indexOf(s) >= 0)
},
}
class KoaOAuthServer {
constructor(authServer) {
this.getAuthServer = () => authServer
this.transformContext = ctx => {
const req = new OAuthServer.Request({
method: ctx.request.method,
query: ctx.request.query,
headers: ctx.request.headers,
body: ctx.request.body,
})
const res = new OAuthServer.Response()
return { req, res }
}
this.authenticate = async ctx => {
console.log(ctx.request.query)
const { req, res } = this.transformContext(ctx)
await authServer.authenticate(req, res)
ctx.response.headers = res.headers
ctx.response.body = res.body
}
this.token = async ctx => {
const { req, res } = this.transformContext(ctx)
await authServer.token(req, res, { allowExtendedTokenAttributes: true, accessTokenLifetime: 3600 * 24 * 7 })
ctx.response.headers = res.headers
ctx.response.body = res.body
}
}
}
module.exports = new KoaOAuthServer(new OAuthServer({ model }))
module.exports.KoaOauthServer = KoaOAuthServer
const Router = require('koa-router')
const controller = (name, method) => require(`./controllers/${ name }`)[method]
const AuthServer = require('domain/authentication/AuthServer')
const web = new Router()
web.get('/auth/authenticate', AuthServer.authenticate)
web.post('/auth/token', AuthServer.token)
// --- API ROUTES
const api = new Router({ prefix: '/api' })
......@@ -9,4 +15,5 @@ api.get('/', controller('api/site', 'hello'))
module.exports = {
api,
web,
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment