Merge pull request #1825 from automatisch/access-tokens

feat: Use persisted access tokens for authentication
This commit is contained in:
Ali BARIN
2024-04-24 16:39:57 +02:00
committed by GitHub
50 changed files with 143 additions and 57 deletions

View File

@@ -22,7 +22,7 @@ describe('GET /api/v1/admin/apps/:appKey/auth-clients/:appAuthClientId', () => {
appKey: 'deepl',
});
token = createAuthTokenByUserId(currentUser.id);
token = await createAuthTokenByUserId(currentUser.id);
});
it('should return specified app auth client', async () => {

View File

@@ -17,7 +17,7 @@ describe('GET /api/v1/admin/apps/:appKey/auth-clients', () => {
adminRole = await createRole({ key: 'admin' });
currentUser = await createUser({ roleId: adminRole.id });
token = createAuthTokenByUserId(currentUser.id);
token = await createAuthTokenByUserId(currentUser.id);
});
it('should return specified app auth client info', async () => {

View File

@@ -14,7 +14,7 @@ describe('GET /api/v1/admin/permissions/catalog', () => {
role = await createRole({ key: 'admin' });
currentUser = await createUser({ roleId: role.id });
token = createAuthTokenByUserId(currentUser.id);
token = await createAuthTokenByUserId(currentUser.id);
});
it('should return roles', async () => {

View File

@@ -18,7 +18,7 @@ describe('GET /api/v1/admin/roles/:roleId', () => {
permissionTwo = await createPermission({ roleId: role.id });
currentUser = await createUser({ roleId: role.id });
token = createAuthTokenByUserId(currentUser.id);
token = await createAuthTokenByUserId(currentUser.id);
});
it('should return role', async () => {

View File

@@ -15,7 +15,7 @@ describe('GET /api/v1/admin/roles', () => {
roleTwo = await createRole({ key: 'user' });
currentUser = await createUser({ roleId: roleOne.id });
token = createAuthTokenByUserId(currentUser.id);
token = await createAuthTokenByUserId(currentUser.id);
});
it('should return roles', async () => {

View File

@@ -28,7 +28,7 @@ describe('GET /api/v1/admin/saml-auth-providers/:samlAuthProviderId/role-mapping
remoteRoleName: 'User',
});
token = createAuthTokenByUserId(currentUser.id);
token = await createAuthTokenByUserId(currentUser.id);
});
it('should return role mappings', async () => {

View File

@@ -17,7 +17,7 @@ describe('GET /api/v1/admin/saml-auth-provider/:samlAuthProviderId', () => {
currentUser = await createUser({ roleId: role.id });
samlAuthProvider = await createSamlAuthProvider();
token = createAuthTokenByUserId(currentUser.id);
token = await createAuthTokenByUserId(currentUser.id);
});
it('should return saml auth provider with specified id', async () => {

View File

@@ -18,7 +18,7 @@ describe('GET /api/v1/admin/saml-auth-providers', () => {
samlAuthProviderOne = await createSamlAuthProvider();
samlAuthProviderTwo = await createSamlAuthProvider();
token = createAuthTokenByUserId(currentUser.id);
token = await createAuthTokenByUserId(currentUser.id);
});
it('should return saml auth providers', async () => {

View File

@@ -18,7 +18,7 @@ describe('GET /api/v1/admin/users/:userId', () => {
anotherUser = await createUser();
anotherUserRole = await anotherUser.$relatedQuery('role');
token = createAuthTokenByUserId(currentUser.id);
token = await createAuthTokenByUserId(currentUser.id);
});
it('should return specified user info', async () => {

View File

@@ -28,7 +28,7 @@ describe('GET /api/v1/admin/users', () => {
fullName: 'Another User',
});
token = createAuthTokenByUserId(currentUser.id);
token = await createAuthTokenByUserId(currentUser.id);
});
it('should return users data', async () => {

View File

@@ -11,7 +11,7 @@ describe('GET /api/v1/apps/:appKey/actions/:actionKey/substeps', () => {
beforeEach(async () => {
currentUser = await createUser();
token = createAuthTokenByUserId(currentUser.id);
token = await createAuthTokenByUserId(currentUser.id);
exampleApp = await App.findOneByKey('github');
});

View File

@@ -11,7 +11,7 @@ describe('GET /api/v1/apps/:appKey/actions', () => {
beforeEach(async () => {
currentUser = await createUser();
token = createAuthTokenByUserId(currentUser.id);
token = await createAuthTokenByUserId(currentUser.id);
});
it('should return the app actions', async () => {

View File

@@ -11,7 +11,7 @@ describe('GET /api/v1/apps/:appKey', () => {
beforeEach(async () => {
currentUser = await createUser();
token = createAuthTokenByUserId(currentUser.id);
token = await createAuthTokenByUserId(currentUser.id);
});
it('should return the app info', async () => {

View File

@@ -11,7 +11,7 @@ describe('GET /api/v1/apps', () => {
beforeEach(async () => {
currentUser = await createUser();
token = createAuthTokenByUserId(currentUser.id);
token = await createAuthTokenByUserId(currentUser.id);
apps = await App.findAll();
});

View File

@@ -19,7 +19,7 @@ describe('GET /api/v1/apps/:appKey/auth-clients/:appAuthClientId', () => {
appKey: 'deepl',
});
token = createAuthTokenByUserId(currentUser.id);
token = await createAuthTokenByUserId(currentUser.id);
});
it('should return specified app auth client', async () => {

View File

@@ -15,7 +15,7 @@ describe('GET /api/v1/apps/:appKey/auth-clients', () => {
currentUser = await createUser();
token = createAuthTokenByUserId(currentUser.id);
token = await createAuthTokenByUserId(currentUser.id);
});
it('should return specified app auth client info', async () => {

View File

@@ -11,7 +11,7 @@ describe('GET /api/v1/apps/:appKey/auth', () => {
beforeEach(async () => {
currentUser = await createUser();
token = createAuthTokenByUserId(currentUser.id);
token = await createAuthTokenByUserId(currentUser.id);
});
it('should return the app auth info', async () => {

View File

@@ -22,7 +22,7 @@ describe('GET /api/v1/apps/:appKey/config', () => {
disabled: false,
});
token = createAuthTokenByUserId(currentUser.id);
token = await createAuthTokenByUserId(currentUser.id);
});
it('should return specified app config info', async () => {

View File

@@ -14,7 +14,7 @@ describe('GET /api/v1/apps/:appKey/connections', () => {
currentUser = await createUser();
currentUserRole = await currentUser.$relatedQuery('role');
token = createAuthTokenByUserId(currentUser.id);
token = await createAuthTokenByUserId(currentUser.id);
});
it('should return the connections data of specified app for current user', async () => {

View File

@@ -15,7 +15,7 @@ describe('GET /api/v1/apps/:appKey/flows', () => {
currentUser = await createUser();
currentUserRole = await currentUser.$relatedQuery('role');
token = createAuthTokenByUserId(currentUser.id);
token = await createAuthTokenByUserId(currentUser.id);
});
it('should return the flows data of specified app for current user', async () => {

View File

@@ -11,7 +11,7 @@ describe('GET /api/v1/apps/:appKey/triggers/:triggerKey/substeps', () => {
beforeEach(async () => {
currentUser = await createUser();
token = createAuthTokenByUserId(currentUser.id);
token = await createAuthTokenByUserId(currentUser.id);
exampleApp = await App.findOneByKey('github');
});

View File

@@ -11,7 +11,7 @@ describe('GET /api/v1/apps/:appKey/triggers', () => {
beforeEach(async () => {
currentUser = await createUser();
token = createAuthTokenByUserId(currentUser.id);
token = await createAuthTokenByUserId(currentUser.id);
});
it('should return the app triggers', async () => {

View File

@@ -14,7 +14,7 @@ describe('POST /api/v1/connections/:connectionId/test', () => {
currentUser = await createUser();
currentUserRole = await currentUser.$relatedQuery('role');
token = createAuthTokenByUserId(currentUser.id);
token = await createAuthTokenByUserId(currentUser.id);
});
it('should update the connection as not verified for current user', async () => {

View File

@@ -16,7 +16,7 @@ describe('GET /api/v1/connections/:connectionId/flows', () => {
currentUser = await createUser();
currentUserRole = await currentUser.$relatedQuery('role');
token = createAuthTokenByUserId(currentUser.id);
token = await createAuthTokenByUserId(currentUser.id);
});
it('should return the flows data of specified connection for current user', async () => {

View File

@@ -20,7 +20,7 @@ describe('GET /api/v1/executions/:executionId/execution-steps', () => {
anotherUser = await createUser();
token = createAuthTokenByUserId(currentUser.id);
token = await createAuthTokenByUserId(currentUser.id);
});
it('should return the execution steps of current user execution', async () => {

View File

@@ -17,7 +17,7 @@ describe('GET /api/v1/executions/:executionId', () => {
currentUser = await createUser();
currentUserRole = await currentUser.$relatedQuery('role');
token = createAuthTokenByUserId(currentUser.id);
token = await createAuthTokenByUserId(currentUser.id);
});
it('should return the execution data of current user', async () => {

View File

@@ -18,7 +18,7 @@ describe('GET /api/v1/executions', () => {
anotherUser = await createUser();
token = createAuthTokenByUserId(currentUser.id);
token = await createAuthTokenByUserId(currentUser.id);
});
it('should return the executions of current user', async () => {

View File

@@ -16,7 +16,7 @@ describe('GET /api/v1/flows/:flowId', () => {
currentUser = await createUser();
currentUserRole = await currentUser.$relatedQuery('role');
token = createAuthTokenByUserId(currentUser.id);
token = await createAuthTokenByUserId(currentUser.id);
});
it('should return the flow data of current user', async () => {

View File

@@ -15,7 +15,7 @@ describe('GET /api/v1/flows', () => {
currentUser = await createUser();
currentUserRole = await currentUser.$relatedQuery('role');
token = createAuthTokenByUserId(currentUser.id);
token = await createAuthTokenByUserId(currentUser.id);
});
it('should return the flows data of current user', async () => {

View File

@@ -12,7 +12,7 @@ describe('GET /api/v1/payment/paddle-info', () => {
beforeEach(async () => {
user = await createUser();
token = createAuthTokenByUserId(user.id);
token = await createAuthTokenByUserId(user.id);
vi.spyOn(appConfig, 'isCloud', 'get').mockReturnValue(true);
vi.spyOn(billing.paddleInfo, 'vendorId', 'get').mockReturnValue(

View File

@@ -11,7 +11,7 @@ describe('GET /api/v1/payment/plans', () => {
beforeEach(async () => {
user = await createUser();
token = createAuthTokenByUserId(user.id);
token = await createAuthTokenByUserId(user.id);
vi.spyOn(appConfig, 'isCloud', 'get').mockReturnValue(true);
});

View File

@@ -18,7 +18,7 @@ describe('POST /api/v1/steps/:stepId/dynamic-data', () => {
currentUser = await createUser();
currentUserRole = await currentUser.$relatedQuery('role');
token = createAuthTokenByUserId(currentUser.id);
token = await createAuthTokenByUserId(currentUser.id);
});
describe('should return dynamically created data', () => {

View File

@@ -16,7 +16,7 @@ describe('POST /api/v1/steps/:stepId/dynamic-fields', () => {
currentUser = await createUser();
currentUserRole = await currentUser.$relatedQuery('role');
token = createAuthTokenByUserId(currentUser.id);
token = await createAuthTokenByUserId(currentUser.id);
});
it('should return dynamically created fields of the current users step', async () => {

View File

@@ -17,7 +17,7 @@ describe('GET /api/v1/steps/:stepId/connection', () => {
currentUser = await createUser();
currentUserRole = await currentUser.$relatedQuery('role');
token = createAuthTokenByUserId(currentUser.id);
token = await createAuthTokenByUserId(currentUser.id);
});
it('should return the current user connection data of specified step', async () => {

View File

@@ -17,7 +17,7 @@ describe('GET /api/v1/steps/:stepId/previous-steps', () => {
currentUser = await createUser();
currentUserRole = await currentUser.$relatedQuery('role');
token = createAuthTokenByUserId(currentUser.id);
token = await createAuthTokenByUserId(currentUser.id);
});
it('should return the previous steps of the specified step of the current user', async () => {

View File

@@ -17,7 +17,7 @@ describe('GET /api/v1/users/:userId/apps', () => {
currentUserRole = await createRole();
currentUser = await createUser({ roleId: currentUserRole.id });
token = createAuthTokenByUserId(currentUser.id);
token = await createAuthTokenByUserId(currentUser.id);
});
it('should return all apps of the current user', async () => {

View File

@@ -25,7 +25,7 @@ describe('GET /api/v1/users/me', () => {
roleId: role.id,
});
token = createAuthTokenByUserId(currentUser.id);
token = await createAuthTokenByUserId(currentUser.id);
});
it('should return current user info', async () => {

View File

@@ -11,7 +11,7 @@ describe('GET /api/v1/user/invoices', () => {
beforeEach(async () => {
currentUser = await createUser();
token = createAuthTokenByUserId(currentUser.id);
token = await createAuthTokenByUserId(currentUser.id);
});
it('should return current user invoices', async () => {

View File

@@ -14,7 +14,7 @@ describe('GET /api/v1/users/:userId/plan-and-usage', () => {
beforeEach(async () => {
const trialExpiryDate = DateTime.now().plus({ days: 30 }).toISODate();
user = await createUser({ trialExpiryDate });
token = createAuthTokenByUserId(user.id);
token = await createAuthTokenByUserId(user.id);
vi.spyOn(appConfig, 'isCloud', 'get').mockReturnValue(true);
});

View File

@@ -22,7 +22,7 @@ describe('GET /api/v1/users/:userId/subscription', () => {
subscription = await createSubscription({ userId: currentUser.id });
token = createAuthTokenByUserId(currentUser.id);
token = await createAuthTokenByUserId(currentUser.id);
});
it('should return subscription info of the current user', async () => {
@@ -41,7 +41,7 @@ describe('GET /api/v1/users/:userId/subscription', () => {
roleId: role.id,
});
const token = createAuthTokenByUserId(userWithoutSubscription.id);
const token = await createAuthTokenByUserId(userWithoutSubscription.id);
await request(app)
.get(`/api/v1/users/${userWithoutSubscription.id}/subscription`)

View File

@@ -14,7 +14,7 @@ describe('GET /api/v1/users/:userId/trial', () => {
beforeEach(async () => {
const trialExpiryDate = DateTime.now().plus({ days: 30 }).toISODate();
user = await createUser({ trialExpiryDate });
token = createAuthTokenByUserId(user.id);
token = await createAuthTokenByUserId(user.id);
vi.spyOn(appConfig, 'isCloud', 'get').mockReturnValue(true);
});

View File

@@ -0,0 +1,15 @@
export async function up(knex) {
return knex.schema.createTable('access_tokens', (table) => {
table.uuid('id').primary().defaultTo(knex.raw('gen_random_uuid()'));
table.string('token').notNullable();
table.integer('expires_in').notNullable();
table.timestamp('revoked_at').nullable();
table.uuid('user_id').references('id').inTable('users');
table.timestamps(true, true);
});
}
export async function down(knex) {
return knex.schema.dropTable('access_tokens');
}

View File

@@ -0,0 +1,13 @@
export async function up(knex) {
return knex.schema.table('access_tokens', (table) => {
table.index('token');
table.index('user_id');
});
}
export async function down(knex) {
return knex.schema.table('access_tokens', (table) => {
table.dropIndex('token');
table.dropIndex('user_id');
});
}

View File

@@ -7,7 +7,7 @@ const login = async (_parent, params) => {
});
if (user && (await user.login(params.input.password))) {
const token = createAuthTokenByUserId(user.id);
const token = await createAuthTokenByUserId(user.id);
return { token, user };
}

View File

@@ -1,7 +1,6 @@
import { allow, rule, shield } from 'graphql-shield';
import jwt from 'jsonwebtoken';
import appConfig from '../config/app.js';
import User from '../models/user.js';
import AccessToken from '../models/access-token.js';
export const isAuthenticated = async (_parent, _args, req) => {
const token = req.headers['authorization'];
@@ -9,10 +8,22 @@ export const isAuthenticated = async (_parent, _args, req) => {
if (token == null) return false;
try {
const { userId } = jwt.verify(token, appConfig.appSecretKey);
const accessToken = await AccessToken.query().findOne({
token,
revoked_at: null,
});
const expirationTime =
new Date(accessToken.createdAt).getTime() + accessToken.expiresIn * 1000;
if (Date.now() > expirationTime) {
return false;
}
const user = await accessToken.$relatedQuery('user');
req.currentUser = await User.query()
.findById(userId)
.findById(user.id)
.leftJoinRelated({
role: true,
permissions: true,

View File

@@ -17,7 +17,7 @@ describe('isAuthenticated', () => {
it('should return true if token is valid and there is a user', async () => {
const user = await createUser();
const token = createAuthTokenByUserId(user.id);
const token = await createAuthTokenByUserId(user.id);
const req = { headers: { authorization: token } };
expect(await isAuthenticated(null, null, req)).toBe(true);
@@ -25,7 +25,7 @@ describe('isAuthenticated', () => {
it('should return false if token is valid and but there is no user', async () => {
const user = await createUser();
const token = createAuthTokenByUserId(user.id);
const token = await createAuthTokenByUserId(user.id);
await user.$query().delete();
const req = { headers: { authorization: token } };

View File

@@ -1,10 +1,16 @@
import jwt from 'jsonwebtoken';
import appConfig from '../config/app.js';
import crypto from 'crypto';
import User from '../models/user.js';
import AccessToken from '../models/access-token.js';
const TOKEN_EXPIRES_IN = '14d';
const TOKEN_EXPIRES_IN = 14 * 24 * 60 * 60; // 14 days in seconds
const createAuthTokenByUserId = (userId) => {
const token = jwt.sign({ userId }, appConfig.appSecretKey, {
const createAuthTokenByUserId = async (userId) => {
const user = await User.query().findById(userId).throwIfNotFound();
const token = await crypto.randomBytes(48).toString('hex');
await AccessToken.query().insert({
token,
userId: user.id,
expiresIn: TOKEN_EXPIRES_IN,
});

View File

@@ -76,8 +76,8 @@ export default function configurePassport(app) {
failureRedirect: '/',
failureFlash: true,
}),
(req, res) => {
const token = createAuthTokenByUserId(req.currentUser.id);
async (req, res) => {
const token = await createAuthTokenByUserId(req.currentUser.id);
const redirectUrl = new URL(
`/login/callback?token=${token}`,

View File

@@ -0,0 +1,32 @@
import Base from './base.js';
import User from './user.js';
class AccessToken extends Base {
static tableName = 'access_tokens';
static jsonSchema = {
type: 'object',
required: ['token', 'expiresIn'],
properties: {
id: { type: 'string', format: 'uuid' },
userId: { type: 'string', format: 'uuid' },
token: { type: 'string', minLength: 32 },
expiresIn: { type: 'integer' },
revokedAt: { type: ['string', 'null'], format: 'date-time' },
},
};
static relationMappings = () => ({
user: {
relation: Base.BelongsToOneRelation,
modelClass: User,
join: {
from: 'access_tokens.user_id',
to: 'users.id',
},
},
});
}
export default AccessToken;

View File

@@ -8,6 +8,7 @@ import userAbility from '../helpers/user-ability.js';
import createAuthTokenByUserId from '../helpers/create-auth-token-by-user-id.js';
import Base from './base.js';
import App from './app.js';
import AccessToken from './access-token.js';
import Connection from './connection.js';
import Execution from './execution.js';
import Flow from './flow.js';
@@ -42,6 +43,14 @@ class User extends Base {
};
static relationMappings = () => ({
accessTokens: {
relation: Base.HasManyRelation,
modelClass: AccessToken,
join: {
from: 'users.id',
to: 'access_tokens.user_id',
},
},
connections: {
relation: Base.HasManyRelation,
modelClass: Connection,
@@ -176,7 +185,7 @@ class User extends Base {
});
if (user && (await user.login(password))) {
const token = createAuthTokenByUserId(user.id);
const token = await createAuthTokenByUserId(user.id);
return token;
}
}