Skip to content
This repository has been archived by the owner on Apr 23, 2024. It is now read-only.

Commit

Permalink
implement new auth flow with jwt
Browse files Browse the repository at this point in the history
  • Loading branch information
mgilangjanuar committed Sep 16, 2021
1 parent 7d46c64 commit f77d00d
Show file tree
Hide file tree
Showing 10 changed files with 127 additions and 62 deletions.
2 changes: 1 addition & 1 deletion server/.env-example
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,4 @@ DB_PORT=
DB_USERNAME=
DB_PASS=

JWT_SECRET=
API_JWT_SECRET=
3 changes: 2 additions & 1 deletion server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@
"source-map-support": "^0.5.19",
"telegram": "^1.8.13",
"typeorm": "^0.2.37",
"typeorm-naming-strategies": "^2.0.0"
"typeorm-naming-strategies": "^2.0.0",
"uuid-random": "^1.3.2"
},
"devDependencies": {
"@types/axios": "^0.14.0",
Expand Down
16 changes: 12 additions & 4 deletions server/src/api/middlewares/Auth.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { NextFunction, Request, Response } from 'express'
import { verify } from 'jsonwebtoken'
import { TelegramClient } from 'telegram'
import { StringSession } from 'telegram/sessions'
import { Users } from '../../model/entities/Users'
Expand All @@ -10,22 +11,29 @@ export async function Auth(req: Request, _: Response, next: NextFunction): Promi
throw { status: 401, body: { error: 'Auth key is required' } }
}

let data: { session: string }
try {
const session = new StringSession(authkey)
data = verify(authkey, process.env.API_JWT_SECRET) as { session: string }
} catch (error) {
throw { status: 401, body: { error: 'Access token is invalid' } }
}

try {
const session = new StringSession(data.session)
req.tg = new TelegramClient(session, TG_CREDS.apiId, TG_CREDS.apiHash, { connectionRetries: 5 })
} catch (error) {
throw { status: 401, body: { error: 'Invalid key' } }
}

await req.tg.connect()
const data = await req.tg.getMe()
const userAuth = await req.tg.getMe()

const user = await Users.findOne({ tg_id: data['id'] })
const user = await Users.findOne({ tg_id: userAuth['id'] })
if (!user) {
throw { status: 401, body: { error: 'User not found' } }
}
req.user = user
req.userAuth = data
req.userAuth = userAuth

return next()
}
11 changes: 9 additions & 2 deletions server/src/api/middlewares/TGSessionAuth.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { NextFunction, Request, Response } from 'express'
import { verify } from 'jsonwebtoken'
import { TelegramClient } from 'telegram'
import { StringSession } from 'telegram/sessions'
import { TG_CREDS } from '../../utils/Constant'
Expand All @@ -9,9 +10,15 @@ export async function TGSessionAuth(req: Request, _: Response, next: NextFunctio
throw { status: 401, body: { error: 'Auth key is required' } }
}

console.log('UAHSSA', authkey)
let data: { session: string }
try {
const session = new StringSession(authkey)
data = verify(authkey, process.env.API_JWT_SECRET) as { session: string }
} catch (error) {
throw { status: 401, body: { error: 'Access token is invalid' } }
}

try {
const session = new StringSession(data.session)
req.tg = new TelegramClient(session, TG_CREDS.apiId, TG_CREDS.apiHash, { connectionRetries: 5 })
} catch (error) {
throw { status: 401, body: { error: 'Invalid key' } }
Expand Down
106 changes: 66 additions & 40 deletions server/src/api/v1/Auth.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { Request, Response } from 'express'
import { Api } from 'telegram'
import { sign, verify } from 'jsonwebtoken'
import { Api, TelegramClient } from 'telegram'
import { generateRandomBytes } from 'telegram/Helpers'
import { computeCheck } from 'telegram/Password'
import { StringSession } from 'telegram/sessions'
import { getRepository } from 'typeorm'
import { Users } from '../../model//entities/Users'
import { Waitings } from '../../model/entities/Waitings'
import { COOKIE_AGE, TG_CREDS } from '../../utils/Constant'
Expand Down Expand Up @@ -35,8 +38,9 @@ export class Auth {
})
}))
const session = req.tg.session.save()
return res.cookie('authorization', `Bearer ${session}`)
.send({ phoneCodeHash, accessToken: session })
const accessToken = sign({ session }, process.env.API_JWT_SECRET, { expiresIn: '3h' })
return res.cookie('authorization', `Bearer ${accessToken}`)
.send({ phoneCodeHash, accessToken })
}

