Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: make login more secure #242

Merged
merged 1 commit into from
Jul 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ DATABASE_URL=postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:$

# Frontend
XD_API_URL=http://${BACKEND_HOST}:${BACKEND_PORT}
SECRET_KEY=SecretKey

# Swagger
# Just the subpath of the URL where the Swagger UI will be available
Expand Down
2 changes: 2 additions & 0 deletions apps/frontend/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,11 @@ RUN pnpm install --frozen-lockfile
FROM base as builder

ARG XD_API_URL
ARG XD_SECRET_KEY

ENV NX_DEAMON="false"
ENV XD_API_URL=$XD_API_URL
ENV XD_SECRET_KEY=$XD_SECRET_KEY

COPY --from=installer /usr/src/app/node_modules ./node_modules

Expand Down
1 change: 1 addition & 0 deletions apps/frontend/src/environments/environment.prod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ import { IAppEnvironment } from 'common-frontend-models';
export const environment: IAppEnvironment = {
production: true,
apiUrl: process.env.XD_API_URL,
secretKey: process.env.XD_SECRET_KEY,
};
1 change: 1 addition & 0 deletions apps/frontend/src/environments/environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ import { IAppEnvironment } from 'common-frontend-models';
export const environment: IAppEnvironment = {
production: false,
apiUrl: process.env.XD_API_URL,
secretKey: process.env.XD_SECRET_KEY,
};
1 change: 1 addition & 0 deletions apps/frontend/src/types/types.d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
declare const process: {
env: {
XD_API_URL: string;
XD_SECRET_KEY: string;
};
};
1 change: 1 addition & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ services:
dockerfile: apps/frontend/Dockerfile
args:
XD_API_URL: localhost:3000
XD_SECRET_KEY: XceleratorDemoAppSecretKey
restart: unless-stopped
ports:
- '4200:80'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,9 @@ export interface IAppEnvironment {
* The url of the main api of the frontend application
*/
apiUrl: string;

/**
* the secretKey for user authentication
*/
secretKey: string;
}
221 changes: 114 additions & 107 deletions libs/common/frontend/models/src/lib/services/authentication.service.ts
Original file line number Diff line number Diff line change
@@ -1,113 +1,120 @@
import { Injectable } from '@angular/core';
import { inject, Injectable } from '@angular/core';
import * as CryptoJS from 'crypto-js';

import { APP_CONFIG } from '../tokens';

