diff --git a/backend/src/auth/CustomJwtPayload.ts b/backend/src/auth/CustomJwtPayload.ts new file mode 100644 index 000000000..2b52c3cea --- /dev/null +++ b/backend/src/auth/CustomJwtPayload.ts @@ -0,0 +1,5 @@ +import { JwtPayload } from 'jsonwebtoken' + +export interface CustomJwtPayload extends JwtPayload { + pubKey: Buffer +} diff --git a/backend/src/auth/INALIENABLE_RIGHTS.ts b/backend/src/auth/INALIENABLE_RIGHTS.ts new file mode 100644 index 000000000..eb367d643 --- /dev/null +++ b/backend/src/auth/INALIENABLE_RIGHTS.ts @@ -0,0 +1,13 @@ +import { RIGHTS } from './RIGHTS' + +export const INALIENABLE_RIGHTS = [ + RIGHTS.LOGIN, + RIGHTS.GET_COMMUNITY_INFO, + RIGHTS.COMMUNITIES, + RIGHTS.LOGIN_VIA_EMAIL_VERIFICATION_CODE, + RIGHTS.CREATE_USER, + RIGHTS.SEND_RESET_PASSWORD_EMAIL, + RIGHTS.RESET_PASSWORD, + RIGHTS.CHECK_USERNAME, + RIGHTS.CHECK_EMAIL, +] diff --git a/backend/src/auth/RIGHTS.ts b/backend/src/auth/RIGHTS.ts new file mode 100644 index 000000000..08dca83b1 --- /dev/null +++ b/backend/src/auth/RIGHTS.ts @@ -0,0 +1,23 @@ +export enum RIGHTS { + LOGIN = 'LOGIN', + BALANCE = 'BALANCE', + GET_COMMUNITY_INFO = 'GET_COMMUNITY_INFO', + COMMUNITIES = 'COMMUNITIES', + LIST_GDT_ENTRIES = 'LIST_GDT_ENTRIES', + EXIST_PID = 'EXIST_PID', + GET_KLICKTIPP_USER = 'GET_KLICKTIPP_USER', + GET_KLICKTIPP_TAG_MAP = 'GET_KLICKTIPP_TAG_MAP', + UNSUBSCRIBE_NEWSLETTER = 'UNSUBSCRIBE_NEWSLETTER', + SUBSCRIBE_NEWSLETTER = 'SUBSCRIBE_NEWSLETTER', + TRANSACTION_LIST = 'TRANSACTION_LIST', + SEND_COINS = 'SEND_COINS', + LOGIN_VIA_EMAIL_VERIFICATION_CODE = 'LOGIN_VIA_EMAIL_VERIFICATION_CODE', + LOGOUT = 'LOGOUT', + CREATE_USER = 'CREATE_USER', + SEND_RESET_PASSWORD_EMAIL = 'SEND_RESET_PASSWORD_EMAIL', + RESET_PASSWORD = 'RESET_PASSWORD', + UPDATE_USER_INFOS = 'UPDATE_USER_INFOS', + CHECK_USERNAME = 'CHECK_USERNAME', + CHECK_EMAIL = 'CHECK_EMAIL', + HAS_ELOPAGE = 'HAS_ELOPAGE', +} diff --git a/backend/src/auth/ROLES.ts b/backend/src/auth/ROLES.ts new file mode 100644 index 000000000..3650ca7da --- /dev/null +++ b/backend/src/auth/ROLES.ts @@ -0,0 +1,24 @@ +import { INALIENABLE_RIGHTS } from './INALIENABLE_RIGHTS' +import { RIGHTS } from './RIGHTS' +import { Role } from './Role' + +// TODO from database +export const ROLES = [ + new Role('unauthorized', INALIENABLE_RIGHTS), // inalienable rights + new Role('user', [ + ...INALIENABLE_RIGHTS, + RIGHTS.BALANCE, + RIGHTS.LIST_GDT_ENTRIES, + RIGHTS.EXIST_PID, + RIGHTS.GET_KLICKTIPP_USER, + RIGHTS.GET_KLICKTIPP_TAG_MAP, + RIGHTS.UNSUBSCRIBE_NEWSLETTER, + RIGHTS.SUBSCRIBE_NEWSLETTER, + RIGHTS.TRANSACTION_LIST, + RIGHTS.SEND_COINS, + RIGHTS.LOGOUT, + RIGHTS.UPDATE_USER_INFOS, + RIGHTS.HAS_ELOPAGE, + ]), + new Role('admin', Object.values(RIGHTS)), // all rights +] diff --git a/backend/src/auth/Role.ts b/backend/src/auth/Role.ts new file mode 100644 index 000000000..8e2cc7deb --- /dev/null +++ b/backend/src/auth/Role.ts @@ -0,0 +1,11 @@ +import { RIGHTS } from './RIGHTS' + +export class Role { + id: string + rights: RIGHTS[] + + constructor(id: string, rights: RIGHTS[]) { + this.id = id + this.rights = rights + } +} diff --git a/backend/src/auth/hasRight.ts b/backend/src/auth/hasRight.ts new file mode 100644 index 000000000..3f736fb6f --- /dev/null +++ b/backend/src/auth/hasRight.ts @@ -0,0 +1,6 @@ +import { RIGHTS } from './RIGHTS' +import { Role } from './Role' + +export const hasRight = (right: RIGHTS, role: Role): boolean => { + return role.rights.includes(right) +} diff --git a/backend/src/graphql/directive/isAuthorized.ts b/backend/src/graphql/directive/isAuthorized.ts index 6245ef8ba..5303600bc 100644 --- a/backend/src/graphql/directive/isAuthorized.ts +++ b/backend/src/graphql/directive/isAuthorized.ts @@ -2,19 +2,36 @@ import { AuthChecker } from 'type-graphql' -import decode from '../../jwt/decode' -import encode from '../../jwt/encode' +import { decode, encode } from '../../auth/JWT' +import { ROLES } from '../../auth/ROLES' +import { hasRight } from '../../auth/hasRight' +import { RIGHTS } from '../../auth/RIGHTS' -const isAuthorized: AuthChecker = async ( - { /* root, args, */ context /*, info */ } /*, roles */, -) => { +const isAuthorized: AuthChecker = async ({ context }, rights) => { + context.role = ROLES[0] // unauthorized user + + // Do we have a token? if (context.token) { const decoded = decode(context.token) + if (!decoded) { + // we always throw on an invalid token + throw new Error('403.13 - Client certificate revoked') + } + // Set context pubKey context.pubKey = Buffer.from(decoded.pubKey).toString('hex') + // set new header token context.setHeaders.push({ key: 'token', value: encode(decoded.pubKey) }) - return true + // TODO - load from database dynamically & admin - maybe encode this in the token to prevent many database requests + context.role = ROLES[1] // logged in user } - throw new Error('401 Unauthorized') + + // check for correct rights + const missingRights = (rights).filter((right) => !hasRight(right, context.role)) + if (missingRights.length !== 0) { + throw new Error('401 Unauthorized') + } + + return true } export default isAuthorized