Added Bearer authentification

- added the getToken method to the graphql query type
- added the graphql Token type
- added database fields for the token and the expire date
- added a way to authorize as a user via the Authorization HTTP header
pull/5/head
Trivernis 5 years ago
parent 6e5eab84a1
commit 126f53dbbf

@ -54,6 +54,7 @@
"typescript": "^3.7.2"
},
"dependencies": {
"@types/uuid": "^3.4.6",
"compression": "^1.7.4",
"connect-session-sequelize": "^6.0.0",
"cookie-parser": "^1.4.4",
@ -77,6 +78,7 @@
"socket.io": "^2.2.0",
"socket.io-redis": "^5.2.0",
"sqlite3": "^4.1.0",
"uuid": "^3.3.3",
"winston": "^3.2.1",
"winston-daily-rotate-file": "^4.2.1"
}

@ -80,6 +80,18 @@ class App {
if (globals.config.server?.cors) {
this.app.use(cors());
}
// handle authentification via bearer in the Authorization header
this.app.use(async (req, res, next) => {
if (!req.session.userId && req.headers.authorization) {
const bearer = req.headers.authorization.split("Bearer ")[1];
if (bearer) {
const user = await dataaccess.getUserByToken(bearer);
// @ts-ignore
req.session.userId = user.id;
}
}
next();
});
this.app.use((req, res, next) => {
logger.verbose(`${req.method} ${req.url}`);
process.send({cmd: "notifyRequest"});

@ -101,6 +101,23 @@ export function resolver(req: any, res: any): any {
return new NotLoggedInGqlError();
}
},
async getToken({email, passwordHash}: {email: string, passwordHash: string}) {
if (email && passwordHash) {
try {
const user = await dataaccess.getUserByLogin(email, passwordHash);
return {
expires: Number(user.authExpire),
value: user.token(),
};
} catch (err) {
res.status(400);
return err.graphqlError;
}
} else {
res.status(400);
return new GraphQLError("No email or password specified.");
}
},
async register({username, email, passwordHash}: { username: string, email: string, passwordHash: string }) {
if (username && email && passwordHash) {
if (!is.email(email)) {

@ -25,6 +25,9 @@ type Query {
"returns the post filtered by the sort type with pagination."
getPosts(first: Int=20, offset: Int=0, sort: SortType = NEW): [Post]
"Returns an access token for the user that can be used in requests. To user the token in requests, it has to be set in the HTTP header 'Authorization' with the format Bearer <token>."
getToken(email: String!, passwordHash: String!): Token!
}
type Mutation {
@ -32,7 +35,7 @@ type Mutation {
acceptCookies: Boolean
"Login of the user. The passwordHash should be a sha512 hash of the password."
login(email: String, passwordHash: String): Profile
login(email: String!, passwordHash: String!): Profile
"Registers the user."
register(username: String, email: String, passwordHash: String): Profile
@ -374,6 +377,12 @@ type Event {
participants(first: Int=10, offset: Int=0): [User!]!
}
"respresents an access token entry with the value as the acutal token and expires as the date the token expires."
type Token {
value: String!
expires: String!
}
"represents the type of vote performed on a post"
enum VoteType {
UPVOTE

@ -93,6 +93,14 @@ namespace dataaccess {
}
}
/**
* Returns the user by auth token.
* @param token
*/
export async function getUserByToken(token: string): Promise<models.User> {
return models.User.findOne({where: {authToken: token}});
}
/**
* Registers a user with a username and password returning a user
* @param username

@ -24,22 +24,37 @@ export class Post extends Model<Post> {
@CreatedAt
public readonly createdAt!: Date;
/**
* Returns the author of a post
*/
public async author(): Promise<User> {
return await this.$get("rAuthor") as User;
}
/**
* Returns the votes on a post
*/
public async votes(): Promise<Array<User & {PostVote: PostVote}>> {
return await this.$get("rVotes") as Array<User & {PostVote: PostVote}>;
}
/**
* Returns the markdown-rendered html content of the post
*/
public get htmlContent() {
return markdown.render(this.getDataValue("content"));
}
/**
* Returns the number of upvotes on the post
*/
public async upvotes() {
return (await this.votes()).filter((v) => v.PostVote.voteType === VoteType.UPVOTE).length;
}
/**
* Returns the number of downvotes on the post
*/
public async downvotes() {
return (await this.votes()).filter((v) => v.PostVote.voteType === VoteType.DOWNVOTE).length;
}

@ -10,6 +10,7 @@ import {
Unique,
UpdatedAt,
} from "sequelize-typescript";
import * as uuidv4 from "uuid/v4";
import {RequestNotFoundError} from "../errors/RequestNotFoundError";
import {UserNotFoundError} from "../errors/UserNotFoundError";
import {ChatMember} from "./ChatMember";
@ -53,6 +54,13 @@ export class User extends Model<User> {
@Column({defaultValue: {}, allowNull: false, type: sqz.JSON})
public frontendSettings: any;
@Unique
@Column({defaultValue: uuidv4, unique: true})
public authToken: string;
@Column({defaultValue: () => Date.now() + 7200000})
public authExpire: Date;
@BelongsToMany(() => User, () => Friendship, "userId")
public rFriends: User[];
@ -130,6 +138,18 @@ export class User extends Model<User> {
return JSON.stringify(this.getDataValue("frontendSettings"));
}
/**
* Returns the token for the user that can be used as a bearer in requests
*/
public async token(): Promise<string> {
if (this.getDataValue("authExpire") < new Date(Date.now())) {
this.authToken = null;
this.authExpire = null;
await this.save();
}
return this.getDataValue("authToken");
}
/**
* All friends of the user
* @param first

@ -232,6 +232,13 @@
dependencies:
"@types/node" "*"
"@types/uuid@^3.4.6":
version "3.4.6"
resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-3.4.6.tgz#d2c4c48eb85a757bf2927f75f939942d521e3016"
integrity sha512-cCdlC/1kGEZdEglzOieLDYBxHsvEOIg7kp/2FYyVR9Pxakq+Qf/inL3RKQ+PA8gOlI/NnL+fXmQH12nwcGzsHw==
dependencies:
"@types/node" "*"
"@types/validator@*", "@types/validator@^10.11.3":
version "10.11.3"
resolved "https://registry.yarnpkg.com/@types/validator/-/validator-10.11.3.tgz#945799bef24a953c5bc02011ca8ad79331a3ef25"

Loading…
Cancel
Save