@Injectable({
providedIn: 'root',
providedIn: 'root',
})
export class AuthenticationService {

private readonly tokenKey = 'authToken';
private readonly tokenExpirationTime = 1000 * 60 * 60;

// this is a demo app, don't actually store these things in the code
private readonly secretKey = 'XceleratorDemoApp';
private readonly userDataBase = [
{ userMail: '[email protected]', password: 'siemens' },
{ userMail: '[email protected]', password: 'siemens' },
{ userMail: '[email protected]', password: 'siemens' },
{ userMail: '[email protected]', password: 'siemens' },
{ userMail: '[email protected]', password: 'siemens' },
{ userMail: '[email protected]', password: 'siemens' },
{ userMail: '[email protected]', password: 'siemens' },
{ userMail: '[email protected]', password: 'siemens' },
{ userMail: '[email protected]', password: 'siemens' },
{ userMail: '[email protected]', password: 'siemens' },
{ userMail: '[email protected]', password: 'siemens' },
{ userMail: '[email protected]', password: 'siemens' },
{ userMail: '[email protected]', password: 'siemens' },
{ userMail: '[email protected]', password: 'siemens' },
];

login(email: string, password: string) {

if (this.checkCredentials(email, password)) {
const token = this.generateToken(email, password);
localStorage.setItem(this.tokenKey, token);
return true;
}

return false;
}

logout() {
localStorage.removeItem(this.tokenKey);
}

isLoggedIn() {
const token = localStorage.getItem(this.tokenKey);
if (!token) {
return false;
}

const userData = this.decryptToken(token);
if (!userData) {
return false;
}

if (!this.checkCredentials(userData.userMail, userData.password)) {
return false;
}

if (new Date().getTime() - userData.time > this.tokenExpirationTime) {
this.logout();
return false;
}

return true;
}

getUserMail() {
const token = localStorage.getItem(this.tokenKey);
if (!token) {
return false;
}

const userData = this.decryptToken(token);
if (!userData) {
return false;
}

return userData.userMail;
}

private checkCredentials(email: string, password: string) {
return this.userDataBase.some(user => user.userMail === email && user.password === password);
}

private generateToken(email: string, password: string) {
// ':' is not allowed in the email, so we can use it as a separator
const time = new Date().getTime();
const tokenData = `${email}:${password}:${time}`;
return CryptoJS.AES.encrypt(tokenData, this.secretKey).toString();
}

private decryptToken(token: string) {
try {
const bytes = CryptoJS.AES.decrypt(token, this.secretKey);
const decryptedData = bytes.toString(CryptoJS.enc.Utf8);

// the token should have the format email:password:time, where password could contain :
const firstColonIndex = decryptedData.indexOf(':');
const userMail = decryptedData.slice(0, firstColonIndex);

const lastColonIndex = decryptedData.lastIndexOf(':');
const password = decryptedData.slice(firstColonIndex + 1, lastColonIndex);
const time = decryptedData.slice(lastColonIndex + 1);

return { userMail: userMail, password: password, time: parseInt(time) };
} catch (error) {
return null;
}
}

private readonly tokenKey = 'authToken';
private readonly tokenExpirationTime = 1000 * 60 * 60;

private readonly secretKey = inject(APP_CONFIG).secretKey;

// this is a demo app, don't actually store these in the code
private readonly userDataBase = [
'U2FsdGVkX1/9RSRUE4j4qGAQ48gPSr8iLF242snE0hwJmOsluUk/hPzDuXgcgkSGNa8YCrm2GW8lxAxwCCDq1w==',
'U2FsdGVkX1+ifYJJA7x2MVnkOiqx8DywlVyKYOzPM6HPftzUdoGrg8X2c9i2f2ofZKF8I7sKZGFhuJQtVqBeGA==',
'U2FsdGVkX18yC09eQaBPI4LllwUbBAJTYWZoDmBROU4tOaHfB6n9+FCWMOUYGhbVJCV370aVvszr6YaFywC5AQ==',
'U2FsdGVkX19BSG6V31LwzAGRkfYg2lFJ73Y2azUbT2E/qhUXXoqnjzsmBSUuOrJJFapTiYrAiVLzD5aVlAwSyA==',
'U2FsdGVkX198xdVbzi9qI8e7hsQb6R7Tqr+H0TC0rXt/0fHYDzFAYZYvPNYbj9v/9kz9gIDcJ0ltqr0ykYoVfQ==',
'U2FsdGVkX18OT9osxUQklFvha2WUhpKHMABSczBqbZM/aIwOpsOrmP+nLM40iKhQy93uRHYH9cDSRiRKXysYJg==',
'U2FsdGVkX18Q9+qDzKhowDLaeKfC3JYs7bchhCrc1e1XHfme0WnWT8fmrjJYTPhlVcty1uRhxUSplmSqPbHI1g==',
'U2FsdGVkX19jahQtTcRewdjzN9VinNEA5O5GAc42IcuMJ8u9qq2SoPlSZAVQKq0S55a33vFT0gE5KdB6sMw1kA==',
'U2FsdGVkX18z8urywB/i1cCYvUs9QtcmiGIT7fMr8/romPQswAtQT0br6/Q5pyd5dyt9MmiIRzj5/3Hpbe0DEg==',
'U2FsdGVkX19scTGfMHvQ9zQU2DnoQ3+OUHcZneou5CBC3oEiVGOk9j9tSG62qMXoGLV/kEPNRB0os3RrEaQkAK91JwiyJXkIK/OZQibRvnE=',
'U2FsdGVkX19ZUtXbsjHxFCLMsf80K1lEUoavBNrqfrDLlKCqQ8lvvB69/oj3pCOeqS5krHHa2dfp+KtEfwzhYey6WeWBsg02XhnHdPL6uhQ=',
'U2FsdGVkX1+TsxHJS6SCdDRXDrSmcSTLYFUt1eU+vOt3akqoeakFakr2kyxXUloxeCvEGtK54eDyPGssF2o+7xkWxPW+kHKgMdVk7aD7Ma4=',
'U2FsdGVkX1/7vbTIhc5KsLi+Mv81zqX2905hWUa0mlZhkcxiTKcddtBR42SnW3/CmenCGGpQJOFJmyOZtFMCuA==',
'U2FsdGVkX1805OkL41+N6QhaS38fAmqxTPsEUg1j30Y/x6sV9iyfQHYxMYhEtb/B231AfFTzPar9cF6N4AU6UQ==',
];

login(email: string, password: string) {
if (this.checkCredentials(email, password)) {
const token = this.generateToken(email, password);
localStorage.setItem(this.tokenKey, token);
return true;
}

return false;
}

logout() {
localStorage.removeItem(this.tokenKey);
}

isLoggedIn() {
const token = localStorage.getItem(this.tokenKey);
if (!token) {
return false;
}

const userData = this.decryptToken(token);
if (!userData) {
return false;
}

if (!this.checkCredentials(userData.userMail, userData.password)) {
return false;
}

if (new Date().getTime() - userData.time > this.tokenExpirationTime) {
this.logout();
return false;
}

return true;
}

getUserMail() {
const token = localStorage.getItem(this.tokenKey);
if (!token) {
return false;
}

const userData = this.decryptToken(token);
if (!userData) {
return false;
}

return userData.userMail;
}

private checkCredentials(email: string, password: string) {
return this.userDataBase.some((encryptedUser) => {
const user = this.decryptToken(encryptedUser);
if (!user) {
return false;
}

return user.userMail === email && user.password === password;
});
}

private generateToken(email: string, password: string) {
// ':' is not allowed in the email, so we can use it as a separator
const time = new Date().getTime();
const tokenData = `${email}:${password}:${time}`;
return CryptoJS.AES.encrypt(tokenData, this.secretKey).toString();
}

private decryptToken(token: string) {
try {
const bytes = CryptoJS.AES.decrypt(token, this.secretKey);
const decryptedData = bytes.toString(CryptoJS.enc.Utf8);

// the token should have the format email:password:time, where password could contain :
const firstColonIndex = decryptedData.indexOf(':');
const userMail = decryptedData.slice(0, firstColonIndex);

const lastColonIndex = decryptedData.lastIndexOf(':');
const password = decryptedData.slice(firstColonIndex + 1, lastColonIndex);
const time = decryptedData.slice(lastColonIndex + 1);

return { userMail: userMail, password: password, time: parseInt(time) };
} catch (error) {
return null;
}
}
}