Compare commits

...

42 Commits

Author SHA1 Message Date
993096741d Merge branch 'dev' of https://git.odit.services/lfk/backend into dev 2020-12-05 18:00:48 +01:00
8607af62b5 Merge branch 'feature/12-jwt-creation' of https://git.odit.services/lfk/backend into feature/12-jwt-creation 2020-12-05 17:59:50 +01:00
76e19ca28d implement proper jwt checking in authchecker
ref #12
2020-12-05 17:59:43 +01:00
3ac150331a Merge branch 'feature/12-jwt-creation' of git.odit.services:lfk/backend into feature/12-jwt-creation 2020-12-05 17:47:35 +01:00
5a4a6cdcef Added basic openapi security scheme for the bearer auth header
ref #12
2020-12-05 17:47:32 +01:00
e5b605cc55 🧹 cleanups 2020-12-05 17:25:57 +01:00
7e4ce00c30 added await (async stuff und so)
ref #12
2020-12-05 17:20:39 +01:00
13d568ba3f implemented refreshcount increase
ref #12
2020-12-05 17:20:18 +01:00
8c229dba82 add response schemas to AuthController 2020-12-05 13:40:59 +01:00
675717f8ca 🚧 starting work on LogoutHandler
ref #12
2020-12-05 13:38:59 +01:00
0d21497c2f 🚧 AuthController - add proper response schemas 2020-12-05 13:31:46 +01:00
e5f65d0b80 note on refreshtokencount checking
ref #12
2020-12-05 13:30:22 +01:00
51addd4a31 🚧 RefreshAuth - refresh tokens now working
ref #12
2020-12-05 13:12:47 +01:00
126799dab9 basic RefreshAuth checking
ref #12
2020-12-05 13:07:33 +01:00
82f31185a1 🚧 CreateAuth - use proper refreshTokenCount
ref #12
2020-12-05 13:07:18 +01:00
c0c95056bf better errors
ref #12
2020-12-05 13:06:58 +01:00
093f6f5f78 🚧 UserNotFoundOrRefreshTokenCountInvalidError
ref #12
2020-12-05 12:59:02 +01:00
2f902755c4 🚧 starting work on RefreshAuth
ref #12
2020-12-05 12:55:38 +01:00
a0fe8c0017 🚧 CreateAuth - basic jwt creation with user details
ref #12
2020-12-05 12:34:07 +01:00
c33097f773 first accesstoken generation
ref #12
2020-12-05 12:28:59 +01:00
28c2b862f0 🚧 AuthController with multiple endpoints
ref #12
2020-12-05 12:28:43 +01:00
d23ed002b2 🚧 JwtNotProvidedError
ref #12
2020-12-05 12:28:06 +01:00
1850dd542d 🧹 clean up CreateAuth
ref #12
2020-12-05 11:22:59 +01:00
2a1b65f424 🚧AuthController - add all Error response schemas to post
ref #12
2020-12-05 11:22:45 +01:00
bd0c7ce042 🚧 CreateAuth - credential validation
ref #12
2020-12-05 11:18:12 +01:00
d46ad59546 🚧 CreateAuth now returns a sample jwt
ref #12
2020-12-05 11:14:26 +01:00
b8bc39d691 🚧 User - mark columns as unique
ref #11 #12
2020-12-05 11:14:06 +01:00
52dfe83354 Merge branch 'dev' into feature/12-jwt-creation 2020-12-05 11:07:01 +01:00
6ae0c1b955 first jwt generation
ref #12
2020-12-04 23:03:24 +01:00
6244c969af integrate UserNotFoundError
ref #12
2020-12-04 23:03:10 +01:00
d803704eee UserNotFoundError
ref #12
2020-12-04 23:02:23 +01:00
a5b1804e19 Merge branch 'dev' into feature/12-jwt-creation 2020-12-04 22:51:57 +01:00
3e38bc5950 Merge branch 'dev' into feature/12-jwt-creation 2020-12-04 22:48:57 +01:00
92cd58e641 Merge branch 'dev' into feature/12-jwt-creation 2020-12-04 22:45:54 +01:00
6cb01090d0 working on AuthController + CreateAuth
ref #12
2020-12-04 22:43:41 +01:00
c4b7ece974 class-validator on Auth model
ref #12
2020-12-04 22:34:03 +01:00
c5c3058f3d clean up jwtauth
ref #12
2020-12-04 22:28:17 +01:00
a7afcf4cd1 CreateAuth model
ref #12
2020-12-04 22:19:55 +01:00
f251b7acdb authchecker - use new custom Errors
ref #12
2020-12-04 22:18:54 +01:00
b0a24c6a74 basic Auth model
ref #12
2020-12-04 22:18:40 +01:00
b9bbdee826 🚧 basic AuthErrors 🔒
ref #12
2020-12-04 22:17:03 +01:00
1f3b312675 🚧 basic JWTAuth Middleware
ref #12
2020-12-04 21:39:55 +01:00
12 changed files with 405 additions and 33 deletions