@Endpoint.POST({ middlewares: [TGSessionAuth] })
Expand All @@ -55,14 +59,18 @@ export class Auth {
const { phoneCodeHash: newPhoneCodeHash } = await req.tg.invoke(new Api.auth.ResendCode({
phoneNumber, phoneCodeHash }))
const session = req.tg.session.save()
return res.cookie('authorization', `Bearer ${session}`)
.send({ phoneCodeHash: newPhoneCodeHash, accessToken: session })
const accessToken = sign({ session }, process.env.API_JWT_SECRET, { expiresIn: '3h' })
return res.cookie('authorization', `Bearer ${accessToken}`)
.send({ phoneCodeHash: newPhoneCodeHash, accessToken })
}

@Endpoint.POST({ middlewares: [TGSessionAuth] })
public async login(req: Request, res: Response): Promise<any> {
const { token: id, phoneNumber, phoneCode, phoneCodeHash } = req.body
if (!id || !phoneNumber || !phoneCode || !phoneCodeHash) {
const { token: id, phoneNumber, phoneCode, phoneCodeHash, password } = req.body
if ((!id || (!phoneNumber || !phoneCode || !phoneCodeHash)) && !password) {
if (!password) {
throw { status: 400, body: { error: 'Token and password are required' } }
}
throw { status: 400, body: { error: 'Token, phone number, phone code, and phone code hash are required' } }
}

Expand All @@ -72,54 +80,72 @@ export class Auth {
}

await req.tg.connect()
const signIn = await req.tg.invoke(new Api.auth.SignIn({ phoneNumber, phoneCode, phoneCodeHash }))
const user = signIn['user']
if (!await Users.findOne({ tg_id: user.id })) {
const username = user.username || user.phone || phoneNumber
await Users.insert([{
let signIn: any
if (password) {
const data = await req.tg.invoke(new Api.account.GetPassword())
data.newAlgo['salt1'] = Buffer.concat([data.newAlgo['salt1'], generateRandomBytes(32)])
signIn = await req.tg.invoke(new Api.auth.CheckPassword({ password: await computeCheck(data, password) }))
} else {
signIn = await req.tg.invoke(new Api.auth.SignIn({ phoneNumber, phoneCode, phoneCodeHash }))
}
const userAuth = signIn['user']
let user = await Users.findOne({ tg_id: userAuth.id })
if (!user) {
const username = userAuth.username || userAuth.phone || phoneNumber
user = await getRepository<Users>(Users).save({
username,
name: `${user.firstName || ''} ${user.lastName || ''}`.trim() || username,
name: `${userAuth.firstName || ''} ${userAuth.lastName || ''}`.trim() || username,
email: waiting.email,
tg_id: user.id
}])
tg_id: userAuth.id
}, { reload: true })
}

const session = req.tg.session.save()
return res.cookie('authorization', `Bearer ${session}`, { expires: new Date(Date.now() + COOKIE_AGE) })
.send({ user, accessToken: session })
const auth = {
accessToken: sign({ session }, process.env.API_JWT_SECRET, { expiresIn: '15h' }),
refreshToken: sign({ session }, process.env.API_JWT_SECRET, { expiresIn: '100y' }),
expiredAfter: Date.now() + COOKIE_AGE
}
return res.cookie('authorization', `Bearer ${auth.accessToken}`, { maxAge: COOKIE_AGE, expires: new Date(auth.expiredAfter) })
.send({ user, ...auth })
}

@Endpoint.POST({ middlewares: [TGSessionAuth] })
public async checkPassword(req: Request, res: Response): Promise<any> {
const { token: id, password } = req.body
if (!id || !password) {
throw { status: 400, body: { error: 'Token and password are required' } }
@Endpoint.POST()
public async refreshToken(req: Request, res: Response): Promise<any> {
const { refreshToken } = req.body
if (!refreshToken) {
throw { status: 400, body: { error: 'Refresh token is required' } }
}

const waiting = await Waitings.findOne({ id })
if (!waiting) {
throw { status: 400, body: { error: 'Invalid token' } }
let data: { session: string }
try {
data = verify(refreshToken, process.env.API_JWT_SECRET) as { session: string }
} catch (error) {
throw { status: 401, body: { error: 'Refresh token is invalid' } }
}

try {
const session = new StringSession(data.session)
req.tg = new TelegramClient(session, TG_CREDS.apiId, TG_CREDS.apiHash, { connectionRetries: 5 })
} catch (error) {
throw { status: 401, body: { error: 'Invalid key' } }
}

await req.tg.connect()
const data = await req.tg.invoke(new Api.account.GetPassword())
data.newAlgo['salt1'] = Buffer.concat([data.newAlgo['salt1'], generateRandomBytes(32)])
const pass = await computeCheck(data, password)
const signIn = await req.tg.invoke(new Api.auth.CheckPassword({ password: pass }))
const user = signIn['user']
if (!await Users.findOne({ tg_id: user.id })) {
const username = user.username || user.phone
await Users.insert([{
username,
name: `${user.firstName || ''} ${user.lastName || ''}`.trim() || username,
email: waiting.email,
tg_id: user.id
}])
const userAuth = await req.tg.getMe()
const user = await Users.findOne({ tg_id: userAuth['id'] })
if (!user) {
throw { status: 401, body: { error: 'User not found' } }
}

const session = req.tg.session.save()
return res.cookie('authorization', `Bearer ${session}`, { expires: new Date(Date.now() + COOKIE_AGE) })
.send({ user, accessToken: session })
const auth = {
accessToken: sign({ session }, process.env.API_JWT_SECRET, { expiresIn: '15h' }),
refreshToken: sign({ session }, process.env.API_JWT_SECRET, { expiresIn: '100y' }),
expiredAfter: Date.now() + COOKIE_AGE
}
return res.cookie('authorization', `Bearer ${auth.accessToken}`, { maxAge: COOKIE_AGE, expires: new Date(auth.expiredAfter) })
.send({ user, ...auth })
}

/**
Expand Down
3 changes: 2 additions & 1 deletion server/src/utils/Constant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ export const TG_CREDS = {
apiHash: process.env.TG_API_HASH
}

export const COOKIE_AGE = 3.154e+12
// export const COOKIE_AGE = 3.154e+12
export const COOKIE_AGE = 54e6
27 changes: 23 additions & 4 deletions server/yarn-error.log
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
Arguments:
/Users/mgilangjanuar/.nvm/versions/node/v14.17.2/bin/node /Users/mgilangjanuar/.yarn/lib/cli.js add @types/crypto -D
/Users/mgilangjanuar/.nvm/versions/node/v14.17.2/bin/node /Users/mgilangjanuar/.yarn/lib/cli.js add @types/uuid-random -D

PATH:
/var/folders/fd/_46vmhcd68qcp7mh4h7r44lh0000gn/T/yarn--1631722225997-0.842781836296006:/Users/mgilangjanuar/Documents/workspace/teledrive/node_modules/.bin:/Users/mgilangjanuar/.config/yarn/link/node_modules/.bin:/Users/mgilangjanuar/Documents/workspace/teledrive/node_modules/.bin:/Users/mgilangjanuar/.nvm/versions/node/v14.17.2/libexec/lib/node_modules/npm/bin/node-gyp-bin:/Users/mgilangjanuar/.nvm/versions/node/v14.17.2/lib/node_modules/npm/bin/node-gyp-bin:/Users/mgilangjanuar/.nvm/versions/node/v14.17.2/bin/node_modules/npm/bin/node-gyp-bin:/Users/mgilangjanuar/.rvm/gems/ruby-2.6.5/bin:/Users/mgilangjanuar/.rvm/gems/ruby-2.6.5@global/bin:/Users/mgilangjanuar/.rvm/rubies/ruby-2.6.5/bin:/Users/mgilangjanuar/.deta/bin:/Users/mgilangjanuar/.cargo/bin:/Users/mgilangjanuar/Downloads/google-cloud-sdk/bin:/Users/mgilangjanuar/.yarn/bin:/Users/mgilangjanuar/.config/yarn/global/node_modules/.bin:/Users/mgilangjanuar/.pyenv/shims:/Users/mgilangjanuar/.pyenv/bin:/Users/mgilangjanuar/Documents/apps/elasticsearch-7.6.1/bin:/usr/local/sbin:/usr/local/opt/gettext/bin:/Users/mgilangjanuar/go/bin:usr/local/sbin:/Users/mgilangjanuar/.nvm/versions/node/v14.17.2/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/Library/Apple/usr/bin:/Library/Frameworks/Mono.framework/Versions/Current/Commands:/Users/mgilangjanuar/.cargo/bin:/Users/mgilangjanuar/.rvm/bin
/var/folders/fd/_46vmhcd68qcp7mh4h7r44lh0000gn/T/yarn--1631806576911-0.20831223035563462:/Users/mgilangjanuar/Documents/workspace/teledrive/node_modules/.bin:/Users/mgilangjanuar/.config/yarn/link/node_modules/.bin:/Users/mgilangjanuar/Documents/workspace/teledrive/node_modules/.bin:/Users/mgilangjanuar/.nvm/versions/node/v14.17.2/libexec/lib/node_modules/npm/bin/node-gyp-bin:/Users/mgilangjanuar/.nvm/versions/node/v14.17.2/lib/node_modules/npm/bin/node-gyp-bin:/Users/mgilangjanuar/.nvm/versions/node/v14.17.2/bin/node_modules/npm/bin/node-gyp-bin:/Users/mgilangjanuar/.rvm/gems/ruby-2.6.5/bin:/Users/mgilangjanuar/.rvm/gems/ruby-2.6.5@global/bin:/Users/mgilangjanuar/.rvm/rubies/ruby-2.6.5/bin:/Users/mgilangjanuar/.deta/bin:/Users/mgilangjanuar/.cargo/bin:/Users/mgilangjanuar/Downloads/google-cloud-sdk/bin:/Users/mgilangjanuar/.yarn/bin:/Users/mgilangjanuar/.config/yarn/global/node_modules/.bin:/Users/mgilangjanuar/.pyenv/shims:/Users/mgilangjanuar/.pyenv/bin:/Users/mgilangjanuar/Documents/apps/elasticsearch-7.6.1/bin:/usr/local/sbin:/usr/local/opt/gettext/bin:/Users/mgilangjanuar/go/bin:usr/local/sbin:/Users/mgilangjanuar/.nvm/versions/node/v14.17.2/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/Library/Apple/usr/bin:/Library/Frameworks/Mono.framework/Versions/Current/Commands:/Users/mgilangjanuar/.cargo/bin:/Users/mgilangjanuar/.rvm/bin

Yarn version:
1.22.5
Expand All @@ -14,7 +14,7 @@ Platform:
darwin x64

Trace:
Error: https://registry.yarnpkg.com/@types%2fcrypto: Not found
Error: https://registry.yarnpkg.com/@types%2fuuid-random: Not found
at Request.params.callback [as _callback] (/Users/mgilangjanuar/.yarn/lib/cli.js:66988:18)
at Request.self.callback (/Users/mgilangjanuar/.yarn/lib/cli.js:140749:22)
at Request.emit (events.js:375:28)
Expand Down Expand Up @@ -56,7 +56,8 @@ npm manifest:
"source-map-support": "^0.5.19",
"telegram": "^1.8.13",
"typeorm": "^0.2.37",
"typeorm-naming-strategies": "^2.0.0"
"typeorm-naming-strategies": "^2.0.0",
"uuid-random": "^1.3.2"
},
"devDependencies": {
"@types/axios": "^0.14.0",
Expand Down Expand Up @@ -10883,6 +10884,11 @@ Lockfile:
resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7"
integrity sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=

qr.js@0.0.0:
version "0.0.0"
resolved "https://registry.yarnpkg.com/qr.js/-/qr.js-0.0.0.tgz#cace86386f59a0db8050fa90d9b6b0e88a1e364f"
integrity sha1-ys6GOG9ZoNuAUPqQ2baw6IoeNk8=

qs@6.7.0:
version "6.7.0"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc"
Expand Down Expand Up @@ -11404,6 +11410,14 @@ Lockfile:
unist-util-visit "^4.0.0"
vfile "^5.0.0"

react-qr-code@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/react-qr-code/-/react-qr-code-2.0.2.tgz#64107c869079aceb897c97496d163720ab2820e8"
integrity sha512-73VGe81MgeE5FJNFgdY42ez/wPPJTHuooU3iE4CX+6F8M88O1Gg4zNA0L4bKEpoySQ0QjqreJgyXjFrG/QfsdA==
dependencies:
prop-types "^15.7.2"
qr.js "0.0.0"

react-refresh@^0.8.3:
version "0.8.3"
resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.8.3.tgz#721d4657672d400c5e3c75d063c4a85fb2d5d68f"
Expand Down Expand Up @@ -13705,6 +13719,11 @@ Lockfile:
resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"
integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=

uuid-random@^1.3.2:
version "1.3.2"
resolved "https://registry.yarnpkg.com/uuid-random/-/uuid-random-1.3.2.tgz#96715edbaef4e84b1dcf5024b00d16f30220e2d0"
integrity sha512-UOzej0Le/UgkbWEO8flm+0y+G+ljUon1QWTEZOq1rnMAsxo2+SckbiZdKzAHHlVh6gJqI1TjC/xwgR50MuCrBQ==

uuid@^3.3.2, uuid@^3.4.0:
version "3.4.0"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee"
Expand Down
5 changes: 3 additions & 2 deletions web/src/pages/Login.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,9 @@ const Login: React.FC = () => {
const token = localStorage.getItem('invitationCode')
const { phoneNumber, phoneCode, password } = formLogin.getFieldsValue()
try {
const { data } = needPassword ? await req.post('/auth/checkPassword', { token, password }) : await req.post('/auth/login', { token, phoneNumber, phoneCode, phoneCodeHash })
const { data } = await req.post('/auth/login', { token, ...needPassword ? { password } : { phoneNumber, phoneCode, phoneCodeHash } })
setLoadingLogin(false)
localStorage.setItem('refreshToken', data.refreshToken)
message.success(`Welcome back, ${data.user.username}!`)
return history.replace('/dashboard')
} catch (error: any) {
Expand Down Expand Up @@ -100,7 +101,7 @@ const Login: React.FC = () => {
<Input.Search placeholder="6289123456789" type="tel" enterButton={countdown ? `Re-send in ${countdown}s...` : phoneCodeHash ? 'Re-send' : 'Send'} loading={loadingSendCode} onSearch={sendCode} />
</Form.Item>
<Form.Item label="Code" name="phoneCode" rules={[{ required: true, message: 'Please input your code' }]}>
<Input disabled={!phoneCodeHash} />
<Input disabled={!phoneCodeHash || needPassword} />
</Form.Item>
<Form.Item label="Password 2FA" name="password" hidden={!needPassword}>
<Input.Password />
Expand Down
11 changes: 4 additions & 7 deletions web/src/pages/components/Navbar.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { LoginOutlined, LogoutOutlined } from '@ant-design/icons'
import { Avatar, Button, Form, Input, Layout, Menu, Modal, Typography } from 'antd'
import { useForm } from 'antd/lib/form/Form'
import React, { useEffect, useState } from 'react'
import JSCookie from 'js-cookie'
import React, { useEffect, useState } from 'react'
import { useHistory } from 'react-router'
import { Link } from 'react-router-dom'
import { apiUrl, req } from '../../utils/Fetcher'
Expand All @@ -20,7 +20,7 @@ const Navbar: React.FC<Props> = ({ user }) => {
const logout = async () => {
await req.post('/auth/logout')
JSCookie.remove('authorization')
history.replace('/')
location.replace('/')
}

const saveInvitationCode = () => {
Expand Down Expand Up @@ -51,11 +51,8 @@ const Navbar: React.FC<Props> = ({ user }) => {
</Menu> : <Button onClick={() => setWantToLogin(true)} size="large" type="link" style={{ color: '#ffff', float: 'right', top: '11px' }} icon={<LoginOutlined />}>Login</Button>}
</Layout.Header>
<Modal visible={wantToLogin} title="Invitation Code" onCancel={() => setWantToLogin(false)} onOk={form.submit} okText="Continue">
<Typography.Paragraph>
The access is limited for early users<br />
<Typography.Text type="secondary">
Please <a href="/">join</a> the waiting list first and always check your inbox.
</Typography.Text>
<Typography.Paragraph type="secondary">
The access is limited for early users.
</Typography.Paragraph>
<Form form={form} onFinish={saveInvitationCode}>
<Form.Item label="Code" name="code" rules={[{ required: true, message: 'Please input your invitation code' }]}>
Expand Down
5 changes: 5 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -13628,6 +13628,11 @@ utils-merge@1.0.1:
resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"
integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=

uuid-random@^1.3.2:
version "1.3.2"
resolved "https://registry.yarnpkg.com/uuid-random/-/uuid-random-1.3.2.tgz#96715edbaef4e84b1dcf5024b00d16f30220e2d0"
integrity sha512-UOzej0Le/UgkbWEO8flm+0y+G+ljUon1QWTEZOq1rnMAsxo2+SckbiZdKzAHHlVh6gJqI1TjC/xwgR50MuCrBQ==

uuid@^3.3.2, uuid@^3.4.0:
version "3.4.0"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee"
Expand Down

0 comments on commit f77d00d

Please sign in to comment.