Skip to content

Commit

Permalink
Fix new login flow (#930)
Browse files Browse the repository at this point in the history
* Revert "Revert/new login 2 (#924)"

This reverts commit 1649e2e.

* [deps] Replace get-port with detect-port

Signed-off-by: Tiago Martins Nápoli <napoli.tiago96@gmail.com>

* Fix free port retrieval

Signed-off-by: Tiago Martins Nápoli <napoli.tiago96@gmail.com>

* Fix WSL2 login

Signed-off-by: tiagonapoli <napoli.tiago96@gmail.com>

* changelog

Signed-off-by: Tiago Martins Nápoli <napoli.tiago96@gmail.com>

* Add WSL issue warning

Signed-off-by: tiagonapoli <napoli.tiago96@gmail.com>
Signed-off-by: Tiago Martins Nápoli <napoli.tiago96@gmail.com>

* Update src/lib/auth/AuthProviders/OAuthAuthenticator/index.ts

Co-authored-by: Larícia Mota <lmmc2@cin.ufpe.br>

Co-authored-by: Larícia Mota <lmmc2@cin.ufpe.br>
  • Loading branch information
tiagonapoli and lariciamota committed Aug 12, 2020
1 parent 59ce5fe commit f4cd57f
Show file tree
Hide file tree
Showing 11 changed files with 619 additions and 110 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Expand Up @@ -6,6 +6,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Changed
- [vtex login] Use new VTEX ID login flow:
- Fix free TCP port retrieval: use `detect-port` package instead of `get-port`.
- Fix issue on WSL2 by specifying host binding to `127.0.0.1` when starting the server.

## [2.108.0] - 2020-08-11
### Added
- [vtex workspace promote] Conflict handling.
Expand Down
8 changes: 6 additions & 2 deletions package.json
Expand Up @@ -5,8 +5,7 @@
"bin": "bin/run",
"main": "build/api/index.js",
"scripts": {
"exe": "node scripts/make-executable.js",
"watch": "bash ./scripts/symlink.sh && yarn nodemon && yarn oclif-dev manifest",
"watch": "yarn build-clean && bash ./scripts/symlink.sh && yarn nodemon",
"format": "prettier --config ./.prettierrc --write \"./src/**/*.{ts,tsx,js,jsx,json}\"",
"lint:node": "yarn eslint ./src --cache --ext ts --config .eslintrc",
"format-lint": "yarn format && yarn lint:node",
Expand Down Expand Up @@ -75,9 +74,11 @@
"cli-table": "~0.3.1",
"cli-table2": "~0.2.0",
"clipboardy": "~2.1.0",
"co-body": "^6.0.0",
"configstore": "^5.0.1",
"csvtojson": "~2.0.10",
"debounce": "~1.2.0",
"detect-port": "^1.3.0",
"diff": "~3.5.0",
"enquirer": "~2.3.2",
"eventsource": "~1.0.7",
Expand All @@ -90,6 +91,7 @@
"is-wsl": "^2.1.1",
"js-yaml": "~3.13.1",
"jsonwebtoken": "~8.5.1",
"koa": "^2.13.0",
"latest-version": "^4.0.0",
"moment": "~2.24.0",
"node-notifier": "^6.0.0",
Expand Down Expand Up @@ -123,8 +125,10 @@
"devDependencies": {
"@oclif/dev-cli": "^1",
"@types/async-retry": "1.4.1",
"@types/co-body": "^5.1.0",
"@types/configstore": "^4.0.0",
"@types/debounce": "^1.2.0",
"@types/detect-port": "^1.3.0",
"@types/eventsource": "^1.1.2",
"@types/fs-extra": "5.0.4",
"@types/jest": "24.0.23",
Expand Down
34 changes: 34 additions & 0 deletions src/api/clients/IOClients/external/VTEXID.ts
@@ -1,5 +1,6 @@
import { InstanceOptions, IOClient, IOContext } from '@vtex/api'
import opn from 'opn'
import querystring from 'querystring'
import { storeUrl } from '../../../storeUrl'
import { IOClientFactory } from '../IOClientFactory'

Expand All @@ -8,6 +9,7 @@ export class VTEXID extends IOClient {
private static readonly DEFAULT_RETRIES = 2
private static readonly BASE_URL = 'https://vtexid.vtex.com.br'
private static readonly API_PATH_PREFIX = '/api/vtexid'
private static readonly TOOLBELT_API_PATH_PREFIX = `${VTEXID.API_PATH_PREFIX}/toolbelt`
private static readonly VTEX_ID_AUTH_COOKIE = 'VtexIdClientAutCookie'

public static createClient(customContext: Partial<IOContext> = {}, customOptions: Partial<InstanceOptions> = {}) {
Expand All @@ -30,6 +32,25 @@ export class VTEXID extends IOClient {
})
}

public startToolbeltLogin({ account, secretHash, loopbackUrl }: StartToolbeltLoginInput) {
const body = querystring.stringify({
secretHash,
loopbackUrl,
})

return this.http.post<string>(`${VTEXID.TOOLBELT_API_PATH_PREFIX}/start?an=${account}`, body)
}

public validateToolbeltLogin({ account, state, secret, ott }: ValidateToolbeltLoginInput) {
const body = querystring.stringify({
state,
secret,
ott,
})

return this.http.post<{ token: string }>(`${VTEXID.TOOLBELT_API_PATH_PREFIX}/validate?an=${account}`, body)
}

public invalidateToolbeltToken(token: string) {
return this.http.get(`/api/vtexid/pub/logout?scope=`, {
headers: {
Expand All @@ -38,3 +59,16 @@ export class VTEXID extends IOClient {
})
}
}

interface StartToolbeltLoginInput {
account: string
secretHash: string
loopbackUrl: string
}

interface ValidateToolbeltLoginInput {
account: string
state: string
secret: string
ott: string
}
1 change: 1 addition & 0 deletions src/api/error/ErrorKinds.ts
Expand Up @@ -16,4 +16,5 @@ export const ErrorKinds = {
APP_LOGS_PARSE_ERROR: 'LogsParseError',
STICKY_HOST_ERROR: 'StickyHostError',
FLOW_ISSUE_ERROR: 'FlowIssue',
LOGIN_SERVER_START_ERROR: 'LoginServerStartError',
}
64 changes: 0 additions & 64 deletions src/lib/auth/AuthProviders/OAuthAuthenticator.ts

This file was deleted.

185 changes: 185 additions & 0 deletions src/lib/auth/AuthProviders/OAuthAuthenticator/LoginServer.ts
@@ -0,0 +1,185 @@
import asyncRetry from 'async-retry'
import coBody from 'co-body'
import detectPort from 'detect-port'
import { Server } from 'http'
import Koa from 'koa'
import { ErrorKinds, logger } from '../../../../api'
import { VTEXID } from '../../../../api/clients/IOClients/external/VTEXID'
import { ErrorReport } from '../../../../api/error/ErrorReport'

const SUCCESS_PAGE = `
<!doctype html>
<html>
<head>
<title>Success</title>
<meta charset="utf-8">
</head>
<body>
<p> Você já pode fechar essa janela. </p>
<p> You may now close this window. </p>
<p> Ahora puedes cerrar esta ventana. </p>
</body>
</html>`

export class LoginServer {
private static readonly LOGIN_CALLBACK_PATH = '/login_callback'
private static readonly SERVER_START_RETRIES = 1
private static readonly HOSTNAME = '127.0.0.1'

public static async create(loginConfig: LoginConfig) {
const loginServer = new LoginServer(loginConfig)
await loginServer.start()
return loginServer
}

private app: Koa
private port: number
private server: Server

private loginState?: string

private tokenPromise: Promise<string>
private resolveTokenPromise: (val: string) => void
private rejectTokenPromise: (err: any) => void

constructor(private loginConfig: LoginConfig) {
this.app = new Koa()
this.registerLoginHandler()

this.tokenPromise = new Promise((resolve, reject) => {
this.resolveTokenPromise = resolve
this.rejectTokenPromise = reject
})
}

get token() {
return this.tokenPromise
}

get loginCallbackUrl() {
if (!this.port) {
throw new Error('LoginServer not initialized')
}

return `http://${LoginServer.HOSTNAME}:${this.port}${LoginServer.LOGIN_CALLBACK_PATH}`
}

public setLoginState(val: string) {
this.loginState = val
}

public start() {
return asyncRetry(
async (bail, attemptNumber) => {
try {
// detectPort will get the specified port or, if it's in use, another ramdom unnused port
this.port = await detectPort(3000)
this.server = await this.initServer(this.port)
logger.debug(`LoginServer started on http://${LoginServer.HOSTNAME}:${this.port}`)
} catch (err) {
logger.debug(`LoginServer failed to start on port:${this.port}. Reason: ${err.message}.`)
if (err.code !== 'EADDRINUSE') {
return bail(err)
}

if (attemptNumber < LoginServer.SERVER_START_RETRIES + 1) {
logger.debug(`Retrying to start LoginServer...`)
}

throw ErrorReport.createAndMaybeRegisterOnTelemetry({
originalError: err,
kind: ErrorKinds.LOGIN_SERVER_START_ERROR,
details: { attemptNumber },
})
}
},
{ retries: LoginServer.SERVER_START_RETRIES, maxTimeout: 100, minTimeout: 100 }
)
}

public close() {
this.server.unref()
this.server.close()
}

private initServer(port: number): Promise<Server> {
return new Promise((resolve, reject) => {
const server = this.app.listen(port, LoginServer.HOSTNAME, () => {
server.on('connection', socket => {
socket.unref()
})

server.removeListener('error', reject)
resolve(server)
})

server.on('error', reject)
})
}

private registerLoginHandler() {
this.app.use(async ctx => {
ctx.set('connection', 'close')

if (ctx.path !== LoginServer.LOGIN_CALLBACK_PATH) {
ctx.status = 404
ctx.body = 'Not found'
return
}

ctx.socket.ref()
logger.debug(`Received ${ctx.method} login callback`)
if (!this.loginState) {
return this.handleError(ctx, new Error('Received login callback before setting login state'))
}

let body
if (ctx.method.toLowerCase() === 'post') {
try {
body = await coBody(ctx.req)
} catch (err) {
return this.handleError(ctx, err)
}
} else {
body = { ott: ctx.query?.ott }
}

if (!body.ott) {
return this.handleError(ctx, new Error('Missing ott on VTEX ID callback call'), { body })
}

const vtexId = VTEXID.createClient({ account: this.loginConfig.account })
try {
const { token } = await vtexId.validateToolbeltLogin({
account: this.loginConfig.account,
secret: this.loginConfig.secret,
ott: body.ott,
state: this.loginState,
})

this.resolveTokenPromise(token)
ctx.status = 200
ctx.set('content-type', 'text/html')
ctx.body = SUCCESS_PAGE
} catch (err) {
return this.handleError(ctx, err)
}
})
}

private handleError(ctx: Koa.ParameterizedContext, err: any, details?: Record<string, any>) {
const errReport = ErrorReport.createAndMaybeRegisterOnTelemetry({ originalError: err, details })
ctx.status = 500
ctx.body = {
errorId: errReport.metadata.errorId,
message: errReport.message,
}

this.rejectTokenPromise(errReport)
}
}

interface LoginConfig {
account: string
secret: string
}

0 comments on commit f4cd57f

Please sign in to comment.