View File

@ -1,9 +1,9 @@
import "reflect-metadata";
import * as dotenvSafe from "dotenv-safe";
import { createExpressServer } from "routing-controllers";
import consola from "consola"; import consola from "consola";
import loaders from "./loaders/index"; import * as dotenvSafe from "dotenv-safe";
import "reflect-metadata";
import { createExpressServer } from "routing-controllers";
import authchecker from "./authchecker"; import authchecker from "./authchecker";
import loaders from "./loaders/index";
import { ErrorHandler } from './middlewares/ErrorHandler'; import { ErrorHandler } from './middlewares/ErrorHandler';
dotenvSafe.config(); dotenvSafe.config();

View File

@ -1,12 +1,15 @@
import * as jwt from "jsonwebtoken"; import * as jwt from "jsonwebtoken";
import { Action, HttpError } from "routing-controllers"; import { Action } from "routing-controllers";
import { getConnectionManager } from 'typeorm';
import { IllegalJWTError, NoPermissionError, UserNonexistantOrRefreshtokenInvalidError } from './errors/AuthError';
import { User } from './models/entities/User';
// ----------- // -----------
const sampletoken = jwt.sign({ const sampletoken = jwt.sign({
"permissions": { "permissions": {
"TRACKS": ["read", "update", "delete", "add"] "TRACKS": ["read", "update", "delete", "add"]
// "TRACKS": [] // "TRACKS": []
} }
}, process.env.JWT_SECRET || "secretjwtsecret") }, "securekey")
console.log(`sampletoken: ${sampletoken}`); console.log(`sampletoken: ${sampletoken}`);
// ----------- // -----------
const authchecker = async (action: Action, permissions: string | string[]) => { const authchecker = async (action: Action, permissions: string | string[]) => {
@ -20,9 +23,14 @@ const authchecker = async (action: Action, permissions: string | string[]) => {
const provided_token = action.request.query["auth"]; const provided_token = action.request.query["auth"];
let jwtPayload = undefined let jwtPayload = undefined
try { try {
jwtPayload = <any>jwt.verify(provided_token, process.env.JWT_SECRET || "secretjwtsecret"); jwtPayload = <any>jwt.verify(provided_token, "securekey");
} catch (error) { } catch (error) {
throw new HttpError(401, "jwt_illegal") console.log(error);
throw new IllegalJWTError()
}
const count = await getConnectionManager().get().getRepository(User).count({ id: jwtPayload["userdetails"]["id"], refreshTokenCount: jwtPayload["userdetails"]["refreshTokenCount"] })
if (count !== 1) {
throw new UserNonexistantOrRefreshtokenInvalidError()
} }
if (jwtPayload.permissions) { if (jwtPayload.permissions) {
action.response.local = {} action.response.local = {}
@ -34,11 +42,11 @@ const authchecker = async (action: Action, permissions: string | string[]) => {
if (actual_accesslevel_for_permission.includes(permission_access_level)) { if (actual_accesslevel_for_permission.includes(permission_access_level)) {
return true; return true;
} else { } else {
throw new HttpError(403, "no") throw new NoPermissionError()
} }
}); });
} else { } else {
throw new HttpError(403, "no") throw new NoPermissionError()
} }
// //
try { try {

View File

@ -0,0 +1,71 @@
import { Body, JsonController, Post } from 'routing-controllers';
import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi';
import { IllegalJWTError, InvalidCredentialsError, JwtNotProvidedError, PasswordNeededError, RefreshTokenCountInvalidError, UsernameOrEmailNeededError } from '../errors/AuthError';
import { UserNotFoundError } from '../errors/UserErrors';
import { CreateAuth } from '../models/creation/CreateAuth';
import { HandleLogout } from '../models/creation/HandleLogout';
import { RefreshAuth } from '../models/creation/RefreshAuth';
import { Auth } from '../models/responses/Auth';
import { Logout } from '../models/responses/Logout';
@JsonController('/auth')
export class AuthController {
constructor() {
}
@Post("/login")
@ResponseSchema(Auth)
@ResponseSchema(InvalidCredentialsError)
@ResponseSchema(UserNotFoundError)
@ResponseSchema(UsernameOrEmailNeededError)
@ResponseSchema(PasswordNeededError)
@ResponseSchema(InvalidCredentialsError)
@OpenAPI({ description: 'Create a new access token object' })
async login(@Body({ validate: true }) createAuth: CreateAuth) {
let auth;
try {
auth = await createAuth.toAuth();
console.log(auth);
} catch (error) {
return error;
}
return auth
}
@Post("/logout")
@ResponseSchema(Logout)
@ResponseSchema(InvalidCredentialsError)
@ResponseSchema(UserNotFoundError)
@ResponseSchema(UsernameOrEmailNeededError)
@ResponseSchema(PasswordNeededError)
@ResponseSchema(InvalidCredentialsError)
@OpenAPI({ description: 'Create a new access token object' })
async logout(@Body({ validate: true }) handleLogout: HandleLogout) {
let logout;
try {
logout = await handleLogout.logout()
console.log(logout);
} catch (error) {
return error;
}
return logout
}
@Post("/refresh")
@ResponseSchema(Auth)
@ResponseSchema(JwtNotProvidedError)
@ResponseSchema(IllegalJWTError)
@ResponseSchema(UserNotFoundError)
@ResponseSchema(RefreshTokenCountInvalidError)
@OpenAPI({ description: 'refresh a access token' })
async refresh(@Body({ validate: true }) refreshAuth: RefreshAuth) {
let auth;
try {
auth = await refreshAuth.toAuth();
console.log(auth);
} catch (error) {
return error;
}
return auth
}
}

123
src/errors/AuthError.ts Normal file
View File

@ -0,0 +1,123 @@
import { IsString } from 'class-validator';
import { ForbiddenError, NotAcceptableError, NotFoundError, UnauthorizedError } from 'routing-controllers';
/**
* Error to throw when a jwt is expired
*/
export class ExpiredJWTError extends UnauthorizedError {
@IsString()
name = "ExpiredJWTError"
@IsString()
message = "your provided jwt is expired"
}
/**
* Error to throw when a jwt could not be parsed
*/
export class IllegalJWTError extends UnauthorizedError {
@IsString()
name = "IllegalJWTError"
@IsString()
message = "your provided jwt could not be parsed"
}
/**
* Error to throw when user is nonexistant or refreshtoken is invalid
*/
export class UserNonexistantOrRefreshtokenInvalidError extends UnauthorizedError {
@IsString()
name = "UserNonexistantOrRefreshtokenInvalidError"
@IsString()
message = "user is nonexistant or refreshtoken is invalid"
}
/**
* Error to throw when provided credentials are invalid
*/
export class InvalidCredentialsError extends UnauthorizedError {
@IsString()
name = "InvalidCredentialsError"
@IsString()
message = "your provided credentials are invalid"
}
/**
* Error to throw when a jwt does not have permission for this route/ action
*/
export class NoPermissionError extends ForbiddenError {
@IsString()
name = "NoPermissionError"
@IsString()
message = "your provided jwt does not have permission for this route/ action"
}
/**
* Error to thow when no username and no email is set
*/
export class UsernameOrEmailNeededError extends NotAcceptableError {
@IsString()
name = "UsernameOrEmailNeededError"
@IsString()
message = "Auth needs to have email or username set! \n You provided neither."
}
/**
* Error to thow when no password is provided
*/
export class PasswordNeededError extends NotAcceptableError {
@IsString()
name = "PasswordNeededError"
@IsString()
message = "no password is provided - you need to provide it"
}
/**
* Error to thow when no user could be found for provided credential
*/
export class UserNotFoundError extends NotFoundError {
@IsString()
name = "UserNotFoundError"
@IsString()
message = "no user could be found for provided credential"
}
/**
* Error to thow when no jwt token was provided
*/
export class JwtNotProvidedError extends NotAcceptableError {
@IsString()
name = "JwtNotProvidedError"
@IsString()
message = "no jwt token was provided"
}
/**
* Error to thow when user was not found or refresh token count was invalid
*/
export class UserNotFoundOrRefreshTokenCountInvalidError extends NotAcceptableError {
@IsString()
name = "UserNotFoundOrRefreshTokenCountInvalidError"
@IsString()
message = "user was not found or refresh token count was invalid"
}
/**
* Error to thow when refresh token count was invalid
*/
export class RefreshTokenCountInvalidError extends NotAcceptableError {
@IsString()
name = "RefreshTokenCountInvalidError"
@IsString()
message = "refresh token count was invalid"
}

View File

@ -1,8 +1,8 @@
import { validationMetadatasToSchemas } from "class-validator-jsonschema";
import { Application } from "express"; import { Application } from "express";
import * as swaggerUiExpress from "swagger-ui-express";
import { getMetadataArgsStorage } from "routing-controllers"; import { getMetadataArgsStorage } from "routing-controllers";
import { routingControllersToSpec } from "routing-controllers-openapi"; import { routingControllersToSpec } from "routing-controllers-openapi";
import { validationMetadatasToSchemas } from "class-validator-jsonschema"; import * as swaggerUiExpress from "swagger-ui-express";
export default async (app: Application) => { export default async (app: Application) => {
const storage = getMetadataArgsStorage(); const storage = getMetadataArgsStorage();
@ -17,6 +17,13 @@ export default async (app: Application) => {
{ {
components: { components: {
schemas, schemas,
"securitySchemes": {
"AuthToken": {
"type": "http",
"scheme": "bearer",
"bearerFormat": "JWT"
}
}
}, },
info: { info: {
description: "The the backend API for the LfK! runner system.", description: "The the backend API for the LfK! runner system.",

View File

@ -1,17 +0,0 @@
import { Request, Response, NextFunction } from "express";
// import bodyParser from 'body-parser';
// import cors from 'cors';
import * as jwt from "jsonwebtoken";
export default (req: Request, res: Response, next: NextFunction) => {
const token = <string>req.headers["auth"];
try {
const jwtPayload = <any>jwt.verify(token, "secretjwtsecret");
// const jwtPayload = <any>jwt.verify(token, process.env.JWT_SECRET);
res.locals.jwtPayload = jwtPayload;
} catch (error) {
console.log(error);
return res.status(401).send();
}
next();
};

View File

@ -0,0 +1,57 @@
import * as argon2 from "argon2";
import { IsEmail, IsOptional, IsString } from 'class-validator';
import * as jsonwebtoken from 'jsonwebtoken';
import { getConnectionManager } from 'typeorm';
import { InvalidCredentialsError, PasswordNeededError, UserNotFoundError } from '../../errors/AuthError';
import { UsernameOrEmailNeededError } from '../../errors/UserErrors';
import { User } from '../entities/User';
import { Auth } from '../responses/Auth';
export class CreateAuth {
@IsOptional()
@IsString()
username?: string;
@IsString()
password: string;
@IsOptional()
@IsEmail()
@IsString()
email?: string;
public async toAuth(): Promise<Auth> {
let newAuth: Auth = new Auth();
if (this.email === undefined && this.username === undefined) {
throw new UsernameOrEmailNeededError();
}
if (!this.password) {
throw new PasswordNeededError()
}
const found_users = await getConnectionManager().get().getRepository(User).find({ where: [{ username: this.username }, { email: this.email }] });
if (found_users.length === 0) {
throw new UserNotFoundError()
} else {
const found_user = found_users[0]
if (await argon2.verify(found_user.password, this.password + found_user.uuid)) {
const timestamp_accesstoken_expiry = Math.floor(Date.now() / 1000) + 5 * 60
delete found_user.password;
newAuth.access_token = jsonwebtoken.sign({
userdetails: found_user,
exp: timestamp_accesstoken_expiry
}, "securekey")
newAuth.access_token_expires_at = timestamp_accesstoken_expiry
//
const timestamp_refresh_expiry = Math.floor(Date.now() / 1000) + 10 * 36000
newAuth.refresh_token = jsonwebtoken.sign({
refreshtokencount: found_user.refreshTokenCount,
userid: found_user.id,
exp: timestamp_refresh_expiry
}, "securekey")
newAuth.refresh_token_expires_at = timestamp_refresh_expiry
} else {
throw new InvalidCredentialsError()
}
}
return newAuth;
}
}

View File

@ -0,0 +1,35 @@
import { IsString } from 'class-validator';
import * as jsonwebtoken from 'jsonwebtoken';
import { getConnectionManager } from 'typeorm';
import { IllegalJWTError, JwtNotProvidedError, RefreshTokenCountInvalidError, UserNotFoundError } from '../../errors/AuthError';
import { User } from '../entities/User';
import { Logout } from '../responses/Logout';
export class HandleLogout {
@IsString()
token: string;
public async logout(): Promise<Logout> {
let logout: Logout = new Logout();
if (!this.token || this.token === undefined) {
throw new JwtNotProvidedError()
}
let decoded;
try {
decoded = jsonwebtoken.verify(this.token, 'securekey')
} catch (error) {
throw new IllegalJWTError()
}
logout.timestamp = Math.floor(Date.now() / 1000)
let found_user: User = await getConnectionManager().get().getRepository(User).findOne({ id: decoded["userid"] });
if (!found_user) {
throw new UserNotFoundError()
}
if (found_user.refreshTokenCount !== decoded["refreshtokencount"]) {
throw new RefreshTokenCountInvalidError()
}
found_user.refreshTokenCount++;
await getConnectionManager().get().getRepository(User).update({ id: found_user.id }, found_user)
return logout;
}
}

View File

@ -0,0 +1,49 @@
import { IsString } from 'class-validator';
import * as jsonwebtoken from 'jsonwebtoken';
import { getConnectionManager } from 'typeorm';
import { IllegalJWTError, JwtNotProvidedError, RefreshTokenCountInvalidError, UserNotFoundError } from '../../errors/AuthError';
import { User } from '../entities/User';
import { Auth } from '../responses/Auth';
export class RefreshAuth {
@IsString()
token: string;
public async toAuth(): Promise<Auth> {
let newAuth: Auth = new Auth();
if (!this.token || this.token === undefined) {
throw new JwtNotProvidedError()
}
let decoded
try {
decoded = jsonwebtoken.verify(this.token, 'securekey')
} catch (error) {
throw new IllegalJWTError()
}
const found_user = await getConnectionManager().get().getRepository(User).findOne({ id: decoded["userid"] });
if (!found_user) {
throw new UserNotFoundError()
}
if (found_user.refreshTokenCount !== decoded["refreshtokencount"]) {
throw new RefreshTokenCountInvalidError()
}
delete found_user.password;
const timestamp_accesstoken_expiry = Math.floor(Date.now() / 1000) + 5 * 60
delete found_user.password;
newAuth.access_token = jsonwebtoken.sign({
userdetails: found_user,
exp: timestamp_accesstoken_expiry
}, "securekey")
newAuth.access_token_expires_at = timestamp_accesstoken_expiry
//
const timestamp_refresh_expiry = Math.floor(Date.now() / 1000) + 10 * 36000
newAuth.refresh_token = jsonwebtoken.sign({
refreshtokencount: found_user.refreshTokenCount,
userid: found_user.id,
exp: timestamp_refresh_expiry
}, "securekey")
newAuth.refresh_token_expires_at = timestamp_refresh_expiry
return newAuth;
}
}

View File

@ -19,14 +19,14 @@ export class User {
/** /**
* uuid * uuid
*/ */
@Column() @Column({ unique: true })
@IsUUID(4) @IsUUID(4)
uuid: string; uuid: string;
/** /**
* user email * user email
*/ */
@Column({ nullable: true }) @Column({ nullable: true, unique: true })
@IsEmail() @IsEmail()
email?: string; email?: string;
@ -41,7 +41,7 @@ export class User {
/** /**
* username * username
*/ */
@Column({ nullable: true }) @Column({ nullable: true, unique: true })
@IsString() @IsString()
username?: string; username?: string;
@ -109,7 +109,7 @@ export class User {
/** /**
* profilepic * profilepic
*/ */
@Column({ nullable: true }) @Column({ nullable: true, unique: true })
@IsString() @IsString()
@IsOptional() @IsOptional()
profilePic?: string; profilePic?: string;

View File

@ -0,0 +1,27 @@
import { IsInt, IsString } from 'class-validator';
/**
* Defines a auth object
*/
export class Auth {
/**
* access_token - JWT shortterm access token
*/
@IsString()
access_token: string;
/**
* refresh_token - longterm refresh token (used for requesting new access tokens)
*/
@IsString()
refresh_token: string;
/**
* access_token_expires_at - unix timestamp of access token expiry
*/
@IsInt()
access_token_expires_at: number;
/**
* refresh_token_expires_at - unix timestamp of access token expiry
*/
@IsInt()
refresh_token_expires_at: number;
}

View File

@ -0,0 +1,12 @@
import { IsString } from 'class-validator';
/**
* Defines a Logout object
*/
export class Logout {
/**
* timestamp of logout
*/
@IsString()
timestamp: number;
}