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

@ -80,6 +80,18 @@ class App {
if (globals.config.server?.cors) { if (globals.config.server?.cors) {
this.app.use(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) => { this.app.use((req, res, next) => {
logger.verbose(`${req.method} ${req.url}`); logger.verbose(`${req.method} ${req.url}`);
process.send({cmd: "notifyRequest"}); process.send({cmd: "notifyRequest"});

@ -101,6 +101,23 @@ export function resolver(req: any, res: any): any {
return new NotLoggedInGqlError(); 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 }) { async register({username, email, passwordHash}: { username: string, email: string, passwordHash: string }) {
if (username && email && passwordHash) { if (username && email && passwordHash) {
if (!is.email(email)) { if (!is.email(email)) {

@ -25,6 +25,9 @@ type Query {
"returns the post filtered by the sort type with pagination." "returns the post filtered by the sort type with pagination."
getPosts(first: Int=20, offset: Int=0, sort: SortType = NEW): [Post] 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 { type Mutation {
@ -32,7 +35,7 @@ type Mutation {
acceptCookies: Boolean acceptCookies: Boolean
"Login of the user. The passwordHash should be a sha512 hash of the password." "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." "Registers the user."
register(username: String, email: String, passwordHash: String): Profile register(username: String, email: String, passwordHash: String): Profile
@ -374,6 +377,12 @@ type Event {
participants(first: Int=10, offset: Int=0): [User!]! 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" "represents the type of vote performed on a post"
enum VoteType { enum VoteType {
UPVOTE 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 * Registers a user with a username and password returning a user
* @param username * @param username

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

@ -10,6 +10,7 @@ import {
Unique, Unique,
UpdatedAt, UpdatedAt,
} from "sequelize-typescript"; } from "sequelize-typescript";
import * as uuidv4 from "uuid/v4";
import {RequestNotFoundError} from "../errors/RequestNotFoundError"; import {RequestNotFoundError} from "../errors/RequestNotFoundError";
import {UserNotFoundError} from "../errors/UserNotFoundError"; import {UserNotFoundError} from "../errors/UserNotFoundError";
import {ChatMember} from "./ChatMember"; import {ChatMember} from "./ChatMember";
@ -53,6 +54,13 @@ export class User extends Model<User> {
@Column({defaultValue: {}, allowNull: false, type: sqz.JSON}) @Column({defaultValue: {}, allowNull: false, type: sqz.JSON})
public frontendSettings: any; public frontendSettings: any;
@Unique
@Column({defaultValue: uuidv4, unique: true})
public authToken: string;
@Column({defaultValue: () => Date.now() + 7200000})
public authExpire: Date;
@BelongsToMany(() => User, () => Friendship, "userId") @BelongsToMany(() => User, () => Friendship, "userId")
public rFriends: User[]; public rFriends: User[];
@ -130,6 +138,18 @@ export class User extends Model<User> {
return JSON.stringify(this.getDataValue("frontendSettings")); 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 * All friends of the user
* @param first * @param first

@ -232,6 +232,13 @@
dependencies: dependencies:
"@types/node" "*" "@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": "@types/validator@*", "@types/validator@^10.11.3":
version "10.11.3" version "10.11.3"
resolved "https://registry.yarnpkg.com/@types/validator/-/validator-10.11.3.tgz#945799bef24a953c5bc02011ca8ad79331a3ef25" resolved "https://registry.yarnpkg.com/@types/validator/-/validator-10.11.3.tgz#945799bef24a953c5bc02011ca8ad79331a3ef25"

Loading…
Cancel
Save